No More Static Keys: How We Secured AWS Access With OIDC and IAM Identity Center
No More Static Keys: How We Secured AWS Access With OIDC and IAM Identity Center
Static AWS access keys are a liability. We had them in GitHub secrets, attached to an IAM user with too many permissions, and we knew it was wrong. This week we fixed it.
Here’s exactly what we changed, why it matters, and how you can do the same.
What We Had Before
Our GitHub Actions workflows were authenticating to AWS using a classic IAM user — github-actions-aws-deployer — with static AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY stored as GitHub repository secrets.
This is the default path most tutorials show. It’s also a security pattern with real problems:
- Long-lived credentials — static keys don’t expire. If they leak, they’re valid until manually rotated.
- Keys in secrets storage — GitHub secrets are secure, but they’re a target. Any workflow that logs environment variables, or a compromised third-party Action, can expose them.
- Over-permissioned user — the IAM user had permissions scoped for CI/CD, but still wider than any individual workflow needed.
- No audit trail linking keys to specific workflow runs — you can see the key was used, not which run used it.
We had this exact setup. It worked. It also failed its first real test: when GitHub Actions workflows started throwing 403 errors, we couldn’t tell if it was a permissions problem, a key rotation problem, or something else. We had to hunt.
What We Changed
Two things, done in parallel:
1. GitHub Actions → OIDC (No More Static Keys)
OpenID Connect (OIDC) lets GitHub Actions exchange a short-lived JWT — generated fresh for each workflow run — for temporary AWS credentials. No static keys stored anywhere. Credentials expire when the run ends.
The setup: create an OIDC identity provider in AWS IAM, create an IAM role that trusts it, and scope the trust to your specific repo and branch.
Our Terraform for the OIDC provider:
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
The IAM role trust policy — scoped to our repo only:
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:base-bit/alienbraintrust-private:*"
}
}
In the workflow, replace the static key auth step with:
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::YOUR_ACCOUNT_ID:role/github-actions-deployer
aws-region: us-east-1
Add permissions: id-token: write to the job. That’s it. Delete the old secrets.
What this buys you: credentials that can’t leak because they don’t exist until the workflow runs, and expire the moment it ends. If a workflow run is compromised, the blast radius is that run only.
2. Local Access → IAM Identity Center (AWS SSO)
For local Terraform runs, we previously used another IAM user — terraform-deployer — with long-lived access keys stored in ~/.aws/credentials.
We replaced that with AWS IAM Identity Center (formerly AWS SSO). The setup:
- Created an Identity Center instance
- Created a
TerraformDeployerpermission set with the same policy as the old user - Assigned it to our user account
Now local access works like this:
aws sso login --profile deployer
You get a browser prompt, authenticate, and receive short-lived credentials (default: 8 hours). No long-lived keys on disk. When the session expires, you re-auth. No rotation needed because there’s nothing to rotate.
The IAM Policy Cleanup
Moving to OIDC was also an opportunity to audit what permissions we actually needed.
The old user had a policy that had grown organically — permissions added when needed, rarely removed. We wrote a clean policy from scratch, scoped to exactly what our Terraform and GitHub Actions workflows actually touch:
- EC2, VPC, security groups, EBS (for the test-bot instance)
- S3 (Terraform state + scripts bucket)
- IAM (creating roles for EC2 instances)
- SSM (Parameter Store read/write for secrets, Session Manager for instance access)
- KMS (encrypting SSM parameters)
- CloudWatch, SNS, DLM (monitoring and snapshots)
- OIDC provider management (so Terraform can manage its own auth setup)
Everything scoped by resource ARN where possible. No * resources except where AWS requires it for Describe-type calls.
We also removed stale permissions — the old policy still had entries for a Paperclip infrastructure we decommissioned. Dead permissions are attack surface. We cut them.
What We Deleted
After the migration was confirmed working:
- Deleted the
github-actions-aws-deployerIAM user entirely - Deleted both static key secrets from GitHub (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - Rotated and cleaned up stale PATs on our bot account
The IAM user no longer exists. There’s nothing to rotate, nothing to leak, nothing to audit for usage.
Why This Matters for AI Infrastructure
If you’re running AI tooling on AWS — EC2 instances for agents, S3 for scripts and artifacts, SSM for secrets — you almost certainly have credentials somewhere that could be tightened.
The pattern we see in a lot of early-stage AI infrastructure: move fast, use the default auth approach (static keys), add permissions when things break. That’s fine to get started. It’s not fine to leave in place.
The specific risks for AI workloads:
Agents with AWS access need tightly scoped credentials. Our test-bot EC2 instance has an IAM instance profile. That profile can only read from specific SSM parameter paths and a specific S3 bucket. If the bot is compromised, the blast radius is contained.
CI/CD pipelines deploying AI infrastructure are high-value targets. A compromised pipeline that deploys your infrastructure can modify what your agents have access to. OIDC scoping means a compromised run can’t do anything a human reviewer didn’t approve via the role’s permissions.
Secrets in Parameter Store beat secrets in code or environment. Our agents read API keys from SSM at runtime — Anthropic key, GitHub PAT, Telegram token. They’re encrypted at rest, access is logged in CloudTrail, and rotating them doesn’t require redeploying anything.
The Sequence That Works
If you’re starting from static keys and want to get to this state:
- Create the OIDC provider and IAM role (Terraform it so it’s reproducible)
- Update one workflow to use OIDC auth — test it, confirm it works
- Update all workflows to use OIDC
- Delete the old IAM user and GitHub secrets — don’t leave them as fallback
- Set up IAM Identity Center for local access if you haven’t already
- Audit the permission policy while you’re in there — remove anything unused
Do it in that order. Step 4 is the one people skip, leaving the old credentials active “just in case.” That’s not a migration, that’s running two auth systems simultaneously.
The Takeaway
Static AWS access keys are the password123 of cloud infrastructure — everyone knows they’re a problem, most people have them anyway because they work.
OIDC and IAM Identity Center aren’t harder to set up than the old approach once you’ve done it once. They’re just less familiar. The Terraform for the OIDC provider is about 40 lines. The workflow change is three lines. The security improvement is substantial.
If you’re running any AI infrastructure on AWS, this is the week to do it.
Comments