Threat Model (STRIDE)
Condensed summary — see docs/security/THREAT_MODEL.md in the repo for the full
analysis (admin-console privileged-access model, WebAuthn/passkey ceremony detail,
public-endpoint entropy/rate-limit accounting, self-service privacy-request execution
flow, and residual-risk tracking). Last reviewed 2026-08-15, and several of its findings
have been overtaken by fixes since — the corrections are noted inline below and in the full
document. Covering the full live surface: the core passport module, the admin console, public QR/lost-pet endpoints, and
self-service privacy requests.
| Threat | Example | Mitigation |
|---|---|---|
| Spoofing | forged actor/tenant | OIDC tokens; server-side membership; dev bypass off in prod |
| Tampering | editing another owner's animal | OpenFGA checks; deny by default; parameterized SQL |
| Repudiation | denying a delegation grant, or an admin denying a data view | append-only immutable audit; admin record-level reads require a logged justification (self-audit-on-read) |
| Information disclosure | chip number / address via public QR; a compromised admin browsing cross-tenant data | separate emergency projection (medical summary included, no identity numbers); masked relay; justification-before-reveal on admin detail views |
| Denial of service | body floods, token enumeration, admin login brute force | body caps; per-IP/per-actor rate limiting (surface-wide, report, relay-message, and WebAuthn-login limiters — see docs/security/CI_AND_WEB_HARDENING.md); uniform 404 on unknown/disabled tokens. Read the per-IP claim with ADR-030 in hand: until 2026-08-28 every one of those limiters was a single global bucket behind Traefik, and Keycloak's own brute-force protection was switched off — both fixed and verified that day. See Security Baseline |
| Elevation of privilege | delegate self-escalates; an admin approves their own sensitive request | can_manage_delegates = owner/co-owner only (tested); dual-control approver-must-differ check (currently covers one action type, evidence export) |
Admin console (new since the original baseline)
A compromised platform_admin account is a cross-tenant threat, unlike a compromised
owner account (OpenFGA-confined). Controls: justification-before-reveal on record
detail views (not list views), dual control on evidence export (not yet on
suspend/moderate/dispute actions — a known, tracked gap), and audit coverage on every
write path.
The "optional MFA" finding this review raised has since been fixed, and this page said
otherwise until 2026-08-28. internal/server/server.go now wraps
admin.RegisterAuthenticated(r) in auth.RequireAuthMethod("webauthn"), landed 2026-08-18 —
three days after this review's date, which is why the review did not see it.
The fix is conditional, and the condition is the residual finding. That gate sits inside
if webauthnHandler != nil, so a deployment with no WebAuthn RP configuration
(AdminWebAuthnRPID/RPOrigins, which config.Load deliberately does not hard-require
outside development) still falls back to role-only gating — the else branch says so in
words. Check the deployment's configuration before assuming the gate is active.
Domain abuse cases
- Stalking via lost-and-found — never expose exact owner/home/real-time tracker location; contact via masked relay; audit GPS access.
- QR token guessing — ~244-bit unguessable tokens; enumeration yields uniform failure.
- Anonymous relay-message spam — real vector, mitigated by a dedicated per-IP rate limit. That limit is 5 per hour, and until the trusted-proxy fix landed on 2026-08-28 the whole internet shared those five: a small budget is hurt worse by a shared bucket than a large one. It is now genuinely per-IP (ADR-030).
- Self-service account deletion — creating a request never deletes data by itself; execution requires a separate admin-side action, gated and audited independently of the UI's own typed confirmation.