Files
T
SluijsensandClaude Opus 5 357d395629 Designs the security headers and observability units
Records the functional design for the two remaining application units,
before any of their code exists.

Security headers have to come from the application, because relying on
nginx or IIS configuration is exactly what this deployment model rules
out. Strict applies to /admin, /api/v1 and /health; a relaxed policy
applies to the public website, which this repository does not author.

The strict policy needs style-src 'unsafe-inline'. That is not a
shortcut: Radix positions dropdowns and dialogs with inline style
attributes recalculated per click and scroll position, and CSP nonces
apply only to style elements, never to style attributes. No nonce- or
hash-based variant leaves the admin UI working. The exception is bounded
to styles — script-src stays closed, which is where XSS actually lives.

The website's policy is enforcing rather than absent, so every
HTML-serving path carries a CSP and no exception has to be recorded. It
still blocks external script origins, so it remains a real boundary.

HSTS is skipped in development: browsers remember it per host and
localhost is shared with unrelated projects. Every other header applies
locally, so a CSP violation surfaces while developing.

For observability, browser error reports tunnel through the API rather
than going to Sentry directly. Ad blockers block Sentry domains, which
loses errors precisely for the users most likely to have browser
oddities. The tunnel forwards only to the host derived from the
configured DSN — a caller-supplied destination would turn an anonymous
endpoint into a request-forgery primitive.

Two consequences of the chosen options are recorded rather than left
implicit:

Enabling SendDefaultPii attaches request headers, and this application
carries two standing credentials in them. Besides the refreshToken
cookie, X-Master-Api-Key would have been sent to a third party on every
error raised during a master/slave call. The scrub list removes the
whole Cookie header, Authorization, X-Master-Api-Key and the request
body.

Console logging at Information plus structured logging to Sentry would,
taken literally, mean one Sentry event per request — exhausting the free
plan within hours and burying real errors in request noise. The
thresholds are split: console keeps Information, Sentry takes warnings
and above as events with Information as breadcrumbs, so every event
arrives carrying the trail that led to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 00:01:04 +02:00

10 KiB

Business Rules — U4 Observability Integration


Reporting Decision Logic

graph TD
    entry["Log entry or exception"]
    console["Write to console<br/>Information and above"]
    dsn{"Sentry DSN configured ?"}
    stop["Done: console only"]
    level{"Level is Warning or above ?"}
    crumb["Attach as breadcrumb<br/>context for a future event"]
    scrub["Scrub credentials from request context"]
    event["Send as Sentry event<br/>environment and release tagged"]

    entry --> console
    console --> dsn
    dsn -->|no| stop
    dsn -->|yes| level
    level -->|no| crumb
    level -->|yes| scrub
    scrub --> event

    classDef entrynode fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
    classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
    classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
    classDef neutral fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
    class entry entrynode;
    class dsn,level decision;
    class console,crumb,scrub,event step;
    class stop neutral;

Text alternative: everything goes to the console; with a DSN configured, informational entries become breadcrumbs while warnings and above become Sentry events, always after credentials are scrubbed from the request context.


Logging Rules

ID Rule
BR-U4-01 Structured console logging is always active, independent of Sentry.
BR-U4-02 Console threshold is Information, including framework categories.
BR-U4-03 Every log entry carries a correlation identifier.
BR-U4-04 Logging is registered before Sentry, so a Sentry initialisation problem is itself logged.
BR-U4-05 No log entry may contain a password, token, API key, connection string or cookie value.
BR-U4-06 Sentry event threshold is Warning and above.
BR-U4-07 Sentry breadcrumb threshold is Information, so events arrive with the trail that led to them.

Rationale for BR-U4-06 and BR-U4-07 — reconciling two answers rather than choosing between them: Q3 = C asked for Information everywhere, and Q16 = C asked for structured logging to Sentry beyond exceptions. Taken literally together, every framework Information entry would become a Sentry event — an event per request, exhausting the free plan within hours and burying real errors in request noise.

Splitting the thresholds honours both: the console gets everything (Q3 = C), and Sentry gets warnings and errors — still more than exceptions, as Q16 = C requires — each carrying its informational breadcrumbs.


Sentry Rules

ID Rule
BR-U4-08 An absent DSN is a normal, supported state. Sentry initialisation is skipped and console logging continues.
BR-U4-09 An unreachable Sentry never blocks or fails a request.
BR-U4-10 Every event is tagged with the environment and the release.
BR-U4-11 One Sentry project serves both backend and frontend, and both environments, distinguished by tags.
BR-U4-12 Request context is included, with credentials scrubbed per BR-U4-13.
BR-U4-13 Before any event leaves the process, these are removed: the entire Cookie header, the Authorization header, the X-Master-Api-Key header, and the request body.
BR-U4-14 Scrubbing happens in-process, before transmission — never relying on a server-side setting in Sentry.

Rationale for BR-U4-13 — this list is longer than the question implied. Enabling SendDefaultPii attaches request headers, and this application carries two standing credentials in headers: the refreshToken cookie and the X-Master-Api-Key used by the master/slave protocol. The API key was not mentioned when the setting was chosen, but it is the same class of secret and would otherwise be sent to a third party on every error raised during a master/slave call.

The request body is removed because the login and password-change endpoints carry passwords in it.

