Introduction – The All‑Too‑Common Shortcut

Many teams accelerate delivery by dropping static AWS access keys straight into their .github/workflows/*.yml files. The practice looks harmless at first glance – the pipeline can push Docker images, update CloudFormation stacks, and spin up test environments without extra configuration. However, hard‑coded credentials create a permanent backdoor that can survive repository migrations, forking, and even accidental public exposure.

The Hidden Risks of Hard‑Coded Secrets

Credential leakage. A single commit pushed to a public fork can reveal privileged keys to anyone scanning the internet. Attackers can then use those keys to enumerate S3 buckets, launch EC2 instances, or exfiltrate sensitive data.

Privilege creep. When the same key is reused across dozens of repositories, revoking it for a single compromised project becomes impossible without breaking every other pipeline that depends on it.

Lack of auditability. AWS CloudTrail records actions taken with the key, but it cannot easily attribute them to a particular CI job or developer because the same IAM user is used everywhere.

# Example of a vulnerable GitHub Actions workflow
name: Deploy to Production
on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Configure AWS
        env:
          AWS_ACCESS_KEY_ID: AKIA****   # ← hard‑coded!
          AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCY****
        run: |
          aws s3 sync ./static s3://my‑prod‑bucket --delete

The snippet above demonstrates the pattern that should be avoided. Below we replace this approach with a zero‑secret, OIDC‑based workflow that leverages GitHub’s built‑in identity federation.

Prerequisites – Setting Up the Trust Relationship

1. An AWS account with administrative rights to create an IAM role.
2. A GitHub repository where you control the workflow files.
3. The aws-cli version 2.x installed in the runner (default on ubuntu-latest images).

# Create an IAM role that trusts GitHub OIDC
aws iam create-role \
  --role-name GitHubActionsDeploy \
  --assume-role-policy-document '{
    "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": {
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:${GITHUB_OWNER}/${GITHUB_REPO}:ref:refs/heads/main"
        },
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        }
      }
    }]
  }'

The policy above tells AWS to trust tokens issued by GitHub’s OIDC provider, but only when the token originates from the main branch of the specified repository. Next, attach the permissions the CI job needs.

# Attach a minimal policy – e.g., S3 sync and CloudFormation update
aws iam put-role-policy \
  --role-name GitHubActionsDeploy \
  --policy-name DeployPolicy \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "s3:PutObject",
          "s3:DeleteObject",
          "cloudformation:UpdateStack",
          "cloudformation:CreateChangeSet",
          "cloudformation:ExecuteChangeSet"
        ],
        "Resource": "*"
      }
    ]
  }'

Secure Workflow – No Secrets, Only Identity

With the role in place, the GitHub Actions workflow can request a short‑lived AWS credential by invoking the aws-actions/configure-aws-credentials action. The action automatically exchanges the OIDC token for a set of temporary credentials.

# .github/workflows/deploy.yml
name: Secure Deploy
on:
  push:
    branches: [ main ]

permissions:
  id-token: write          # Required for OIDC token exchange
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsDeploy
          aws-region: us-east-1

      - name: Sync static assets
        run: |
          aws s3 sync ./static s3://my-prod-bucket --delete

      - name: Deploy CloudFormation stack
        run: |
          aws cloudformation deploy \
            --template-file infra/template.yml \
            --stack-name my-prod-stack \
            --parameter-overrides Env=prod

Notice that the workflow never references AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY. Instead, it relies on the OIDC token that GitHub injects into the runner. The token is short‑lived (typically a few minutes) and scoped exactly to the role we defined.

Why This Approach Eliminates the Silent Liability

Zero static secrets. There is nothing to accidentally commit, rotate, or revoke across multiple repositories.

Fine‑grained, per‑branch access. The trust condition can be tightened to only allow deployments from a protected branch or from a specific GitHub environment.

Automatic credential rotation. Each run receives a fresh set of temporary credentials that automatically expire, removing the attack window.

# Verify the temporary credentials (optional debugging step)
aws sts get-caller-identity
# Expected output:
# {
#   "Account": "123456789012",
#   "UserId": "AROAEXAMPLEID:GitHubActionsDeploy",
#   "Arn": "arn:aws:sts::123456789012:assumed-role/GitHubActionsDeploy/GitHubActionsDeploy"
# }

By inspecting the ARN you can confirm that the identity is an assumed role, not a long‑lived IAM user.

Security and Best Practices

  • Least privilege. Grant only the actions your pipeline truly needs.
  • Branch protection. Combine OIDC with GitHub branch protection rules to ensure only reviewed code can trigger deployments.
  • Audit logs. Enable CloudTrail data events for S3 and CloudFormation to trace every deployment back to the originating commit SHA.
  • Rotate the role. Periodically rotate the role name or replace the policy to force a re‑evaluation of trust relationships.

Related Insights

Continue exploring Cloud & DevOps: