GitLab CI Error Guide: 'The requested URL returned error: 403' — Fix CI_JOB_TOKEN Scope
Fix 'fatal: unable to access: The requested URL returned error: 403' in GitLab CI: add the source project to the target's CI/CD job token allowlist, grant the pipeline user access, and authenticate cross-project clones correctly.
- #gitlab
- #ci-cd
- #troubleshooting
- #errors
Stuck on this GitLab CI/CD error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
This error appears when a job uses the automatic CI_JOB_TOKEN to reach another GitLab project — cloning a dependency repo, pulling a package, or calling the API — and that target project refuses the token:
fatal: unable to access 'https://gitlab.example.com/group/internal-lib.git/': The requested URL returned error: 403
The API and package-manager variants carry the same 403:
{"message":"403 Forbidden - CI_JOB_TOKEN scope is not allowed to access this project"}
npm error code E403
npm error 403 Forbidden - GET https://gitlab.example.com/api/v4/projects/.../packages/npm/...
A 403 is authorization, not authentication: the token is valid, but the target project has not allowed the source project’s job token to access it. This is the CI/CD job token scope allowlist doing exactly what it is designed to do.
Symptoms
- A cross-project
git cloneorgit submodulefetch fails with403during the job, even though the same URL works with your personal token. - Pulling a private package (npm, Maven, PyPI, Composer) from another project’s package registry returns
403 Forbidden. - The error started after upgrading GitLab or after an admin tightened token scoping — previously “just worked” pipelines now fail.
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" "$CI_API_V4_URL/projects/<id>"returns403 ... scope is not allowed.- A 401 would mean the credential was missing or wrong; here you are authenticated as the pipeline but not authorized for that project.
Common Root Causes
- Source project not in the target’s allowlist — the target project’s CI/CD → Token Access → Allow access to this project with a CI_JOB_TOKEN list does not include the source project or its group. This is the default-deny behavior on current GitLab.
- Pipeline user lacks membership — even with the allowlist entry,
CI_JOB_TOKENacts with the permissions of the user who triggered the pipeline; if that user is not a member of the target project, private resources stay forbidden. - Private visibility — the target project is private and nothing grants the job token read access.
- Wrong credential form — using
CI_JOB_TOKENwith a plain HTTPS clone URL instead of thegitlab-ci-token:${CI_JOB_TOKEN}@hostform, or as an API header, so it is not sent correctly. - Package registry access — the job token needs project access to read another project’s package registry; a missing allowlist entry blocks it.
- Cross-instance access —
CI_JOB_TOKENonly works within the same GitLab instance; a token cannot authorize a project on a different host.
Diagnostic Workflow
First prove it is the job token scope and identify the exact target project id. Run a minimal API probe from the failing job:
debug-token-scope:
stage: test
script:
- 'curl -sS -o /dev/null -w "%{http_code}\n" --header "JOB-TOKEN: $CI_JOB_TOKEN" "$CI_API_V4_URL/projects/<TARGET_ID>"'
# 200 = allowed, 403 = source project not on the target allowlist / user lacks access
Confirm you are using the correct clone form for cross-project fetches — this is the supported way to use the job token for HTTPS clones:
fetch-dependency:
stage: build
script:
- git clone "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/group/internal-lib.git"
- ls internal-lib
The fix for the 403 is on the target project. Add the source project (or its group) to the target’s job token allowlist. In the UI: Target project → Settings → CI/CD → Token Access → Allow access to this project with a CI_JOB_TOKEN → Add project/group. Automate it with the API from an admin/maintainer context (not from the failing job’s job token):
# Allow SOURCE project to access TARGET project via CI_JOB_TOKEN
curl --request POST \
--header "PRIVATE-TOKEN: $ADMIN_TOKEN" \
--data "target_project_id=<SOURCE_PROJECT_ID>" \
"https://gitlab.example.com/api/v4/projects/<TARGET_PROJECT_ID>/job_token_scope/allowlist"
Always validate any .gitlab-ci.yml change with CI Lint (Pipeline editor → Validate) before pushing so a config typo is not mistaken for a scope problem.
If the allowlist entry exists but 403 persists, the triggering user is not a member of the target project. Add the pipeline user (or a group) as at least a Reporter on the target so the token’s inherited permissions include read access.
Example Root Cause Analysis
A microservice pipeline cloned a shared library from another project and had worked for months. After a GitLab upgrade that enabled job token scope enforcement by default, every pipeline began failing:
fatal: unable to access 'https://gitlab.example.com/platform/shared-proto.git/': The requested URL returned error: 403
Running the API probe from the job confirmed it:
> curl ... "$CI_API_V4_URL/projects/1487"
403
{"message":"403 Forbidden - CI_JOB_TOKEN scope is not allowed to access this project"}
The clone URL was already using the gitlab-ci-token:${CI_JOB_TOKEN}@ form, so the credential was correct. The real cause was that the target platform/shared-proto project had a default-deny job token allowlist that did not include the consuming service’s project. The maintainer added the source project to platform/shared-proto’s allowlist:
curl --request POST --header "PRIVATE-TOKEN: $ADMIN_TOKEN" \
--data "target_project_id=982" \
"https://gitlab.example.com/api/v4/projects/1487/job_token_scope/allowlist"
The next pipeline cloned the library and passed. No token was rotated and no secret was added — the fix was purely authorization scope.
Prevention Best Practices
- Treat the CI/CD job token allowlist as the primary control: for every cross-project clone, package pull, or API call, add the source project (or a shared group) to the target’s allowlist.
- Prefer group-level allowlist entries for a fleet so new consumer projects inherit access instead of failing one by one.
- Use the
https://gitlab-ci-token:${CI_JOB_TOKEN}@host/...clone form (or theJOB-TOKENheader for the API) — do not hand-build URLs that omit the credential. - Ensure the pipeline-triggering identity has at least Reporter membership on any private target project; the job token only inherits that user’s access.
- Document cross-project dependencies so a project’s required allowlist entries are recreated intentionally, not rediscovered via 403s after an upgrade.
- Keep
CI_JOB_TOKENscope tight; grant only the projects that genuinely need each other rather than opening the allowlist broadly.
Quick Command Reference
# Probe whether the current job token can reach a target project (403 vs 200)
curl -sS -o /dev/null -w "%{http_code}\n" \
--header "JOB-TOKEN: $CI_JOB_TOKEN" "$CI_API_V4_URL/projects/<TARGET_ID>"
# Correct cross-project clone using the job token
git clone "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/group/dep.git"
# Add SOURCE project to TARGET project's job-token allowlist (admin/maintainer token)
curl --request POST --header "PRIVATE-TOKEN: $ADMIN_TOKEN" \
--data "target_project_id=<SOURCE_PROJECT_ID>" \
"https://gitlab.example.com/api/v4/projects/<TARGET_PROJECT_ID>/job_token_scope/allowlist"
# List a project's current job-token allowlist
curl --header "PRIVATE-TOKEN: $ADMIN_TOKEN" \
"https://gitlab.example.com/api/v4/projects/<TARGET_PROJECT_ID>/job_token_scope/allowlist"
Conclusion
A 403 from CI_JOB_TOKEN means the token is valid but the target project has not authorized the source project to use it — the job token scope allowlist is default-deny by design. Confirm with a JOB-TOKEN-header API probe, then fix it on the target project: add the source project or group to its CI/CD job token allowlist and make sure the triggering user has at least Reporter access. Use the gitlab-ci-token:${CI_JOB_TOKEN}@ clone form, and manage allowlists deliberately so GitLab upgrades that tighten scoping never surprise your cross-project pipelines.
Fixed it? Get 500 GitLab CI/CD & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.