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
This commit is contained in:
2026-07-28 00:01:04 +02:00
co-authored by Claude Opus 5
parent 5f3eda2680
commit 357d395629
13 changed files with 2471 additions and 0 deletions
@@ -0,0 +1,122 @@
# Functional Design Questions — U3 HTTP Security Headers & CSP
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past.
---
## Question 1 — De admin-SPA gaat kapot van een echt strikte CSP
**Context**: de admin-SPA gebruikt Radix UI-componenten (dialogen, dropdowns, selects) via shadcn-stijl wrappers. Die plaatsen positionering als **inline `style`-attributen** op elementen — dat is hoe een dropdown weet waar hij moet staan.
Een CSP met `style-src 'self'` en zonder `'unsafe-inline'` blokkeert die inline styles. Het gevolg is niet een foutmelding maar verkeerd gepositioneerde of onzichtbare menu's en dialogen: kapot op een manier die je pas in de browser ziet, en pas op de plekken waar je klikt.
Er zijn drie manieren om hiermee om te gaan:
A) `style-src 'self' 'unsafe-inline'` in de strikte policy, met een gedocumenteerde onderbouwing waarom die uitzondering nodig is — pragmatisch en meteen werkend. `script-src` blijft wél strikt, en dat is waar XSS-risico echt zit
B) Nonces of hashes gebruiken voor styles — theoretisch netter, maar Radix genereert styles op runtime per interactie, dus dit werkt in de praktijk niet zonder de componenten te herschrijven
C) Begin met `Content-Security-Policy-Report-Only` voor `/admin`, kijk wat er daadwerkelijk overtreden wordt, en zet hem daarna afdwingend — niets breekt stil, maar de bescherming staat er in de tussentijd niet
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]: B
---
## Question 2 — Wat mag de publieke website?
**Context**: je koos een ruimere CSP voor de publieke website (D-31), omdat een website-bouwer niet beperkt moet worden door een policy die hij nooit gezien heeft. Maar "ruimer" moet nog wel iets betekenen — de website wordt door hetzelfde proces geserveerd als de admin-UI en de API.
A) Ruim maar niet leeg: sta scripts, styles, afbeeldingen en fonts van eigen origin plus inline toe, en verbindingen naar eigen origin plus de geconfigureerde origins. Externe scripts (bijv. een YouTube-embed) worden dan geblokkeerd tenzij toegevoegd
B) Alleen de echt beschermende headers voor de website (`X-Content-Type-Options`, HSTS, `Referrer-Policy`) en **geen** CSP — dan kan een website-bouwer nooit verrast worden
C) Ruim, plus in het website-contract vastleggen dat een bouwer extra origins kan laten toevoegen aan de configuratie als hij externe bronnen nodig heeft
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:B
---
## Question 3 — Gelden de headers ook lokaal?
**Context**: `Strict-Transport-Security` vertelt de browser: gebruik voor dit domein voortaan altijd HTTPS. Browsers onthouden dat **per host, langdurig**. Stuur je die header op `localhost`, dan kan dat je lokale ontwikkeling van andere projecten op dezelfde `localhost` gaan dwarszitten, en het is niet triviaal om weer ongedaan te maken.
De CSP lokaal wél actief hebben is juist nuttig: dan merk je een overtreding tijdens ontwikkelen in plaats van in productie.
A) CSP en de overige headers ook in Development; **HSTS alleen buiten Development** — je ziet CSP-problemen vroeg, zonder je `localhost` te vervuilen
B) Alle headers in alle omgevingen, inclusief HSTS lokaal
C) Geen enkele header in Development — lokaal zo weinig ruis als mogelijk
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:A
---
## Question 4 — Mag `/admin` in een iframe?
**Context**: je koos `X-Frame-Options: DENY` in de requirements (FR-18). Dat betekent dat geen enkele pagina van dit domein in een iframe geplaatst mag worden — ook niet door de site zelf.
Dat is voor de admin-UI precies goed. Maar het geldt dan ook voor de publieke website, en een klant die op zijn eigen site een pagina in een iframe toont (bijvoorbeeld een formulier of een kaart in een eigen iframe), loopt daar tegenaan.
A) `DENY` voor `/admin` en `/api/v1`, `SAMEORIGIN` voor de publieke website — de admin-UI blijft maximaal beschermd, de website kan zijn eigen pagina's insluiten
B) `DENY` overal, zoals in de requirements — en een klant die iframes nodig heeft, komt dan bij jou terecht
C) `SAMEORIGIN` overal — eenvoudiger, iets minder strikt voor de admin-UI
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:A
---
## Question 5 — Wat als er geen origins geconfigureerd zijn?
**Context**: de Umami- en Sentry-origins komen uit configuratie en verschillen per omgeving. In een omgeving zonder Sentry of Umami — bijvoorbeeld lokaal, of een instantie zonder analytics — zijn die lijsten leeg.
Een strikte CSP zonder die origins is correcter (minder toegestaan), maar als iemand later Sentry aanzet en de origin vergeet toe te voegen, worden de foutmeldingen stil geblokkeerd: je monitoring lijkt dan te werken maar ontvangt niets.
A) Lege lijsten zijn normaal — de policy wordt dan simpelweg strikter. Bij opstarten een informatieregel loggen welke origins actief zijn, zodat je in het log kunt zien wat is toegestaan
B) Lege lijsten zijn normaal, en géén logging — het is een gewone toestand
C) Bij opstarten een waarschuwing als er een Sentry-DSN of Umami-website-ID is geconfigureerd maar de bijbehorende origin niet in de CSP staat — vangt precies de fout waarbij monitoring stil niets ontvangt
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:C
---
# Vervolgvragen (ronde 2)
---
## Follow-up Question 1 — Optie B bij vraag 1 kan technisch niet
Je koos bij vraag 1 optie B: nonces of hashes voor styles. Ik moet dat terugleggen, want ik heb die optie te mild beschreven — hij is niet "theoretisch netter maar lastig", hij **werkt principieel niet** voor dit probleem.
**Waarom niet:** een CSP-nonce werkt alleen op `<style>`- en `<script>`-**elementen**. Inline `style="..."`-**attributen** vallen daar volledig buiten; een nonce kan er niet aan gehangen worden. Dat is geen implementatiedetail maar hoe de specificatie werkt.
En juist die attributen zijn wat Radix gebruikt: een dropdown die opengaat krijgt zijn positie als `style="transform: translate(...)"` op het element. Die waarde verandht per klik, per schermgrootte en per scrollpositie.
De enige CSP-mechanismen die inline `style`-attributen kunnen toestaan zijn:
- `'unsafe-inline'` — staat ze allemaal toe
- `'unsafe-hashes'` met een hash per exacte attribuutwaarde — en die waarden zijn hier dynamisch, dus er is geen eindige verzameling om te hashen
Er is dus geen variant van B die de admin-UI heel laat. Wat overblijft:
A) `style-src 'self' 'unsafe-inline'` in de strikte policy, met gedocumenteerde onderbouwing. `script-src` blijft strikt zonder `unsafe-inline` en zonder `unsafe-eval` — en daar zit het werkelijke XSS-risico. Dit is wat vrijwel elke applicatie met een component-bibliotheek doet
C) Beginnen met `Content-Security-Policy-Report-Only` voor `/admin`, kijken wat er in de praktijk overtreden wordt, en daarna afdwingend zetten met de dan blijkbaar benodigde uitzonderingen. Niets breekt stil, maar de CSP beschermt `/admin` in de tussentijd niet
D) Radix vervangen door componenten zonder inline styles — een herbouw van de hele admin-UI, ver buiten de scope van deze feature
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]: A
---
## Follow-up Question 2 — Bevestiging: geen CSP op de publieke website is een afwijking
Bij vraag 2 koos je B: alleen de beschermende headers voor de publieke website en **geen** CSP. Dat is een verdedigbare keuze en past bij je uitgangspunt dat een website-bouwer niet mag struikelen over een policy die hij nooit gezien heeft.
Wel moet ik het expliciet vastleggen, want SECURITY-04 vraagt een CSP op **alle** HTML-servende endpoints. Zonder CSP op de website betekent dat: als daar ooit een XSS-gat zit, is er geen tweede verdedigingslaag die het beperkt. `/admin` en `/api/v1` houden hun strikte policy, dus het beheerdeel blijft wel beschermd.
Hoe wil je dit vastleggen?
A) Als gedocumenteerde afwijking (DEV-06) met deze onderbouwing, en in het website-contract opnemen dat een bouwer zelf verantwoordelijk is voor uitvoer-escaping op zijn site
B) Toch een CSP op de website, maar alleen rapporterend (`Report-Only`) — dan beperkt hij niets en breekt er niets, maar je ziet wel wat er zou zijn geblokkeerd
C) Toch een afdwingende ruime CSP op de website (optie A van de oorspronkelijke vraag), zodat SECURITY-04 gewoon gehaald wordt
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]: C
@@ -0,0 +1,45 @@
# Functional Design Plan — U3 HTTP Security Headers & CSP
**Unit**: U3 HTTP Security Headers & CSP
**Round**: R2 (with U4 Observability)
**Requirements**: FR-18
**Components**: C-01, C-02, C-03, U3 portion of C-16
**Scope boundary**: this stage designs *behaviour* — which headers apply to which response, what each policy permits, and what happens in edge cases. The *pattern* decisions (how the policy is composed, how the configuration surface is shaped) belong to this unit's NFR Design, which follows.
---
## Step 1: Analyze unit context
- [x] Read the U3 definition from `unit-of-work.md`
- [x] Read FR-18 and D-31 from `requirements.md`
- [x] Read the per-header scoping decision (FU1 = A) and the configuration split (FU2 = A)
- [x] Confirm the settled path layout from U1 as delivered
## Step 2: Design header applicability
- [x] Define which headers apply to all responses and which to HTML only
- [x] Define behaviour for redirects, `304 Not Modified` and error responses
- [x] Define behaviour when a header is already present
- [x] Define whether headers apply in Development — see Question 3
## Step 3: Design the two policies
- [x] Define what the `Strict` policy permits for `/admin` and `/api/v1` — see Question 1
- [x] Define what the `Relaxed` policy permits for the public website — see Question 2
- [x] Define how the Umami and Sentry origins enter the policy
- [x] Define behaviour for the placeholder page
## Step 4: Design failure behaviour
- [x] Define startup behaviour on an unknown policy name
- [x] Define behaviour when no origins are configured
- [x] Confirm header application never throws into the response path
## Step 5: Define business rules
- [x] Enumerate applicability rules
- [x] Enumerate policy-content rules
- [x] Identify error and edge-case scenarios
## Step 6: Generate artifacts
- [x] Generate `business-logic-model.md`
- [x] Generate `business-rules.md`
- [x] Generate `domain-entities.md`
- [x] Validate all diagrams against the Mermaid standards
- [x] Verify Security Baseline compliance for this unit's design
@@ -0,0 +1,83 @@
# Functional Design Questions — U4 Observability Integration
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past.
---
## Question 1 — Gaan Sentry-meldingen direct of via een tunnel?
**Context**: in je SlpSoftware-workflow loopt Sentry via een **tunnel**: de browser stuurt fouten naar een pad op je eigen domein (`/sentry-tunnel`), dat ze doorstuurt naar Sentry. Dat is daar gedaan omdat adblockers verzoeken naar Sentry-domeinen blokkeren met `ERR_BLOCKED_BY_CLIENT` — waardoor je juist bij de gebruikers met een adblocker geen fouten meer ziet.
Dat probleem geldt hier net zo goed. Een tunnel heeft bovendien een neveneffect dat hier goed uitkomt: alle verkeer blijft same-origin, dus `connect-src 'self'` volstaat en de CSP van U3 heeft geen externe Sentry-origin nodig.
Nadeel: de tunnel moet ergens draaien. In jouw referentie doet nginx dat; hier kan het geen serverconfiguratie zijn, dus zou de .NET-app het zelf moeten doorsturen.
A) Tunnel via de API — een endpoint in de app stuurt de Sentry-envelope door. Werkt met adblockers, houdt de CSP eenvoudig, en vereist geen serverconfiguratie. Wel nieuwe code die uitgaand verkeer doet
B) Direct naar Sentry, en de Sentry-ingest-origin in de CSP toestaan — eenvoudiger, geen extra endpoint, maar fouten van gebruikers met een adblocker komen niet aan
C) Direct nu, tunnel als vervolgpunt als blijkt dat er te veel gemist wordt
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]: A
---
## Question 2 — Mag Sentry gebruikersgegevens meesturen?
**Context**: `Sentry.AspNetCore` heeft een instelling `SendDefaultPii`. Staat die aan, dan stuurt Sentry bij elke fout ook het IP-adres, de gebruikersnaam en request-headers mee — inclusief cookies. In deze applicatie zit in die cookies de `refreshToken`.
Standaard staat de instelling uit. SECURITY-03 verbiedt expliciet het loggen van secrets en PII.
A) Uit laten — geen PII, geen cookies, geen IP. Fouten bevatten dan alleen de technische context, wat voor diagnose vrijwel altijd genoeg is
B) Aan, maar met een filter dat de `refreshToken`-cookie en de `Authorization`-header verwijdert — meer context bij een fout, met het gevoelige eruit gehaald
C) Aan zonder filter — maximale context
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]: B
---
## Question 3 — Welk logniveau in productie?
**Context**: `appsettings.json` staat nu op `Warning`. Dat betekent dat een normale productie-run vrijwel niets logt — geen opstartmeldingen, geen module-discovery, geen migratie-uitkomst. Juist die drie zijn na een deploy het interessantst, en `ModuleOrchestrator` logt op `Information` dat hij modules gevonden heeft (bij falen logt hij een waarschuwing, maar hij stopt niet).
Je koos structured logging naar Sentry op niveaus verder dan alleen exceptions (Q16 = C).
A) `Information` als standaard in productie, met `Microsoft.AspNetCore` op `Warning` — dan zie je opstart, modules en migraties, zonder request-ruis
B) `Warning` behouden en de belangrijke opstartmeldingen expliciet naar `Warning` tillen — minimale logvolume, maar dan staat "alles ging goed" als waarschuwing in het log
C) `Information` voor alles, inclusief het framework — meeste inzicht, meeste volume
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:C
---
## Question 4 — Welke gebeurtenissen zijn het waard om op te alarmeren?
**Context**: SECURITY-14 vraagt alerting op authenticatiefouten en autorisatieschendingen. De applicatie moet die dus eerst als gebeurtenis uitsturen voordat je er in Sentry een alertregel op kunt zetten.
Kandidaten die deze applicatie kan onderscheiden. Meerdere letters mogen (bijv. `A, B, D`).
A) Mislukte logins — meerdere achter elkaar duidt op een aanval of een vergeten wachtwoord
B) Geweigerde autorisatie op een beveiligd endpoint — iemand probeert iets waarvoor hij geen rechten heeft
C) Een geweigerde master-API-key op `/api/v1/master/*` of `/api/v1/SlaveStatus` — dat betekent óf een aanvaller, óf een echt probleem met de key ring
D) Een geweigerde admin-bypass op de availability-gate — sinds de FR-24-fix betekent dit iemand die het met een ongeldig token probeerde
E) Rate limiting die aanslaat op de login-endpoints
F) Migratiefouten bij het opstarten — geen beveiligingsgebeurtenis, maar wel iets waarvan je direct wilt weten
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:A, B, C, D, E, F
---
## Question 5 — Umami op de admin-SPA: altijd, of respecteren wat de browser vraagt?
**Context**: je koos Umami op zowel de publieke website als de admin-SPA (Q20 = B). De admin-SPA is een intern beheerscherm, en de gebruikers daarvan zijn jouw klanten en hun medewerkers — herkenbare, kleine groepen.
Umami is privacyvriendelijk (geen cookies, geen persoonsgegevens), dus dit is geen juridische vraag maar een keuze over verwachtingen.
A) Altijd meten in test en productie, nooit lokaal — eenvoudig en consistent
B) Altijd meten, maar de `Do Not Track`-voorkeur van de browser respecteren en dan niets laden
C) Alleen de publieke website meten en de admin-SPA overslaan, in afwijking van Q20 — beheerders worden dan niet gemeten
X) Anders (beschrijf hieronder na de [Answer]:-tag)
[Answer]:A
@@ -0,0 +1,54 @@
# Functional Design Plan — U4 Observability Integration
**Unit**: U4 Observability Integration
**Round**: R2 (with U3 Security Headers & CSP)
**Requirements**: FR-13, FR-14, FR-15, FR-16, FR-19
**Components**: C-08, C-09, C-14, C-15, U4 portion of C-16
**Scope boundary**: this stage designs *behaviour* — what is reported, when, and what happens when a service is absent or unreachable. The *pattern* decisions (structured-log shape, correlation-ID mechanism per OPEN-01, alert-rule design) belong to this unit's NFR Design, which follows.
---
## Step 1: Analyze unit context
- [x] Read the U4 definition from `unit-of-work.md`
- [x] Read FR-13 through FR-16 and FR-19 from `requirements.md`
- [x] Read the reference project's Sentry and Umami implementation for the frontend
- [x] Note the coupling to U3: the origins this unit introduces must be permitted by U3's CSP
## Step 2: Design backend error and log reporting
- [x] Define behaviour when no Sentry DSN is configured
- [x] Define behaviour when Sentry is configured but unreachable
- [x] Define what is sent and what is deliberately withheld — see Question 2
- [x] Define the production log level — see Question 3
- [x] Define environment and release tagging
## Step 3: Design security event emission
- [x] Define which events are emitted for alerting — see Question 4
- [x] Define what context each event carries, and what it must never carry
- [x] Confirm no event carries a password, token or PII
## Step 4: Design frontend observability
- [x] Define Sentry initialisation and the absent-DSN path
- [x] Define the transport route to Sentry — see Question 1
- [x] Define Umami inclusion behaviour and its absent-configuration path — see Question 5
- [x] Define behaviour in local development
## Step 5: Design the same-origin API base URL
- [x] Define resolution when `VITE_API_BASE_URL` is absent or empty
- [x] Define resolution when an explicit absolute URL is supplied
- [x] Define validation behaviour for a malformed value
- [x] Confirm the local master and slave development setups keep working
## Step 6: Define business rules
- [x] Enumerate reporting rules
- [x] Enumerate degradation rules
- [x] Enumerate configuration-resolution rules
- [x] Identify error and edge-case scenarios
## Step 7: Generate artifacts
- [x] Generate `business-logic-model.md`
- [x] Generate `business-rules.md`
- [x] Generate `domain-entities.md`
- [x] Generate `frontend-components.md`
- [x] Validate all diagrams against the Mermaid standards
- [x] Verify Security Baseline compliance for this unit's design