Skip to content

Commit c2609a6

Browse files
Enachclaude
andauthored
[PAC-114] docs+test(nginx): PAC-14 §4 pointer, D-8 amendment marker, build-time VITE_SENTRY_DSN rollback; whitespace-only Sentry origin fails closed (#280)
PAC-86 review follow-ups, items 2 and 4 (item 1, DSN rotation, is an operator action and is not part of this change). - contracts/features/PAC-14.md: §4 points to the PAC-86 frontend amendment in §2; §7 rollback item 3 gets an amendment note: VITE_SENTRY_DSN is inlined at build time, so it is not a runtime kill switch (rebuild, or unset NGINX_CSP_SENTRY_ORIGIN to block events). Approved text kept. - contracts/features/PAC-14.yaml: rollback line corrected the same way. - docs/specs/NEW-deploy-hardening.md: amendment marker on D-8, which still described the default Sentry origin. - nginx/test/nginx_test.go: TestWhitespaceOnlySentryOriginRefusesToStart: " ", " " and a space-padded origin are refused by the render step (nginx does not start). Mutation-checked: trimming spaces in the render script makes it fail. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8f3bfc7 commit c2609a6

4 files changed

Lines changed: 45 additions & 1 deletion

File tree

‎contracts/features/PAC-14.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ func main() {
127127

128128
Target repo: `Enach/smart-calendar-flow` (React, bun).
129129

130+
- **Amended (pointer added by PAC-114, 2026-11-14).** The frontend init below is
131+
amended by PAC-86: there is **no default DSN**, and the `?? "<DSN>"` fallback
132+
is superseded. The amendment note sits in §2 ("Amended by PAC-86"), next to
133+
the backend's PAC-81 amendment. The text below is kept as PAC-14 approved it.
134+
130135
- Install `@sentry/react`. Initialize once at the app entrypoint, **before** the
131136
React tree mounts:
132137

@@ -215,6 +220,15 @@ schema, any `frontend/` source (separate repo), any secret/credential file.
215220
`VITE_SENTRY_DSN` (frontend) -> init degrades to "monitoring disabled" on next
216221
start, so monitoring can be turned off without reverting code.
217222

223+
**Amended by PAC-114 (2026-11-14).** Item 3 holds for the backend only:
224+
`SENTRY_DSN` is read at process start, so unsetting it and restarting turns
225+
backend monitoring off. `VITE_SENTRY_DSN` is not a runtime kill switch: Vite
226+
inlines it at build time, so turning web monitoring off means rebuilding and
227+
redeploying the frontend image without it. Without a rebuild, unsetting
228+
`NGINX_CSP_SENTRY_ORIGIN` and restarting nginx removes the Sentry origin from
229+
`connect-src` (PAC-86), so browsers block the events. The text above is kept
230+
as PAC-14 approved it.
231+
218232
---
219233

220234
## 8. Release gates

‎contracts/features/PAC-14.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@
3535
"Frontend error boundary captures a render exception and shows fallback UI.",
3636
"Contract/provider/consumer tests must pass locally before implementation approval."
3737
],
38-
"rollback": "Revert the implementation PRs and keep the approved contract revision immutable. Runtime kill switch: unset SENTRY_DSN / VITE_SENTRY_DSN."
38+
"rollback": "Revert the implementation PRs and keep the approved contract revision immutable. Runtime kill switch (backend only): unset SENTRY_DSN and restart the backend. VITE_SENTRY_DSN is set at build time, not a runtime kill switch: turn web monitoring off by rebuilding and redeploying the frontend without it, or, without a rebuild, by unsetting NGINX_CSP_SENTRY_ORIGIN and restarting nginx so the CSP blocks the events (amended by PAC-114, 2026-11-14; see contracts/features/PAC-14.md section 7)."
3939
}

‎docs/specs/NEW-deploy-hardening.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,12 @@ Factory-assumed (pending challenge):
11501150
no wildcard; AC-B4) + operator-configured origins; `object-src 'none'`;
11511151
`base-uri 'self'`; `form-action 'self'`; `frame-ancestors 'none'`. Enforced,
11521152
because AC-B3 proves zero violations with a positive and negative control.
1153+
**Amended by PAC-86 (2026-11-14; marker added by PAC-114).** The Sentry
1154+
ingest origin has no default any more: the web has no hard-coded DSN, so with
1155+
`NGINX_CSP_SENTRY_ORIGIN` unset or empty `connect-src` holds no Sentry origin,
1156+
and when set it holds exactly that origin, which must be the origin of the
1157+
frontend build's `VITE_SENTRY_DSN`. See AC-B4's amendment. The text above is
1158+
kept as approved.
11531159
- **D-9. Real client in nginx**: right-most address not in the trusted list,
11541160
recursive; list empty by default (spoofing impossible; limits shared until
11551161
configured; the runbook makes configuring it a deploy step).

‎nginx/test/nginx_test.go‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,30 @@ func TestInvalidSettingsRefuseToStart(t *testing.T) {
279279
}
280280
}
281281

282+
// PAC-114: a whitespace-only NGINX_CSP_SENTRY_ORIGIN is not "unset". It is
283+
// neither treated as empty (no Sentry origin) nor trimmed: the render step
284+
// refuses it, so nginx does not start. The refusal must name the setting, so
285+
// it comes from the validation and not from a broken generated config.
286+
func TestWhitespaceOnlySentryOriginRefusesToStart(t *testing.T) {
287+
t.Parallel()
288+
stub := startStub(t)
289+
for what, v := range map[string]string{
290+
"one space": " ",
291+
"several spaces": " ",
292+
"space-padded ok": " https://o1.ingest.sentry.example ",
293+
} {
294+
env := map[string]string{"NGINX_CSP_SENTRY_ORIGIN": v}
295+
out, err := runNginxOnce(t, stub, env, "nginx", "-t")
296+
if err == nil {
297+
t.Errorf("%s: started with %q; want a refusal\n%s", what, env, out)
298+
continue
299+
}
300+
if !strings.Contains(out, "paceday-render: NGINX_CSP_SENTRY_ORIGIN") {
301+
t.Errorf("%s: refused, but not by the NGINX_CSP_SENTRY_ORIGIN check\n%s", what, out)
302+
}
303+
}
304+
}
305+
282306
func TestConfigLoads(t *testing.T) {
283307
t.Parallel()
284308
stub := startStub(t)

0 commit comments

Comments
 (0)