You cannot extend a GitLab personal access token. Expiry is fixed at creation, a daily job at 01:00 UTC emails you when one is close, and the only path forward is to create a new token and replace the old one everywhere it is used.
The email is doing you a favour, and it usually arrives at the least convenient moment — which is still much better than the alternative, where a pipeline that has run for a year starts failing on a Tuesday morning with an authentication error and nobody remembers what credential it uses.
The fix is mechanical. The interesting part is that most of the tokens generating these emails should not have been personal access tokens in the first place.
Table of contents
- Rotating the token
- Where the token is hiding
- Most of these should not be personal tokens
- Living with mandatory expiry
- How this fits the rest of the stack
- FAQ
Rotating the token
Under User Settings, Access Tokens, create a replacement with the same scopes, then update every place the old one is stored before revoking it. Overlap is the point — revoke first and you have created an outage.
Scopes are where restraint pays off. The common ones:
read_repository— clone and fetch. Enough for most CI that only reads code.write_repository— push. Only if something genuinely commits.read_registry/write_registry— container registry access.read_api— read-only API access.api— full API access, including deleting projects. Grant this only when you know why you need it.
The instinct under time pressure is to tick api and move on. That instinct is how a token scraped from a log ends up able to delete a repository. Match the scope to the job, and where a token needs to do exactly one thing, give it exactly that.
Update git remotes without leaving the token in your shell history:
# Store the credential rather than embedding it in the remote URL
git config --global credential.helper store
git remote set-url origin https://gitlab.example.com/group/project.git
git fetch # prompts once, then remembers
# On macOS, use the keychain instead of a plaintext file
git config --global credential.helper osxkeychain
A token embedded in a remote URL ends up in .git/config, in shell history, and occasionally in a screenshot. A credential helper avoids all three.
Where the token is hiding
The rotation itself takes two minutes. Finding every place the old token lives is the part that takes an afternoon, and the list is longer than anyone expects:
- CI/CD variables in GitLab projects and groups.
- Git remotes with the token embedded in the URL.
~/.git-credentialson developer machines and build agents.- Docker registry logins in
~/.docker/config.json. - Kubernetes secrets pulling from the registry.
- Deployment scripts, Terraform variables, Ansible vaults.
- Third-party services with GitLab integrations.
- Anywhere someone pasted it because it was faster than doing it properly.
The practical mitigation is to write it down when you create the token, not when it expires. A short note recording what the token is for, where it is stored, and who owns it turns the next rotation from an investigation into a checklist.
Search before you rotate, and be prepared to be surprised:
# GitLab tokens have recognisable prefixes
grep -rn 'glpat-' --exclude-dir=.git . 2>/dev/null
# Check git history too -- deleting a file does not remove it from history
git log -p --all -S 'glpat-' | head -50
If that second command finds anything, the token is compromised regardless of expiry. Revoke it now and rotate properly.
Most of these should not be personal tokens
The recurring cause of token pain is using a personal credential for automation. A personal access token carries your identity and your permissions, which creates three separate problems:
- It has your access. Every project you can see, the pipeline can see.
- It dies with your account. When you change teams, the deploy breaks, and nobody knows why.
- It attributes machine actions to you. The audit log says you did it, at 3am, from a build agent.
GitLab offers narrower credentials, and picking the right one usually removes the expiry problem rather than solving it:
- CI job token. Automatically available inside every pipeline as a predefined variable, scoped to that job, and it expires when the job ends. For anything running in CI against your own instance, this is the correct answer and it needs no rotation at all.
- Project access token. Scoped to one project, owned by the project rather than a person. Survives staff changes.
- Group access token. The same idea across a group.
- Deploy token. Read-only repository or registry access, purpose-built for deployment and read-only by default.
- Deploy key. An SSH key with read or read-write access to one project — no expiry, no HTTP credential to leak into a log.
The rule of thumb: if a human is not typing it, it should not be a personal token. Applying that once removes most of the expiry emails permanently.
Living with mandatory expiry
Newer GitLab versions apply a default expiry — a year out — to tokens created without one, and administrators can enforce a shorter maximum. There is no supported way to make a token permanent, and that is intentional: a credential that never expires is a credential that outlives the person, the project and the reason it was created.
So build the rotation into your calendar rather than fighting it:
- Set expiry deliberately. Ninety days for anything sensitive, a year for low-risk read-only access. Short enough that rotation stays a familiar procedure.
- Stagger the dates. Tokens created on the same afternoon expire on the same afternoon. Spread them.
- Keep an inventory. Name, purpose, scope, storage location, owner, expiry. A spreadsheet is fine.
- Rotate at 75 percent of the lifetime, not on the warning email. Rotating under time pressure is how the wrong scope gets granted.
- Test the new token before revoking the old one. Always have an overlap window.
And treat the warning email as a prompt to ask a better question than how do I extend this. Nine times out of ten the right answer is that this token should be a CI job token, a project token, or a deploy key — and then it stops being a recurring task.
How this fits the rest of the stack
Credential expiry is a scheduling problem disguised as an authentication error. The version of it that hurts is a token buried in a deployment script, doing something important, owned by nobody in particular — and that shape is a symptom of a deploy pipeline assembled by hand.
On RunxBuild, deploys connect to your GitHub repository through the platform rather than through a credential you paste into a script, and environment variables live as service settings rather than in files that get copied between machines. Build logs and runtime logs sit in one place, so an authentication failure at deploy time is visible in the build output instead of appearing as a silent non-deploy. Web services in Node, Next.js, Python, Go, Ruby, Java, .NET or Docker, managed MySQL and Postgres on private networking, and rollback to the previous deploy when a release goes wrong. To see what a service and its database come to, the RunxBuild hosting calculator lists them as separate line items.
Useful related references:
- The Okta API: Tokens, Scopes, and the Rate Limit That Catches Everyone
- Docker Pricing: Personal, Pro, Team, Business (and the Free Tier)
- Redis Expire: The TTL, the EXPIRE Command, the Key Expiration, and the One Mistake That Fills the Memory
- Services on RunxBuild
FAQ
Can I extend a GitLab personal access token?
No. Expiry is set when the token is created and cannot be changed afterwards. The only path is to create a replacement with the same scopes, update everywhere the old one is stored, verify it works, and then revoke the old one. Always overlap — revoking first creates an outage.
When does GitLab warn me about expiring tokens?
A daily job runs at 01:00 UTC to find tokens nearing expiry and emails the owner. Treat that as a prompt to rotate rather than as a deadline, and rotate at around 75 percent of the token’s lifetime so the work happens without time pressure.
What should I use instead of a personal access token in CI?
The CI job token, which is available automatically inside every pipeline, is scoped to that job, and expires when the job finishes — so there is nothing to rotate. For access that must outlive a job, use a project or group access token, which belongs to the project rather than to a person and survives staff changes.
How do I find everywhere a token is used before rotating?
Search your repositories for the token prefix, check CI/CD variables at project and group level, look in git remotes and credential files on build agents, and check Docker registry logins and any third-party integrations. If the token appears anywhere in git history, treat it as compromised and revoke immediately rather than waiting for expiry.
What scopes should I grant?
The minimum the task needs. read_repository covers cloning for most CI. Full api scope includes destructive operations like deleting projects, so grant it only with a specific reason. Broad scopes granted under time pressure are how a leaked token turns a minor incident into a serious one.