Skip to content

Commit da74a5f

Browse files
feat(scripts): hcg-policy-smoke.sh — §1.5 operator pre-check (Phase E) (#210)
## Summary Lands `scripts/hcg-policy-smoke.sh` — a checked-in smoke that exercises the HCG tier-2 live Verb Governance Spec (`config/gateway-policy-boj.yaml`) from outside the gateway. Replaces the manual probe sequence the rollout runbook §1.5 last open item formerly described as "out of band — operator pre-check". Single-lane HCG tier-2 channel (`standards#91`). Phase A (#96), B (#97), C (#98), D (#99) are joint-closed; Phase E (`standards#100`) is the active phase. This PR lands one tractable artefact (§1.5 operator pre-check now checked-in and reproducible); staging soak (§2), production traffic split (§3), and the §6.4 Trustfile flip remain owner-driven. ## What this PR lands - **`scripts/hcg-policy-smoke.sh`** — POSIX-conformant bash + curl, no jq/yq dependency. - **Deny mode (default)**: sends one no-trust-header probe to every non-public route in the live policy (25 routes spanning the 19 authenticated and 6 internal+stealth entries) and asserts a 4xx response. The 4xx assertion covers both bare 403 and stealth-profile codes regardless of the gateway's `:stealth_profiles` runtime config. Plus a default-deny verb canary (DELETE /cartridges, PUT /health, PATCH /cartridges) confirming `global_verbs: [GET, POST]` enforces the ADR-0004 verb-governance invariant for un-listed verbs. Gateway-internal — BoJ does **not** have to be reachable. - **`--with-backend` mode**: additionally probes the allow path with `X-Trust-Level: authenticated` (and `internal` for internal+stealth routes), asserting the response is NOT a gateway-origin 4xx (2xx / 3xx / 5xx all pass — BoJ's own status is fine; only a gateway deny is a failure). Requires BoJ reachable at the gateway's `BACKEND_URL` and the script to run from a trusted-proxy IP so the trust header is not stripped by the gateway's `strip_untrusted_headers` plug. - Exits 0 on all-PASS, 1 on any FAIL (with per-probe summary), 64 on usage error. - **Runbook §1.5** — last unchecked operator pre-check item flips from a free-form "stand the gateway up ... exercise one allow + one deny per route" sequence (which was deferred to boj-server#165's test plan and documented as out-of-band) to a single `scripts/hcg-policy-smoke.sh` invocation. The PASS/FAIL summary attaches to the cut-over ticket; a single FAIL is a stop-the-rollout condition with the three failure modes named (policy not enforcing, BoJ unreachable, non-trusted-proxy caller stripping the header). - **Runbook header** — version 0.3 → 0.4; date 2026-06-09 → 2026-06-10; status line acknowledges the smoke script landing alongside the existing live policy promotion. - **Runbook Appendix B** — new cross-reference entry for `scripts/hcg-policy-smoke.sh`. ## What this PR deliberately does NOT do - **Close `standards#100`.** Per runbook §6.5 the joint-close happens after the §6.4 Trustfile flip (`tier_2_gateway.status: PENDING → DEPLOYED`), which itself follows the §3.3 100% production-soak window. Using `Refs` to match the Phase E PR convention established by #208 / #38 / #168 and documented in §6.5 ("Do not self-close standards#100; joint-close is owner-only per the single-lane channel discipline"). The owner remains the sole closer of `standards#100`. - **Touch HCG.** This is a BoJ-side artefact: the script lives in `scripts/`, reads `config/gateway-policy-boj.yaml`, and probes the gateway over HTTP. No companion PR on the gateway repo required. - **Run during CI deployment.** The script is checked in but only the operator's explicit invocation against a live gateway URL exercises it. CI does not stand up a gateway to run it (would require an external service); the script is intentionally operator-driven, with the PASS/FAIL summary attached to the cut-over ticket as the evidence-of-pre-check artefact. - **Diverge the policy from the script's route matrix.** The script's route matrix mirrors the 25-route live policy. When the policy file evolves (new BoJ surface routes wired in), the script must be updated in lock-step — that is a benefit not a cost (the script doubles as a policy-completeness checklist), but it must be observed. ## Verification - [x] `bash -n scripts/hcg-policy-smoke.sh` — syntax check passes. - [x] `scripts/hcg-policy-smoke.sh` (no args) — exits 64 (usage error). - [x] `scripts/hcg-policy-smoke.sh --help` — exits 64 with full help. - [x] Against a synthetic always-403 mock on :18443 — `PASS=28 FAIL=0`, exits 0 (deny-only mode covers all 25 policy routes + 3 verb canaries). - [x] Against a closed port (no gateway up) — every probe FAILs with `got=000 expected=deny`; exits 1 with the FAIL line summary. - [x] SPDX header `MPL-2.0` matches repo convention (scripts/, docs/). - [x] Runbook cross-references resolve (`§1.5`, Appendix B, sibling docs). ## Channel position ``` standards#91 (parent, open) ├── #96 Phase A — closed (boj-server: contract + policy-authoring + example; gateway: -) ├── #97 Phase B — closed (gateway#10: mTLS primary path) ├── #98 Phase C — closed (gateway#11: strip; boj-server#106: TrustPolicy clause) ├── #99 Phase D — closed (boj-server#168 on 2026-06-01; gateway#12/#14/#22/#26/#30) └── #100 Phase E — IN PROGRESS ├── E5 runbook draft — boj-server#128 (landed; rehearsal pending) ├── E1 loopback prereqs — boj-server#130/#131/#132/#165/#173 (landed) ├── E1 deploy spec — http-capability-gateway#38 (landed) ├── E1 live policy promotion — boj-server#208 (landed) ├── §1.5 operator pre-check smoke — THIS PR (in review) ├── E1 .ctp signing — owner follow-up ├── E2 staging cut-over — owner follow-up ├── E3 telemetry verification — owner follow-up ├── E4 production rollout — owner follow-up └── §6.4 Trustfile flip + §6.5 joint-close — owner-only ``` Refs hyperpolymath/standards#91 Refs hyperpolymath/standards#100 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01SE4pkWg8SEKsSWK9geD5r9)_ Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a289907 commit da74a5f

2 files changed

Lines changed: 250 additions & 4 deletions

File tree

docs/integration/hcg-tier2-rollout-runbook.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
# HCG tier-2 — rollout & rollback runbook
55

6-
**Version:** 0.3 (live policy promoted, Phase E in-progress)
7-
**Date:** 2026-06-09 (rev. from 2026-06-08)
8-
**Status:** Phase E deliverables E1 (deploy spec) + E5 (rollback runbook) drafted; live gateway policy (`config/gateway-policy-boj.yaml`) promoted from the worked example (§1.5). Owner-input markers (`!OWNER:`) remain to be filled before any traffic-shift action is taken.
6+
**Version:** 0.4 (policy-deny smoke script landed, Phase E in-progress)
7+
**Date:** 2026-06-10 (rev. from 2026-06-09)
8+
**Status:** Phase E deliverables E1 (deploy spec) + E5 (rollback runbook) drafted; live gateway policy (`config/gateway-policy-boj.yaml`) promoted from the worked example (§1.5); `scripts/hcg-policy-smoke.sh` lands as the checked-in §1.5 operator pre-check (deny-path covers gateway-alone; `--with-backend` adds allow-path coverage). Owner-input markers (`!OWNER:`) remain to be filled before any traffic-shift action is taken.
99
**ADR:** [`docs/decisions/0004-adopt-http-capability-gateway.md`](../decisions/0004-adopt-http-capability-gateway.md)
1010
**Plan:** [`docs/integration/http-capability-gateway-plan.md`](http-capability-gateway-plan.md) (§ Phase E)
1111
**Contract:** [`docs/integration/http-capability-gateway-boj-contract.md`](http-capability-gateway-boj-contract.md)
@@ -88,7 +88,7 @@ These cannot be inferred from the code/contract; the owner must fill them before
8888
- [x] `container/gateway-deploy.k9.ncl` exists in the gateway repo (plan §E1) — http-capability-gateway#38 (2026-06-03). Five-level k9-svc pedigree (Snout / Scent / Leash / Gut / Muscle) modelled on `boj-server:container/deploy.k9.ncl`; per-environment `BACKEND_URL` (`http://127.0.0.1:7700` staging, `http://unix:/run/boj/gnosis.sock:/` production); trust source `"header"` staging → `"mtls"` production after §2.4 rehearsal; `max_unavailable = 0`; `failure_mode = "fail-closed"` matching the `[SEAMS] gateway-boj-gnosis` declaration.
8989
- [x] Gateway policy file in place: `config/gateway-policy-boj-example.yaml`, covering all BoJ surface routes (`/.well-known/boj-node-pubkey`, `/health`, `/menu`, `/cartridges`, `/cartridge/:name`, `/cartridge/:name/invoke`, `/cartridge/:name/sse`, plus any added since contract v1.0). Re-verified 2026-05-28 against `BojRest.Router`; the `POST /cartridge/:name/sse` route (router.ex line 130, wired since the SSE landing — ADR-0013 §6, STATE entry 2026-05-18) was the only drift since contract v1.0 and is now governed by the `cartridge-sse-post` rule alongside `cartridge-invoke-post` (boj-server#165).
9090
- [x] Live policy file (`config/gateway-policy-boj.yaml`) promoted from the example. Content-identical to the example at promotion time; future BoJ-surface evolution lands in the live file and the example remains as the worked-example artefact (Phase A A3). Both §2.1 staging and §3.1 production load the live file via `POLICY_PATH`.
91-
- [ ] Gateway has been smoke-tested in isolation with the policy, returning expected allow/deny on each route. Sequence: stand the gateway up against `gateway-policy-boj-example.yaml`, exercise one allow + one deny per route from §1.5 above; confirm `POST /cartridge/:name/sse` with `X-Trust-Level: authenticated` proxies through and with `X-Trust-Level: untrusted` returns 403 (deferred to this step by boj-server#165's test plan). Out of band of code review — operator pre-check before §2.1.
91+
- [ ] Gateway has been smoke-tested in isolation with the policy, returning expected allow/deny on each route. Run `scripts/hcg-policy-smoke.sh --gateway-url <staging-gateway-url>` against the gateway loaded with `config/gateway-policy-boj.yaml`; the script exercises a no-trust-header deny probe for every non-public route plus default-deny verb canaries (DELETE/PUT/PATCH on `/cartridges` and `/health`) and is fully gateway-internal — BoJ does **not** need to be reachable for this run. Once BoJ is up behind the gateway, re-run with `--with-backend` from a trusted-proxy IP (loopback by default) to also cover the allow path on authenticated/internal routes including the `POST /cartridge/:name/sse` authenticated/untrusted pair carried over from boj-server#165's test plan. Attach the script's PASS/FAIL summary to the cut-over ticket; a single FAIL is a stop-the-rollout condition (gateway loaded the policy but is not enforcing as declared, or BoJ is unreachable from the gateway, or the script is being run from a non-trusted-proxy IP and the trust header is being stripped).
9292

9393
---
9494

@@ -312,3 +312,4 @@ Also update `[HTTP_CAPABILITY_GATEWAY]` section per plan §E acceptance: `status
312312
- `http-capability-gateway/docs/perf-contract.md` — Phase D perf-contract.
313313
- `elixir/lib/boj_rest/trust_policy.ex` — `satisfies?/3` Phase C enforcement.
314314
- `.machine_readable/contractiles/trust/Trustfile.a2ml` — `[CLOUDFLARE_EDGE_SECURITY].rate_limiting.tier_2_gateway` (current `PENDING` site; §6.4 flip target) + `[SEAMS]` (Phase C gateway↔BoJ-gnosis declaration).
315+
- `scripts/hcg-policy-smoke.sh` — §1.5 operator pre-check: deny-path smoke (gateway-alone) + optional `--with-backend` allow-path smoke against the live policy.

scripts/hcg-policy-smoke.sh

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
# Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
#
5+
# hcg-policy-smoke.sh — Exercise the HCG tier-2 live Verb Governance
6+
# Spec from outside the gateway. Returns non-zero on any unexpected
7+
# response, so it can be invoked from the §1.5 / §2.1 prerequisite
8+
# checklist in `docs/integration/hcg-tier2-rollout-runbook.md`.
9+
#
10+
# The default mode probes the *deny* path for every non-public route in
11+
# `config/gateway-policy-boj.yaml` plus a default-deny verb canary for
12+
# DELETE/PUT/PATCH. The deny path is fully gateway-internal — it does
13+
# not require BoJ to be reachable, so this script is the cheapest way
14+
# to confirm policy enforcement before staging cut-over.
15+
#
16+
# With `--with-backend`, the script additionally sends an authenticated
17+
# (or internal) probe per route and asserts the gateway *forwarded* it
18+
# (response did not come from the gateway's own deny path). Allow
19+
# probes require BoJ to be reachable from the gateway's BACKEND_URL,
20+
# and the script itself must run from an IP listed in the gateway's
21+
# `:trusted_proxies` config (loopback by default) so that the
22+
# X-Trust-Level header is not stripped by the gateway's
23+
# `strip_untrusted_headers` plug.
24+
#
25+
# Usage:
26+
# ./scripts/hcg-policy-smoke.sh --gateway-url http://127.0.0.1:8080
27+
# ./scripts/hcg-policy-smoke.sh --gateway-url https://stage:8443 \
28+
# --insecure --with-backend
29+
#
30+
# Exit codes: 0 = all probes matched expectations, 1 = at least one
31+
# mismatch, 64 = bad usage.
32+
#
33+
# Cross-refs:
34+
# docs/integration/hcg-tier2-rollout-runbook.md §1.5 / §2.1
35+
# docs/integration/http-capability-gateway-plan.md §Phase E
36+
# config/gateway-policy-boj.yaml source of truth
37+
# standards#100 tracking issue
38+
39+
set -euo pipefail
40+
41+
GATEWAY_URL=""
42+
WITH_BACKEND=0
43+
INSECURE=0
44+
TRUST_HEADER_NAME="X-Trust-Level"
45+
46+
usage() {
47+
cat >&2 <<'EOF'
48+
hcg-policy-smoke.sh — Exercise the HCG live policy.
49+
50+
USAGE:
51+
hcg-policy-smoke.sh --gateway-url URL [--with-backend] [--insecure]
52+
[--trust-header NAME]
53+
54+
OPTIONS:
55+
--gateway-url URL Base URL of the gateway (required), e.g.
56+
http://127.0.0.1:8080 or https://stage:8443.
57+
--with-backend Additionally probe the allow path on each route
58+
(requires BoJ reachable at the gateway's
59+
BACKEND_URL, and this script to run from a
60+
trusted-proxy IP).
61+
--insecure Pass `-k` to curl (self-signed staging TLS).
62+
--trust-header NAME Override the trust-level header name. Defaults
63+
to the gateway default `X-Trust-Level`; set this
64+
only if `:trust_level_header` was customised.
65+
-h, --help Show this help.
66+
67+
EXAMPLES:
68+
# Deny-only smoke against a local gateway with no BoJ behind it:
69+
./scripts/hcg-policy-smoke.sh --gateway-url http://127.0.0.1:8080
70+
71+
# Full smoke against staging, BoJ up, self-signed TLS:
72+
./scripts/hcg-policy-smoke.sh --gateway-url https://stage:8443 \
73+
--insecure --with-backend
74+
75+
Designed to be run by the operator from the rollout runbook §1.5 last
76+
open item (replacing the out-of-band manual probe sequence) and §2.1
77+
post-stand-up sanity check.
78+
EOF
79+
exit 64
80+
}
81+
82+
while [ $# -gt 0 ]; do
83+
case "$1" in
84+
--gateway-url) GATEWAY_URL="${2:-}"; shift 2 ;;
85+
--with-backend) WITH_BACKEND=1; shift ;;
86+
--insecure) INSECURE=1; shift ;;
87+
--trust-header) TRUST_HEADER_NAME="${2:-}"; shift 2 ;;
88+
-h|--help) usage ;;
89+
*) echo "unknown arg: $1" >&2; usage ;;
90+
esac
91+
done
92+
93+
[ -n "$GATEWAY_URL" ] || usage
94+
command -v curl >/dev/null || { echo "curl: not found" >&2; exit 1; }
95+
96+
GATEWAY_URL="${GATEWAY_URL%/}" # strip trailing slash
97+
CURL_BASE=(curl -sS -o /dev/null -w '%{http_code}' --max-time 10)
98+
[ "$INSECURE" = "1" ] && CURL_BASE+=(-k)
99+
100+
PASS=0
101+
FAIL=0
102+
FAIL_LINES=()
103+
104+
# probe VERB PATH EXPECTED_PATTERN LABEL [trust_level]
105+
#
106+
# EXPECTED_PATTERN is an extended-regex matched against the three-digit
107+
# status code; "deny" expands to 4xx, "allow_or_upstream" expands to
108+
# "anything but a gateway-origin 4xx" (2xx, 3xx, 5xx).
109+
#
110+
# trust_level (optional) is sent as the X-Trust-Level header. Without
111+
# it the gateway treats the caller as untrusted, which is the deny-path
112+
# input.
113+
probe() {
114+
local verb="$1" path="$2" pattern="$3" label="$4" trust="${5:-}"
115+
local url="${GATEWAY_URL}${path}"
116+
local args=("${CURL_BASE[@]}" -X "$verb")
117+
if [ -n "$trust" ]; then
118+
args+=(-H "${TRUST_HEADER_NAME}: ${trust}")
119+
fi
120+
# Some routes are POST; send an empty JSON body so Plug.Parsers
121+
# does not 400 on missing content-type.
122+
if [ "$verb" = "POST" ]; then
123+
args+=(-H "Content-Type: application/json" --data '{}')
124+
fi
125+
args+=("$url")
126+
127+
local code
128+
# Quote "${args[@]}" so multi-word array elements (the JSON
129+
# Content-Type header in particular) stay as single arguments to
130+
# curl — without quoting, word-splitting turned "Content-Type:
131+
# application/json" into two args and curl saw "application/json"
132+
# as a second URL, double-writing %{http_code}.
133+
code="$("${args[@]}" 2>/dev/null || true)"
134+
case "$pattern" in
135+
deny)
136+
if [[ "$code" =~ ^4[0-9][0-9]$ ]]; then
137+
printf ' PASS %-65s %s\n' "$label" "$code"
138+
PASS=$((PASS + 1))
139+
return
140+
fi
141+
;;
142+
allow_or_upstream)
143+
# The gateway forwarded the request iff the response is NOT
144+
# a gateway-origin 4xx deny. 2xx/3xx mean BoJ replied;
145+
# 5xx is upstream-down (also forwarded). The gateway's own
146+
# circuit-breaker 503 is indistinguishable from an upstream
147+
# 503 at this level, which is fine — neither indicates a
148+
# policy regression.
149+
if [[ ! "$code" =~ ^4[0-9][0-9]$ ]]; then
150+
printf ' PASS %-65s %s\n' "$label" "$code"
151+
PASS=$((PASS + 1))
152+
return
153+
fi
154+
;;
155+
esac
156+
printf ' FAIL %-65s %s (expected %s)\n' "$label" "$code" "$pattern"
157+
FAIL=$((FAIL + 1))
158+
FAIL_LINES+=("$label got=$code expected=$pattern")
159+
}
160+
161+
echo "==> HCG policy deny smoke against ${GATEWAY_URL}"
162+
echo " (config/gateway-policy-boj.yaml; no X-Trust-Level header)"
163+
164+
# Authenticated routes — gateway must 4xx without a trust header.
165+
# Internal+stealth routes — also 4xx (status code shape depends on
166+
# `:stealth_profiles` runtime config; 4xx covers both stealth and
167+
# bare 403).
168+
probe GET /status deny "auth:status-get"
169+
probe GET /menu deny "auth:menu-get"
170+
probe GET /matrix deny "auth:matrix-get"
171+
probe GET /cartridges deny "auth:cartridges-list-get"
172+
probe GET /cartridge/probe deny "auth:cartridge-detail-get"
173+
probe POST /cartridge/probe/invoke deny "auth:cartridge-invoke-post"
174+
probe POST /cartridge/probe/sse deny "auth:cartridge-sse-post"
175+
probe POST /graphql deny "auth:graphql-post"
176+
probe POST /grpc/svc/method deny "auth:grpc-method-post"
177+
probe GET /sse deny "auth:sse-get"
178+
probe POST /order deny "auth:order-post"
179+
probe POST /order-ticket deny "auth:order-ticket-post"
180+
probe GET /umoja/status deny "auth:umoja-status-get"
181+
probe GET /umoja/transport deny "auth:umoja-transport-get"
182+
probe GET /umoja/peers deny "auth:umoja-peers-get"
183+
probe GET /coprocessor/status deny "auth:coprocessor-status-get"
184+
probe GET /sla/status deny "auth:sla-status-get"
185+
probe GET /community/submissions deny "auth:community-submissions-get"
186+
probe POST /community/submit deny "auth:community-submit-post"
187+
188+
probe POST /cartridge/probe/load deny "internal:cartridge-load-post"
189+
probe POST /cartridge/probe/unload deny "internal:cartridge-unload-post"
190+
probe POST /cartridge/probe/reload deny "internal:cartridge-reload-post"
191+
probe POST /umoja/peers deny "internal:umoja-peers-post"
192+
probe POST /coprocessor/select deny "internal:coprocessor-select-post"
193+
probe GET /sdp/status deny "internal:sdp-status-get"
194+
195+
# Default-deny verb canaries — global_verbs is [GET, POST], so any
196+
# DELETE/PUT/PATCH on a known path must be denied via the no-match
197+
# (or unknown-method) path. Verifies the verb-governance core invariant
198+
# of ADR-0004.
199+
probe DELETE /cartridges deny "verb-canary:DELETE /cartridges"
200+
probe PUT /health deny "verb-canary:PUT /health"
201+
probe PATCH /cartridges deny "verb-canary:PATCH /cartridges"
202+
203+
if [ "$WITH_BACKEND" = "1" ]; then
204+
echo
205+
echo "==> HCG policy allow smoke (--with-backend)"
206+
echo " (X-Trust-Level: authenticated/internal; requires BoJ up)"
207+
208+
# Authenticated routes — gateway forwards under X-Trust-Level: authenticated.
209+
# We assert "not a gateway-origin 4xx"; BoJ's own 200/404/500 is fine.
210+
probe GET /status allow_or_upstream "auth-allow:status-get" authenticated
211+
probe GET /menu allow_or_upstream "auth-allow:menu-get" authenticated
212+
probe GET /cartridges allow_or_upstream "auth-allow:cartridges-list-get" authenticated
213+
probe GET /cartridge/probe allow_or_upstream "auth-allow:cartridge-detail-get" authenticated
214+
probe POST /cartridge/probe/invoke allow_or_upstream "auth-allow:cartridge-invoke-post" authenticated
215+
probe POST /cartridge/probe/sse allow_or_upstream "auth-allow:cartridge-sse-post" authenticated
216+
217+
# Public routes — should forward without any trust header.
218+
probe GET /health allow_or_upstream "public-allow:health-get" ""
219+
probe GET /.well-known/boj-node-pubkey allow_or_upstream "public-allow:node-pubkey-get" ""
220+
221+
# Internal+stealth routes — gateway forwards only under
222+
# X-Trust-Level: internal.
223+
probe POST /cartridge/probe/load allow_or_upstream "internal-allow:cartridge-load-post" internal
224+
probe POST /cartridge/probe/unload allow_or_upstream "internal-allow:cartridge-unload-post" internal
225+
probe POST /cartridge/probe/reload allow_or_upstream "internal-allow:cartridge-reload-post" internal
226+
probe GET /sdp/status allow_or_upstream "internal-allow:sdp-status-get" internal
227+
fi
228+
229+
echo
230+
echo "────────────────────────────────────────────────────────────────────────"
231+
echo "HCG policy smoke: PASS=${PASS} FAIL=${FAIL}"
232+
if [ "$FAIL" -gt 0 ]; then
233+
echo
234+
echo "Mismatches:"
235+
for line in "${FAIL_LINES[@]}"; do
236+
echo " - ${line}"
237+
done
238+
echo
239+
echo "Investigate before flipping the §1.5 checkbox. A 4xx miss on a"
240+
echo "deny probe means the policy was loaded but is not enforcing as"
241+
echo "declared; a 4xx on an allow probe means the trust header was"
242+
echo "stripped (run from a trusted-proxy IP) or BoJ is unreachable."
243+
exit 1
244+
fi
245+
echo "All probes matched policy. Safe to proceed with §2.1 staging cut-over."

0 commit comments

Comments
 (0)