AWS Builder Center
Deploy to AWS with GitHub OIDC

Deploy to AWS with GitHub OIDC

Deploy a static website from GitHub Actions to Amazon S3 and CloudFront without storing long-lived AWS access keys.

aimlops | devops engineer | aws community builder | open source contributor |
Deploying an application from GitHub Actions to AWS usually requires authentication.
A common approach is to create an IAM user, generate an access key and secret access key, and store both values inside GitHub Secrets.
It works.
But now we have a better approach.
Instead of keeping long-lived AWS credentials inside GitHub, we can establish trust between GitHub Actions and AWS using OpenID Connect, or OIDC.
GitHub proves the identity of the workflow. AWS verifies that identity and allows the workflow to assume an IAM role. AWS Security Token Service then provides temporary credentials for that workflow run.
No permanent AWS access key needs to live inside the GitHub repository.
In this guide, we will build a complete deployment pipeline:
By the end, pushing code to main will automatically deploy a static website to a private S3 bucket and serve it through CloudFront.

What are we building?

Our architecture has four important parts.
GitHub Actions runs the CI/CD workflow when code is pushed.
GitHub OIDC provides proof that the workflow really came from the GitHub repository and branch we trust.
Amazon S3 stores the built website files.
Amazon CloudFront serves those files globally while keeping the S3 bucket private.
The important security boundary looks like this:
The GitHub runner receives temporary AWS credentials only for the workflow execution.
That is very different from storing an IAM user's long-lived access key indefinitely.

Why OIDC instead of AWS access keys?

Consider the traditional approach:
Those credentials remain valid until somebody rotates, disables, or deletes them.
With OIDC, our workflow instead uses:
and assumes an AWS IAM role:
AWS checks the GitHub-issued identity before allowing the role to be assumed.
This gives us an important improvement:
OIDC does not mean the workflow never receives AWS credentials.
It means we do not need to create and store a long-lived AWS access key for the deployment.

Prerequisites

You will need an AWS account, a GitHub repository, and a static web application.
This article assumes that the production build of your application is generated inside:
For example, many Vite projects produce:
If your framework produces build/, out/, or another directory, simply change the path in the workflow later.
For a plain HTML/CSS/JavaScript site that does not require a build step, you can also remove the Node.js build steps completely.

Step 1: Create a private S3 bucket

Open:
Choose a globally unique bucket name.
For example:
Choose the AWS Region you want to use.
For this example:
Keep Block Public Access enabled.
Do not make the bucket public.
Also, do not enable S3 static website hosting for this architecture.
CloudFront will use the regular S3 bucket origin with Origin Access Control.
After creation, we have:
At this stage the bucket contains no files.

Step 2: Create the CloudFront distribution

Open:
For the origin, select the S3 bucket we just created.
For Origin access, choose:
Create a new Origin Access Control if necessary.
Use the recommended option:
This allows CloudFront to authenticate when accessing the private S3 bucket.
For the default behavior, configure:
Set:
Then create the distribution.
CloudFront will provide a distribution ID similar to:
and a domain similar to:
Keep both values.
Our architecture is now:
Users should access the application through CloudFront, not directly through an S3 public URL.

Step 3: Allow CloudFront to read the bucket

CloudFront needs permission to retrieve objects from S3.
Open:
Add the following policy.
Replace:
with your actual values.
Notice what we did not do.
We did not add:
The bucket remains private.
Only CloudFront requests associated with our specific distribution receive permission to read the objects.

Step 4: Add GitHub as an AWS OIDC provider

Now we need AWS to recognize GitHub as a trusted identity provider.
Open:
Select:
Use this provider URL:
Use this audience:
Create the provider.
You only need one GitHub OIDC provider per AWS account for this issuer. Multiple IAM roles and repositories can use it with their own trust policies.
The AWS side can now recognize identity tokens issued by GitHub Actions.
But we still have not given any repository permission to deploy.
That happens next.

Step 5: Create a deployment IAM role

Create an IAM role named something descriptive, such as:
This role needs only the permissions required by our deployment.
We need to:
Create an IAM permissions policy similar to this:
Attach this policy to:
This is deliberately much narrower than attaching something like:
Our CI/CD workflow should receive only the AWS permissions it actually needs.

Step 6: Configure the role trust policy

Permissions answer:
What can this role do?
The trust policy answers:
Who is allowed to become this role?
This distinction is important.
Open:
A common GitHub OIDC trust policy looks like this:
For example:
This means a random GitHub repository cannot assume the role.
Even another branch of the same repository cannot assume it.
The trusted identity is specifically:

Important GitHub OIDC change in 2026

There is one important detail for newer repositories.
GitHub changed its default OIDC subject format for repositories created after July 15, 2026.
New repositories include immutable owner and repository IDs.
Instead of:
the subject can look like:
For example:
If your repository uses this newer format, your AWS trust policy must match it.
You can get useful repository information with GitHub CLI:
Then use the correct subject in AWS.
A trust policy for the immutable format would look like:
Repositories created before July 15, 2026 normally retain the older format unless immutable subjects have been enabled or the repository has since been renamed or transferred.
If your organization has customized its OIDC subject claims, make sure the IAM condition matches that configuration instead.

Step 7: Save deployment configuration in GitHub

Now open your GitHub repository.
Go to:
Create these repository variables:
Example:
These values identify infrastructure.
They are not AWS passwords.
Most importantly, we are not creating:
inside GitHub Secrets.

Step 8: Create the GitHub Actions workflow

Create:
Add:
That is the complete deployment pipeline.
No AWS access key.
No AWS secret access key.

