<registry>/atoll/<project>/<module>:<deployment id>) and the nodes
pull it by digest when an app pod starts. This page describes the registry used for self-hosting and how it is
secured.
Why zot
The platform needs a private store for its own images, and the platform itself knows which build may push which repository.
zot keeps that store small and lets the platform become the token issuer (phase 2 below), so per-project access needs
no second user database. Harbor would work too (a robot account per project, created through its API), but it
adds a large stateful system and couples the server to Harbor’s API.
Topology
zot runs in theregistry namespace behind Traefik on registry.example.com with a Let’s Encrypt
certificate. It keeps blobs in the SeaweedFS bucket atoll-registry (S3 user registry, limited to that
bucket), so images share SeaweedFS’ replication and the zot pod itself holds no state. Its NetworkPolicy only
admits Traefik. Build pods push and nodes pull through that one name (ATOLL_REGISTRY_PUSH =
ATOLL_REGISTRY_PULL = registry.example.com, ATOLL_REGISTRY_INSECURE=false), so there is no plain-HTTP
path and no node-level mirror to configure. The atoll-builds NetworkPolicy allows build pods to reach the
Traefik pods for that (networkPolicy.builds.allowIngressController).
Phase 1: static users (this install)
scripts/create-secrets.sh creates three zot users (zot-htpasswd, passwords in registry/zot-users):
Anonymous access is off and nobody can delete except
admin.
How the server uses them
The chart exports the Secret names asATOLL_REGISTRY_PUSH_SECRET, ATOLL_REGISTRY_PULL_SECRET and
ATOLL_BUILDER_PULL_SECRET (all optional; ATOLL_REGISTRY_INSECURE defaults to false).
- Build pods run the user’s Gradle build in the init container
build, which has no registry credentials, noDOCKER_CONFIGand no service account token. It leaves one image layer per Ktor module in an emptyDir. The containerpublishthen pushes them: only it mounts the push Secret (key.dockerconfigjsonasconfig.jsonat/var/run/atoll/docker,DOCKER_CONFIGpointing there). It runs as its own uid with a read-only root filesystem, without the project’s build env vars, refuses links and special files in the build’s outputs, checks every name it reads from them and only hands the layer files to crane as data. The build’s processes are gone before it starts. The builder pull Secret goes into the pod’simagePullSecrets. A build whose push Secret is missing, not of typekubernetes.io/dockerconfigjsonor without the.dockerconfigjsonkey fails before its Job is created. The credentials never appear in the Job spec or the build log. - App pods get the pull Secret as
imagePullSecrets. Itsauthskey must be the host inATOLL_REGISTRY_PULL(the bundled registry writes both the push and the pull host). - Digests:
crane appendprints the digest of the image it pushed,crane mutatecontinues from that digest (not the tag) and prints the final digest, whichpublishreports asrepo:<deployment id>@sha256:.... The server accepts only digest-pinned images underatoll/<project>/with the build’s deployment id as tag, and the app Deployment references that digest. Promotions roll out the stored reference again, so they run exactly the image that was built. Deployments from before digest pinning must be promoted with--rebuild. - Version check: the server refuses a builder image older than itself (“The builder image is older than
this server; set ATOLL_BUILDER_IMAGE …”). Upgrade the server and
builds.imagetogether.
ImagePullBackOff once anonymous pulls are off.
Redeploy each Ktor app once, or give the nodes credentials (below) until you have.
Remaining risk: every build shares atoll-push. The user’s build can no longer read it, but anything that
runs in the publish container (the builder image itself) could push to another project’s repository.
Deploying by digest makes such a push harmless for running apps and promotions: an overwritten tag is never
pulled. Phase 2 removes the shared credential.
Nodes can also pull with node-level credentials instead of the pull Secret:
deploy/selfhost/k3s/registries.yaml.example.
Phase 2: per-build tokens issued by the platform
zot supports Docker registry token authentication: on a 401 the client fetches a short-lived JWT from a “realm” URL and zot checks its signature and theaccess claims for the requested repository.
The per-build token is already random and only valid while that build runs, so a malicious build can only push
to its own project. On the deploy side
create-secrets.sh and the zot values change; the chart already has
everything else.
Bundled registry (evaluation)
registry.bundled.enabled runs a single zot pod inside the chart, over plain HTTP. It always requires a login:
without registry.bundled.htpasswdSecret the chart generates atoll-push and atoll-pull once (Secret
atoll-registry-auth, kept on uninstall) and writes atoll-registry-push (builds namespace) and
atoll-registry-pull (builds and apps namespaces), which it also exports as ATOLL_REGISTRY_PUSH_SECRET and
_PULL_SECRET. Both users only reach atoll/**. The Service is ClusterIP; only build pods may connect. For node pulls on kind/k3d see
install.md.