From db60d8c97e06fdad5e353d0f6f527ed20e722ef8 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 3 Aug 2026 10:22:36 -0700 Subject: [PATCH 1/3] fix: isolate Actions OIDC from agent Keep GitHub Actions OIDC request credentials in the API proxy sidecar. Prevent default, env-all, env-file, and explicit environment paths from exposing them to untrusted agent code. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01b67b6c-fcd9-4c60-8409-827495c49557 --- docs/api-proxy-sidecar.md | 8 +-- docs/auth-matrix.md | 2 + docs/authentication-architecture.md | 16 +++--- docs/awf-config-spec.md | 7 ++- docs/environment.md | 4 +- docs/sbx-integration.md | 10 ++-- src/config/sandbox-mount-policy.json | 2 +- .../agent-environment-credentials.test.ts | 50 ++++++++++--------- .../agent-environment/env-passthrough.ts | 2 - .../agent-environment/excluded-vars.ts | 2 + src/services/api-proxy-service-oidc.test.ts | 3 ++ 11 files changed, 61 insertions(+), 45 deletions(-) diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index 5b350a286..835f76bd7 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -706,8 +706,10 @@ AWF supports OIDC-based credential exchange with multiple cloud providers via Gi | `ACTIONS_ID_TOKEN_REQUEST_URL` | ✅ | Provided automatically by the GitHub Actions runtime | | `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | ✅ | Provided automatically by the GitHub Actions runtime | -:::caution[OIDC request capability reaches the agent] -AWF forwards `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` to both the sidecar and the agent container. These values let code request a GitHub OIDC JWT when the job has `permissions: id-token: write`. Exchanged provider credentials remain isolated in the sidecar, but the token-minting capability itself is not isolated. Restrict provider federation policies to the expected repository, workflow, ref, and audience. +:::note[OIDC request capability is sidecar-only] +AWF forwards `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` only to the api-proxy sidecar when `AWF_AUTH_TYPE=github-oidc`. The variables are excluded from the agent even when `--env-all`, `--env-file`, or explicit `--env` options request them. The minted GitHub JWT and exchanged provider credentials also remain inside the sidecar. + +OIDC-dependent MCP servers must run outside the agent container, such as behind a trusted MCP gateway or in a dedicated sidecar that receives the Actions variables directly. The agent should connect to that service over MCP rather than receiving the token-minting capability. ::: When `AWF_AUTH_TYPE=github-oidc` is set but `ACTIONS_ID_TOKEN_REQUEST_URL`/`ACTIONS_ID_TOKEN_REQUEST_TOKEN` are not available in the sidecar, Anthropic OIDC requests fail closed with: @@ -770,7 +772,7 @@ Exchanges the GitHub OIDC JWT for a GCP access token via the Security Token Serv Default OIDC audience: the `gcpWorkloadIdentityProvider` value :::note -`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` are injected by the Actions runner automatically. AWF forwards them to the sidecar when `AWF_AUTH_TYPE=github-oidc` and currently also passes them through to the agent. +`ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` are injected by the Actions runner automatically. AWF forwards them to the sidecar when `AWF_AUTH_TYPE=github-oidc` and excludes them from the agent container. ::: :::tip diff --git a/docs/auth-matrix.md b/docs/auth-matrix.md index c74be77bc..58dd3fa3e 100644 --- a/docs/auth-matrix.md +++ b/docs/auth-matrix.md @@ -277,6 +277,8 @@ All OIDC flows require GitHub Actions runtime tokens: - `ACTIONS_ID_TOKEN_REQUEST_URL` — endpoint to mint OIDC JWTs - `ACTIONS_ID_TOKEN_REQUEST_TOKEN` — auth token for the OIDC endpoint +AWF forwards these variables only to the api-proxy sidecar in `github-oidc` mode and excludes them from the agent container. OIDC-dependent MCP servers must run behind a trusted external MCP gateway or in a dedicated sidecar rather than as agent child processes. + ### Azure (Entra ID) | Config | Env Var | Required | diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md index 832fae4a8..b6c54ae37 100644 --- a/docs/authentication-architecture.md +++ b/docs/authentication-architecture.md @@ -570,7 +570,7 @@ AWF does not provide this mode. The diagram shows the risk that the always-on si ## OIDC authentication (keyless credential exchange) -AWF also supports **keyless authentication** via GitHub Actions OIDC workload identity federation. Instead of static API keys, the api-proxy sidecar exchanges a short-lived GitHub-issued JWT for provider-specific credentials — without the agent ever seeing any secret. +AWF also supports **keyless authentication** via GitHub Actions OIDC workload identity federation. Instead of static API keys, the api-proxy sidecar exchanges a short-lived GitHub-issued JWT for provider-specific credentials. The Actions token-minting variables, minted JWT, and exchanged credentials remain outside the agent container. ### How native GitHub Actions OIDC works @@ -609,7 +609,7 @@ In a standard GitHub Actions workflow (without AWF), OIDC federation works like ### How AWF OIDC works (credential isolation) -AWF keeps the minted GitHub JWT and exchanged cloud credential in the api-proxy sidecar. However, the Actions runtime URL and token used to request a JWT are currently forwarded to the agent as well as the sidecar: +AWF keeps the Actions OIDC request capability, minted GitHub JWT, and exchanged cloud credential in the api-proxy sidecar: ``` ┌─────────────────────────────┐ ┌───────────────────────────────────────┐ @@ -617,7 +617,7 @@ AWF keeps the minted GitHub JWT and exchanged cloud credential in the api-proxy │ 172.30.0.20 │ │ 172.30.0.30 │ │ │ │ │ │ Environment: │ │ Environment: │ -│ ✓ ACTIONS_ID_TOKEN_* │ │ ✓ ACTIONS_ID_TOKEN_REQUEST_URL │ +│ ✗ No ACTIONS_ID_TOKEN_* │ │ ✓ ACTIONS_ID_TOKEN_REQUEST_URL │ │ ✗ No cloud credentials │ │ ✓ ACTIONS_ID_TOKEN_REQUEST_TOKEN │ │ ✗ No API keys │ │ ✓ AWF_AUTH_TYPE=github-oidc │ │ ✓ OPENAI_BASE_URL= │ │ ✓ AWF_AUTH_PROVIDER=azure|aws|gcp|anthropic │ @@ -648,7 +648,7 @@ AWF keeps the minted GitHub JWT and exchanged cloud credential in the api-proxy #### Step 1: Configuration forwarding -The AWF CLI forwards `AWF_AUTH_*` configuration only to the api-proxy sidecar. The Actions runtime OIDC request URL and token are different: `passthroughHostEnvironment()` currently forwards both to the agent as well as `buildOidcEnv()` forwarding them to the sidecar. +The AWF CLI forwards `AWF_AUTH_*` configuration and the Actions runtime OIDC request URL and token only to the api-proxy sidecar. `buildOidcEnv()` conditionally adds the runtime variables to the sidecar in `github-oidc` mode, while `buildExclusionSet()` prevents every agent environment input path from adding them. ``` Host environment Sidecar container Agent container @@ -656,11 +656,11 @@ Host environment Sidecar container Agent container AWF_AUTH_TYPE=github-oidc ──► AWF_AUTH_TYPE ✓ ✗ (excluded) AWF_AUTH_PROVIDER=azure ──► AWF_AUTH_PROVIDER ✓ ✗ (excluded) AWF_AUTH_AZURE_TENANT_ID=... ──► AWF_AUTH_AZURE_TENANT_ID ✓ ✗ (excluded) -ACTIONS_ID_TOKEN_REQUEST_URL ──► forwarded when type=oidc ✓ ✓ (forwarded) +ACTIONS_ID_TOKEN_REQUEST_URL ──► forwarded when type=oidc ✓ ✗ (excluded) ``` -:::caution[OIDC minting capability is visible to the agent] -With `permissions: id-token: write`, `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` let agent code request a GitHub OIDC JWT for an audience. AWF keeps the resulting provider credential in the sidecar, but it does not currently isolate this token-minting capability from the agent. Scope federation trust policies narrowly to the repository, workflow, ref, and expected audience. +:::note[OIDC-authenticated MCP servers] +Stdio MCP servers launched as agent child processes do not receive the Actions OIDC variables. Run an OIDC-dependent MCP server behind a trusted external MCP gateway or in a dedicated sidecar, give that component the runtime variables directly, and expose only its MCP endpoint to the agent. This preserves OIDC support without placing token-minting capability in the untrusted agent container. ::: #### Step 2: GitHub OIDC token minting @@ -752,7 +752,7 @@ For Anthropic bearer requests, AWF merges the OAuth beta with client-supplied `a |----------|----------------|-----------------| | Credential type | Long-lived secret | Short-lived token (~1h) | | Rotation | Manual | Automatic (proactive refresh) | -| Agent sees credential material | No real provider key | No minted JWT or exchanged provider credential, but the Actions OIDC request token/URL are currently forwarded to the agent | +| Agent sees credential material | No real provider key | No Actions OIDC request token, minted JWT, or exchanged provider credential | | GitHub Actions requirement | API key in secrets | `permissions: id-token: write` | | Cloud provider setup | Generate API key | Configure trust policy/federation | | Supported providers | OpenAI, Anthropic, Copilot, Gemini, Vertex AI | Azure (OpenAI/Copilot), GCP (OpenAI/Copilot adapters only — not the native Vertex/Gemini adapters), Anthropic WIF, AWS Bedrock Runtime via OpenAI/Copilot adapters | diff --git a/docs/awf-config-spec.md b/docs/awf-config-spec.md index 981fa85fb..2a1a63229 100644 --- a/docs/awf-config-spec.md +++ b/docs/awf-config-spec.md @@ -298,7 +298,7 @@ passthrough. A conforming implementation MUST NOT inherit them from the host: |----------|-----------| | System | `PATH`, `PWD`, `OLDPWD`, `SHLVL`, `_`, `SUDO_COMMAND`, `SUDO_USER`, `SUDO_UID`, `SUDO_GID` | | Proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, `https_proxy`, `NO_PROXY`, `no_proxy`, `ALL_PROXY`, `all_proxy`, `FTP_PROXY`, `ftp_proxy` | -| Actions artifact tokens | `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RESULTS_URL` | +| Actions runtime credentials | `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RESULTS_URL`, `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | | AWF internal controls | `AWF_PREFLIGHT_BINARY`, `AWF_GEMINI_ENABLED` | > **Note:** Host proxy variables are read for upstream proxy auto-detection @@ -314,13 +314,16 @@ the following host variables into the agent container: |----------|-----------| | GitHub authentication | `GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_PERSONAL_ACCESS_TOKEN` | | GitHub enterprise | `GITHUB_SERVER_URL`, `GITHUB_API_URL` | -| Actions OIDC | `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | | Docker client | `DOCKER_HOST`, `DOCKER_TLS`, `DOCKER_TLS_VERIFY`, `DOCKER_CERT_PATH`, `DOCKER_CONFIG`, `DOCKER_CONTEXT`, `DOCKER_API_VERSION`, `DOCKER_DEFAULT_PLATFORM` | | User environment | `USER`, `XDG_CONFIG_HOME` | When `--env-all` IS active, all host variables not in the excluded set (§8.3) SHALL be forwarded, subject to credential isolation rules (§9). +Actions OIDC request variables MUST be forwarded directly to the api-proxy +sidecar when `apiProxy.auth.type` is `github-oidc` and MUST NOT be forwarded +to the agent through any environment input path. + ### 8.5 Explicit Overrides Variables passed via `-e` / `--env` MUST override values from `--env-all` diff --git a/docs/environment.md b/docs/environment.md index c88317688..0dd3f6ff6 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -44,7 +44,7 @@ Using `--env-all` passes all host environment variables to the container, which 3. **Unnecessary Access**: Extra variables increase attack surface (violates least privilege) 4. **Accidental Sharing**: Easy to forget what's in your environment when sharing commands -**Excluded variables** (even with `--env-all`): `PATH`, `PWD`, `OLDPWD`, `SHLVL`, `_`, `SUDO_*` +**Excluded variables** (even with `--env-all`): `PATH`, `PWD`, `OLDPWD`, `SHLVL`, `_`, `SUDO_*`, `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RESULTS_URL`, `ACTIONS_ID_TOKEN_REQUEST_URL`, and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`. Actions OIDC variables are forwarded directly to the api-proxy sidecar in `github-oidc` mode, never to the agent. **Proxy variables:** `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, `https_proxy`, `NO_PROXY`, `no_proxy`, `ALL_PROXY`, and `FTP_PROXY` (all case variants) from the host are **excluded from container passthrough** when using `--env-all`. The firewall sets its own proxy variables pointing to Squid inside the container. However, host proxy variables **are read** for upstream proxy auto-detection — if the host has `https_proxy`/`http_proxy` set, AWF configures Squid to chain outbound traffic through that corporate proxy (see [Upstream Proxy Support](#upstream-corporate-proxy-support)). @@ -64,7 +64,7 @@ Using `--env-all` passes all host environment variables to the container, which 3. `--env-file` variables 4. `--env` / `-e` explicit variables (highest priority) -**Excluded variables** in `--env-file` (same list as `--env-all`): `PATH`, `PWD`, `HOME`, `SUDO_*`, etc. +**Excluded variables** in `--env-file` (same list as `--env-all`): `PATH`, `PWD`, `HOME`, `SUDO_*`, Actions runtime credentials, etc. Explicit `--env` cannot override credential exclusions. **Example use case — Safe Outputs MCP:** ```bash diff --git a/docs/sbx-integration.md b/docs/sbx-integration.md index a30b1b567..494bdb294 100644 --- a/docs/sbx-integration.md +++ b/docs/sbx-integration.md @@ -219,9 +219,9 @@ What `createSandbox()` shares, in order: `accessTokens.json`, `service_principal_entries.json`) are treated as credential stores and scrubbed before sandbox creation (sbx) or masked with `/dev/null` overlays (compose). Agents cannot read host Azure auth tokens - directly. Azure authentication must be obtained at runtime via OIDC - (`ACTIONS_ID_TOKEN_REQUEST_URL`/`TOKEN`, already forwarded) or via the - `ADO_MCP_AUTH_TOKEN` environment variable. + directly. Azure API authentication must be handled by the api-proxy's + sidecar-only OIDC exchange or by an external trusted service. The separate + `ADO_MCP_AUTH_TOKEN` environment variable remains available for ADO MCP. ::: **Scrubbing nested credential stores.** Several whitelisted dirs legitimately @@ -240,7 +240,9 @@ This is the sbx analog of compose mode's `/dev/null` credential overlays, and th central credential list in `sandbox-mount-policy.json` is shared between backends to prevent drift. The agent receives whatever credentials it needs through the api-proxy or environment (e.g. `ADO_MCP_AUTH_TOKEN`, OIDC tokens), not by reading -the host's on-disk auth store, so removing these paths is safe. +the host's on-disk auth store. Provider credentials and Actions OIDC +request variables remain in the api-proxy or another trusted external +service, so removing these paths is safe. A `seenPaths` set deduplicates so no path is mounted twice, and `execInSandbox(..., { workDir })` passes `--workdir` so commands run inside the diff --git a/src/config/sandbox-mount-policy.json b/src/config/sandbox-mount-policy.json index 8a999c776..d5cc6e67c 100644 --- a/src/config/sandbox-mount-policy.json +++ b/src/config/sandbox-mount-policy.json @@ -9,7 +9,7 @@ "etc": ["/etc/ssl", "/etc/ca-certificates", "/etc/pki/ca-trust/extracted", "/etc/pki/tls/certs", "/etc/alternatives", "/etc/ld.so.cache", "/etc/nsswitch.conf"] }, "home": { - "$comment": "Agent $HOME exposure. `toolSubdirs` is the ALLOW list: tool caches, language toolchains and agent state the agent legitimately needs. `forbiddenSubdirs` is a DENY guard: dirs whose primary purpose is storing credentials and which must NEVER be added to the allow list. Compose mounts an empty home + binds toolSubdirs on top; sbx mounts toolSubdirs wholesale instead of the whole $HOME. EXCEPTION: `.azure` is credential-bearing — it is intentionally mounted to provide Azure CLI config and account metadata, but its live token caches (msal_token_cache.bin, msal_token_cache.json, accessTokens.json, service_principal_entries.json) are masked by the credentials deny list so agents cannot read host auth tokens directly. Azure auth must come via OIDC (ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN) or the ADO_MCP_AUTH_TOKEN env var.", + "$comment": "Agent $HOME exposure. `toolSubdirs` is the ALLOW list: tool caches, language toolchains and agent state the agent legitimately needs. `forbiddenSubdirs` is a DENY guard: dirs whose primary purpose is storing credentials and which must NEVER be added to the allow list. Compose mounts an empty home + binds toolSubdirs on top; sbx mounts toolSubdirs wholesale instead of the whole $HOME. EXCEPTION: `.azure` is credential-bearing — it is intentionally mounted to provide Azure CLI config and account metadata, but its live token caches (msal_token_cache.bin, msal_token_cache.json, accessTokens.json, service_principal_entries.json) are masked by the credentials deny list so agents cannot read host auth tokens directly. Azure API auth must use the api-proxy's sidecar-only OIDC exchange or another trusted external service; ADO MCP may use its separate ADO_MCP_AUTH_TOKEN env var.", "toolSubdirs": [ ".cache", ".config", diff --git a/src/services/agent-environment-credentials.test.ts b/src/services/agent-environment-credentials.test.ts index 2d890f2c8..0868f423b 100644 --- a/src/services/agent-environment-credentials.test.ts +++ b/src/services/agent-environment-credentials.test.ts @@ -147,48 +147,39 @@ describe('agent environment: credentials', () => { expect(env.GH_TOKEN).not.toBe('ghp_real_secret_token_12345'); }); - it('should pass through ACTIONS_ID_TOKEN_REQUEST_URL when present in environment', () => { - const originalEnv = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; + it('should never pass Actions OIDC minting variables to the agent', () => { + const origUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; + const origToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'https://token.actions.githubusercontent.com/abc'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-oidc-token-value'; try { const result = generateDockerCompose(mockConfig, mockNetworkConfig); const env = result.services.agent.environment as Record; - expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBe('https://token.actions.githubusercontent.com/abc'); + expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBeUndefined(); + expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBeUndefined(); } finally { - if (originalEnv !== undefined) { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = originalEnv; + if (origUrl !== undefined) { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = origUrl; } else { delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL; } - } - }); - - it('should pass through ACTIONS_ID_TOKEN_REQUEST_TOKEN when present in environment', () => { - const originalEnv = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-oidc-token-value'; - - try { - const result = generateDockerCompose(mockConfig, mockNetworkConfig); - const env = result.services.agent.environment as Record; - expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBe('test-oidc-token-value'); - } finally { - if (originalEnv !== undefined) { - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = originalEnv; + if (origToken !== undefined) { + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = origToken; } else { delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; } } }); - it('should not pass through OIDC variables when not in environment', () => { + it('should exclude Actions OIDC minting variables from --env-all', () => { const origUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; const origToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL; - delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'https://token.actions.githubusercontent.com/abc'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-oidc-token-value'; try { - const result = generateDockerCompose(mockConfig, mockNetworkConfig); + const result = generateDockerCompose({ ...mockConfig, envAll: true }, mockNetworkConfig); const env = result.services.agent.environment as Record; expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBeUndefined(); expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBeUndefined(); @@ -206,6 +197,19 @@ describe('agent environment: credentials', () => { } }); + it('should reject explicit Actions OIDC minting variables in additionalEnv', () => { + const result = generateDockerCompose({ + ...mockConfig, + additionalEnv: { + ACTIONS_ID_TOKEN_REQUEST_URL: 'https://token.actions.githubusercontent.com/abc', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-oidc-token-value', + }, + }, mockNetworkConfig); + const env = result.services.agent.environment as Record; + expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBeUndefined(); + expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBeUndefined(); + }); + it('should never pass ACTIONS_RUNTIME_TOKEN to agent container', () => { const originalToken = process.env.ACTIONS_RUNTIME_TOKEN; process.env.ACTIONS_RUNTIME_TOKEN = 'test-runtime-token-value'; diff --git a/src/services/agent-environment/env-passthrough.ts b/src/services/agent-environment/env-passthrough.ts index 018c45395..0f249b10b 100644 --- a/src/services/agent-environment/env-passthrough.ts +++ b/src/services/agent-environment/env-passthrough.ts @@ -41,8 +41,6 @@ export function passthroughHostEnvironment(params: EnvPassthroughParams): void { 'XDG_CONFIG_HOME', 'GITHUB_SERVER_URL', 'GITHUB_API_URL', - 'ACTIONS_ID_TOKEN_REQUEST_URL', - 'ACTIONS_ID_TOKEN_REQUEST_TOKEN', 'AZURE_CONFIG_DIR', 'ADO_MCP_AUTH_TOKEN', 'DOCKER_HOST', diff --git a/src/services/agent-environment/excluded-vars.ts b/src/services/agent-environment/excluded-vars.ts index 9df3adde5..73ef060b7 100644 --- a/src/services/agent-environment/excluded-vars.ts +++ b/src/services/agent-environment/excluded-vars.ts @@ -14,6 +14,8 @@ export function buildExclusionSet(config: WrapperConfig): Set { 'SUDO_GID', 'ACTIONS_RUNTIME_TOKEN', 'ACTIONS_RESULTS_URL', + 'ACTIONS_ID_TOKEN_REQUEST_URL', + 'ACTIONS_ID_TOKEN_REQUEST_TOKEN', ...PROXY_ENV_VARS, 'AWF_PREFLIGHT_BINARY', 'AWF_STAGED_RUNNER_BINARY_NAME', diff --git a/src/services/api-proxy-service-oidc.test.ts b/src/services/api-proxy-service-oidc.test.ts index 22f2d9a5f..16407b827 100644 --- a/src/services/api-proxy-service-oidc.test.ts +++ b/src/services/api-proxy-service-oidc.test.ts @@ -37,8 +37,11 @@ describe('API proxy sidecar: OIDC env forwarding', () => { const config = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-openai-test' }; const result = generateDockerCompose(config, mockNetworkConfigWithProxy); const env = result.services['api-proxy'].environment as Record; + const agentEnv = result.services.agent.environment as Record; expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBe('https://actions.local/token'); expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBe('runtime-token'); + expect(agentEnv.ACTIONS_ID_TOKEN_REQUEST_URL).toBeUndefined(); + expect(agentEnv.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBeUndefined(); }); it('should forward ACTIONS_ID_TOKEN_REQUEST_* when config.authType is github-oidc (config-file path)', () => { From 8282d23fb84eee3fe45db117b7b2afd9bc384b1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:41:39 +0000 Subject: [PATCH 2/3] docs: clarify OIDC agent credential boundary --- docs/sbx-integration.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sbx-integration.md b/docs/sbx-integration.md index 494bdb294..abe13e125 100644 --- a/docs/sbx-integration.md +++ b/docs/sbx-integration.md @@ -238,11 +238,11 @@ them after the sandbox is torn down** (`scrubHomeCredentials` / the secrets are absent from the VM while the benign tool state stays available. This is the sbx analog of compose mode's `/dev/null` credential overlays, and the central credential list in `sandbox-mount-policy.json` is shared between backends -to prevent drift. The agent receives whatever credentials it needs through the -api-proxy or environment (e.g. `ADO_MCP_AUTH_TOKEN`, OIDC tokens), not by reading -the host's on-disk auth store. Provider credentials and Actions OIDC -request variables remain in the api-proxy or another trusted external -service, so removing these paths is safe. +to prevent drift. The agent accesses OIDC-backed providers through requests +routed to the api-proxy, or receives separately allowed environment credentials +such as `ADO_MCP_AUTH_TOKEN`, not by reading the host's on-disk auth store. +Provider credentials and Actions OIDC request variables remain in the api-proxy +or another trusted external service, so removing these paths is safe. A `seenPaths` set deduplicates so no path is mounted twice, and `execInSandbox(..., { workDir })` passes `--workdir` so commands run inside the From 964dcc83d62028d173cd7847591928edf5c8f000 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 3 Aug 2026 10:50:00 -0700 Subject: [PATCH 3/3] docs: align OIDC isolation with MCP gateway Describe the runner-owned gateway flow generated by gh-aw and add direct regression coverage for env-file credential exclusion. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01b67b6c-fcd9-4c60-8409-827495c49557 --- docs/api-proxy-sidecar.md | 2 +- docs/auth-matrix.md | 2 +- docs/authentication-architecture.md | 4 +++- .../agent-environment-credentials.test.ts | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index 835f76bd7..37cb342e5 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -709,7 +709,7 @@ AWF supports OIDC-based credential exchange with multiple cloud providers via Gi :::note[OIDC request capability is sidecar-only] AWF forwards `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` only to the api-proxy sidecar when `AWF_AUTH_TYPE=github-oidc`. The variables are excluded from the agent even when `--env-all`, `--env-file`, or explicit `--env` options request them. The minted GitHub JWT and exchanged provider credentials also remain inside the sidecar. -OIDC-dependent MCP servers must run outside the agent container, such as behind a trusted MCP gateway or in a dedicated sidecar that receives the Actions variables directly. The agent should connect to that service over MCP rather than receiving the token-minting capability. +GitHub Agentic Workflows handles HTTP MCP `auth.type: github-oidc` separately: the compiler-generated, runner-owned **Start MCP Gateway** step passes the Actions variables directly to the MCP gateway, which mints an audience-bound JWT for the remote server. AWF neither launches nor configures that gateway, and the variables do not need to pass through the agent. Recompile older workflow lock files that do not use this direct runner-to-gateway path; compatibility tracking is available in [github/gh-aw#50053](https://github.com/github/gh-aw/issues/50053). ::: When `AWF_AUTH_TYPE=github-oidc` is set but `ACTIONS_ID_TOKEN_REQUEST_URL`/`ACTIONS_ID_TOKEN_REQUEST_TOKEN` are not available in the sidecar, Anthropic OIDC requests fail closed with: diff --git a/docs/auth-matrix.md b/docs/auth-matrix.md index 58dd3fa3e..3d9e63ff8 100644 --- a/docs/auth-matrix.md +++ b/docs/auth-matrix.md @@ -277,7 +277,7 @@ All OIDC flows require GitHub Actions runtime tokens: - `ACTIONS_ID_TOKEN_REQUEST_URL` — endpoint to mint OIDC JWTs - `ACTIONS_ID_TOKEN_REQUEST_TOKEN` — auth token for the OIDC endpoint -AWF forwards these variables only to the api-proxy sidecar in `github-oidc` mode and excludes them from the agent container. OIDC-dependent MCP servers must run behind a trusted external MCP gateway or in a dedicated sidecar rather than as agent child processes. +AWF forwards these variables only to the api-proxy sidecar in `github-oidc` mode and excludes them from the agent container. GitHub Agentic Workflows independently passes them from its runner-owned **Start MCP Gateway** step directly to the MCP gateway when a remote HTTP MCP server uses `auth.type: github-oidc`; AWF does not launch or configure that gateway. See [github/gh-aw#50053](https://github.com/github/gh-aw/issues/50053) for lock-file compatibility tracking. ### Azure (Entra ID) diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md index b6c54ae37..bc2c31931 100644 --- a/docs/authentication-architecture.md +++ b/docs/authentication-architecture.md @@ -660,7 +660,9 @@ ACTIONS_ID_TOKEN_REQUEST_URL ──► forwarded when type=oidc ✓ ✗ (excl ``` :::note[OIDC-authenticated MCP servers] -Stdio MCP servers launched as agent child processes do not receive the Actions OIDC variables. Run an OIDC-dependent MCP server behind a trusted external MCP gateway or in a dedicated sidecar, give that component the runtime variables directly, and expose only its MCP endpoint to the agent. This preserves OIDC support without placing token-minting capability in the untrusted agent container. +GitHub Agentic Workflows supports `auth.type: github-oidc` for remote HTTP MCP servers through its compiler-managed MCP gateway. The generated **Start MCP Gateway** workflow step runs on the Actions runner before the AWF agent, passes the Actions variables directly to the gateway, and supplies only the gateway endpoint to the agent. The gateway mints an audience-bound JWT and injects it into the remote MCP request. AWF does not launch or configure the gateway. + +Lock files generated by compiler versions that do not pass the variables directly from the runner to the gateway must be recompiled. See [github/gh-aw#50053](https://github.com/github/gh-aw/issues/50053) for compatibility validation and migration tracking. ::: #### Step 2: GitHub OIDC token minting diff --git a/src/services/agent-environment-credentials.test.ts b/src/services/agent-environment-credentials.test.ts index 0868f423b..eeaad5ca1 100644 --- a/src/services/agent-environment-credentials.test.ts +++ b/src/services/agent-environment-credentials.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; + import { generateDockerCompose, WrapperConfig, baseConfig, mockNetworkConfig, useTempWorkDir } from './service-test-setup.test-utils'; // Create mock functions (must remain per-file — jest.mock() is hoisted before imports) @@ -210,6 +213,19 @@ describe('agent environment: credentials', () => { expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBeUndefined(); }); + it('should exclude Actions OIDC minting variables from env files', () => { + const envFile = path.join(mockConfig.workDir, 'oidc.env'); + fs.writeFileSync(envFile, [ + 'ACTIONS_ID_TOKEN_REQUEST_URL=https://token.actions.githubusercontent.com/abc', + 'ACTIONS_ID_TOKEN_REQUEST_TOKEN=test-oidc-token-value', + ].join('\n')); + + const result = generateDockerCompose({ ...mockConfig, envFile }, mockNetworkConfig); + const env = result.services.agent.environment as Record; + expect(env.ACTIONS_ID_TOKEN_REQUEST_URL).toBeUndefined(); + expect(env.ACTIONS_ID_TOKEN_REQUEST_TOKEN).toBeUndefined(); + }); + it('should never pass ACTIONS_RUNTIME_TOKEN to agent container', () => { const originalToken = process.env.ACTIONS_RUNTIME_TOKEN; process.env.ACTIONS_RUNTIME_TOKEN = 'test-runtime-token-value';