Files
HealthLog/docs/diagrams/05-security-model.svg
T
Marc-André Bombeck 88028fa082 v1.4.37 — final web polish before iOS focus
Carries the v1.4.36 perf carry-over for the full /api/analytics
route (correlations + healthScore + bp_in_target on the rollup-
coverage probe, 111s cold worst-case down to ~1.5-3s on power-user
accounts), consolidates Apple Health step samples into a one-row-
per-day list view with chevron drill-down and a nightly drain
(~50x table compression once it converges), restores
IntakeHistoryListV2 to the pre-v1.4.28 contract, converges every
Coach affordance behind a single feature gate with an invariant
test pin, lifts the medication-detail Mounjaro card to byte-
equivalent parity with Ramipril, brings the Insights overview
hero row to height parity, hardens getClientIp behind a new
TRUST_CF_CONNECTING_IP env flag, ships the Arztbericht hero card
on Settings -> Export, adds the Medikamenteneinnahme quick-add
overlay to the dashboard, drops the timezone override toggle for
a silent browser-zone auto-seed, and fixes the v1.4.36 red CI
workflows. Documentation refreshed across all three repos with
five new architecture diagrams in the Dracula palette. Unit suite
4354 -> 4486 passing; typecheck + lint clean.
2026-05-17 15:36:38 +02:00

