Security release for the k7-api control plane. Fixes a server-side request forgery reachable through a sandbox's image registry host: registry hosts are now resolved and checked against public/allowlisted ranges before any OCI fetch, the localhost-to-plain-HTTP downgrade is gone, and redirects are disabled so an allowlisted host cannot bounce the request inward. Adds optional per-key namespace authorization, so an API key can be confined to the namespaces it owns and cannot perform all-namespaces operations. Keys without a scope keep their previous unrestricted behaviour, so upgrading changes nothing until you scope your keys. Both issues were reported privately by Jirayu Thongchotchaung, who held disclosure until this release was available. See CHANGELOG.md and the published advisories for detail.
Docker workload benchmark — Spec 10b
Quantifies the storage tax of running Docker inside a k7 sandbox vs
natively on the host. Output lands in PERFORMANCE.md
under a dated section, formatted so the median + ratio numbers can be
cited verbatim from any blog post.
What gets benched
| Label | What it is |
|---|---|
host |
Native Docker on the Hetzner node (no k7 involved) |
k7-fd |
k7 sandbox, kata-firecracker-devmapper (kfd) backend, --sidecar docker (docker daemon's /var/lib/docker is an emptyDir) |
k7-ql-r1 |
k7 sandbox, kata-qemu-longhorn (kql) backend, --sidecar docker, Longhorn replicas=1 |
k7-ql-r2 |
Same as k7-ql-r1 but Longhorn replicas=2 (requires ≥ 2-node cluster) |
The four environments stack the storage path cleanly: host → no VM,
no Longhorn. k7-fd → VM but no Longhorn. k7-ql-r1 → VM + one local
Longhorn replica. k7-ql-r2 → VM + one local + one cross-node Longhorn
replica. Subtracting k7-fd / host from k7-ql-r1 / host isolates the
Longhorn tax from the kata-qemu tax.
Workloads (5 runs each, after a discarded warm-up)
| Operation | Isolates |
|---|---|
pull debian:12-slim |
Network + extract + write of an external image |
build (no-cache) |
The headline number: apt + pip + git + 256 MB fsync |
build (cached) |
Sanity — should be sub-second; non-zero on k7-ql-* would mean the bind mount of /var/lib/docker is broken |
run cpu (10s budget) |
Kata-qemu CPU overhead (Python busy-loop) |
run io (2k small + 512 MB fsync) |
Write path through the storage stack |
run read (venv tree cat) |
Read path (mostly page-cache hits after the first call) |
The Dockerfile that drives build (no-cache) is pinned at
bench/docker-perf/bench.Dockerfile — apt + pip
generate thousands of small files (metadata pressure), git clone is
inode-heavy, and dd … conv=fsync measures sync-write throughput.
How to run it
The bench is a pytest module (not a shell script) — it reuses the
exact same k7_core / test_namespace fixtures that
tests/integration/test_sidecar_docker.py already uses to spin up a
docker:27.5-cli sandbox with --sidecar docker. The harness times
k7_core.exec_command(...) calls instead of doing anything new.
Run all three envs:
# On the cluster node (or any host that already runs the integration suite):
K7_BENCH_ENVS=host,k7-ql-r1,k7-ql-r2 \
uv run pytest -m bench tests/integration/bench_docker_perf.py -v -s
Pick a subset (useful for iterating on tooling):
K7_BENCH_ENVS=host uv run pytest -m bench -v -s tests/integration/bench_docker_perf.py
K7_BENCH_ENVS=k7-ql-r1 uv run pytest -m bench -v -s tests/integration/bench_docker_perf.py
Switching r1 → r2 is done in-cluster by patching Longhorn's
default-replica-count setting via kubectl — no k7 install reinstall,
no cluster-wide downtime, and the value is restored at session teardown.
Environment variables
| Var | Default | Purpose |
|---|---|---|
K7_BENCH_ENVS |
host,k7-ql-r1,k7-ql-r2 |
Comma-separated subset of envs to run |
K7_BENCH_REPS |
5 |
Reps per cell |
K7_BENCH_WARMUP |
1 |
0 to disable the warm-up |
K7_BENCH_OUT |
/tmp |
Where logs + the aggregated CSV land |
Output
Each leg writes a bench-<label>-<ts>.log of the same shape render.py
already parses:
# label=k7-ql-r1
# timestamp=20260529T120000Z
# kernel=...
# docker_version=...
# longhorn_replicas=1
# columns: OPERATION RUN SECONDS
pull 1 3.142
pull 2 3.098
build_nocache 1 60.42
...
When the pytest session ends, a session-scoped fixture aggregates all
logs from bench_logs into a single CSV at
$K7_BENCH_OUT/bench-results-<ts>.csv. Feed that to render.py:
uv run python bench/docker-perf/render.py \
--input /tmp/bench-results-<ts>.csv \
--title "Docker workloads inside sandboxes — 2026-05-29 (k7 <sha>)" \
--hardware "Hardware: 2x Hetzner AX42, Ubuntu 24.04, kernel 6.8, Docker 27.5, Longhorn 1.7.x." \
--output - >> PERFORMANCE.md
Known confounds (acknowledge these honestly in any post)
- Page cache — the harness drops
/proc/sys/vm/drop_cachesonly on the host (where it has the privilege). Inside the sandbox we can't, so therun readleg is mostly a page-cache measurement, not a storage-stack one. Don't over-interpret that row. - Network egress for
pull— Cilium FQDN egress + the sandbox's network path aren't the same as the host's.pullis here for context, not as a storage-tax signal. - Kata-qemu CPU overhead — the
run cpurow reflects both CPU virtualization overhead and any scheduler differences. Reporting it explicitly lets readers do their own subtraction. - Workload immutability — the Dockerfile is committed and the base
image (
debian:12-slim) is captured by digest in each log header. The only real variable across re-runs is the k7 / Longhorn version.
Statistical handling
- 5 reps per cell, first warm-up run is discarded.
- Reported as median + min/max (mean is too sensitive to outliers on storage benchmarks).
- Cells with
(max − min) / median > 0.30are flagged with⚠. Treat flagged cells as "rerun with more reps before publishing" — don't quote them.
Re-rendering without re-running
Already have a CSV (manually edited, or salvaged from a partial run)? Re-render with:
python3 bench/docker-perf/render.py \
--input /tmp/bench-results-<ts>.csv \
--title "Docker workloads inside sandboxes — 2026-05-29 (k7 <sha>)" \
--hardware "Hardware: ..." \
--output /tmp/section.md
That same output is what bench_docker_perf.py's teardown prints
instructions for — re-rendering is just the path for fixing typos in
the title / hardware line without re-running the bench.