Join our Newsletter — 33% off our NHI Course
Home› Guides› GitHub and GitHub Actions Security Guide: Tokens and…
Guide Software & AI Supply Chain

GitHub and GitHub Actions Security Guide: Tokens and Runners

← All guides
By Lalit Choda, NHI Mgmt Group Updated 18 October 2026 17 min read
On this page

A GitHub organisation is a credential store, a build system and a release channel at once, and most GitHub compromises come down to one token, one runner or one workflow trusted more than it should be. For the wider picture see the CI/CD Security Guide; this guide goes deeper on GitHub Actions security and the platform itself: the credentials GitHub issues, GitHub token permissions, GitHub Apps, OIDC, workflow attacks, third-party actions, runners, rulesets and the audit log. It is for platform engineers, organisation owners and application security teams who want GitHub Actions hardening settings they can apply.

Key takeaways

  • Every GitHub credential is a non-human identity. Tokens, App keys and deploy keys differ sharply in scope and lifetime; choose deliberately and inventory them.
  • Default to read, grant per job. Set the organisation default for GITHUB_TOKEN to read-only and declare permissions in every workflow, with write scopes only on the job that needs them.
  • Replace cloud keys with OIDC, and pin the trust. Federated tokens remove stored cloud secrets only if the cloud trust policy checks the subject, ideally repository and environment.
  • Third-party actions run with your secrets. Enforce full-length SHA pinning through the allowed actions policy, block known-bad actions, and review what you allow; the tj-actions compromise showed how fast a moved tag spreads.
  • Self-hosted runners are production servers. Use just-in-time or ephemeral runners, restrict runner groups to named repositories and workflows, and never attach them to public repositories.

The GitHub credential map

Each row below is a machine credential with its own failure mode. See the CI/CD Pipeline Identity Security Guide for the identity model across platforms.

CredentialScopeLifetimeUse it forMain risk
GITHUB_TOKENThe workflow's repository, limited by permissionsEnds with the job; at most 6 hours hosted, 24 hours self-hostedAnything inside the same repositoryWrite scopes exposed to injected or third-party code
GitHub App installation tokenRepositories and permissions granted to the installation, narrowable per token1 hourCross-repository automation, bots, integrationsLeaked app private key mints new tokens
Fine-grained personal access tokenOne user or organisation owner, optionally selected repositories, specific permissionsSet by user, capped by organisation policyPersonal scripts where an App is impracticalActs as the user; survives role changes
Classic personal access tokenAll repositories in every organisation the user can access, by broad scopeOptional expiry; removed after a year unusedOnly the few cases fine-grained tokens do not supportBroadest blast radius on the platform
Deploy keyOne repository, read-only unless write is tickedNo expiryRead-only checkout on a serverUsually no passphrase; not tied to membership
OIDC ID tokenA single job, exchanged for cloud credentialsMinutesCloud deploymentsLoose trust conditions in the cloud role

GitHub's own guidance points the same way: use fine-grained tokens instead of classic ones, and for organisation resources or long-lived integrations, "you should use a GitHub App." Treat shared tokens and bot accounts as service accounts. See the Service Account Security Guide.

GITHUB_TOKEN and workflow permissions

"The GITHUB_TOKEN secret is a GitHub App installation access token", created at the start of each job. Its permissions come from the repository or organisation default ("Workflow permissions") and the workflow's permissions key. Older organisations may still grant read and write by default, so check the setting at every level, and turn off "Allow GitHub Actions to create and approve pull requests" unless an automation genuinely needs it.

These are the GitHub Actions permissions to watch. Once a workflow lists any scope, every unlisted scope becomes none. Set permissions: {} or contents: read at the top of each workflow and raise scopes per job:

ScopeGrant write only whenRisk if over-granted
contentsThe job pushes commits, tags or creates releasesCode and tag tampering, workflow file changes
pull-requestsThe job comments, labels or opens pull requestsSelf-approval and review bypass paths
id-tokenThe job requests an OIDC token for cloud accessAny step in the job can mint cloud credentials
packagesThe job publishes to GitHub PackagesMalicious package or image publication
actionsThe job manages workflow runs, caches or artifactsCache and artifact manipulation
security-eventsThe job uploads code scanning resultsDismissal or suppression of findings

Events triggered with GITHUB_TOKEN do not start new workflow runs, except workflow_dispatch and repository_dispatch, and pull requests it opens, synchronises or reopens create runs that need approval from a user with write access. Do not route around this brake with a broad personal access token.

GitHub Apps, fine-grained tokens and deploy keys

GitHub Apps

