sync: auto-sync from HOWARD-HOME at 2026-06-21 12:00:27
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-21 12:00:27
This commit is contained in:
34
.claude/memory/rmm-dashboard-beta-before-main.md
Normal file
34
.claude/memory/rmm-dashboard-beta-before-main.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: rmm-dashboard-beta-before-main
|
||||||
|
description: GuruRMM website/dashboard changes must be tested on beta BEFORE being pushed to main, unless told otherwise
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
For the GuruRMM **website/dashboard**, Howard wants changes to land on the **beta
|
||||||
|
test site first** (`https://rmm-beta.azcomputerguru.com`) and be tested there
|
||||||
|
**before** the branch is pushed/merged to `main` — unless he explicitly says
|
||||||
|
otherwise. Production is `https://rmm.azcomputerguru.com`.
|
||||||
|
|
||||||
|
**Why:** he wants to eyeball UI/UX changes against real data before they go
|
||||||
|
anywhere near the main branch / production.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
- Do NOT merge dashboard changes to `main` to get them onto beta. The webhook
|
||||||
|
pipeline auto-builds beta *from* `origin/main` (`deploy/build-pipeline/build-dashboard.sh`
|
||||||
|
does `git reset --hard origin/main`), so merging to main is exactly what he's
|
||||||
|
trying to avoid doing first.
|
||||||
|
- Instead, deploy the **feature branch** to the beta web root manually: build the
|
||||||
|
branch's `dashboard/` (VITE_API_URL unset → bakes the prod API URL, same as the
|
||||||
|
pipeline) and rsync `dist/` to `/var/www/gururmm/dashboard-beta/` on the server.
|
||||||
|
Use a `git worktree` off the existing `/home/guru/gururmm` checkout so the main
|
||||||
|
checkout isn't disturbed. Do NOT update `/opt/gururmm/last-built-commit-dashboard`
|
||||||
|
(leave it so the next real main push rebuilds beta from main and reclaims it).
|
||||||
|
- Beta talks to **live production data/API**, so the preview is real — safe to
|
||||||
|
click through, but running an installer enrolls a real agent (use a throwaway
|
||||||
|
test site/device).
|
||||||
|
- Promotion beta→prod is a separate, human-run step: `sudo /opt/gururmm/promote-dashboard.sh --confirm`.
|
||||||
|
- A cleaner long-term fix would be a pipeline option to build a named branch to
|
||||||
|
beta; until then it's the manual worktree build above.
|
||||||
|
|
||||||
|
Related: [[gururmm-deploy-pipeline]]
|
||||||
@@ -368,10 +368,105 @@ PS
|
|||||||
echo "[OK] logs saved to: $outdir"
|
echo "[OK] logs saved to: $outdir"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# verify-each: re-seed a fresh EICAR before EACH engine and run that engine
|
||||||
|
# ALONE in clean mode, so the first engine's quarantine can't mask the others.
|
||||||
|
# Reports a per-engine detect+remove matrix. RKill is run but is a process
|
||||||
|
# killer (not a file scanner), so it is expected NOT to touch the file.
|
||||||
|
# Seeds in several locations to give targeted scanners (HitmanPro) a fair shot.
|
||||||
|
# ===========================================================================
|
||||||
|
VE_DIRS=('C:\GuruScanTest' 'C:\Users\Public\Desktop' 'C:\Windows\Temp')
|
||||||
|
VE_FILES=('C:\GuruScanTest\eicar_test.com' 'C:\Users\Public\Desktop\eicar_test.com' 'C:\Windows\Temp\eicar_test.com')
|
||||||
|
|
||||||
|
ve_seed_ps() { # emits PS that (re)creates a fresh EICAR in every VE_DIRS location
|
||||||
|
cat <<'PS'
|
||||||
|
$ErrorActionPreference='Continue'
|
||||||
|
$e='X5O!P%@AP[4\PZX54(P^)7CC)7}' + '$EICAR' + '-STANDARD-ANTIVIRUS-' + 'TEST-FILE!$H+H*'
|
||||||
|
foreach($d in @('C:\GuruScanTest','C:\Users\Public\Desktop','C:\Windows\Temp')){
|
||||||
|
if(-not (Test-Path $d)){ New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
||||||
|
Set-Content -Path (Join-Path $d 'eicar_test.com') -Value $e -Encoding ASCII -NoNewline
|
||||||
|
}
|
||||||
|
$n=@(Get-ChildItem 'C:\GuruScanTest\eicar_test.com','C:\Users\Public\Desktop\eicar_test.com','C:\Windows\Temp\eicar_test.com' -ErrorAction SilentlyContinue).Count
|
||||||
|
Write-Output ("SEEDED $n/3 EICAR copies")
|
||||||
|
PS
|
||||||
|
}
|
||||||
|
|
||||||
|
phase_verify_each() {
|
||||||
|
echo ""; echo "=== PHASE: verify-each (per-engine re-seed, clean mode) ==="
|
||||||
|
|
||||||
|
# Defender exclusions for all seed locations (so only GuruScan's engines act)
|
||||||
|
local sfx="$WORK_DIR/ve_excl.ps1"
|
||||||
|
cat > "$sfx" <<'PS'
|
||||||
|
$ErrorActionPreference='Continue'
|
||||||
|
foreach($p in @('C:\GuruScanTest','C:\Users\Public\Desktop','C:\Windows\Temp','C:\GuruScan','C:\GuruScan\downloads','C:\EmsisoftCmd')){
|
||||||
|
try{ Add-MpPreference -ExclusionPath $p -ErrorAction Stop; Write-Output "EXCLUDED $p" }catch{ Write-Output ("EXCL-SKIP "+$p) }
|
||||||
|
}
|
||||||
|
PS
|
||||||
|
run_ps "$sfx" 60 24 "ve-defender-exclusions" || echo "[WARN] exclusion step issues"
|
||||||
|
|
||||||
|
local engines="Emsisoft HitmanPro RKill"
|
||||||
|
local matrix=""
|
||||||
|
for eng in $engines; do
|
||||||
|
echo ""; echo "--- verify engine: $eng ---"
|
||||||
|
# 1) re-seed fresh EICAR everywhere
|
||||||
|
ve_seed_ps > "$WORK_DIR/ve_seed.ps1"
|
||||||
|
run_ps "$WORK_DIR/ve_seed.ps1" 60 24 "seed-for-$eng" || { echo "[ERROR] seed failed for $eng"; return 1; }
|
||||||
|
|
||||||
|
# 2) run ONLY this engine in clean mode (long; Emsisoft updates+scans C:\)
|
||||||
|
cat > "$WORK_DIR/ve_run.ps1" <<PS
|
||||||
|
\$ErrorActionPreference='Continue'
|
||||||
|
& C:\\GuruScan\\Invoke-GuruScan.ps1 -Scanners $eng -Headless
|
||||||
|
PS
|
||||||
|
run_ps "$WORK_DIR/ve_run.ps1" 2700 600 "run-$eng" || echo "[WARN] $eng run reported non-zero"
|
||||||
|
|
||||||
|
# 3) check which seeded copies survived + read that run's result json
|
||||||
|
cat > "$WORK_DIR/ve_check.ps1" <<'PS'
|
||||||
|
$ErrorActionPreference='Continue'
|
||||||
|
foreach($f in @('C:\GuruScanTest\eicar_test.com','C:\Users\Public\Desktop\eicar_test.com','C:\Windows\Temp\eicar_test.com')){
|
||||||
|
if(Test-Path $f){ Write-Output ("PRESENT $f") } else { Write-Output ("GONE $f") }
|
||||||
|
}
|
||||||
|
$d=Get-ChildItem C:\ScanLogs -Directory -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||||
|
if($d){
|
||||||
|
try{ $r=Get-Content (Join-Path $d.FullName 'results.json') -Raw | ConvertFrom-Json
|
||||||
|
Write-Output ("RESULT total_threats=" + $r.total_threats + " | " + (($r.scanners | ForEach-Object { $_.name + ' threats=' + $_.threats_found + ' exit=' + $_.exit_code }) -join ' ; ')) }catch{}
|
||||||
|
}
|
||||||
|
PS
|
||||||
|
run_ps "$WORK_DIR/ve_check.ps1" 60 24 "check-$eng" || true
|
||||||
|
local out gone present
|
||||||
|
out="$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null)"
|
||||||
|
gone="$(printf '%s' "$out" | grep -c '^GONE ')"
|
||||||
|
present="$(printf '%s' "$out" | grep -c '^PRESENT ')"
|
||||||
|
local verdict
|
||||||
|
if [ "$eng" = "RKill" ]; then verdict="n/a (process killer, not a file scanner)"
|
||||||
|
elif [ "$gone" -gt 0 ]; then verdict="DETECTED+REMOVED ($gone/3 copies removed)"
|
||||||
|
else verdict="MISSED (0/3 copies removed)"; fi
|
||||||
|
matrix="${matrix}\n ${eng}: ${verdict}"
|
||||||
|
echo " -> $eng: $verdict"
|
||||||
|
done
|
||||||
|
|
||||||
|
# cleanup: remove seeded files + the exclusions we added
|
||||||
|
cat > "$WORK_DIR/ve_clean.ps1" <<'PS'
|
||||||
|
$ErrorActionPreference='Continue'
|
||||||
|
foreach($f in @('C:\GuruScanTest\eicar_test.com','C:\Users\Public\Desktop\eicar_test.com','C:\Windows\Temp\eicar_test.com')){ Remove-Item $f -Force -ErrorAction SilentlyContinue }
|
||||||
|
Remove-Item 'C:\GuruScanTest' -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
foreach($p in @('C:\GuruScanTest','C:\Users\Public\Desktop','C:\Windows\Temp','C:\GuruScan','C:\GuruScan\downloads','C:\EmsisoftCmd')){ try{ Remove-MpPreference -ExclusionPath $p -ErrorAction Stop }catch{} }
|
||||||
|
Write-Output ("REMAINING-EXCLUSIONS: " + (((Get-MpPreference).ExclusionPath) -join '; '))
|
||||||
|
PS
|
||||||
|
run_ps "$WORK_DIR/ve_clean.ps1" 60 24 "ve-cleanup" || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================================="
|
||||||
|
echo " VERIFY-EACH RESULT (per-engine, independent re-seed)"
|
||||||
|
echo -e "$matrix"
|
||||||
|
echo "=========================================================="
|
||||||
|
post_alert "[RMM] GuruScan verify-each on $AGENT_HOST complete - see per-engine detect/remove matrix"
|
||||||
|
}
|
||||||
|
|
||||||
case "$PHASE" in
|
case "$PHASE" in
|
||||||
prep) phase_prep ;;
|
prep) phase_prep ;;
|
||||||
scan) phase_scan ;;
|
scan) phase_scan ;;
|
||||||
collect) phase_collect ;;
|
collect) phase_collect ;;
|
||||||
all) phase_prep && phase_scan && phase_collect ;;
|
verify-each) phase_verify_each ;;
|
||||||
*) echo "[ERROR] Unknown phase '$PHASE' (prep|scan|collect|all)" >&2; exit 1 ;;
|
all) phase_prep && phase_scan && phase_collect ;;
|
||||||
|
*) echo "[ERROR] Unknown phase '$PHASE' (prep|scan|collect|verify-each|all)" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ path is Cascades — override with the script's vault-path arg per client.
|
|||||||
- **[WORKING] pfSense gateway compatibility layer via SSH** — `scripts/pfsense-ssh.sh <slug> <verb>`.
|
- **[WORKING] pfSense gateway compatibility layer via SSH** — `scripts/pfsense-ssh.sh <slug> <verb>`.
|
||||||
DECISION (Mike 2026-06-16): **no RESTAPI package needed** — VPN + SSH shell reads the same data and makes
|
DECISION (Mike 2026-06-16): **no RESTAPI package needed** — VPN + SSH shell reads the same data and makes
|
||||||
changes. Cred = `clients/<slug>/pfsense-firewall` (host + admin user/pass), system OpenSSH via askpass.
|
changes. Cred = `clients/<slug>/pfsense-firewall` (host + admin user/pass), system OpenSSH via askpass.
|
||||||
Validated live on Cascades (pfSense Plus 25.07).
|
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`
|
- **Reads (no gate):** `audit` (WAN/DHCP/states/DNS/NIC health), `dhcp` (pool pressure), `pf-list`
|
||||||
(NAT port-forwards), `fw-list` (filter rules), `showblock [--if wan]` (active easyrule blocks),
|
(NAT port-forwards), `fw-list` (filter rules), `showblock [--if wan]` (active easyrule blocks),
|
||||||
`run "<cmd>"` (arbitrary; incl. changes — operator-gated, no dry-run).
|
`run "<cmd>"` (arbitrary; incl. changes — operator-gated, no dry-run).
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ Writes (DRY-RUN default; `--apply` to commit — `write_config` + `filter_config
|
|||||||
`php`, which bootstraps `$config` via `config.inc` (do NOT re-require util/functions/filter — "cannot redeclare"
|
`php`, which bootstraps `$config` via `config.inc` (do NOT re-require util/functions/filter — "cannot redeclare"
|
||||||
fatal). Each write backs up `/cf/conf/config.xml` to `/tmp` first; `write_config()` also keeps pfSense's own
|
fatal). Each write backs up `/cf/conf/config.xml` to `/tmp` first; `write_config()` also keeps pfSense's own
|
||||||
config history. Cred = `clients/<slug>/pfsense-firewall` (host + admin user/pass), system OpenSSH via askpass.
|
config history. Cred = `clients/<slug>/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] **Dispatch rewired:** `gw-control.sh` / `gw-audit.sh` now prefer the SSH backend (keyed on
|
- [x] **Dispatch rewired:** `gw-control.sh` / `gw-audit.sh` now prefer the SSH backend (keyed on
|
||||||
`clients/<slug>/pfsense-firewall`) and route the same verbs to it; dispatch runs BEFORE UOS site resolution so a
|
`clients/<slug>/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.
|
pfSense-only slug works. REST path is the dormant fallback.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
# VPN + SSH shell we can read the same data and make changes directly. (Confirmed on Cascades pfSense Plus
|
# VPN + SSH shell we can read the same data and make changes directly. (Confirmed on Cascades pfSense Plus
|
||||||
# 25.07: admin SSH drops straight to a shell, no menu gotcha.)
|
# 25.07: admin SSH drops straight to a shell, no menu gotcha.)
|
||||||
#
|
#
|
||||||
# Cred from the vault: clients/<slug>/pfsense-firewall (top-level `host`, credentials.username/password).
|
# Cred from the vault: clients/<slug>/pfsense-firewall (top-level `host`, credentials.username/password,
|
||||||
|
# optional `port` — else `--port N`, else default 22; e.g. the ACG office box is on 2248).
|
||||||
# Uses SYSTEM OpenSSH via an SSH_ASKPASS helper (no sshpass dependency); runs each call as `sh -s` over a
|
# Uses SYSTEM OpenSSH via an SSH_ASKPASS helper (no sshpass dependency); runs each call as `sh -s` over a
|
||||||
# heredoc so awk/quoting is clean.
|
# heredoc so awk/quoting is clean.
|
||||||
#
|
#
|
||||||
@@ -33,25 +34,34 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
GWC_PHP="$HERE/pfsense-gwc.php"
|
GWC_PHP="$HERE/pfsense-gwc.php"
|
||||||
SLUG="${1:?usage: pfsense-ssh.sh <slug> <action> [args] [--apply]}"
|
SLUG="${1:?usage: pfsense-ssh.sh <slug> <action> [args] [--apply]}"
|
||||||
ACT="${2:?action: audit|dhcp|pf-list|fw-list|pf-*|fw-*|block-ips|unblock|showblock|run|shell}"; shift 2 || true
|
ACT="${2:?action: audit|dhcp|pf-list|fw-list|pf-*|fw-*|block-ips|unblock|showblock|run|shell}"; shift 2 || true
|
||||||
RAWARGS=("$@") # preserved verbatim for `run`
|
APPLY=0; BLOCK_IF="wan"; PORT=""; POS=()
|
||||||
APPLY=0; BLOCK_IF="wan"; POS=()
|
|
||||||
while [ $# -gt 0 ]; do case "$1" in
|
while [ $# -gt 0 ]; do case "$1" in
|
||||||
--apply) APPLY=1; shift;;
|
--apply) APPLY=1; shift;;
|
||||||
--if) BLOCK_IF="${2:?--if needs an interface}"; shift 2;;
|
--if) BLOCK_IF="${2:?--if needs an interface}"; shift 2;;
|
||||||
|
--port) PORT="${2:?--port needs a number}"; shift 2;;
|
||||||
*) POS+=("$1"); shift;;
|
*) POS+=("$1"); shift;;
|
||||||
esac; done
|
esac; done
|
||||||
VP="clients/$SLUG/pfsense-firewall"
|
VP="clients/$SLUG/pfsense-firewall"
|
||||||
HOST="$(bash "$VAULT" get-field "$VP" host 2>/dev/null || bash "$VAULT" get-field "$VP" credentials.host 2>/dev/null || true)"
|
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)"
|
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
|
PP="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null || true)"; export PP
|
||||||
|
# SSH port precedence: --port flag > vault `port`/`credentials.port` field > default 22.
|
||||||
|
# (vault get-field returns the literal "null" for a missing field, so normalize "" and "null".)
|
||||||
|
if [ -z "$PORT" ]; then
|
||||||
|
for pf in port credentials.port; do
|
||||||
|
v="$(bash "$VAULT" get-field "$VP" "$pf" 2>/dev/null || true)"
|
||||||
|
case "$v" in ""|null) ;; *) PORT="$v"; break;; esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
PORT="${PORT:-22}"
|
||||||
if [ -z "$HOST" ] || [ -z "$U" ] || [ -z "$PP" ]; then
|
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] need host + admin creds at vault:$VP (fields: host, credentials.username, credentials.password)"; exit 2; fi
|
||||||
if [ "$ACT" = "shell" ]; then echo "ssh ${U}@${HOST} # password in vault:$VP"; exit 0; 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
|
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||||
ASKP="$TMP/a.sh"; printf '#!/bin/sh\nprintf "%%s\\n" "$PP"\n' >"$ASKP"; chmod +x "$ASKP"
|
ASKP="$TMP/a.sh"; printf '#!/bin/sh\nprintf "%%s\\n" "$PP"\n' >"$ASKP"; chmod +x "$ASKP"
|
||||||
# pfssh: feed remote sh a script on stdin; password via askpass (stderr noise dropped)
|
# pfssh: feed remote sh a script on stdin; password via askpass (stderr noise dropped)
|
||||||
pfssh(){ SSH_ASKPASS="$ASKP" SSH_ASKPASS_REQUIRE=force DISPLAY=:0 ssh \
|
pfssh(){ SSH_ASKPASS="$ASKP" SSH_ASKPASS_REQUIRE=force DISPLAY=:0 ssh -p "$PORT" \
|
||||||
-o ConnectTimeout=12 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
-o ConnectTimeout=12 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
||||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1 \
|
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1 \
|
||||||
"$U@$HOST" 'sh -s' 2>/dev/null; }
|
"$U@$HOST" 'sh -s' 2>/dev/null; }
|
||||||
@@ -69,10 +79,10 @@ run_gwc(){
|
|||||||
} | pfssh
|
} | pfssh
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "[INFO] pfSense $ACT @ $U@$HOST (vault:$VP)"
|
echo "[INFO] pfSense $ACT @ $U@$HOST:$PORT (vault:$VP)"
|
||||||
case "$ACT" in
|
case "$ACT" in
|
||||||
run)
|
run)
|
||||||
CMD="${RAWARGS[*]}"; [ -n "$CMD" ] || { echo "[ERROR] run needs a command"; exit 1; }
|
CMD="${POS[*]}"; [ -n "$CMD" ] || { echo "[ERROR] run needs a command"; exit 1; }
|
||||||
printf '%s\n' "$CMD" | pfssh ;;
|
printf '%s\n' "$CMD" | pfssh ;;
|
||||||
|
|
||||||
pf-list) run_gwc pf-list 0 "" "" "" ;;
|
pf-list) run_gwc pf-list 0 "" "" "" ;;
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ The REST backend (`pfsense-backend.sh`, `clients/<slug>/pfsense-api`) is a dorma
|
|||||||
`pfsense-firewall` cred) and run the dispatch BEFORE UOS site resolution, so a pfSense-only client
|
`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 <slug>` if the names differ).
|
slug works without a matching UOS site name (pass `--pfsense <slug>` if the names differ).
|
||||||
|
|
||||||
**Caveat for THIS office box:** `pfsense-ssh.sh` currently assumes SSH **port 22**; the ACG office
|
**THIS office box:** listens on SSH **port 2248** (not 22). The skill supports non-default ports as
|
||||||
pfSense listens on **2248**, so the skill needs a port option before it can manage the office
|
of 2026-06-21 — pass `--port 2248`, or (preferred) store `port: 2248` in the box's vault entry and
|
||||||
gateway. Cred for it is vaulted at `infrastructure/pfsense-firewall` (verify).
|
it's automatic. Cred for it is vaulted at `infrastructure/pfsense-firewall` (verify), but the SSH
|
||||||
|
backend expects the cred at `clients/<slug>/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).
|
||||||
|
|
||||||
**pfSense PHP gotchas** (baked into the scripts; carry forward to any new helper):
|
**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
|
- Bootstrap with `require_once("config.inc")` ONLY — re-requiring util/functions/filter → "cannot
|
||||||
|
|||||||
Reference in New Issue
Block a user