From abf0f56afc130a24c09acf70dfb71e484083670b Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 5 Aug 2026 08:46:09 -0400 Subject: [PATCH] fix(security): close the five host-networked app UIs the audit could not see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scanning archi-dev-box from OUTSIDE found five ports serving their screens with no login — lnd-ui 18083, bitcoin-ui 8334, fips-ui 8336, electrs-ui 50002 and the Fedimint Guardian 8175 — none of which appeared in the gate's unprotected list. They are host-networked, so Podman publishes nothing to pin and their manifests declared 'ports: []'; the gate builds its map from declared ports, so it neither protected them nor reported them. An audit that reports success while five screens are open is worse than no audit. Their nginx now listens on 127.0.0.1 instead of 0.0.0.0, and each port is declared 'auth: gated' so the daemon owns the outside. 'bind:' on a host-networked app is a statement of where the container listens, not a publish instruction — quadlet already skips PublishPort in host mode. Guardian 8175 is declared on the fedimint app because its companion has no manifest, and the gate keys on port, not container. Credential paths were NOT exposed and are verified so: /lnd-connect-info, the /proxy/lnd/ passthrough, container logs and every RPC method through these screens all return 401 unauthenticated. What leaked was the page shell. Also fixes the delivery gap that would have made this unshippable: only bitcoin-ui, lnd-ui and electrs-ui were ever rsynced to /opt/archipelago/docker, so edits to fips-ui and fedimint-ui reached nodes through no path at all. All five now sync; the two whose rebuilds the daemon owns are synced without being handed to container-specs. Every remaining undeclared port is now declared with a stated reason — gated: botfights 9100, router 8084, pine 10380; exempt with rationale: fedimint consensus 8173/8174, gateway 8176/9737, netbird 8086/8087 (TLS + own auth, and enrolled devices cannot hold a session), pine TLS 10381, lightning-stack REST 8091 (macaroon, mirrors lnd). Zero undeclared ports remain across all 56 manifests. Co-Authored-By: Claude Fable 5 --- apps/bitcoin-ui/manifest.yml | 13 +++- apps/botfights/manifest.yml | 2 + apps/electrs-ui/manifest.yml | 13 +++- apps/fedimint-gateway/manifest.yml | 8 ++ apps/fedimint/manifest.yml | 18 +++++ apps/fips-ui/manifest.yml | 13 +++- apps/lightning-stack/manifest.yml | 5 ++ apps/lnd-ui/manifest.yml | 13 +++- apps/netbird-server/manifest.yml | 5 ++ apps/netbird/manifest.yml | 5 ++ apps/pine/manifest.yml | 7 ++ apps/router/manifest.yml | 2 + core/archipelago/src/container/bitcoin_ui.rs | 2 +- .../container/bitcoin_ui_nginx.conf.template | 9 ++- docker/electrs-ui/nginx.conf | 9 ++- docker/fedimint-ui/nginx.conf | 9 ++- docker/fips-ui/nginx.conf | 9 ++- docker/lnd-ui/nginx.conf | 9 ++- releases/app-catalog.json | 73 +++++++++++++++++-- scripts/self-update.sh | 26 ++++++- 20 files changed, 230 insertions(+), 20 deletions(-) diff --git a/apps/bitcoin-ui/manifest.yml b/apps/bitcoin-ui/manifest.yml index a9e10f4d..6fb05656 100644 --- a/apps/bitcoin-ui/manifest.yml +++ b/apps/bitcoin-ui/manifest.yml @@ -31,7 +31,18 @@ app: # proxies to 127.0.0.1:8332 which is where the bitcoin backend binds # its RPC. `ports:` is intentionally empty because host networking # bypasses port mapping. - ports: [] + # Declared so the APP GATE can see this port. Host networking means Podman + # publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here + # is a statement of where the container's own nginx listens — 127.0.0.1 — + # not a publish instruction. Without this declaration the gate had no idea + # the port existed: it was neither protected nor listed as unprotected, and + # served the Bitcoin screen unauthenticated on every interface. + ports: + - host: 8334 + container: 8334 + protocol: tcp + bind: 127.0.0.1 + auth: gated volumes: # Bind-mount the rendered nginx.conf read-only. The prod orchestrator diff --git a/apps/botfights/manifest.yml b/apps/botfights/manifest.yml index eba5410c..a7e3e1fd 100644 --- a/apps/botfights/manifest.yml +++ b/apps/botfights/manifest.yml @@ -62,6 +62,8 @@ app: - host: 9100 container: 9100 protocol: tcp # Web UI + API + bind: 127.0.0.1 + auth: gated volumes: # A bare relative source (was "botfights-data", no leading slash) is diff --git a/apps/electrs-ui/manifest.yml b/apps/electrs-ui/manifest.yml index 0d5f6be3..4f224565 100644 --- a/apps/electrs-ui/manifest.yml +++ b/apps/electrs-ui/manifest.yml @@ -23,7 +23,18 @@ app: network_policy: host # Host networking: nginx listens on 50002 directly on the host IP. - ports: [] + # Declared so the APP GATE can see this port. Host networking means Podman + # publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here + # is a statement of where the container's own nginx listens — 127.0.0.1 — + # not a publish instruction. Without this declaration the gate had no idea + # the port existed: it was neither protected nor listed as unprotected, and + # served the Electrs screen unauthenticated on every interface. + ports: + - host: 50002 + container: 50002 + protocol: tcp + bind: 127.0.0.1 + auth: gated volumes: [] diff --git a/apps/fedimint-gateway/manifest.yml b/apps/fedimint-gateway/manifest.yml index 9363bb10..42ed8239 100644 --- a/apps/fedimint-gateway/manifest.yml +++ b/apps/fedimint-gateway/manifest.yml @@ -60,9 +60,17 @@ app: - host: 8176 container: 8176 protocol: tcp + auth: none + auth_rationale: >- + Fedimint gateway API, protected by its own bcrypt password (--bcrypt-password-hash) + and reached by federation peers and clients that cannot hold a browser session. - host: 9737 container: 9737 protocol: tcp + auth: none + auth_rationale: >- + LDK Lightning p2p for the gateway. The BOLT-8 noise handshake authenticates and + encrypts the connection itself. volumes: - type: bind diff --git a/apps/fedimint/manifest.yml b/apps/fedimint/manifest.yml index c3c7985a..e93fcf0e 100644 --- a/apps/fedimint/manifest.yml +++ b/apps/fedimint/manifest.yml @@ -50,11 +50,29 @@ app: - host: 8173 container: 8173 protocol: tcp + auth: none + auth_rationale: >- + Fedimint guardian consensus. Other guardians speak the federation's own + authenticated protocol here; a login page would break consensus. - host: 8174 container: 8174 protocol: tcp + auth: none + auth_rationale: >- + Fedimint guardian API for federation clients, which authenticate to the + federation itself and cannot hold a browser session. # Public launch port 8175 is owned by archy-fedimint-ui, which serves a # wait page while Bitcoin syncs and proxies here after fedimintd starts. + # Declared HERE because that companion has no manifest of its own, and the + # gate keys on the port rather than the container: without this entry it + # served the Guardian UI unauthenticated on every interface and never + # appeared in the audit. Its nginx is pinned to 127.0.0.1 + # (docker/fedimint-ui/nginx.conf) so the gate can own the outside. + - host: 8175 + container: 8175 + protocol: tcp + bind: 127.0.0.1 + auth: gated - host: 8177 container: 8175 protocol: tcp diff --git a/apps/fips-ui/manifest.yml b/apps/fips-ui/manifest.yml index 6983387c..e16c49f9 100644 --- a/apps/fips-ui/manifest.yml +++ b/apps/fips-ui/manifest.yml @@ -27,7 +27,18 @@ app: # Host networking: nginx listens on 8336 directly on the host IP and # proxies to 127.0.0.1:5678 (the archipelago RPC). `ports:` is # intentionally empty because host networking bypasses port mapping. - ports: [] + # Declared so the APP GATE can see this port. Host networking means Podman + # publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here + # is a statement of where the container's own nginx listens — 127.0.0.1 — + # not a publish instruction. Without this declaration the gate had no idea + # the port existed: it was neither protected nor listed as unprotected, and + # served the FIPS mesh screen unauthenticated on every interface. + ports: + - host: 8336 + container: 8336 + protocol: tcp + bind: 127.0.0.1 + auth: gated volumes: [] diff --git a/apps/lightning-stack/manifest.yml b/apps/lightning-stack/manifest.yml index ac7fc9fe..e9befcd6 100644 --- a/apps/lightning-stack/manifest.yml +++ b/apps/lightning-stack/manifest.yml @@ -41,9 +41,14 @@ app: auth: none auth_rationale: >- LND gRPC, authenticated by macaroon over TLS. Remote wallets depend on reaching this directly. + # Mirrors lnd's 18080 exemption — same LND REST API, same macaroon auth. - host: 8091 container: 8080 protocol: tcp # REST/Web UI + auth: none + auth_rationale: >- + LND REST, authenticated by macaroon over TLS. A browser login page would break + Zeus and every non-browser wallet client, exactly as for lnd's 18080. volumes: - type: bind diff --git a/apps/lnd-ui/manifest.yml b/apps/lnd-ui/manifest.yml index 713a1afe..cf186bcf 100644 --- a/apps/lnd-ui/manifest.yml +++ b/apps/lnd-ui/manifest.yml @@ -35,7 +35,18 @@ app: # port to a container port where nothing listens. scripts/container-specs.sh # carried the identical mistake and was fixed alongside this; recreating from # it on archi-dev-box left :18083 refusing connections. - ports: [] + # Declared so the APP GATE can see this port. Host networking means Podman + # publishes nothing (quadlet skips PublishPort in host mode), so `bind:` here + # is a statement of where the container's own nginx listens — 127.0.0.1 — + # not a publish instruction. Without this declaration the gate had no idea + # the port existed: it was neither protected nor listed as unprotected, and + # served the LND screen unauthenticated on every interface. + ports: + - host: 18083 + container: 18083 + protocol: tcp + bind: 127.0.0.1 + auth: gated volumes: [] diff --git a/apps/netbird-server/manifest.yml b/apps/netbird-server/manifest.yml index 994949f6..287be710 100644 --- a/apps/netbird-server/manifest.yml +++ b/apps/netbird-server/manifest.yml @@ -48,6 +48,11 @@ app: - host: 8086 container: 80 protocol: tcp # management API + embedded OIDC issuer (/oauth2) + auth: none + auth_rationale: >- + NetBird management API and its OIDC issuer. Enrolled devices authenticate + themselves with setup keys and JWTs, and they cannot hold a browser session — + a login page here would disconnect every VPN client on the network. - host: 3478 container: 3478 protocol: udp # STUN — must be UDP; tcp here breaks relay discovery diff --git a/apps/netbird/manifest.yml b/apps/netbird/manifest.yml index 6464335a..32cf44d5 100644 --- a/apps/netbird/manifest.yml +++ b/apps/netbird/manifest.yml @@ -44,6 +44,11 @@ app: - host: 8087 container: 443 protocol: tcp + auth: none + auth_rationale: >- + NetBird dashboard over TLS, with its own login. The gate speaks plain HTTP, + so fronting this port would break the secure context the dashboard requires + (issue #15) and the certificate clients pin. volumes: - type: bind diff --git a/apps/pine/manifest.yml b/apps/pine/manifest.yml index 3984cd3f..c673df3e 100644 --- a/apps/pine/manifest.yml +++ b/apps/pine/manifest.yml @@ -53,9 +53,16 @@ app: - host: 10380 container: 80 protocol: tcp + bind: 127.0.0.1 + auth: gated - host: 10381 container: 443 protocol: tcp + auth: none + auth_rationale: >- + Pine's TLS listener. The gate speaks plain HTTP, so fronting this port would + break the secure context navigator.bluetooth needs for WiFi provisioning. + The plain-HTTP entry point (10380) is gated, and it is what the UI opens. volumes: - type: bind diff --git a/apps/router/manifest.yml b/apps/router/manifest.yml index fb4300d4..bada2040 100644 --- a/apps/router/manifest.yml +++ b/apps/router/manifest.yml @@ -30,6 +30,8 @@ app: - host: 8084 container: 8080 protocol: tcp # Web UI + bind: 127.0.0.1 + auth: gated - host: 5353 container: 5353 protocol: udp # mDNS/Bonjour diff --git a/core/archipelago/src/container/bitcoin_ui.rs b/core/archipelago/src/container/bitcoin_ui.rs index eec7526a..37759a54 100644 --- a/core/archipelago/src/container/bitcoin_ui.rs +++ b/core/archipelago/src/container/bitcoin_ui.rs @@ -293,6 +293,6 @@ mod tests { // Lock in the core shape so a bad template edit doesn't ship. assert!(TEMPLATE.contains("proxy_pass http://127.0.0.1:8332/")); assert!(TEMPLATE.contains("location /bitcoin-rpc/")); - assert!(TEMPLATE.contains("listen 8334")); + assert!(TEMPLATE.contains("listen 127.0.0.1:8334")); } } diff --git a/core/archipelago/src/container/bitcoin_ui_nginx.conf.template b/core/archipelago/src/container/bitcoin_ui_nginx.conf.template index 6e7e2bdb..ddd4c1b6 100644 --- a/core/archipelago/src/container/bitcoin_ui_nginx.conf.template +++ b/core/archipelago/src/container/bitcoin_ui_nginx.conf.template @@ -1,5 +1,12 @@ server { - listen 8334; + # Loopback ONLY. This container is host-networked, so this nginx binds the + # HOST's address directly — `listen 8334;` meant every interface, and the + # app gate could never stand in front of it (there is no podman publish to + # pin, and the manifest declared no port, so the gate neither protected it + # nor reported it — it served this page to anyone who asked, on LAN, + # Tailscale and the mesh alike). Binding loopback lets the daemon claim the + # external addresses and authenticate them; see appgate::listener. + listen 127.0.0.1:8334; server_name _; root /usr/share/nginx/html; index index.html; diff --git a/docker/electrs-ui/nginx.conf b/docker/electrs-ui/nginx.conf index 9f927d8f..b9d4932f 100644 --- a/docker/electrs-ui/nginx.conf +++ b/docker/electrs-ui/nginx.conf @@ -1,5 +1,12 @@ server { - listen 50002; + # Loopback ONLY. This container is host-networked, so this nginx binds the + # HOST's address directly — `listen 50002;` meant every interface, and the + # app gate could never stand in front of it (there is no podman publish to + # pin, and the manifest declared no port, so the gate neither protected it + # nor reported it — it served this page to anyone who asked, on LAN, + # Tailscale and the mesh alike). Binding loopback lets the daemon claim the + # external addresses and authenticate them; see appgate::listener. + listen 127.0.0.1:50002; server_name _; root /usr/share/nginx/html; diff --git a/docker/fedimint-ui/nginx.conf b/docker/fedimint-ui/nginx.conf index 8a8539af..a4136378 100644 --- a/docker/fedimint-ui/nginx.conf +++ b/docker/fedimint-ui/nginx.conf @@ -1,5 +1,12 @@ server { - listen 8175; + # Loopback ONLY. This container is host-networked, so this nginx binds the + # HOST's address directly — `listen 8175;` meant every interface, and the + # app gate could never stand in front of it (there is no podman publish to + # pin, and the manifest declared no port, so the gate neither protected it + # nor reported it — it served this page to anyone who asked, on LAN, + # Tailscale and the mesh alike). Binding loopback lets the daemon claim the + # external addresses and authenticate them; see appgate::listener. + listen 127.0.0.1:8175; server_name _; proxy_intercept_errors on; diff --git a/docker/fips-ui/nginx.conf b/docker/fips-ui/nginx.conf index 5faf33c5..b2d8eae6 100644 --- a/docker/fips-ui/nginx.conf +++ b/docker/fips-ui/nginx.conf @@ -1,5 +1,12 @@ server { - listen 8336; + # Loopback ONLY. This container is host-networked, so this nginx binds the + # HOST's address directly — `listen 8336;` meant every interface, and the + # app gate could never stand in front of it (there is no podman publish to + # pin, and the manifest declared no port, so the gate neither protected it + # nor reported it — it served this page to anyone who asked, on LAN, + # Tailscale and the mesh alike). Binding loopback lets the daemon claim the + # external addresses and authenticate them; see appgate::listener. + listen 127.0.0.1:8336; server_name _; root /usr/share/nginx/html; index index.html; diff --git a/docker/lnd-ui/nginx.conf b/docker/lnd-ui/nginx.conf index 2c372562..c6ad7386 100644 --- a/docker/lnd-ui/nginx.conf +++ b/docker/lnd-ui/nginx.conf @@ -1,7 +1,14 @@ server { # Host-networked: listen on the app's own port directly (NOT 80, which the # host's main nginx already owns). The app is reached at http(s)://:18083. - listen 18083; + # Loopback ONLY. This container is host-networked, so this nginx binds the + # HOST's address directly — `listen 18083;` meant every interface, and the + # app gate could never stand in front of it (there is no podman publish to + # pin, and the manifest declared no port, so the gate neither protected it + # nor reported it — it served this page to anyone who asked, on LAN, + # Tailscale and the mesh alike). Binding loopback lets the daemon claim the + # external addresses and authenticate them; see appgate::listener. + listen 127.0.0.1:18083; server_name _; root /usr/share/nginx/html; diff --git a/releases/app-catalog.json b/releases/app-catalog.json index 11feb497..5a304b38 100644 --- a/releases/app-catalog.json +++ b/releases/app-catalog.json @@ -711,7 +711,15 @@ }, "id": "bitcoin-ui", "name": "Bitcoin UI", - "ports": [], + "ports": [ + { + "auth": "gated", + "bind": "127.0.0.1", + "container": 8334, + "host": 8334, + "protocol": "tcp" + } + ], "resources": { "memory_limit": "128Mi" }, @@ -805,6 +813,8 @@ "name": "BotFights", "ports": [ { + "auth": "gated", + "bind": "127.0.0.1", "container": 9100, "host": 9100, "protocol": "tcp" @@ -1159,7 +1169,15 @@ }, "id": "electrs-ui", "name": "Electrs UI", - "ports": [], + "ports": [ + { + "auth": "gated", + "bind": "127.0.0.1", + "container": 50002, + "host": 50002, + "protocol": "tcp" + } + ], "resources": { "memory_limit": "64Mi" }, @@ -1355,15 +1373,26 @@ "name": "Fedimint Guardian", "ports": [ { + "auth": "none", + "auth_rationale": "Fedimint guardian consensus. Other guardians speak the federation's own authenticated protocol here; a login page would break consensus.", "container": 8173, "host": 8173, "protocol": "tcp" }, { + "auth": "none", + "auth_rationale": "Fedimint guardian API for federation clients, which authenticate to the federation itself and cannot hold a browser session.", "container": 8174, "host": 8174, "protocol": "tcp" }, + { + "auth": "gated", + "bind": "127.0.0.1", + "container": 8175, + "host": 8175, + "protocol": "tcp" + }, { "auth": "local", "bind": "127.0.0.1", @@ -1548,11 +1577,15 @@ "name": "Fedimint Gateway", "ports": [ { + "auth": "none", + "auth_rationale": "Fedimint gateway API, protected by its own bcrypt password (--bcrypt-password-hash) and reached by federation peers and clients that cannot hold a browser session.", "container": 8176, "host": 8176, "protocol": "tcp" }, { + "auth": "none", + "auth_rationale": "LDK Lightning p2p for the gateway. The BOLT-8 noise handshake authenticates and encrypts the connection itself.", "container": 9737, "host": 9737, "protocol": "tcp" @@ -1700,7 +1733,15 @@ }, "id": "fips-ui", "name": "FIPS Mesh", - "ports": [], + "ports": [ + { + "auth": "gated", + "bind": "127.0.0.1", + "container": 8336, + "host": 8336, + "protocol": "tcp" + } + ], "resources": { "memory_limit": "128Mi" }, @@ -2909,6 +2950,8 @@ "protocol": "tcp" }, { + "auth": "none", + "auth_rationale": "LND REST, authenticated by macaroon over TLS. A browser login page would break Zeus and every non-browser wallet client, exactly as for lnd's 18080.", "container": 8080, "host": 8091, "protocol": "tcp" @@ -3078,7 +3121,15 @@ }, "id": "lnd-ui", "name": "LND UI", - "ports": [], + "ports": [ + { + "auth": "gated", + "bind": "127.0.0.1", + "container": 18083, + "host": 18083, + "protocol": "tcp" + } + ], "resources": { "memory_limit": "64Mi" }, @@ -3413,6 +3464,8 @@ "name": "NetBird", "ports": [ { + "auth": "none", + "auth_rationale": "NetBird dashboard over TLS, with its own login. The gate speaks plain HTTP, so fronting this port would break the secure context the dashboard requires (issue #15) and the certificate clients pin.", "container": 443, "host": 8087, "protocol": "tcp" @@ -3617,6 +3670,8 @@ "name": "NetBird Server", "ports": [ { + "auth": "none", + "auth_rationale": "NetBird management API and its OIDC issuer. Enrolled devices authenticate themselves with setup keys and JWTs, and they cannot hold a browser session — a login page here would disconnect every VPN client on the network.", "container": 80, "host": 8086, "protocol": "tcp" @@ -4012,11 +4067,15 @@ "name": "Pine", "ports": [ { + "auth": "gated", + "bind": "127.0.0.1", "container": 80, "host": 10380, "protocol": "tcp" }, { + "auth": "none", + "auth_rationale": "Pine's TLS listener. The gate speaks plain HTTP, so fronting this port would break the secure context navigator.bluetooth needs for WiFi provisioning. The plain-HTTP entry point (10380) is gated, and it is what the UI opens.", "container": 443, "host": 10381, "protocol": "tcp" @@ -4458,6 +4517,8 @@ }, "ports": [ { + "auth": "gated", + "bind": "127.0.0.1", "container": 8080, "host": 8084, "protocol": "tcp" @@ -4837,7 +4898,5 @@ } }, "schema": 1, - "signature": "cc83d0be50ce6144e2b5693a7175d7743d4a19141f4ef9a46a3c88d2dadd848acda9c25063e7a8b5643cecb2ccde279762a00a9715a5d26c99a95b492bc82a05", - "signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur", - "updated": "2026-08-04" + "updated": "2026-08-05" } diff --git a/scripts/self-update.sh b/scripts/self-update.sh index 99ea24f6..fb2ed45f 100755 --- a/scripts/self-update.sh +++ b/scripts/self-update.sh @@ -321,7 +321,17 @@ fi UI_DOCKER_DEST="/opt/archipelago/docker" sudo mkdir -p "$UI_DOCKER_DEST" UI_REBUILD_LIST="" -for ui in bitcoin-ui lnd-ui electrs-ui; do +# fips-ui and fedimint-ui are synced but NOT added to UI_REBUILD_LIST below: +# container-specs.sh has no spec for either (and their container names break +# the archy- assumption — the FIPS one is plain `fips-ui`). Their rebuilds +# come from elsewhere — the daemon's companion installer for fedimint-ui, the +# orchestrator's build context for fips-ui — but BOTH read +# /opt/archipelago/docker/, and nothing was ever updating that directory. +# So source edits to those two trees reached nodes through no path at all: +# their nginx kept listening on 0.0.0.0 and served the Guardian and FIPS +# screens unauthenticated on every interface (found by scanning archi-dev-box +# from outside, 2026-08-05 — the in-node audit could not see them). +for ui in bitcoin-ui lnd-ui electrs-ui fips-ui fedimint-ui; do src="$REPO_DIR/docker/$ui" dst="$UI_DOCKER_DEST/$ui" [ -d "$src" ] || continue @@ -335,9 +345,19 @@ for ui in bitcoin-ui lnd-ui electrs-ui; do dst_hash=$( (cd "$dst" && find . -type f | LC_ALL=C sort | xargs sha256sum 2>/dev/null) | sha256sum | cut -d' ' -f1) fi if [ "$src_hash" != "$dst_hash" ]; then - log "UI source changed for $ui; syncing and marking for rebuild" + log "UI source changed for $ui; syncing" sudo rsync -a --delete "$src/" "$dst/" - UI_REBUILD_LIST="$UI_REBUILD_LIST $ui" + case "$ui" in + # Rebuilt below from container-specs.sh. + bitcoin-ui|lnd-ui|electrs-ui) + UI_REBUILD_LIST="$UI_REBUILD_LIST $ui" ;; + # Synced only — rebuilt by the daemon (companion installer / + # orchestrator build context), which watches this directory. + # Adding them to the rebuild list would fail: no spec exists and + # the container names are not archy-. + *) + log " $ui synced; rebuild is owned by the daemon" ;; + esac else ok "UI source unchanged for $ui" fi