GitHub Packages, Container Registry, Package Permissions, Provenance, and Distribution: Configuration, Design Choices, and Tradeoffs
The guided lab proved one safe GHCR path. Production architecture requires choosing the distribution surface, ownership model, visibility, identity convention, and credential boundary deliberately. This lesson turns those choices into an auditable policy instead of letting package-manager defaults decide governance.
Learning objectives
- Choose package, Release asset, Actions artifact, or external repository based on consumer/lifecycle needs.
- Choose inherited repository access or granular package access for supported registries.
- Design semantic tags for humans while preserving digest/version identity for machines.
- Separate public, private, and Enterprise internal distribution concerns.
- Justify registry authentication and provenance controls by maintainability, security, reliability, compatibility, and cost.
Availability. Examples target GitHub.com. Granular permissions currently apply to Container, npm, NuGet, and RubyGems; Maven and Gradle remain repository-scoped. Internal package visibility is an organization/enterprise concept and Maven/Gradle do not currently support internal repository visibility. GHES package hostnames and availability depend on administrator configuration; GHES 3.21 requires Packages configuration and subdomain isolation for its Container registry.
1. Package registry versus Release asset versus Actions artifact versus external repository
Start with consumer behavior. A developer declaring an npm dependency expects semver resolution and a package registry. A Kubernetes deployment expects an OCI registry. A customer downloading one installer may prefer a Release asset. CI passing a test bundle between jobs needs an Actions artifact. A company already operating Nexus/Artifactory may need proxying, promotion repositories, retention, replication, or ecosystem breadth beyond the scope of this GitHub course.
| Choice | Maintainability | Security/governance | Reliability/compatibility | Cost/operating burden |
|---|---|---|---|---|
| GitHub Packages/GHCR | Low friction near GitHub source/workflows | Package roles + repository linkage + Actions identity | Native package/OCI clients; GitHub availability dependency | Public packages free; private usage/quotas apply |
| Release assets | Simple for release-coupled files | Governed by repository/release permissions | Good for explicit downloads; not dependency resolution | Low operational burden |
| Actions artifacts | Excellent for CI evidence | Run/repository access + retention | Not a long-term package distribution contract | Storage/retention tied to Actions |
| External artifact repository | Centralized enterprise capabilities | Can unify policy across many SCM/CI systems | Adds external availability/integration dependency | Software/infrastructure/admin cost |
2. Repository-linked inheritance versus granular package access
Inherited access minimizes configuration when package lifecycle follows one repository: repository readers/writers and its Actions workflows naturally align with package consumers/publishers. Granular access is useful when one package is a shared platform resource consumed by a carefully selected set of repositories independent of source-repository membership.
Granularity creates another policy object to maintain. If teams cannot answer “which repositories have package read/write/admin and why?”, the flexibility can become authorization drift. Prefer inheritance until independent package access solves a real boundary.
3. Cross-repository workflow access: explicitly grant the consumer
For a private granular package, adding
packages: read to the consumer workflow is only half
the configuration. Package settings must also grant that repository
read access under Manage Actions access. The
producer can retain write/admin while consumers get read only.
permissions:
contents: read
packages: read
# Registry authentication in that approved consumer job:
printf '%s' "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
Do not grant a public repository access to a private package casually: GitHub warns that forks of that public repository may be able to access the private packages. Package access therefore participates in the fork trust model from Chapters 13 and 20.
4. Semantic version/tag for navigation; digest for immutable deployment identity
Humans need labels such as 2.4.1, stable,
or latest. Automation needs a statement that survives
label movement. For containers, record both:
human_selector = ghcr.io/acme/service:2.4.1
immutable_ref = ghcr.io/acme/service@sha256:0123...cdef
A release policy can forbid moving semantic version tags after publication while still treating the digest as the cryptographic content identifier. Deployments should record the digest actually pulled, not merely the requested tag.
5. Public, private, and internal distribution are different threat models
| Visibility | Who can discover/read | Typical use | Governance note |
|---|---|---|---|
| Public | Anyone; public GHCR images support anonymous pull | Open source and public runtime images | Visibility cannot later be reverted to private |
| Private | Explicitly authorized users/teams/repositories | Restricted dependencies/images | Storage/data transfer quotas and access mapping matter |
| Internal | Enterprise members for supported granular registries | Enterprise-wide shared components | Enterprise Cloud/organization policy; not a GitHub Free personal path |
6. Credential design: prefer workflow identity; do not broaden registry tokens by habit
Inside Actions, use GITHUB_TOKEN when the package is
associated with or grants access to the workflow repository. Outside
Actions, GitHub Packages registry docs use classic PAT package
scopes. For automation spanning many repositories, a GitHub App may
be a better GitHub API identity, but do not assume that makes its
installation token a supported native registry-client login in every
ecosystem.
Never place registry tokens directly in .npmrc,
Dockerfiles, image layers, command history, Actions artifacts, or
cache paths. If a package credential is exposed, revoke/rotate it
first; deleting a config file later does not invalidate a leaked
token.
7. Provenance policy: hosted, digest-pinned, attested, verified are four different states
Define maturity explicitly:
- Hosted: bytes exist in a registry.
- Digest-pinned: consumer can request exact bytes.
- Attested: producer generated signed provenance binding subject name/digest to build identity.
- Verified: consumer checked that attestation against an acceptance policy.
Skipping from “hosted” to “trusted” is a supply-chain category error. Chapter 25 will implement and verify attestations in depth.
8. Cost and retention belong in the package architecture
Public Packages usage is free. Private Packages usage has plan-specific included storage/data transfer and may incur cost beyond that allowance. GitHub currently states Container registry image storage and bandwidth are free, but the documentation also says this policy could change with advance notice. Avoid freezing a cost assumption into architecture: monitor current billing docs and define cleanup/retention by supported-version policy rather than “delete whatever is old.”
9. Worked decision: internal service platform
A platform team builds a base container used by 40 private services. The services need reproducible deployments; only the platform repository may publish; all service repositories may pull; security wants build provenance.
| Decision | Selected approach | Why |
|---|---|---|
| Surface | GHCR granular package | OCI-native and close to GitHub workflows |
| Scope | Organization package linked to platform repo; explicit consumer repositories | Publisher ownership stays narrow; consumers get read only |
| Identity | Semver tag + recorded digest | Humans get release semantics; machines get immutable identity |
| Visibility | Private/internal according to enterprise policy | Avoid public disclosure of internal base image |
| Credentials | GITHUB_TOKEN per approved workflow repo | Short-lived; no shared registry PAT |
| Provenance | Attest digest, require verification in deploy policy | Hosting alone does not satisfy provenance requirement |
| Retention | Keep every supported digest; delete only after support/EOL review | Prevents rollback/support breakage |
Knowledge check
When is repository-inherited package access usually preferable?
When package ownership and consumers align closely with one repository and separate package ACLs would add unnecessary authorization drift.
Why must a private cross-repository consumer configure both packages:read and package-side Actions access?
Token permission constrains what the job may request; package-side access decides whether that repository is authorized to the package. Both boundaries must permit the read.
Why record a digest even if policy says semantic version tags never move?
The digest independently identifies content and lets consumers verify what was actually pulled; policy promises and content identity are different evidence.
What changes when a package is made public?
Anyone can access it; public GHCR pulls can be anonymous, and GitHub warns the visibility cannot later be reverted to private.
What extra claim is needed before “this GitHub-hosted image has provenance”?
An actual generated attestation or equivalent provenance evidence bound to the image digest, followed by consumer verification.
Summary
Choose the distribution surface from the consumer lifecycle, package access from the ownership boundary, and identity from the verification requirement. Tags are ergonomic; digests are immutable content identity. Prefer short-lived workflow identity, keep visibility decisions deliberate, and do not let package hosting substitute for provenance. Next, diagnose the ways these layers fail independently.
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.