Advanced Lesson 10 of 14

Security & Permissions

GITHUB_TOKEN permissions, OIDC federation, branch protection, supply chain security, and hardening your CI/CD pipeline.

πŸ§’ Simple Explanation (ELI5)

Imagine your GitHub Actions workflow is a person working inside a bank vault.

Put it all together: every worker has a limited badge, visitors get temporary passes, and guards check everyone at the door. That's how you secure a CI/CD pipeline.

πŸ”‘ GITHUB_TOKEN

Every workflow run automatically receives a GITHUB_TOKEN β€” a short-lived token scoped to the repository. Understanding and restricting its permissions is the foundation of workflow security.

How It Works

Restricting Permissions

Declare a permissions block at the workflow or job level. Any scope not listed is automatically set to none:

yaml
# Workflow-level β€” applies to ALL jobs
permissions:
  contents: read
  packages: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Token can read code, write packages, write PR comments
      # Everything else (issues, deployments, etc.) = none
yaml
# Job-level β€” overrides workflow-level for this job only
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write   # Needed for OIDC
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Available Permission Scopes

ScopeControls Access ToCommon Use Case
actionsWorkflow runs, artifacts, cachesManage workflow runs from another workflow
checksCheck runs and check suitesCreate status checks on commits
contentsRepository contents, commits, branches, tagsCheckout code, create releases
deploymentsDeployment statusesTrack deployment progress
id-tokenOIDC token requestAuthenticate to cloud providers via OIDC
issuesIssues and commentsAuto-label or comment on issues
packagesGitHub PackagesPublish npm / Docker / NuGet packages
pagesGitHub PagesDeploy static sites
pull-requestsPull requests and commentsPost PR comments, approve PRs
repository-projectsProject boardsAutomate project card movements
security-eventsCode scanning, Dependabot alertsUpload SARIF results
statusesCommit statusesPost build status to commits

Setting Default to Read-Only

At the organization or repository level, navigate to Settings β†’ Actions β†’ General β†’ Workflow permissions and select "Read repository contents and packages permissions". This forces all workflows to start with read-only access β€” individual workflows must explicitly opt-in to write permissions.

⚠️
Always Default to Read-Only

Setting the org-wide default to read-only is the single most impactful security change you can make. It ensures a compromised or misconfigured workflow can't silently push code, delete branches, or publish packages unless permissions are explicitly granted.

🌐 OIDC Federation

OpenID Connect (OIDC) lets your workflow exchange GitHub's built-in JWT for short-lived cloud credentials β€” no stored secrets required.

Why OIDC?

How It Works

text
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  GitHub       β”‚  1.   β”‚  GitHub OIDC          β”‚  3.   β”‚  Cloud Provider  β”‚
β”‚  Workflow     │──────▢│  Provider             │◀──────│  IAM             β”‚
β”‚  (requests    β”‚       β”‚  (issues JWT with     β”‚       β”‚  (validates JWT, β”‚
β”‚   id-token)   β”‚       β”‚   repo/branch/env     β”‚       β”‚   checks claims, β”‚
β”‚               β”‚  2.   β”‚   claims)             β”‚  4.   β”‚   issues creds)  β”‚
β”‚               │◀──────│                       │──────▢│                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  JWT   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                                                        β”‚
       β”‚  5. Short-lived credentials                            β”‚
       β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό
  Deploy / Push / Access Cloud Resources

Azure Setup

  1. App Registration β€” create (or reuse) an app in Azure AD
  2. Federated Credential β€” add a credential that trusts GitHub's OIDC provider
  3. Subject claim β€” scope to specific repo, branch, or environment
bash
# Create federated credential for main branch
az ad app federated-credential create \
  --id <APP_OBJECT_ID> \
  --parameters '{
    "name": "github-main",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:myorg/myrepo:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"]
  }'

AWS Setup

  1. IAM Identity Provider β€” add token.actions.githubusercontent.com as an OIDC provider
  2. IAM Role β€” create a role that trusts the OIDC provider
  3. Trust condition β€” restrict to your repo using sub claim
json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::<ACCOUNT_ID>: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"
      }
    }
  }]
}

GCP Setup

  1. Workload Identity Pool β€” create a pool for GitHub Actions
  2. Provider β€” add an OIDC provider pointing to GitHub
  3. Service Account binding β€” bind the pool to a GCP service account with appropriate roles
bash
# Create workload identity pool
gcloud iam workload-identity-pools create github-pool \
  --location="global" --display-name="GitHub Actions Pool"

# Create OIDC provider
gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location="global" \
  --workload-identity-pool="github-pool" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository"

