A personal access token is easy to create and surprisingly hard to reason about after six months. “automation-token-final” appears in a secret store, nobody remembers which script owns it, and revoking it feels like pulling an unlabeled cable in a server room. We can avoid that anxiety at creation time.
The goal is not merely to produce a string that works. It is to choose the narrowest credential type, give it only the capabilities the consumer needs, store it without copying it into logs or URLs, prove the intended request succeeds, and know exactly how to replace it.
Before creating a PAT, ask who is doing the work
A person using Git over HTTPS or a local tool: a personal access token may fit because actions should carry that user’s permissions and identity.
A GitLab CI/CD job: prefer the short-lived
CI_JOB_TOKENwhen its supported endpoints and allowlist meet the need.One project’s automation: consider a project access token so access does not follow a human across every project they can reach.
A group-wide integration: consider a group access token with deliberate group scope.
Deployment pulling code or registry artifacts: a deploy token or deploy key may express the job more narrowly.
A third-party application serving many users: OAuth is often a better lifecycle and consent model than collecting personal tokens.
Write the credential contract first
Consumer: exact script, CLI, integration, or developer device.
Resources: project, group, repository, registry, and API endpoints it must reach.
Actions: read, write, push, package pull, or self-rotation—not “everything we might need.”
Owner: person or team accountable for use, renewal, and incident response.
Lifetime: the shortest practical expiry plus a reminder before midnight UTC on that date.
Storage: named password manager, operating-system credential helper, or managed secret store.
Replacement test: how to update the consumer, validate the new value, and revoke the old one without guessing.
Scope is capability; token type is reach
Repository work over HTTPS
read_repositoryallows pull access to repositories available to the PAT owner.write_repositoryallows pull and push over Git-over-HTTP. It does not authenticate general API requests.Do not add
apimerely because a Git client asks for a password; repository scopes are designed for this job.
API clients
read_apipermits read access through the API within the token’s reach.apigrants complete read and write API access and, for a personal token, also encompasses repository and registry access documented by GitLab.read_useris narrower for reading the authenticated user profile and supported user endpoints.self_rotatepermits a token to rotate itself without granting the full API scope.
Registries
read_registrysupports pulling authorized container images.write_registrysupports pushing container images; GitLab’s scope rules may also require read access.Package, virtual-registry, and instance-specific capabilities have their own availability. Choose from the live form and current scope reference, not from an old screenshot.
Create the token in GitLab’s current account settings
Open your avatar → Edit profile → Access → Personal access tokens.
Choose the token style offered by your GitLab version. Current documentation describes selecting Legacy token from the Generate token menu for the traditional scopes discussed here.
Enter the workload-focused name and a description identifying owner and consumer.
Choose an explicit expiration date. GitLab expires access tokens at midnight UTC on that date.
Select only the scopes justified by the credential contract.
Generate the token, copy it once into the approved secret store, and label the secret with its non-secret metadata.
Before leaving the page, confirm the name, expiry, and scopes against the request you approved.
GitLab displays the new token value once. After you navigate away or refresh, it cannot be viewed again. If it was not stored safely, revoke it and create a replacement rather than scattering hurried copies across notes and terminals.
Expiry is a timestamp, not a vague day
New personal, project, and group access tokens require an expiry. If you omit one, GitLab normally applies 365 days; administrators can impose a different maximum, and some configurations can extend the maximum to 400 days. The token expires at 00:00:00 UTC on its expiry date, so teams west of UTC may experience the cutoff on the previous local evening.
Use hours or days for one-time migrations when the interface permits the necessary date boundary.
Schedule renewal before the expiry notification window and before weekends or release freezes.
Never solve rotation ownership by choosing the longest allowed lifetime.
Treat an unexpected expiry change after an instance upgrade as a migration event worth auditing.
Test the API without putting the secret in the URL
read -rsp 'GitLab token: ' GITLAB_TOKEN
printf '\n'
curl --fail-with-body --silent --show-error \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
https://gitlab.example.com/api/v4/user
unset GITLAB_TOKEN{
"id": 42,
"username": "your-username",
"name": "Your Name"
}Risk level: caution. Review the command before running it.
The test minimizes exposure; it does not erase history everywhere
read -ssuppresses terminal echo, while-pshows a prompt. The token is stored temporarily in the process environment.PRIVATE-TOKENis GitLab’s documented header for personal, project, and group access tokens.--fail-with-bodyreturns a failure status for HTTP errors while retaining the response body for diagnosis;--silent --show-errorremoves progress noise but preserves errors.unsetremoves the shell variable after the request. Child processes, tracing, crash capture, terminal tooling, and privileged inspection still require a trusted machine.The hostname and response are placeholders. This request was documentation-reviewed, not executed with a real credential.
Git over HTTPS should prompt or use a credential helper
git clone https://gitlab.example.com/YOUR_NAMESPACE/project.gitCloning into 'project'...
Username for 'https://gitlab.example.com': your-username
Password for 'https://your-username@gitlab.example.com':Why the missing token is the important part
For Git over HTTPS, GitLab accepts any non-blank username in most cases and the PAT as the password; some integrations still require the actual GitLab username.
The prompt does not echo the token, and the remote URL remains free of credential text.
Use an approved credential helper or secret manager if the tool must remember it. Understand whether that helper encrypts storage or only caches plaintext.
A token embedded in a URL can land in
.git/config, proxy logs, command history, process listings, and error messages. GitLab explicitly advises against URL storage.For routine developer Git access, SSH keys avoid using a PAT as the repository password.
A 401 and a 403 tell different stories
401 Unauthorized: the token may be mistyped, expired, revoked, disabled, sent incorrectly, or issued by another GitLab instance.
403 Forbidden: authentication may have succeeded while the user, token type, scope, role, policy, or endpoint denies the requested action.
404 Not Found: GitLab may conceal a private resource from an unauthorized caller; verify host, project path, and membership before assuming deletion.
Git push works but API fails:
write_repositorydoes not provide general API authentication; choose the narrow API scope the actual request needs.API reads work but writes fail: a read scope or underlying project role may be insufficient.
It worked yesterday: inspect expiry at midnight UTC, token status, owner access, last-use information, recent IPs, group policy, and audit events.
Rotate as a planned handoff
GitLab rotation creates a new token with the same permissions and scope, immediately makes the old token inactive, and retains both records for audit. Every consumer using the old value stops working until updated. That is why “click Rotate and then find the jobs” is the wrong order.
Inventory every consumer and pause risky schedules.
Confirm the existing scopes are still justified; create a narrower replacement instead of blindly preserving excess privilege when necessary.
Open the active token’s menu and choose Rotate.
Store the newly displayed value immediately.
Update consumers through their secret-management path and run a bounded health check.
Confirm usage moved to the new token and no old-value failures remain.
Record the change, owner, new expiry, verification result, and rollback decision.
Revoke first when exposure is possible
Revoke the token in Access → Personal access tokens; do not wait to prove exploitation.
Remove it from secret stores, CI variables, credential helpers, local configuration, logs, tickets, and integrations.
Search appropriate Git history and artifacts without copying the token into more tools.
Review the token’s scopes, last-used time, recent connection IP addresses, account activity, project events, registry actions, and audit records available to your tier.
Rotate downstream credentials or artifacts the token could modify, not merely the token itself.
Create a least-privilege replacement only after understanding the original exposure path.
The token dashboard should become boring
Every active row should make sense without archaeology: recognizable workload name, accountable owner, necessary scopes, expected IP or usage pattern, recent use, and an expiry with a renewal plan. Revoke forgotten experiments and tokens whose consumer no longer exists. GitLab retains revoked records for audit even though the value cannot be used.
A well-designed token is almost forgettable in daily work—but never mysterious. It opens one needed door, lives in one protected place, leaves a useful audit trail, and expires on a date somebody already owns.
Comments and corrections