diff --git a/AGENTS.md b/AGENTS.md index c540882..f82be42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,18 @@ first image is `wordpress` (WordPress served by Caddy on PHP-FPM). - `/healthz` is a dedicated PHP-backed endpoint that returns `200`, reached by the container's own Docker/Coolify health check from `localhost`. HTTP Basic Auth is enforced **by the image** (Caddy) from the `WORKSPACE_AUTH_*` - secret env vars (portal-plugin-ipfs PR #1031) on requests from **external** - (public-address) peers; loopback and private-network (RFC1918/ULA) peers - bypass it via Caddy's `remote_ip` (real peer, never spoofable forwarding - headers). Missing/invalid `WORKSPACE_AUTH_*` fails the container closed. Do - not weaken this to trust forwarding headers or to add an auth-disabled mode. + secret env vars (portal-plugin-ipfs PR #1031) on requests whose **resolved + client IP** is external (public-range); loopback and private-network + (RFC1918/ULA) resolved clients bypass it via Caddy's `client_ip` (resolved + through `trusted_proxies`: the private-range Coolify proxy's + `X-Forwarded-For`, or the raw peer when there is no forwarded chain — so a + proxied public client still authenticates while in-network peers and the + localhost health probe stay exempt). Direct spoofed forwarding headers from a + public peer are ignored (untrusted peer). Missing/invalid + `WORKSPACE_AUTH_*` fails the container closed. Do not weaken this to an + auth-disabled mode or to a peer-IP-based (`remote_ip`) bypass — that would + exempt the private-range proxy peer and silently disable auth for the whole + internet. - The **Cast** plugin is platform-managed: baked from the latest GitHub `develop` tree snapshot (Composer deps vendored at build; not checksum-pinned yet) and force-kept active by an image-owned MU plugin. Plugin/theme editing diff --git a/README.md b/README.md index 9f44d37..0c36919 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,15 @@ it as a named context (`contexts.base = target:php-caddy`). - **`/healthz`**: dedicated PHP-backed health probe, **loopback-exempt** (the container's Docker health check reaches it from `localhost` without auth). - **HTTP Basic Auth**: enforced by the image from the `WORKSPACE_AUTH_USERNAME` - / `WORKSPACE_AUTH_PASSWORD` secret env vars on all requests from **external** - (public-address) peers (portal-plugin-ipfs PR #1031) — not by the Coolify - proxy. Bypass keys on the real peer IP (`remote_ip`) for loopback and - private-network (RFC1918/ULA) peers — in-deployment probes (e.g. Cast's - anonymous export probe hairpinning through the published port) and the health - check stay unauthenticated; never spoofable forwarding headers; missing - credentials fail the container closed. + / `WORKSPACE_AUTH_PASSWORD` secret env vars on all requests whose **resolved + client IP** is external/public (portal-plugin-ipfs PR #1031) — including + every proxied request where the Coolify proxy forwards a public-range + `X-Forwarded-For` — not by the Coolify proxy. Bypass keys on the + private-range resolved client IP via Caddy's `client_ip` through + `trusted_proxies`: in-deployment probes (e.g. Cast's anonymous export probe + hairpinning through the published port) and the health check stay + unauthenticated; a public-range peer's spoofed forwarding headers are + ignored (untrusted); missing credentials fail the container closed. - DB via `WORDPRESS_DB_HOST/PORT/NAME/USER/PASSWORD`; public URL via `COOLIFY_URL` (sets `WP_HOME`/`WP_SITEURL` and drives `wp core install`); `X-Forwarded-*` drives HTTPS-correct links. diff --git a/images/php-caddy/Caddyfile b/images/php-caddy/Caddyfile index 899d3eb..9d380b4 100644 --- a/images/php-caddy/Caddyfile +++ b/images/php-caddy/Caddyfile @@ -13,21 +13,34 @@ # is never baked into the image, written to disk, or echoed to logs — it lives # only transiently in the process environment. # -# Loopback + private-network exemption: direct requests whose real peer IP is +# Loopback + private-network exemption: requests whose resolved client IP is # 127.0.0.1, ::1, or an RFC1918/ULA private address bypass auth. Loopback # covers the container's own Docker/Coolify health probe -# (curl localhost:${PORT}/healthz). Private ranges cover in-deployment peers -# that the published-port hairpin NATs (e.g. the bridge gateway when a service -# inside the deployment's Docker network requests the public URL, as Cast's -# anonymous export probe and capture fetches do via wp_remote_get(home_url)). -# Exposing the site to such in-network peers is the existing contract: TLS and -# public ingress already terminate on a private-network peer (the Coolify -# proxy), so "protect from the internet" is the threat model, not -# "protect from the deployment's own network". The bypass keys on the actual -# TCP peer via Caddy's remote_ip matcher — NEVER on spoofable -# X-Forwarded-For / X-Real-IP headers, which this matcher ignores by default -# (trusted_proxies below influences only forwarded-header handling, never the -# remote_ip matcher). +# (curl localhost:${PORT}/healthz), which sends no forwarded headers. Private +# ranges cover in-deployment peers: the upstream Coolify proxy connects from a +# private address and forwards each public client's real (public-range) IP in +# X-Forwarded-For, while in-deployment services that request the public URL +# (as Cast's anonymous export probe and capture fetches do via +# wp_remote_get(home_url)) arrive either with a private-range XFF hop or as a +# direct hairpinned private peer — all resolve to a private client IP. +# +# The bypass therefore keys on Caddy's client_ip matcher, which resolves the +# client IP through the trusted_proxies below (+ X-Forwarded-For), NOT on the +# raw TCP peer alone. This matters because the Coolify proxy's own private +# peer address would exempt ALL proxied traffic if the bypass keyed on +# remote_ip — public clients must still authenticate. +# +# Trust assumptions: private-range peers are trusted, so their forwarding +# headers resolve the client IP (a public-range client is never trusted, so a +# direct attacker's spoofed X-Forwarded-For / X-Real-IP is ignored and the +# real peer is used). trusted_proxies_strict (set in the global block above) +# is required for this to hold against proxied clients: proxies append to +# X-Forwarded-For, so the leftmost entry is client-controlled; strict parsing +# scans right-to-left and takes the first untrusted address, the one the proxy +# actually appended. The proxy is assumed to always set X-Forwarded-For +# (Traefik/Caddy, Coolify's proxies, do by default); an XFF-less proxy would +# resolve to its own private IP and be exempt, which is documented as an +# accepted part of the private-network contract. # # The workspace always runs behind the upstream Coolify proxy, which terminates # TLS and forwards plain HTTP on a private network. Caddy by default treats @@ -41,12 +54,25 @@ # private_ranges is as narrow as a generic image can get: the upstream proxy's # source IP varies per deployment (Podman/Docker bridge, overlay network), so # the exact CIDR cannot be pinned at build time. Trusting all private peers is -# an accepted risk: exploiting it means forging X-Forwarded-Proto/Host on a -# request that ALREADY originates from inside the deployment's private network, -# and the headers only influence scheme/host derivation, never authorization. +# an accepted risk: exploiting it means forging forwarded headers on a request +# that ALREADY originates from inside the deployment's private network — +# X-Forwarded-Proto/Host influence only scheme/host derivation, and a forged +# X-Forwarded-For value would resolve the client to a private address — except +# that trusted_proxies_strict parses right-to-left, so the proxy-appended +# rightmost address (never spoofable by an outside client) anchors the +# decision: a forged private leftmost entry cannot grant an outside client a +# bypass. { servers { trusted_proxies static private_ranges + # Parse X-Forwarded-For right-to-left (first untrusted IP) instead of + # the default left-to-right scan. With the default, a client behind the + # Coolify proxy could spoof a private-range leftmost XFF value (e.g. + # "X-Forwarded-For: 127.0.0.1") and have client_ip resolve to it, + # bypassing basic auth internet-wide. Strict parsing anchors on the + # rightmost, proxy-appended address, which no outside client controls. + # Available since Caddy v2.8; this image pins the current stable. + trusted_proxies_strict } } @@ -56,13 +82,16 @@ root * /var/www/html encode gzip - # Enforce HTTP Basic Auth on every request from outside the private - # network (including /healthz): the health path is only ever probed from - # localhost by the container health check, so a non-exempt /healthz is - # just another gated route. The username and derived hash names are provided by the + # Enforce HTTP Basic Auth on every request whose resolved client IP is + # outside the loopback/private set (including /healthz): the health path is + # only ever probed from localhost by the container health check, so a + # non-exempt /healthz is just another gated route. client_ip (not + # remote_ip) is required here: every proxied request's TCP peer is the + # private-range Coolify proxy, so a peer-based matcher would exempt the + # entire internet. The username and derived hash names are provided by the # auth setup step; missing/invalid WORKSPACE_AUTH_* fails the container # at start, so this block is always populated when Caddy runs. - @external not remote_ip 127.0.0.1 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 fc00::/7 + @external not client_ip 127.0.0.1 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 fc00::/7 basicauth @external { {$WORKSPACE_AUTH_USERNAME} {$WORKSPACE_AUTH_HASH} } diff --git a/images/php-caddy/README.md b/images/php-caddy/README.md index 3ed876c..30aaac5 100644 --- a/images/php-caddy/README.md +++ b/images/php-caddy/README.md @@ -2,7 +2,8 @@ Reusable base image: **PHP-FPM + Caddy**, the runtime every Pinner workspace image builds on. Non-root after startup, no integrated app logic. Enforces -workspace HTTP Basic Auth on public (non-loopback) requests. +workspace HTTP Basic Auth on requests whose resolved client IP is public +(proxied public clients included). | Aspect | Value | |---|---| @@ -26,28 +27,50 @@ the same values) and rebuild. Verify with `make deps-verify`. ## HTTP Basic Auth (portal-plugin-ipfs PR #1031) The Pinner portal injects two **secret** environment variables, and this image -enforces them with Caddy's `basicauth` on every request from an **external** -(public-address) peer: +enforces them with Caddy's `basicauth` on every request whose **resolved client +IP** is external (public-range): - `WORKSPACE_AUTH_USERNAME` - `WORKSPACE_AUTH_PASSWORD` Behavior: -- **External** requests (the actual TCP peer is a public address) must present - valid HTTP Basic Auth credentials, otherwise they get `401`. -- **Loopback** requests (real peer `127.0.0.1` or `::1`) and **private-network** +- **External** requests (resolved client IP is a public address) must present + valid HTTP Basic Auth credentials, otherwise they get `401`. This includes + every request forwarded by the Coolify proxy for a public client: the proxy + is a private-range trusted peer whose `X-Forwarded-For` carries the real + public client IP. Keying the bypass on the raw TCP peer instead would exempt + the proxy's own private-range peer address and silently disable auth for the + whole internet. +- **Loopback** requests (client IP `127.0.0.1` or `::1`) and **private-network** requests (RFC 1918 / ULA ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7`) bypass auth. Loopback covers the container's - own Docker/Coolify health probe of `/healthz`. Private-network coverage keeps - in-deployment consumers — the Coolify proxy's plain-HTTP hop, and any service - inside the deployment's Docker network that requests the workspace's *public* - URL and arrives hairpinned through the published port with the bridge - gateway as its peer (e.g. Cast's anonymous export probe and capture fetches - via `wp_remote_get(home_url('/'))`) — outside "the internet" in the auth + own Docker/Coolify health probe of `/healthz` (it sends no forwarded + headers, so its client IP is the loopback peer itself). Private-network + coverage keeps in-deployment consumers — the Coolify proxy's plain-HTTP hop + for private-range forwarded clients, and any service inside the deployment's + Docker network that requests the workspace's *public* URL (e.g. Cast's + anonymous export probe and capture fetches via + `wp_remote_get(home_url('/'))`, arriving with a private-range XFF hop or as + a direct hairpinned private peer) — outside "the internet" in the auth threat model. The portal itself never HTTP-probes the workspace URL. -- The bypass keys on Caddy's `remote_ip` matcher — the **real peer IP**, never - spoofable `X-Forwarded-For` / `X-Real-IP` headers. +- The bypass resolves the client IP with Caddy's `client_ip` matcher through + the `trusted_proxies` setting (private ranges): a private-range peer's + `X-Forwarded-For` determines the client IP, while a public-range peer is + untrusted, so its (attacker-controlled) `X-Forwarded-For` / `X-Real-IP` is + ignored and the real peer address is used. `trusted_proxies_strict` makes + the XFF chain parse right-to-left (first untrusted address, skipping the + trusted proxies that append to it): the leftmost entries are + client-controlled, so the default left-to-right parsing would let a proxied + client spoof `X-Forwarded-For: 127.0.0.1` and bypass auth internet-wide. A + client cannot gain a bypass in strict mode either — the proxy appends the + peer it actually saw, so the chain never *ends* in a spoofable private + value; forging a *public* XFF entry only moves the request into the + authenticated "external" set. +- Trust assumption: the deployment's proxy always sets `X-Forwarded-For` + (Coolify's Traefik/Caddy do by default). An XFF-less private proxy would + resolve to its own private IP and be exempt — an accepted part of the + private-network contract below. - **Fail closed**: if either `WORKSPACE_AUTH_*` var is missing/empty at start, the container refuses to start rather than serve the workspace publicly unauthenticated. This matches the portal's environment builder, which fails @@ -74,16 +97,21 @@ the global `servers > trusted_proxies static private_ranges` option: requests from the private-network proxy are trusted and their forwarded headers (`X-Forwarded-Proto` / `X-Forwarded-Host`) reach the application unmodified. This keeps WordPress `is_ssl()` correct behind TLS termination and avoids the -`force_ssl_admin()` login redirect loop. The trust setting does **not** relax -the Basic Auth bypass, which continues to key on the real peer IP (`remote_ip`), -never on forwarded headers. +`force_ssl_admin()` login redirect loop. The same trust decision is what makes +the Basic Auth bypass resolve the real client IP: a private-range peer's +forwarded headers honor `X-Forwarded-For`, while a public-range (untrusted) +peer's spoofed headers are ignored. `private_ranges` is as narrow as a generic image can be pinned to: the upstream proxy's source IP varies per deployment (Docker bridge / overlay network), so an exact proxy CIDR cannot be known at build time. Trusting all private-range peers is an accepted risk: an attacker would need to already sit on the deployment's -private network, and the forwarded headers only influence scheme/host -derivation in the application — never authorization. +private network. Forged `X-Forwarded-Proto` / `X-Forwarded-Host` influence only +scheme/host derivation in the application; a forged *private-range* +`X-Forwarded-For` can at most move a request from the authenticated +"in-network peer" case into the auth-exempt private set it would already fall +into, and a forged *public-range* one strictly requires more auth — never a +bypass for an outside client. ## Health diff --git a/images/wordpress/README.md b/images/wordpress/README.md index a9a35a7..6bd5ad6 100644 --- a/images/wordpress/README.md +++ b/images/wordpress/README.md @@ -221,11 +221,15 @@ the portal via `PORTAL_API_KEY` / `COOLIFY_URL`. Public HTTP Basic Auth is enforced **by the image's Caddy layer** from the `WORKSPACE_AUTH_USERNAME` / `WORKSPACE_AUTH_PASSWORD` secret env vars injected by the portal (portal-plugin-ipfs PR #1031) — it is no longer the Coolify -proxy's responsibility. All non-loopback requests require valid credentials -(`401` otherwise); loopback requests (real peer `127.0.0.1` / `::1`) bypass -auth so the container's Docker health check of `/healthz` passes. The bypass -never trusts spoofable `X-Forwarded-For` / `X-Real-IP`. Missing credentials -fail the container closed. See [`images/php-caddy/README.md`](../php-caddy/README.md). +proxy's responsibility. All requests with a public-range resolved client IP +require valid credentials (`401` otherwise) — including every request the +Coolify proxy forwards for a public client; loopback requests (client IP +`127.0.0.1` / `::1`) and private-network clients bypass auth so the container's +Docker health check of `/healthz` and in-deployment probes (e.g. Cast) pass. +The bypass resolves the client IP via Caddy's `client_ip` through +`trusted_proxies`: a public-range peer's spoofed `X-Forwarded-For` / +`X-Real-IP` is ignored (untrusted peer). Missing credentials fail the +container closed. See [`images/php-caddy/README.md`](../php-caddy/README.md). ## Security trade-off (privilege drop) diff --git a/scripts/verify-php-caddy.sh b/scripts/verify-php-caddy.sh index 1c3f767..46a43a9 100644 --- a/scripts/verify-php-caddy.sh +++ b/scripts/verify-php-caddy.sh @@ -3,9 +3,10 @@ # the workspace HTTP Basic Auth contract (portal-plugin-ipfs PR #1031), and # assert the final Caddy/PHP-FPM processes are non-root. # -# Auth contract under test (the bypass keys on the real TCP peer via Caddy's -# remote_ip matcher; loopback and private-network peers are in-deployment and -# exempt — see images/php-caddy/Caddyfile): +# Auth contract under test (the bypass resolves the client IP via Caddy's +# client_ip matcher through trusted_proxies + X-Forwarded-For; loopback and +# private-network resolved client IPs are in-deployment and exempt — see +# images/php-caddy/Caddyfile): # - public-range peer without credentials -> 401 (via the TEST-NET-1 # ${compose}_publictest network, so the peer is genuinely 192.0.2.x) # - valid credentials -> 200 / app access @@ -13,8 +14,15 @@ # - private-network peer (RFC1918/ULA) -> 200 with NO credentials # (in-deployment peer: the container's Docker network, and every # published-port hairpin NAT, falls under this case) +# - proxied public client (private-range proxy peer forwarding a +# public-range X-Forwarded-For) -> 401 with NO credentials: +# the production regression case — a peer-based bypass would exempt the +# whole internet because every proxied request's TCP peer is private # - loopback /healthz (127.0.0.1) -> 200 with NO credentials # - spoofed X-Forwarded-For / X-Real-IP never bypass auth on a public peer +# (public-range peer is not a trusted proxy, so its headers are ignored) +# - a trusted private peer forging a PUBLIC X-Forwarded-For can only ever +# require MORE auth, never gain a bypass # - IPv6 loopback (::1) bypass -> 200 when the environment supports it # - auth always required (no silent auth-disabled mode): the contract’s # environment builder fails closed, so there is deliberately no @@ -98,10 +106,56 @@ code="$(peer_curl "$PRIVATENET" "http://web:8080/healthz")" [ "$code" = "200" ] || die "private-peer no-auth expected 200, got $code" pass "private-network peer without credentials -> 200" +echo "== [php-caddy] proxied public client must NOT bypass auth (regression case) ==" +# Production topology: the Coolify proxy is a private-range peer that forwards +# each public client's real IP in X-Forwarded-For. Keying the bypass on the +# raw TCP peer (remote_ip) exempted ALL proxied traffic and silently disabled +# auth; the client_ip resolver through trusted_proxies must still require +# credentials when the forwarded client is a public-range address. +code="$(peer_curl "$PRIVATENET" -H 'X-Forwarded-For: 192.0.2.77' "http://web:8080/healthz")" +[ "$code" = "401" ] || die "proxied public client (private peer + public XFF) expected 401, got $code" +pass "private-range proxy peer forwarding public client in XFF -> 401 (no credentials)" + +code="$(peer_curl "$PRIVATENET" -H 'X-Forwarded-For: 192.0.2.77' -u "$AUTH_USER:$AUTH_PASS" "http://web:8080/healthz")" +[ "$code" = "200" ] || die "proxied public client with valid credentials expected 200, got $code" +pass "private-range proxy peer forwarding public client in XFF with valid credentials -> 200" + +code="$(peer_curl "$PRIVATENET" -H 'X-Forwarded-For: 192.0.2.77' -u "$AUTH_USER:wrongpass" "http://web:8080/healthz")" +[ "$code" = "401" ] || die "proxied public client with bad credentials expected 401, got $code" +pass "private-range proxy peer forwarding public client in XFF with bad credentials -> 401" + +echo "== [php-caddy] chained XFF with spoofed private prefix must not bypass ==" +# Coolify-style proxies APPEND to X-Forwarded-For, so a spoofed leftmost entry +# arrives as "127.0.0.1, ". Caddy's default left-to-right XFF +# parsing resolves client_ip to the spoofed 127.0.0.1 and bypasses auth +# internet-wide; trusted_proxies_strict parses right-to-left, skipping trusted +# proxies, and anchors on the rightmost (proxy-appended, attacker-controlled +# only through the proxy's honest append of the peer it saw) public address. +code="$(peer_curl "$PRIVATENET" -H 'X-Forwarded-For: 127.0.0.1, 192.0.2.77' "http://web:8080/healthz")" +[ "$code" = "401" ] || die "chained XFF (spoofed private prefix + public client) expected 401, got $code" +pass "private peer with chained XFF (spoofed 127.0.0.1 prefix + public client) -> 401" + +# A spoofed all-private chain resolves to a private address only because every +# entry is trusted, which an outside attacker cannot achieve: the real proxy +# appends the peer it actually saw, so no outside client can make the chain end +# in a private address served by the proxy. Assert the strict resolver's +# fallback: an all-trusted chain falls back to the private peer itself (exempt, +# as with Cast), while the mixed case above stays gated. +code="$(peer_curl "$PRIVATENET" -H 'X-Forwarded-For: 172.19.0.9, 172.19.0.10' -u "$AUTH_USER:wrongpass" "http://web:8080/healthz")" +[ "$code" = "200" ] || die "all-private chained XFF via private peer expected 200, got $code" +pass "private peer with all-private chained XFF -> 200 (in-network exemption)" + +echo "== [php-caddy] in-network private client still exempt ==" +# A trusted private-range peer whose XFF chain resolves to a private client +# (or that sends no XFF at all) stays exempt — Cast's anonymous probe contract. +code="$(peer_curl "$PRIVATENET" -H 'X-Forwarded-For: 172.19.0.9' "http://web:8080/healthz")" +[ "$code" = "200" ] || die "private client via private peer with private XFF expected 200, got $code" +pass "private-range peer with private-range XFF -> 200 (no credentials)" + echo "== [php-caddy] spoofed forwarding headers must not bypass auth ==" -# The bypass keys on Caddy's remote_ip (the real TCP peer of the request). A -# spoofed X-Forwarded-For/X-Real-IP claiming loopback must NOT exempt a -# public-range request, because its actual peer is 192.0.2.x. +# A public-range peer is not a trusted proxy, so Caddy's client_ip resolver +# ignores its forged X-Forwarded-For/X-Real-IP (claiming loopback) and keys on +# the real 192.0.2.x address: still 401. code="$(peer_curl "$PUBLICNET" -H 'X-Forwarded-For: 127.0.0.1' -H 'X-Real-IP: 127.0.0.1' "http://web:8080/healthz")" [ "$code" = "401" ] || die "spoofed XFF/X-Real-IP must not bypass auth, got $code" pass "spoofed X-Forwarded-For/X-Real-IP (claiming loopback) from public peer -> 401" @@ -113,11 +167,16 @@ code="$(docker exec "$container" curl -s -o /dev/null -w '%{http_code}' "http:// [ "$code" = "200" ] || die "loopback /healthz (127.0.0.1) expected 200, got $code" pass "loopback /healthz (127.0.0.1) returns 200 with NO credentials" -# Loopback is genuinely exempt regardless of spoofed headers (the matcher -# never inspects them); this asserts peer-IP-based bypass, not header trust. +# Loopback is a trusted peer, so a forwarded foreign PUBLIC client resolves to +# that public address and is correctly gated; the container health check never +# sends XFF, so the real probe path stays exempt (asserted above). code="$(docker exec "$container" curl -s -o /dev/null -w '%{http_code}' -H 'X-Forwarded-For: 8.8.8.8' -H 'X-Real-IP: 8.8.8.8' "http://127.0.0.1:8080/healthz")" -[ "$code" = "200" ] || die "loopback + foreign XFF expected 200, got $code" -pass "loopback /healthz + foreign X-Forwarded-For/X-Real-IP still 200 (peer-based bypass)" +[ "$code" = "401" ] || die "loopback + public-range forwarded client expected 401, got $code" +pass "loopback peer forwarding public client in XFF -> 401 (trusted-peer header resolution)" + +code="$(docker exec "$container" curl -s -o /dev/null -w '%{http_code}' -H 'X-Forwarded-For: 127.0.0.1' "http://127.0.0.1:8080/healthz")" +[ "$code" = "200" ] || die "loopback + loopback XFF expected 200, got $code" +pass "loopback /healthz + loopback X-Forwarded-For still 200 (private resolved client)" echo "== [php-caddy] IPv6 loopback (::1) bypass (if supported) ==" if docker exec "$container" curl -s -o /dev/null -w '%{http_code}' 'http://[::1]:8080/healthz' >/dev/null 2>&1; then