GitHub personal access tokens let scripts, tools and integrations act with a user's permissions, and a single leaked token has repeatedly been enough to poison widely used software. Classic tokens can reach every repository a user can access across every organisation they belong to, and by default they never need to expire. GitHub now offers fine-grained tokens, organisation approval, maximum lifetime policies and token-level audit fields, but most of these controls are off, permissive or optional until an owner configures them. This guide explains how both token types work, which organisation and enterprise settings matter, how to detect token misuse and what a hardened baseline looks like. It is written for GitHub organisation and enterprise owners, platform engineers and security teams responsible for source code and CI/CD.
Key takeaways
- Fine-grained tokens are the default choice for people, scoped to one resource owner, selected repositories and specific permissions, with a 366-day maximum lifetime by default at organisation level.
- Classic tokens are the main risk: they span all of a user's organisations and repositories, have no expiry requirement by default and are not subject to approval, so restrict them unless a documented gap requires them.
- Organisation policies do the real work: restrict classic tokens, keep approval required for fine-grained tokens and set maximum lifetimes for both types.
- Automation should not run on a person's token. GitHub recommends GitHub Apps for long-lived integrations because they use short-lived tokens and do not depend on a user who may leave.
- Every token action can be traced: the audit log records a SHA-256
hashed_tokenfield, so a leaked token's activity can be scoped quickly once you hash it.
How identity and access work in GitHub
A personal access token (PAT) authenticates as the user who created it. It is used with the REST and GraphQL APIs and for Git over HTTPS (not SSH remotes). GitHub tells users to "treat your access tokens like passwords" and to prefer GitHub CLI or Git Credential Manager over creating a token at all. Tokens carry recognisable prefixes, which is what makes secret scanning and the revocation API effective.
| Prefix | Credential | Bound to | Typical lifetime |
|---|---|---|---|
ghp_ | Personal access token (classic) | A user, all their organisations and repositories, by OAuth scope | Optional expiry; revoked after one year unused |
github_pat_ | Fine-grained personal access token | A user, one resource owner, selected repositories, specific permissions | User-selected, default 30 days; capped by policy |
gho_ | OAuth app access token | A user's authorisation of an OAuth app | Until revoked |
ghu_ / ghr_ | GitHub App user access token and refresh token | A user, through a GitHub App | Eight hours by default, renewed with the refresh token |
ghs_ | GitHub App installation access token | The app installation, not a person | Short-lived |
GitHub is rolling out a new stateless ghs_ format from 27 April 2026, so any tooling that validates token length or pattern should be checked.
Fine-grained versus classic tokens
| Property | Fine-grained | Classic |
|---|---|---|
| Scope | One user or organisation; all or selected repositories; per-permission read or write | Broad OAuth scopes such as repo, across all organisations the user belongs to |
| Organisation approval | Required by default; pending tokens can read only public resources | Not available |
| Organisation visibility and revocation | Owners can list and revoke them | Owners cannot list or revoke them in the token UI |
| Expiry | Default 30 days at creation; organisation maximum 366 days by default | No requirement by default |
| SAML SSO | Authorised during creation | Must be authorised per organisation with Configure SSO |
| Per-user limit | Up to 50 tokens | Not stated |
| Known gaps | Cannot contribute to public repositories where the user is not a member, act as an outside collaborator, span several organisations, access Packages or call the Checks API | Covers these cases |
Decision guidance: use a fine-grained token for any personal scripting against your own organisation. Use a classic token only for the documented gaps above, with a short expiry and an exception record. Use a GitHub App for anything that runs unattended, and the built-in GITHUB_TOKEN inside GitHub Actions. See the GitHub and GitHub Actions Security Guide.
Admin roles and privileged access
- Organisation owners: set the organisation's token policy (Settings, Personal access tokens), approve or deny fine-grained token requests, and list and revoke active fine-grained tokens. Tokens created by owners are approved automatically, so owner accounts deserve the strongest protection.
- Enterprise owners (GitHub Enterprise Cloud): can restrict or allow each token type for all organisations, require or disable approval enterprise-wide, and set maximum lifetimes that organisations may only tighten. Enterprise-level restrict and allow choices cannot be overridden by organisations.
- Enterprise Managed Users: enterprise lifetime policies also apply to user namespaces because the enterprise owns those accounts. GitHub warns that setting a lifetime policy may interrupt services unless administrators are exempted.
- Administrator exemption: the enterprise lifetime setting can exempt enterprise administrators. Treat this as an exception that needs a reason, because administrator tokens are the most valuable to steal.
- Review delegation: GitHub's documentation describes token request review as an owner task; the REST endpoints for listing, reviewing and revoking fine-grained tokens can be called only by GitHub Apps, which lets you automate review without handing out owner rights to a person.
Keep the owner count small, enforce phishing-resistant MFA on owner accounts and review owner membership quarterly. See the Privileged Access Management Guide.
Integrations, API tokens and service accounts
The most common PAT failure is not a careless person but an integration built on a person's token. GitHub's own guidance explains why that is fragile: a PAT is tied to a user, so automation breaks or keeps working on borrowed authority when that user changes role or leaves. A GitHub App installed on an organisation does not depend on any user, does not consume a seat, uses fine-grained permissions and receives short-lived installation tokens.
| Use case | Recommended credential | Why |
|---|---|---|
| Workflow in the same repository | GITHUB_TOKEN with minimal permissions: | Created per job and expires with it |
| Workflow touching other repositories or organisations | GitHub App installation token | Short-lived, scoped, not tied to a person |
| Third-party SaaS integration | GitHub App from the vendor | Owner-visible installation with explicit permissions |
| Deploying to cloud from Actions | OIDC federation to the cloud provider | Removes stored cloud keys entirely |
| Personal scripting and API testing | Fine-grained PAT, short expiry, selected repositories | GitHub suggests PATs for testing and short-lived scripts |
| "Machine user" with a PAT | Avoid; migrate to a GitHub App | Shared password-equivalent with no natural owner |
Where a token must exist, store it in a secrets manager or Actions or Codespaces secrets, never in code, issue one token per integration so it can be revoked alone, and record an owner. Be especially careful with tokens stored as repository secrets in workflows triggered by pull_request_target, which can expose secrets to code from forks. A maintainer's PAT stored that way in a SpotBugs workflow was the starting point of the SpotBugs token leak that led to the reviewdog and tj-actions compromises. See the OWASP CI/CD Top 10 CICD-SEC-4 Poisoned Pipeline Execution Guide.
Logging and detection
For actions performed outside the web UI, the audit log records token details: hashed_token (the SHA-256 hash of the token, base64 encoded), programmatic_access_type and token_scopes. When a token leaks, hash the value locally (GitHub documents openssl dgst -sha256 -binary piped to base64), then search hashed_token:"VALUE" in the audit log UI or via the phrase parameter of GET /orgs/{org}/audit-log. UI and API searches do not return Git events, so export the log to review clone and push activity.
| What to watch | Source | Why |
|---|---|---|
Changes to auto-approval of token requests (auto_approve_personal_access_token_requests.enable and .disable) | Organisation audit log | Turning off approval silently widens access |
| Token activity from unexpected IP ranges or at unusual volume | Audit log streamed to SIEM, filtered by hashed_token and programmatic_access_type | Stolen tokens are used from attacker infrastructure |
| Mass clone or archive download by one token | Exported Git events | Typical of source code theft |
| Workflow file changes, new releases and tag moves by a bot or personal token | Audit log and repository events | Typical of supply chain compromise |
| Push protection bypasses | Security alerts, audit log, owner email | A secret was knowingly committed |
| Pending fine-grained token requests | Daily owner email and pending requests list | Unreviewed requests stall work or get rubber-stamped |
Prevention at the edge: user push protection is enabled by default and blocks pushing secrets to public repositories; repository push protection requires GitHub Secret Protection and is off until enabled. It covers command line pushes, web commits, uploads and REST API requests, and every bypass creates an alert and an audit entry. See the Git Secret Scanning Guide.
Metrics
- Number of classic tokens able to access the organisation (target zero once classic access is restricted).
- Percentage of active fine-grained tokens with expiry of 90 days or less.
- Number of integrations running on personal tokens versus GitHub Apps.
- Median time from secret scanning alert on a GitHub token to revocation.
- Push protection bypasses per month, by reason.
Hardening baseline
| Setting | Default | Recommended |
|---|---|---|
| Classic token access to organisation | Allowed | Restrict, with exceptions handled through a GitHub App or a temporary policy change |
| Fine-grained token access | Allowed | Allow |
| Require approval of fine-grained tokens | Required | Keep required; automate triage with a GitHub App calling the review endpoints |
| Fine-grained maximum lifetime | 366 days | 90 days or less for most organisations; shorter for write access to production code |
| Classic maximum lifetime | None | Set one (for example 30 days) even if classic access is restricted, as a backstop |
| Repository push protection | Off | Enable at organisation or enterprise level with delegated bypass |
| SAML SSO | Not configured | Enforce, so tokens need a linked identity and revoking it cuts access |
Two behaviours matter when you tighten policy. First, setting a maximum lifetime "does not revoke or disable" existing tokens: non-compliant tokens owned by members are blocked from the organisation and users discover it when API calls fail. Second, restricting tokens does not disable SSH keys that were created with them. Plan communications and an SSH key review accordingly.
Rolling out the baseline
- Inventory: list fine-grained tokens with organisation access, export audit log entries with
programmatic_access_typefor 90 days to find classic token and OAuth usage, and identify integrations running on personal tokens. - Migrate automation: move CI and integrations to
GITHUB_TOKEN, GitHub Apps or OIDC before touching policy. - Pilot: apply lifetime limits and classic restriction in a test organisation or a low-risk organisation first and watch for rejected API calls.
- Announce and enforce: give a dated notice, then set fine-grained maximum lifetime, a classic maximum lifetime and finally restrict classic access.
- Roll back safely: if a critical integration fails, re-allow classic access or raise the lifetime temporarily with an expiry date for the exception, rather than removing the policy permanently.
- Operate: review pending requests daily, review active tokens quarterly and include token revocation in leaver processing. See the Employee Offboarding Security Guide.
Small organisations can usually restrict classic tokens immediately and rely on owner approval. Large enterprises should set the enterprise policy, automate approval triage and treat GitHub tokens as non-human identities in their inventory. See the SaaS API Token Security Guide.
Framework mapping
| Framework | Relevant entries |
|---|---|
| MITRE ATT&CK v20 | T1528 Steal Application Access Token; T1550.001 Application Access Token; T1552.001 Credentials In Files; T1213.003 Code Repositories; T1195.002 Compromise Software Supply Chain |
| NIST SP 800-53 Rev. 5 | IA-5 Authenticator Management; IA-5(7) No Embedded Unencrypted Static Authenticators; AC-6 Least Privilege; AC-2(3) Disable Accounts; AU-6 Audit Record Review |
| NIST CSF 2.0 | PR.AA-01, PR.AA-05, DE.CM-03 |
| OWASP Non-Human Identities Top 10 | NHI2 Secret Leakage, NHI7 Long-Lived Secrets |
Common misconfigurations
- Classic tokens left allowed: any member's
repo-scoped token reaches every organisation they belong to and owners cannot see it. Restrict classic access. - "No expiration" tokens: unlimited lifetimes are allowed unless a maximum lifetime policy blocks them. The Home Depot token exposure left a leaked GitHub token usable for more than a year. See the OWASP NHI Top 10 NHI7 Long-Lived Secrets Guide.
- Bot accounts holding personal tokens: a privileged PAT for a bot or service account is a shared secret with broad reach. In the Trivy supply chain attack a stolen PAT and a non-atomic rotation left valid credentials behind, which were later used to publish a malicious release.
- Rotation after exposure delayed: the SpotBugs token was taken in December 2024 and used in March 2025. Rotate any token exposed to untrusted code immediately, even with no sign of misuse.
- Tokens in code and config: committed tokens are revoked automatically when found in a public repository or gist, but not in private repositories, so private leaks persist. See the OWASP NHI Top 10 NHI2 Secret Leakage Guide.
- Offboarding that ignores tokens: removal from the organisation ends a classic token's SSO authorisation there, but the token still works on the user's personal and other repositories. Ask leavers to revoke tokens and check for SSH keys created by tokens.
Practitioner checklist
- Restrict classic personal access tokens at organisation or enterprise level.
- Keep administrator approval required for fine-grained tokens and review the queue daily.
- Set maximum lifetimes for both token types, 90 days or less for fine-grained tokens.
- Migrate CI and integrations from personal tokens to
GITHUB_TOKEN, GitHub Apps or OIDC. - Enforce SAML SSO and phishing-resistant MFA for owners.
- Enable repository push protection with delegated bypass and alert on every bypass.
- Stream the audit log to the SIEM and document the
hashed_tokenlookup in the leak runbook. - Use
POST /credentials/revoke(unauthenticated, up to 1,000 credentials per request) to revoke GitHub tokens you find exposed but do not own. - Add GitHub tokens to the NHI inventory with an owner, purpose and expiry.
Standards and references
- GitHub Docs: Managing your personal access tokens
- GitHub Docs: Setting a personal access token policy for your organization
- GitHub Docs: Enforcing policies for personal access tokens in your enterprise
- GitHub Docs: Token expiration and revocation
- GitHub Docs: Identifying audit log events performed by an access token
- GitHub Docs: Deciding when to build a GitHub App
- GitHub Docs: About push protection
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: GitHub and GitHub Actions Security Guide · GitLab Tokens Security Guide · Secrets Management Guide · CI/CD Pipeline Identity Security Guide