Git, CI/CD & GitOpsXXXIV · Git SecurityLeastPrivilege
The least-privilege credential — scoping tokens to minimum scope, ephemeral credentials, and the principle
What you'll learn
- Apply the principle of least privilege to Git credentials at three levels (repository, permission, lifetime)
- Issue ephemeral credentials (per-job tokens, short-lived OAuth tokens) and explain why they are the strongest form of the principle
- Distinguish between credentials that are scoped at issuance and credentials whose scope drifts upward over time
- Recognise the operational trade-offs of strict scoping and the cases where broader scope is justified
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The principle of least privilege, applied to Git credentials, means: the narrowest scope (one repository), the narrowest permission (one action), the shortest lifetime (one job). The credential’s blast radius is the intersection of those three axes; the goal is to make the intersection as small as the use case allows. This lesson walks the three axes, the trade-offs of strict scoping, and the operational discipline of issuing narrow credentials on demand rather than broad credentials in advance.
The three axes of credential scope
A credential’s scope is the product of three independent properties:
flowchart LR
A["credential scope"] --> B["repository axis"]
A --> C["permission axis"]
A --> D["lifetime axis"]
B -->|one repo| E["smallest blast radius"]
B -->|all repos| F["largest blast radius"]
C -->|read contents| E
C -->|admin| F
D -->|one job| E
D -->|forever| F
- Repository axis. One repository, a few repositories, or every repository the issuing account can reach. The smallest is one; the largest is account-wide.
- Permission axis. Read contents, write contents, manage pull requests, manage settings, or admin. The smallest is the single permission the use case requires; the largest is the account’s full rights.
- Lifetime axis. One job, one hour, one day, one quarter, or no expiry. The smallest is the duration of the work; the largest is forever.
The principle is to minimise the product: a credential that is scoped to one repository, with one permission, for one job has the smallest possible blast radius. A credential that is scoped to every repository, with admin rights, forever has the largest.
# The narrowest possible HTTPS credential: per-job, scoped, ephemeral
git clone https://x-access-token:${GITHUB_TOKEN}@github.com/acme/repo.git
The GITHUB_TOKEN is injected by the runner at job
start, scoped to the repository the job is running against,
valid only for the duration of the job, and discarded when
the job ends. The credential’s lifetime is the work’s
lifetime; the credential’s scope is the work’s scope; the
credential’s permission is the work’s permission.
Scoping at issuance versus scope drift
The narrowest scope is the scope set at issuance. The broadest scope is the scope the credential accumulates over time:
flowchart LR
A["credential issued\nscoped to repo A"] --> B["engineer added to repo B"]
B --> C["account-wide PAT inherits repo B"]
C --> D["engineer added to repo C"]
D --> E["account-wide PAT inherits repo C"]
The drift is invisible to the credential: a classic PAT issued for one repository last year has the rights of the issuing account today, which includes every repository the account has been added to since. The credential’s scope has grown with the account’s access; the issuing team may not have noticed.
Fine-grained PATs do not drift the same way: the scope is pinned at issuance to specific repositories, and the credential’s rights do not expand when the account is added to a new repository. The pinning is the structural property that makes fine-grained PATs the right default (XXXIV-03); the drift is the structural property that makes classic PATs the wrong default.
The discipline is to issue credentials with the narrowest scope the use case allows, to use the credential model that pins the scope at issuance, and to audit the credential inventory quarterly to catch the cases where scope has drifted (because the account’s access has changed, because the use case has changed, or because the team has added repositories since issuance).
Ephemeral credentials
The strongest form of the principle is the ephemeral credential: a credential whose lifetime is the work’s lifetime. The credential is issued for one job, used for the duration of the job, and discarded when the job ends. There is no credential to leak because there is no credential to persist.
flowchart LR
A["job starts"] --> B["runner issues token"]
B --> C["token scoped to job"]
C --> D["job runs"]
D --> E["job ends"]
E --> F["token discarded"]
Three forms of ephemeral credential, by forge:
GITHUB_TOKENon GitHub Actions. Injected by the runner at job start, scoped to the repository the workflow runs against, valid only for the duration of the job, and discarded when the job ends. The token appears in the environment as$GITHUB_TOKEN; the workflow uses it forgit clone,git push, and API calls without ever persisting it.CI_JOB_TOKENon GitLab CI. Injected by the runner, scoped to the project the pipeline runs against, valid only for the duration of the job. The token appears in the environment as$CI_JOB_TOKEN; the pipeline uses it forgit cloneandgit pushwithout persisting it.- Short-lived OAuth access tokens. Issued by the forge in exchange for a refresh token, valid for one hour (typical), and refreshed by the integration only while the integration is active. The access token’s lifetime is the work’s lifetime; the refresh token’s lifetime is the integration’s lifetime.
The right time to use an ephemeral credential is every time the work has a defined end. A CI job has a defined end; a deploy step has a defined end; a one-off API call has a defined end. The right time to use a long-lived credential is when the work does not have a defined end — an engineer’s workstation, a long-running service that must authenticate on demand. Even then, the credential should be scoped as narrowly as the use case allows and rotated on a schedule.
The trade-offs of strict scoping
Strict scoping is not free. The trade-offs are operational, not technical, and they are real:
- More credentials to manage. A team with fifty repositories and one credential per repository has fifty credentials to issue, rotate, and revoke. The discipline is to issue them on demand (per-job tokens) rather than in advance.
- Credentials that expire during long-running work. A fine-grained PAT with a one-hour expiry is invalid by the time a four-hour build finishes. The discipline is to set the expiry to the expected duration of the work, with a margin, and to refresh or reissue if the work runs long.
- Credentials that cannot be shared across users. A fine-grained PAT scoped to one engineer’s account cannot be used by a different engineer. The discipline is to use OAuth apps or machine users for shared credentials, with the scoping applied at the authorisation level rather than at the user level.
- Loss of convenience in low-risk environments. A throwaway test environment does not need fine-grained PATs; a classic PAT with the narrowest scope the test allows is fine. The discipline is to match the credential model to the environment, not to apply the strictest model everywhere by reflex.
The right framing is that strict scoping is the default for every environment where the credential matters, and the trade-offs are accepted because the alternative — an over-scoped credential in a breach dump — is worse.
Production discipline
- Default to ephemeral credentials on every CI job. The runner injects the token; the workflow uses it for the duration of the job; the runner discards it when the job ends.
- Pin scope at issuance. Use fine-grained PATs scoped to specific repositories; do not let the scope drift with the account’s access.
- Set the shortest useful lifetime. Per-job for CI; quarterly for engineer personal access tokens; per- deploy for production deploy hosts.
- Audit the credential inventory quarterly. Every credential issued by the team should appear in the inventory with its scope, permission, and lifetime; unused credentials are revoked; over-scoped credentials are narrowed.
- Match the credential model to the work’s lifetime. Ephemeral for work with a defined end; scoped long- lived for work without a defined end.
Cross-course references
- Git, CI/CD & GitOps — Part XXXIV-03 (HTTPS tokens) — the fine-grained PAT model that this lesson applies the principle to.
- Git, CI/CD & GitOps — Part XXXIV-02 (SSH keys and deploy keys) — the deploy-key scoping that is the SSH analogue of the principle.
- Git, CI/CD & GitOps — Part XXXIV-05 (The compromised account) — the blast-radius argument that motivates the principle.
- Terraform for Production Sysadmins — Part XXXIV (StateSecurity) — the analogous principle for Terraform Cloud tokens and workspace access.
Quiz
Knowledge check · 4 questions
Q1. A CI job needs to clone a private repository, run tests, and report results back to the forge. The job runs on a shared runner. Which credential model best satisfies the principle of least privilege?
Q2. A classic personal access token issued for one repository last year has the same rights today as it did at issuance, because the forge pins the token's scope at the moment it is issued.
Q3. Name the three axes of credential scope and the property that minimises the credential's blast radius on each axis.
Q4. Diagnose why a team's credentials are over-scoped, and recommend the principle-of-least-privilege changes that minimise future blast radius.
A team operates fifty repositories across two forge organisations. Every CI job uses a long-lived PAT issued two years ago against the engineer's account who set up CI; the PAT is account-wide and has no expiry. The PAT has been copied into `~/.git-credentials` on each runner via the `store` helper. The engineer who set up CI has since left the team; the PAT remains valid because no one revoked it. Last week a runner was compromised via a vulnerable build step, and the PAT — along with every other credential in `~/.git-credentials` — was exfiltrated. The attacker now has the rights of a senior engineer against every repository in both organisations.
Passing score: 75%. Answers are checked in this browser.