# Bind service account
gcloud iam service-accounts add-iam-policy-binding deploy-sa@myproject.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/<PROJECT_NUM>/locations/global/workloadIdentityPools/github-pool/attribute.repository/myorg/myrepo"

Subject Claims

Subject claims control exactly which workflows can request tokens. Use these to enforce fine-grained trust:

Claim PatternTrust Scope
repo:myorg/myrepo:ref:refs/heads/mainOnly the main branch
repo:myorg/myrepo:environment:productionOnly the production environment
repo:myorg/myrepo:ref:refs/tags/v*Only version tags
repo:myorg/myrepo:pull_requestOnly pull request workflows
πŸ’‘
Environment-Scoped OIDC

For maximum security, use environment-scoped subject claims (e.g., repo:org/repo:environment:production). This ties cloud access to a specific GitHub Environment β€” which can also require manual approval, further reducing the blast radius of a compromised workflow.

πŸ›‘οΈ Branch Protection & Rulesets

Branch protection ensures that code reaching your main branch (and triggering deployments) has been reviewed, tested, and verified.

Required Status Checks

Required Reviews

Signed Commits

GitHub Rulesets (New)

Rulesets are the next generation of branch protection β€” more flexible and stack-able:

text
How to enforce:
1. Navigate to Settings β†’ Rules β†’ Rulesets β†’ New ruleset
2. Name the ruleset (e.g., "Production Protection")
3. Target: branches matching "main", "release/*"
4. Rules: Require status checks, Require pull request, Require signed commits
5. Bypass: Add "release-bot" app
6. Enforcement: Active

πŸ“¦ Supply Chain Security

Your workflow is only as secure as the actions it consumes. A compromised third-party action can steal secrets, inject malicious code, or exfiltrate data.

Pin Actions by SHA

Never reference actions by mutable tag β€” always pin to a specific commit SHA:

yaml
# ❌ BAD β€” tag can be moved to a malicious commit
- uses: actions/checkout@v4

# βœ… GOOD β€” pinned to an immutable commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Add a comment with the version tag for readability. The SHA is immutable β€” even if the tag is moved, your workflow runs the exact code you audited.

Dependabot for Actions

Automate SHA updates by adding a dependabot.yml to your repository:

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Dependabot will open PRs when new versions of pinned actions are available, including the updated SHA.

SLSA Provenance & Attestations

yaml
permissions:
  id-token: write
  contents: read
  attestations: write

steps:
  - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
  - name: Build artifact
    run: npm run build
  - uses: actions/attest-build-provenance@v2
    with:
      subject-path: 'dist/'

Private Action Restrictions

Fork Pull Request Restrictions

⚠️
The pull_request_target Trap

pull_request_target runs with the base branch's permissions and secrets β€” even for fork PRs. If you checkout the PR's head ref and run its code, a malicious fork can steal all your secrets. Only use pull_request_target when you are not executing any code from the PR.

βœ… Security Hardening Checklist

πŸ”’
10-Point Security Checklist
  1. Set org/repo default token permissions to read-only
  2. Declare explicit permissions in every workflow and job
  3. Pin all third-party actions by SHA, not tag
  4. Enable Dependabot for github-actions ecosystem
  5. Use OIDC federation instead of stored cloud secrets
  6. Require status checks and PR reviews before merge
  7. Restrict fork PR workflows β€” require approval for first-time contributors
  8. Never use pull_request_target to run untrusted PR code
  9. Maintain an allow-list of approved actions at the org level
  10. Generate SLSA provenance for all production build artifacts

πŸ“Š OIDC Authentication Flow

text
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              β”‚ 1. β”‚                      β”‚    β”‚                  β”‚
β”‚   Workflow   │───▢│  Request id-token    β”‚    β”‚   Cloud IAM      β”‚
β”‚   Job Starts β”‚    β”‚  from GITHUB_TOKEN   β”‚    β”‚   (Azure / AWS   β”‚
β”‚              β”‚    β”‚                      β”‚    β”‚    / GCP)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚                         β”‚
                          2. Request                 4. Validate
                               β”‚                      JWT claims
                               β–Ό                         β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
                    β”‚   GitHub OIDC        β”‚  3. Present β”‚
                    β”‚   Provider           │────JWT─────▢│
                    β”‚   (issues JWT with   β”‚             β”‚
                    β”‚    repo, branch,     β”‚             β”‚
                    β”‚    env claims)       β”‚             β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
                                                         β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  5. Issue   β”‚
                    β”‚   Short-lived        β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚   Cloud Credentials  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                          6. Use credentials
                               β”‚
                               β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Deploy / Push /    β”‚
                    β”‚   Access Resources   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ› οΈ Hands-on Lab

