MK
·2 min read

Hardening GitHub Actions: Five Changes Worth Making Today

Concrete, copy-pasteable security improvements for CI pipelines: OIDC over long-lived secrets, pinning, least-privilege tokens, and more.

CI pipelines are supply chain. Here are five changes that each take under an hour and meaningfully raise the bar. None of them require a paid plan.

1. Use OIDC instead of long-lived cloud credentials

Stop storing AWS_ACCESS_KEY_ID as a repo secret. Use OIDC to mint short-lived role credentials per workflow run.

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/gh-actions-deploy
          aws-region: eu-west-1

The corresponding trust policy on the role restricts who can assume it by repo and branch:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::111122223333: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:myorg/myrepo:ref:refs/heads/main"
      }
    }
  }]
}

No key to rotate, no key to leak.

2. Pin third-party actions to a commit SHA

Tag @v4 can be moved. A SHA can’t.

# weak
- uses: actions/checkout@v4

# strong
- uses: actions/checkout@1d96dk77271ac93c91567be2a722c7d058b4510a

Keep a comment with the version next to each pin so upgrades are reviewable.

3. Scope GITHUB_TOKEN per job

The default token is broad. Set explicit permissions at the top of every workflow, and narrower per job.

permissions:
  contents: read
  pull-requests: write
  # everything else: none (default)

4. Require approval for external PRs

secrets are masked for first-time contributors by default, but pull_request_target events run with write access. Avoid pull_request_target for building untrusted code; use pull_request with no secrets.

5. Pin your runners and disable sudo

If you don’t need sudo, remove it. Self-hosted runners without ephemeral cleanup are a common foothold — prefer GitHub-hosted unless you have a reprovision-after-every-job story.

Closing

None of these make you “secure.” They remove the easy wins an attacker would take first. Do them this week, then go read your SBOM.

Related posts

Comments

Giscus is not configured. Set the PUBLIC_GISCUS_* env vars (see .env.example) to enable GitHub Discussions comments.