Git, CI/CD & GitOpsLXXI · CI/CD Threat ModellingAttack
The account-compromise attack — the stolen-credentials scenario
What you'll learn
- Describe the account-compromise attack: a stolen PAT, SSH key, or OIDC session used to act as a developer or runner
- Distinguish a long-lived credential (PAT, SSH key) from a short-lived credential (OIDC token) by the cost of compromise
- Identify the detection signals: impossible-travel logins, credentials used outside expected workflows, tokens reused across services
- Apply controls — short-lived credentials, OIDC federation, hardware-key-protected accounts, mandatory rotation — that reduce the half-life of a stolen credential
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 account-compromise attack is the scenario where the attacker holds a legitimate identity. The identity is a developer’s personal access token, a runner’s deploy credential, an SSH key, a session cookie, or an OIDC token. The attacker is, from the platform’s perspective, the developer. The platform cannot distinguish the attacker from the developer; the audit log cannot either. The attack succeeds because the team has chosen to delegate trust to an identity the attacker can steal.
What gets stolen
The credentials a CI/CD pipeline uses are not one thing. They are four distinct kinds of credential, each with a different half-life and a different revocation cost.
- Personal access tokens (PATs). Long-lived tokens a developer generates for use against the GitHub API or the container registry. The token has the developer’s scopes. The token does not expire until the developer revokes it. A leaked PAT is a credential that is valid until manually revoked; the team may not know it has been leaked.
- SSH keys. Long-lived keys a developer or runner uses to push to the repository. The key has the developer’s push rights. The key does not expire until manually removed from the account. A leaked SSH key is a credential that is valid until manually rotated.
- OIDC tokens. Short-lived tokens the CI platform issues to a workflow run. The token is bound to a specific workflow, a specific ref, and a specific job. The token expires when the job ends. A leaked OIDC token is a credential that is valid for minutes.
- Deploy credentials. Long-lived secrets stored in the CI platform’s secret store, exposed to the runner as environment variables. The secret has the scope the runner needs — registry push, cloud role assumption, signing key access. The secret does not expire until manually rotated. A leaked deploy credential is a credential that is valid until manually rotated.
The four kinds differ in half-life. The PAT, the SSH key, and the deploy credential are long-lived; the OIDC token is short-lived. The team’s exposure to a credential-theft attack is the half-life of the credential. The control that closes the credential-theft attack is the control that makes every credential short-lived.
flowchart LR
A["Credential types"] --> B["PAT (long-lived)"]
A --> C["SSH key (long-lived)"]
A --> D["OIDC token (short-lived)"]
A --> E["Deploy secret (long-lived)"]
B -. "valid until manually revoked" .-> X["Steal window"]
C -. "valid until manually rotated" .-> X
D -. "valid for minutes" .-> X
E -. "valid until manually rotated" .-> X
The diagram is the threat model. The OIDC token’s steal window is the job duration; the PAT’s steal window is forever (or until the developer notices and revokes).
The attack in three steps
The account-compromise attack has three steps. Each step leaves a different trail; each step produces a different detection signal.
- Steal the credential. The attacker phishes the developer (a fake CI failure email, a fake “rotate your PAT” notification), scrapes a leaked PAT from a public Git commit, or compromises the developer’s workstation through a vulnerable extension. The credential leaves the developer’s control.
- Use the credential as the developer. The attacker pushes commits, opens PRs, triggers workflows, or pushes images using the developer’s identity. From the platform’s perspective, the activity is the developer’s. The audit log records the developer’s name; the IP address may be unfamiliar.
- Persist through the credential. The attacker configures a webhook, registers a deploy key, or schedules a workflow that will run again later. The persistence is the difference between a one-time compromise and a long-term foothold. The attacker returns through the same credential, or through the persistence the credential granted them.
flowchart LR
A["Phish / scrape / compromise"] --> B["Credential in attacker control"]
B --> C["Act as developer"]
C --> D["Push commits / open PRs"]
C --> E["Push images / run workflows"]
C --> F["Register webhook / deploy key"]
D --> G["Audit log shows developer"]
E --> G
F --> H["Persistence"]
The audit log shows the developer’s name because the attacker used the developer’s credential. The signal that distinguishes the attacker from the developer is in the metadata: the IP address, the user-agent, the timing, the action sequence.
Detection signals
The signals that distinguish an account-compromise attack from legitimate developer activity are not in the repository state; they are in the session metadata.
- Impossible travel. The developer’s session logs in from a city they were not in an hour ago. The CI platform records the IP and the geolocation; an alert fires when the geolocation does not match the developer’s recent history.
- Credentials used outside expected workflows. A developer’s PAT is used to push an image at 03:00 UTC when the developer is in a time zone where 03:00 is sleep time. The signal is in the timestamp.
- Tokens reused across services. The same PAT is used against the GitHub API and against the team’s container registry; the cross-service correlation is a signal that the token has been exfiltrated.
- New deploy key or webhook. The attacker registers a deploy key on a private repository; the team receives a notification, or does not, depending on the audit configuration.
The four signals require the platform to record session metadata and the team to alert on it. Most teams do not configure the alerts; most platforms expose the metadata but do not raise it.
Controls that make a stolen credential short-lived
The controls that close the account-compromise attack fall into three groups.
- Short-lived credentials. Replace every long-lived PAT, SSH key, and deploy secret with an OIDC token, issued per-job, scoped to the workflow, expiring when the job ends. The credential has no value after the job.
- Hardware-key-protected accounts. Require every developer with production access to use a WebAuthn hardware key (YubiKey, Titan, etc.) as a second factor. The credential cannot be phished because the key requires a physical interaction; the credential cannot be used from a session the developer did not start because the key is bound to the developer’s presence.
- Mandatory rotation. For credentials that must remain long-lived (machine accounts, third-party integrations), enforce a rotation interval. The credential’s half-life is bounded by the rotation interval; a leaked credential expires on schedule even if the team does not notice the leak.
The three groups together reduce the half-life of every credential the team issues. The shorter the half-life, the smaller the window during which a stolen credential is useful.
Production discipline
- Replace long-lived credentials with OIDC. Every workflow that needs a cloud role uses an OIDC token, issued per-job, scoped to the workflow and ref.
- Require hardware-key MFA on production accounts. A WebAuthn key on every developer with repository write or workflow edit rights; the key requires physical presence.
- Rotate machine credentials on a schedule. PATs, SSH keys, and deploy secrets that must remain long-lived are rotated on a fixed interval; the interval is the half-life.
- Alert on impossible travel and unexpected IP ranges. The audit log records session metadata; the team alerts on the metadata the developer cannot produce.
Cross-course references
- Git, CI/CD & GitOps — Part LXXI-02 (Malicious Commit) is the upstream attack this lesson assumes.
- Git, CI/CD & GitOps — Part LXVIII (Identity and Token Hygiene) covers the credential-lifecycle model in detail.
- Git, CI/CD & GitOps — Part LXX (Provenance) covers the attestation model that binds a workflow run to an identity.
- Linux for Production Sysadmins — Part XXXII (SSH Key Hygiene) covers the key-rotation pattern.
Quiz
Knowledge check · 4 questions
Q1. Why is an OIDC token a structurally different credential from a personal access token?
Q2. A team that uses long-lived PATs for production deploys has structurally chosen to make the credential-theft attack cheap.
Q3. Name the four kinds of credential a CI/CD pipeline uses, and identify which has the shortest half-life by design.
Q4. Diagnose the credential-theft attack, identify the half-life gap, and recommend the controls that close it.
Team T uses a self-hosted runner with a long-lived personal access token (scopes: repo, write:packages) to push images to ghcr.io and a long-lived AWS access key (scopes: sts:AssumeRole on production) to deploy to production. A developer's laptop is compromised through a malicious VS Code extension; the extension exfiltrates the developer's ~/.aws/credentials file and the developer's GitHub PAT from the OS keychain. Six days later, an unfamiliar IP address pushes an image to ghcr.io using the team's PAT; four hours later, the production AWS role is assumed from a hosting provider in a country the team does not operate in.
Passing score: 75%. Answers are checked in this browser.