Lab 1: Set Default Permissions to Read-Only

  1. Navigate to your repository Settings β†’ Actions β†’ General
  2. Under Workflow permissions, select "Read repository contents and packages permissions"
  3. Save β€” all existing workflows now run with read-only tokens unless they explicitly declare permissions
  4. Test: trigger a workflow that writes (e.g., creates a comment). Observe the 403 failure, then add the required permissions block

Lab 2: Configure GITHUB_TOKEN Scoping

  1. Create a workflow with a job that comments on a PR:
yaml
name: PR Comment
on: pull_request

permissions:
  pull-requests: write

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'CI checks are running!'
            })
  1. Open a PR and verify the comment appears
  2. Remove the permissions block β€” observe the failure when the default is read-only

Lab 3: Pin an Action by SHA

  1. Visit the action's repository on GitHub (e.g., actions/checkout)
  2. Find the latest release tag (e.g., v4.1.1) and copy the full commit SHA from the tag
  3. Replace uses: actions/checkout@v4 with uses: actions/checkout@<FULL_SHA> # v4.1.1
  4. Add a .github/dependabot.yml for github-actions to receive update PRs

Lab 4: Set Up Required Status Checks

  1. Create a workflow that runs tests on every PR
  2. Navigate to Settings β†’ Branches β†’ Add rule for main
  3. Enable "Require status checks to pass before merging"
  4. Search for and select your workflow's job name (e.g., test)
  5. Open a PR with a failing test β€” verify that GitHub blocks the merge

πŸ› Debugging Common Issues

"Resource not accessible by integration"

"OIDC token audience mismatch"

"Status check not found" in branch protection

🎯 Interview Questions

Basic (5)

1. What is the GITHUB_TOKEN and how is it created?

The GITHUB_TOKEN is a short-lived access token automatically generated by GitHub at the start of every workflow run. It's scoped to the repository, expires when the job ends (or after 24 hours), and requires no manual setup. Its permissions can be restricted using the permissions key in the workflow YAML.

2. What is the principle of least privilege and how does it apply to GitHub Actions?

Least privilege means granting only the minimum permissions required to do the job β€” nothing more. In GitHub Actions, this means: setting the org/repo default to read-only, declaring explicit permissions blocks in every workflow, and never granting write access to scopes the workflow doesn't need. This limits damage if a workflow is compromised.

3. Why should you pin actions by SHA instead of tag?

Tags are mutable β€” an action maintainer (or attacker who compromises their account) can move a tag to a different commit. A SHA is immutable and points to a specific, auditable version of the code. Pinning by SHA ensures you run exactly the code you reviewed, preventing supply chain attacks via tag hijacking.

4. What is a required status check in branch protection?

A required status check is a CI job that must pass before a pull request can be merged into the protected branch. It's configured in the repository's branch protection rules and references a specific workflow job name. If the check fails or hasn't run, GitHub blocks the merge button.

5. What happens to secrets when a workflow is triggered by a fork PR?

