Skip to content

Environment variables are not part of attestation #73

@amiller

Description

@amiller

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)

  1. 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).

  2. env: blocks CAN override system varsenv: PATH: ${{ vars.PATH }} successfully replaced PATH. But this is visible in the YAML.

  3. 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.

  4. 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:

  1. Fixed defaults — name AND value must match reference snapshot (e.g., PATH, HOME, SHELL, toolchain vars). If any differ → abort.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions