Chapter 24

Artifact Registry

Scope. This chapter owns the artifact at rest: repository formats and modes, container image and language package storage, repository IAM, immutable tags, vulnerability scanning, promotion between repositories, and retention and cleanup. Building the artifact is Chapter 23; deploying and verifying it is Chapter 25. §9.26 and §10.26 cite this chapter for GKE and Cloud Run image consumption. Prerequisites. Chapter 3 (§3.14 resource-level IAM), Chapter 14 (§14.8 CMEK), Chapter 22 (§22.11 dependency scanning), Chapter 23. Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0, hashicorp/google provider 8.x; see sources at end.

Between the build and the deployment sits a store, and the store is where most of a delivery pipeline's security properties are actually enforced. A build produces provenance; the registry is where that provenance lives. A deployment gate verifies a signature; the registry is what it verifies against. A promotion from staging to production is, mechanically, a copy between two repositories with different IAM.

Artifact Registry is also the only registry Google Cloud supports for new work — §9.25 covers the deprecated predecessor and why every image reference in this book uses REGION-docker.pkg.dev. And it is not just a container registry: it stores Maven, npm, Python, Go, apt, and yum artifacts under the same IAM model, which means the language dependencies your build pulls can come from a repository you control rather than from the public internet.

The design decision that matters most is how many repositories and where the boundary between them sits. One repository per environment, with different write permissions on each, turns promotion into an IAM-enforced act rather than a naming convention. One shared repository with tags for environments turns it into a string comparison anybody can defeat. Everything in §24.7 through §24.10 follows from that choice.

24.1 Repository Types §

A repository is the unit of format, location, IAM, and encryption. Two attributes fix its behavior at creation: format and mode.

Eight formats are supported, and the SDK's enum is the authoritative list: "apt (APT package format), docker (Docker image format), go (Go module format), kfp (KFP package format), maven (Maven package format), npm (NPM package format), python (Python package format), yum (YUM package format)."

Three modes matter, and they are what make Artifact Registry a supply chain control rather than a bucket:

ModeGoogle's descriptionUse
standard-repository"should be possible to write/read data to this repo"Your own artifacts
remote-repository"fetches data from upstream and caches it"A controlled proxy for public dependencies
virtual-repository"aggregates data from several upstreams"One endpoint fronting several repositories

Remote repositories are the answer to public-registry dependency risk. Instead of your build pulling directly from Docker Hub, npm, PyPI, Maven Central, or the Go proxy, it pulls through a remote repository that caches what it fetched — so a package deleted or altered upstream does not change your build, and everything you consumed is scannable and auditable in your own project.

gcloud artifacts repositories create remote-npm \
  --project=rc-saas-shared-art-01 \
  --location=us-central1 \
  --repository-format=npm \
  --mode=remote-repository \
  --remote-npm-repo=npmjs \
  --description="Cached proxy for the public npm registry."

Virtual repositories give one endpoint and a priority order. --upstream-policy-file takes a JSON list of {id, repository, priority} entries, so a build configures one registry URL and the virtual repository resolves internal-first, public-second.

Pitfall. A virtual repository whose internal upstream has lower priority than the public one is a dependency-confusion attack waiting to happen: an attacker publishes a package with your internal name to the public registry and your builds prefer it. Internal upstreams always take the lower priority number.

24.2 Container Images §

A container image in Artifact Registry is addressed as REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE, by tag or by digest.

Authentication is your Google identity, through a credential helper. gcloud auth configure-docker LOCATION-docker.pkg.dev "adds the host to your Docker configuration file's credHelpers section, enabling automatic authentication" — so a developer's docker pull uses their own credentials and a workload's pull uses its attached service account (§9.26, §10.26).

gcloud auth configure-docker us-central1-docker.pkg.dev

gcloud artifacts docker images list \
  us-central1-docker.pkg.dev/rc-saas-shared-art-01/prod-docker \
  --include-tags --show-occurrences

Reference by digest everywhere it matters. A tag is a mutable pointer; IMAGE@sha256:... names one specific set of bytes. Every deployment manifest, Binary Authorization policy (§25.10), and provenance check in this book uses the digest form, and the tag exists for humans reading a console.

--show-occurrences surfaces the metadata attached to an image — build occurrences, discovery, vulnerabilities, and SBOM references — with --occurrence-filter defaulting to build, image, discovery, and SBOM reference kinds. This is how you check what Artifact Analysis knows about an image without leaving the CLI.

Pitfall. Deleting an image also deletes its attachments, including build provenance (§23.13). A cleanup policy tuned to reclaim storage will remove the record of how a released artifact was built, which is the evidence you need if that release turns out to matter.

24.3 Language Packages §

The same repository model stores Maven, npm, Python, Go, apt, and yum artifacts, with the same IAM and the same scanning.

gcloud artifacts print-settings emits the client configuration for each ecosystem — the .npmrc, settings.xml, or pip.conf fragment that points a tool at the repository and uses the Google credential helper.

Two reasons to host language packages here rather than externally:

  • Internal libraries get real access control. A private npm package in an Artifact Registry repository is governed by roles/artifactregistry.reader on that repository, which is the same IAM as everything else and appears in the same audit log.
  • Public dependencies get cached and scanned. Through a remote repository (§24.1), every package your build consumed is stored in your project and evaluated by Artifact Analysis.

Maven has two extra controls worth setting. --version-policy takes none, release, or snapshot — "The repository accepts release versions only" is the setting for a production repository, so a snapshot build can never be published where a release is expected.

Pitfall. Language ecosystems resolve names before they resolve registries. A build configured with both a public registry and an internal one, without an explicit scope or priority, can be served an attacker's package that happens to share a name. Scope internal packages (@rc-saas/ for npm, a reserved group ID for Maven) and use a virtual repository with the internal upstream first.

24.4 Repository IAM §

Repository-level IAM is what makes the multi-repository promotion model (§24.7) an enforced control rather than a convention.

Four roles cover every legitimate need:

RoleGrant to
roles/artifactregistry.readerWorkload identities that pull — GKE node service accounts, Cloud Run services
roles/artifactregistry.writerBuild service accounts, on the one repository they publish to
roles/artifactregistry.repoAdminThe platform team, for repository configuration
roles/artifactregistry.adminRarely; project-level administration
gcloud artifacts repositories add-iam-policy-binding prod-docker \
  --location=us-central1 \
  --project=rc-saas-shared-art-01 \
  --member="serviceAccount:sa-gke-node-rc-saas-prod-gke-01@\
rc-saas-prod-app-01.iam.gserviceaccount.com" \
  --role=roles/artifactregistry.reader

Grant on the repository, never on the project. A project-level roles/artifactregistry.writer lets a development pipeline publish into the production repository, which erases the boundary the repositories exist to create.

A build identity must not hold roles/artifactregistry.repoAdmin. Writer publishes; repository admin can change cleanup policies, disable immutable tags, and delete artifacts — which is exactly what a compromised build would do to hide itself.

Pitfall. Reader on a repository is reader on everything in it, including the metadata that reveals what you build and when. Where a workload only needs one image, the repository is still the smallest IAM boundary available — so structure repositories so that "everything in it" is an acceptable grant.

24.5 Immutable Tags §

Immutable tags make a tag a permanent binding to one digest, which turns a human-readable name into a trustworthy identifier.

The flag's own description states exactly what it does: "--immutable-tags (Docker only) Prevent changes to tagged images in the repository. Tags cannot be deleted or moved to a different image digest, and tagged images cannot be deleted."

gcloud artifacts repositories create prod-docker \
  --project=rc-saas-shared-art-01 \
  --location=us-central1 \
  --repository-format=docker \
  --immutable-tags \
  --kms-key=projects/rc-saas-shared-sec-01/locations/us-central1/keyRings/\
kr-us-central1-cicd/cryptoKeys/k-artifact-registry \
  --description="Production container images. Immutable tags, CMEK."

Three attacks it closes, all of which involve a valid signature or scan result on one image and a different image at the same name:

  • Repointing a scanned, signed tag at a new digest after verification.
  • Deleting and re-pushing a tag to substitute content.
  • Deleting a tagged image that a running workload or an audit still references.

It is Docker-only and it changes operations. With immutable tags, :latest cannot exist as a moving pointer, and a rebuild of the same commit must produce a new tag. That is the intended discipline for a production repository and it is genuinely inconvenient for a development one — which is why §24.8 and §24.10 configure them differently.

Pitfall. Immutable tags interact with cleanup policies: a tagged image cannot be deleted, so a policy written to reclaim space by age will silently keep every tagged version forever. Size the repository for that, or scope the immutable repository to release artifacts only.

24.6 Vulnerability Scanning §

