~/blog/zero-long-lived-keys-github-actions-oidc
Zero long-lived cloud keys: GitHub Actions → AWS with OIDC
How to delete every AWS access key from your CI and your laptop, replace them with short-lived roles, and make the blast radius of a leaked secret exactly zero.
On this page
In part one the drift job assumed an AWS role instead of using a stored key. This post is about why that one line is the most important security decision in a small company's cloud, and how to make it the only option.
The problem with keys
An access key is a password that never expires, lives in at least three places (a secrets store, a developer laptop, and wherever it was pasted once in a hurry), and grants whatever the attached policy says, forever. Rotating it is a project. Auditing who used it is guesswork because the key is the identity.
Every "leaked AWS key" incident you've read about is this.
What OIDC changes
GitHub Actions can mint a short-lived OpenID Connect token for a workflow run. The token says, cryptographically, "this is repo org/infra-ops, branch main, workflow apply, environment prod". AWS IAM can be told to trust that issuer and exchange the token for temporary credentials, scoped to a role, valid for an hour.
No stored key. Nothing to rotate. The identity is the run, and CloudTrail records it as such.
The trust policy is the whole security model
data "aws_iam_policy_document" "gha_trust" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
# only this repo, only the prod environment
values = ["repo:nikhilsahu07/infra-ops:environment:prod"]
}
}
}Read the sub condition slowly, because that is where people get it wrong:
repo:ORG/REPO:*lets any branch, PR or fork-PR workflow assume the role. A malicious PR can then runterraform destroy.repo:ORG/REPO:ref:refs/heads/mainrestricts to pushes on main.repo:ORG/REPO:environment:prodrestricts to jobs that passed GitHub's environment protection rules, i.e. the manual approval gate. This is the one I use for apply roles.
Two roles per environment: gha-plan-<env> (read-only, used by PR plans and the drift job) and gha-apply-<env> (write, environment-gated). Plan roles can be broad; apply roles are the thing you spend an afternoon tightening.
Then delete the keys
This is the step teams skip. OIDC in CI doesn't help if the old keys still work.
- List every IAM user with access keys. Find out what uses them (CloudTrail
LastUsedis your friend). - Move CI to OIDC. Move humans to IAM Identity Center (SSO) with short-lived sessions, or at minimum to
aws sso login. - Move applications to instance roles / task roles; anything that needs a secret reads it from SSM Parameter Store or Vault at runtime.
- Deactivate keys for a week. Delete them.
On the infra-ops: one Terraform + Ansible platform for dev, staging and prod platform this took two weeks of archaeology and one afternoon of actual change, and it is the reason the résumé line says "zero long-lived credentials" rather than "we rotate keys quarterly".
http_tokens = "required"). Instance roles are only as safe as the metadata endpoint that serves them.
What it feels like afterwards
- A leaked repository secret leaks nothing, because there isn't one.
- CloudTrail shows
gha-apply-prodas the actor with the workflow run id in the session name. "Who changed this?" becomes a link. - New environments are a Terraform module call; no human ever handles a credential.
The same identity pattern extends to Kubernetes (IRSA, or Pod Identity) when the services move there, which is the next phase of the platform. The observability side, where traces also carry identity, is covered in the lgtm-sidecar: a resource-capped observability stack for small teams write-up.
Series · Infra that doesn't drift · 2/2
All parts- 01Terraform drift detection that opens GitHub issues
- 02Zero long-lived cloud keys: GitHub Actions → AWS with OIDC
Linked from
Keep reading
Monthly notes
Get the next one in your inbox.
One email a month on infrastructure, backend design and applied AI. No noise, unsubscribe any time.
Comments (0)
name + comment · no login