Skip to content

Risk of Long Lived Cloud Secrets


Back in previous section, every AWS deployment you built — the S3 static site push, the ECR image push, the EC2 and ECS rollouts — authenticated the same way: an IAM user’s access key and secret key, stored as repository secrets, handed to aws-actions/configure-aws-credentials at the start of the job. It worked. It’s also, deliberately, the version of this course that showed you “how most people start.” Now it’s time to look honestly at what that approach costs you.

What You Actually Built, And Its Blind Spot

Recall the shape of those workflows: an IAM user existed in your AWS account with a policy attached, its access key and secret key sat in your repository’s secrets, and every deploy job pulled those two strings out and used them to authenticate. That’s a real, working credential, generated once and valid indefinitely — AWS has no idea whether the request using it came from this week’s workflow run or a copy of that key sitting on someone’s laptop from eight months ago.

Three specific problems fall out of that:

A leaked key works forever, until a human notices and rotates it. If that access key ever ends up somewhere it shouldn’t — pasted into a public gist, logged accidentally, extracted from a compromised runner — it keeps working. Not until the workflow finishes, not until the day ends: until someone manually goes into IAM and deactivates it. There’s no natural expiry acting as a safety net.

The same keys sprawl across every repository that needs AWS access. The practical back in previous section: S3 deploy and ECR push likely didn’t share a single IAM user with a broad policy — but even if you scoped them separately, you now have multiple long-lived credential pairs to track, each one a separate thing that can leak, each one needing its own rotation discipline. The more repos and services you add, the more copies of “a permanent password to your AWS account” exist somewhere.

There’s no way to tie a specific AWS action back to a specific workflow run. CloudTrail will show you that IAM user github-deploy-user called s3:PutObject — but not which workflow run, which commit, or which actor triggered it. If you’re trying to reconstruct what happened after something goes wrong, an access key gives you a name, not a story.

    flowchart LR
    Key["Long-lived access key<br/>(stored as repo secret)"]
    Key --> Leak["Leaked once →<br/>valid until manually rotated"]
    Key --> Sprawl["Same pattern repeated<br/>across every repo"]
    Key --> Audit["CloudTrail shows the IAM user,<br/>not the workflow run"]
  

None of this means that approach was all wrong to show you — it’s genuinely how most teams’ AWS integrations start, and understanding it is what makes the alternative make sense. But it’s worth sitting with the fact that every deploy job you’ve built so far has been resting on a secret that, once created, never expires on its own.

What You Actually Want Instead

The properties you’re after are the opposite of what a static key gives you:

  • Short-lived. A credential that’s valid for a matter of minutes, not indefinitely.
  • Scoped to a single workflow run. Nothing left over for a leaked log or a compromised runner to reuse tomorrow.
  • Traceable back to its source. AWS should be able to tell you exactly which repository, which branch, and which workflow requested access.
  • Nothing sitting in Settings → Secrets to leak in the first place. No access key, no secret key, stored anywhere.

That combination is exactly what OpenID Connect (OIDC) gives you. Instead of your workflow presenting a pre-issued password, GitHub Actions issues a short-lived, cryptographically signed identity token for that specific run — proving “I am workflow run #4821 on branch main in repo you/your-repo” — and AWS is configured to trust tokens signed by GitHub and exchange them for temporary credentials, scoped exactly as narrowly as you define.

    sequenceDiagram
    participant Job as Workflow run
    participant GH as GitHub OIDC provider
    participant AWS as AWS STS

    Job->>GH: Request identity token for this run
    GH->>Job: Signed, short-lived OIDC token
    Job->>AWS: Present token, request role assumption
    AWS->>AWS: Verify signature + trust policy conditions
    AWS->>Job: Temporary credentials (minutes, not forever)
  

No access key ever exists in a secrets vault, because no access key exists at all. The credential AWS hands back is valid for the duration of the job and nothing more, and the trust policy on the AWS side can restrict it down to “only this repository, only this branch” — which directly answers all three problems above. A leaked runner log doesn’t hand anyone a permanent credential, because whatever token it contained is already useless by the time anyone reads it. There’s one trust relationship to manage per role instead of a key pair per repository. And every credential AWS issued is tied, by the trust policy’s own logic, to a specific workflow run it can point back to.

What’s Ahead

This chapter is deliberately just the argument — the next two chapters do the actual work. The next chapter sets up the AWS side: registering GitHub as an OIDC identity provider and creating an IAM role with a trust policy scoped to your repository, plus a minimal permissions policy limited to the exact S3 bucket and ECR repository previous section used. And the chapter after that then goes back into the actual deploy workflow, strips out the stored access-key secrets, and switches it over to assuming that role — ending with you deleting the old IAM user’s keys for good.

Note

If you’re wondering whether this is overkill for a personal project with a single deploy workflow — the setup cost is genuinely small (one IAM role, one trust policy), and it removes an entire category of “did I just leak a permanent credential” anxiety permanently. It scales the same way whether you have one workflow or fifty.

Last updated on