Artifact Analysis scans artifacts in Artifact Registry and stores the results as occurrences attached to the artifact.

Scanning covers OS packages and two language ecosystems by default. Google's wording: "Operating System packages and language packages (Java and Go) can be scanned for vulnerabilities. Automatic Scanning (On-Push): Operating system packages are automatically scanned when pushed to Artifact Registry. Language packages are also automatically scanned, but automatic language package scanning is only available for Artifact Registry."

The Container Analysis API is the prerequisitecontaineranalysis.googleapis.com — and scanning is enabled or disabled per repository with --allow-vulnerability-scanning and --disable-vulnerability-scanning.

On-demand scanning covers more ecosystems and runs anywhere in the pipeline (§23.12). gcloud artifacts docker images scan scans all package types by default — the older --additional-package-types flag is deprecated for exactly that reason — and --skip-package-types narrows it, taking values from a set including COMPOSER, GO, MAVEN, NPM, NUGET, PYTHON, RUBYGEMS, and RUST.

Read the results without leaving the CLI:

gcloud artifacts docker images describe \
  us-central1-docker.pkg.dev/rc-saas-shared-art-01/prod-docker/billing@sha256:DIGEST \
  --show-package-vulnerability \
  --show-provenance \
  --show-sbom-references

Continuous re-evaluation is the property that build-time scanning cannot provide. An image scanned clean at build time becomes vulnerable when a new CVE is published against something inside it, and the registry's view — surfaced as a Security Command Center finding (§16.4) — is what tells you.

Pitfall. Scanning a repository does not scan what is running. An image deleted from the registry but still running on a node has no findings, and an image running from a different registry has none either. Reconcile the running inventory against the registry rather than trusting the registry's list.

24.7 Image Promotion §

Promotion is moving an artifact from a lower-trust repository to a higher-trust one after it has passed a gate. It is the mechanism that makes "tested in staging" a fact rather than a claim.

Promotion is a copy between repositories with different IAM, not a retag. Because a tag lives inside one repository, crossing the boundary means moving the manifest and its layers:

SRC=us-central1-docker.pkg.dev/rc-saas-shared-art-01/stg-docker/billing
DST=us-central1-docker.pkg.dev/rc-saas-shared-art-01/prod-docker/billing

docker pull "${SRC}@sha256:DIGEST"
docker tag "${SRC}@sha256:DIGEST" "${DST}:v1.4.2"
docker push "${DST}:v1.4.2"

Retagging within a repository — marking a validated candidate as the release — is the one-command case:

gcloud artifacts docker tags add \
  us-central1-docker.pkg.dev/rc-saas-shared-art-01/stg-docker/billing@sha256:DIGEST \
  us-central1-docker.pkg.dev/rc-saas-shared-art-01/stg-docker/billing:rc-1.4.2

The digest must not change. Re-pushing an unmodified manifest reproduces the same digest, so a correct promotion moves the same bytes under a new name; a promotion that rebuilds the artifact for production has tested something else. This is why every gate in the pipeline records a digest, and why the deployment references one (§25.12).

The identity that promotes is not the identity that builds. sa-promote-prod holds roles/artifactregistry.writer on prod-docker and reader on stg-docker; sa-build-billing holds writer on dev-docker only. That separation is what stops a compromised build from publishing straight to production.

Gate the promotion on evidence, not on a schedule: a clean vulnerability scan (§24.6), passing tests (§23.11), and, where used, a valid attestation (§25.11).

Pitfall. Copying an image between repositories creates a new artifact record in the destination, and the provenance and scan occurrences are attached to the source. Verify that the metadata your deployment gate checks is reachable from the promoted reference, or the gate will fail on a legitimately promoted image.

24.8 Development Repositories §

The development repository is the highest-volume, lowest-trust store in the estate, and its configuration should reflect both.

Optimize for churn and cost, not for permanence:

SettingDevelopmentWhy
Immutable tagsOffRebuilding a branch tag is the normal workflow
Cleanup policyAggressive — untagged after 7 days, everything after 30Volume is high and value is low
Vulnerability scanningOnFindings here are cheapest to fix
CMEKOptionalNo production data, and the operational cost is real
Write accessEvery build service accountDevelopers need to publish freely

It is still a supply chain input. An attacker who can push to the development repository is one promotion mistake away from production, so the repository is not unprotected — it is differently protected, and the control that matters is that nothing promotes out of it without passing §24.7's gate.

