The module lifecycle stage: Preview
The module has requirements for installation
This guide shows how to authenticate a CI/CD pipeline to the registry and push
images to a project. The examples are platform-agnostic (portable shell with
docker); map the steps to your CI system’s job/secret primitives.
The registry authenticates clients with Kubernetes tokens (validated via
TokenReview) and authorizes them with RBAC on the virtual
payloadrepositoryaccesses resource — see Authentication and authorization.
A pipeline therefore needs a robot ServiceAccount, RBAC granting push,
and a token the pipeline logs in with.
Prerequisites
-
The registry endpoint:
payload-registry.${PUBLIC_DOMAIN}. -
The CI runner must trust the registry’s TLS certificate. If the registry uses a publicly trusted certificate, nothing is needed. If it uses a custom CA, add the CA to the runner so
docker/cranecan verify it, e.g.:# Docker honours per-registry CA files at this path: install -d /etc/docker/certs.d/payload-registry.${PUBLIC_DOMAIN} cp ca.crt /etc/docker/certs.d/payload-registry.${PUBLIC_DOMAIN}/ca.crt
1. Create a robot ServiceAccount and grant it push access
Images live under a project namespace: the first path segment of the image
reference is the namespace (e.g. project-1/app/... lives in namespace
project-1). Create a dedicated ServiceAccount for CI and grant it create
(push) on the paths it may write. Scope resourceNames as tightly as possible.
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-robot
namespace: project-1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: payload-registry-ci-push
namespace: project-1
rules:
- apiGroups: ["payload-registry.deckhouse.io"]
resources: ["payloadrepositoryaccesses"]
# Tighten to the paths CI may write, e.g. app/** for project-1/app/...
resourceNames: ["app/*", "app/**"]
# create = push, get = pull, list = list tags, delete = delete
verbs: ["get", "list", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: payload-registry-ci-push
namespace: project-1
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: payload-registry-ci-push
subjects:
- kind: ServiceAccount
name: ci-robot
namespace: project-1The verb→operation mapping (create = push, get = pull, list = list tags,
delete = delete) is described in Access levels.
2. Obtain a token for the robot
The pipeline logs in to the registry with username token and a Kubernetes
token as the password. There are two reliable ways to get that token; pick one
based on your security policy.
Option A — long-lived ServiceAccount token (recommended for CI)
Create a ServiceAccount-token Secret. Kubernetes fills it with a
non-expiring token bound to the robot SA. This is the simplest reliable
robot credential: it never expires mid-pipeline, needs no extra cluster
permissions to mint, and its blast radius is bounded by the RBAC above.
apiVersion: v1
kind: Secret
metadata:
name: ci-robot-token
namespace: project-1
annotations:
kubernetes.io/service-account.name: ci-robot
type: kubernetes.io/service-account-tokenRead the token and store it as a masked / protected CI variable (e.g.
REGISTRY_TOKEN):
kubectl -n project-1 get secret ci-robot-token -o jsonpath='{.data.token}' | base64 -dBecause the token does not expire on its own, treat it like any other long-lived secret: store it masked/protected, never echo it, rotate periodically, and revoke it when no longer needed (see Revoking and rotating a credential).
Option B — short-lived token per job
Most pipelines do not need this — prefer Option A. Use this only if your security policy forbids long-lived tokens. Mint a fresh token at the start of each job via the TokenRequest API:
kubectl -n project-1 create token ci-robot --duration=2hCaveats:
- The identity running this command needs
createonserviceaccounts/tokenforci-robot(a regular cluster-admin kubeconfig does not have it by default) — so the CI still needs a stored cluster credential (kubeconfig) with that permission. - The lifetime is capped by the apiserver’s
--service-account-max-token-expiration. Request at least the maximum job duration, and re-login if a job can outlive the token.
Revoking and rotating a credential
A token is validated against the ServiceAccount (and, for Option A, against its
token Secret) on every login. There are two ways to revoke:
-
Rotate one credential (Option A): delete the token
Secretand create a new one, then update the CI variable. The old token stops working shortly after (token validation is briefly cached by the apiserver, so revocation takes effect within tens of seconds, not instantly).kubectl -n project-1 delete secret ci-robot-token kubectl -n project-1 apply -f ci-robot-token.yaml # recreate, then re-read the token -
Disable the robot entirely: delete the
ServiceAccount. This invalidates all of its tokens at once (Option A and Option B alike — they no longer match an existing ServiceAccount) and removes it as an RBAC subject.kubectl -n project-1 delete serviceaccount ci-robot
Deleting the RoleBinding instead keeps the token valid but removes its
push/pull authorization (the login still succeeds, but every operation is denied).
3. Pipeline: log in, build, push
A portable job (maps directly to script: in GitLab CI, run: steps in GitHub
Actions, etc.). REGISTRY_TOKEN is the masked CI variable from step 2.
set -euo pipefail
REGISTRY="payload-registry.${PUBLIC_DOMAIN}"
IMAGE="${REGISTRY}/project-1/app/myservice:${CI_COMMIT_SHORT_SHA}"
# 1. Authenticate (username is always the literal "token").
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u token --password-stdin
# 2. Build and tag.
docker build -t "${IMAGE}" .
# 3. Push.
docker push "${IMAGE}"Use --password-stdin (not -p) so the token never appears in the process list
or job log.
Storage quota in pipelines
Pushes are enforced against the project’s storage quota (the
PayloadRegistryQuota resource, when one is set in the project namespace). When
a project is over quota, the push is rejected with HTTP 413 and error code
QUOTA_EXCEEDED; docker push fails. Check and, if needed, raise the quota:
kubectl -n project-1 get prquota defaultTo avoid pipelines slowly filling the project, delete obsolete tags and rely on the registry’s garbage collection to reclaim space (note: the quota counts the physical footprint, which is freed by GC, not merely by deleting a manifest).
Pull from CI
The same token (with at least get access to the path) works for pulling:
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u token --password-stdin
docker pull "${REGISTRY}/project-1/app/myservice:${TAG}"To consume images from inside the cluster (Pods), use imagePullSecrets
instead — see Using images in cluster.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
401 Unauthorized on login |
Wrong username (must be token), expired/invalid token, or the SA/Secret was deleted. |
denied / 403 on push |
The robot SA lacks create on that path. Check the Role resourceNames and the RoleBinding. |
x509: certificate signed by unknown authority |
The runner does not trust the registry CA — see Prerequisites. |
413 QUOTA_EXCEEDED on push |
Project is over its storage quota — see Storage quota. |