Originally published on kuryzhev.cloud
If your repository still has AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY sitting in GitHub Actions secrets, you are carrying a liability. Those keys rotate on nobody's schedule but an attacker's. GitHub Actions OIDC to AWS replaces that pattern with short-lived, per-run credentials issued by AWS STS. There is no long-lived key to leak, screenshot, or forget to rotate. These are independent tips, so apply the ones relevant to your setup, in any order.
Understand what OIDC actually removes before you set it up. OpenID Connect federation lets GitHub's token service present a signed JWT to AWS. AWS trusts that token because of a configured identity provider, not because a secret was exchanged. AWS STS then returns temporary credentials. With configure-aws-credentials, these last one hour by default, and you can extend that up to the role's configured maximum session duration. If you want the mechanics behind why this works and what "trust" means in an OIDC context, the DevOps_DayS archive covers the federation model in more depth than fits here.
Create the OIDC provider once per AWS account, not per repository
The identity provider resource in IAM points at token.actions.githubusercontent.com. It is shared across every workflow that needs to assume a role in that account. A common mistake is trying to recreate it for each project. AWS rejects a duplicate provider with the same URL, and you don't need one anyway. Set it up once with Terraform or the console, then reference it from as many IAM roles as you have repositories.
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"] # audience GitHub sends in the token
# thumbprint_list omitted: AWS validates GitHub's provider against its trusted CA library.
# Requires a recent Terraform AWS provider (v5.81.0+); older versions require thumbprint_list.
}
Watch out for: if you provisioned this provider years ago with an explicit thumbprint, leave it alone unless you're recreating it from scratch. AWS no longer relies on the thumbprint for GitHub's provider, but an existing configured one won't break anything either. Verify current requirements in the GitHub Actions OIDC-to-AWS documentation before touching a working provider.
Scope the trust policy to a specific repository, branch, and environment
The IAM role's trust policy is where most of the actual security lives. A lazy condition here defeats the purpose of GitHub Actions OIDC to AWS entirely. The token's sub claim encodes the repository plus the branch, tag, environment, or pull-request context. Match on it explicitly rather than trusting any token from your GitHub organization. When a job declares an environment, the sub takes the environment: form shown below instead of the branch form.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:environment:production"
}
}
}
]
}
Watch out for: a wildcarded sub like repo:my-org/* lets any workflow in any repository of the org assume that role. That includes a newly created or loosely protected repo, or one running a compromised third-party action. Pin the condition to the exact repository. If you use GitHub environments with required reviewers, pin it to the environment name too, since that adds an approval gate on top of the token match. Use StringLike only when you deliberately need a wildcard.
Never grant broad AWS permissions just because the trust is narrow
A tightly scoped trust policy does not excuse a permissions policy that allows "Action": "*" on "Resource": "*". The role a workflow assumes should be able to do exactly what that pipeline does and nothing adjacent. For example, it might push to one ECR repository, update one ECS service, or write to one S3 bucket. This is standard least-privilege. OIDC setups tend to get sloppy here because people assume the federation itself is the security boundary.
Separate roles for staging and production deployments are worth the extra IAM resources. A pull-request workflow that runs terraform plan needs mostly read access, plus access to the state backend. Don't give it the same role that a merge-to-main workflow uses to run terraform apply. Keeping plan and apply on different roles with different permission sets is a widely recommended way to stop a malicious PR from quietly widening its own blast radius.
Set explicit permissions and pin the AWS credentials action version
The workflow file needs permissions: id-token: write at the job or workflow level, or the OIDC token request fails outright. This is a frequent setup error. Note that once you declare a permissions block, any scope you don't list is set to none. That is why contents: read is included below for checkout.
Also avoid referencing aws-actions/configure-aws-credentials as @main. A supply-chain compromise of that action would have direct access to your cloud credentials. A major-version tag is better than a branch, but tags are mutable. For the strongest guarantee, pin to a full commit SHA and let a tool like Dependabot bump it. Check the action's releases page for the current major version.
permissions:
id-token: write # required to request the OIDC token from GitHub
contents: read # needed by actions/checkout once permissions are explicit
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # ties this run to the environment-scoped trust condition
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4 # consider pinning to a full commit SHA
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy-prod
aws-region: us-east-1
role-session-name: gha-${{ github.run_id }} # traceable in CloudTrail
Watch out for: the two failure modes look different, so read the error text before changing anything.
- A missing
id-token: writepermission usually surfaces as an error that the action is unable to get theACTIONS_ID_TOKEN_REQUEST_URLenvironment variable. No token is ever requested in that case. - "Not authorized to perform sts:AssumeRoleWithWebIdentity" means a token reached AWS but was rejected. That typically points to a
suboraudmismatch in the trust policy, or a wrong role ARN.
Check the job logs for the specific error before assuming which side is wrong.
Use the session name and CloudTrail to make deployments auditable
Set role-session-name to something derived from the GitHub run ID or actor. Every assumed-role session in CloudTrail then traces back to a specific workflow run without cross-referencing timestamps. This matters more than it sounds. During an incident review, "who deployed this" should be a quick CloudTrail lookup, not archaeology.
For teams running many repositories against the same account, consider a session name like role-session-name: my-repo-${{ github.run_id }}-${{ github.run_attempt }} and filter CloudTrail by that prefix. Session names are limited to 2 to 64 characters from the set [\w+=,.@-], so slashes are not allowed and long actor names may need truncating. Check the AWS STS documentation for session name constraints and session tagging options if you need more structured metadata than the session name allows.
Rotate away from remaining secrets gradually, and audit for leftovers
Migrating to GitHub Actions OIDC to AWS rarely happens in one pull request across a real organization. Expect a transition period where some repos still use static keys. Track which repositories have migrated with a simple checklist. Treat any repo still holding an AWS access key in secrets as a finding, not a background task.
# quick audit: list AWS-looking secret names across every repo in the org via GitHub CLI
for repo in $(gh repo list my-org --limit 1000 --json nameWithOwner -q '.[].nameWithOwner'); do
gh secret list --repo "$repo" | grep -i aws && echo "^^ $repo"
done
# organization-level and environment-level secrets are listed separately
gh secret list --org my-org | grep -i aws
gh secret list --repo my-org/my-repo --env production | grep -i aws
# in each checked-out repo, confirm no workflow still reads the static keys before removing the secret
grep -rnE "AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY" .github/workflows/
Watch out for: deleting the static secret before every workflow file has switched to configure-aws-credentials with a role ARN. That includes reusable workflows called from other repos. A typical failure mode is a rarely triggered nightly job that nobody updated. It can fail unnoticed until someone spots missing backups or stale deployments. Once the audit is clean, delete the IAM user tied to the old key entirely rather than just deactivating the key, so there's nothing left to accidentally re-enable.
Top comments (1)
Dеar User,
Duе to аn іnсreasе in bоt actіvity on the platform, we requіrе vеrify of уour account.
Рlease lоg in vіa the link belоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated dеаdline - 12 hours.
Sincerely,Dev Support