From 106e61cfbb818973a1f6ca469b396e3dbb54166c Mon Sep 17 00:00:00 2001 From: Malin Date: Fri, 7 Aug 2026 19:44:23 +0200 Subject: [PATCH] docs: document NPM dedicated-port jail convention and zsh $path footgun Two real gotchas from today's goddy->gringo Laravel migration: every jail on an NPM-fronted host needs a dedicated listen port (8000+last-octet), not just the shared port 80, or NPM cutover silently can't reach it even though every direct/Host-header test passes. Separately, zsh ties a variable literally named `path` to $PATH itself - overwriting it broke every subsequent command in a config-editing script with no clear error. Co-Authored-By: Claude Sonnet 5 --- skills/bastille-jail-provisioning/SKILL.md | 39 +++++++++++++++++++++ skills/remote-shell-quoting-safety/SKILL.md | 20 +++++++++++ 2 files changed, 59 insertions(+) diff --git a/skills/bastille-jail-provisioning/SKILL.md b/skills/bastille-jail-provisioning/SKILL.md index a0b3c11..3abd73c 100644 --- a/skills/bastille-jail-provisioning/SKILL.md +++ b/skills/bastille-jail-provisioning/SKILL.md @@ -130,6 +130,45 @@ curl -H 'Host: ' http:/// Only switch to testing via the host's `127.0.0.1` (or the public domain) once the `sites-enabled` vhost for this specific site actually exists. +## Every jail needs a dedicated NPM-facing port, not just `listen 80` + +**Confirmed real gap, 2026-08-07**: on jail hosts fronted by a shared +Nginx Proxy Manager (NPM) instance, every existing jail's host-level +nginx vhost has **two** `listen` directives — the shared port 80 (for +same-host/Host-header testing) **and a dedicated port unique to that +jail**, following the convention `8000 + ` (e.g. a +jail at `10.20.0.43` gets `listen 8043;`, one at `.50` gets `listen +8050;`). This dedicated port is what NPM's own proxy-host config actually +targets when routing the public domain to this jail host — **NPM does +not rely on Host-header-based routing through the shared port 80 for +this fleet**, it connects to a specific port per site. + +This was missed migrating 5 Laravel apps from a Linux host to a FreeBSD +jail host (gringo) in one session: all 5 new vhosts were created with +only `listen 80;`, matching the *shared* port but missing the dedicated +one entirely. Every functional test the delegate ran (raw jail IP, and +via the shared port 80 with the right `Host:` header) passed cleanly — +the gap was invisible until specifically checked against sibling jails' +configs, at which point every other jail on the host turned out to +follow the two-port pattern without exception. **Before considering any +new jail's nginx vhost complete, diff its `listen` directives against at +least one working sibling jail's config on the same host** — don't just +confirm the site loads via a manual test, confirm the *routing surface* +matches the established convention, since NPM (external, not scriptable +from this sandbox) is what will actually determine reachability once +cut over, and a working manual test doesn't prove NPM's real path works. + +```bash +# find the convention on any host that uses it +for f in /usr/local/etc/nginx/sites-available/*.conf; do + echo "=== $(basename $f) ==="; grep -E '^\s*listen' "$f" +done +``` + +If a host doesn't show this pattern on ANY existing jail, it likely +doesn't use per-jail dedicated ports — don't assume the convention +applies fleet-wide without checking the specific host first. + ## Stock stack includes Valkey, not just nginx/php/MariaDB The fleet's standard per-host stack (see `docs/server-funky.md`) is diff --git a/skills/remote-shell-quoting-safety/SKILL.md b/skills/remote-shell-quoting-safety/SKILL.md index 705b60c..b868b14 100644 --- a/skills/remote-shell-quoting-safety/SKILL.md +++ b/skills/remote-shell-quoting-safety/SKILL.md @@ -60,6 +60,26 @@ decode step per real filesystem boundary crossed is enough. special characters — check for quoting corruption before looking elsewhere. +## zsh footgun: never name a variable `path` (or a few other reserved names) + +If the remote host's shell is zsh (several boxes in this fleet default to +it), assigning a plain string to a variable literally named `path` does +not create a normal local variable — zsh ties the lowercase `path` array +to `$PATH` (colon-joined ⇄ array elements) automatically. Overwriting it +with a single string **replaces `$PATH` itself**, and every subsequent +command in that shell (including ones on later lines of the same script) +fails with `command not found: cp` / `command not found: awk` / etc. — +confirmed live 2026-08-07 editing nginx configs over SSH into a zsh +remote shell, where a loop using `path="/usr/local/etc/nginx/..."` broke +every command after the assignment with no explanation beyond +`command not found`. **Rename to anything else** (`cfgpath`, `target`, +`dest`, ...) — this isn't specific to nginx configs, it'll happen with +any script that happens to pick `path` as a variable name on a zsh +remote. A few other zsh-reserved lowercase names exist for the same +reason (`status`, `pipestatus`) — when in doubt, avoid single common +English words as variable names in scripts meant to run under an +unknown/remote shell, or check `typeset -p ` first if unsure. + ## What NOT to do Don't respond to a quoting failure by adding more layers of escaping