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.
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.
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?
It exercises versioned publication, CI_JOB_TOKEN authentication, immutable coordinate consumption, checksums, and cleanup without requiring a privileged container engine.
What evidence proves promotion did not rebuild the artifact?
The producer checksum and consumer checksum match, and the promoted manifest/reference points to the same exact package version or container digest.
Why use --password-stdin for registry
login?
It keeps the credential out of the command-line argument list and avoids printing it in normal logs.
What should you preserve from an intentional cross-project denial?
Source/target project identity, pipeline/job IDs, endpoint, HTTP status, and the non-secret authorization assumptions—not the token value.
What is the safe cleanup selector?
The exact disposable package ID/version or exact guarded container tag created by this lab, never latest/oldest/wildcard state.
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.
- Package registry — official reference.
- Generic packages — official reference.
- Supported package functionality — official reference.
- Protected packages — official reference.
- Reduce package registry storage — official reference.
- Container registry — official reference.
- Container registry authentication — official reference.
- Protected container tags — official reference.
- Immutable container tags — official reference.
- Reduce container registry storage — official reference.
- Dependency Proxy — official reference.
- CI job token — official reference.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.