What happens when we push?

Suppose I change the homepage and run:
GitHub sees the push to main and starts the workflow.
First:
allows the job to request an OIDC identity token.
Then:
uses that GitHub identity to request access to our IAM role.
AWS evaluates the trust relationship.
Conceptually, it checks:
AWS STS then issues temporary credentials.
Those credentials are made available to subsequent AWS CLI commands in the job.
The workflow then runs:
which synchronizes the new site with S3.
The --delete option is important because it also removes remote files that disappeared from the new build.
Finally:
tells CloudFront not to continue serving stale cached files.
The result is:

Step 9: Verify that OIDC is actually working

The workflow contains:
A successful output should resemble:
Look carefully at the ARN:
That confirms the GitHub workflow assumed the IAM role through AWS STS.
Now open your CloudFront domain:
Your deployed application should appear.

What happens if someone copies the workflow?

Imagine someone copies our entire deploy.yml into another GitHub repository.
They know:
Those identifiers alone should not give them permission to deploy.
Their OIDC subject might be:
but our role expects:
AWS evaluates the trust policy and rejects the role assumption.
This is why the sub condition in the role trust policy matters.
Do not create an unnecessarily broad condition such as trusting every repository without a specific reason.

Understanding least privilege

There are actually two permission boundaries in this setup.
The first is the trust policy:
The second is the permissions policy:
Our trust policy restricts access to a specific GitHub identity.
Our permissions policy restricts the resulting AWS session to:
Even if the deployment role is successfully assumed, it does not automatically have permission to create EC2 instances, modify IAM users, delete databases, or administer the entire AWS account.
That is the principle of least privilege in practice.

Optional: Handle SPA routes

If you are deploying a single-page application using React, Vue, or another client-side router, URLs such as:
might not correspond to real objects inside S3.
In that situation, configure CloudFront custom error responses so those requests can return:
For many SPA architectures you can configure responses for 403 and 404 to return /index.html with an HTTP 200 response.
Do this only when your application's routing model requires it.
A traditional multi-page static website usually does not need this behavior.

Better cache handling for larger applications

Our example invalidates:
after every deployment.
That keeps the tutorial straightforward and works well for a small site.
For larger production applications, a better strategy is to give compiled assets versioned or hashed filenames:
and cache them for longer periods.
Then only short-lived entry files such as:
need frequent revalidation or invalidation.
That reduces unnecessary cache invalidations while still allowing new releases to appear quickly.

Common problems

Not authorized to perform sts:AssumeRoleWithWebIdentity

The first place to check is the role trust policy.
Verify:
For repositories created after July 15, 2026, also check whether GitHub is using the immutable owner ID and repository ID format.

No OpenIDConnect provider found

Verify that IAM contains:
with the audience:

Could not load credentials from any providers

Make sure the workflow contains:
and that the AWS credentials action contains:

AccessDenied during aws s3 sync

Check the deployment role's IAM policy.
It should have the required permissions on the correct bucket:
Also make sure the bucket ARN is correct.
Bucket:
Objects:
They are different resources.

CloudFront returns 403 Access Denied

Check the S3 bucket policy and CloudFront Origin Access Control.
The bucket policy should grant s3:GetObject to:
and restrict access using the ARN of the correct CloudFront distribution.
Also make sure CloudFront uses the normal S3 bucket origin rather than an S3 website endpoint when using OAC.

Deployment succeeded but the old website still appears

CloudFront may still have cached objects.
Check that this step succeeded:
Then check the Invalidations section of the CloudFront distribution.

Security improvements for production

The architecture is already substantially better than keeping a long-lived IAM user's access key in GitHub, but production environments can go further.
You can use a GitHub Environment named something like:
and protect it with deployment rules or required approvals.
Be careful when doing this.
When a GitHub Actions job references an environment, the OIDC subject changes.
Instead of a branch subject such as:
it uses an environment subject such as:
Your AWS role trust policy must therefore match the identity generated by the workflow.
You can also separate development and production into different AWS roles:
For higher-security environments, consider pinning third-party GitHub Actions to reviewed commit SHAs instead of relying only on moving major-version tags.
And continue applying the same rule everywhere:

What about GitHub Secrets?

OIDC does not eliminate every possible use of GitHub Secrets.
Your application may still need secrets for other systems.
But AWS authentication itself no longer requires us to store credentials like:
That is the important change.
Configuration such as:
can normally live in GitHub Actions Variables instead.

Cleaning everything up

A good AWS tutorial should also explain how to remove what it creates.
When you no longer need this deployment:
First disable the CloudFront distribution and delete it after CloudFront allows deletion.
Then empty and delete the S3 bucket.
Delete the deployment IAM role and its permissions policy if nothing else uses them.
Remove the GitHub repository variables.
Finally, consider the GitHub OIDC identity provider separately.
Do not automatically delete:
if another GitHub repository or IAM role in the AWS account still depends on it.
The OIDC provider can be shared across multiple roles.

Final architecture

We started with the possibility of doing this:
Instead, we built:
The final workflow is still simple.
Push code.
GitHub proves the workflow's identity.
AWS verifies that identity.
STS issues temporary credentials.
The application is uploaded to S3.
CloudFront serves the new version globally.
And we did it without maintaining a long-lived AWS deployment access key inside GitHub.
That is the part I like most about OIDC: it does not just automate deployment.
It improves how the automation earns permission to deploy.
If you are still using permanent AWS access keys for GitHub Actions deployments, this is a good architecture to explore for your next pipeline.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article