diff --git a/.claude/skills/unifi-wifi/SKILL.md b/.claude/skills/unifi-wifi/SKILL.md index 8cd77e6b..db8ba4b4 100644 --- a/.claude/skills/unifi-wifi/SKILL.md +++ b/.claude/skills/unifi-wifi/SKILL.md @@ -48,6 +48,10 @@ path is Cascades — override with the script's vault-path arg per client. - **[WORKING] pfSense gateway compatibility layer via SSH** — `scripts/pfsense-ssh.sh `. DECISION (Mike 2026-06-16): **no RESTAPI package needed** — VPN + SSH shell reads the same data and makes changes. Cred = `clients//pfsense-firewall` (host + admin user/pass), system OpenSSH via askpass. + **Cred path (Mike 2026-06-21, option A):** the 1st arg is a **full vault path** if it contains `/` + (any infra-vaulted device, e.g. `pfsense-ssh.sh infrastructure/pfsense-firewall audit`), else a client + slug -> `clients//pfsense-firewall`. A path that resolves to no cred fails loud (`[ERROR] no cred + at vault:`). gw-audit/gw-control's `--pfsense` inherits the same convention. SSH port: `--port N` flag > optional vault `port`/`credentials.port` field > default 22 (e.g. the ACG office box on 2248 — store `port: 2248` in its vault entry). Validated live on Cascades (pfSense Plus 25.07). - **Reads (no gate):** `audit` (WAN/DHCP/states/DNS/NIC health), `dhcp` (pool pressure), `pf-list` diff --git a/.claude/skills/unifi-wifi/references/ROADMAP.md b/.claude/skills/unifi-wifi/references/ROADMAP.md index 69a79734..00bb0915 100644 --- a/.claude/skills/unifi-wifi/references/ROADMAP.md +++ b/.claude/skills/unifi-wifi/references/ROADMAP.md @@ -140,6 +140,11 @@ Writes (DRY-RUN default; `--apply` to commit — `write_config` + `filter_config config history. Cred = `clients//pfsense-firewall` (host + admin user/pass), system OpenSSH via askpass. - [x] **SSH port configurable** (2026-06-21): `--port N` > vault `port`/`credentials.port` field > default 22. Unblocks non-standard-port boxes like the ACG office gateway (2248) — store `port: 2248` in its vault entry. +- [x] **Cred-path convention = option A** (Mike 2026-06-21): the slug arg is a FULL vault path when it + contains `/` (any infra-vaulted device, e.g. `infrastructure/pfsense-firewall`), else `clients//pfsense-firewall`. + No cred duplication; explicit (no implicit fallback ladder). Resolves loud (`[ERROR] no cred at `) on a + bad path. `gw-audit`/`gw-control` `--pfsense` inherits it. So the ACG office box is now reachable: + `gw-audit '' --pfsense infrastructure/pfsense-firewall` (+ `port: 2248` in that entry). - [x] **Dispatch rewired:** `gw-control.sh` / `gw-audit.sh` now prefer the SSH backend (keyed on `clients//pfsense-firewall`) and route the same verbs to it; dispatch runs BEFORE UOS site resolution so a pfSense-only slug works. REST path is the dormant fallback. @@ -154,8 +159,8 @@ as a dormant alternative (works if a site ever installs the pkg) but is no longe classified UniFi-gateway (model) vs no-UniFi-gateway (pfSense/third-party candidate), plus the list of vaulted `clients/*/pfsense-firewall` creds with resolved host:port (SSH-backend-ready). Generated live (always current), not a static file. Today: 12 UniFi-gw / 36 no-UniFi-gw sites; 1 pfSense cred (Cascades). - - [ ] **Auto-select from the map** (driver picks the pfSense cred for a site WITHOUT `--pfsense`): needs a - UOS-site-name→slug binding + the cred-path convention (PARKED on Mike's clients/ vs infrastructure/ answer). + - [ ] **Auto-select from the map** (driver picks the pfSense cred for a site WITHOUT `--pfsense`): cred-path + convention now settled (option A, above) — remaining blocker is just a persisted UOS-site-name→cred binding. - [ ] **VPN convergence:** the "Deeper VPN — gateway-hosted VPN server" item (C) is *easier and better* on pfSense (WireGuard/OpenVPN) than on a USG — fold the Grabb-style "retire Windows RRAS PPTP → gateway VPN" play into the pfSense driver from the start. diff --git a/.claude/skills/unifi-wifi/scripts/gw-audit.sh b/.claude/skills/unifi-wifi/scripts/gw-audit.sh index 41eb8a96..79723c91 100644 --- a/.claude/skills/unifi-wifi/scripts/gw-audit.sh +++ b/.claude/skills/unifi-wifi/scripts/gw-audit.sh @@ -103,16 +103,22 @@ PY # pfSense gateway/WAN/DHCP audit via the backend so one `gw-audit ` covers either gateway vendor. NGW="$(cat "$TMP/ngw" 2>/dev/null || echo 1)" if [ "$NGW" = "0" ]; then - # PREFERRED: SSH backend (Mike's 2026-06-16 decision), keyed on clients//pfsense-firewall. - ssh_slug="" - for s in "$PFARG" "$SITEARG"; do - [ -n "$s" ] || continue - case "$s" in */*) continue;; esac - if [ -n "$(bash "$VAULT" get-field "clients/$s/pfsense-firewall" credentials.password 2>/dev/null)" ]; then ssh_slug="$s"; break; fi - done - if [ -n "$ssh_slug" ]; then - echo; echo "[INFO] pfSense gateway (SSH cred vault:clients/$ssh_slug/pfsense-firewall) -> pfSense gateway audit:" - bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "$ssh_slug" audit || true + # PREFERRED: SSH backend (Mike 2026-06-16). Target = full vault path if --pfsense contains '/' + # (option A, Mike 2026-06-21), else a client slug -> clients//pfsense-firewall. + ssh_target="" + case "$PFARG" in + */*) if [ -n "$(bash "$VAULT" get-field "$PFARG" credentials.password 2>/dev/null || bash "$VAULT" get-field "$PFARG" password 2>/dev/null)" ]; then ssh_target="$PFARG"; fi ;; + esac + if [ -z "$ssh_target" ]; then + for s in "$PFARG" "$SITEARG"; do + [ -n "$s" ] || continue + case "$s" in */*) continue;; esac + if [ -n "$(bash "$VAULT" get-field "clients/$s/pfsense-firewall" credentials.password 2>/dev/null)" ]; then ssh_target="$s"; break; fi + done + fi + if [ -n "$ssh_target" ]; then + echo; echo "[INFO] pfSense gateway (SSH backend, target '$ssh_target') -> pfSense gateway audit:" + bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "$ssh_target" audit || true else # REST fallback (dormant): only if a pfSense-api cred is vaulted. cands=() diff --git a/.claude/skills/unifi-wifi/scripts/gw-control.sh b/.claude/skills/unifi-wifi/scripts/gw-control.sh index cf41e33d..5f52c48d 100644 --- a/.claude/skills/unifi-wifi/scripts/gw-control.sh +++ b/.claude/skills/unifi-wifi/scripts/gw-control.sh @@ -47,15 +47,22 @@ done # so the SAME verb (pf-*/fw-*/block-ips) routes to a pfSense backend here. PREFERRED = SSH backend # (Mike's 2026-06-16 decision: no REST package needed), keyed on a vaulted clients//pfsense-firewall # cred. The REST backend (pfsense-backend.sh, clients//pfsense-api) is kept only as a dormant fallback. -ssh_slug="" -for s in "$PFARG" "$SITEARG"; do - [ -n "$s" ] || continue - case "$s" in */*) continue;; esac # SSH backend takes a slug, not a vault path - if [ -n "$(bash "$VAULT" get-field "clients/$s/pfsense-firewall" credentials.password 2>/dev/null)" ]; then ssh_slug="$s"; break; fi -done -if [ -n "$ssh_slug" ]; then - echo "[INFO] pfSense gateway (SSH cred vault:clients/$ssh_slug/pfsense-firewall) -> dispatching '$ACT' to pfsense-ssh.sh" - args=("$ssh_slug" "$ACT"); [ ${#POS[@]} -gt 0 ] && args+=("${POS[@]}") +ssh_target="" +# option A (Mike 2026-06-21): --pfsense may be a FULL vault path (contains '/') for an infra-vaulted +# device, else a client slug -> clients//pfsense-firewall. pfsense-ssh.sh resolves both. +case "$PFARG" in + */*) if [ -n "$(bash "$VAULT" get-field "$PFARG" credentials.password 2>/dev/null || bash "$VAULT" get-field "$PFARG" password 2>/dev/null)" ]; then ssh_target="$PFARG"; fi ;; +esac +if [ -z "$ssh_target" ]; then + for s in "$PFARG" "$SITEARG"; do + [ -n "$s" ] || continue + case "$s" in */*) continue;; esac + if [ -n "$(bash "$VAULT" get-field "clients/$s/pfsense-firewall" credentials.password 2>/dev/null)" ]; then ssh_target="$s"; break; fi + done +fi +if [ -n "$ssh_target" ]; then + echo "[INFO] pfSense gateway (SSH backend, target '$ssh_target') -> dispatching '$ACT' to pfsense-ssh.sh" + args=("$ssh_target" "$ACT"); [ ${#POS[@]} -gt 0 ] && args+=("${POS[@]}") [ "$APPLY" = "1" ] && args+=(--apply) exec bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "${args[@]}" fi diff --git a/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh b/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh index 3a00c437..8e15b311 100644 --- a/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh +++ b/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh @@ -37,7 +37,7 @@ GWC_PHP="$HERE/pfsense-gwc.php" # bad args, site not found). Soft-fails so it never breaks the caller. SKILL_ID="unifi-wifi/pfsense-ssh" logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "$SKILL_ID" "$1" --context "${2:-}" >/dev/null 2>&1 || true; } -SLUG="${1:?usage: pfsense-ssh.sh [args] [--apply]}" +SLUG="${1:?usage: pfsense-ssh.sh [args] [--apply]}" ACT="${2:?action: audit|dhcp|pf-list|fw-list|pf-*|fw-*|block-ips|unblock|showblock|run|shell}"; shift 2 || true APPLY=0; BLOCK_IF="wan"; PORT=""; POS=() while [ $# -gt 0 ]; do case "$1" in @@ -46,7 +46,9 @@ while [ $# -gt 0 ]; do case "$1" in --port) PORT="${2:?--port needs a number}"; shift 2;; *) POS+=("$1"); shift;; esac; done -VP="clients/$SLUG/pfsense-firewall" +# Cred path (Mike 2026-06-21, option A): arg containing '/' = a full vault path (any infra/-vaulted +# device, e.g. infrastructure/pfsense-firewall); else clients//pfsense-firewall. +case "$SLUG" in */*) VP="$SLUG" ;; *) VP="clients/$SLUG/pfsense-firewall" ;; esac HOST="$(bash "$VAULT" get-field "$VP" host 2>/dev/null || bash "$VAULT" get-field "$VP" credentials.host 2>/dev/null || true)" U="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null || true)" PP="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null || true)"; export PP @@ -59,8 +61,12 @@ if [ -z "$PORT" ]; then done fi PORT="${PORT:-22}" +# Fail loud (Mike's add): a wholly-empty resolve = path typo/missing -> clear [ERROR], not a confusing +# SSH failure later. A partial resolve = the entry exists but is missing a field. +if [ -z "$HOST" ] && [ -z "$U" ] && [ -z "$PP" ]; then + echo "[ERROR] no cred at vault:$VP (path not found or empty — check the slug/vault-path)"; exit 2; fi if [ -z "$HOST" ] || [ -z "$U" ] || [ -z "$PP" ]; then - echo "[BLOCKED] need host + admin creds at vault:$VP (fields: host, credentials.username, credentials.password)"; exit 2; fi + echo "[BLOCKED] incomplete cred at vault:$VP (need fields: host, credentials.username, credentials.password)"; exit 2; fi if [ "$ACT" = "shell" ]; then echo "ssh -p ${PORT} ${U}@${HOST} # password in vault:$VP"; exit 0; fi TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT diff --git a/errorlog.md b/errorlog.md index 61ffc6f6..fd52dbfa 100644 --- a/errorlog.md +++ b/errorlog.md @@ -17,6 +17,8 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure · +2026-06-21 | Howard-Home | unifi-wifi/pfsense-ssh | SSH connect/auth failed (rc=255) [ctx: host=192.168.0.1:22 slug=clients/cascades-tucson/pfsense-firewall act=showblock] + 2026-06-21 | Howard-Home | unifi-wifi/pfsense-ssh | SSH connect/auth failed (rc=255) [ctx: host=192.168.0.1:9999 slug=cascades-tucson act=showblock] 2026-06-21 | Howard-Home | unifi-wifi/pfsense-ssh | SSH connect/auth failed (rc=255) [ctx: host=192.168.0.1:22 slug=cascades-tucson act=showblock] diff --git a/wiki/systems/pfsense.md b/wiki/systems/pfsense.md index b3295fa7..eed4569f 100644 --- a/wiki/systems/pfsense.md +++ b/wiki/systems/pfsense.md @@ -99,11 +99,12 @@ The REST backend (`pfsense-backend.sh`, `clients//pfsense-api`) is a dorma `pfsense-firewall` cred) and run the dispatch BEFORE UOS site resolution, so a pfSense-only client slug works without a matching UOS site name (pass `--pfsense ` if the names differ). -**THIS office box:** listens on SSH **port 2248** (not 22). The skill supports non-default ports as -of 2026-06-21 — pass `--port 2248`, or (preferred) store `port: 2248` in the box's vault entry and -it's automatic. Cred for it is vaulted at `infrastructure/pfsense-firewall` (verify), but the SSH -backend expects the cred at `clients//pfsense-firewall`, so an `infrastructure/`-path cred -would need a slug alias or a small path tweak before `pfsense-ssh.sh` can read it (verify). +**THIS office box:** SSH **port 2248** (not 22). **Fully reachable by the skill as of 2026-06-21.** +Cred vaulted at `infrastructure/pfsense-firewall` (verify) — pass it as a **full vault path** +(option A, Mike 2026-06-21: a 1st arg containing `/` is a vault path, not a client slug), e.g. +`pfsense-ssh.sh infrastructure/pfsense-firewall audit` or +`gw-audit '' --pfsense infrastructure/pfsense-firewall`. Add `port: 2248` to that vault entry +so the non-standard port is automatic (or pass `--port 2248`). No cred duplication needed. **pfSense PHP gotchas** (baked into the scripts; carry forward to any new helper): - Bootstrap with `require_once("config.inc")` ONLY — re-requiring util/functions/filter → "cannot