An App authenticates with a JWT signed by its private key, then requests an installation token that "will expire after 1 hour". Request each token with the repositories or repository_ids and permissions parameters so it carries only what the task needs; it "cannot be granted permissions that the app was not granted". Keep the private key in a secrets manager or HSM and delete keys you cannot account for: the leaked GitHub App private keys case found 474 forgotten keys that still worked. Require owner approval for installations and review GitHub App permissions upgrades in the audit log.

Personal access token policy

  • Restrict classic tokens: organisation owners can stop classic tokens reaching organisation resources; by default both kinds are enabled, and classic tokens are not subject to approval or an expiry requirement.
  • Approve fine-grained tokens: "Require administrator approval" is the default; a pending token can read only public resources.
  • Cap lifetime: the default maximum for fine-grained tokens is 366 days; shorten it to 90 or fewer. Non-compliant tokens are blocked but not revoked, so clean them up.
  • Know the gaps: fine-grained tokens do not cover multiple organisations, Packages or the Checks API, so keep a short, owned list of classic token exceptions.

Deploy keys

GitHub lists the drawbacks plainly: deploy keys are "usually not protected by a passphrase", are "credentials that don't have an expiry date", and are not linked to organisation membership, so they survive the creator leaving. Replace write-enabled keys with a GitHub App, because a write key pushes without review.

OIDC from GitHub Actions to the cloud

With OIDC a job requests the GitHub Actions OIDC token (it needs id-token: write) and exchanges it with AWS, Azure or Google Cloud for short-lived credentials. The security sits entirely in the cloud trust condition, which compares claims such as sub, aud, repository_id, repository_owner_id, environment and job_workflow_ref.

Trust conditionVerdictWhy
Audience only, or a wildcard ownerNeverAny repository, possibly any GitHub user, can assume the role
repo:ORG/REPO:pull_requestAvoid for privileged rolesMatches jobs triggered by pull requests, including contributor code
repo:ORG/REPO:ref:refs/heads/mainAcceptableLimits to the default branch, but any workflow on it qualifies
repo:ORG/REPO:environment:prodPreferred for deploymentInherits environment reviewers and branch rules
Custom sub with job_workflow_refPreferred for central pipelinesOnly an approved reusable workflow at an approved ref can deploy

GitHub's reference says to define at least one subject condition so "untrusted repositories can't request access tokens". Repositories created after 15 July 2026 use an immutable default subject that embeds IDs, repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/BRANCH, so a re-registered name cannot produce the same subject. Customising the subject (include_claim_keys through PUT /repos/{owner}/{repo}/actions/oidc/customization/sub or the organisation endpoint) changes the whole sub format, so create the matching cloud condition first or deployments will fail. See the Cloud Workload Identity Guide.

Workflow attacks: injection, pull_request_target and cache poisoning

These GitHub Actions workflow attacks map to ATT&CK T1677 Poisoned Pipeline Execution: attacker code or input runs inside a trusted workflow and uses its token, secrets or cache.

  • GitHub Actions script injection: values such as pull request titles, issue bodies, comments and branch names are attacker-controlled. Interpolating them with ${{ }} inside a run: block makes them part of the shell script. Pass them through a quoted environment variable instead; CodeQL has queries for this pattern. The Ultralytics PyPI compromise started with an injectable workflow next to a publishing credential.
  • pull_request_target and workflow_run: both run with the base repository's token and secrets. They are safe only if they never check out or execute code from the pull request; build untrusted code under pull_request and treat its artifacts as untrusted data.
  • GitHub Actions cache poisoning: runs can restore caches created on their own branch or the default branch, and GitHub notes that privileged triggers "share the same cache of the main branch". A workflow that runs untrusted input in the default branch context can therefore write a cache entry that a later release job restores. GitHub's own warning is that "Anyone with read access can create a pull request on a repository and access the contents of a cache." Never cache secrets, give release workflows their own cache keys or no cache, and do not grant actions: write to jobs that handle untrusted input.

Require approval before fork pull request workflows run (first-time contributors at minimum); fork runs receive no secrets other than a read-only GITHUB_TOKEN.

Third-party actions and the Marketplace

Third-party actions risk is simple: an action runs inside your job with your token and secrets, so every uses: line is a dependency with production access. The tj-actions/changed-files compromise (CVE-2025-30066) is the reference case: CISA set the exposure window as 12 March 2025 00:00 to 15 March 2025 12:00 UTC, noted that secrets "may be obfuscated as a double-encoded base64 payload", and said identified secrets "should be considered compromised". It was enabled by an earlier compromise of reviewdog/action-setup@v1 (CVE-2025-30154); see the reviewdog/action-setup compromise.

  • Allowed actions policy: at enterprise or organisation level, allow GitHub-owned actions plus a named list rather than all actions. Since August 2025 the policy can block entries prefixed with ! (evaluated last) and enforce GitHub Actions SHA pinning, after which "any workflow that attempts to use an action that isn't pinned will fail".
  • Pin and update: pin actions to a commit SHA (all 40 characters) from the action's own repository with a version comment, and enable Dependabot version updates for the github-actions ecosystem so pins do not rot. GitHub's secure use reference also describes workflow dependency lockfiles, which record the exact commit of every action including transitive ones.
  • Review before allowing: a GitHub Actions Marketplace listing or verified creator badge identifies the publisher, not the safety of the code. Check the source, maintainers, release process and any nested unpinned actions.

