An enterprise with excellent SAML, complete SCIM and rigorous access reviews can still have hundreds of credentials belonging to nobody, doing anything, forever.
Machine identity is the layer that identity programmes reliably leave uncovered, and it is where most of the standing access in a mature GitHub estate actually lives.
The short answer
Section titled “The short answer”Four mechanisms, in descending order of preference.
GitHub Apps. An app is an identity in its own right, with explicitly scoped permissions, per-repository installation, short-lived installation tokens, and attributable actions. This is the right answer for essentially all automation.
The workflow token. Inside GitHub Actions, the token available to a workflow is scoped to the repository, short-lived, and requires no storage. Use it wherever it suffices.
Fine-grained personal access tokens. Scoped to specific repositories with explicit permissions and an expiry. Acceptable for individual use and for a genuinely small automation, and still owned by a person.
Classic personal access tokens and deploy keys. Broad, long-lived, and hard to govern. Use only where nothing else works, and inventory them.
Why user tokens are the wrong answer
Section titled “Why user tokens are the wrong answer”The single most common machine-identity mistake, and it fails in several ways at once.
It is owned by a person. When they leave and SCIM deprovisions them, the token stops working and something in production breaks. This is the failure that recurs in every organisation and is discovered the same way every time.
It carries their access. A personal access token has the permissions of the person, which is far more than the automation needs. A build pipeline authenticating as a senior engineer can do everything that engineer can do.
Its actions are attributed to them. The audit log shows the person, not the process. When something unexpected happens, the investigation starts by asking a confused engineer why they deleted a branch at 3 a.m.
It is affected by identity controls it should not be. Under SAML it needs authorising. Under conditional access it may be blocked from the location the automation runs.
It is invisible. Personal tokens are created by individuals and are not enumerable in the way organisation-owned resources are.
The correct response to finding one is not to rotate it. It is to replace it with a GitHub App.
GitHub Apps as the answer
Section titled “GitHub Apps as the answer”An app is not a person. It has its own identity, its own permissions, and its own audit trail.
Permissions are explicit and minimal. An app declares exactly what it needs — read on contents, write on pull requests — rather than inheriting a person’s access.
Installation is per repository or per organisation. An app installed on twelve repositories cannot touch the thirteenth. This is the property that makes least privilege achievable.
Installation tokens are short-lived. The app authenticates with its private key to obtain a token that expires quickly, rather than holding a long-lived credential.
Actions are attributable to the app. The audit log names it, which makes investigation tractable.
It survives people leaving. An app’s existence has nothing to do with any individual’s employment.
The cost is setup effort, and it is a real cost — creating and configuring an app is more work than generating a token. That cost is paid once; the token’s cost is paid continuously.
The private key problem
Section titled “The private key problem”An app authenticates with a private key, and that key is the credential that matters.
It must be stored in a secret manager, not in a repository, not in a configuration file, not in a variable somebody can print.
It should be rotatable. Apps support multiple keys, which allows rotation without downtime — generate the new key, deploy it, remove the old one.
Rotate on a schedule, and immediately when anybody who had access to it leaves.
Restrict who can generate keys. App management is a privileged capability, and an app installed broadly with an unrestricted key is equivalent to a very powerful token.
Scope installations narrowly. An app installed on all repositories in an organisation has, in effect, organisation-wide access at its permission level. Install on the repositories it needs.
Choosing between the mechanisms
Section titled “Choosing between the mechanisms”A decision table, because the choice is usually made by habit rather than by fit.
| Need | Mechanism |
|---|---|
| Anything inside a workflow, same repository | Workflow token |
| Cloud credentials from a workflow | OIDC, not a stored secret |
| Cross-repository automation | GitHub App installation token |
| A bot that comments, labels, opens pull requests | GitHub App |
| A deployment system cloning one repository | Deploy key, read-only |
| An integration that only supports user auth | Fine-grained token on a team-owned account |
| A developer’s local script | Fine-grained token, their own, with expiry |
| Anything else | GitHub App |
The pattern is visible: almost everything is an app, and the exceptions are narrow.
The two questions that resolve most cases: does this run inside a workflow, and does it need to touch more than one repository? Inside a workflow, one repository: workflow token. Inside a workflow, several repositories: app. Outside a workflow: app, unless something specifically prevents it.
Classic personal access tokens do not appear in the table, which is deliberate. There is essentially no case where a classic token is the best available answer today, and every one in your estate is a migration candidate.
Deploy keys
Section titled “Deploy keys”A deploy key is an SSH key granting access to one repository.
Narrower than a token in scope, which is genuinely useful — a deploy key cannot reach anything else.
And broader in a different way: it is long-lived, it does not act on behalf of a user, and it is exempt from controls that apply to user authentication. Under conditional access, GitHub documents that CAP IP conditions do not apply to deploy keys.
Prefer read-only wherever the use case allows. A read-only deploy key is a substantially smaller risk than a write one.
Inventory them. They are per repository and easy to forget, and an estate of a thousand repositories can have several hundred deploy keys nobody can account for.
The common legitimate use is a deployment system cloning a repository. Even there, a GitHub App is frequently better — but deploy keys remain the simplest thing that works, and they are not wrong when inventoried and rotated.
The “service account” question
Section titled “The “service account” question”A request that arrives in every enterprise: “can we have a GitHub service account?”
GitHub does not define a service account as a distinct product concept. There is no account type that is a service account. What exists is: user accounts, GitHub Apps, and repository-scoped keys.
Which means a “service account” in practice is a user account created for a machine, with an email address that belongs to a team rather than a person.
And that pattern has real problems. It is a user account, so it counts as a member, needs a licence, needs MFA configured somewhere, and is subject to SAML and SCIM. Under SCIM it may be deprovisioned when the directory does not know about it, or may never be provisioned at all. Its credentials are shared, which means no attribution and no clean rotation trigger.
It is sometimes still the answer. Some integrations only support user authentication. Some workflows genuinely need an identity that can be a member of a team. In those cases the pattern is legitimate and needs handling deliberately.
If you must have one:
- Own it as a team, with the email going to a distribution list that more than one person reads.
- Register it in the directory if SCIM is in use, so it is provisioned deliberately rather than accidentally.
- Name it obviously. A convention that makes it clear at a glance that this is not a person.
- Give it minimal permissions, through team membership rather than direct grants.
- Inventory it alongside every other machine identity.
- Enforce MFA, with the second factor stored in the secret manager and accessible to the owning team.
But ask first whether a GitHub App does the job. In the large majority of cases it does, and it avoids every one of the problems above.
Inventory and ownership
Section titled “Inventory and ownership”The governance problem underneath all of this: knowing what exists.
Every machine identity needs a named owner — a team, not a person. Without one, nobody rotates it, nobody removes it, and nobody can say what breaks if it stops working.
Build the inventory from the API. App installations per organisation, deploy keys per repository, organisation-owned tokens where enumerable. Personal access tokens are the hard case, because they belong to individuals.
Record, for each: what it is, what it can do, where it is used, who owns it, when it was created, and when it was last rotated.
Find the unused ones. A credential with no recent activity is either a redundancy nobody documented or residue. Both are worth resolving.
Find the over-permissioned ones. An app with write on all repositories, doing something that needs read on three.
Publish it. An inventory nobody sees is an inventory nobody maintains.
Governing app installation
Section titled “Governing app installation”Apps are the recommended answer, and an ungoverned app estate has its own problems.
Anybody with the right permission can install an app, including a third-party app from the marketplace, granting it whatever permissions it requests.
Which is a supply-chain decision. An installed app can read your code, and depending on permissions, write to it. The decision to trust a third party with that is not one an individual repository administrator should make unilaterally.
Restrict installation. GitHub allows organisations to require approval for app installation, and turning that on is a small change with a large effect.
Review requested permissions. An app requesting write on all repositories to do something that needs read on one is a signal, and it is visible at install time.
Maintain an approved list. The apps your organisation has evaluated, with a note on what each is for. New requests go through review; approved ones are self-service.
Review the installed list periodically. Apps accumulate, vendors change, and an app installed for a trial three years ago still has its permissions.
Distinguish internal from third-party. An app your platform team wrote is a different risk from one built by an external vendor, and the review depth should differ accordingly.
Watch permission changes. An app requesting additional permissions after installation requires approval, and that approval is a decision point worth routing to somebody who will actually evaluate it rather than clicking through.
Rotation
Section titled “Rotation”Every long-lived credential needs a rotation schedule, and the schedule is only real if rotation has been practised.
Rotate on a calendar — annually at minimum for anything long-lived.
Rotate on departure. Anybody who had access to a credential leaving is a rotation trigger.
Rotate on suspicion, without hesitation. The cost of an unnecessary rotation is small.
Practise it. A rotation procedure that has never been executed is a document, not a capability. The first attempt during an incident will find the step nobody wrote down.
Prefer credentials that expire. Fine-grained tokens have expiry dates; installation tokens are short-lived by design. A credential that expires forces the rotation process to exist.
Rolling out better machine identity
Section titled “Rolling out better machine identity”-
Inventory what exists. Apps, deploy keys, tokens with recent activity. This is the largest step and it is mostly discovery.
-
Assign an owner to everything. Anything unowned after a reasonable effort is a candidate for revocation — with notice.
-
Find the user tokens doing production work. These are the highest-risk items and the clearest wins.
-
Replace them with apps, highest-impact first. Each one removes a dependency on somebody’s employment.
-
Narrow app installations. An app on all repositories that uses four is over-installed.
-
Set expiry on everything that supports it.
-
Establish the rotation schedule, and execute one rotation to prove the procedure.
-
Add creation controls. Enterprise policy can restrict deploy key creation; app installation can require approval.
-
Re-inventory quarterly. New credentials appear continuously.
Credentials inside GitHub Actions
Section titled “Credentials inside GitHub Actions”The largest single population of machine credentials in most enterprises, and the one with the best available answers.
The workflow token is the default. Every workflow run gets a token scoped to its repository, valid for the run, requiring no storage and no rotation. Where it suffices, nothing else should be used.
Restrict its default permissions. The token’s default permission set is configurable at organisation and repository level, and setting it to read-only by default — with workflows requesting more explicitly — is a substantial reduction in blast radius for a one-time setting change.
OIDC for cloud access. A workflow can exchange its identity for short-lived cloud credentials rather than holding a long-lived cloud key as a secret. This eliminates an entire category of stored credential, and it is one of the highest-value changes available in a CI estate. See Terraform with GitHub Actions for the pattern in context.
GitHub Apps for cross-repository work. The workflow token cannot reach other repositories. The common workaround is a personal access token stored as a secret; the correct answer is an app installation token generated within the workflow.
Environment secrets with required reviewers for anything sensitive, so a deployment credential is gated by an approval rather than available to any workflow run.
Audit stored secrets periodically. Organisation and repository secrets accumulate, they are not visible once set, and nobody knows which are still used. A list of secret names, with their last-updated dates, is enough to start the conversation.
The pattern to eliminate: a long-lived credential for an external system, stored as a repository secret, created by somebody who has left, used by a workflow nobody owns. Every estate has several.
Detecting what is actually in use
Section titled “Detecting what is actually in use”Inventory tells you what exists; activity tells you what matters.
The audit log attributes actions to apps and to users. Filtering by actor over a period shows which machine identities are active and which are dormant.
Token last-used information, where available, is the cheapest signal for a personal access token.
Deploy key activity is harder to see directly, and the repository’s traffic and Git event data are the available proxies.
Git events have short retention. GitHub documents that the audit log retains Git events for seven days, which means clone and push activity by a credential is a short window rather than a historical record. If you want to answer “has this deploy key been used in the last six months”, you need to have been streaming the log. See audit log streaming.
Which is an argument for streaming before you need it. Retention is a rolling window, and the question “what has this credential been doing” is asked retrospectively.
Dormant credentials are the easiest wins. A credential with no activity in six months can usually be revoked with a notice period and no consequence, and each one removed is one fewer thing to inventory, rotate and explain.
Common mistakes
Section titled “Common mistakes”Personal access tokens running production automation. They break when the person leaves.
Apps installed on all repositories by default. Convenience becomes a very large grant.
No inventory. You cannot govern what you cannot enumerate.
No owner per credential. Nothing is ever rotated or removed.
Private keys outside a secret manager. The credential that matters, least protected.
Never practising rotation. The procedure fails when it is needed.
Long-lived credentials where expiring ones would work. Expiry forces the process to exist.
Ignoring deploy keys. Per repository, long-lived, and exempt from user-level controls.
Treating machine identity as separate from identity governance. It is where the standing access is.
When a machine credential is compromised
Section titled “When a machine credential is compromised”Worth rehearsing, because the response differs from a human account compromise.
Revoke first, investigate second. A machine credential has no productivity cost to revoking — something breaks, and you fix it. A person locked out is a different calculation. Here, revoke immediately.
Know what it could reach. This is where the inventory earns its keep. An app’s permissions and installation scope, a token’s repository scope, a deploy key’s single repository. Without the inventory, the blast radius is a guess.
Check what it did. The audit log attributes actions to the identity. Filter by actor across the exposure window and read everything.
Look for what it created. A compromised credential with write access may have added a workflow, a deploy key, a webhook, or a collaborator. Revoking the original credential does not remove what it established, and this is the step most often missed.
Check for pushed code. A credential with write access can commit. Review the commits made by that identity during the window, particularly changes to workflow files, which are the highest-value target.
Then replace it. New credential, deployed, old one confirmed dead.
And ask why it was long-lived. A compromised short-lived installation token has an exposure window of minutes. A compromised year-old personal access token has an exposure window of a year, and the difference is the credential design rather than the incident.
Practise this once, on a credential you deliberately revoke. The rehearsal finds the gaps in the inventory faster than any audit.
Mental model
Section titled “Mental model”Human identity is governed because people join and leave and somebody notices. Machine identity is not governed because nothing happens when it is wrong — until it does. The goal is to make every automation an identity in its own right, owned by a team, scoped to what it needs, with a credential that expires.
What you learned
Section titled “What you learned”- GitHub Apps are the right answer for almost all automation: own identity, explicit permissions, per-repository installation, short-lived tokens
- The workflow token requires no storage and should be used wherever it suffices
- A personal access token used by automation breaks when its owner leaves, carries their access, and misattributes actions
- An app’s private key is the credential that matters and belongs in a secret manager, rotatable without downtime
- Installing an app on all repositories is one of the largest standing grants available
- Deploy keys are repository-scoped, long-lived, and exempt from user-level controls including CAP IP conditions
- Every machine identity needs a team owner, an inventory entry and a rotation schedule
- A rotation procedure that has never been executed is a document, not a capability
- Prefer credentials that expire, because expiry forces the process to exist
Exercise
Section titled “Exercise”Use an organisation you administer.
-
List every GitHub App installed. For each: permissions, repository scope, owner. Predict: how many are installed on all repositories?
-
Count deploy keys across the estate. Find the oldest. Is it read-only?
-
Find every automation authenticating with a personal access token. Predict: what happens to each when its owner leaves?
-
For the highest-impact one, design the GitHub App that would replace it: which permissions, which repositories.
-
For one app, locate its private key. Predict: is it in a secret manager, and who can read it?
-
Find a credential with no activity in six months. Determine whether anything still needs it.
-
Pick one credential and rotate it, end to end. Record what the documented procedure got wrong.
-
Write the inventory schema you would maintain, and name its owner.