This error occurs when a Git authentication token expires during a long-running CI/CD build process. The token that was valid at the start of the job becomes invalid before Git operations complete, causing authentication failures mid-build.
This error indicates that the authentication token used by your CI/CD pipeline to access Git repositories has expired while the build was still running. CI/CD systems typically use time-limited tokens (Personal Access Tokens, GitHub App tokens, or OAuth tokens) that have a defined lifetime. When a build takes longer than the token's validity period, any Git operations attempted after expiration will fail with an authentication error. This is particularly common in: - Long-running builds with extensive test suites - Builds that perform Git operations at multiple stages - Pipelines with multiple sequential jobs sharing the same token - Builds that fetch additional repositories or submodules late in the process The root cause is a timing mismatch: the token's expiration time is shorter than the build duration. Most CI systems generate tokens at job start, so if your build runs for 2 hours but the token expires after 1 hour, any Git operation in the second half will fail.
First, identify the mismatch between your token's lifetime and your build duration:
Check build duration:
# In GitHub Actions, check the job summary
# In GitLab CI, check the pipeline duration
# In Jenkins, check the build time in console outputToken expiration times by type:
| Token Type | Default Expiration |
|------------|-------------------|
| GitHub App installation token | 1 hour |
| GitHub Actions GITHUB_TOKEN | Job duration (with refresh) |
| GitLab CI job token | Job duration |
| Personal Access Token | User-configured (often 90 days) |
| Azure DevOps PAT | User-configured |
If your build consistently takes longer than your token's expiration, you need to either reduce build time or implement token refresh.
Most CI systems provide built-in tokens that automatically refresh or have extended validity:
GitHub Actions (GITHUB_TOKEN):
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# GITHUB_TOKEN is automatically available and refreshes
token: ${{ secrets.GITHUB_TOKEN }}
# For operations requiring more permissions
- name: Push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}GitLab CI (CI_JOB_TOKEN):
build:
script:
- git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/group/repo.git
# CI_JOB_TOKEN is valid for the entire job durationAzure DevOps (System.AccessToken):
steps:
- checkout: self
persistCredentials: true
# System.AccessToken is automatically managedFor builds that exceed token lifetime, implement a refresh mechanism:
GitHub Actions with GitHub App:
jobs:
long-build:
runs-on: ubuntu-latest
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- name: Long running task
run: |
# ... your long build steps ...
# Refresh token before late-stage Git operations
- name: Refresh Token
id: refresh-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Late Git Operations
run: |
git push origin main
env:
GITHUB_TOKEN: ${{ steps.refresh-token.outputs.token }}Generic approach with token regeneration:
#!/bin/bash
# Function to refresh token before Git operations
refresh_git_token() {
# Call your token generation API/service
NEW_TOKEN=$(curl -s "https://your-token-service/generate")
git remote set-url origin "https://x-access-token:${NEW_TOKEN}@github.com/org/repo.git"
}
# Use before critical Git operations
refresh_git_token
git push origin mainBreak your pipeline into smaller jobs with fresh tokens:
GitHub Actions:
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps:
- uses: actions/checkout@v4
- name: Build
run: npm run build
- uses: actions/upload-artifact@v4
id: upload
with:
name: build-output
path: dist/
test:
needs: build
runs-on: ubuntu-latest
steps:
# Fresh token for this job
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Test
run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
# Fresh token for deployment
- uses: actions/checkout@v4
- name: Deploy
run: ./deploy.shBenefits:
- Each job gets a fresh token
- Parallel execution where possible
- Better failure isolation
- Clearer pipeline visibility
SSH keys don't expire during builds, making them ideal for long-running pipelines:
GitHub Actions with SSH:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Setup SSH Key
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Clone via SSH
run: git clone [email protected]:org/repo.git
- name: Long running build
run: |
# Hours later, SSH still works
make build-all
- name: Push changes
run: |
cd repo
git push origin mainGitLab CI with SSH:
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
build:
script:
- git clone [email protected]:group/repo.git
# SSH key has no expirationImportant: Store SSH keys securely in your CI secrets and use deploy keys with minimal permissions.
If possible, reduce build duration to fit within token expiration:
Caching strategies:
# GitHub Actions
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# GitLab CI
cache:
paths:
- node_modules/
- .npm/Parallel test execution:
# GitHub Actions matrix
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npm test -- --shard=${{ matrix.shard }}/4Skip unnecessary Git operations:
# Shallow clone
- uses: actions/checkout@v4
with:
fetch-depth: 1 # Only latest commit
# Don't fetch submodules if not needed
- uses: actions/checkout@v4
with:
submodules: falseMove Git operations early:
steps:
# Do ALL Git operations first while token is fresh
- uses: actions/checkout@v4
- run: git fetch --all
- run: git submodule update --init
# Then do long-running non-Git tasks
- run: npm install
- run: npm run build
- run: npm testIf your CI system allows, extend the token lifetime:
GitHub Personal Access Tokens:
1. Go to Settings > Developer settings > Personal access tokens
2. Select or create a token
3. Set expiration to a longer period (or no expiration for CI)
GitHub App tokens:
GitHub App installation tokens are limited to 1 hour maximum. Use refresh strategies instead.
GitLab Project Access Tokens:
1. Go to Project > Settings > Access Tokens
2. Create a token with a longer expiration date
3. Use sparingly - long-lived tokens are a security risk
Azure DevOps PATs:
1. Go to User Settings > Personal Access Tokens
2. Set expiration up to 1 year
3. Consider using shorter expirations with automated rotation
Security considerations:
- Longer token lifetimes increase security risk if leaked
- Use the shortest practical expiration
- Implement token rotation for production systems
- Restrict token permissions to minimum required scopes
Add retry logic to handle token expiration during builds:
Bash retry wrapper:
#!/bin/bash
git_with_retry() {
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
if "$@"; then
return 0
fi
echo "Git operation failed (attempt $attempt/$max_attempts)"
# Check if it's an auth error
if git ls-remote origin 2>&1 | grep -q "Authentication failed"; then
echo "Authentication error detected, refreshing credentials..."
# Add your token refresh logic here
refresh_credentials
fi
attempt=$((attempt + 1))
sleep 5
done
echo "Git operation failed after $max_attempts attempts"
return 1
}
# Usage
git_with_retry git push origin mainGitHub Actions with retry action:
- uses: nick-invision/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: |
git push origin mainPython with retry decorator:
import subprocess
import time
from functools import wraps
def retry_git(max_attempts=3, delay=5):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except subprocess.CalledProcessError as e:
if "Authentication failed" in str(e.output):
print(f"Auth failed, retrying ({attempt + 1}/{max_attempts})")
refresh_token() # Your refresh logic
time.sleep(delay)
else:
raise
raise Exception("Max retries exceeded")
return wrapper
return decorator### Understanding Token Types and Lifetimes
Different CI/CD systems use different token mechanisms:
GitHub Actions GITHUB_TOKEN:
- Automatically provided and refreshed
- Scoped to the repository
- Permissions can be customized in workflow
- Generally doesn't expire during job execution
GitHub App Installation Tokens:
- Maximum lifetime: 1 hour
- Can be regenerated within workflow
- Better for organization-wide operations
- Require app installation setup
GitLab CI/CD Job Tokens:
- Valid for job duration
- Limited permissions by default
- Can access other projects with CI_JOB_TOKEN if configured
### Token Refresh Patterns
Proactive refresh:
# Refresh before expected expiration
- name: Check token age
run: |
TOKEN_AGE=$((($(date +%s) - $TOKEN_CREATED_AT)))
if [ $TOKEN_AGE -gt 3000 ]; then # 50 minutes
echo "needs_refresh=true" >> $GITHUB_OUTPUT
fi
id: check
- name: Refresh if needed
if: steps.check.outputs.needs_refresh == 'true'
uses: actions/create-github-app-token@v1
# ...Reactive refresh:
# Retry on auth failure with fresh token
git push || (refresh_token && git push)### Debugging Token Issues
# Check token expiration (GitHub)
curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/rate_limit
# Decode JWT token (if applicable)
echo $TOKEN | cut -d. -f2 | base64 -d | jq .exp
# Test authentication without side effects
git ls-remote origin HEAD### Security Best Practices
1. Minimum privilege: Only grant tokens the permissions they need
2. Short lifetimes: Use the shortest practical expiration
3. Rotate regularly: Automated rotation prevents stale credentials
4. Audit usage: Monitor token usage in CI logs
5. Secure storage: Use CI secret management, never hardcode tokens
### Common CI Platform Specifics
GitHub Actions:
- GITHUB_TOKEN refreshes automatically
- Use actions/create-github-app-token for controlled refresh
- Matrix builds each get independent tokens
GitLab CI:
- CI_JOB_TOKEN is job-scoped and doesn't expire during job
- Use CI_DEPLOY_TOKEN for cross-project access
- Group-level tokens available for shared runners
Jenkins:
- Use Credentials plugin for token management
- Jenkins Git plugin handles many auth scenarios
- Consider Blue Ocean for better token handling
CircleCI:
- Project tokens don't expire during jobs
- Use contexts for shared secrets
- SSH keys recommended for long builds
kex_exchange_identification: Connection closed by remote host
Connection closed by remote host when connecting to Git server
fatal: unable to access: Proxy auto-configuration failed
How to fix 'Proxy auto-configuration failed' in Git
fatal: unable to access: Authentication failed (proxy requires basic auth)
How to fix 'Authentication failed (proxy requires basic auth)' in Git
fatal: unable to access: no_proxy configuration not working
How to fix 'no_proxy configuration not working' in Git
fatal: unable to read tree object in treeless clone
How to fix 'unable to read tree object in treeless clone' in Git