Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 9 min read

GitLab CI Error Guide: 'HTTP Basic: Access denied' Git Clone Authentication Failures

Fix GitLab CI git clone auth errors fast: CI_JOB_TOKEN allowlists, submodule URLs, expired deploy tokens, and 'HTTP Basic: Access denied' Authentication failed.

  • #gitlab-cicd
  • #troubleshooting
  • #errors
  • #authentication

Overview

One of the most disruptive failures in a GitLab pipeline happens before any of your real work even starts: the runner cannot clone or fetch the repository. The job dies in the “Getting source from Git repository” or “Updating/initializing submodules” phase, long before your script: ever runs. Because the failure is an authentication problem rather than a code problem, retrying does nothing, and the message is usually some variant of HTTP Basic: Access denied or could not read Username.

The confusion comes from the fact that GitLab CI uses a short-lived, automatically-injected credential called CI_JOB_TOKEN for the main project checkout. That token works transparently for the job’s own repository, but the moment a job needs to reach another project — a cross-project clone, a private submodule, or a third-party private repo — the job token’s permissions, the target project’s allowlist, or a separately configured deploy token all come into play. Any mismatch produces an auth failure that looks identical regardless of the underlying cause.

A typical failure in the job log looks like this:

Fetching changes with git depth set to 20...
Initialized empty Git repository in /builds/group/app/.git/
Created fresh repository.
remote: HTTP Basic: Access denied. The provided password or token is incorrect
remote: or your account has 2FA enabled.
fatal: Authentication failed for 'https://gitlab.com/group/private-lib.git/'
ERROR: Job failed: exit code 1

When the credential is missing entirely rather than wrong, the runner — which is non-interactive and has no terminal to prompt on — fails differently:

