feat(security): digest-pin every registry image; drop batch-killing HostAddr #74

Merged
dank merged 2 commits from security/quadlet-digest-pins into main 2026-10-21 16:32:44 -07:00
42 changed files with 129 additions and 52 deletions

No files matched your search

@@ -20,7 +20,7 @@ Wants=authentik-server.service
[Container]
ContainerName=authentik-outpost
Image=ghcr.io/goauthentik/server:2026.8.3
Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849
Exec=worker
Network=dankclaw_default
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
@@ -14,7 +14,7 @@ After=network.target
[Container]
ContainerName=authentik-postgres
Image=docker.io/library/postgres:16-alpine
Image=docker.io/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
Network=dankclaw_default
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
Environment=POSTGRES_DB=authentik
@@ -19,7 +19,7 @@ Wants=authentik-postgres.service
[Container]
ContainerName=authentik-server
Image=ghcr.io/goauthentik/server:2026.8.3
Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849
Exec=server
Network=dankclaw_default
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
@@ -16,7 +16,7 @@ Wants=authentik-server.service
[Container]
ContainerName=authentik-worker
Image=ghcr.io/goauthentik/server:2026.8.3
Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849
Exec=worker
Network=dankclaw_default
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
@@ -15,7 +15,7 @@ After=network.target
[Container]
ContainerName=gitea-postgres
Network=gitea.network
Image=docker.io/library/postgres:16-alpine
Image=docker.io/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
EnvironmentFile=/home/dank/Containers/gitea/env
Environment=POSTGRES_USER=gitea
Environment=POSTGRES_PASSWORD=${GITEA_DB_PASSWORD}
@@ -19,7 +19,7 @@ After=gitea-postgres.service network.target
[Container]
ContainerName=gitea-server
Network=gitea.network
Image=docker.io/gitea/gitea:latest
Image=docker.io/gitea/gitea:latest@sha256:30777c91280d973fa02be48b1ade735bd6859e8f6f7af3b4444d519437852504
EnvironmentFile=/home/dank/Containers/gitea/env
Environment=DATABASE_TYPE=postgres
Environment=DATABASE_HOST=gitea-postgres
@@ -23,7 +23,7 @@ After=gluetun.service network.target
[Container]
ContainerName=marreta_browser
Network=gluetun.container
Image=docker.io/lightpanda/browser:nightly
Image=docker.io/lightpanda/browser:nightly@sha256:979c36ed869f1016b8dcf22939a0633c262f2c30e892a03a9f720eba2ff1931e
[Service]
TimeoutStartSec=300
+1 -1
View File
@@ -32,7 +32,7 @@ After=gluetun.service marreta-browser.service network.target
[Container]
ContainerName=marreta
Network=gluetun.container
Image=ghcr.io/manualdousuario/marreta:latest
Image=ghcr.io/manualdousuario/marreta:latest@sha256:30f2a34567b79b66da794b4b9423751bbbcd07ceff600d9f6a2d219e7e439362
# Relocate off SearXNG's 8080 inside the shared netns.
Environment=NGINX_HTTP_PORT=8085
Environment=APP_NAME=Marreta
@@ -33,7 +33,7 @@ Wants=network-online.target
[Container]
ContainerName=alertmanager
Image=docker.io/prom/alertmanager:latest
Image=docker.io/prom/alertmanager:latest@sha256:e9733bafb1bdef9b00e25a21f8f99dc26a22224bf16641ad754d1649f4c3357a
Network=dankclaw_default
User=0:0
Exec=--config.file=/etc/alertmanager/alertmanager.yml --storage.path=/alertmanager
@@ -35,7 +35,7 @@ Wants=network-online.target
[Container]
ContainerName=cadvisor
Image=gcr.io/cadvisor/cadvisor:v0.49.1
Image=gcr.io/cadvisor/cadvisor:v0.49.1@sha256:3cde6faf0791ebf7b41d6f8ae7145466fed712ea6f252c935294d2608b1af388
PodmanArgs=--pid=host --cgroupns=host
PublishPort=127.0.0.1:8080:8080
PublishPort=192.168.0.143:8080:8080
@@ -34,7 +34,7 @@ Wants=network-online.target prometheus.service
[Container]
ContainerName=grafana
Image=docker.io/grafana/grafana:latest
Image=docker.io/grafana/grafana:latest@sha256:dbbc38ea21103cefb50396340cd88afec779c7e070b0a7bf5a75b4ce46651d3f
Network=dankclaw_default
User=0:0
PublishPort=127.0.0.1:3002:3000
@@ -33,7 +33,7 @@ Wants=network-online.target
[Container]
ContainerName=node-exporter
Image=docker.io/prom/node-exporter:latest
Image=docker.io/prom/node-exporter:latest@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0
Network=host
PodmanArgs=--pid=host
Exec=--path.procfs=/host/proc --path.sysfs=/host/sys --path.rootfs=/rootfs --web.listen-address=127.0.0.1:9100 --web.listen-address=192.168.0.143:9100
@@ -43,14 +43,16 @@ Wants=network-online.target
[Container]
ContainerName=prometheus
Image=docker.io/prom/prometheus:latest
Image=docker.io/prom/prometheus:latest@sha256:fa3302279710be2f21a8301ec17a8b4f2301fd829bd113839b3b6b02b6f191a2
Network=dankclaw_default
# Name the podman gateway so scrape jobs can reach same-host targets (the
# netavark hairpin gap bars this container from dialing its own host's LAN
# IP; the cadvisor/gpu jobs and the blackbox scrape all ride this name).
# Same mechanism the hermes Caddy upstream learned the hard way (Container
# Host Reachability in the vault).
HostAddr=host.containers.internal=169.254.1.2
# Same-host scrape targets are dialed at host.containers.internal (the podman
# gateway): a bridge container cannot dial its own host's LAN IP (netavark
# hairpin gap, measured 2026-10-21), and traffic to the gateway lands on host
# loopback / published ports (cadvisor/gpu jobs and the blackbox scrape all
# ride it). The name resolves natively — NO HostAddr key: quadlet 5.7.0 calls
# it unsupported and the WHOLE user quadlet batch then fails to generate,
# silently orphaning every other unit (live 2026-10-21: prometheus.service
# vanished while its container kept running as an orphan).
User=0:0
Exec=--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d --web.enable-lifecycle
PublishPort=127.0.0.1:9090:9090
@@ -27,7 +27,7 @@ After=network.target
[Container]
ContainerName=open-webui
Network=open-webui.network
Image=ghcr.io/open-webui/open-webui:main
Image=ghcr.io/open-webui/open-webui:main@sha256:4860ba005ae8f19f08d1e12b0868855b7b339ea0dad9927f196820fc0de04575
PublishPort=3005:8080
Environment=OPENAI_API_BASE_URL=http://192.168.0.143:7770
Environment=ANONYMIZED_TELEMETRY=False
+1 -1
View File
@@ -24,7 +24,7 @@ Wants=network-online.target
[Container]
ContainerName=pihole
Image=docker.io/pihole/pihole:latest
Image=docker.io/pihole/pihole:latest@sha256:d34134716fc32038b052a8674c7c6879e0ff21f0e6d8af345166fffa2c27c113
Network=slirp4netns
PublishPort=0.0.0.0:53:53/udp
PublishPort=0.0.0.0:53:53/tcp
+1 -1
View File
@@ -16,7 +16,7 @@ After=searxng-network.service network.target
[Container]
ContainerName=gluetun
Network=searxng.network
Image=docker.io/qmcgaw/gluetun:latest
Image=docker.io/qmcgaw/gluetun:latest@sha256:c2ee4191762fca6b6a3c3359a731198a95d9c9af4db32d466e223bf3a84e779b
AddCapability=NET_ADMIN
AddDevice=/dev/net/tun:/dev/net/tun
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
@@ -16,7 +16,7 @@ After=searxng-network.service network.target
ContainerName=searxng-valkey
Network=searxng.network
NetworkAlias=valkey
Image=docker.io/valkey/valkey:9-alpine
Image=docker.io/valkey/valkey:9-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
Volume=/opt/searxng/valkey:/data
[Service]
+1 -1
View File
@@ -16,7 +16,7 @@ After=gluetun.service searxng-valkey.service network.target
[Container]
ContainerName=searxng
Network=gluetun.container
Image=ghcr.io/searxng/searxng:latest
Image=ghcr.io/searxng/searxng:latest@sha256:5ed0323b5f5f8701264ef65e239a2bd83cb57098f2b971fa30bf79c06eeae271
Environment=SEARXNG_SECRET=${SEARXNG_SECRET}
Environment=SEARXNG_BASE_URL=${SEARXNG_BASE_URL}
Volume=/opt/searxng/config:/etc/searxng
+1 -1
View File
@@ -29,7 +29,7 @@ Wants=network-online.target
[Container]
ContainerName=seerr
Image=ghcr.io/seerr-team/seerr:v3.4.1
Image=ghcr.io/seerr-team/seerr:v3.4.1@sha256:f4768de5f616248d723e05891f3345a1402123775d03bf0890dbfedc0831bda1
Network=dankclaw_default
PublishPort=127.0.0.1:5055:5055
Volume=/home/dank/Containers/seerr/data:/app/config
@@ -43,7 +43,7 @@ After=network.target
[Container]
ContainerName=calibre-server
Image=lscr.io/linuxserver/calibre:9.15.0
Image=lscr.io/linuxserver/calibre:9.15.0@sha256:59e4b0e036e62f228f8bcfae1ade42f47047719885fe454ddbad38e4597b987b
Network=pasta
EnvironmentFile=/home/dank/Containers/calibre-server/env
Environment=PUID=1000
@@ -16,7 +16,7 @@ After=network.target
[Container]
ContainerName=calibre-web
Image=lscr.io/linuxserver/calibre-web:0.6.27-ls402
Image=lscr.io/linuxserver/calibre-web:0.6.27-ls402@sha256:802729966faed5e32d671cd4f92ba6196e9e754a6c0f740ce1ee9acd86b10e55
Network=pasta
Environment=DOCKER_MODS=linuxserver/mods:universal-calibre
Environment=LANG=en_US.UTF-8
@@ -21,7 +21,13 @@ After=network.target
[Container]
ContainerName=copyparty
Image=ghcr.io/9001/copyparty-ac:latest
# Digest-pinned: upstream stopped publishing version tags after 1.19.15, so
# the pin rides :latest@digest (the tag in the reference is cosmetic; the
# digest is the pull). Pinned 2026-10-21 at the build whose label is
# 1.20.26 - closes GHSA-3c88-p299-7hvr (1.20.25) and GHSA-hg99-jqfp-rf76
# (1.20.26); the previously running build was the rolling :latest at
# 1.20.24 (issue #73 on command-center).
Image=ghcr.io/9001/copyparty-ac:latest@sha256:7f77459160a4bf34fdca79f2965dff4f0c1d7069a3d9cdfb3632acb041dd2f6a
Network=dankserver_appnet
EnvironmentFile=/home/dank/Containers/copyparty/env
Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD} -v /w::r:ra,dank
@@ -16,7 +16,7 @@ After=network.target
[Container]
ContainerName=jellyfin
Image=ghcr.io/jellyfin/jellyfin:latest
Image=ghcr.io/jellyfin/jellyfin:latest@sha256:c3efa69a22f2691fc88d6bb035e87a8560e395a6047f062fb381c61c3185e8f5
Network=dankserver_appnet
Environment=DEBIAN_FRONTEND=noninteractive
Environment=DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true
@@ -18,7 +18,7 @@ Wants=network-online.target
[Container]
ContainerName=cadvisor
Image=gcr.io/cadvisor/cadvisor:v0.49.1
Image=gcr.io/cadvisor/cadvisor:v0.49.1@sha256:3cde6faf0791ebf7b41d6f8ae7145466fed712ea6f252c935294d2608b1af388
PodmanArgs=--pid=host --cgroupns=host
PublishPort=127.0.0.1:8080:8080
PublishPort=192.168.0.235:8080:8080
@@ -37,7 +37,7 @@ Wants=network-online.target
[Container]
ContainerName=node-exporter
Image=docker.io/prom/node-exporter:latest
Image=docker.io/prom/node-exporter:latest@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0
Network=host
PodmanArgs=--pid=host
Exec=--path.procfs=/host/proc --path.sysfs=/host/sys --path.rootfs=/rootfs --web.listen-address=127.0.0.1:9100 --web.listen-address=192.168.0.235:9100
+1 -1
View File
@@ -4,7 +4,7 @@ After=network-online.target
Wants=network-online.target
[Container]
Image=lscr.io/linuxserver/plex:1.43.5.11029-6bf8af3f0-ls329
Image=lscr.io/linuxserver/plex:1.43.5.11029-6bf8af3f0-ls329@sha256:8f655bad7dcbc5398a78f70a5af12600b4ed3424f901566ed1eaa6710e9e2334
ContainerName=plex
Network=dankserver_appnet:alias=plex
Environment=PUID=0
@@ -14,7 +14,7 @@ After=network.target
[Container]
ContainerName=romm-mariadb
Image=docker.io/library/mariadb:11
Image=docker.io/library/mariadb:11@sha256:f16d7a344413e519dff518f27bdb221388f84b0d274c97c4fb5f2cb0dc1b34fa
Network=dankserver_appnet:alias=mariadb
EnvironmentFile=/home/dank/Containers/romm/env
Environment=MARIADB_DATABASE=romm
+1 -1
View File
@@ -12,7 +12,7 @@ After=network.target
[Container]
ContainerName=romm-redis
Image=docker.io/library/redis:7-alpine
Image=docker.io/library/redis:7-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
Network=dankserver_appnet:alias=redis
Volume=/home/dank/Containers/romm/redis:/data
HealthCmd=redis-cli ping
+1 -1
View File
@@ -18,7 +18,7 @@ After=romm-mariadb.service romm-redis.service network.target
[Container]
ContainerName=romm
Image=ghcr.io/rommapp/romm:latest
Image=ghcr.io/rommapp/romm:latest@sha256:03295f95a6290e752641f9553ac4198b0e5b71b879d57f7b2371b98f608feb54
Network=dankserver_appnet
EnvironmentFile=/home/dank/Containers/romm/env
Environment=DB_HOST=mariadb
@@ -4,7 +4,7 @@ After=network-online.target
Wants=network-online.target
[Container]
Image=lscr.io/linuxserver/tautulli:v2.18.1-ls245
Image=lscr.io/linuxserver/tautulli:v2.18.1-ls245@sha256:c66d3afa8f7a701a48dd1b1f57b22eca0483d895d670fe11ecff783f6a9bbd73
ContainerName=tautulli
Network=dankserver_appnet:alias=tautulli
Environment=PUID=0
@@ -19,7 +19,7 @@ Description=Gluetun VPN tunnel (FrankBook-Pro podman machine)
[Container]
ContainerName=gluetun
Image=docker.io/qmcgaw/gluetun:latest
Image=docker.io/qmcgaw/gluetun:latest@sha256:c2ee4191762fca6b6a3c3359a731198a95d9c9af4db32d466e223bf3a84e779b
AddCapability=NET_ADMIN
AddDevice=/dev/net/tun:/dev/net/tun
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
@@ -15,7 +15,7 @@ After=gluetun.service
[Container]
ContainerName=lidarr
Image=lscr.io/linuxserver/lidarr:latest
Image=lscr.io/linuxserver/lidarr:latest@sha256:d8549ff9ea29b4015c32257360652c1000c31ea471a2b3f0835ae77292faf35b
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
@@ -24,7 +24,7 @@ After=gluetun.service
[Container]
ContainerName=prowlarr
Image=lscr.io/linuxserver/prowlarr:latest
Image=lscr.io/linuxserver/prowlarr:latest@sha256:b3b00047ed20f6ce2b16780145cb6a0acb51c9e28ea6aa6f5dcd33c7034f1797
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
@@ -29,7 +29,7 @@ Network=gluetun.container
# Pinned to the last 4.x line: Readarr's retired qB client requires the
# qB 4.x login response (HTTP 200, body "Ok."); qB 5.x answers 204 with an
# empty body, which fails Readarr's auth check. All other clients accept 4.x.
Image=lscr.io/linuxserver/qbittorrent:4.6.7
Image=lscr.io/linuxserver/qbittorrent:4.6.7@sha256:55f15d44396315551f87294a176efae733b16e283f38980308e46073950257c6
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
@@ -15,7 +15,7 @@ After=gluetun.service
[Container]
ContainerName=radarr
Image=lscr.io/linuxserver/radarr:latest
Image=lscr.io/linuxserver/radarr:latest@sha256:fc0ae3c50f8128b97e84381903ec8d6845c3a569641cb7117c124376cccb57c1
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
@@ -19,7 +19,7 @@ After=gluetun.service
[Container]
ContainerName=readarr
Image=lscr.io/linuxserver/readarr:amd64-1.0.980
Image=lscr.io/linuxserver/readarr:amd64-1.0.980@sha256:a95e82dbb0eb2bb43de4c150f1dab9dd821306c67ea0b418983a957e731f2b61
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
@@ -16,7 +16,7 @@ After=gluetun.service
[Container]
ContainerName=sonarr
Image=lscr.io/linuxserver/sonarr:latest
Image=lscr.io/linuxserver/sonarr:latest@sha256:889dcc58c6aea859033985894b519b2e6b1e55d92527ed123f9f52e42ae7c97b
Environment=PUID=0
Environment=PGID=0
Environment=TZ=America/Los_Angeles
@@ -130,10 +130,19 @@ Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
## 4. Podman 5.x quadlet v1 generator quirks (verified live 2026-08-28)
- **Hard-fail keys** — the whole unit silently fails to generate
(`systemctl --user cat <unit>` shows "could not be found"): any
dash-prefixed `[Container]` option (`--shm-size=…` → `unsupported key …
in group 'Container'`); `Command=` is unsupported, use `Exec=`.
- **Hard-fail keys abort the WHOLE BATCH, not one unit** (podman 5.7.0,
live 2026-10-21): one unsupported key — e.g. `HostAddr=` — makes the
generator log `unsupported key … in group 'Container'` plus
`processing encountered some errors` and generate NOTHING for that
manager. The running containers keep going as orphans, so the failure
first appears on the next deploy as `Unit <x>.service not found` for a
service that is visibly up. Other offenders: any dash-prefixed
`[Container]` option (`--shm-size=…`), `Command=` (use `Exec=`).
The seam `tests/quadlet_image_pins.bats` fails the build on any
generator-rejected key in any host's units.
Also: an image whose ENTRYPOINT is the binary takes FLAGS ONLY in
`Exec=` — repeating the binary makes it a positional arg
(`unexpected /bin/blackbox_exporter`, live 2026-10-21).
- **HealthCmd space-splitting:** the v1 generator splits `HealthCmd` on
spaces into the healthcheck argv; a quoted probe
`/bin/sh -c "pg_isready …"` lands as argv token `"pg_isready` →
@@ -148,6 +157,16 @@ Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
"unsupported key"`, `systemctl --user cat probe.service` (shows the
generated ExecStart verbatim), then remove + reload.
## 4a. Image pins (fleet law, 2026-10-21)
Every registry image in a quadlet is digest-pinned (`tag@sha256:…`);
locally built images (`localhost/*`, `franksdank/*`) are exempt — their
build is the pin. A rolling tag lets a rebuild move under a running
service with nobody approving it, and it makes a CVE audit ungradeable.
Gate: `tests/quadlet_image_pins.bats`. Where upstream publishes no version
tags (copyparty-ac stops at 1.19.15), pin `:latest@sha256:` and name the
version the config label carries in the comment beside it.
## 5. Rootless Quadlet log retrieval
Step 0 — establish ownership: `systemctl --user status <name>.service`
+2 -1
View File
@@ -77,6 +77,7 @@ FRANKBOOK_UNITS=(
}
@test "readarr keeps its pinned image" {
grep -q '^Image=lscr.io/linuxserver/readarr:amd64-1.0.980$' \
# 2026-10-21 fleet law: version tag AND digest (tests/quadlet_image_pins.bats)
grep -qE '^Image=lscr\.io/linuxserver/readarr:amd64-1\.0\.980@sha256:[0-9a-f]{64}$' \
"$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
}
+3 -2
View File
@@ -15,8 +15,9 @@ QDIR="$REPO_ROOT/dankclaw/open-webui/quadlet"
}
@test "open-webui uses the CPU image, not :cuda (dankclaw is CPU-only)" {
grep -qE '^Image=ghcr\.io/open-webui/open-webui:main$' "$QDIR/open-webui.container" \
|| { echo "open-webui.container must use the :main (CPU) image"; return 1; }
# 2026-10-21 fleet law: tag AND digest (tests/quadlet_image_pins.bats)
grep -qE '^Image=ghcr\.io/open-webui/open-webui:main@sha256:[0-9a-f]{64}$' "$QDIR/open-webui.container" \
|| { echo "open-webui.container must use the :main (CPU) image, digest-pinned"; return 1; }
if grep -qE '^Image=.*:cuda' "$QDIR/open-webui.container"; then
echo "open-webui.container must not use the :cuda image on a CPU-only host"
return 1
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bats
# Seam: fleet-wide quadlet laws learned the hard way.
# 1. Every registry image is digest-pinned. A rolling tag (":latest",
# ":9-alpine", ":nightly") lets a rebuild move under a running
# service at the next restart/reboot with nobody approving it, and it
# makes a CVE audit ungradeable (you cannot score a tag). Locally
# built images (localhost/*, franksdank/*) are exempt: their build
# IS the pin, and they never pull.
# 2. No unit carries a key the podman 5.7.0 quadlet generator rejects.
# The generator aborts the WHOLE batch on an unsupported key, so one
# bad file silently un-generates every other unit in that manager -
# live 2026-10-21: HostAddr= in prometheus.container orphaned
# prometheus.service while its container kept running, and the next
# deploy died on "Unit prometheus.service not found".
load helpers
@test "every registry Image= is digest-pinned across all three hosts" {
bad=""
while IFS= read -r unit; do
while IFS= read -r img; do
case "$img" in
localhost/*|franksdank/*) continue ;;
esac
case "$img" in
*@sha256:*) ;;
*) bad="$bad$unit: $img"$'\n' ;;
esac
done < <(sed -n 's/^Image=//p' "$unit")
done < <(find "$REPO_ROOT"/dankserver "$REPO_ROOT"/dankclaw "$REPO_ROOT"/frankbook-pro \
-path '*/quadlet/*.container' -not -path '*.AGENTTRASH*')
[ -z "$bad" ] || { printf 'unpinned images:\n%s' "$bad" >&2; return 1; }
}
@test "no unit uses a key the 5.7.0 quadlet generator rejects (batch killer)" {
# HostAddr rejected by podman 5.7.0 (live 2026-10-21); Device= and
# Command= are the older v1 findings, guarded host-locally too.
# Keep this list current with every generator finding.
bad=""
while IFS= read -r unit; do
for key in HostAddr Device Command; do
grep -q "^${key}=" "$unit" && bad="$bad$unit: ${key}="$'\n'
done
done < <(find "$REPO_ROOT"/dankserver "$REPO_ROOT"/dankclaw "$REPO_ROOT"/frankbook-pro \
-path '*/quadlet/*.container' -not -path '*.AGENTTRASH*')
[ -z "$bad" ] || { printf 'generator-rejected keys:\n%s' "$bad" >&2; return 1; }
}
+3 -2
View File
@@ -20,8 +20,9 @@ SEERR_UNIT="$REPO_ROOT/dankclaw/seerr/quadlet/seerr.container"
grep -q '^Restart=always$' "$SEERR_UNIT"
}
@test "seerr image is pinned to an exact version tag" {
grep -Eq '^Image=ghcr\.io/seerr-team/seerr:v[0-9]+\.[0-9]+\.[0-9]+$' "$SEERR_UNIT"
@test "seerr image is pinned to an exact version tag and digest" {
# 2026-10-21 fleet law: tag AND digest (tests/quadlet_image_pins.bats)
grep -Eq '^Image=ghcr\.io/seerr-team/seerr:v[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}$' "$SEERR_UNIT"
}
@test "seerr publishes 5055 on loopback only (guard chain is the user path)" {