Problem
Environment variables on GitHub Actions runners are not included in the Sigstore attestation. The OIDC claims baked into the Fulcio certificate only contain repo, commit, run_id, workflow_ref — no snapshot of the runtime environment.
This means execution behavior can be manipulated without changing the workflow YAML, via env vars like LD_PRELOAD, NODE_OPTIONS, BASH_ENV, PYTHONWARNINGS, PATH, etc. that alter program behavior without modifying the binary.
See: elttam — Environment Variable Injection
See: Synacktiv — GitHub Actions Exploitation
Comparison with dstack
Dstack TEEs handle this explicitly with allowed_envs in app-compose.json. The compose-hash (measured into RTMR3) declares which env vars may vary — everything else is locked by the measurement. Verifiers can check exactly which vars are host-mutable.
GitHub Actions has no equivalent mechanism.
What we've learned so far
Experimental findings (branch: env-confinement)
-
Repo variables (vars.*) do NOT auto-inject into the shell environment. They're only accessible via ${{ vars.* }} template expansion and only enter the env through explicit env: blocks in the YAML (auditable at the proven commit).
-
env: blocks CAN override system vars — env: PATH: ${{ vars.PATH }} successfully replaced PATH. But this is visible in the YAML.
-
The real injection vector is $GITHUB_ENV file writes — a compromised action in step N can write LD_PRELOAD=/evil to $GITHUB_ENV, and step N+1 inherits it silently. Not visible in the YAML.
-
Runner image changes are unattested — GitHub updates ubuntu-latest frequently (currently 20260209.23.1), new vars can appear or values change without notice.
Current implementation
.github/allowed-env-reference.txt — baseline of 95 env var names from a clean ubuntu-24.04 runner
- Guard step in
.github/workflows/dump-env.yml that aborts if unexpected var names appear
- Gap: current guard only checks names, not values
Proposed design: three-tier allowed_envs model
Mimicking dstack's approach:
-
Fixed defaults — name AND value must match reference snapshot (e.g., PATH, HOME, SHELL, toolchain vars). If any differ → abort.
-
Runner-dynamic — set by GitHub per-run, values vary naturally (GITHUB_SHA, GITHUB_RUN_ID, RUNNER_NAME, etc.). GitHub protects GITHUB_* and RUNNER_* from user override. Check that no unexpected new ones appeared.
-
Workflow-declared — the actual allowed_envs. Only vars the workflow explicitly declares in its env: block. These are the ONLY ones where user-chosen values are expected (e.g., PROVER_DIGEST, EXPECTED_VK_HASH).
Open questions
- Should
ImageVersion be pinned? It changes on runner image updates — pinning it would break workflows on the next update, but not pinning means unattested drift.
- How to handle
$GITHUB_ENV writes mid-job? The guard can run as step 1, but a compromised action in a later step could still inject.
- Could this become a reusable GitHub Action for other projects?
- What's the right way to document this in the trust model — known limitation vs. mitigation?
References
- Branch:
env-confinement
.github/allowed-env-reference.txt (baseline snapshot)
.github/workflows/dump-env.yml (experiments + guard)
docs/trust-model.md (needs update)
docs/auditing-workflows.md (needs new red flag)
Thanks to James Austgen for identifying this problem.
Problem
Environment variables on GitHub Actions runners are not included in the Sigstore attestation. The OIDC claims baked into the Fulcio certificate only contain
repo,commit,run_id,workflow_ref— no snapshot of the runtime environment.This means execution behavior can be manipulated without changing the workflow YAML, via env vars like
LD_PRELOAD,NODE_OPTIONS,BASH_ENV,PYTHONWARNINGS,PATH, etc. that alter program behavior without modifying the binary.See: elttam — Environment Variable Injection
See: Synacktiv — GitHub Actions Exploitation
Comparison with dstack
Dstack TEEs handle this explicitly with
allowed_envsinapp-compose.json. The compose-hash (measured into RTMR3) declares which env vars may vary — everything else is locked by the measurement. Verifiers can check exactly which vars are host-mutable.GitHub Actions has no equivalent mechanism.
What we've learned so far
Experimental findings (branch:
env-confinement)Repo variables (
vars.*) do NOT auto-inject into the shell environment. They're only accessible via${{ vars.* }}template expansion and only enter the env through explicitenv:blocks in the YAML (auditable at the proven commit).env:blocks CAN override system vars —env: PATH: ${{ vars.PATH }}successfully replaced PATH. But this is visible in the YAML.The real injection vector is
$GITHUB_ENVfile writes — a compromised action in step N can writeLD_PRELOAD=/evilto$GITHUB_ENV, and step N+1 inherits it silently. Not visible in the YAML.Runner image changes are unattested — GitHub updates ubuntu-latest frequently (currently
20260209.23.1), new vars can appear or values change without notice.Current implementation
.github/allowed-env-reference.txt— baseline of 95 env var names from a clean ubuntu-24.04 runner.github/workflows/dump-env.ymlthat aborts if unexpected var names appearProposed design: three-tier
allowed_envsmodelMimicking dstack's approach:
Fixed defaults — name AND value must match reference snapshot (e.g.,
PATH,HOME,SHELL, toolchain vars). If any differ → abort.Runner-dynamic — set by GitHub per-run, values vary naturally (
GITHUB_SHA,GITHUB_RUN_ID,RUNNER_NAME, etc.). GitHub protectsGITHUB_*andRUNNER_*from user override. Check that no unexpected new ones appeared.Workflow-declared — the actual
allowed_envs. Only vars the workflow explicitly declares in itsenv:block. These are the ONLY ones where user-chosen values are expected (e.g.,PROVER_DIGEST,EXPECTED_VK_HASH).Open questions
ImageVersionbe pinned? It changes on runner image updates — pinning it would break workflows on the next update, but not pinning means unattested drift.$GITHUB_ENVwrites mid-job? The guard can run as step 1, but a compromised action in a later step could still inject.References
env-confinement.github/allowed-env-reference.txt(baseline snapshot).github/workflows/dump-env.yml(experiments + guard)docs/trust-model.md(needs update)docs/auditing-workflows.md(needs new red flag)Thanks to James Austgen for identifying this problem.