DEV Community

Cover image for Deploy a Static Website to AWS S3 with GitHub Actions and OIDC
Muhammad Usama Saleem
Muhammad Usama Saleem

Posted on

Deploy a Static Website to AWS S3 with GitHub Actions and OIDC

In this guide, we'll build a simple deployment pipeline where a website stored in GitHub is automatically deployed to an Amazon S3 bucket whenever changes are pushed to the main branch.

No long-lived AWS access keys will be stored in GitHub.


🏗️ What is being built?

The idea is simple:

  1. Create a website.
  2. Store it in GitHub.
  3. Push the code.
  4. GitHub Actions starts automatically.
  5. GitHub authenticates with AWS using OIDC.
  6. AWS allows the workflow to assume an IAM role.
  7. The workflow uploads the website to S3.
  8. S3 serves the website.

The final flow looks like this:

GitHub
   ↓
GitHub Actions
   ↓
GitHub OIDC
   ↓
AWS IAM Role
   ↓
Amazon S3
   ↓
Static Website 🌐
Enter fullscreen mode Exit fullscreen mode

📋 What is required?

Before starting, you'll need the following.

AWS

  • An AWS account
  • An S3 bucket
  • An IAM role
  • An IAM permissions policy
  • GitHub OIDC configured for AWS

GitHub

  • A GitHub repository
  • A main branch
  • GitHub Actions enabled

Local machine

  • Git
  • A text/code editor
  • A simple index.html

You don't need a server or an EC2 instance for this project.


🧰 What to prepare

For my project, I used the following setup:

AWS Region: <AWS_REGION>
S3 Bucket: <S3_BUCKET_NAME>
IAM Role: <IAM_ROLE_NAME>
GitHub Repository: <GITHUB_USERNAME>/<GITHUB_REPOSITORY>
Branch: main
Enter fullscreen mode Exit fullscreen mode

For example, throughout this guide, replace:

<AWS_REGION>
<S3_BUCKET_NAME>
<IAM_ROLE_NAME>
<GITHUB_USERNAME>
<GITHUB_REPOSITORY>
<AWS_ACCOUNT_ID>
Enter fullscreen mode Exit fullscreen mode

with your own values.

We'll prepare four main things:

  1. S3 bucket
  2. IAM role + permissions
  3. GitHub OIDC authentication
  4. GitHub Actions workflow

1️⃣ Prepare the S3 Bucket

Go to:

AWS Console → S3 → Create bucket

Create a bucket with a name such as:

my-aws-s3-project
Enter fullscreen mode Exit fullscreen mode

💡 S3 bucket names must be globally unique. If this name is already taken, choose another one.

Choose your AWS region.

For example:

ap-southeast-1
Enter fullscreen mode Exit fullscreen mode

Encryption

For server-side encryption, I used:

SSE-S3
Enter fullscreen mode Exit fullscreen mode

This means S3 manages the encryption keys for the objects stored in the bucket.

Once the bucket is created, open it.


2️⃣ Prepare Static Website Hosting

Open:

S3 → Your bucket → Properties

Find:

Static website hosting

Enable it and set the index document to:

index.html
Enter fullscreen mode Exit fullscreen mode

Save the configuration.

At this point, S3 knows that this bucket is going to be used to serve a website.

But we still need to put a website inside it.


3️⃣ Prepare the IAM Role

Next, we need an IAM role that GitHub Actions can use.

Go to:

AWS Console → IAM → Roles → Create role

Create a role such as:

Git-S3-Project
Enter fullscreen mode Exit fullscreen mode

The important thing here is that we're creating a role, not creating an AWS access key and putting that key into GitHub.

The authentication flow will be:

GitHub
   ↓
OIDC token
   ↓
AWS STS
   ↓
IAM Role
   ↓
Temporary credentials
Enter fullscreen mode Exit fullscreen mode

This means GitHub Actions can use temporary AWS credentials instead of storing long-lived AWS credentials.


4️⃣ Prepare the Trust Policy

The role needs to know:

Who is allowed to assume me?

That's what the trust policy controls.

For the GitHub repository, use a trust policy like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": [
            "repo:<GITHUB_USERNAME>/<GITHUB_REPOSITORY>:ref:refs/heads/main"
          ]
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Replace:

<AWS_ACCOUNT_ID>
<GITHUB_USERNAME>
<GITHUB_REPOSITORY>
Enter fullscreen mode Exit fullscreen mode

with your own values.

For example, this part:

repo:<GITHUB_USERNAME>/<GITHUB_REPOSITORY>:ref:refs/heads/main
Enter fullscreen mode Exit fullscreen mode

should point to the repository and branch that are allowed to assume the role.

Trust policy vs permissions policy

Keep this distinction in mind:

Trust Policy
     ↓
Who can assume the role?

Permissions Policy
     ↓
What can the role do?
Enter fullscreen mode Exit fullscreen mode

We'll configure the permissions policy next.


5️⃣ Prepare the S3 Permissions

Now we need to tell AWS what the role is actually allowed to do.

For this project, the role needs permission to upload objects to the S3 bucket.

