Skip to content

ci: authenticate to Google Cloud keylessly with WIF - #230

Merged
SanabriaRusso merged 1 commit into
mainfrom
ci/keyless-auth
Sep 9, 2026
Merged

ci: authenticate to Google Cloud keylessly with WIF#230
SanabriaRusso merged 1 commit into
mainfrom
ci/keyless-auth

Conversation

@SanabriaRusso

Copy link
Copy Markdown
Collaborator

Summary

GCP_SA_KEY — the long-lived Google service-account key this workflow used — leaked and has been disabled, so Docker Build CI/CD currently fails at the auth step. This replaces it with Workload Identity Federation and rewrites the job so a live credential can never reach dependency code again.

  • Keyless auth. google-github-actions/auth now takes workload_identity_provider + service_account instead of credentials_json, with token_format: access_token, create_credentials_file: false and access_token_lifetime: 900s. There is no GCP_SA_KEY anywhere in .github/ any more, and no credential file is written into $GITHUB_WORKSPACE.
  • Scoped, not global. The workflow-level env.GCP_SA_KEY (which exported the raw key into every step, including npm ci's install scripts) is gone. The minted token is referenced only as steps.auth.outputs.access_token, by the GAR login, the ghcr login and npm publish.
  • Reordered. npm ci --ignore-scripts and npm run build now run before authentication. After the auth step only the registry logins, npm publish --ignore-scripts and the docker build remain. An ORDERING RULE comment at the top of the job records why.
  • No credentials on disk. gcloud auth configure-docker is replaced by docker/login-action (writes ~/.docker/config.json, logs out in its post step); the hand-rolled echo … > .npmrc / rm -f .npmrc pair is replaced by actions/setup-node with registry-url + scope: '@o1-labs', which keeps its .npmrc in $RUNNER_TEMP, plus a per-step NODE_AUTH_TOKEN on the publish step only.
  • Guard step, run twice — right before npm publish and again right before the docker build context is sent — failing the job if any gha-creds-*.json, .npmrc or dotenv file is in the workspace root, or if npm pack would ship one.
  • Ignore files. gha-creds-*.json added to .gitignore, .dockerignore and .npmignore; .npmrc added to .gitignore.
  • Hardening (issue 06, workflow part only). Every workflow now declares a top-level permissions: block, and every non-local uses: is pinned to a 40-char commit SHA with the tag kept as a trailing comment so Dependabot's github-actions group still works.

Tag formats, image names, registries, the npm version scheme, the fork/Dependabot if: guard and the buildx layer cache are all unchanged.

Docs: new "CI credentials" subsection in docs/versioning.md, one constraint line in AGENTS.md.

⛔ Do not merge until the Terraform is applied

This PR references a WIF pool, provider and service account that do not exist yet:

  • provider projects/1020762690228/locations/global/workloadIdentityPools/github-actions/providers/github
  • service account archive-node-api-ci@o1labs-192920.iam.gserviceaccount.com

They come from the o1-labs/gitops-infrastructure PR that adds platform/gcloud/service-accounts/workload-identity.tf (WIF pool + provider) and the archive-node-api-ci entry in service-accounts.tfvars (SA + the per-repository roles/iam.workloadIdentityUser binding for attribute.repository/o1-labs/Archive-Node-API). Merging before a human applies that Terraform makes every build fail at auth. Keep this PR in draft until then.

Follow-up human steps after the first green WIF run: delete the GCP_SA_KEY repository secret, and apply the repository-settings half of issue 06 (default_workflow_permissions=read, allowed_actions=selected, sha_pinning_required=true, branch protection) — deliberately not done here.

Deviation from the spec worth reviewing

Issue 04's guard script uses -name '.env.*', which matches the committed, secret-free .env.example.compose and .env.example.lightnet — verified locally, the script as written fails on a clean checkout every time. The predicate is \( -name '.env.*' ! -name '.env.example.*' \) here, with a comment saying why. Everything else in the script is verbatim.

Acceptance criteria

Issue 04 — no credential on disk

  • grep -c 'gha-creds-\*.json' .gitignore .dockerignore .npmignore prints 1 for each; grep -c '^\.npmrc$' .gitignore .npmignore prints 1 for each.
  • Auth step has create_credentials_file: false and token_format: access_token; grep -nE 'configure-docker|print-access-token|> \.npmrc|rm -f \.npmrc' .github/workflows/build.yaml returns nothing.
  • Run log has no Created credentials file and no GOOGLE_APPLICATION_CREDENTIALS: line — needs a CI run.
  • Log shows the guard passing twice and lists the packed files (only package.json, README.md, schema.graphql, tsconfig.json and paths under build/, src/) — needs a CI run; verified locally: 216 files, nothing outside that set.
  • Negative test recorded on a throw-away branch — see below.
  • The run publishes @o1-labs/mina-archive-node-graphql@<version>-dev.<sha> and pushes dev-<sha>needs a CI run.
  • transferring context: stays around 1 MB — needs a CI run.

Issue 03 — scope the credential, run npm first

  • No GCP_SA_KEY under any workflow- or job-level env: — there is no reference to it anywhere in .github/ at all.
  • yq '.jobs["build-and-deploy"].steps[].name' lists Install dependencies and Build before Authenticate to Google Cloud; the only npm step after it is npm publish.
  • npm ci --ignore-scripts; --ignore-scripts on both npm publish branches.
  • printenv | grep -c '^GCP_SA_KEY=' probe prints 0 after Install dependencies and after the auth step — needs a throw-away-branch run (structurally guaranteed: the secret no longer exists and nothing is exported).
  • npm run test:unit passes locally after npm ci --ignore-scripts — 21 suites, fail 0 in every one.
  • The PR run publishes the -dev.<sha> package and pushes the dev-<sha> image — needs a CI run.

Issue 02 — Workload Identity Federation (workflow part)

  • grep -rnE 'GCP_SA_KEY|credentials_json' .github/ prints nothing.
  • yq '.permissions' .github/workflows/build.yaml prints exactly contents: read, id-token: write, packages: write.
  • gh secret list --repo o1-labs/Archive-Node-API prints nothing — human step after the first green WIF run.
  • gcloud iam service-accounts keys list --iam-account=archive-node-api-ci@… --managed-by=user prints nothing — after Terraform apply.
  • One pull-request build and one v* tag build succeed on WIF; the auth step prints Successfully authenticated without Created credentials fileafter Terraform apply.
  • Cloud-audit evidence shows an empty serviceAccountKeyName and a principalSubject beginning principal://iam.googleapis.com/projects/1020762690228/…/subject/repo:o1-labs/Archive-Node-API:after Terraform apply.
  • Cross-repository negative test — see below.

Issue 06 — hardening (workflow part only)

  • grep -L '^permissions:' .github/workflows/*.y*ml prints nothing.
  • grep -hoE 'uses: [^@ ]+@[^ ]+' .github/workflows/*.y*ml | grep -vE '@[0-9a-f]{40}$' prints nothing.
  • Every pin keeps its # vX.Y.Z comment; anchore/sbom-action left as it was; no version bumped.
  • Unpinned-action negative test under sha_pinning_required: trueneeds the repository setting, which is out of scope for this PR.
  • All workflows green on the PR and on main after merge — needs a CI run.
  • Dependabot's next github-actions group PR updates SHAs and comments together — wait for the weekly run.

Repository-settings criteria from issue 06 (actions/permissions/workflow, selected-actions, branch protection) are deliberately not addressed here; they are gh api calls needing admin approval.

Negative tests to run once on a throw-away branch, then delete

  1. Guard step. Add - run: echo '{}' > gha-creds-test.json immediately before the first guard. The run must fail at the guard with ::error::credential file(s) present in workspace:. (Reproduced locally against the exact script body: exits 1, prints ./gha-creds-test.json.)
  2. Secret-not-exported probe. Add - run: printenv | grep -c '^GCP_SA_KEY=' || true right after Install dependencies and again right after the auth step; both must print 0.
  3. Unpinned action. Once sha_pinning_required: true is set on the repository, reference actions/checkout@v7 on a branch; the run must refuse to start with GitHub's "must be pinned to a full-length commit SHA" error.
  4. Per-repository WIF binding. In another o1-labs repository (or a throw-away branch of gitops-images), use the same provider with service_account: archive-node-api-ci@…; auth must fail with PERMISSION_DENIED / unable to acquire impersonated credentials.

Link each failed run here, then delete the branches.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BMNuJzL2Lctj1jeabkLury

GCP_SA_KEY, the long-lived service-account key, leaked and is disabled, so
build.yaml fails at auth today. Replace it with Workload Identity
Federation: each run presents its GitHub OIDC token and receives a
15-minute access token that is never written to the workspace.
Order matters: npm ci --ignore-scripts and npm run build now finish before
the auth step, so dependency install scripts never execute while a cloud
credential is live, and a guard asserts the workspace holds no credential
file before npm publish and before the docker build context is sent. Also
adds a permissions: block to every workflow and pins every action to a SHA.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BMNuJzL2Lctj1jeabkLury
@SanabriaRusso

Copy link
Copy Markdown
Collaborator Author

Keyless path verified end to end before merge. After gitops-infrastructure #1811 was applied (pool + provider + archive-node-api-ci, repository writer bindings), the failed build job of this PR was re-run and went green: https://github.com/o1-labs/Archive-Node-API/actions/runs/34341746761 (5m43s).

Evidence from that run:

  • auth step: no Created credentials file and no GOOGLE_APPLICATION_CREDENTIALS line anywhere in the log
  • guard step passed twice; npm pack would ship 216 files, all under build/, src/ or the four whitelisted root files
  • published @o1-labs/mina-archive-node-graphql@1.0.0-dev.85b63e2 to euro-npm
  • pushed euro-docker-repo/archive-node-api:dev-85b63e2 (and the full-sha tag); Docker context transferred: 1.01 MB
  • Artifact Registry audit log: every write by archive-node-api-ci@… with an empty serviceAccountKeyName

Account state: zero user-managed keys, zero project-level roles, roles/artifactregistry.writer on euro-docker-repo and euro-npm only, WIF binding limited to attribute.repository/o1-labs/Archive-Node-API.

Ready to merge. GCP_SA_KEY and the old pusher keys are deleted right after.

@SanabriaRusso
SanabriaRusso marked this pull request as ready for review September 9, 2026 11:30
@SanabriaRusso
SanabriaRusso merged commit 6595986 into main Sep 9, 2026
8 of 9 checks passed
@SanabriaRusso
SanabriaRusso deleted the ci/keyless-auth branch September 9, 2026 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant