Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dind-host-passthrough-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: minor
---

dind-box: add `DIND_HOST_PASSTHROUGH_IMAGES`, a per-repository (image-name) allowlist for host-image passthrough (issue #97). When non-empty, only host images whose reference matches at least one space-separated pattern (glob) are passed through, composed with the existing mode gate (so `public` still requires a public RepoDigest). Empty/unset preserves the current mode + registry behavior. Patterns match against several normalized forms of each reference, so `konard/hive-mind` matches `konard/hive-mind:latest` and `docker.io/konard/hive-mind:latest` alike. This is one level finer than `DIND_HOST_PASSTHROUGH_REGISTRIES` — it scopes passthrough to specific repositories / image names so a deployment can seed the inner daemon with only the images it owns rather than every public image on the host. Covered by new cases in `experiments/preload-unit-test.sh` and `tests/dind/example-preload-images.sh`, documented in `docs/dind/USAGE.md` and `README.md`.
47 changes: 47 additions & 0 deletions .github/actions/setup-buildx-resilient/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: 'Set up Docker Buildx (resilient)'
description: >
Wrapper around docker/setup-buildx-action that first pre-pulls the pinned
BuildKit image with retries and exponential backoff. Booting the
docker-container driver otherwise pulls moby/buildkit straight from Docker
Hub, and a transient registry timeout there fails the whole job (and, for
the amd64 essentials build, cascades into "box-essentials:<ver>-amd64: not
found" across every dependent dind build). Seeding the image locally first
means the boot reuses the cached image instead of hitting the registry.
See the CI investigation in issue #97.
inputs:
buildkit-image:
description: 'Pinned BuildKit image used by the docker-container driver.'
required: false
default: 'moby/buildkit:buildx-stable-1'
runs:
using: 'composite'
steps:
- name: Pre-pull BuildKit image (retry on transient registry errors)
shell: bash
env:
BUILDKIT_IMAGE: ${{ inputs.buildkit-image }}
run: |
set -u
attempts=5
delay=5
for attempt in $(seq 1 "$attempts"); do
echo "==> Pulling ${BUILDKIT_IMAGE} (attempt ${attempt}/${attempts})..."
if docker pull "${BUILDKIT_IMAGE}"; then
echo "==> BuildKit image cached locally; boot will not need a registry pull"
exit 0
fi
if [ "$attempt" -lt "$attempts" ]; then
echo "==> Pull failed, waiting ${delay}s before next attempt..."
sleep "$delay"
delay=$((delay * 2))
fi
done
# Non-fatal: fall through to setup-buildx, which will attempt its own
# pull during boot. This preserves the previous behaviour in the worst
# case while making the common transient-failure case recover.
echo "==> WARNING: could not pre-pull ${BUILDKIT_IMAGE} after ${attempts} attempts; letting setup-buildx try its own boot pull"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: image=${{ inputs.buildkit-image }}
24 changes: 12 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ jobs:
swap-storage: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Build full chain (JS -> essentials -> languages -> full)
run: |
Expand Down Expand Up @@ -788,7 +788,7 @@ jobs:
swap-storage: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Build base box for dind variant
run: |
Expand Down Expand Up @@ -986,7 +986,7 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -1115,7 +1115,7 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -1334,7 +1334,7 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -1479,7 +1479,7 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -1732,7 +1732,7 @@ jobs:

- name: Set up Docker Buildx
if: steps.check-lang.outputs.should_build == 'true'
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
if: steps.check-lang.outputs.should_build == 'true'
Expand Down Expand Up @@ -1933,7 +1933,7 @@ jobs:

- name: Set up Docker Buildx
if: steps.check-lang.outputs.should_build == 'true'
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
if: steps.check-lang.outputs.should_build == 'true'
Expand Down Expand Up @@ -2202,7 +2202,7 @@ jobs:
echo "Building version: $VERSION"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -2470,7 +2470,7 @@ jobs:
echo "Building version: $VERSION"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -2778,7 +2778,7 @@ jobs:
echo "dind_suffix=${DIND_SUFFIX}" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down Expand Up @@ -2919,7 +2919,7 @@ jobs:
echo "dind_suffix=${DIND_SUFFIX}" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: ./.github/actions/setup-buildx-resilient

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ Each row below has the same toolchain as its non-dind sibling **plus** a working
> - **Recommended secure invocation:** [`docker run --runtime=sysbox-runc konard/box-dind`](https://github.com/nestybox/sysbox) — Sysbox is a drop-in OCI runtime that runs system containers without `--privileged` and without exposing host devices.
> - **Do NOT bind-mount `/var/run/docker.sock`.** That gives the container root on the host ([Quarkslab](https://blog.quarkslab.com/why-is-exposing-the-docker-socket-a-really-bad-idea.html), [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html)) and breaks the per-box `docker ps` scoping property.
> - **Storage:** the inner daemon writes to `/var/lib/docker` inside the container by default. For persistence, mount a volume: `-v box-dind-data:/var/lib/docker`.
> - **Reusing host images:** the nested daemon starts with an empty image store, so a fresh container re-downloads images the host already has. Seed it explicitly at startup with `DIND_PRELOAD_TARBALL` (mount `docker save` tarballs) or `DIND_PRELOAD_IMAGES` (pull from a registry/mirror); see [Reusing Host Images](docs/dind/USAGE.md#reusing-host-images-preload). For automatic seeding, mount the host socket at `-v /var/run/docker.sock:/var/run/host-docker.sock:ro` — host-image passthrough is on by default and copies the host's **public** images (those re-pullable from a public registry, so no local secrets or private credentials leak) into the inner daemon; `DIND_HOST_PASSTHROUGH=all` passes everything and `=off` disables it. The host socket is mounted at a non-default path and read only at startup to seed images, so the inner daemon keeps its own isolated socket. See [Host-Image Passthrough](docs/dind/USAGE.md#host-image-passthrough-dind_host_passthrough).
> - **Reusing host images:** the nested daemon starts with an empty image store, so a fresh container re-downloads images the host already has. Seed it explicitly at startup with `DIND_PRELOAD_TARBALL` (mount `docker save` tarballs) or `DIND_PRELOAD_IMAGES` (pull from a registry/mirror); see [Reusing Host Images](docs/dind/USAGE.md#reusing-host-images-preload). For automatic seeding, mount the host socket at `-v /var/run/docker.sock:/var/run/host-docker.sock:ro` — host-image passthrough is on by default and copies the host's **public** images (those re-pullable from a public registry, so no local secrets or private credentials leak) into the inner daemon; `DIND_HOST_PASSTHROUGH=all` passes everything and `=off` disables it. To copy only specific images rather than every public one, set `DIND_HOST_PASSTHROUGH_IMAGES` to a space-separated allowlist of names/globs (e.g. `"konard/hive-mind konard/hive-mind-dind"`), composed with the mode filter. The host socket is mounted at a non-default path and read only at startup to seed images, so the inner daemon keeps its own isolated socket. See [Host-Image Passthrough](docs/dind/USAGE.md#host-image-passthrough-dind_host_passthrough).
> - **Usage examples:** see [`docs/dind/USAGE.md`](docs/dind/USAGE.md). Its examples are backed by executable tests under `tests/dind/`.

See [docs/case-studies/issue-80/CASE-STUDY.md](docs/case-studies/issue-80/CASE-STUDY.md) for the full design and threat model.
Expand Down
37 changes: 37 additions & 0 deletions docs/dind/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ The entrypoint supports these environment variables:
| `DIND_HOST_PASSTHROUGH` | `public` | Copy images already present on the host into the nested daemon at startup when a host socket is mounted (see below). `public` only passes images with a RepoDigest from an allowlisted public registry; `all` passes every tagged image; `off` disables it. A quiet no-op when no host socket is mounted. |
| `DIND_HOST_DOCKER_SOCK` | `/var/run/host-docker.sock` | Path inside the container to the mounted *host* Docker socket used for passthrough. Deliberately **not** `/var/run/docker.sock`, so the inner daemon keeps its own isolated socket. |
| `DIND_HOST_PASSTHROUGH_REGISTRIES` | common public registries | Space-separated allowlist of registries treated as "public" in `DIND_HOST_PASSTHROUGH=public` mode (default: `docker.io ghcr.io quay.io gcr.io registry.k8s.io public.ecr.aws mcr.microsoft.com`). |
| `DIND_HOST_PASSTHROUGH_IMAGES` | _(empty)_ | Space-separated allowlist of image references / globs. When non-empty, only host images matching at least one entry are passed through, composed with the mode filter (so `public` still requires a public RepoDigest). Empty keeps the mode + registry filter only. One level finer than `DIND_HOST_PASSTHROUGH_REGISTRIES` — scope to specific repositories / image names. |

Use a named volume when the inner Docker state should survive container removal:

Expand Down Expand Up @@ -226,6 +227,42 @@ daemon (from a volume, tarball, or earlier run) is skipped, and any single
image that fails to copy logs a warning and continues. Like preload, it is
skipped entirely when `DIND_SKIP_DAEMON=1`.

### Scoping to specific images (`DIND_HOST_PASSTHROUGH_IMAGES`)

The mode gate decides *whether* an image is safe to pass; the registry allowlist
narrows it *by registry host*. `DIND_HOST_PASSTHROUGH_IMAGES` is one level finer
— it scopes passthrough to **specific repositories / image names**. When it is
non-empty, a host image must match the mode filter **and** at least one
space-separated pattern; empty (the default) keeps the mode + registry behavior.

This is the precise fit for "seed the inner daemon with only the images I own"
rather than every public host image:

```bash
# Pass through only hive-mind's own images, nothing else on the host:
docker run -d --privileged \
-v /var/run/docker.sock:/var/run/host-docker.sock:ro \
-e DIND_HOST_PASSTHROUGH=public \
-e DIND_HOST_PASSTHROUGH_IMAGES="konard/hive-mind konard/hive-mind-dind" \
--name box-dind \
konard/box-dind sleep infinity

# Globs and explicit tags / registry-qualified refs also work:
docker run -d --privileged \
-v /var/run/docker.sock:/var/run/host-docker.sock:ro \
-e DIND_HOST_PASSTHROUGH_IMAGES="docker.io/konard/hive-mind* konard/hive-mind-dind:latest" \
--name box-dind \
konard/box-dind sleep infinity
```

Patterns are matched against several normalized forms of each host image
reference, so a bare repository like `konard/hive-mind` matches the tagged
`konard/hive-mind:latest` and the registry-qualified
`docker.io/konard/hive-mind:latest` alike. Because it composes with the mode
gate, `public` mode still refuses a locally-built or private image even when it
matches a pattern — the allowlist only ever *narrows* the eligible set, it never
widens it past the security filter.

## Commit Cycles

`DIND_SKIP_DAEMON=1` is useful for setup containers where you want to install or
Expand Down
85 changes: 85 additions & 0 deletions experiments/preload-unit-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,91 @@ DIND_HOST_PASSTHROUGH=off DIND_HOST_DOCKER_SOCK="$HOST_SOCK" \
check "off mode made no docker calls" bash -c '! test -s "$DOCKER_CALLS"'
rm -f "$HOST_SOCK"

echo "== Case 14: DIND_HOST_PASSTHROUGH_IMAGES allowlist scopes passthrough to named repos =="
reset_state
# Three Docker Hub images, all with a public RepoDigest so the mode gate passes;
# the allowlist must narrow to only the two hive-mind repos.
printf '%s\n%s\n%s\n' "konard/hive-mind:latest" "konard/hive-mind-dind:latest" "alpine:3.20" > "$HOST_IMAGES"
{
echo "konard/hive-mind:latest|konard/hive-mind@sha256:aaa "
echo "konard/hive-mind-dind:latest|konard/hive-mind-dind@sha256:bbb "
echo "alpine:3.20|alpine@sha256:ccc "
} > "$HOST_DIGESTS"
make_sock "$HOST_SOCK"
DIND_HOST_PASSTHROUGH=public DIND_HOST_DOCKER_SOCK="$HOST_SOCK" \
DIND_HOST_PASSTHROUGH_IMAGES="konard/hive-mind konard/hive-mind-dind" \
DIND_PRELOAD_TARBALL="" DIND_PRELOAD_IMAGES="" DOCKER_INFO_OK=1 HOST_DOCKER_OK=1 \
preload_into_daemon
check "allowlisted hive-mind image saved" grep -qx "konard/hive-mind:latest" "$DOCKER_SAVED"
check "allowlisted hive-mind-dind image saved" grep -qx "konard/hive-mind-dind:latest" "$DOCKER_SAVED"
check "non-allowlisted alpine NOT saved" bash -c '! grep -qx "alpine:3.20" "$DOCKER_SAVED"'
rm -f "$HOST_SOCK"

echo "== Case 15: allowlist composes with mode (public still drops a local image even if allowlisted) =="
reset_state
printf '%s\n%s\n' "konard/hive-mind:latest" "konard/hive-mind-dev:latest" > "$HOST_IMAGES"
# hive-mind has a public digest; hive-mind-dev is locally built (no digest line).
echo "konard/hive-mind:latest|konard/hive-mind@sha256:aaa " > "$HOST_DIGESTS"
make_sock "$HOST_SOCK"
DIND_HOST_PASSTHROUGH=public DIND_HOST_DOCKER_SOCK="$HOST_SOCK" \
DIND_HOST_PASSTHROUGH_IMAGES="konard/hive-mind*" \
DIND_PRELOAD_TARBALL="" DIND_PRELOAD_IMAGES="" DOCKER_INFO_OK=1 HOST_DOCKER_OK=1 \
preload_into_daemon
check "allowlisted public image saved" grep -qx "konard/hive-mind:latest" "$DOCKER_SAVED"
check "allowlisted local image dropped by mode" bash -c '! grep -qx "konard/hive-mind-dev:latest" "$DOCKER_SAVED"'
rm -f "$HOST_SOCK"

echo "== Case 16: globs and docker.io-qualified / tagged patterns all match =="
reset_state
printf '%s\n%s\n' "konard/hive-mind:latest" "ghcr.io/owner/tool:v1" > "$HOST_IMAGES"
{
echo "konard/hive-mind:latest|konard/hive-mind@sha256:aaa "
echo "ghcr.io/owner/tool:v1|ghcr.io/owner/tool@sha256:ddd "
} > "$HOST_DIGESTS"
make_sock "$HOST_SOCK"
# A docker.io-qualified glob for the hub image and an exact tagged ghcr ref.
DIND_HOST_PASSTHROUGH=all DIND_HOST_DOCKER_SOCK="$HOST_SOCK" \
DIND_HOST_PASSTHROUGH_IMAGES="docker.io/konard/hive-mind* ghcr.io/owner/tool:v1" \
DIND_PRELOAD_TARBALL="" DIND_PRELOAD_IMAGES="" DOCKER_INFO_OK=1 HOST_DOCKER_OK=1 \
preload_into_daemon
check "docker.io-qualified glob matched the hub image" grep -qx "konard/hive-mind:latest" "$DOCKER_SAVED"
check "exact tagged ghcr ref matched" grep -qx "ghcr.io/owner/tool:v1" "$DOCKER_SAVED"
rm -f "$HOST_SOCK"

echo "== Case 17: empty allowlist preserves prior behavior (all eligible images pass) =="
reset_state
printf '%s\n%s\n' "konard/hive-mind:latest" "alpine:3.20" > "$HOST_IMAGES"
{
echo "konard/hive-mind:latest|konard/hive-mind@sha256:aaa "
echo "alpine:3.20|alpine@sha256:ccc "
} > "$HOST_DIGESTS"
make_sock "$HOST_SOCK"
DIND_HOST_PASSTHROUGH=public DIND_HOST_DOCKER_SOCK="$HOST_SOCK" \
DIND_HOST_PASSTHROUGH_IMAGES="" \
DIND_PRELOAD_TARBALL="" DIND_PRELOAD_IMAGES="" DOCKER_INFO_OK=1 HOST_DOCKER_OK=1 \
preload_into_daemon
check "empty allowlist still saves hive-mind" grep -qx "konard/hive-mind:latest" "$DOCKER_SAVED"
check "empty allowlist still saves alpine" grep -qx "alpine:3.20" "$DOCKER_SAVED"
rm -f "$HOST_SOCK"

echo "== Case 18: image-matching helper normalization (direct calls) =="
reset_state
# Like Case 13, drive the sourced helper in the current shell via `eval` so the
# function stays in scope (a `bash -c` subshell would not have it). Each check
# sets DIND_HOST_PASSTHROUGH_IMAGES inline so the case is self-contained.
check "bare repo matches tagged hub ref" \
eval 'DIND_HOST_PASSTHROUGH_IMAGES="konard/hive-mind" host_image_matches_images_filter "konard/hive-mind:latest"'
check "docker.io-qualified pattern matches hub ref" \
eval 'DIND_HOST_PASSTHROUGH_IMAGES="docker.io/konard/hive-mind" host_image_matches_images_filter "konard/hive-mind:latest"'
check "glob matches hub ref" \
eval 'DIND_HOST_PASSTHROUGH_IMAGES="konard/hive-mind*" host_image_matches_images_filter "konard/hive-mind-dind:latest"'
check "unrelated pattern does not match" \
eval '! DIND_HOST_PASSTHROUGH_IMAGES="konard/other" host_image_matches_images_filter "konard/hive-mind:latest"'
check "empty allowlist matches anything" \
eval 'DIND_HOST_PASSTHROUGH_IMAGES="" host_image_matches_images_filter "anything:latest"'
check "ghcr exact ref matches" \
eval 'DIND_HOST_PASSTHROUGH_IMAGES="ghcr.io/owner/tool:v1" host_image_matches_images_filter "ghcr.io/owner/tool:v1"'

echo "== Case 13: registry classification helpers =="
reset_state
# These call the sourced functions in the current shell (command substitution
Expand Down
Loading