How Senior Engineers Deal with Cloud Credentials in CI/CD
4 min read

For a long time, this looked completely normal to me:
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Create an IAM user. Generate an access key. Put it into GitHub Secrets. Let the pipeline use it to deploy.
It works.
And that's partly the problem.
Because when something works, you don't always question whether there's a better way to do it.
As I started building more production-style infrastructure, I realised I was solving authentication in the wrong way.
My CI/CD pipeline didn't need a secret.
It needed an identity.
Stage 1: Just Put the Credentials in Secrets
My initial mental model was simple:

The credentials weren't committed to Git, so everything seemed fine.
And to be clear, using a secret store is significantly better than hardcoding credentials into your repository.
But there's still a problem.
Those credentials exist.
They're long-lived credentials that remain valid until they're rotated, revoked or deleted.
If they leak, an attacker doesn't necessarily need access to my GitHub Actions workflow anymore.
They have the keys.
That's when I started asking a different question:
Why does my pipeline need a permanent password to AWS in the first place?
Stage 2: IAM Roles and Service Principals
The next step was understanding cloud identities properly.
In AWS, I started thinking more about IAM roles.
In Azure, I encountered the same concept through identities and service principals.
The terminology changes between cloud providers, but the principle is similar:
Give workloads an identity and give that identity only the permissions it needs.
For example, my Terraform pipeline might need permission to provision infrastructure.
My Docker pipeline might need permission to push images to ECR or ACR.
Neither pipeline needs to be an administrator.
This introduced another important concept into how I design CI/CD:
least privilege.
But there was still another question.
How does GitHub prove that it's allowed to assume that identity?
That's where OIDC completely changed my mental model.
Stage 3: OIDC
OIDC, OpenID Connect, allows my CI/CD platform and cloud provider to establish trust without me storing long-lived cloud credentials in GitHub.
Instead of this:

I can have something closer to:

GitHub can essentially prove:
"This workflow is running from this repository, on this branch, under these conditions."
AWS or Azure can then verify that identity against a trust configuration I've defined.
If everything matches, the pipeline receives temporary credentials.
Those credentials expire.
There is no permanent AWS access key sitting inside GitHub Secrets waiting to be stolen.
The Difference Is Subtle but Important
Previously, I thought:
"How do I safely give GitHub my AWS credentials?"
Now I think:
"How does GitHub prove who it is to AWS?"
Those are two very different security models.
One revolves around protecting a secret.
The other revolves around establishing trust between identities.
That's the mental model I now use whenever I'm designing authentication between systems.
Don't ask what credentials you can give something.
Ask whether it can authenticate using an identity instead.
What This Looks Like in GitHub Actions
For AWS, a workflow can request an OIDC token:
permissions:
id-token: write
contents: readThen assume an IAM role:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: eu-west-2Notice what's missing.
No:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEYstored in GitHub.
AWS provides temporary credentials after GitHub successfully authenticates.
The IAM role's trust policy can then restrict which repository, branch or environment is allowed to assume it.
The same general idea applies when I work with Azure.
Different implementation.
Same mental model.
OIDC Doesn't Remove the Need for Security
It's important not to treat OIDC as some magic security checkbox.
If my GitHub Actions role has:
AdministratorAccess
I've still designed a bad permission model.
If any repository can assume my production role, I've created another problem.
If anyone can modify the workflow that deploys production without review, OIDC isn't going to save me.
Authentication answers:
Who are you?
Authorization still needs to answer:
What are you allowed to do?
So I combine OIDC with things like:
- least-privilege IAM/RBAC permissions
- protected branches
- GitHub Environments for production
- required approvals
- restricted OIDC trust conditions
- short-lived credentials
OIDC isn't the entire security model.
It's one piece of it.
Secrets Aren't Bad
I also don't think the lesson here is:
"Never use GitHub Secrets."
Some systems still require API keys, tokens or passwords.
Sometimes there simply isn't an identity federation mechanism available.
The lesson I've taken from it is much simpler:
Don't create a long-lived secret when you don't need one.
If GitHub Actions can authenticate to AWS or Azure using OIDC, I'd rather establish that trust relationship than generate another credential that I'll need to store, protect, rotate and eventually revoke.
And that's why my CI/CD pipelines no longer start with:
"What secret do I need to put into GitHub?"
They start with:
"How can these two systems trust each other without one?"