GitLab Security Specialist study materials
GitLab Security Specialist study resources

A Maintainer Token Leaked Fourteen Months Ago

GitLab Security Specialist · Put your knowledge to work in context.

All case studies

A Maintainer Token Leaked Fourteen Months Ago

Vertex Logistics had a GitLab personal access token with api scope committed to a private repository's deployment script 14 months ago. Secret detection flagged nothing because the feature was enabled after the commit. Last week the token appeared in a public fork created by a contractor. The token belonged to a service account with Maintainer on 22 projects, and it was rotated three days after discovery. The company has 180 group-level CI/CD variables; 40 are masked, 12 are protected, and several contain base64-encoded credentials that masking does not cover. Pipeline logs are retained for 30 days, and audit event streaming was enabled six months ago. The incident response lead needs to know how far back exposure goes, what else may have leaked, and what would prevent a repeat. The token had no expiration date.

1. How would you scope the incident? Describe what you can determine from GitLab data and what you cannot.

Read the worked answer

Start with the token identity and its capabilities. A personal access token with api scope and Maintainer on 22 projects can read and write repositories, change CI/CD variables, create tokens, register runners, and modify project settings. Treat every one of those as potentially exposed, not just source code.

Use audit events, filtered by the service account and the relevant projects, to reconstruct pushes, merges, protected branch changes, variable changes, member additions, runner registration, and project exports. Check the token's last-used timestamp and creation date, and enumerate tokens and deploy keys created by that account. Review webhooks and integrations, because an attacker with Maintainer can add an outbound webhook and exfiltrate data without pushing anything. Check package and container registry activity for unexpected publishes.

Know the limits. GitLab does not log every repository clone or file read by default. Audit event streaming covers the last six months, so the first eight months of the exposure window may have no centralized record. Pipeline logs are retained for 30 days, so any log-based evidence before that is gone. A fork is visible, but copies downloaded by the contractor are not. State these gaps in the incident report rather than implying complete reconstruction.

2. Why did rotation after three days, masking, and protected variables not contain the exposure?

Read the worked answer

Rotation stops future use but does not undo 14 months of access. The three-day gap is itself an exposure window, and any token-derived credentials created during that window remain valid unless they are found and revoked. Rotation also does not invalidate clones, caches, artifacts, or forks that already contain the token.

Masking only prevents an exact string from appearing in job logs. It does not protect base64-encoded values, values embedded in URLs, or values transformed by a script. Masking also does nothing for a token committed to a repository, because the repository is the leak channel, not the log. Protected variables only restrict which branches and tags can use a variable; they do not stop someone with repository read access from reading the committed file.

The deeper problem is the credential model. A service account with Maintainer on 22 projects and no expiration is a high-blast-radius credential used for deployment, where a project access token scoped to a single project and a single role would have been sufficient. Revoke the token, revoke any tokens it created, rotate every secret reachable from the 22 projects, and invalidate sessions. Then reduce the account to the minimum role and scope, and set an expiration.

3. Design the prevention program, including how you would handle the 180 group-level variables.

Read the worked answer

Enable secret push protection on all projects so a commit containing a recognized credential is blocked before it lands, and run secret detection in merge request pipelines and on a schedule to catch what already exists and what push protection misses. Enable the scanner on historic projects and triage the backlog rather than waiting for the next incident.

For the 180 variables, inventory them through the API and classify each as configuration or secret. Configuration values can stay as variables. Secrets should move to an external secret manager, with CI jobs retrieving them at runtime through OIDC federation rather than storing long-lived credentials in GitLab. Where variables must remain, scope them to protected branches and environments, mask them where the value permits it, and set expiration or rotation dates. Remove the base64-encoded credentials entirely; encoding is not protection, and it defeats masking.

Change the token model. Use project or group access tokens with the minimum role and scope, set expirations, and prefer short-lived ID tokens for cloud access. Restrict who can create tokens. Monitor token creation and use, and alert on tokens with api scope and Maintainer access that have no expiration. Finally, add a pre-commit hook and a CI check that rejects base64 blobs and known credential patterns in deployment scripts, since the original leak came from exactly that pattern.