Most GitHub automation still runs on a personal access token belonging to whoever set it up, which means the integration's reach, lifetime and audit trail are those of a person rather than a machine. GitHub offers three identities for code that calls its API: GitHub Apps, personal access tokens (PATs, in fine-grained and classic forms) and OAuth apps. They differ in who the activity is attributed to, how far access reaches, how long credentials live and who in the organisation controls them. This guide gives the short answer, a side-by-side comparison, a threat model and controls for each option, worked scenarios and a decision checklist. It is for platform engineers, DevOps and security teams choosing an identity for CI, bots, internal tools and third-party integrations, and for reviewers deciding whether an existing integration should be migrated.
This is an independent guide from NHI Mgmt Group, based on GitHub's public documentation as of November 2026. It is not sponsored or reviewed by GitHub. Product features change, so confirm settings against GitHub's current documentation.
Key takeaways
- Default to a GitHub App for anything long-lived. GitHub's own guidance recommends a GitHub App for long-lived integrations and over OAuth apps generally: fine-grained permissions, installer-chosen repositories and installation tokens that expire after one hour.
- PATs are for people and short-lived scripts. A PAT is tied to its creator and stops working when that user loses access, so automation built on one inherits a person's lifecycle and blast radius.
- Choose fine-grained over classic. A classic PAT reaches every repository its user can access; a fine-grained PAT is limited to one owner, selected repositories and specific permissions, and organisations can require approval.
- OAuth apps always act as the user. They use broad scopes such as
repo, reach everything the authorising user can see and, by default, issue tokens that last until revoked. - Inside Actions, use neither. For workflows, use the built-in
GITHUB_TOKENwith minimal permissions, and OIDC federation for cloud access; reach for a GitHub App token only when the workflow needs more thanGITHUB_TOKENcan grant.
The short answer
| Use case | Use | Why |
|---|---|---|
| Bot, CI service, internal tool or integration acting for the organisation | GitHub App, authenticating as an installation | Activity attributed to the app, repository-scoped, one-hour tokens, survives staff changes |
| Tool that must act as the signed-in user (code review UI, developer portal) | GitHub App with user access tokens | Actions limited to what both the app and the user can do; tokens can expire and refresh |
| Workflow inside one repository | GITHUB_TOKEN with explicit permissions | Created per job and scoped to the repository; no stored secret |
| One-off script, API testing, personal tooling | Fine-grained PAT with expiry | Quick to create, limited to selected repositories and permissions |
| Feature only classic PATs support (some outside collaborator and public-repository contribution cases, Packages) | Classic PAT, minimal scopes, short expiry, recorded exception | No better option for that specific call; contain it |
| Existing third-party product that only offers OAuth | OAuth app, approved by an owner under OAuth app access restrictions | Accept with compensating controls; ask the vendor for a GitHub App |
Side-by-side comparison
The facts below are taken from GitHub's documentation on the differences between GitHub Apps and OAuth apps, on managing PATs and on REST API rate limits.
| Property | GitHub App | Fine-grained PAT | Classic PAT | OAuth app |
|---|---|---|---|---|
| Acts as | Itself (a name[bot] account) or on behalf of a user | The creating user | The creating user | The authorising user, always |
| Permission model | Fine-grained read or write per permission | Fine-grained per permission | Broad scopes | Broad scopes (e.g. repo for issues or pull requests) |
| Repository reach | Repositories chosen at installation | One resource owner; all or selected repositories | Everything the user can access, across organisations | Everything the authorising user can access |
| Credential lifetime | Installation tokens expire after one hour; user tokens can expire and be refreshed | Set by the creator; organisations can cap it (default cap 366 days) | Set by the creator; no cap by default; removed after a year unused | Long-lived by default; optional eight-hour expiring user tokens with refresh |
| Long-term secret | App private key (up to 25 per app, no expiry) | The token itself | The token itself | Client secret plus each user's token |
| Organisation control | Owners install and choose repositories; owner approval for organisation permissions | Organisation can require admin approval (default) and restrict access | Organisation can restrict access; no approval flow | OAuth app access restrictions (on by default for new organisations) require owner approval |
| When the creator leaves | Keeps working (owned by the organisation if created there) | Stops working when the user loses access | Stops working when the user loses access | That user's tokens stop working |
| REST rate limit | 5,000 per hour minimum per installation, scaling to 12,500 (15,000 on Enterprise Cloud) | User's 5,000 per hour | User's 5,000 per hour | User's 5,000 per hour |
| Webhooks | One built-in webhook, disabled on uninstall | None | None | Per repository or organisation, needs hook scopes, not cleaned up automatically |
| Token prefix | ghs_ installation, ghu_ user, ghr_ refresh | github_pat_ | ghp_ | gho_ |
The prefixes matter for detection: secret scanners and your own log searches can tell at a glance which kind of identity leaked. For a wider look at token types, see the API Keys vs OAuth Tokens Guide.
GitHub Apps
Architecture in words
A GitHub App is a registered identity with its own permissions and a private key. The app's backend signs a JSON Web Token (JWT) with that key, using RS256, with an expiry no more than 10 minutes ahead; GitHub recommends setting the issued-at time 60 seconds in the past to absorb clock drift. The JWT lets the app act as itself, for example to list installations, and to request an installation access token for a specific installation. That installation token carries only the permissions and repositories granted when an owner installed the app, and expires after one hour. Activity is attributed to the app's bot account. Alternatively, the app can obtain a user access token through a user's sign-in, and then acts only where both the app and the user have access.
Threat model
- Private key theft: the key is the crown jewel. Anyone holding it can mint installation tokens for every installation of the app until the key is deleted, because keys do not expire. This maps to T1552.001 Credentials In Files when keys are left in repositories or images.
- Over-broad installation: installing on "All repositories" when the app needs three, or granting write where read would do.
- Multi-tenant apps: a third-party app installed across many customers concentrates risk in its vendor's backend; compromise there yields tokens for every customer installation.
- Token replay within the hour: a stolen installation token is usable until it expires (T1550.001 Application Access Token), which limits but does not remove exposure.
Controls
- Key custody: GitHub's guidance is to keep the private key in a key vault configured sign-only, never hard-coded even in a private repository; environment variables are weaker. Keep two keys so a compromised one can be deleted without downtime (up to 25 keys per app).
- Least privilege: request only the permissions used, install on selected repositories, and review installations when the app's code changes its needs.
- Ownership: register organisation apps under the organisation, not a personal account, and record a technical owner.
- Token handling: request installation tokens just in time, optionally narrowed to specific repositories and permissions at request time, and do not cache them beyond use.
Failure modes
Keys pasted into CI variables and forgotten; one app shared across unrelated systems so its permissions become the union of all needs; installations left on repositories after a project ends.
Personal access tokens
Architecture in words
A PAT is a bearer secret that stands in for a password when a user calls the API or Git over HTTPS. A classic PAT carries scopes and reaches every repository the user can access, in every organisation. A fine-grained PAT is bound to one resource owner (a user or an organisation), lists all or selected repositories, and carries specific permissions; a user can hold up to 50. Neither type can exceed the creator's own access, and both become inactive if the creator loses access to the resource. GitHub removes classic PATs unused for a year.
Threat model
- Leak in code or CI: PATs are long-lived strings that end up in repositories, logs and workflow secrets. The SpotBugs token leak began when a maintainer's personal token was added to a workflow that ran code from pull requests, and the stolen token led on to other projects months later.
- Blast radius of a person: a classic PAT belonging to an organisation owner carries owner-level reach across every organisation they belong to.
- Attribution gap: actions appear as the user, so automated changes and human changes are indistinguishable without token data in the audit log.
- Lifecycle coupling: automation breaks when the person leaves, which pushes teams to keep leavers' accounts alive or to share accounts.
Controls
- Organisation PAT policy: restrict classic PAT access to organisation resources, keep "Require administrator approval" for fine-grained tokens (the default), and set maximum lifetimes for both types; GitHub blocks non-compliant tokens rather than revoking them. See How to set expiry policies on personal access tokens.
- Use alternatives for people: GitHub suggests GitHub CLI or Git Credential Manager for command-line access instead of hand-made PATs.
- Push protection: block tokens from being committed at all; see How to enable GitHub secret scanning push protection.
- Machine users as a last resort: where a PAT must drive automation, a dedicated machine account with a fine-grained token is better than a person's token, but it is still a shared credential to govern. See How to audit GitHub outside collaborators and machine users.
Detailed PAT hardening is in the GitHub Personal Access Tokens Security Guide.
Failure modes
Non-expiring tokens created "temporarily"; a fine-grained PAT set to all repositories; classic PATs kept because one endpoint needed them, then reused everywhere.
OAuth apps
Architecture in words
An OAuth app is a registered client with a client ID and secret. A user signs in and authorises requested scopes; the app receives an access token that acts as that user, with the user's identity on every action. Scopes are coarse: reading issues or pull requests requires repo, which also grants code access. The token reaches everything the user can access, and lasts until revoked unless the app opts into eight-hour expiring tokens with refresh tokens. Webhooks have to be created per repository or organisation with hook scopes and are not removed when access ends.
Threat model
- Token theft from the vendor: a third-party service holding thousands of users' long-lived
gho_tokens is a high-value target (T1528 Steal Application Access Token). - Consent abuse: users authorising look-alike or over-scoped apps. See the Consent Phishing and Illicit OAuth Grants Guide.
- Orphaned hooks: webhooks that keep sending repository events after the integration is retired.
Controls
- Keep OAuth app access restrictions on: they are enabled by default for new organisations. Members can then only request access, and owners approve. Unapproved apps lose access to private organisation resources.
- Prefer expiring tokens: where you build or configure an OAuth app, turn on expiring user tokens.
- Review approved apps: scopes, last use and owner, at least quarterly; see How to audit GitHub Apps and OAuth apps in an organisation.
- Ask for a GitHub App: when a vendor offers both, choose the GitHub App; when it offers only OAuth, record it as an exception with a review date.
Failure modes
Restrictions turned off to unblock one team and never re-enabled; app approved for an organisation owner, which gives it everything visible to the owner's token in that organisation.
What about GITHUB_TOKEN and OIDC
Two further identities often replace all three options above in CI. Each workflow job gets a GITHUB_TOKEN scoped to its repository; set permissions explicitly at workflow or job level so it carries only what the job needs (see How to Restrict GITHUB_TOKEN Permissions in Workflows). Its REST limit is 1,000 requests per hour per repository (15,000 for Enterprise Cloud resources). For cloud deployments, OIDC federation exchanges a short-lived GitHub-issued identity token for cloud credentials, removing stored cloud keys altogether; see the GitHub Actions OIDC Federation Hardening Guide. Use a GitHub App token inside a workflow only when the job must reach other repositories or organisation resources.
Worked scenarios
1. Release bot that tags and publishes across 40 repositories
The team runs it today on a lead engineer's classic PAT with repo and write:packages. Risk: the token reaches every repository the engineer can access in every organisation, and the bot breaks when they move team. Decision: a GitHub App owned by the organisation, installed on the 40 repositories, with Contents write and the minimum other permissions; private key in a sign-only vault; installation tokens minted per run. Package publishing: confirm whether the registry in use accepts GitHub App tokens and, if a classic PAT is still needed for that one step, isolate it on a machine account with only that scope and a short expiry.
2. Internal developer portal showing each engineer their pull requests
The portal must act as the signed-in user. Decision: a GitHub App using user access tokens, so the portal can do only what both the app permissions and the user allow, with refresh rather than long-lived tokens. An OAuth app would need repo scope and reach all of the user's repositories.
3. Security team script to inventory branch protection
Run weekly by one analyst. Decision: a GitHub App with read-only Administration and Metadata permissions installed on all repositories gives a stable, attributable identity. If the script is genuinely one-off, a fine-grained PAT with the organisation as resource owner, read-only permissions and a seven-day expiry, is acceptable.
4. Third-party code quality SaaS
The vendor offers OAuth only. Decision: an owner approves it under OAuth app access restrictions, knowing that an app authorised by an owner can reach anything visible to that owner's token in the organisation; prefer having a non-owner member request and use it where the workflow allows. Record scopes and owner, review quarterly, and ask the vendor for a GitHub App on its roadmap.
Decision checklist
- Does it run inside a single repository's workflow? Use
GITHUB_TOKEN, plus OIDC for cloud access. Stop here if that covers it. - Must actions be attributed to a person? Use a GitHub App with user access tokens; use an OAuth app only if you cannot change the product.
- Will it run for longer than a few weeks or outlive its author? Use a GitHub App installation.
- Is it a short, personal task? Fine-grained PAT, selected repositories, minimum permissions, expiry in days.
- Does it need a capability only classic PATs provide? Classic PAT on a machine account, minimum scopes, short expiry, written exception.
- Who holds the long-term secret? Private key in a vault, PAT in a secrets manager, OAuth tokens with the vendor: decide whose compromise you are accepting.
Small vs large organisations: a small team with a handful of repositories can begin by enforcing fine-grained PAT approval and expiry and moving its two or three busiest automations to GitHub Apps. Large enterprises should treat GitHub Apps as the standard, provide a shared internal service for minting installation tokens so teams do not each handle private keys, and restrict classic PATs at enterprise level.
Detection and measurement
GitHub audit log events carry token data: hashed_token (the SHA-256 hash of the token), programmatic_access_type and token_scopes. Searching hashed_token:"VALUE" finds activity from a specific PAT, OAuth token or GitHub App token, though Git events need an audit log export. Use this to separate machine and human activity and to scope a leak; see the GitHub Audit Log Detection Guide.
- Alert on: new GitHub App installations or permission changes, new OAuth app approvals, fine-grained PAT approval requests for write permissions, and private key creation on organisation apps (T1098.001 Additional Cloud Credentials is the closest ATT&CK pattern for added credentials).
- Hunt for: classic PAT use against organisation resources, tokens used from unexpected IP ranges, and app tokens touching repositories outside the app's normal set.
| Metric | Direction |
|---|---|
Share of API activity from GitHub App and GITHUB_TOKEN identities versus PATs | Rising |
| Classic PATs with access to organisation resources | Zero, or a recorded exception list |
| Fine-grained PATs without expiry or above the lifetime cap | Zero |
| GitHub Apps installed on all repositories without a recorded justification | Falling |
| Approved OAuth apps without an owner or review in 90 days | Zero |
| Median time from leaked-token alert to revocation | Hours, not days |
Common failure patterns
- Personal token in CI: a maintainer's PAT added to workflow secrets to work around a permission problem, as in the SpotBugs token leak.
- Migrating to an app but keeping the key in the repository: moving from PAT to GitHub App, then committing the private key, which never expires.
- "All repositories" by default: installing apps or creating fine-grained PATs with all-repository access because selecting repositories was inconvenient.
- Disabling OAuth restrictions: switching off restrictions to let one tool work, which removes owner approval for every OAuth app.
- Leaver accounts kept alive: preserving a departed engineer's account because automation depends on their token.
Framework mapping
| Control objective | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|
| Distinct, owned identities for automation | IA-4, AC-2 | PR.AA-01 | A.5.16 |
| Token and key lifetime, storage, rotation | IA-5 | PR.AA-01 | A.5.17 |
| Least privilege permissions and repository scope | AC-6 | PR.AA-05 | A.5.15, A.8.2 |
| Approval and review of apps and tokens | AC-2 | PR.AA-05 | A.5.18 |
| Monitoring token activity | AU-6 | DE.CM-03 | A.8.16 |
Leaked tokens are a common route into software supply chains (T1195.002 Compromise Software Supply Chain), which is why repository identity choice sits in supply chain programmes as well as identity governance.
Practitioner checklist
- Inventory every GitHub App installation, OAuth app approval and PAT with organisation access, with an owner for each.
- Make GitHub Apps the default for long-lived automation and integrations, owned by the organisation.
- Store app private keys in a sign-only vault, keep a second key for rotation, and never commit them.
- Restrict classic PAT access to organisation resources and record any exceptions.
- Keep administrator approval for fine-grained PATs and set maximum lifetimes for both PAT types.
- Keep OAuth app access restrictions on and review approved apps quarterly.
- Set explicit
permissionsonGITHUB_TOKENand use OIDC for cloud access from workflows. - Use
hashed_tokensearches and token prefixes to attribute activity and scope leaks.
Standards and references
- GitHub Docs: Differences between GitHub Apps and OAuth apps
- GitHub Docs: Deciding when to build a GitHub App
- GitHub Docs: Managing your personal access tokens
- GitHub Docs: Setting a personal access token policy for your organization
- GitHub Docs: About OAuth app access restrictions
- GitHub Docs: Managing private keys for GitHub Apps
- GitHub Docs: Rate limits for the REST API
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: GitHub Personal Access Tokens Security Guide · How to audit GitHub Apps and OAuth apps in an organisation · GitHub and GitHub Actions Security Guide · GitHub Actions OIDC Federation Hardening Guide · API Keys vs OAuth Tokens Guide