GitHub Container Registry (GHCR)
GHCR is a thin auth wrapper over a Distribution-spec registry, but its access model is unusual: package visibility is independent of the source repository's visibility, and ownership lives on a user or org namespace rather than a single account. Most GHCR connection problems trace back to these two facts.
Auth options
| Method | When to use | HarborGuard fields |
|---|---|---|
Classic PAT with read:packages | Simplest path, broad access | Username = GitHub login, Password = PAT |
| Fine-grained PAT | Restricted to specific repositories or organizations | Same as above |
| GitHub App installation token | Automated / org-managed access without a personal account | Username = x-access-token, Password = installation token |
GITHUB_TOKEN (Actions only) | Workflow scanning the same repo's package | Not applicable — issued at job runtime |
GHCR does not accept the GitHub UI password — only tokens.
Add the registry
- Registries → Connect Registry.
- Registry URL —
ghcr.io(no namespace; HarborGuard discovers what the token can see). - The wizard shows
GitHub GHCR Detected. - Cloud: enter your GitHub login + PAT.
- Save.
GHCR's /v2/_catalog returns only what the authenticated principal can read, so the catalog reflects exactly what your PAT was issued for.
Required PAT scopes
For a classic PAT:
read:packages— required, sufficient for public + private packages the user can see.repo— required only if any of the packages are linked to a private repository (GHCR currently inherits the repo's read permission).
For a fine-grained PAT:
- Repository access: select the repos whose packages you want to scan.
- Permissions → Repository → Packages: Read-only.
- Permissions → Account → Packages: Read-only (if scanning user-namespace packages outside any repo).
For a GitHub App:
- Repository permissions → Packages: Read-only + Metadata: Read-only.
- Install on the relevant repositories or org.
Push-event sync
GHCR doesn't have a registry-side webhook. Instead, GitHub fires a package repository event on push.
- On the source repository (or org level), add a webhook for the
Packageevent. - URL:
https://<your-harborguard-host>/api/webhooks/github. - Set the registry's schedule to
on_push.
For a multi-repo org, prefer an organization-level webhook so you don't manage one per repo.
Common pitfalls
- User vs org namespaces. A package owned by user
alicelives atghcr.io/alice/<image>. The same image moved to orgacmelives atghcr.io/acme/<image>— same registry, different ACL. Forking does not move packages. - Package visibility. A package can be public even when its source repo is private (and vice versa). The package's own visibility setting governs
/v2/access. - PAT expiry. Classic PATs default to no expiry but can be rotated by org SSO policy. SSO-protected orgs require the PAT be authorized via Configure SSO after creation; otherwise it 404s on every package.
- Restricted scopes on fine-grained PATs. The default fine-grained PAT has no package permission. The most common failure mode is a PAT that authenticates against the GitHub API but 401s on
ghcr.io. docker.pkg.github.com. That's the deprecated GitHub Packages predecessor. HarborGuard does not target it; migrate to GHCR.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| 401 on test, PAT was created seconds ago | SSO authorization not granted on the PAT |
| Catalog is empty for an org with many packages | PAT scoped to user-only resources; needs org-level package read |
| Specific image 404s | Package visibility is private and PAT lacks repo scope on the linked repo |
| Push events ignored | Webhook is on the wrong repo (not the one that publishes the package) |
| Sensor mode works once, fails after token rotation | GitHub App installation token expired (1 hour) — refresh logic missing on the sensor |