GitHub Packages, Container Registry, Package Permissions, Provenance, and Distribution: Concepts, Architecture, and Mental Model
Chapter 20 narrowed workflow identity and credentials. Chapter 21 follows the output of those workflows beyond one run: how GitHub Packages and the Container registry name, authorize, version, distribute, and retire software artifacts without confusing a mutable label with immutable identity or package hosting with provenance.
Learning objectives
- Distinguish a package/version from an Actions artifact and a repository Release asset.
- Map the GitHub Packages registries to granular or repository-scoped permission models.
- Explain GITHUB_TOKEN, package access roles, cross-repository Actions access, and non-Actions registry authentication.
- Separate mutable tags/versions from immutable container digest identity and consumer verification.
- Explain why repository/package hosting is not itself provenance and where artifact attestations fit.
Availability. The mandatory path targets GitHub.com, GitHub Free, a disposable public personal repository, and GitHub Container Registry (GHCR). Public Packages usage is free; GitHub currently also states that Container registry image storage and bandwidth are free. Private/internal distribution, enterprise namespaces, and organization policy are taught as optional variants rather than requirements.
1. The practical problem: a successful build is not yet a governed distribution
Chapter 17 produced run-scoped artifacts and Chapter 19 promoted
known bytes through environments. Production consumers, however,
need a durable distribution address with version history, access
control, and lifecycle. If a deployment says “pull
latest,” the package exists, but its identity is
ambiguous. If a private package is published correctly but the
consuming repository has no package access, authentication succeeds
and authorization still fails. If an image is on GitHub, that proves
where it is hosted—not how it was built.
The operating model is therefore: namespace → package → version/tag → immutable identifier → permissions → provenance evidence → lifecycle policy. Each layer answers a different question.
2. Four artifact surfaces that solve different lifecycle problems
| Surface | Primary lifetime | Consumer model | Identity | Use it when |
|---|---|---|---|---|
| Actions artifact | Workflow/run evidence | Workflow jobs and operators | Artifact ID + digest/run identity | Passing files between jobs, test evidence, temporary CI output |
| Release asset | Repository release | Humans/tools fetching a release | Release/tag + asset bytes/digest | Distributing a finite file attached to a release |
| GitHub package | Package/version lifecycle | Native package manager or registry client | Package namespace + version metadata | Dependencies and versioned distribution |
| Container package in GHCR | Registry lifecycle | Docker/OCI clients | Tag plus immutable manifest digest | Distributing Docker/OCI images |
Moving a ZIP from an Actions run into a Package registry does not make it the same object. Retention, permissions, download APIs, namespace ownership, and consumer tooling all change. Chapter 11 covered release assets; Chapter 17 covered workflow artifacts. This chapter focuses on the package/registry boundary.
3. Supported registries and their permission models
| Registry/ecosystem | Typical client | Package scope model | Important implication |
|---|---|---|---|
| Container registry | Docker / OCI tooling | Granular user/organization scope; may link to repository | Visibility/access can be managed separately unless inherited |
| npm | npm | Granular user/organization scope | Package can have package-level access independent of repository |
| NuGet | dotnet / nuget | Granular user/organization scope | Package-level read/write/admin roles available |
| RubyGems | gem | Granular user/organization scope | Package-level access available |
| Apache Maven | mvn | Repository-scoped only | Package inherits repository permissions/visibility |
| Gradle | gradle | Repository-scoped only | Package inherits repository permissions/visibility |
“Linked to a repository” and “owned by a repository” are not synonyms. Granular registries scope the package to a personal account or organization; linking supplies source context and can enable inherited access. Maven and Gradle remain repository-scoped, so their authorization model is structurally different.
4. Package objects, container versions, tags, and digests
flowchart TD
R["Repository + workflow"] -->|packages: write / GITHUB_TOKEN| P["GHCR package namespace"]
P --> V1["Version manifest digest D1"]
P --> V2["Version manifest digest D2"]
T1["tag 1.0.0"] --> V1
TL["tag latest"] --> V2
C["Consumer"] -->|pull by digest| V1
A["Attestation, if generated"] -->|binds subject-name + digest| V1
The repository workflow publishes into the account namespace. GHCR
records image versions whose durable content identity is a
manifest digest, such as sha256:….
Tags such as 1.0.0 and latest are labels
that can point at a manifest; a tag can later move to another
digest. A consumer that needs byte-for-byte identity therefore
records and pulls the digest.
The last arrow is intentionally conditional: an attestation exists only if the producer generated one. Hosting the package does not automatically create provenance evidence for an ordinary package publish.
5. Authentication is not package authorization
Granular packages expose read, write, and admin roles. Read can download and inspect metadata; write can upload new versions; admin can additionally manage/delete and grant access. When a package is linked before publication and inherits repository permissions, workflows in that repository normally receive package access automatically. A different repository must be explicitly added under the package’s Actions access when the package is private or otherwise restricted.
| Operation | Workflow token permission | Package-side access | Result |
|---|---|---|---|
| Pull linked/private package | packages: read |
Consumer repository has read access | Allowed |
| Push new version | packages: write |
Publisher repository has write access | Allowed |
| Delete/restore via Actions REST preview | Token + package admin | Repository has admin access | Potentially allowed; preview, destructive |
| Anonymous pull of public GHCR image | No GitHub token | Package is public | Allowed |
A token can be valid yet receive 403/denied
because the repository is not authorized for that package or because
the job reduced packages to read. Chapter 20’s identity
model therefore continues directly into package distribution.
6. GITHUB_TOKEN versus registry credentials
Inside GitHub Actions, GitHub recommends
GITHUB_TOKEN for registries that support granular
permissions when the workflow repository is associated with or
granted access to the package. The token is job-scoped and avoids a
stored long-lived registry credential.
Outside Actions, GitHub Packages registry clients still document a personal access token (classic) with the corresponding package scopes. This is an important boundary: support for fine-grained or GitHub App tokens on individual Packages REST endpoints does not make a fine-grained PAT a supported Docker/npm registry login credential. Treat registry protocol authentication and REST API authentication as separate interfaces.
7. Repository linking, source labels, and inheritance
For a GHCR image, the OCI label
org.opencontainers.image.source=https://github.com/OWNER/REPO
connects the image to its source repository when publishing from the
command line. Publishing from a workflow with
GITHUB_TOKEN also associates the workflow repository,
but the source label remains valuable metadata and prevents
namespace/link ambiguity.
If a package is linked before it is first published, GitHub can inherit access permissions from that repository by default. Linking a package later does not retroactively mean the same thing as inheriting access; the package can retain its existing granular permission set unless inheritance is explicitly selected.
8. Visibility and lifecycle are governance decisions
A new granular package is private by default. Making a disposable GHCR package public enables anonymous pulls and a clean no-credential consumer lab, but GitHub warns that once a package is public it cannot be made private again. This is why the lab package contains only synthetic text and why the visibility change is explicitly marked irreversible.
Deletion is also not routine cleanup for supported releases. A public package/version with more than 5,000 downloads cannot be deleted through normal controls; deleted packages/versions can normally be restored within 30 days if the namespace has not been reused. Production retention should therefore be defined before consumers depend on a version.
9. Digest integrity is necessary; provenance is separate evidence
A digest answers “are these the same registry bytes?” It does not answer “which reviewed source and workflow produced them?” Artifact attestations can cryptographically bind a subject name and SHA-256 digest to workflow/source claims, and GitHub can push container attestations to an OCI registry. Chapter 25 will build that mechanism in depth. In this chapter the rule is narrower: never infer provenance merely because a package is hosted on GitHub.
10. Read-only inspection before publication
Start by proving repository identity and package absence. Do not create a package merely to discover where it would live.
gh auth status --hostname github.com
OWNER=$(gh api user --jq .login)
REPO="$OWNER/github-packages-lab"
IMAGE="ghcr.io/${OWNER,,}/github-packages-lab"
echo "repo=$REPO"
echo "planned_image=$IMAGE"
gh repo view "$REPO" --json nameWithOwner,visibility,viewerPermission 2>/dev/null || true
If the repository does not exist yet, the last command failing is expected. Existing public package metadata can be inspected from the account’s Packages page; after publication the workflow will also record the exact registry digest and structured package-version metadata.
Knowledge check
Why is an Actions artifact not a substitute for a package registry?
Actions artifacts are run-scoped workflow data with their own retention/access model; packages are durable namespace/version resources designed for package-manager or registry consumers.
Which current GitHub Packages registries support granular permissions?
Container registry, npm, NuGet, and RubyGems. Maven and Gradle are repository-scoped.
If latest points at digest D1 today, is D1
guaranteed to remain the meaning of latest?
No. The tag can move. Record and pull the immutable digest when exact content identity matters.
Can a fine-grained PAT be assumed to work as a GHCR Docker login because some Packages REST endpoints accept fine-grained tokens?
No. Registry protocol authentication and REST authentication are separate interfaces; GitHub Packages registry docs still specify a classic PAT outside Actions, while Actions should prefer GITHUB_TOKEN.
Does a package hosted on GitHub automatically prove build provenance?
No. Hosting and digest integrity do not establish source/workflow provenance; an attestation or equivalent provenance evidence must be generated and verified separately.
Summary
Packages are distribution resources, not Git objects and not
workflow artifacts. Model namespace, permission scope, version/tag,
immutable digest, and provenance separately. Prefer
GITHUB_TOKEN inside Actions, grant cross-repository
package access deliberately, pin consumers to exact digests when
identity matters, and define deletion/retention before a version
becomes supported.
Next, the lab publishes a synthetic scratch image to GHCR, makes that disposable package public, consumes it anonymously by digest in a second workflow, and proves a read-only token cannot push.
Official references
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.