Fixing Docker Login to GitHub Container Registry (ghcr.io) Failing with Invalid Username/Password — PAT Scope Error
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Your GitHub Personal Access Token (PAT) is missing the
write:packages(and/orread:packages) scope, or you're passing it incorrectly todocker loginagainstghcr.io. - How to fix it: Regenerate the PAT with
write:packages,read:packages, anddelete:packages(if needed) scopes, then pipe it correctly via stdin — never as a CLI argument. - Shortcut: Use our Client-Side Sandbox below to auto-refactor your failing
docker logincommand and validate your PAT scope configuration without leaking your token.
The Incident (What Does the Error Mean?)
You ran:
docker login ghcr.io -u YOUR_GITHUB_USERNAME -p ghp_xxxxxxxxxxxxxxxxxxxx
And got slapped with:
Error response from daemon: Get "https://ghcr.io/v2/": unauthorized: unauthenticated: User cannot be authenticated with the token provided.
# or
Error: Cannot perform an interactive login from a non TTY device
# or the classic:
Login did not succeed, error: Error response from daemon: unauthorized: incorrect username or password
Immediate consequence: Every docker push and docker pull from your CI runner is dead. Any pipeline step depending on a private GitHub Package image is now blocked. If this is a deploy pipeline, you have a production release freeze.
The Attack Vector / Blast Radius
This isn't just an annoyance — it's a symptom of a deeper PAT hygiene failure with real security consequences:
Why this is dangerous:
Over-scoped PAT as a workaround: The most common "fix" engineers reach for is regenerating a PAT with
repo(full repository access) scope because it happens to also grant package access. This is catastrophically over-privileged. A leakedrepo-scoped PAT gives an attacker full read/write access to all your private repositories — source code, secrets in Actions, everything.PAT passed as a CLI flag: Using
-pon the command line writes your token into shell history (~/.bash_history,~/.zsh_history) and is visible inps auxoutput on multi-tenant CI runners. Any process on that host can read it.Classic token vs. fine-grained token confusion: GitHub now has two PAT types. Classic PATs use package-level scopes. Fine-grained PATs handle packages differently — they require explicit repository permissions AND the token must be scoped to the correct org/repo. Mixing these up causes silent auth failures that look identical to wrong-password errors.
Blast radius: A single misconfigured PAT stored as a CI/CD secret and shared across pipelines means one token compromise = full registry access for every service image in your org.
How to Fix It (The Solution)
Root Cause Checklist (Run Through This First)
| Check | Command/Location |
|---|---|
PAT has write:packages scope? |
GitHub → Settings → Developer Settings → PAT |
PAT has read:packages scope? |
Same as above |
repo scope present? (Required if repo is private) |
Same as above |
Passing token via stdin, not -p flag? |
See fix below |
| Using correct registry URL? | Must be ghcr.io, not docker.pkg.github.com (deprecated) |
Basic Fix — Correct PAT Scope + Login Method
- docker login ghcr.io -u myusername -p ghp_mytoken
+ echo $CR_PAT | docker login ghcr.io -u myusername --password-stdin
Where CR_PAT is an environment variable holding your PAT. Never hardcode the token in the command.
For your PAT, the minimum required scopes for ghcr.io:
- Scope selected: repo (full control) ← WRONG, over-privileged
+ Scope selected: read:packages ← for pulling
+ Scope selected: write:packages ← for pushing
+ Scope selected: delete:packages ← only if you need image deletion
+ Scope selected: repo ← ONLY if the linked repository is private
Enterprise Best Practice — Fine-Grained PAT + GitHub Actions OIDC
Stop using long-lived PATs entirely for CI. Use the built-in GITHUB_TOKEN in GitHub Actions, which is automatically scoped and rotated per-job:
- name: Log in to ghcr.io
- run: echo "${{ secrets.MY_LONG_LIVED_PAT }}" | docker login ghcr.io -u myorg --password-stdin
+ name: Log in to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
The GITHUB_TOKEN is:
- Automatically scoped to the current repo only
- Rotated every job run — zero long-lived credential exposure
- Auditable via GitHub's token permission model
For the GITHUB_TOKEN to push packages, ensure your workflow has explicit permissions declared:
- # No permissions block — inherits org defaults (often read-only)
+ permissions:
+ contents: read
+ packages: write
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Enforce PAT Scope Policy with GitHub's Token Permissions API
Audit existing PAT scopes programmatically before they hit production:
curl -H "Authorization: token $PAT" https://api.github.com/user \
-I 2>&1 | grep -i x-oauth-scopes
# X-OAuth-Scopes: read:packages, write:packages, repo
If write:packages is absent from the response header, the token will fail on push.
2. Checkov — Scan IaC for Hardcoded Tokens
checkov -d . --check CKV_SECRET_6 # Detects hardcoded GitHub tokens in any file
3. Enforce permissions Block in All GitHub Actions Workflows (OPA/Conftest)
Write an OPA policy to reject any workflow YAML that lacks an explicit permissions block:
deny[msg] {
input.jobs[_].steps[_].uses == "docker/login-action@v3"
not input.permissions
msg := "Workflow using docker/login-action must declare explicit 'permissions' block with packages:write"
}
4. Rotate PATs via GitHub Secret Scanning
Enable GitHub Secret Scanning + Push Protection at the org level. This blocks any commit that contains a ghp_ prefixed token before it lands in the repo — catching the -p ghp_xxx antipattern in committed scripts.
Settings → Code security and analysis → Secret scanning → Push protection: Enable
5. Prefer Workload Identity Over PATs for Non-GitHub CI (GitLab, Jenkins, etc.)
For external CI systems, use a dedicated machine account (bot user) with only read:packages/write:packages scopes, store the token in your secrets manager (Vault, AWS Secrets Manager), and rotate it on a 90-day schedule via automation — not manually.