Container images
SlowShield serves container images at https://slowshield.example.com/v2/, from Docker Hub,
GHCR, Quay, registry.k8s.io, gcr.io, MCR and ECR Public. Tags lag behind: FROM nginx:latest keeps
working, about a week behind.
Set it up
Each runtime has its own setting. These were checked against containerd 2.2, Docker 29.8, Podman 5.8, BuildKit 0.33, skopeo and crane: with them, containerd, Docker's default image store and Podman only ever ask SlowShield.
containerd
/etc/containerd/certs.d/_default/hosts.toml: every registry, pull only
server = "https://slowshield.example.com"
capabilities = ["pull", "resolve"]/etc/containerd/certs.d/ghcr.io/hosts.toml: a registry you also push to
server = "https://ghcr.io"
capabilities = ["push"]
[host."https://slowshield.example.com"]
capabilities = ["pull", "resolve"]Docker
/etc/docker/certs.d/_default/hosts.toml: the containerd image store (docker info lists io.containerd.snapshotter.v1), for docker pull and docker build
server = "https://slowshield.example.com"
capabilities = ["pull", "resolve"]/etc/docker/daemon.json: the classic image store. Docker Hub only, and Docker pulls from Docker Hub itself after a refusal
{
"registry-mirrors": ["https://slowshield.example.com"]
}Podman
/etc/containers/registries.conf.d/50-slowshield.conf: Podman, CRI-O, Buildah and skopeo
[[registry]]
prefix = "docker.io"
location = "slowshield.example.com/docker.io"
[[registry]]
prefix = "gcr.io"
location = "slowshield.example.com/gcr.io"
[[registry]]
prefix = "ghcr.io"
location = "slowshield.example.com/ghcr.io"
[[registry]]
prefix = "mcr.microsoft.com"
location = "slowshield.example.com/mcr.microsoft.com"
[[registry]]
prefix = "public.ecr.aws"
location = "slowshield.example.com/public.ecr.aws"
[[registry]]
prefix = "quay.io"
location = "slowshield.example.com/quay.io"
[[registry]]
prefix = "registry.k8s.io"
location = "slowshield.example.com/registry.k8s.io"BuildKit
buildkitd.toml (also docker buildx create --buildkitd-config): BuildKit pulls from the registry itself after a refusal
[registry."docker.io"]
mirrors = ["slowshield.example.com"]
[registry."gcr.io"]
mirrors = ["slowshield.example.com"]
[registry."ghcr.io"]
mirrors = ["slowshield.example.com"]
[registry."mcr.microsoft.com"]
mirrors = ["slowshield.example.com"]
[registry."public.ecr.aws"]
mirrors = ["slowshield.example.com"]
[registry."quay.io"]
mirrors = ["slowshield.example.com"]
[registry."registry.k8s.io"]
mirrors = ["slowshield.example.com"]Image names
any client: SlowShield's host in front of the image name
crane pull slowshield.example.com/docker.io/library/nginx:1.29 nginx.tarDockerfile
FROM slowshield.example.com/docker.io/library/nginx:1.29Docker Desktop (macOS, Windows)
Docker Desktop's engine runs in a VM. When Docker Desktop starts, it copies ~/.docker/certs.d on your
machine to /etc/docker/certs.d in the VM, so put the file there and restart Docker Desktop:
mkdir -p ~/.docker/certs.d/_default
cat > ~/.docker/certs.d/_default/hosts.toml <<'EOF'
server = "https://slowshield.example.com"
capabilities = ["pull", "resolve"]
EOFFrom then on every docker pull and every base image of docker build comes through
SlowShield. If SlowShield is unreachable, pulls fail instead of going to the registry. Pushes need the registry's
own file (see containerd above).
Podman on macOS and Windows
Podman runs in a VM too. Put the drop-in there:
podman machine ssh 'mkdir -p ~/.config/containers/registries.conf.d && cat > ~/.config/containers/registries.conf.d/50-slowshield.conf' < 50-slowshield.conf(/etc/containers/registries.conf.d/ for a rootful machine.) Short names like python:3.13-slim
resolve to docker.io/library/python first, and the drop-in then sends them to SlowShield.
Kubernetes
The kubelet pulls through containerd, which reads the hosts.toml files only when its CRI plugin's
config_path names the directory. The default is empty:
# /etc/containerd/config.toml (containerd 2.x)
[plugins.'io.containerd.cri.v1.images'.registry]
config_path = "/etc/containerd/certs.d"In containerd 1.7 the section is [plugins."io.containerd.grpc.v1.cri".registry]. Check a node with
containerd config dump | grep config_path. A pod whose image is held stays in
ImagePullBackOff and starts by itself once the hold ends.
What changes
- Tags lag behind. A tag resolves to the newest digest it has pointed to for 7 days, so
nginx:lateststays about a week behind. The time a tag got its digest comes from the registry where it keeps one (Docker Hub's API, Quay, Artifact Registry, MCR), and from SlowShield's own first sight, whichever is earlier. GHCR and ECR Public only have the second. - A pinned digest that is too new is refused, not swapped for an older one.
- New instances. During an instance's first 7 days, a tag it has no history for yet is served at its current digest and recorded as fail-open, so a new instance doesn't refuse half of Docker Hub. Your administrator can make it strict from the start.
- Takedowns propagate. When a registry removes an image, as Docker Hub did with the compromised Trivy images in March 2026, SlowShield stops serving it too: it checks a stored image again at most every 5 minutes while it is being pulled.
- Blocks. No malware feed covers images; administrators block a repository, tag, digest or layer
in
config.toml. - Everything is verified. Manifests must match their digests; layers are checked while they stream.
Every refusal is 403 with a message. What the clients print:
| Client | Output |
|---|---|
| Docker (containerd image store) | Error response from daemon: error from registry: slowshield: docker.io/library/brandnew:latest (sha256:162a60de2ed8…) is too new. It was pushed …; this proxy requires 7 days. It becomes available at … |
docker build, BuildKit | 403 Forbidden, then denied: slowshield: … is too new … |
| Podman, skopeo | reading manifest latest in slowshield.example.com/docker.io/library/brandnew: denied: slowshield: … |
| crane | DENIED: slowshield: … |
| containerd 2.2 and older (ctr, nerdctl, Kubernetes events) | … 403 Forbidden, without the message |
The second layer
No container runtime has a release-age setting, so SlowShield is the only layer.
What can go around it
- BuildKit (also
docker buildxbuilders with the docker-container or kubernetes driver) and Docker's classic image store pull from the registry themselves after SlowShield refuses an image. Only a firewall that blocks the registries makes them binding. - A
[host]entry instead ofserverin containerd'shosts.tomlfalls back to the registry after a refusal. Keep SlowShield as theserver. - Apple's
containerCLI has no mirror setting: name images with SlowShield's host (slowshield.example.com/docker.io/library/nginx). Read from its source, not tested yet. - Block registry-1.docker.io, ghcr.io, quay.io, registry.k8s.io and the others, and their CDNs, at the firewall for machines that should only use SlowShield.
To build images with SlowShield for the base images and every dependency, see Building images.
For agents: this page as Markdown · llms.txt · the SlowShield skill