Use:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::<S3_BUCKET_NAME>/*"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Replace:

<S3_BUCKET_NAME>
Enter fullscreen mode Exit fullscreen mode

with your bucket name.

The permission that matters most for our deployment is:

s3:PutObject
Enter fullscreen mode Exit fullscreen mode

because GitHub Actions needs to upload the website files into S3.

💡 For a production setup, review these permissions carefully and follow the principle of least privilege. The example above is intended to keep this learning project straightforward.


6️⃣ Prepare the Website

Create an index.html in your GitHub repository.

For example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My AWS Website</title>
</head>

<body>
    <h1>Hello from AWS S3 🚀</h1>
    <p>This website is deployed using GitHub Actions.</p>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Your repository can now look like:

aws-s3-project/
│
├── index.html
│
└── .github/
    └── workflows/
        └── deploy.yml
Enter fullscreen mode Exit fullscreen mode

7️⃣ Prepare GitHub Actions

Now we connect GitHub to AWS.

Create:

.github/workflows/deploy.yml
Enter fullscreen mode Exit fullscreen mode

Add:

name: Deploy to S3 Bucket

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<AWS_ACCOUNT_ID>:role/<IAM_ROLE_NAME>
          aws-region: <AWS_REGION>

      - name: Upload files to S3
        run: |
          aws s3 cp ./index.html s3://<S3_BUCKET_NAME>/
Enter fullscreen mode Exit fullscreen mode

Replace:

<AWS_ACCOUNT_ID>
<IAM_ROLE_NAME>
<AWS_REGION>
<S3_BUCKET_NAME>
Enter fullscreen mode Exit fullscreen mode

with your own values.

There are three important parts here.

Trigger

on:
  push:
    branches:
      - main
Enter fullscreen mode Exit fullscreen mode

This means the workflow runs when code is pushed to main.

OIDC permission

permissions:
  id-token: write
  contents: read
Enter fullscreen mode Exit fullscreen mode

id-token: write allows GitHub Actions to request an OIDC token.

It does not give GitHub access to S3.

AWS still decides whether that identity is allowed to assume the IAM role.

Upload

Finally:

aws s3 cp ./index.html s3://<S3_BUCKET_NAME>/
Enter fullscreen mode Exit fullscreen mode

This copies the file into the S3 bucket.


8️⃣ Prepare Public Website Access

At this point, GitHub Actions can upload the file.

But that's a different question from:

Can someone visiting the website read the file?

For a basic S3 static website, the website objects need to be publicly readable.

For this learning project, you can use a bucket policy that allows public read access to index.html:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadForWebsite",
      "Principal": "*",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::<S3_BUCKET_NAME>/index.html"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Replace:

<S3_BUCKET_NAME>
Enter fullscreen mode Exit fullscreen mode

with your bucket name.

Here:

"Principal": "*"
Enter fullscreen mode Exit fullscreen mode

means anyone can be the requester, while:

"Action": "s3:GetObject"
Enter fullscreen mode Exit fullscreen mode

allows them to read the object.

⚠️ This makes index.html publicly readable. This is appropriate for demonstrating a public static website, but you should not blindly use this access model for sensitive or private data.

⚠️ This example only makes index.html public. If your website contains CSS, JavaScript, images, or other files, you'll need to account for those objects too.

For production applications, a common approach is to keep the S3 bucket private and use Amazon CloudFront to serve the website.


✅ Once Done — How to Check

Now we have everything prepared.

Let's test it from end to end.


Check 1: Push the Code

Commit and push:

git add .
git commit -m "Deploy website to S3"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Check 2: Open GitHub Actions

Go to your GitHub repository:

Actions → Deploy to S3 Bucket

You should see a workflow run.

The steps should complete successfully:

✓ Checkout code
✓ Configure AWS credentials
✓ Upload files to S3
Enter fullscreen mode Exit fullscreen mode

If these are green, GitHub successfully authenticated with AWS and uploaded the file.


Check 3: Check the S3 Bucket

Go to:

AWS Console → S3 → Your Bucket → Objects

You should see:

index.html
Enter fullscreen mode Exit fullscreen mode

This confirms the file reached S3.


Check 4: Open the Website

Go back to:

S3 → Properties → Static website hosting

Copy the website endpoint shown there and open it in your browser.

You should see:

Hello from AWS S3 🚀
Enter fullscreen mode Exit fullscreen mode

🎉 Your GitHub repository is now deploying a website to S3.


🔎 If Something Goes Wrong

Here are the main things to check.

GitHub Actions cannot assume the role

Check the IAM trust policy.

The GitHub repository and branch in the sub condition must match the workflow.

For example:

repo:<GITHUB_USERNAME>/<GITHUB_REPOSITORY>:ref:refs/heads/main
Enter fullscreen mode Exit fullscreen mode

Also make sure the GitHub Actions workflow has:

permissions:
  id-token: write
  contents: read
Enter fullscreen mode Exit fullscreen mode

GitHub assumes the role but S3 returns AccessDenied

Check the IAM permissions policy.

Make sure the role has:

s3:PutObject
Enter fullscreen mode Exit fullscreen mode

for the correct bucket:

arn:aws:s3:::<S3_BUCKET_NAME>/*
Enter fullscreen mode Exit fullscreen mode

Deployment succeeds but website shows AccessDenied

GitHub's deployment permission and visitor access are separate.

Check:

  • S3 bucket policy
  • Static website hosting
  • Block Public Access settings

Presigned URL works but normal URL doesn't

A presigned URL provides temporary authorization to access an object.

It does not mean the object is publicly accessible.

For example:

Normal S3 URL
      ↓
Requires permission

Presigned URL
      ↓
Contains temporary authorization
Enter fullscreen mode Exit fullscreen mode

🎯 Final Result

What we started with:

GitHub
Enter fullscreen mode Exit fullscreen mode

What we ended with:

GitHub
   ↓
GitHub Actions
   ↓
OIDC
   ↓
IAM Role
   ↓
S3
   ↓
Static Website 🌐
Enter fullscreen mode Exit fullscreen mode

And the deployment process is now automatic:

Change index.html
       ↓
   git push
       ↓
GitHub Actions
       ↓
 Upload to S3
       ↓
Website updated
Enter fullscreen mode Exit fullscreen mode

The main takeaway is that GitHub Actions can authenticate with AWS using OIDC and assume an IAM role without storing long-lived AWS access keys in the repository.

That's the complete setup. 🚀

Top comments (0)