Chapter 23Lesson 02~340 minutes

Container Registry, OCI Images, Cleanup, Authentication, Vulnerability Scanning, and Retention: Guided Hands-On Workflow and Core Operations

Inspect a disposable registry, publish or safely simulate one tiny OCI image, prove tag-to-digest identity, inspect metadata, exercise scanning or a fixture, and clean up without confusing tag deletion with garbage collection.

Hands-onRegistry APIglabDigest verificationScanningCleanup

Learning objectives

  • Inspect source/project/registry state before publishing.
  • Publish or safely simulate one tiny disposable OCI image without requiring privileged infrastructure.
  • Compare tag and digest references and capture registry metadata with glab/API.
  • Run current container scanning or a clearly labeled fixture and verify image/report identity.
  • Delete only verified disposable registry state after downstream-impact analysis.
Availability baseline (verified 2026-08-22 against current GitLab 19.3 documentation). The integrated Container Registry is Free/Premium/Ultimate on GitLab.com, Self-Managed, and Dedicated; Self-Managed administrators must enable and operate it. Registry authentication supports job-scoped CI credentials and token credentials with registry scopes. Cleanup policies are Free across all three offerings. Container scanning is Free/Premium/Ultimate, but some vulnerability-management presentation/governance capabilities vary by tier. Protected container repositories are Free across all three offerings. Protected container tags are Free on GitLab.com and Self-Managed with the current registry backend prerequisites. Immutable container tags are Ultimate-only on GitLab.com/Self-Managed. The mandatory labs therefore require neither Ultimate nor Self-Managed administration, privileged runners, cloud spend, nor a persistent registry credential.

1. Disposable scenario and two execution lanes

Create or reuse a disposable GitLab Free project such as learner-example/ch23-registry-lab. The live lane publishes one tiny image only if your project has an enabled registry and you have suitable local/runner container tooling. The fallback lane uses the exact same metadata/digest reasoning with an OCI fixture and read-only registry output, so completion never depends on paid compute, privileged Docker-in-Docker, or cloud infrastructure.

Do not experiment in a production registry. Image/tag deletion is destructive. Never create a persistent token for the mandatory path. If you use an optional deploy token, scope it to the disposable project, keep the value out of commands/logs/source, and revoke it during cleanup.

2. Preflight: prove project, Git SHA, registry variables, and existing registry state

Before publishing, record the source state and inspect the registry:

git status --short --branch
git rev-parse HEAD
glab repo view --output json | jq '{path_with_namespace,default_branch,web_url}'
glab container-registry repository list --output json

In CI, check only presence of registry variables; never print the password:

test -n "$CI_REGISTRY"
test -n "$CI_REGISTRY_IMAGE"
test -n "$CI_REGISTRY_USER"
test -n "$CI_REGISTRY_PASSWORD"
printf 'registry=%s image_base=%s\n' "$CI_REGISTRY" "$CI_REGISTRY_IMAGE"

The image base should be the project registry path. Record the project ID and expected nested repository $CI_REGISTRY_IMAGE/ch23-lab.

3. Predict identity before mutation

Write evidence/ch23/predictions.md before any push. Predict:

  • source commit SHA and pipeline source;
  • repository path <CI_REGISTRY_IMAGE>/ch23-lab;
  • immutable tag based on full commit SHA (or a short lab-only tag plus recorded commit);
  • that a later human tag can move while the recorded digest must not;
  • which actor may push/delete and which credential lifetime applies;
  • what cleanup will delete (tag/repository metadata) versus what storage reclamation may happen later.

4. Live lane: publish one tiny image only when safe tooling exists

If an existing CI runner already provides safe container tooling, or if you run this inside a disposable local container environment, you may publish an intentionally tiny image. The following login pattern is specifically for a CI job, where GitLab provides the job-only registry variables and the password stays off the command line. For a local publish outside CI, use a separately created disposable least-privilege registry credential through --password-stdin, then revoke it; do not expect job-only CI_REGISTRY_* credentials to exist locally.

printf '%s' "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password-stdin
IMAGE="$CI_REGISTRY_IMAGE/ch23-lab"
TAG="$CI_COMMIT_SHA"
# Build with your already-approved non-privileged/local builder, then:
docker tag ch23-local:lab "$IMAGE:$TAG"
docker push "$IMAGE:$TAG"
docker logout "$CI_REGISTRY"

Do not enable a privileged runner solely for this chapter. If the current runner lacks a safe builder, use the fixture lane below or publish from your disposable local container environment.

5. No-runner/no-registry fallback: an OCI fixture still proves the identity model

Create a local evidence fixture representing two observations of the same tag at different times:

{
  "repository": "registry.example.invalid/learner/ch23-lab",
  "observations": [
    {"tag":"latest","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","source_sha":"1111111111111111111111111111111111111111"},
    {"tag":"latest","digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","source_sha":"2222222222222222222222222222222222222222"}
  ]
}

Both rows have tag latest; only the digest distinguishes exact content. Use this fixture for the scan/retention exercises when live registry compute is unavailable.

