Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
57 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Brokered secrets work on self-hosted runners whose jobs run on Linux or macOS, and the value still never reaches the runner's computer. The job keeps its stand-in; a harness that can broker says so when it fetches its spec (`broker`), and the spec then lists each stand-in's hosts (`brokered`), never a value. The harness runs a proxy for the job on 127.0.0.1 and gives the steps HTTPS_PROXY, NODE_USE_ENV_PROXY and the trusted-certificate variables g1t's own sandboxes set (SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, PIP_CERT, GIT_SSL_CAINFO, CARGO_HTTP_CAINFO), each pointing at what it pointed at before with the job's certificate added. HTTPS to any other host is a plain tunnel. HTTPS to a brokered host is opened with a certificate from an authority made for that job alone, name-constrained to the brokered hosts and with its key only in memory; each request on it that carries one of the job's stand-ins goes to POST /actions/jobs/{job}/broker with the job's current token in Authorization, and a request without one goes straight to the host on a connection of its own.1//! `POST /actions/jobs/{job}/broker`: a request a self-hosted runner's job
2//! made to one of its brokered secrets' hosts, which the job's harness
3//! passes to g1t so the secret's value never reaches the runner's
4//! computer (services/actions broker.rs, crates/runner actions/broker.rs).
5//!
6//! The job's current token comes in `Authorization: Bearer`, never in the
7//! URL, and the request to make in `x-g1t-broker-request` (base64 of
8//! `{ method, url, headers }`), with its body as this request's. The
9//! runner Worker does the rest (services/runner broker-relay.ts): it
10//! fills in the stand-ins, sends the request without following redirects,
11//! and answers with what came back, scrubbed of the values, its status and
12//! headers in `x-g1t-broker-response`. Bodies stream through here as they
13//! are; the Worker holds the limits.
14
15use worker::wasm_bindgen::JsValue;
16use worker::{Method, Request, RequestInit, Response, Result};
17
18use crate::operations::Services;
19
20/// The headers passed on to the runner Worker; nothing else of the
21/// request is. Its body's length the Worker counts as it reads.
22const PASSED: &[&str] = &["authorization", "x-g1t-broker-request"];
23
24/// The job a broker request is for, from its path, or None when the path
25/// is not one.
26pub fn job_of(path: &str) -> Option<&str> {
27 let job = path.strip_prefix("/actions/jobs/")?.strip_suffix("/broker")?;
28 (!job.is_empty() && !job.contains('/')).then_some(job)
29}
30
31/// Passes a broker request to the runner Worker, and its answer back.
32pub async fn relay(request: Request, services: &Services, job: &str) -> Result<Response> {
33 let headers = worker::Headers::new();
34 headers.set("x-g1t-broker-job", job)?;
35 for name in PASSED {
36 if let Some(value) = request.headers().get(name)? {
37 headers.set(name, &value)?;
38 }
39 }
40 let mut init = RequestInit::new();
41 init.with_method(Method::Post).with_headers(headers).with_body(request.inner().body().map(JsValue::from));
42 services.runner.fetch_request(Request::new_with_init("https://runner/rpc/broker", &init)?).await
43}
44
45#[cfg(test)]
46mod tests {
47 use super::job_of;
48
49 #[test]
50 fn only_a_job_s_own_broker_path_is_one() {
51 assert_eq!(job_of("/actions/jobs/job_1/broker"), Some("job_1"));
52 assert_eq!(job_of("/actions/jobs//broker"), None);
53 assert_eq!(job_of("/actions/jobs/job_1/spec"), None);
54 assert_eq!(job_of("/actions/jobs/a/b/broker"), None);
55 assert_eq!(job_of("/repos/a/b/actions/jobs/job_1/broker"), None);
56 }
57}