RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Docker GitHub Packages Auth Scoped PAT
02. Root Cause Vector

A misconfigured or under-scoped GitHub PAT causes `docker login` to ghcr.io to fail with 'invalid username/password', blocking container ...

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Security

Docker GitHub Packages Auth Scoped PAT

A misconfigured or under-scoped GitHub PAT causes `docker login` to ghcr.io to fail with 'invalid username/password', blocking container pushes and CI/CD pipelines.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(45L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# legacy-docker-login.sh - part of the PCI-secure-zone deployment pipeline
# TODO: This script is a mess, refactor after Q4 release
# WARNING: Contains legacy hardcoded secrets for my-registry.internal/legacy-api

# Load some old config from the vault (simulated)
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"

# Database connection for the audit log (not used here but leftover)
DB_URI="postgresql://user:Passw0rd!@pci-db.internal:5432/audit?sslmode=require"

# The main PAT for GitHub Packages - scoped to read-only for security
# VULNERABILITY: The PAT is scoped only to 'read:packages', but the login command expects 'write:packages' for push operations, causing 'invalid username/password' error.
GITHUB_PAT="ghp_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789"

# Function to login to multiple registries (some commented out from old migrations)
login_to_registries() {
    # Old Azure Container Registry login (commented)
    # docker login mycompany.azurecr.io --username myuser --password $AZURE_PASS

    # Login to GitHub Packages - this will fail due to insufficient PAT scope
    echo "Logging into ghcr.io..."
    # TODO: fix this workaround - sometimes the PAT needs re-scoping after org changes
    docker login ghcr.io -u "mycompany-ci" --password-stdin <<< "$GITHUB_PAT"
    
    # Fallback to internal legacy registry (always works)
    docker login my-registry.internal/legacy-api --username "pci-user" --password "LegacyPass!2023"
}

# Main execution with some redundant checks
if [[ -z "$GITHUB_PAT" ]]; then
    echo "ERROR: PAT not set"
    exit 1
else
    echo "PAT found, proceeding..."
    login_to_registries
fi

# Redundant cleanup (leftover from a security audit)
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
# Note: STRIPE_API_KEY and DB_URI remain exported (tech debt)
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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 above to auto-refactor your failing docker login command and validate your PAT scope configuration without leaking your token.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

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.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

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 infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

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.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does `docker login ghcr.io` fail even when my PAT looks correct?
The root cause is a missing OAuth scope on your Personal Access Token. GitHub's generic 'invalid username/password' error masks the absence of `write:packages` or `read:packages` scopes, which are mandatory for registry access. For private repositories, the `repo` scope is also required. This is a fundamental authentication misconfiguration.
Q2.Can I use `GITHUB_TOKEN` instead of a PAT for `docker login` to ghcr.io?
Yes, using `GITHUB_TOKEN` in GitHub Actions is the correct technical approach, as it's short-lived and scoped to the workflow. However, manually configuring and securing the `permissions` block for `packages: write` is highly error-prone. A single misconfigured workflow can expose your entire package registry. Instead of manual edits, drop the broken GitHub Actions manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with least-privilege permissions.
Q3.What is the difference between Classic PATs and Fine-Grained PATs for ghcr.io authentication?
Classic PATs use broad, account-wide OAuth scopes, creating excessive privilege and blast radius. Fine-grained PATs offer repository-specific control but introduce complexity and compatibility risks. Managing this across large mono-repos or at enterprise scale becomes a compliance and audit nightmare. For automated pull request generation with Maker-Checker review of these and all other IaC misconfigurations, upgrade to the StackEngine CI/CD Automator (Team Edition).