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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions