-
Notifications
You must be signed in to change notification settings - Fork 7
Description
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 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- Guard step in
.github/workflows/dump-env.ymlthat 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 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
- Should
ImageVersionbe 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_ENVwrites 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.