GitHub Actions has access to your code, your build artifacts, and — through secrets — your cloud accounts. A poorly secured workflow is a credential leak waiting to happen. This lesson covers the seven hardening practices every team should adopt.
1. Lock Down GITHUB_TOKEN Permissions
Every workflow run is given a token with permissions to the repo's resources. Make it read-only by default and grant per-job:
permissions: read-all # workflow default
jobs:
release:
permissions:
contents: write # tag and publish a release
pull-requests: write # comment on PRs
runs-on: ubuntu-latest
steps: [...]
You can also set the default for the whole org at Org Settings → Actions → General → Workflow permissions. Set "Read repository contents and packages permissions" and require workflows to explicitly opt in to write scopes.
2. Pin Actions by SHA
Branch refs like @main on third-party actions are a supply-chain risk — the maintainer (or an attacker who compromises them) can push code that runs in your context.
- uses: hashicorp/setup-terraform@b9cd5d3eea4d0e02e1e80fb0ff5a6b95f6b0a5e0 # v3.1.2
Use Dependabot's github-actions ecosystem to keep pinned SHAs updated via reviewable PRs.
3. OIDC: Stop Storing Cloud Keys
OpenID Connect lets your workflow request a short-lived token signed by GitHub. AWS, Azure, GCP, HashiCorp Vault, and many other services trust GitHub's OIDC provider directly. The flow:
- Configure the cloud provider to trust GitHub's OIDC issuer (
token.actions.githubusercontent.com) - Create a role/identity bound to specific repo + branch + workflow claims
- Your workflow requests an OIDC token and exchanges it for cloud credentials
- The credentials are valid for the duration of the job — typically 60 minutes
AWS example
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: us-east-1
- run: aws sts get-caller-identity
The IAM role's trust policy restricts which repo and branch can assume it — typically scoped to repo:acme/my-app:ref:refs/heads/main. No stored AWS keys; no key rotation; no leakage risk.
Azure / GCP
Azure: azure/login@v2 with federated-credential-configuration. GCP: google-github-actions/auth@v2 with workload identity federation. All three clouds support the same pattern.
4. Protect Workflow Files Themselves
A workflow file is executable infrastructure. Add it to CODEOWNERS and require reviews from a security team:
# .github/CODEOWNERS
/.github/workflows/ @acme/devops-security
Combined with branch protection requiring CODEOWNERS review, this prevents drive-by workflow changes.
5. Be Careful with pull_request_target
The pull_request trigger runs PRs without repo secrets — by design, because fork PRs shouldn't see your AWS key. The pull_request_target trigger runs with secrets, in the context of the base branch. It is a dangerous primitive — if the workflow checks out the PR's code and runs it, an attacker's PR can exfiltrate your secrets.
Rule: if you use pull_request_target, never check out the PR's code unless you also restrict the workflow to label/comment-driven invocation by maintainers.
6. Secret Scanning and Push Protection
GitHub Advanced Security (GHAS) — included free for public repos, paid for private — scans pushed code for credentials. Push Protection rejects pushes containing known credential formats before they enter the repo. Enable both:
- Settings → Code security and analysis → Secret scanning: Enabled
- Settings → Code security and analysis → Push protection: Enabled
Hundreds of credential patterns from partners (AWS, Stripe, Twilio, etc.) are recognised.
7. Code Scanning (CodeQL)
CodeQL is GitHub's semantic code analysis engine. Enable it via the default setup:
name: CodeQL
on:
push: { branches: [main] }
pull_request: { branches: [main] }
schedule: [{ cron: '0 6 * * 1' }]
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
strategy:
matrix:
language: [javascript, python]
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with: { languages: ${{ matrix.language }} }
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
Findings appear in the Security tab. The GH-300 exam covers this in detail.
8. Dependency Review
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
allow-licenses: MIT, Apache-2.0, BSD-3-Clause
On PRs that change dependencies, this action blocks the merge if the diff introduces high-severity CVEs or disallowed licences.
Defence-in-Depth Checklist
| Control | Where |
|---|---|
| Read-only default GITHUB_TOKEN | Org/Repo settings |
| Workflow approvals from outside collaborators | Org/Repo settings |
| Pinned third-party actions | Workflow YAML |
| OIDC for cloud deploys | Workflow YAML + cloud IAM |
CODEOWNERS on .github/workflows | Repo |
| Required reviews on protected branches | Branch protection rules |
| Secret scanning + push protection | Repo / Org |
| CodeQL on PRs | Workflow |
| Dependency review on PRs | Workflow |
| Environments with required reviewers for prod | Repo environments |
Adopt these incrementally — even just steps 1, 2, and 3 dramatically shrink the blast radius of a CI/CD compromise.
Where to Go Next
To go deeper, take the GH-200 (Actions) exam first, then the GH-300 (Advanced Security) exam. The GH-300 blueprint covers CodeQL, secret scanning, supply-chain attestations (SLSA), and Dependabot — all of which build on the foundation you have now.