Initializing Enclave...

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/or read:packages) scope, or you're passing it incorrectly to docker login against ghcr.io.
  • How to fix it: Regenerate the PAT with write:packages, read:packages, and delete: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 login command 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:

  1. 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 leaked repo-scoped PAT gives an attacker full read/write access to all your private repositories — source code, secrets in Actions, everything.

  2. PAT passed as a CLI flag: Using -p on the command line writes your token into shell history (~/.bash_history, ~/.zsh_history) and is visible in ps aux output on multi-tenant CI runners. Any process on that host can read it.

  3. 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.

  4. 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.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →