# 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.tar
```

*Dockerfile*

```
FROM slowshield.example.com/docker.io/library/nginx:1.29
```

### Docker 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"]
EOF
```

From 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:latest` stays 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 buildx` builders 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 of `server`** in containerd's `hosts.toml` falls back to the registry after a refusal. Keep SlowShield as the `server`.

- **Apple's `container` CLI** 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](https://slowshield.org/docs/container-builds/).

---

This page as HTML: https://slowshield.org/docs/containers/. All of the guide in one file: https://slowshield.org/llms-full.txt