Runners: GitHub-hosted and self-hosted

A GitHub-hosted GitHub Actions runner gives a fresh virtual machine per job and is the right default. Self-hosted runner security matters because GitHub warns a self-hosted GitHub Actions runner "can be persistently compromised by untrusted code in a workflow". To harden runner fleets:

ControlHow on GitHubWhat it prevents
Single-use runnersJust-in-time runners created through the REST API ("perform at most one job"), or ephemeral autoscaled runners such as Actions Runner Controller on KubernetesPersistence and cross-job secret theft
Runner groupsRestrict each group to selected repositories and, where available, selected workflows; separate production deploy runnersUntrusted repositories reaching privileged runners
No public repositoriesLeave "Allow public repositories" off on runner groupsAnyone who opens a pull request running code on your hardware
Network egress controlDedicated subnet, egress allowlist, cloud metadata endpoint blocked unless neededExfiltration and cloud credential theft from the host
Least host privilegeNon-root runner user, no Docker socket mounts, no shared credentials on diskEscape from job to host and to other jobs
Rebuild from imageImmutable, patched runner images rebuilt regularly; runner application kept currentDrift and known vulnerabilities

Monitor runner hosts like servers, with per-job process and network telemetry and alerts on new runner registrations. A self-hosted job can run for up to 5 days but GITHUB_TOKEN refreshes for only 24 hours, which tempts teams to add long-lived tokens; split long jobs instead.

GitHub Actions secrets

  • Scope by level: anything that can use OIDC should not be a stored secret; organisation secrets should be visible to selected repositories only; deployment credentials belong in environment secrets, because "a workflow job cannot access environment secrets until approval is granted by a reviewer".
  • One value per secret: GitHub advises against storing JSON, XML or YAML blobs as a secret, because redaction is less reliable for structured values; register any derived value as a secret so it is masked.
  • Block and detect leaks: enable secret scanning and repository push protection (it requires GitHub Secret Protection and is off by default at repository level), restrict who may bypass it with delegated bypass, and review bypasses, which are logged and emailed. See the Secrets Management Guide.

Repository and organisation controls

Rulesets and branch protection rules

Branch protection rules still work, but repository rulesets are better: "Multiple rulesets can apply to the same branch at the same time", "the most restrictive version of the rule applies", and organisation rulesets on Team and Enterprise cover many repositories. Apply an organisation ruleset to default and release branches requiring pull requests with approval, code owner review, dismissal of stale approvals, status checks and blocked force pushes and deletions. Add tag rules so release tags cannot be moved or deleted (the Laravel-Lang compromise rewrote every release tag with one stolen token), and push rulesets to block file types, paths or sizes across a private repository's fork network. Keep bypass lists short and reviewed.

CODEOWNERS

A CODEOWNERS file can sit in .github/, the root or docs/ (the first one found wins), and review requests use the copy on the pull request's base branch. Owners need write access; any invalid line is skipped, and files over 3 MB are ignored entirely, so validate the file in CI. Assign owners for .github/workflows/, .github/actions/ and the CODEOWNERS file itself, and enable "Require review from Code Owners", noting that approval from any one listed owner satisfies it.

GitHub organisation security and enterprise security settings

  • Authentication: require two-factor authentication for members; on Enterprise Cloud enforce SAML single sign-on, or use Enterprise Managed Users so accounts are provisioned and removed from your identity provider.
  • Base permissions: set member base permission to read or none, restrict repository creation, visibility changes, forking of private repositories and deletion to owners.
  • Network: use the IP allow list where your plan supports it.
  • Integrations: require owner approval for OAuth app access and GitHub App installations, and review third-party access in the same cycle as personal access tokens. See the SaaS and OAuth App Governance Guide.

Detection and measurement

