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.
- GITHUB_TOKEN is your ID badge. Every employee gets one automatically when they walk in. But your badge only opens the doors you're authorised to access β not the entire building.
- The permissions block in your workflow is like choosing which floors your badge can open. By default it might open everything β but a smart security team limits it to only the floors you actually need. That's the principle of least privilege.
- OIDC federation is like a temporary visitor pass from a trusted partner. Instead of giving the visitor a permanent key (a stored secret), the partner company vouches for them and the bank issues a short-lived pass that expires in minutes. No keys to steal, no keys to lose.
- Branch protection is the security guard at the front door. Before anyone can enter (merge code), the guard checks their identity (reviews), makes sure they passed the background check (status checks), and verifies their signature (signed commits). No exceptions.
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
- Auto-generated at the start of every workflow run β no setup required
- Expires when the job completes or after 24 hours, whichever comes first
- Default permissions depend on repository settings: either read & write (permissive) or read-only (restrictive)
- Principle of least privilege: always restrict permissions to only what the job needs
Restricting Permissions
Declare a permissions block at the workflow or job level. Any scope not listed is automatically set to none:
# 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
# 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
| Scope | Controls Access To | Common Use Case |
|---|---|---|
actions | Workflow runs, artifacts, caches | Manage workflow runs from another workflow |
checks | Check runs and check suites | Create status checks on commits |
contents | Repository contents, commits, branches, tags | Checkout code, create releases |
deployments | Deployment statuses | Track deployment progress |
id-token | OIDC token request | Authenticate to cloud providers via OIDC |
issues | Issues and comments | Auto-label or comment on issues |
packages | GitHub Packages | Publish npm / Docker / NuGet packages |
pages | GitHub Pages | Deploy static sites |
pull-requests | Pull requests and comments | Post PR comments, approve PRs |
repository-projects | Project boards | Automate project card movements |
security-events | Code scanning, Dependabot alerts | Upload SARIF results |
statuses | Commit statuses | Post 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.
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?
- No long-lived secrets β nothing to rotate, nothing to leak
- Automatic rotation β tokens are generated fresh for every workflow run
- Fine-grained trust β cloud providers validate the JWT claims (repo, branch, environment) before issuing credentials
How It Works
ββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββ
β 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
- App Registration β create (or reuse) an app in Azure AD
- Federated Credential β add a credential that trusts GitHub's OIDC provider
- Subject claim β scope to specific repo, branch, or environment
# 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
- IAM Identity Provider β add
token.actions.githubusercontent.comas an OIDC provider - IAM Role β create a role that trusts the OIDC provider
- Trust condition β restrict to your repo using
subclaim
{
"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
- Workload Identity Pool β create a pool for GitHub Actions
- Provider β add an OIDC provider pointing to GitHub
- Service Account binding β bind the pool to a GCP service account with appropriate roles
# 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 Pattern | Trust Scope |
|---|---|
repo:myorg/myrepo:ref:refs/heads/main | Only the main branch |
repo:myorg/myrepo:environment:production | Only the production environment |
repo:myorg/myrepo:ref:refs/tags/v* | Only version tags |
repo:myorg/myrepo:pull_request | Only pull request workflows |
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
- Specify which workflow jobs must pass before a PR can be merged
- Prevents broken or untested code from reaching production branches
- Configure via
Settings β Branches β Add rule β Require status checks to pass
Required Reviews
- Enforce a minimum number of approving reviews (1, 2, or more)
- Optionally require review from code owners (
CODEOWNERSfile) - Dismiss stale reviews when new commits are pushed
Signed Commits
- Require all commits in a PR to be GPG or SSH signed
- Ensures the committer's identity is cryptographically verified
- Prevents impersonation and unauthorized commits
GitHub Rulesets (New)
Rulesets are the next generation of branch protection β more flexible and stack-able:
- Stack-able: multiple rulesets can apply to the same branch β their rules are combined
- Bypass list: designate specific teams or apps that can bypass rules (e.g., release bots)
- Tag rules: protect tags the same way you protect branches
- Organization-level: enforce rules across all repos from a single config
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:
# β 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:
# .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
- SLSA (Supply-chain Levels for Software Artifacts) is a framework for ensuring build integrity
- Provenance records who built what, when, where, and how β cryptographically signed
- Use
actions/attest-build-provenanceto generate attestations for your build outputs
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
- At the org level, restrict which actions can be used:
Settings β Actions β General β Actions permissions - Options: allow all, allow only local actions, allow select actions (allow-list)
- Enterprise orgs should maintain a curated allow-list of vetted actions
Fork Pull Request Restrictions
- Workflows triggered by fork PRs run with read-only permissions and no access to secrets by default
- Require approval before running workflows from first-time contributors
- Configure:
Settings β Actions β General β Fork pull request workflows - Never auto-approve fork PR workflows β a malicious PR can exfiltrate secrets if workflows have write permissions
pull_request_target Trappull_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
- Set org/repo default token permissions to read-only
- Declare explicit
permissionsin every workflow and job - Pin all third-party actions by SHA, not tag
- Enable Dependabot for
github-actionsecosystem - Use OIDC federation instead of stored cloud secrets
- Require status checks and PR reviews before merge
- Restrict fork PR workflows β require approval for first-time contributors
- Never use
pull_request_targetto run untrusted PR code - Maintain an allow-list of approved actions at the org level
- Generate SLSA provenance for all production build artifacts
π OIDC Authentication Flow
ββββββββββββββββ ββββββββββββββββββββββββ ββββββββββββββββββββ
β β 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
- Navigate to your repository
Settings β Actions β General - Under Workflow permissions, select "Read repository contents and packages permissions"
- Save β all existing workflows now run with read-only tokens unless they explicitly declare permissions
- Test: trigger a workflow that writes (e.g., creates a comment). Observe the
403failure, then add the requiredpermissionsblock
Lab 2: Configure GITHUB_TOKEN Scoping
- Create a workflow with a job that comments on a PR:
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!'
})
- Open a PR and verify the comment appears
- Remove the
permissionsblock β observe the failure when the default is read-only
Lab 3: Pin an Action by SHA
- Visit the action's repository on GitHub (e.g.,
actions/checkout) - Find the latest release tag (e.g.,
v4.1.1) and copy the full commit SHA from the tag - Replace
uses: actions/checkout@v4withuses: actions/checkout@<FULL_SHA> # v4.1.1 - Add a
.github/dependabot.ymlforgithub-actionsto receive update PRs
Lab 4: Set Up Required Status Checks
- Create a workflow that runs tests on every PR
- Navigate to
Settings β Branches β Add ruleformain - Enable "Require status checks to pass before merging"
- Search for and select your workflow's job name (e.g.,
test) - Open a PR with a failing test β verify that GitHub blocks the merge
π Debugging Common Issues
"Resource not accessible by integration"
- Cause: The
GITHUB_TOKENdoesn't have the required permission scope - Fix: Add the missing scope to the
permissionsblock. For example, if you're creating PR comments, addpull-requests: write. If creating releases, addcontents: write - Check: Verify the org/repo default isn't overriding your workflow permissions β go to
Settings β Actions β General β Workflow permissions
"OIDC token audience mismatch"
- Cause: The audience (
aud) claim in the JWT doesn't match what the cloud provider expects - Fix (Azure): Ensure the federated credential's audience is
api://AzureADTokenExchange - Fix (AWS): Ensure the OIDC provider audience is
sts.amazonaws.com - Debugging: Print the token to inspect claims:
curl -s "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" | jq -R 'split(".") | .[1] | @base64d | fromjson'
"Status check not found" in branch protection
- Cause: The status check name doesn't match the workflow's
name+job namecombination - Fix: Branch protection looks for
<workflow_name> / <job_name>. If your workflow is namedCIand the job istest, the status check isCI / test. Ensure you select this exact name when configuring required checks - Tip: Run the workflow at least once before configuring the branch protection rule β GitHub won't show status checks it hasn't seen yet
π― 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):
- All workflows ran with read-write
GITHUB_TOKENby default - Actions referenced by tag (
@v4) with no SHA pinning - Azure Service Principal secrets stored in 30+ repositories with no rotation schedule
- No branch protection β any developer could push directly to
main - No audit trail for who deployed what and when
After (remediation):
- Org-wide read-only default β every workflow must explicitly declare write permissions. SOC 2 Control: AC-6 (Least Privilege)
- SHA pinning + Dependabot β all 200+ action references pinned by SHA. Dependabot opens weekly PRs for updates. SOC 2 Control: SA-22 (Supply Chain)
- OIDC federation only β deleted all stored Azure/AWS secrets. Federated credentials scoped to
environment:production. SOC 2 Control: IA-5 (Authenticator Management) - Rulesets enforced at org level β required 2 reviews, required status checks (lint + test + security scan), signed commits. SOC 2 Control: CM-3 (Change Control)
- Audit log streaming β all workflow runs, permission changes, and deployment events streamed to Splunk SIEM with 90-day retention. SOC 2 Control: AU-6 (Audit Review)
- SLSA provenance β every production artifact attested with
actions/attest-build-provenance. SOC 2 Control: SI-7 (Software Integrity)
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
- GITHUB_TOKEN: Auto-generated, short-lived token. Always restrict with explicit
permissionsblocks. Set org/repo default to read-only - OIDC: Exchange GitHub's JWT for short-lived cloud credentials. No stored secrets, automatic rotation, fine-grained trust via subject claims
- Branch protection: Required status checks, required reviews, signed commits. Use Rulesets for stackable, org-level enforcement
- Supply chain: Pin actions by SHA, enable Dependabot for updates, generate SLSA provenance, maintain an org-level allow-list
- Fork PRs: Read-only token, no secrets by default. Require approval. Never use
pull_request_targetto run untrusted code - Hardening: Follow the 10-point checklist β defence in depth means multiple overlapping security controls