Never deploy production from it. The temptation is real during an incident, and the outcome is a production workload running an artifact with no scan record, no provenance check, and a mutable tag.

Judgment. One development repository per format, shared across teams, is usually right. Per-team development repositories multiply the cleanup policies you have to maintain and buy an isolation nobody uses.

Pitfall. Development repositories grow without bound because nobody owns the cost. A repository with three years of untagged layers costs real money and makes every list operation slow. Set the cleanup policy at creation, not when the bill arrives.

24.9 Staging Repositories §

The staging repository holds candidates — artifacts that have passed build-time gates and are being validated before promotion.

Its distinguishing property is that its contents are release candidates, not experiments. That justifies immutable tags, since a candidate that changes under a fixed name has invalidated whatever testing was done against it.

Write access belongs to the promotion identity, not to builds. A build publishes to development; a gate promotes to staging; a second gate promotes to production. Each hop is a different principal with writer on exactly one repository.

Retention should exceed the release cycle, not the development cycle. If you release weekly, keeping candidates for ninety days lets you compare a regression against the last dozen candidates. Development's thirty-day policy is too short for that.

Scan results here are the promotion gate. A candidate with a new CRITICAL finding does not promote, and the finding is attached to the artifact rather than living in a pipeline log — which means the decision is auditable months later.

Pitfall. Staging repositories accumulate candidates that were never promoted and never explained. A candidate that failed its gate should be recorded as failed — a security mark (§16.2), a label, or simply a cleanup policy that removes unpromoted candidates — or the repository becomes a list of artifacts with no known status.

24.10 Production Repositories §

The production repository is the smallest, most restricted, and most audited store in the estate. Everything running in production came from here, and nothing else did.

Its configuration is the strict end of every option:

SettingProduction
Immutable tagsOn (§24.5)
CMEKOn, with a key in rc-saas-shared-sec-01 (§14.8)
Vulnerability scanningOn, with findings routed to Security Command Center
Write accessThe promotion identity only
Read accessProduction workload identities only
Cleanup policyConservative, with an explicit Keep for released tags

Read access is a real boundary here. A development service account with reader on the production repository can pull a production image, which is usually harmless and occasionally reveals configuration baked into a layer. Grant reader to production workload identities and to the deployment pipeline, and nothing else.

Every production deployment references a digest (§25.12), which the immutable tag makes redundant and the redundancy is the point: two independent mechanisms saying the same artifact.

CMEK here does something specific. It ties the readability of every production artifact to a key you can disable, which is the containment lever if the registry itself is implicated in an incident (§34.4).

Pitfall. The production repository must be readable by the workload service accounts of every project that runs production code, which are in different projects from the registry. This is a cross-project resource-level grant, and it is the one people implement as a project-level grant in the registry project because it is easier — losing the boundary in the process.

24.11 Artifact Retention §

Retention is how long an artifact stays in a repository, and it is a compliance and forensics question before it is a cost one.

Three retention drivers, and they point in different directions:

DriverWants
CostDelete aggressively; layers are large
RollbackKeep enough released versions to roll back several releases (§25.9)
Forensics and complianceKeep every artifact that ever ran in production, with its provenance

Resolve them per repository, not globally. Development can be aggressive because nothing there ever ran in production. Production must keep every released artifact for as long as you must be able to answer questions about it, which is usually the same horizon as the audit log (§17.13).

Provenance follows the artifact. Google states that build provenance and other attachments "aren't subject to cleanup policies. Instead, attachments are deleted when the image they are attached to is deleted." So deleting an old production image deletes the record of how it was built — the two cannot be retained separately.

Untagged versions are the cheap win. Intermediate and superseded layers with no tag accumulate quickly and are almost never needed. A policy deleting untagged versions older than a short window reclaims most of the storage without touching anything a rollback needs.

Pitfall. A rollback target that was cleaned up is discovered during the rollback. Set the production Keep policy from the number of releases you might realistically roll back through, not from the number you have ever rolled back through.

24.12 Artifact Cleanup Policies §

A cleanup policy is a JSON list attached to a repository, evaluated to delete or keep versions.

The schema is small and worth learning exactly:

