From fe1451f570d7bedf5899134bad53552a92d0dc31 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 11 May 2026 13:12:05 +0200 Subject: [PATCH] ci(smoke): pin curl to 127.0.0.1 via --resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The smoke step previously curled the public hostname unconditionally, which routes the runner's request via DNS → router → back into the same host. Many SOHO routers do not implement hairpin NAT (or do so only after a firmware update), so the deploy may pass on day one and silently fail on day 90. --resolve ":443:127.0.0.1" pins the hostname to the runner's loopback while keeping SNI on the public name (so the cert validates correctly and the Caddy vhost block matches). The smoke test now verifies that the Caddy-on-the-same-host is serving the right hostname end-to-end, with no router dependency. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/nightly.yml | 21 ++++++++++++++------- .gitea/workflows/release.yml | 14 +++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml index cbf10d39..fa343eb4 100644 --- a/.gitea/workflows/nightly.yml +++ b/.gitea/workflows/nightly.yml @@ -93,15 +93,22 @@ jobs: - name: Smoke test deployed environment # Healthchecks confirm containers are healthy; they do NOT confirm the - # public surface works. This step catches: Caddy not reloaded, DNS - # missing, HSTS header dropped, /actuator block bypassed. + # public surface works. This step catches: Caddy not reloaded, HSTS + # header dropped, /actuator block bypassed. + # + # --resolve pins staging.raddatz.cloud to the runner's loopback so we + # do NOT depend on the host router doing hairpin NAT (many SOHO + # routers do not, or do so only after a firmware update). SNI still + # uses the public hostname so the cert validates correctly. run: | set -e - URL="https://staging.raddatz.cloud" - echo "Smoke test: $URL" - curl -fsS --max-time 10 "$URL/login" -o /dev/null - curl -fsS --max-time 10 -I "$URL/" | grep -qi 'strict-transport-security' - status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health") + HOST="staging.raddatz.cloud" + URL="https://$HOST" + RESOLVE="--resolve $HOST:443:127.0.0.1" + echo "Smoke test: $URL (pinned to 127.0.0.1)" + curl -fsS $RESOLVE --max-time 10 "$URL/login" -o /dev/null + curl -fsS $RESOLVE --max-time 10 -I "$URL/" | grep -qi 'strict-transport-security' + status=$(curl -s $RESOLVE -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health") [ "$status" = "404" ] || { echo "expected 404 from /actuator/health, got $status"; exit 1; } echo "All smoke checks passed" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 9ae74ad6..e1eeca2c 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -92,13 +92,17 @@ jobs: - name: Smoke test deployed environment # See nightly.yml — same three checks, against the prod vhost. + # --resolve pins archiv.raddatz.cloud to the runner's loopback so + # the smoke test does NOT depend on hairpin NAT on the host router. run: | set -e - URL="https://archiv.raddatz.cloud" - echo "Smoke test: $URL" - curl -fsS --max-time 10 "$URL/login" -o /dev/null - curl -fsS --max-time 10 -I "$URL/" | grep -qi 'strict-transport-security' - status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health") + HOST="archiv.raddatz.cloud" + URL="https://$HOST" + RESOLVE="--resolve $HOST:443:127.0.0.1" + echo "Smoke test: $URL (pinned to 127.0.0.1)" + curl -fsS $RESOLVE --max-time 10 "$URL/login" -o /dev/null + curl -fsS $RESOLVE --max-time 10 -I "$URL/" | grep -qi 'strict-transport-security' + status=$(curl -s $RESOLVE -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health") [ "$status" = "404" ] || { echo "expected 404 from /actuator/health, got $status"; exit 1; } echo "All smoke checks passed"