Patch the GitLab Read, Then Revoke Before You Rotate
Both identity incidents end with an attacker holding a token that survives the remediation step most runbooks perform first, so response sequence decides the outcome.
The upgrade is the cheap part
A 10.0 on a file-read primitive means the read is unauthenticated, remote and unconstrained. On a GitLab host that is credential disclosure, because the platform stores its trust anchors at predictable paths. Omnibus installs keep encryption key material in gitlab-secrets.json. gitlab.rb holds database connection strings, LDAP bind passwords and SMTP credentials. Repository contents come along too, plus every secret a developer ever committed. Anything that decrypts stored CI/CD variables turns one HTTP request into cloud keys, registry credentials and runner authorization.
Rotation is where teams stall. It is genuinely expensive. Rotating db_key_base forces re-encryption of stored variables, and it will break pipelines that were quietly living off a stale token. Do it in priority order rather than doing none of it because the full rotation looks frightening.
| Exposed artifact | Blast radius | Rotation cost | Priority |
|---|---|---|---|
| Runner registration / auth tokens | Attacker-owned runner joins the fleet and receives real jobs | Low — re-register | P0 |
| Project and group tokens, PATs, deploy keys | Repo write, pipeline trigger, package publish | Low to medium | P0 |
| CI/CD variables (cloud keys, registry creds) | Lateral movement into cloud accounts and registries | Medium — per-project churn | P0 |
gitlab.rb secrets (DB, LDAP, SMTP) | Direct database read, directory enumeration, mail spoofing | Medium | P1 |
Encryption key material (db_key_base) | Offline decryption of everything above | High — re-encryption plus downtime | P1 |
| OIDC trust configuration | Over-broad sub/aud conditions let a forged pipeline assume cloud roles | Low — tighten claim conditions | P1 |
The same failure shape, a different vendor
Microsoft reports helpdesk-impersonation calls that walk employees through the OAuth 2.0 device authorization grant (RFC 8628, the flow built for TVs and CLIs) under a passkey-enrollment pretext. No cryptography broke. The flow's useful property is also its weakness: the browser that consents has no cryptographic binding to the device that receives the token. The attacker starts the flow and reads the user code aloud as an enrollment code. A fully MFA'd, fully compliant session then mints a token onto the attacker's machine.
What leaves is a refresh token. Its lifetime is decoupled from the credential that issued it. A password reset does not touch it. MFA re-enrollment does not touch it either. Only session and refresh-token revocation does. Blocking the grant in Conditional Access prevents reissue, at the cost of breaking az login --use-device-code, kubectl auth plugins and any build container without a browser. So enumerate usage from sign-in logs, exempt a scoped list, and migrate the survivors to workload identity federation.
If your account-takeover runbook opens with "reset the password", the attacker still holds a working token an hour later.
Hunt before you assume you were only probed
Probes arriving within hours of disclosure mean attacker automation sets the exposure window, not the change-approval cycle. Grep GitLab production and access logs for path-traversal encodings and anomalous 200s on non-asset paths across the disclosure window. Then diff outbound connections from the host against baseline. Save those queries as a permanent detection, not a one-off incident artifact. Then re-verify sub and aud conditions on every cloud IAM role that trusts GitLab OIDC. A wildcarded branch condition is how a stolen pipeline identity becomes production cloud access.
Both stories support one prioritization rule: rank by path to compromise, not by score. A 7.5 on an internet-facing box holding CI secrets outranks a 9.8 on an air-gapped internal service. This flaw ranks first on both axes. It goes first.
What to do
Inventory every self-managed GitLab instance, including forgotten staging nodes with public IPs, confirm exact minor versions against the vendor advisory, and upgrade outside the normal change window.
Rotate in documented order, treating it as a priority — runner registration tokens, project and group tokens and PATs, CI/CD variables, then gitlab.rb secrets and db_key_base with the re-encryption procedure.
Rewrite the account-takeover runbook this sprint so session and refresh-token revocation precedes credential rotation, and prove the sequence in a game day.