Repository navigation
sec(ci): sibling deploy and sanity workflows grant id-token write to ungated jobs #140
Description
Follow-up from LeanerCloud/cloud-commitments-cli#1649 / PR LeanerCloud/cloud-commitments-cli#1657. Same structural shape, but — unlike LeanerCloud/cloud-commitments-cli#1649 — no attacker-settable value reaches a run: block today, so this is hardening rather than a live vulnerability. Labelled accordingly.
What
A structural sweep (parsing each workflow's YAML for id-token inheritance versus environment: bindings, rather than grepping) found jobs that hold id-token: write with no environment: binding:
| workflow | ungated jobs holding id-token: write |
|---|---|
deploy-aws-fargate.yml |
prepare, test-deployment, summary |
deploy-gcp.yml |
prepare, build-and-deploy, test-deployment, summary |
deploy-azure.yml |
prepare, build-and-deploy, test-deployment, summary |
aws_sanity.yml |
sanity |
azure_sanity.yml |
sanity |
All five declare permissions: id-token: write at workflow level, which hands it to every job whether or not that job authenticates.
Note deploy-gcp.yml and deploy-azure.yml are worse than deploy-aws-lambda.yml was: their build-and-deploy jobs actually assume cloud credentials and have no environment binding, so their OIDC subject is repo:<org/repo>:ref:refs/heads/main — the subject terraform/environments/aws/ci-cd-permissions/role.tf:29 accepts unconditionally.
Why this is not p0
The only untrusted value reaching a run: block in any of these is inputs.environment, and it is not injectable:
- on
workflow_dispatchit istype: choicewith options[dev, staging, prod], which GitHub validates server-side; - the
workflow_callinput istype: string(unvalidated), but the only caller isdeploy-all.yml, whose ownenvironmentinput is alsotype: choice.
So the escalation path exists structurally but has no injection to trigger it. deploy-aws-lambda.yml was the only one of the four with an attacker-settable value (github.event.release.tag_name) — that is LeanerCloud/cloud-commitments-cli#1649.
github.actor also reaches run: blocks in all four deploy workflows. GitHub usernames are [A-Za-z0-9-], so those are not injectable; listed here only so a reader does not re-flag them.
Fix
Apply the same split PR LeanerCloud/cloud-commitments-cli#1657 applied to deploy-aws-lambda.yml:
- Remove
id-token: writefrom workflow level; grant it per job, only to jobs that actually callconfigure-aws-credentials/azure/login/google-github-actions/auth. - Give those jobs an
environment:binding so their OIDC subject isenvironment:<name>rather thanref:refs/heads/main. - Route
inputs.environmentthroughenv:with adev|staging|prodallowlist, sinceworkflow_calltypes it as a free-form string. - Watch for the same reviewer trap sec(ci): deploy-aws-lambda.yml interpolates a release tag name into a run block in an ungated job holding id-token write cloud-commitments-cli#1649 had:
deploy-aws-fargate.yml,deploy-gcp.ymlanddeploy-azure.ymlall declareoutputs: environment: ..., an output named environment that reads like a binding and gates nothing. PR fix(ci): stop interpolating the release tag into a run block cloud-commitments-cli#1657 renamed it totarget_environment; do the same here.
Once all of these are gated, the repo:<org/repo>:ref:refs/heads/main subject can finally be dropped from the trust policy — currently it cannot, because these jobs plus cleanup-staging.yml and destroy-fargate-dev.yml (LeanerCloud/cloud-commitments-cli#1591) depend on it.
Caveat: per LeanerCloud/cloud-commitments-cli#1648, no Environment in this repo currently has protection rules, so an environment: binding buys the correct OIDC subject but not yet an approval gate. Steps 1-4 are still worth doing — they are the precondition for the gate to mean anything.
Related: LeanerCloud/cloud-commitments-cli#1649 / PR LeanerCloud/cloud-commitments-cli#1657, LeanerCloud/cloud-commitments-cli#1542 / PR LeanerCloud/cloud-commitments-cli#1641, LeanerCloud/cloud-commitments-cli#1591, LeanerCloud/cloud-commitments-cli#1648, LeanerCloud/cloud-commitments-cli#1646.
Activity
docs(ci): regenerate the environments list from actual environment: b…