120 lines
8.3 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 620" font-family="ui-sans-serif, -apple-system, Segoe UI, Roboto, sans-serif" role="img" aria-labelledby="title desc">
<title id="title">HealthLog security model</title>
<desc id="desc">Three concentric layers: an outer auth perimeter (passkey-first WebAuthn, password fallback with Argon2id), a session perimeter (HttpOnly SameSite=Strict cookie, Postgres-backed sessions with sliding expiry), and the encrypted core (AES-256-GCM with versioned keys for secrets at rest). Side rails cover the rate limiter, audit log, HMAC API tokens, CSP/HSTS headers and SSRF-guarded outbound test endpoints.</desc>
<rect x="0" y="0" width="1200" height="620" fill="#282a36" />
<text x="600" y="40" fill="#f8f8f2" font-size="22" font-weight="700" text-anchor="middle">Security model</text>
<text x="600" y="64" fill="#6272a4" font-size="13" text-anchor="middle">The path a request takes through the auth, session and encryption layers.</text>
<!-- Outer auth layer -->
<g>
<rect x="240" y="100" width="720" height="440" rx="20" fill="none" stroke="#8be9fd" stroke-width="2" />
<text x="270" y="124" fill="#8be9fd" font-size="13" font-weight="700">Layer 1 — Auth perimeter (WebAuthn first)</text>
</g>
<!-- Session layer -->
<g>
<rect x="320" y="160" width="560" height="320" rx="18" fill="none" stroke="#ffb86c" stroke-width="2" />
<text x="350" y="184" fill="#ffb86c" font-size="13" font-weight="700">Layer 2 — Session perimeter (server-side sessions)</text>
</g>
<!-- Encryption core -->
<g>
<rect x="420" y="220" width="360" height="220" rx="16" fill="none" stroke="#50fa7b" stroke-width="2" />
<text x="450" y="244" fill="#50fa7b" font-size="13" font-weight="700">Layer 3 — Encrypted core (AES-256-GCM)</text>
<rect x="450" y="265" width="300" height="50" rx="8" fill="#44475a" stroke="#50fa7b" stroke-width="1" />
<text x="600" y="285" text-anchor="middle" fill="#f8f8f2" font-size="12" font-weight="600">Secrets at rest</text>
<text x="600" y="302" text-anchor="middle" fill="#bd93f9" font-size="11">Withings tokens · provider keys · VAPID · Codex</text>
<rect x="450" y="325" width="140" height="50" rx="8" fill="#44475a" stroke="#50fa7b" stroke-width="1" />
<text x="520" y="345" text-anchor="middle" fill="#f8f8f2" font-size="12" font-weight="600">Versioned keys</text>
<text x="520" y="362" text-anchor="middle" fill="#bd93f9" font-size="11">ENCRYPTION_KEYS</text>
<rect x="610" y="325" width="140" height="50" rx="8" fill="#44475a" stroke="#50fa7b" stroke-width="1" />
<text x="680" y="345" text-anchor="middle" fill="#f8f8f2" font-size="12" font-weight="600">Online rotation</text>
<text x="680" y="362" text-anchor="middle" fill="#bd93f9" font-size="11">rotate-encryption-key</text>
<rect x="450" y="385" width="300" height="40" rx="8" fill="#44475a" stroke="#50fa7b" stroke-width="1" />
<text x="600" y="409" text-anchor="middle" fill="#bd93f9" font-size="11">Bearer tokens stored as HMAC-SHA256 digests</text>
</g>
<!-- Auth nodes (cyan) -->
<g>
<rect x="260" y="200" width="180" height="60" rx="10" fill="#44475a" stroke="#8be9fd" stroke-width="1.5" />
<text x="350" y="224" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Passkey (WebAuthn)</text>
<text x="350" y="244" text-anchor="middle" fill="#bd93f9" font-size="11">SimpleWebAuthn v13</text>
<rect x="260" y="280" width="180" height="60" rx="10" fill="#44475a" stroke="#8be9fd" stroke-width="1.5" />
<text x="350" y="304" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Password fallback</text>
<text x="350" y="324" text-anchor="middle" fill="#bd93f9" font-size="11">Argon2id + zxcvbn</text>
</g>
<!-- Session nodes (orange) -->
<g>
<rect x="340" y="380" width="220" height="60" rx="10" fill="#44475a" stroke="#ffb86c" stroke-width="1.5" />
<text x="450" y="404" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">HttpOnly · SameSite=Strict</text>
<text x="450" y="424" text-anchor="middle" fill="#bd93f9" font-size="11">cookie · 30d sliding expiry</text>
<rect x="640" y="380" width="220" height="60" rx="10" fill="#44475a" stroke="#ffb86c" stroke-width="1.5" />
<text x="750" y="404" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Postgres sessions</text>
<text x="750" y="424" text-anchor="middle" fill="#bd93f9" font-size="11">requireAuth · requireAdmin</text>
</g>
<!-- Side rails -->
<g>
<rect x="40" y="160" width="180" height="60" rx="10" fill="#44475a" stroke="#ff5555" stroke-width="1.5" />
<text x="130" y="184" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Rate limiter</text>
<text x="130" y="204" text-anchor="middle" fill="#bd93f9" font-size="11">sliding window · per IP+user</text>
<rect x="40" y="240" width="180" height="60" rx="10" fill="#44475a" stroke="#ff5555" stroke-width="1.5" />
<text x="130" y="264" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Audit log</text>
<text x="130" y="284" text-anchor="middle" fill="#bd93f9" font-size="11">every sensitive op + IP</text>
<rect x="40" y="320" width="180" height="60" rx="10" fill="#44475a" stroke="#ff5555" stroke-width="1.5" />
<text x="130" y="344" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">CSP + HSTS</text>
<text x="130" y="364" text-anchor="middle" fill="#bd93f9" font-size="11">nonce CSP · X-Frame-Options</text>
<rect x="40" y="400" width="180" height="60" rx="10" fill="#44475a" stroke="#ff5555" stroke-width="1.5" />
<text x="130" y="424" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">SSRF guard</text>
<text x="130" y="444" text-anchor="middle" fill="#bd93f9" font-size="11">outbound test endpoints</text>
<rect x="980" y="160" width="180" height="60" rx="10" fill="#44475a" stroke="#bd93f9" stroke-width="1.5" />
<text x="1070" y="184" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">API tokens</text>
<text x="1070" y="204" text-anchor="middle" fill="#bd93f9" font-size="11">hlk_* Bearer · HMAC stored</text>
<rect x="980" y="240" width="180" height="60" rx="10" fill="#44475a" stroke="#bd93f9" stroke-width="1.5" />
<text x="1070" y="264" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Refresh tokens</text>
<text x="1070" y="284" text-anchor="middle" fill="#bd93f9" font-size="11">24h access · rotated refresh</text>
<rect x="980" y="320" width="180" height="60" rx="10" fill="#44475a" stroke="#bd93f9" stroke-width="1.5" />
<text x="1070" y="344" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Off-host backup</text>
<text x="1070" y="364" text-anchor="middle" fill="#bd93f9" font-size="11">AES-GCM · S3-compatible</text>
<rect x="980" y="400" width="180" height="60" rx="10" fill="#44475a" stroke="#bd93f9" stroke-width="1.5" />
<text x="1070" y="424" text-anchor="middle" fill="#f8f8f2" font-size="13" font-weight="600">Per-user scope</text>
<text x="1070" y="444" text-anchor="middle" fill="#bd93f9" font-size="11">no cross-account reads</text>
</g>
<!-- Arrows: side rails into perimeters -->
<g stroke="#ff5555" stroke-width="1.2" stroke-dasharray="3 3" fill="none" marker-end="url(#a5-r)">
<path d="M220 190 L 260 220" />
<path d="M220 270 L 320 320" />
<path d="M220 350 L 320 350" />
<path d="M220 430 L 320 410" />
</g>
<g stroke="#bd93f9" stroke-width="1.2" stroke-dasharray="3 3" fill="none" marker-end="url(#a5-pu)">
<path d="M980 190 L 880 410" />
<path d="M980 270 L 880 410" />
<path d="M980 350 L 780 280" />
<path d="M980 430 L 780 410" />
</g>
<!-- Footnote -->
<text x="600" y="585" fill="#6272a4" font-size="11" text-anchor="middle">No data leaves the host unless an integration explicitly asks for it. Bearer tokens never elevate to admin — admin actions require a cookie session.</text>
<defs>
<marker id="a5-r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#ff5555" /></marker>
<marker id="a5-pu" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#bd93f9" /></marker>
</defs>
</svg>