Chapter 23Lesson 02~200 minutes

Container Registry, Package Registry, Generic Packages, Dependency Proxy, and Artifact Promotion: Guided Hands-On Workflow and Core Operations

Publish and consume a disposable Generic Package by exact version and checksum, optionally push a tiny container image, inspect registry identity, and prove immutable consumption without exposing credentials.

Hands-onGeneric PackagesContainer RegistryDependency ProxyChecksums

Learning objectives

  • Publish one synthetic Generic Package from an exact producer SHA using CI_JOB_TOKEN.
  • Download the exact package version, verify SHA-256, and record producer/consumer evidence.
  • Optionally build a network-free scratch container, push it with CI credentials, and capture a registry digest.
  • Explain and inspect Dependency Proxy variables without treating the proxy as provenance.
  • Clean up only the exact Chapter 23 lab package/tag after explicit identity checks.

1. Disposable scenario and mutation boundary

Use a throwaway GitLab project named glci-ch23-registry-lab. The mandatory path publishes a tiny Generic Package with a per-pipeline version; it works without Docker privileges. The optional container path runs only on an authorized runner with a safe container engine. No production registry, package namespace, or external credential is required.

Cleanup guard: every package version must begin with 0.0.0-ch23-lab., and every optional container tag must begin with ch23-lab-. Refuse deletion when either guard fails.

2. Preflight: record exact execution context

git rev-parse HEAD
printf 'source=%s ref=%s sha=%s pipeline=%s job=%s\n' \
  "$CI_PIPELINE_SOURCE" "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA" \
  "$CI_PIPELINE_ID" "$CI_JOB_ID"
printf 'project=%s api=%s registry=%s\n' \
  "$CI_PROJECT_PATH" "$CI_API_V4_URL" "$CI_REGISTRY"

docker --version 2>/dev/null || true
podman --version 2>/dev/null || true

Record runner/executor/image metadata from the job page. Do not dump the full process environment or enable shell tracing around authentication commands.

3. Create the deterministic payload and immutable version

set -eu
mkdir -p dist evidence downloaded
printf 'chapter=23\nsha=%s\npipeline=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" > dist/payload.txt

PACKAGE_NAME="glci-ch23-demo"
PACKAGE_VERSION="0.0.0-ch23-lab.${CI_PIPELINE_ID}"
FILE_NAME="payload.txt"
sha256sum dist/payload.txt | tee evidence/producer.sha256
printf 'name=%s\nversion=%s\nsource_sha=%s\n' \
  "$PACKAGE_NAME" "$PACKAGE_VERSION" "$CI_COMMIT_SHA" > evidence/package.env

4. Publish with the ephemeral job identity

The publication request changes GitLab package-registry state. It does not change the repository or runner host beyond the local file already created. The expected successful response is HTTP 201 Created.

UPLOAD_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${PACKAGE_VERSION}/${FILE_NAME}"

status="$(curl --silent --show-error --output evidence/upload-response.txt \
  --write-out '%{http_code}' --location \
  --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
  --upload-file "dist/${FILE_NAME}" "$UPLOAD_URL")"
printf 'upload_http=%s\n' "$status" | tee evidence/upload.status
test "$status" = "201"

5. Pull by exact version and verify the bytes

DOWNLOAD_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${PACKAGE_VERSION}/${FILE_NAME}"

curl --fail --silent --show-error --location \
  --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
  --output "downloaded/${FILE_NAME}" "$DOWNLOAD_URL"

EXPECTED="$(cut -d' ' -f1 evidence/producer.sha256)"
ACTUAL="$(sha256sum downloaded/${FILE_NAME} | cut -d' ' -f1)"
printf 'expected=%s\nactual=%s\n' "$EXPECTED" "$ACTUAL" | tee evidence/consumer.sha256
test "$EXPECTED" = "$ACTUAL"

This is the core promotion invariant: the consumer does not rebuild. It receives the exact published version and independently proves the checksum.

6. Optional: build a tiny container without an upstream base image

If an authorized runner has Docker/Podman and the project Container Registry is enabled, use a scratch image so the build requires no external base-image pull. This path is optional because container-engine privilege varies by runner.

FROM scratch
COPY dist/payload.txt /payload.txt
IMAGE_TAG="ch23-lab-${CI_PIPELINE_ID}"
IMAGE_REF="${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"

echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" \
  --username "$CI_REGISTRY_USER" --password-stdin