FieldValues
nameUnique within the repository's policies
action.type"Delete" or "Keep"
condition.tagState"tagged", "untagged", or "any"
condition.tagPrefixes, versionNamePrefixes, packageNamePrefixesPrefix lists
condition.olderThan, newerThanDurations, e.g. "30d"
mostRecentVersions.keepCountNumber of versions to retain
[
  {
    "name": "delete-untagged",
    "action": {"type": "Delete"},
    "condition": {"tagState": "untagged", "olderThan": "7d"}
  },
  {
    "name": "keep-releases",
    "action": {"type": "Keep"},
    "condition": {"tagState": "tagged", "tagPrefixes": ["v"]}
  },
  {
    "name": "keep-recent",
    "action": {"type": "Keep"},
    "mostRecentVersions": {"keepCount": 20}
  }
]

Keep wins over Delete, which is the safety property the whole design rests on: "When an artifact matches the criteria for both a delete policy and a keep policy, the artifact is kept." Write the Delete policy broadly and the Keep policies precisely.

Always dry-run first. --dry-run on set-cleanup-policies disables actual deletion so the policy's effect is logged rather than applied, and there is no undelete for a version that a policy removed.

gcloud artifacts repositories set-cleanup-policies prod-docker \
  --location=us-central1 \
  --project=rc-saas-shared-art-01 \
  --policy=cleanup-prod.json \
  --dry-run

Pitfall. A policy that deletes by age with no Keep for released tags will delete a two-year-old release that is still running on a long-lived customer deployment. Every production policy needs an explicit Keep for the tag prefix your releases use, written before the Delete policy is enabled.

Chapter Summary §

  • Artifact Registry supports eight formats and three useful modes; remote repositories turn public dependency risk into a cached, scannable, auditable store you control.
  • A virtual repository must give internal upstreams the lower priority number, or it is a dependency-confusion vector.
  • Authenticate with gcloud auth configure-docker LOCATION-docker.pkg.dev; workloads authenticate with their attached service account.
  • Reference images by digest everywhere it matters; the tag is for humans.
  • Grant Artifact Registry roles on the repository, never on the project, or the repository boundary buys nothing.
  • A build identity gets writer on one repository and never repository admin.
  • Immutable tags prevent tags being deleted or moved and prevent tagged images being deleted — Docker format only.
  • Immutable tags plus an age-based cleanup policy retain every tagged version forever; size for it or scope the repository to releases.
  • OS packages and Java and Go language packages are scanned automatically on push; on-demand scanning covers more ecosystems.
  • Continuous re-evaluation, not the build-time scan, is what tells you a stored image became vulnerable.
  • Promotion is a copy of the same digest between repositories with different IAM, performed by an identity that is not the build identity.
  • Development repositories are high-churn and low-trust with aggressive cleanup; production is immutable, CMEK-encrypted, and writable only by the promotion identity.
  • Deleting an image deletes its attachments, including build provenance — retention of the artifact and its provenance cannot be separated.
  • In cleanup policies, Keep wins over Delete; write Delete broadly and Keep precisely, and always dry-run first.
  • Every production cleanup policy needs an explicit Keep for the released tag prefix.

Security Checklist §

ControlWhy it mattersHow to verify (CLI + Console)
Separate repositories per environmentPromotion is IAM-enforced only if the repositories differgcloud artifacts repositories list --location=us-central1
IAM granted at repository scope, not projectA project grant erases the environment boundarygcloud artifacts repositories get-iam-policy REPO --location=REGION
Build identity holds writer, never repository adminRepository admin can delete the evidence of a pushRepository IAM policy, filtered to sa-build-*
Production repository has --immutable-tagsPrevents a verified tag being repointedgcloud artifacts repositories describe prod-docker --location=REGION
Production repository has CMEKTies artifact readability to a key you can disableRepository kmsKeyName field
Vulnerability scanning enabled on every repositoryContinuous re-evaluation is the only current viewRepository vulnerabilityScanningConfig
Promotion performed by a distinct identityA compromised build must not reach productionIAM policies of dev-docker, stg-docker, prod-docker
Cleanup policies dry-run before enforcementThere is no undelete for a removed versiongcloud artifacts repositories describe REPO shows cleanupPolicyDryRun
Production cleanup policy has an explicit Keep for release tagsAge-based deletion otherwise removes a running releasePolicy JSON in version control
Remote repositories used instead of direct public pullsCaches, scans, and audits every external dependencyBuild configuration points at the remote repository
Virtual repository priorities put internal upstreams firstOtherwise a public package can shadow an internal name--upstream-policy-file contents

Sources §