6. Inspect repository and tag metadata after publish

Once the live repository exists, use current glab commands:

glab container-registry repository list --include-tags-count --output json > evidence/ch23/repositories.json
REPO_ID="$(jq -r 'map(select(.path | endswith("/ch23-lab")))[0].id // empty' evidence/ch23/repositories.json)"
test -n "$REPO_ID"
glab container-registry tag list "$REPO_ID" --details --per-page 100 --output json > evidence/ch23/tags.json
jq 'map({name,digest,total_size,created_at})' evidence/ch23/tags.json

Record the digest returned for the commit-derived tag. The repository ID is a GitLab metadata identifier; it is not the OCI digest.

7. Compare tag pull and digest pull

With safe local tooling, first pull by tag, then by the recorded digest. The expected image identity is the digest returned by the registry metadata:

IMAGE="$CI_REGISTRY_IMAGE/ch23-lab"
TAG="<recorded-tag>"
DIGEST="sha256:<recorded-digest>"
docker pull "$IMAGE:$TAG"
docker pull "$IMAGE@$DIGEST"
docker image inspect "$IMAGE@$DIGEST" --format '{{json .RepoDigests}}'

If a later tag inspection points elsewhere, the digest reference still identifies the original manifest. For multi-platform images, distinguish an index digest from a selected platform-manifest digest.

8. Run current container scanning only when runner/image prerequisites are available

The current GitLab-managed container scanning template requires a test stage and a suitable runner. A minimal configuration looks like:

stages: [test]

include:
  - template: Jobs/Container-Scanning.gitlab-ci.yml

container_scanning:
  variables:
    CS_IMAGE: "$CI_REGISTRY_IMAGE/ch23-lab:$CI_COMMIT_SHA"

After the job, preserve gl-container-scanning-report.json and the CycloneDX SBOM if produced. If no compatible runner/compute is available, use a clearly labeled synthetic report fixture and still practice binding report metadata to the expected image reference/digest. Do not claim the fixture is a real vulnerability assessment.

9. Verify that scan evidence belongs to the intended image

Before interpreting severity counts, compare the scan job’s CS_IMAGE, pipeline SHA, registry tag metadata, and recorded digest. The dangerous failure is a perfectly valid scan of the wrong image. Preserve the report checksum as evidence:

sha256sum gl-container-scanning-report.json 2>/dev/null || echo "fixture/report not present in this lane"

A scan result should never be detached from the image identity it analyzed.

10. Simulate cleanup policy before enabling it

For a disposable project, design a rule such as “keep latest, keep release tags, keep the newest five commit tags, remove matching development tags older than 30 days.” Before enabling it, classify a fixture table manually:

Tag Age Keep pattern? Protected/immutable? Predicted action
latest 120d special no Keep — cleanup policy always preserves latest.
v1.4.0 80d ^v optional Keep.
dev-a1b2 45d no no Candidate for removal if regex/age match.
dev-c3d4 2d no no Keep — too recent.

Do not use .* first against a valuable registry. A cleanup policy can require multiple runs on GitLab.com; disappearance from UI and physical storage reclamation are different observations.

11. Challenge: choose the correct control

You need a production deployment to keep rolling back to the exact release even if humans retag stable. Which control is primary?

Answer before revealing: record/deploy by digest. Tag protection/immutability can strengthen governance, but digest pinning is the content identity. Cleanup must preserve referenced digests/tags long enough for rollback policy.

12. Cleanup only after proving no downstream references

List the exact tag and record its digest before deletion. Then, in the disposable project only:

REPO_ID="<verified-repository-id>"
TAG="<verified-lab-tag>"
glab container-registry tag view "$REPO_ID" "$TAG" --output json > evidence/ch23/tag-before-delete.json
# Destructive: delete only the verified disposable tag.
glab container-registry tag delete "$REPO_ID" "$TAG" --yes
if glab container-registry tag view "$REPO_ID" "$TAG" --output json >/dev/null 2>&1; then
  echo "tag still exists" >&2
  exit 1
fi
echo "verified tag removal"

Deleting the tag does not mean administrators should immediately run garbage collection. If the whole repository is disposable and you choose to delete it, re-list it, verify no consumer references, use the repository-delete command with an explicit destructive warning, and verify absence.

Knowledge check

Why is docker login -p "$CI_REGISTRY_PASSWORD" inferior to --password-stdin?

What should you record before deleting a lab tag?

A scan report exists, but the tag moved after the scan. What do you verify first?

Why might storage not drop immediately after tag deletion?

What is the free-compatible fallback if you lack safe container-build compute?

Summary

You inspected before mutation, separated tag names from digest identity, used short-lived CI authentication, bound scan evidence to an image, designed retention before cleanup, and verified destructive tag removal. The next lesson turns these mechanics into production architecture choices.

Official references

Primary sources used for the current GitLab 19.3 behavior taught in this lesson:

Next lesson

Configuration, Design Choices, and Tradeoffs

Choose registry identity, credentials, retention, and platform boundaries without trading rollback integrity for convenience.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.