← back to Dw Yolo Loop
deploy-vendor.sh: gate -internal hosts by default (TK-11391 root cause)
4c101204851ed08de08a543f76fa7df71f21a68e · 2026-09-10 11:27:14 -0700 · Steve Abrams
This template emitted an nginx vhost with NO auth_basic at all, so every microsite it
deployed came up publicly browsable. That is how 65 "-internal" hosts ended up ungated —
including phillip-jeffries-internal, which served 4,377 SKUs of a SHOWROOM-ONLY line to
anyone. Gating the 64 by hand fixes the instances; this fixes the source.
Gate is chosen from the fleet's own naming convention: an "-internal" host is a trade/spec
viewer and gets auth_basic + /etc/nginx/.htpasswd-dwvendors (same realm as the other ~453);
a public vendor landing (artmura, grasscloth) does not. GATE=1 / GATE=0 override either way.
Refuses to deploy an internal host at all if the htpasswd file is missing on the target,
rather than silently shipping it ungated. Adds a post-deploy edge check that warns loudly
if a host meant to be gated answers anything but 401/403.
Verified by rendering the template for all three cases: -internal gated, public landing
open, GATE=1 forces gating on a public name. bash -n clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M artmura-site/deploy-vendor.sh
Diff
commit 4c101204851ed08de08a543f76fa7df71f21a68e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 11:27:14 2026 -0700
deploy-vendor.sh: gate -internal hosts by default (TK-11391 root cause)
This template emitted an nginx vhost with NO auth_basic at all, so every microsite it
deployed came up publicly browsable. That is how 65 "-internal" hosts ended up ungated —
including phillip-jeffries-internal, which served 4,377 SKUs of a SHOWROOM-ONLY line to
anyone. Gating the 64 by hand fixes the instances; this fixes the source.
Gate is chosen from the fleet's own naming convention: an "-internal" host is a trade/spec
viewer and gets auth_basic + /etc/nginx/.htpasswd-dwvendors (same realm as the other ~453);
a public vendor landing (artmura, grasscloth) does not. GATE=1 / GATE=0 override either way.
Refuses to deploy an internal host at all if the htpasswd file is missing on the target,
rather than silently shipping it ungated. Adds a post-deploy edge check that warns loudly
if a host meant to be gated answers anything but 401/403.
Verified by rendering the template for all three cases: -internal gated, public landing
open, GATE=1 forces gating on a public name. bash -n clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
artmura-site/deploy-vendor.sh | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/artmura-site/deploy-vendor.sh b/artmura-site/deploy-vendor.sh
index 5818de1..38f2626 100644
--- a/artmura-site/deploy-vendor.sh
+++ b/artmura-site/deploy-vendor.sh
@@ -47,13 +47,36 @@ rsync -az "$DATA" "$HANDLES" "$KAM:$APP/_data/"
echo "→ install + pm2 (BUNDLE=1, VENDOR=$VENDOR, port $PORT)"
ssh "$KAM" "cd $APP && npm install --omit=dev --silent; pm2 delete $PM2 2>/dev/null; cd $APP && VENDOR=$VENDOR BUNDLE=1 PORT=$PORT pm2 start server.js --name $PM2 && pm2 save"
+# TK-11391 — gate by default on the fleet's own naming convention. An "-internal" host is
+# a trade/spec viewer and must sit behind the same Basic Auth as the other ~453 of them; a
+# public vendor LANDING (artmura, grasscloth) must not. This template previously emitted NO
+# auth_basic at all, which is how 65 "-internal" hosts ended up publicly browsable — including
+# phillip-jeffries-internal, a showroom line. Override either way with GATE=1 / GATE=0.
+HTPASSWD="${HTPASSWD:-/etc/nginx/.htpasswd-dwvendors}"
+REALM="${REALM:-Designer Wallcoverings — Trade Lines (Restricted)}"
+case "${GATE:-auto}" in
+ 1) DO_GATE=1 ;;
+ 0) DO_GATE=0 ;;
+ *) case "$DOMAIN" in *-internal.*) DO_GATE=1 ;; *) DO_GATE=0 ;; esac ;;
+esac
+if [ "$DO_GATE" = 1 ]; then
+ ssh "$KAM" "[ -f '$HTPASSWD' ]" || { echo "ABORT: $HTPASSWD missing on $KAM — refusing to deploy an internal host ungated"; exit 1; }
+ AUTH_BLOCK=" auth_basic \"$REALM\";
+ auth_basic_user_file $HTPASSWD;
+"
+ echo " gate: ON (auth_basic via $HTPASSWD)"
+else
+ AUTH_BLOCK=""
+ echo " gate: off (public landing)"
+fi
+
echo "→ nginx vhost"
ssh "$KAM" "cat > /etc/nginx/sites-available/$DOMAIN <<NGINX
server {
listen 80;
server_name $DOMAIN;
location / {
- proxy_pass http://127.0.0.1:$PORT;
+$AUTH_BLOCK proxy_pass http://127.0.0.1:$PORT;
proxy_set_header Host \\\$host;
proxy_set_header X-Real-IP \\\$remote_addr;
proxy_set_header X-Forwarded-For \\\$proxy_add_x_forwarded_for;
@@ -69,4 +92,13 @@ ssh "$KAM" "certbot --nginx -d $DOMAIN --non-interactive --agree-tos -m steve@de
echo "→ smoke test"
ssh "$KAM" "curl -s -o /dev/null -w 'local pm2 → %{http_code}\n' http://127.0.0.1:$PORT/api/config"
+if [ "$DO_GATE" = 1 ]; then
+ echo "→ gate check (public edge must be 401, not 200)"
+ code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "https://$DOMAIN/" 2>/dev/null)
+ case "$code" in
+ 401|403) echo " OK — $DOMAIN is gated ($code)" ;;
+ 000) echo " (edge not reachable yet — DNS/cert pending; re-check after certbot)" ;;
+ *) echo " !! WARNING — $DOMAIN returned $code, expected 401. An internal host is PUBLIC." ;;
+ esac
+fi
echo "DONE. Ensure Cloudflare DNS: A $DOMAIN → 45.61.58.125 (proxied)."
← 5b0f860 Guard the vendor-microsite generator against showroom-only l
·
back to Dw Yolo Loop
·
TK-11860: sync showroom-vendors snapshot to canonical fix-li 0efe7cc →