Threat ModelEN
Threat Model & Risk Register
Version: 2026-08-28 (aligned with commits
f7355b2/117e35b/cc46b20/d81c053) Companion docs:rbac-security-model.md(controls),rbac-verification-2026-08-28.md(evidence),deployment-hardening.md(deployment controls),private-key-hygiene.md(key controls)
Registers asset trust boundaries, the attack surface, and fixed/accepted known
risks for auditors. Track items by R-<id>; append with the next number.
1. Assets
| Asset | Criticality | Location |
|---|---|---|
| Root / issuing CA private keys | highest | offline vault / keys/*/private/ (0600) or key_backend (HSM) |
Management (m-*) private keys + certs (= superadmin authority) |
high | management/users/private/ (0600) |
| Serve TLS private key | high | keys/server.key |
| Database (certs, audit chain, key digests) | high | pki.db / PostgreSQL |
Route table routes.json / role policy authz.json |
high (authorization decisions) | config dir + policy_signing signature |
| Authorization/audit chain | med-high | DB audit_log (Merkle chain) |
| Sessions / tokens / account hashes | med | DB |
2. Trust boundaries and assumptions
- Authorization trust root = CA private signing key: any entity holding a valid management certificate is a trusted principal; role/permission vector comes from cert OU/PA/scope (cert-first). Account credentials can never reach superadmin.
- TLS enforces
RequireAndVerifyClientCert(with the configured CA trust pool): no certificate → no TLS session. - Server and the trusted CA pool are operator-controlled; offline root vaulting is the highest precondition.
- Policy files (routes/authz) are PKCS#7-signed (
policy_signing.require=true); default table is strict. - Config correctness is a deployment responsibility:
init-configoutput is placeholder (its defaultauth_passwordischangeme— must be overridden before going live). All deployment controls indeployment-hardening.md.
3. Attack surface
| Surface | Exposure | Control |
|---|---|---|
| TLS API (mTLS) | cert-first authorization | chain validation, PA permissions, scope resolution |
| HTTP API (admin) | Basic/token (always operator) | internal-only, checklist B |
| ACME / OCSP / TSA endpoints | public paths | protocol auth + deployment allow-list |
| CLI and config/policy files | local actor | policy_signing, fail-closed load |
| Deploy script chain | key/cert material on disk | 0600 checks, helpers.py cert↔key pairing |
| Backup media | keys + audit chain | encrypted backups, cold-backup |
| Direct DB access | record tampering | audit Merkle chain + pki audit verify |
4. Risk register
| ID | Severity | Risk | Mitigation | Status |
|---|---|---|---|---|
| R-001 | P0 | operator cert could mint m-superadmin (OU=SuperAdmin escalation) into the management sub-CA |
management hard-exclusion: m-* superadmin-only + mTLS in hand (401/403 gate) |
✅ fixed 117e35b |
| R-002 | P0 | resolveBasicAuth/resolveAPIToken returned the DB role — superadmin via username+password without a cert |
non-cert auth always operator, no scope injection; mTLS-presence check |
✅ fixed cc46b20 |
| R-003 | P1 | routes_file load failure fell back to the lax embedded table |
fail-closed: startup panic / keep previous table on reload | ✅ fixed 117e35b |
| R-004 | P1 | embedded default route table too permissive | strict least-privilege default + deploy-time drift assertion | ✅ fixed 117e35b |
| R-005 | P2 | m-revoker default profile too broad; certs/ vs private/ confusion lets a public cert be used as a key |
ProfileMRevoker; 0600 enforcement + docs; cert↔key pairing |
✅ fixed f7355b2 / d81c053 |
| R-006 | accepted | account/password delegation only ever has operator capability (can't reach superadmin) | by design; audit rows from such logins are attribution only | accepted (security boundary) |
| R-007 | accepted | exposure of public protocol endpoints (ACME/OCSP/TSA) | deployment allow-list (checklist B); minimal info in protocols | accepted (deployment duty) |
| R-008 | accepted | HTTP admin listener if exposed to the network | checklist B forces internal-only / not exposed | accepted (deployment duty) |
| R-009 | open | sample config weak default password (auth_password: "changeme") |
checklists A/F: override before start; documented prominently | deployment duty |
5. Threat → control mapping (STRIDE)
| Threat | Control |
|---|---|
| S spoofing (fake principal) | strong mTLS + CA chain; accounts always operator; signed policy files |
| T tampering (records/tables) | audit Merkle chain + periodic AuditVerify / pki audit verify; policy_signing |
| R repudiation | audit log (user/IP/path) + chain integrity + pki report evidence |
| I information disclosure | private keys 0600; per-day salt masking of audit PII; cert≠key |
| D denial of service | rate limiting, body cap, engine backpressure (503), readyz |
| E elevation of privilege | cert-first authorization, management sub-CA gate, fail-closed routing |
6. Verification loop
- Automated:
scripts/verify-rbac-api.sh(378×2 matrix + P0 probes), results archived inrbac-verification-2026-08-28.md. - After any policy-table/auth-chain change: rerun verification → update the report → refresh the R-IDs above.
7. Change discipline
- New risk: append
R-010…with severity, mitigation, verification method. - Escalation/de-escalation: update this table and the matching
deployment-hardening.mdchecklist item.