highIntermediateSymptom-first#registry#docker#401#token

GitLab container registry push fails - expired token or 401

docker push to registry.gitlab.com fails with 'unauthorized' or 401

Likely source:availabilitysecurity

Focused incident mode — strips away everything but this runbook.

Symptoms

  • docker push to registry.gitlab.com fails with 'unauthorized' or 401
  • CI job log shows 'denied: access forbidden' or 'authentication required'
  • Image build succeeds but push step fails at the registry

GitLab deploy tokens expire on the date set during creation. If no expires_at is specified, they never expire - but they can still be revoked manually.

Evidence to grab

Collect these artifacts before you change anything — they prove (or rule out) each cause.

  • Docker login output

    glab ci trace <PIPELINE_ID> --job <JOB_ID> | grep -E 'login|unauthorized|denied' | head -10
  • Registry credential configuration

    glab api /projects/<PROJECT_ID>/variables --jq '.[] | select(.key | test("REGISTRY|TOKEN")) | .key'

Diagnostic flow

Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.

Guided decision tree is ready.

Steps with commands, expected output, and pass/fail branches.

Ranked root causes

Evidence checklist

  • Token was created > 90 days ago and has not been rotated
  • docker login fails with 401

Confirm before fixing

glab api /projects/<PROJECT_ID>/deploy_tokens --jq '.[] | {name, expires_at, active}'
READ

Check deploy token expiry

Fix

glab api -X POST /projects/<PROJECT_ID>/deploy_tokens -f name='new-deploy-token' -f scopes='write_registry' -f expires_at='2026-12-31'
CHANGE

Create a new deploy token with write_registry scope

Resolution runbook

Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.

1
glab ci trace <PIPELINE_ID> --job <JOB_ID> | grep -E 'login|unauthorized|denied' | head -10
READ

Read the exact error

2
echo $REGISTRY_PASSWORD | docker login registry.gitlab.com -u <USERNAME> --password-stdin
READ

Test credentials

3
glab api /projects/<PROJECT_ID>/deploy_tokens --jq '.[] | {name, expires_at, scopes}'
READ

Check token expiry and scopes

4
glab api -X POST /projects/<PROJECT_ID>/deploy_tokens -f name='new-token' -f scopes='write_registry'
CHANGE

Create a fresh deploy token if needed

Prevention

  • Set a calendar reminder to rotate deploy tokens before they expire.
  • Use CI/CD job tokens (CI_JOB_TOKEN) for intra-project registry pushes instead of PATs.
  • Always create tokens with the minimum required scope (write_registry only when pushing).
  • Audit deploy token usage quarterly and delete unused tokens.

Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.