By default, workflows triggered by fork PRs have no access to repository secrets. The GITHUB_TOKEN is also limited to read-only permissions. This prevents malicious forks from exfiltrating sensitive data. To access secrets in fork PR workflows, the PR must be explicitly approved first (and even then, it's risky).

Intermediate (5)

6. Explain OIDC federation and why it's preferred over stored secrets.

OIDC (OpenID Connect) federation lets GitHub issue a JWT that a cloud provider validates in exchange for short-lived credentials. Unlike stored secrets, OIDC tokens are generated per-run, expire in minutes, are scoped to a specific repo/branch/environment, and can't be leaked from GitHub's secret store. There are no credentials to rotate, and trust is cryptographically verified via the subject claim.

7. How do GitHub Rulesets differ from traditional branch protection rules?

Rulesets are more flexible: they're stack-able (multiple rulesets can apply to the same branch), support bypass lists for specific teams or apps, can protect tags as well as branches, and can be managed at the organization level across all repos. Traditional branch protection is per-repo, per-branch, and can't be layered.

8. What is the pull_request_target security risk?

pull_request_target runs on the base branch's code with the base branch's secrets and write permissions β€” even when triggered by a fork PR. If the workflow checks out the PR's head ref (github.event.pull_request.head.ref) and executes its code (e.g., runs tests, builds), a malicious fork can access all secrets. Only use it when you're not running any code from the PR itself.

9. How does Dependabot help with GitHub Actions supply chain security?

Dependabot monitors your workflow files for pinned action SHAs and opens pull requests when new versions are available. It updates both the SHA and the version comment. This automates the process of staying current with security patches while maintaining the safety of SHA pinning. Configure it via .github/dependabot.yml with package-ecosystem: "github-actions".

10. What are OIDC subject claims and why are they important?

Subject claims (sub) in the OIDC JWT identify which specific workflow is requesting access β€” including the repository, branch, environment, and trigger type. Cloud providers validate these claims before issuing credentials. By scoping claims tightly (e.g., repo:org/repo:environment:production), you ensure that only the intended workflows can access production cloud resources, even if other workflows in the same repo are compromised.

Senior (5)

11. Design a defense-in-depth strategy for a GitHub Actions CI/CD pipeline serving 50 microservices.

Layer 1 β€” Identity: OIDC federation to all cloud providers, environment-scoped subject claims, no stored secrets. Layer 2 β€” Permissions: Org-wide read-only default, each workflow declares minimum permissions, separate tokens for build vs deploy jobs. Layer 3 β€” Supply chain: All actions pinned by SHA, org-level allow-list of approved actions, Dependabot for automated updates, SLSA provenance for all artifacts. Layer 4 β€” Branch protection: Required reviews from CODEOWNERS, required status checks, signed commits, rulesets enforced at org level. Layer 5 β€” Runtime: Self-hosted runners in ephemeral mode, network-isolated, no persistent state. Layer 6 β€” Monitoring: Audit log streaming to SIEM, alerts on permission escalation, failed OIDC attempts, and unusual workflow patterns.

12. A team reports that their OIDC auth works on main but fails for release tags. How do you investigate?

The federated credential's subject claim is likely scoped to ref:refs/heads/main and doesn't cover tags. Check: (1) the exact subject claim in the cloud provider's federated credential config, (2) the OIDC token's actual sub claim for tag triggers β€” it will be repo:org/repo:ref:refs/tags/v1.0.0, (3) add a second federated credential with a tag-scoped subject pattern. Alternatively, use environment-scoped claims if the release workflow uses a specific GitHub Environment. Decode the JWT in a debug step to verify the actual claims being sent.

13. How would you prevent a supply chain attack where a popular action's maintainer account is compromised?

(1) Pin all actions by SHA β€” the attacker can move tags but can't change the commit a SHA points to. (2) Maintain an org-level allow-list of vetted actions β€” block unknown actions entirely. (3) Use Dependabot to review new version PRs before adopting them β€” the PR diff shows exactly what changed. (4) For critical actions, fork them into your org and pin to your fork. (5) Generate and verify SLSA provenance for actions. (6) Monitor the actions you depend on for unusual release patterns.

14. Explain how you'd implement token scoping in a large enterprise with 200 repos and varying security needs.

Use a layered approach: (1) Org-level policy sets default to read-only for all repos. (2) Org-level rulesets enforce branch protection across all repos. (3) Create org-level reusable workflows with pre-configured permissions blocks β€” teams call these instead of writing their own. (4) Use org-wide action allow-lists (curated by the security team). (5) OIDC federation at the org level with per-repo/per-environment subject claims. (6) Custom GitHub App for operations that need elevated permissions beyond what GITHUB_TOKEN provides β€” the app's install permissions are tightly scoped. (7) Audit log streaming to detect permission escalations and policy violations.

15. How does fork PR security interact with GITHUB_TOKEN permissions, and what are the enterprise implications?

Fork PRs run with a read-only GITHUB_TOKEN and no secret access β€” this is a hard security boundary. Even if the workflow declares permissions: write, fork PR triggers silently downgrade to read-only. Enterprise implications: (1) External contributor workflows can't publish packages, create releases, or deploy. (2) pull_request_target bypasses this boundary β€” it must be strictly audited. (3) Require approval for all fork PR workflows from first-time contributors. (4) Use the environment protection rule to add an approval gate even for trusted fork PRs. (5) In regulated environments, consider disabling fork PR workflows entirely and requiring contributors to push to internal branches.

🏭 Real-World Scenario

A mid-size SaaS company preparing for SOC 2 Type II certification needed to prove their CI/CD pipeline met strict security controls. Here's how they hardened their GitHub Actions setup:

Before (audit findings):

After (remediation):

Result: passed SOC 2 audit with zero findings on the CI/CD pipeline. Mean time to rotate compromised credentials dropped from 2 days to zero (nothing to rotate with OIDC).

πŸ“ Summary

← Back to GitHub Actions Course