Rationale for BR-U4-14: server-side scrubbing means the secret already left the building. Doing it in-process is the only version that actually protects anything.

What is deliberately retained: method, path, query string, user agent, IP address, authenticated username and correlation ID. Note that invitation tokens travel as ?token=… on one endpoint — a single-use, time-limited token rather than a standing credential, and the diagnostic value of the path and query outweighs it. Recorded so the trade-off is visible rather than accidental.


Sentry Tunnel Rules

ID Rule
BR-U4-15 The browser sends Sentry envelopes to a same-origin tunnel endpoint, not directly to Sentry.
BR-U4-16 The tunnel forwards only to the host derived from the configured DSN. A caller-supplied destination is never honoured.
BR-U4-17 The tunnel rejects payloads above a fixed maximum size.
BR-U4-18 With no DSN configured, the tunnel accepts nothing and does nothing.
BR-U4-19 The tunnel is anonymous — error reports must work for a user whose session just expired.
BR-U4-20 The tunnel is not on the availability bypass list.
BR-U4-21 The backend's own Sentry reporting bypasses the tunnel and reports directly.

Rationale for BR-U4-16 — the rule that keeps this endpoint from being a liability: an anonymous endpoint that makes an outbound request on demand is a server-side request forgery primitive if the destination comes from the caller. Deriving the destination solely from configuration removes that entirely.

Rationale for BR-U4-19 and BR-U4-20 together: the tunnel must be anonymous, because the errors most worth capturing include authentication failures. But it need not survive the instance being switched off — if the CMS is deliberately disabled, losing admin-SPA error reports is acceptable, and keeping it off the bypass list means one less anonymous, outbound-capable endpoint reachable on a disabled instance.


Frontend Configuration Rules

ID Rule
BR-U4-22 An absent or empty VITE_API_BASE_URL resolves to same-origin: requests use relative paths.
BR-U4-23 An explicit absolute URL is used as supplied.
BR-U4-24 Validation accepts an empty string or a valid absolute URL — nothing else. A malformed value is not silently accepted.
BR-U4-25 Local development against https://localhost:7221 (master) and :7222 (slave) must keep working unchanged.
BR-U4-26 Frontend Sentry initialisation is skipped when no DSN is configured.
BR-U4-27 The Umami script is never loaded in local development, regardless of configuration.
BR-U4-28 With no Umami website ID configured, nothing is rendered.
BR-U4-29 Do Not Track is not consulted.

Rationale for BR-U4-24: relaxing validation to allow an empty value is not the same as removing validation. A typo such as htp://localhost:7221 must still be caught, or the SPA silently issues requests to a nonexistent origin.

Rationale for BR-U4-29 (Q5 = A): Umami sets no cookies and collects no personal data, and the admin SPA's audience is a known set of operators. Honouring DNT would reduce data without protecting anyone. A conscious choice rather than an omission.


Error and Edge-Case Scenarios

Scenario Expected behaviour
No DSN, application runs normally Console logging only. No error, no warning about the absence
DSN configured, Sentry unreachable Sentry buffers and eventually drops. No request fails
Exception during a master/slave call Event sent; X-Master-Api-Key scrubbed
Failed login Event at warning level; no password, no attempted password
Startup migration fails Event sent, then the process does not start. The event must be delivered before exit
Sentry initialisation itself throws Logged by the already-registered console logger; the application continues without Sentry
Browser posts to the tunnel with no DSN configured Rejected; nothing forwarded
Browser posts an oversized payload to the tunnel Rejected
Instance availability-disabled, browser posts to the tunnel 503 from the gate. Accepted loss
Ad blocker active The tunnel is same-origin, so reports arrive — the reason it exists
VITE_API_BASE_URL unset in a production build Same-origin. The intended production configuration
VITE_API_BASE_URL set to https://localhost:7221 locally Used as given; local development unchanged
VITE_API_BASE_URL set to htp://typo Development: loud warning. Production: used as given, and the requests visibly fail
Umami configured but its origin missing from the CSP Script blocked by the browser; U3's startup warning (BR-U3-22) flags the misconfiguration
Local development with a Umami ID configured Script not loaded

Security Compliance for U4

Rule Status Notes
SECURITY-03 Compliant Structured logging with a correlation ID on every entry (BR-U4-03); credentials and PII excluded by BR-U4-05 and BR-U4-13; centralised destination via Sentry
SECURITY-11 Compliant The tunnel's fixed destination (BR-U4-16) prevents it becoming a request-forgery primitive
SECURITY-13 Compliant The Umami script is external and constrained by U3's CSP; SRI applied where the provider supports it
SECURITY-14 Addressed, with DEV-01 Six alertable event types emitted (BR-U4-01 group); alert rules configured in Operations. Retention remains the accepted deviation — Sentry's plan retains roughly 30 days against the 90 the rule asks for
SECURITY-15 Compliant Observability failures never propagate into request handling (BR-U4-09)

No new deviation. DEV-01 already covers the retention shortfall; nothing here introduces another.

One risk closed that was not in the original scope: BR-U4-13 adds X-Master-Api-Key to the scrub list. Without it, enabling SendDefaultPii would have sent the master/slave shared secret to a third-party service on every error raised during a master/slave call.