Submodule 'vendor/private-lib' (https://gitlab.com/group/private-lib.git) registered for path 'vendor/private-lib'
Cloning into '/builds/group/app/vendor/private-lib'...
fatal: could not read Username for 'https://gitlab.com': No such device or address
fatal: clone of 'https://gitlab.com/group/private-lib.git' into submodule path '/builds/group/app/vendor/private-lib' failed
ERROR: Job failed: exit code 128

Symptoms

  • The job fails during “Getting source from Git repository” or “Updating/initializing submodules”, never reaching your script:.
  • The log shows remote: HTTP Basic: Access denied or fatal: Authentication failed for 'https://gitlab.com/...'.
  • A submodule or cross-project clone fails with fatal: could not read Username for 'https://gitlab.com': No such device or address.
  • The exact same git clone works from your laptop with your own credentials but fails on the runner.
  • Retrying the job produces the identical error every time (it is not flaky).

Start by confirming where in the pipeline lifecycle the clone is failing and which URL is being rejected:

# Inspect the raw job log section that fails
glab ci trace --job <job-id> | sed -n '/Getting source/,/Job failed/p'
Getting source from Git repository
$ git remote set-url origin https://gitlab-ci-token:[MASKED]@gitlab.com/group/app.git
Fetching changes...
Initialized empty Git repository in /builds/group/app/.git/
remote: HTTP Basic: Access denied
fatal: Authentication failed for 'https://gitlab.com/group/app.git/'

Then reproduce the credential context the runner actually uses, so you are testing the job token rather than your personal session:

# From inside a debug job (or `glab ci run` shell), echo the auth-relevant context
echo "Project path: $CI_PROJECT_PATH"
echo "Server host:  $CI_SERVER_HOST"
echo "Token length: ${#CI_JOB_TOKEN}"
git config --get-regexp '^url\.' || echo "no insteadOf rewrites configured"
Project path: group/app
Server host:  gitlab.com
Token length: 0
no insteadOf rewrites configured

A token length of 0 is a strong signal: the job token never reached the git context, so any clone of a protected repo will be denied.

Common Root Causes

1. Cross-project clone blocked by the target project’s job-token allowlist

When job A in group/app clones group/private-lib using CI_JOB_TOKEN, the target project decides whether to accept that token. Under Settings > CI/CD > Job token permissions (the “CI/CD job token allowlist” / Token Access), group/private-lib must list group/app as an allowed project. If it does not, the token is valid but unauthorized for that repo.

git clone "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/group/private-lib.git"
Cloning into 'private-lib'...
remote: HTTP Basic: Access denied. The provided password or token is incorrect
remote: or your account has 2FA enabled.
fatal: Authentication failed for 'https://gitlab.com/group/private-lib.git/'

The fix is to add the calling project (group/app) to the allowlist of the target project (group/private-lib), not the other way around.

2. Submodule pinned to an absolute HTTPS URL the job token can’t authenticate

If .gitmodules uses an absolute URL like https://gitlab.com/group/private-lib.git, GitLab does not automatically rewrite it with the job token, so the runner tries to clone it anonymously and fails. Relative submodule URLs inherit the parent’s authenticated remote and work transparently.

cat .gitmodules
# .gitmodules — problematic absolute URL
[submodule "vendor/private-lib"]
    path = vendor/private-lib
    url = https://gitlab.com/group/private-lib.git
Cloning into '/builds/group/app/vendor/private-lib'...
fatal: could not read Username for 'https://gitlab.com': No such device or address
fatal: clone of 'https://gitlab.com/group/private-lib.git' into submodule path 'vendor/private-lib' failed
Failed to clone 'vendor/private-lib'. Retry scheduled

Fix it by changing the URL to relative (url = ../private-lib.git) so the job token is reused, or by adding an insteadOf rewrite in before_script (see cause 4).

3. Expired or revoked deploy token / project access token stored in a CI/CD variable

Many pipelines clone private dependencies with a long-lived deploy token or project access token stored as a masked CI/CD variable (for example DEP_TOKEN with username gitlab+deploy-token-7). When that token expires or is revoked, every job that uses it starts failing with an auth error even though the YAML is unchanged.

git clone "https://${DEPLOY_USER}:${DEPLOY_TOKEN}@gitlab.com/group/private-lib.git"
Cloning into 'private-lib'...
remote: HTTP Basic: Access denied. The provided password or token is incorrect
remote: or your account has 2FA enabled.
fatal: Authentication failed for 'https://gitlab.com/group/private-lib.git/'

Check the token’s status under Settings > Repository > Deploy tokens (or Settings > Access tokens for a project access token). If it shows expired or is missing, mint a new one with read_repository scope and update the masked variable.

4. Wrong username/token combination or a PAT missing read_repository scope

HTTP Basic: Access denied is GitLab’s generic rejection for a bad credential pair. A common mistake is using a personal access token (PAT) with the wrong username, or a PAT that has the api scope but not read_repository. GitLab requires read_repository (or write_repository) specifically for git-over-HTTPS, regardless of other scopes.

# A PAT used to clone — username can be anything non-empty when using a PAT
git clone "https://oauth2:${MY_PAT}@gitlab.com/group/private-lib.git"
Cloning into 'private-lib'...
remote: HTTP Basic: Access denied. The provided password or token is incorrect
remote: or your account has 2FA enabled.
fatal: Authentication failed for 'https://gitlab.com/group/private-lib.git/'

Regenerate the token with read_repository enabled, and confirm the username convention: deploy tokens use their literal username, while a PAT can use oauth2 as the username with the token as the password.

5. Third-party private repo with no credentials configured on the runner

When a job clones a private repo on an external host (GitHub, a self-managed GitLab, or a vendor server), CI_JOB_TOKEN does not apply at all. With no credential helper and no embedded credentials, the non-interactive runner has nothing to send and cannot prompt, so git reports it could not read the username.

git clone https://github.com/acme/internal-tools.git
Cloning into 'internal-tools'...
fatal: could not read Username for 'https://github.com': No such device or address
ERROR: Job failed: exit code 128

Provide an explicit credential for the external host via a masked CI/CD variable and an insteadOf rewrite (or an SSH deploy key), since the GitLab job token has no authority off-platform.

6. Job token feature disabled, or “Limit access to this project” excluding the needed project

A project can globally restrict its own job token under Settings > CI/CD > Job token permissions with options like “Limit access to this project” enabled and an empty allowlist, or the inbound token access turned off entirely. Likewise, SAML SSO enforcement or 2FA on an account can block password-style git over HTTPS, since GitLab removed plain password auth for HTTPS git operations.

# Even the project's own checkout can fail if job-token access is misconfigured
git clone "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
Cloning into 'app'...
remote: HTTP Basic: Access denied. The provided password or token is incorrect
remote: or your account has 2FA enabled.
fatal: Authentication failed for 'https://gitlab.com/group/app.git/'

Re-enable inbound job-token access (or add the required projects to the allowlist), and for any human credential over HTTPS, switch to a token with read_repository scope rather than an account password, since SAML/2FA will reject the password path.

Diagnostic Workflow

Step 1: Identify which URL is being rejected

Read the failing log section and note the exact repository URL in the fatal: line. Distinguish the project’s own checkout ($CI_PROJECT_PATH) from a submodule path or a cross-project clone — the cause and fix differ entirely depending on which repo is denied.

glab ci trace --job <job-id> | grep -E 'Cloning into|fatal:|Access denied|could not read Username'

Step 2: Determine which credential is in play

Decide whether the clone relies on CI_JOB_TOKEN (same-platform, automatic) or a custom variable (deploy token, PAT, project access token). Echo the token length, never the value, from inside a debug job to confirm the credential is actually present and non-empty.

echo "job token len: ${#CI_JOB_TOKEN} | custom token len: ${#DEPLOY_TOKEN}"

Step 3: Test the credential against the exact target

Run a minimal clone using the same credential the failing job uses, against the specific repo that is being denied. This isolates whether the problem is the credential itself or something around it (submodule wiring, rewrites).

git clone --depth 1 "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/group/private-lib.git" /tmp/probe

Step 4: Check allowlist and token settings in the target project

If the credential is the job token and the target is another GitLab project, open the target project’s Settings > CI/CD > Job token permissions and confirm the calling project is allowlisted. If the credential is a deploy/access token, verify its expiry and that it carries read_repository.

Step 5: Inspect submodule URLs and git rewrites

For submodule failures, read .gitmodules and confirm whether URLs are relative or absolute, and whether an insteadOf rewrite is configured in before_script. Verify GIT_SUBMODULE_STRATEGY is set when submodules are required.

cat .gitmodules && git config --get-regexp '^url\.'

Example Root Cause Analysis

A team’s group/app pipeline began failing only in jobs that pulled a shared library as a submodule. The main checkout succeeded, but the submodule step died:

Updating/initializing submodules recursively...
Submodule 'vendor/shared-lib' (https://gitlab.com/group/shared-lib.git) registered for path 'vendor/shared-lib'
Cloning into '/builds/group/app/vendor/shared-lib'...
fatal: could not read Username for 'https://gitlab.com': No such device or address
fatal: clone of 'https://gitlab.com/group/shared-lib.git' into submodule path 'vendor/shared-lib' failed

Following the workflow, Step 1 showed the rejected URL was the submodule, not the project itself. Step 2 confirmed CI_JOB_TOKEN was present and length non-zero. Step 5 revealed the real problem — .gitmodules used an absolute HTTPS URL, so GitLab never rewrote it with the job token:

[submodule "vendor/shared-lib"]
    path = vendor/shared-lib
    url = https://gitlab.com/group/shared-lib.git

There were two valid fixes. The cleanest was switching the submodule to a relative URL so it inherits the authenticated parent remote:

git config -f .gitmodules submodule.vendor/shared-lib.url ../shared-lib.git
git submodule sync
git add .gitmodules && git commit -m "Use relative submodule URL for CI auth"

Where the absolute URL had to stay, the team added a job-token rewrite in before_script so any https://gitlab.com/ fetch is authenticated automatically:

default:
  before_script:
    - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"

build:
  variables:
    GIT_SUBMODULE_STRATEGY: recursive
  script:
    - make build

After also confirming group/app was on the group/shared-lib job-token allowlist, the submodule clone authenticated cleanly:

Cloning into '/builds/group/app/vendor/shared-lib'...
remote: Enumerating objects: 482, done.
Submodule path 'vendor/shared-lib': checked out 'a1b2c3d4'

Prevention Best Practices

  • Prefer relative submodule URLs (../other-repo.git) so the parent’s authenticated remote — and the job token — are reused automatically.
  • Add a job-token insteadOf rewrite in a shared before_script whenever absolute GitLab HTTPS URLs are unavoidable.
  • Maintain the CI/CD job token allowlist deliberately: add calling projects to each target’s Settings > CI/CD > Job token permissions rather than disabling the protection.
  • Set calendar reminders for deploy-token and project-access-token expiry, and rotate them before they lapse; always grant the minimal read_repository scope.
  • Never commit a token in .gitmodules or .gitlab-ci.yml; use masked, protected CI/CD variables and confirm masking did not silently drop a token that fails the masking rules.
  • For external private repos, store a dedicated credential and use an insteadOf rewrite or SSH deploy key, since CI_JOB_TOKEN has no authority off-platform.
  • Browse more patterns in the GitLab CI/CD category, and when an auth failure blocks a release, our incident response assistant can help triage the failing trace quickly.

Quick Command Reference

# Pull the failing clone section out of a job trace
glab ci trace --job <job-id> | sed -n '/Getting source/,/Job failed/p'
glab ci trace --job <job-id> | grep -E 'Cloning into|fatal:|Access denied|could not read Username'

# Confirm the credential context (lengths only, never echo the token value)
echo "job token len: ${#CI_JOB_TOKEN} | custom token len: ${#DEPLOY_TOKEN}"
echo "host: $CI_SERVER_HOST | project: $CI_PROJECT_PATH"

# Probe the exact repo with the job token
git clone --depth 1 "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/group/private-lib.git" /tmp/probe

# Probe with a deploy token / PAT
git clone --depth 1 "https://${DEPLOY_USER}:${DEPLOY_TOKEN}@gitlab.com/group/private-lib.git" /tmp/probe
git clone --depth 1 "https://oauth2:${MY_PAT}@gitlab.com/group/private-lib.git" /tmp/probe

# Rewrite all gitlab.com HTTPS fetches to use the job token
git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/".insteadOf "https://gitlab.com/"

# Inspect and fix submodule wiring
cat .gitmodules && git config --get-regexp '^url\.'
git config -f .gitmodules submodule.vendor/shared-lib.url ../shared-lib.git
git submodule sync && git submodule update --init --recursive

Conclusion

HTTP Basic: Access denied and could not read Username failures almost always trace back to which credential reached the git context and what that credential is allowed to do — not to your pipeline logic. Working through them methodically turns an opaque rejection into a quick fix.

The six root causes to check, in order:

  1. A cross-project clone blocked because the target project’s job-token allowlist does not include the calling project.
  2. A submodule pinned to an absolute HTTPS URL that the job token cannot authenticate — needs a relative URL or an insteadOf rewrite.
  3. An expired or revoked deploy token / project access token stored in a CI/CD variable.
  4. A wrong username/token pair or a PAT missing the read_repository scope.
  5. An external third-party private repo with no credentials configured on the non-interactive runner.
  6. The job-token feature disabled or “Limit access to this project” excluding the needed project, or SAML/2FA blocking password git over HTTPS.

Identify the rejected URL, confirm the credential in play, then match the symptom to one of these six — and the access-denied wall almost always comes down in minutes.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

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.