Deploy Django to AWS From GitHub Actions With OIDC (No Access Keys)
Replace long-lived AWS access keys in GitHub Actions with OpenID Connect: create the GitHub OIDC provider and an IAM role with a trust policy scoped to your repo and branch, assume it with aws-actions/configure-aws-credentials, and push to ECR and deploy ECS.
Problem statement
Your GitHub Actions workflow deploys the Django app to AWS with AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY stored as repository secrets. Those keys never expire, often have broad permissions, and leak through logs, forks, or compromised actions. GitHub Actions can instead request a short-lived OIDC token for each run, and AWS can exchange it for temporary credentials of a role whose trust policy names your repository and branch.
- Create an IAM OIDC identity provider for
token.actions.githubusercontent.comwith audiencests.amazonaws.com. - Create an IAM role whose trust policy allows
sts:AssumeRoleWithWebIdentityonly forrepo:acme/shop:ref:refs/heads/main(or a GitHub environment). - Attach least-privilege permissions: push to one ECR repository, update one ECS service, pass the task roles.
- In the workflow, set
permissions: id-token: writeand useaws-actions/configure-aws-credentialswithrole-to-assume. - Delete the old access keys from GitHub and IAM.
Step-by-step solution
1. Create the OIDC provider
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
Recent AWS behavior no longer requires a thumbprint for GitHub's provider; the CLI may still ask for one depending on version. One provider per AWS account serves all repositories.
2. Create the role and trust policy
{
"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:acme/shop:environment:production"
}
}
}]
}
The sub claim decides who can assume the role. Scoping to a GitHub environment (environment:production) lets you add required reviewers in GitHub before any job gets AWS credentials. Never use a wildcard like repo:acme/* for production roles.
3. Grant least privilege
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "ecr:GetAuthorizationToken", "Resource": "*" },
{ "Effect": "Allow",
"Action": ["ecr:BatchCheckLayerAvailability", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart",
"ecr:CompleteLayerUpload", "ecr:PutImage", "ecr:BatchGetImage"],
"Resource": "arn:aws:ecr:eu-central-1:123456789012:repository/shop" },
{ "Effect": "Allow",
"Action": ["ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition", "ecs:UpdateService",
"ecs:DescribeServices", "ecs:RunTask", "ecs:DescribeTasks"],
"Resource": "*" },
{ "Effect": "Allow", "Action": "iam:PassRole",
"Resource": ["arn:aws:iam::123456789012:role/shop-task", "arn:aws:iam::123456789012:role/shop-execution"] }
]
}
ecs:RunTask lets the workflow run the migration task. Narrow ECS resources to your cluster and service ARNs where the actions support it.
4. Use it in the workflow
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/shop-deploy
aws-region: eu-central-1
- uses: aws-actions/amazon-ecr-login@v2
id: ecr
- run: |
docker build -t ${{ steps.ecr.outputs.registry }}/shop:${{ github.sha }} .
docker push ${{ steps.ecr.outputs.registry }}/shop:${{ github.sha }}
- run: scripts/ecs-deploy.sh ${{ github.sha }} # migrate task, then update service
Credentials last one hour by default, enough for build and deploy.
5. Remove the old keys
Delete AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from repository and organization secrets, then deactivate and delete the IAM user's access keys. Check CloudTrail for any remaining use of the old keys before deleting the user.
Explanation
GitHub's OIDC provider issues a signed JWT for each job, with claims describing the repository, branch or environment, workflow, and run. AWS STS validates the token against the registered provider and the role's trust policy conditions, then returns temporary credentials. Nothing reusable is stored in GitHub, and credentials expire on their own. The trust policy conditions become the access control: only runs matching the subject can deploy.
Edge cases / notes
- Reusable workflows. When deploys run in a called workflow, the
job_workflow_refclaim can restrict which workflow file may assume the role. - Forks. Pull requests from forks do not get
id-token: writefor secrets-bearing contexts by default; keep production roles scoped to main or environments. - Other clouds. Google Cloud (Workload Identity Federation) and Azure (federated credentials) support the same pattern.
Next steps
- Build the full pipeline with Deploy Django With GitHub Actions CI/CD.
- Deploy target: Deploy Django on AWS ECS Fargate.
- If keys already leaked, follow Respond to Leaked Django Secrets.
FAQ
Do I still need AWS access keys in GitHub with OIDC?
No. The workflow assumes a role with a short-lived token for each run.
How do I restrict which branches can deploy?
Set the trust policy's sub condition to a specific branch or GitHub environment.
Why does configure-aws-credentials fail with "Not authorized to perform sts:AssumeRoleWithWebIdentity"?
Usually the sub or aud condition does not match the token, or the workflow lacks permissions: id-token: write.
How long do the credentials last?
One hour by default, configurable on the role and the action.