docker build --pull=false -t "$IMAGE_REF" .
docker push "$IMAGE_REF"
docker pull "$IMAGE_REF"
REPO_DIGEST="$(docker image inspect "$IMAGE_REF" --format '{{index .RepoDigests 0}}')"
printf 'repo_digest=%s\n' "$REPO_DIGEST" | tee evidence/container.env

After push/pull, RepoDigests exposes the repository digest identity. Record it and make downstream examples consume repository@sha256:…, not the mutable tag alone.

7. Inspect Dependency Proxy safely

printf 'dependency_proxy_server=%s\n' "$CI_DEPENDENCY_PROXY_SERVER"
printf 'group_prefix=%s\n' "$CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX"
printf 'direct_group_prefix=%s\n' "$CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX"

Runners can authenticate to the Dependency Proxy in CI using predefined variables. A conceptual pull such as ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine:3.20 demonstrates cache routing, but a production reproducibility contract should ultimately identify a verified upstream digest, not only a moving tag.

8. Deliberate authorization failure

To prove that authentication and authorization are separate, point one read request at a private disposable target project that has not allowlisted this source project, or faithfully simulate the target decision locally. Preserve the HTTP status, target project ID, source pipeline ID, and non-secret actor identity. Do not “fix” the denial with a broad PAT.

TARGET_PROJECT_ID="${UNAUTHORIZED_LAB_PROJECT_ID:-999999999}"
set +e
code="$(curl --silent --output evidence/denied.txt --write-out '%{http_code}' \
  --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
  "${CI_API_V4_URL}/projects/${TARGET_PROJECT_ID}/packages/generic/glci-ch23-demo/0.0.0/missing.txt")"
set -e
printf 'unauthorized_http=%s target_project=%s\n' "$code" "$TARGET_PROJECT_ID" \
  | tee evidence/denial.status

The exact status can depend on project visibility and endpoint behavior. The learning objective is to preserve the denial and diagnose target allowlist plus triggering-user permissions before changing anything.

9. Promotion means same bytes, new eligibility

For this lab, “promote to staging” means writing a small manifest that points to the exact package coordinate and checksum. A real deployment later consumes that coordinate. For a container image, promotion can add a human-readable environment tag to the same manifest, but the deployment record should retain the digest.

cat > evidence/promotion.env <<EOF
package=${PACKAGE_NAME}
version=${PACKAGE_VERSION}
file=${FILE_NAME}
sha256=$(cut -d' ' -f1 evidence/producer.sha256)
producer_sha=${CI_COMMIT_SHA}
EOF
cat evidence/promotion.env

10. Cleanup without collateral damage

Package deletion is destructive and can create dependency-confusion risk if request forwarding is enabled. The safe default for evidence is to retain the package or delete only the exact lab package after the exercise. Never delete “oldest” or “latest” during a beginner lab.

case "$PACKAGE_VERSION" in
  0.0.0-ch23-lab.*) printf 'cleanup target accepted: %s/%s\n' "$PACKAGE_NAME" "$PACKAGE_VERSION" ;;
  *) echo 'Refusing cleanup: unexpected package version' >&2; exit 2 ;;
esac

# Then use the GitLab UI/API to identify the exact package ID before deletion.
# Record the ID and response. Do not use a wildcard deletion.

11. Challenge: choose the layer, not the shortcut

A downstream job can authenticate to its own project but receives a denial when downloading from a private producer project. The package exists and its checksum is correct. Which layer should you inspect first?

Answer: cross-project job-token authorization: target allowlist and triggering-user permissions. Rebuilding the package, changing the package version, or adding a broad PAT attacks the wrong layer.

Knowledge check

Why is the Generic Package lab the mandatory path?

What evidence proves promotion did not rebuild the artifact?

Why use --password-stdin for registry login?

What should you preserve from an intentional cross-project denial?

What is the safe cleanup selector?

Next lesson

Configuration, design choices, and tradeoffs

Compare ephemeral job identity with deploy tokens, tags with digests, rebuild-per-environment with same-bytes promotion, and GitLab registries with external repository managers.

Version and compatibility note

GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.

Official references and version notes

Documentation verification date: 2026-09-12. Generic Packages, Package Registry, Container Registry, Dependency Proxy, protected packages, and protected container tags are documented for Free/Premium/Ultimate unless noted otherwise. Current immutable container-tag rules are Ultimate-only. Dependency Proxy is group-level and supports Docker Hub images. The mandatory exercise uses same-project Generic Packages and therefore avoids cross-project allowlist setup. The cross-project denial is optional/simulated and should never be solved by broadening credentials blindly.

Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.