Players Engine

Devlog · Entry 17 · Engineering Notes

Keyless deploys with Workload Identity

· Nic Vannetti · 5 min read

There's a file that used to exist in every CI setup I'd ever built: the downloaded service-account key. A JSON blob that grants cloud access, pasted into a CI secret, powerful and immortal — it never expires, it works from anywhere, and if it leaks you may not find out until the cryptominers do. Players Engine's deploy pipeline has no such file, anywhere, and retiring the pattern was one of the highest security returns-per-effort of the whole project.

Federation in one paragraph

Workload Identity Federation works like this: when a GitHub Actions job runs, GitHub will attest — with a signed OIDC token — exactly which repository, branch and workflow is executing. The cloud side is configured to trust those attestations, and only those matching strict conditions, exchanging them for short-lived cloud credentials scoped to a deployer identity. No stored secret, nothing to rotate, nothing to leak at rest. The credential exists for minutes, materialized out of a trust relationship between two identity systems, then evaporates.

The conditions are the security

The magic isn't the token exchange — it's the specificity of what you agree to trust. A federation configured to trust "any workflow in my GitHub org" has quietly made every repository, every contributor and every workflow file in that org part of its production trust boundary. Ours binds trust to named repositories, and the deployer identity it grants is not an administrator — it can push images and deploy services, and that's the list. Getting this right took an actual audit pass (documented pain: reviewing bindings is tedious exactly because each one is small), and the standing rule from the secrets entry applies in full: prefer credentials that expire over credentials you must remember to revoke.

Merge is the deploy button

On top of that foundation, the pipeline itself is deliberately boring: merging to a service's main branch builds its container, pushes it to the artifact registry, and rolls the service to the new revision. One repository per service keeps the blast radius legible — the pull request that changed a service is the pull request that deployed it. There are no snowflake deploy scripts on anyone's laptop with elevated credentials; the human's job ends at code review, and the robot's authority is bounded by the federation conditions above.

The retention policy that ate a live image

The war story, because infrastructure entries owe you one. To keep the artifact registry from accumulating every image ever built, a cleanup policy deleted old artifacts. Sensible. Except: "old" was defined by upload time, and one service that hadn't shipped in a while was still running an image the policy considered elderly. The registry deleted it out from under a live service — which kept running (the platform holds its own copy of a deployed image) right up until the next restart tried to pull, and couldn't. The immediate fix was rebuilding the same tag; the durable fixes were policy conditions that respect recency of use, not just age, and treating a mutable tag referenced by production as a thing that must never be garbage-collectable. The general lesson: every automated cleanup is a deletion bot pointed at your infrastructure — prove to yourself it can't reach anything live.

Humans federate too

The same principle governs the human side. Local development uses personal cloud identities with short-lived tokens minted by the CLI — never a shared "dev service account" file passed around like a gym key. It costs an occasional re-login (and yes, an expired local token has eaten a deploy afternoon here — the error messages are not their best work), but it means access reviews are about people, revocation is per-person and instant, and there is no credential file whose blast radius is "everyone". It also keeps the audit trail honest: every action in the cloud console traces to a named identity, not to a robot four people share. If a workflow can't survive credentials that expire, the workflow is the bug.

The checklist

None of this required a security team — just an afternoon of setup and the willingness to delete the JSON file that made everything "easy". The keyless version is also easy. It's just easy without the part where a leak is forever.

Want in on a future wave?

Drop your email and we'll let you know when Players Engine opens up. Early supporters get first access.

← All devlog entries