Stream the GitHub audit log to your SIEM (audit log streaming is an Enterprise Cloud feature) and alert on:

  • Secrets changes: org.update_actions_secret and environment.create_actions_secret, environment.update_actions_secret and environment.remove_actions_secret, plus environment.remove_protection_rule.
  • App changes: integration_installation.create, integration_installation.repositories_added and integration_installation.version_updated (an App's permissions grew).
  • Workflow changes: commits that add or modify files under .github/workflows/ from accounts that do not normally touch them, the pattern behind GhostAction and Megalodon.
  • Bulk access: clone spikes in git.* events, new webhooks (hook.*) and ip_allow_list.* changes.

Measure monthly: workflows with explicit permissions (target 100%); third-party actions pinned to a SHA (target 100%, then enforce); classic tokens with organisation access (target zero outside owned exceptions); cloud deployments still using stored keys instead of OIDC; self-hosted runners that are not single-use; and median time to revoke a leaked GitHub credential.

Implementation roadmap

  1. Inventory (weeks 1 to 2): export Apps, OAuth apps, tokens, deploy keys, runners, secrets and workflows using pull_request_target, workflow_run or third-party actions; turn on audit log streaming.
  2. Tokens (weeks 2 to 4): set the default GITHUB_TOKEN to read-only on a pilot set of repositories, fix the failures by adding job-level permissions, then roll out organisation-wide. Rollback is per repository by reverting the setting while workflows are fixed.
  3. Actions policy (weeks 3 to 6): pin actions with automated pull requests, move the allowed actions policy to a named list, then enable SHA pinning enforcement on a pilot organisation before the enterprise. Failing unpinned workflows are the test; the checkbox is the rollback.
  4. Cloud access (weeks 4 to 10): replace stored cloud keys with OIDC, environment by environment, using environment or job_workflow_ref subjects; delete each old key only after a clean release.
  5. Runners (weeks 6 to 12): move self-hosted fleets to just-in-time or ephemeral runners, split runner groups by trust level and add egress control.
  6. Governance (ongoing): organisation rulesets, CODEOWNERS on workflow paths and quarterly review of exceptions.

Decision guidance

  • GitHub App or token: use an App for anything shared, long-lived or organisation-wide; a fine-grained token only for one person's short-term scripting; a classic token only for documented gaps; a deploy key only for read-only server checkout.
  • Small organisations: a read-only token default, SHA pinning enforcement, push protection, required two-factor authentication and one ruleset give most of the benefit.
  • Large enterprises: add Enterprise Managed Users or enforced SSO, an enterprise actions policy, a central deployment workflow trusted through job_workflow_ref, runner groups per business unit and SOC-owned audit detections.

Framework mapping

Control areaNIST SP 800-53CIS Controls v8.1ISO/IEC 27001:2022ATT&CK
Token scoping, App permissions, PAT policyAC-6, IA-5Control 5, Control 6A.5.15, A.8.2T1552.001
Rulesets, CODEOWNERS, reviewCM-5, SA-10Control 16A.8.4, A.8.32T1195.002
Workflow injection and trigger hygieneSA-11, SI-10Control 16A.8.25, A.8.28T1677
Third-party actionsSR-3, SA-9Control 16A.5.21T1195.002
Audit log and detectionAU-6, AU-12Control 8A.8.15, A.8.16T1677

Common failure patterns

  • Leaked tokens left live: in the Home Depot token exposure a leaked GitHub token left internal systems open for more than a year.
  • Over-scoped release tokens: the Amazon Q Developer extension compromise abused an over-scoped GitHub token.
  • Unpinned actions: workflows referencing a mutable tag ran attacker code during the tj-actions window; SHA pinning would have prevented it.
  • Stolen accounts planting workflows: GhostAction pushed workflows that exfiltrated 3,325 secrets.
  • One token, many repositories: in the GitHub internal repositories breach a single stolen CLI token led to thousands of repositories.

Practitioner checklist

  • Set the organisation default GITHUB_TOKEN to read-only and require explicit permissions in every workflow.
  • Disable "Allow GitHub Actions to create and approve pull requests" unless an owned automation needs it.
  • Restrict classic personal access tokens, keep fine-grained approval on, and cap lifetime at 90 days or less.
  • Move shared automation to GitHub Apps with per-token repository and permission narrowing; protect and rotate App private keys.
  • Replace stored cloud keys with OIDC trusted on environment or job_workflow_ref subjects.
  • Enforce SHA pinning and a named allowed actions list; enable Dependabot updates for actions.
  • Remove pull_request_target jobs that check out pull request code, and keep caches out of release workflows.
  • Run self-hosted runners as just-in-time or ephemeral, in restricted runner groups, never for public repositories.
  • Apply organisation rulesets with tag protection, and CODEOWNERS on .github/workflows/ and the CODEOWNERS file.
  • Stream the audit log and alert on secret, App, workflow file and runner changes.

Standards and references

Related NHI Mgmt Group resources: CI/CD Security Guide · CI/CD Pipeline Identity Security Guide · Software Supply Chain Security Guide · Secrets Management Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Written and reviewed by Lalit Choda, NHI Mgmt Group. Last updated 18 October 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org