Many development teams use GitHub Actions to build, test, and deploy applications to AWS. A common approach is to create an AWS access key and store it as a GitHub Actions secret. The deployment workflow then uses that key to authenticate with AWS.
This approach works, but it creates a security risk. Long-lived access keys can remain valid for months or years if they are not rotated or removed. If a key is exposed through a compromised account, an unsafe workflow, or an accidental log entry, someone may be able to use it outside the intended deployment process.
AWS provides a better option: OpenID Connect (OIDC).
With OIDC, GitHub Actions can request temporary AWS credentials for an authorized workflow. You do not need to store a permanent AWS access key in your repository secrets.
In this article, you will learn how GitHub Actions authenticates with AWS through OIDC, how to configure the required IAM resources, how to write a deployment workflow, and how to avoid common security mistakes.
Why You Should Avoid Long-Lived AWS Access Keys
An AWS access key typically consists of an access key ID and a secret access key. Applications and automation tools use these credentials to sign AWS API requests.
If you store a long-lived key in GitHub Actions secrets, the secret is protected from ordinary direct access in the repository interface. However, a workflow that is allowed to use the secret may still expose it accidentally or misuse its permissions.
For example, an unsafe workflow could print environment variables, execute an untrusted script, or run code from a pull request in a context where deployment credentials are available.
Long-lived credentials create several problems:
They remain valid until they are rotated, deactivated, or deleted.
They require secure storage and periodic rotation.
They can be reused outside the intended workflow.
A compromised key may provide access for longer than necessary.
Multiple repositories or workflows may end up sharing the same key.
Temporary credentials reduce this exposure because they expire automatically. OIDC also lets AWS verify which GitHub repository, branch, tag, or environment is requesting access.
Temporary credentials are not automatically safe, though. Their permissions and trust policy must still be configured correctly.
What Is OpenID Connect in GitHub Actions?
OpenID Connect is an identity protocol that allows one system to prove its identity to another using signed tokens.
GitHub Actions can act as an OIDC identity provider. During a workflow run, GitHub can issue an identity token containing claims about the workflow's execution context.
AWS Security Token Service (AWS STS) can exchange an accepted GitHub OIDC token for temporary AWS credentials when the IAM trust policy permits it.
The process looks like this:
GitHub Actions Workflow
|
v
Request GitHub OIDC Token
|
v
AWS IAM Trust Policy Validation
|
v
AWS STS AssumeRoleWithWebIdentity
|
v
Temporary AWS Credentials
|
v
Deploy Application to AWS
The main benefit is that GitHub Actions does not need a stored long-lived AWS access key. AWS issues temporary credentials only when the identity token satisfies the configured trust policy.
Step 1: Create an OIDC Identity Provider in AWS
First, configure AWS to trust GitHub's OIDC identity provider.
In the AWS Management Console, open IAM and locate the identity providers section. Add an OpenID Connect provider using these values:
The provider URL identifies GitHub's OIDC issuer. The audience indicates that the token is intended for AWS STS.
If the GitHub OIDC provider already exists in your AWS account, you generally do not need to create another one.
You can also configure IAM resources through infrastructure-as-code tools, such as AWS CloudFormation or Terraform, so the setup can be reviewed and managed consistently.
Step 2: Create an IAM Role for GitHub Actions
Next, create an IAM role that GitHub Actions can assume.
The role has two important parts:
A trust policy that determines which GitHub identities can assume the role.
A permissions policy that determines what the role can do in AWS.
Keeping these policies separate makes the security model easier to understand and maintain.
Configure the Trust Policy
Suppose the repository is named my-company/my-dotnet-app, and deployment should run only from its main branch.
A trust policy can restrict access to that repository and branch:
{
"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-company/my-dotnet-app:ref:refs/heads/main"
}
}
}
]
}
Replace 123456789012 with your AWS account ID and replace the repository name with your actual GitHub owner and repository.
The aud condition restricts the intended audience to AWS STS.
The sub condition restricts the role to the specified repository and branch. This prevents unrelated repositories and branches from assuming the role through the same trust relationship.
For production deployments, consider using a dedicated GitHub environment and restricting the trust policy to that environment. This allows you to combine AWS-side identity restrictions with GitHub environment protection rules.
Avoid using broad patterns that permit every repository or branch in an organization unless that access is explicitly required and carefully controlled.
Step 3: Give the Role Only the Permissions It Needs
The trust policy controls who can assume the role. It does not control which AWS resources the role can access after authentication.
For that, you need a permissions policy.
Suppose your deployment uploads static website files to a specific Amazon S3 bucket. A narrowly scoped policy could look like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-production-site/*"
}
]
}
This example allows object uploads and deletions inside the specified bucket path. It does not grant permission to administer all S3 buckets.
Depending on your deployment process, you may also need permissions such as s3:ListBucket, CloudFront invalidation permissions, or permissions for another AWS service.
Grant only the actions that the workflow actually requires. Be particularly careful with deletion permissions because a deployment command can remove production files if its target path or synchronization settings are incorrect.
The example above is for an S3-based deployment. Deployments to services such as Amazon ECS, AWS Lambda, or Elastic Beanstalk need different permissions.
Step 4: Configure GitHub Actions Permissions
GitHub Actions workflows must be allowed to request an OIDC token.
Add the following permission to the workflow:
permissions:
id-token: write
contents: read
The id-token: write permission allows the workflow to request an OIDC token.
The contents: read permission allows the workflow to check out the repository when needed.
These permissions do not automatically grant AWS access. AWS still validates the token against the IAM role's trust policy, and the role's permissions policy determines what the workflow can do after assuming the role.
Set permissions at the workflow or job level and grant no additional permissions unless they are required.
Step 5: Deploy to AWS Using GitHub Actions
The following example demonstrates a workflow that builds a .NET application and uploads a directory of static files to Amazon S3.
It uses the official AWS credentials action to exchange the GitHub OIDC token for temporary AWS credentials.
name: Deploy to AWS
on:
push:
branches:
- main
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build application
run: dotnet build --configuration Release --no-restore
- name: Run tests
run: dotnet test --configuration Release --no-build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: us-east-1
- name: Upload website files
run: |
aws s3 sync ./publish s3://my-production-site \
--delete
This workflow illustrates the authentication and deployment sequence, but the final upload step assumes that ./publish contains the static files you intend to deploy. A normal .NET application build does not necessarily produce that directory.
For an ASP.NET Core application, publish the project before uploading its output:
- name: Publish application
run: dotnet publish ./MyApp/MyApp.csproj \
--configuration Release \
--output ./publish
Use the actual project path and configure the deployment command for the AWS service hosting your application. An ASP.NET Core application deployed to ECS or Lambda needs a different packaging and deployment process than a static website hosted on S3.
Also, pin third-party actions to reviewed commit SHAs in security-sensitive production workflows. Version tags are convenient, but a reviewed commit SHA provides a stronger guarantee about the exact action code being executed.
What Happens During Authentication?
When the credentials action runs, it requests an OIDC token from GitHub and uses it to call AWS STS.
AWS validates the token and checks whether the requested role's trust policy permits that identity.
If the request is accepted, AWS returns temporary credentials. The AWS CLI can then use those credentials to perform the deployment operations allowed by the role's permissions policy.
The workflow does not need AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY stored as permanent GitHub secrets.
Step 6: Restrict Production Deployments with GitHub Environments
Branch restrictions are useful, but production deployments may need additional controls.
GitHub environments can require approvals, restrict which branches can deploy, and manage environment-specific configuration.
For example, add an environment to the deployment job:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
steps:
- name: Check out repository
uses: actions/checkout@v4
Configure the production environment in GitHub with the required deployment protection rules.
If the AWS trust policy uses an environment-based subject, its sub condition must match the environment identity format, such as:
repo:my-company/my-dotnet-app:environment:production
Use the exact subject format for the workflow and environment configuration. Do not assume that a branch-based subject and an environment-based subject are interchangeable.
Environment-based trust should be combined with appropriate GitHub environment protection rules so that an authorized repository cannot deploy arbitrary code to production without the intended controls.
Common OIDC Deployment Errors
1. Not Authorized to Perform sts
This error usually means the token does not satisfy the role's trust policy.
Check the AWS account ID, OIDC provider ARN, audience, repository name, branch or environment subject, and role ARN used in the workflow.
2. Access Denied During Deployment
If authentication succeeds but an S3 upload or another AWS operation fails, the issue may be in the role's permissions policy.
Check the requested AWS action, target resource ARN, and any applicable bucket policy or service-level restrictions.
Do not resolve the problem by granting administrator access to the deployment role. Identify the missing permission and add only what is required.
3. The Workflow Cannot Request an OIDC Token
Check that id-token: write is granted to the job or workflow.
Also verify that the credentials action is configured to use the intended role and region.
4. Deployment Runs from an Unexpected Branch
Check the workflow triggers, branch protection, environment rules, and IAM trust policy.
A workflow trigger alone is not a complete security boundary. AWS should independently restrict which GitHub identities can assume the role.
Best Practices for Secure AWS Deployments
Use short-lived credentials: Prefer OIDC over permanent AWS access keys for GitHub Actions.
Restrict the trust policy: Allow only the intended repository, branch, or protected environment.
Follow least privilege: Grant only the AWS actions and resource access required for deployment.
Protect production environments: Use required approvals and branch restrictions where appropriate.
Avoid untrusted pull-request execution with deployment access: Do not expose production credentials to workflows that execute untrusted code.
Pin actions to reviewed commit SHAs: Reduce the risk of unexpected changes in third-party workflow dependencies.
Monitor role activity: Use AWS audit logging, such as CloudTrail, to investigate role assumptions and deployment actions.
Test the deployment target: Verify the artifact directory, resource ARN, region, and deletion behavior before deploying to production.
Conclusion
GitHub Actions can deploy to AWS without storing long-lived access keys by using OpenID Connect and IAM role assumption.
The workflow requests a GitHub identity token, AWS validates it against the role's trust policy, and AWS STS issues temporary credentials. A separate permissions policy limits what those credentials can do.
For a secure deployment, configure both policies carefully, restrict production access, and verify that the workflow deploys only the intended artifacts. OIDC removes the need to manage permanent AWS keys in GitHub secrets, but the overall security still depends on the workflow, IAM configuration, and deployment permissions.
Join the conversation! Your thoughts help the community grow.