Threat model + non-functional requirements in one place. Decisions behind it: DECISIONS.md.
Assets: provider tokens, user identities/sessions/password hashes, posts/media/schedules, the master key and DB password, internal TLS keys, plugin packages (pinned hashes), the approval decisions, audit trail, availability.
In scope: remote attacker on the web entry point · authenticated user reaching another user's data/admin functions · buggy or compromised plugin (trusted or untrusted) · compromised web container · malicious provider content (callback params, API responses, descriptors) · operator mistakes/crashes · supply chain.
Assumptions: host root can read everything · plugins are process-isolated but not sandboxed against hostile code (shared kernel, open egress) · containers are not VM boundaries · single customer per host · no external service exists: all trust decisions are local.
Most trusted components (the trusted computing base): the Controller (root, tiny), the Gateway (door + DB + auth + master key), and the Executioner (holds CAP_SETUID/SETGID, enforces plugin rights). Keep them small, sanitized and heavily tested.
| ID | Threat | Mitigation | Residual |
|---|---|---|---|
| T0 | CSRF / weak session | cookie flags, Origin check + CSRF header, hashed server-side sessions, rotation, idle/absolute expiry, rate limit (D-14) | XSS = session theft → CSP, escaping |
| T1 | User A reads user B's data | actor from session/call context only; ownership scoping in the Gateway data layer; deny by default; hidden resources = NOT_FOUND |
— |
| T2 | Malicious/compromised plugin | no DB creds/keys; no socket except its private channel to the Executioner; own UID, no caps, no_new_privs, rlimits, clean env; hash-pinned read-only plugin dir; rights check on every request; approval for untrusted plugins; audit |
can misuse what it is granted/approved, can use any egress host (N-8) |
| T3 | Plugin UI attacks the app | n/a in v1 (no plugin JS); revisit with iframe UIs | — |
| T4 | Call-context misuse | call_id is held by the Executioner, bound to (plugin, user, purpose, scope, expiry); unknown/expired/foreign ids rejected; plugin never supplies a user |
— |
| T5 | SQL injection / schema confusion | typed named operations only, never raw SQL; prepared statements; no dynamic identifiers; public CREATE revoked; fixed search_path; two DB roles |
— |
| T6 | Key/secret compromise | AEAD, key outside DB and DB backups, key_id, secrets on tmpfs per UID (master key + DB password only in the Gateway), non-loggable secret type |
host root; Gateway compromise |
| T7 | Malicious package / supply chain | install only via operator CLI → PMP; strict manifest; SHA-256 pinning, verified by the Executioner before exec; PMP is the only writer; pinned image digests; scans | signing comes later |
| T8 | SSRF / unwanted outbound | backend/dbnet have no internet; only the server container has egress; platform processes fetch no user URLs; plugins validate URLs |
egress not filtered in v1 |
| T9 | Duplicate / lost publish | durable jobs; DISPATCHED before call; OUTCOME_UNKNOWN; checkpoint + reconcile; idempotency keys; no auto-retry unless NOT_PUBLISHED |
no exactly-once promise |
| T10 | Resource exhaustion | container + per-process limits; size/rate/concurrency limits at the Gateway; quotas; bounded queues; crash-loop stop | — |
| T11 | Monitoring blind spots | Watchdog independent of data traffic; Gateway probed directly; UNKNOWN ≠ failure; Docker restarts the container if the Controller dies |
— |
| T12 | Bad backup/upgrade | DB + media + plugins volume backed up together, key separate; checksummed transactional migrations; backup before upgrade; restore drill | — |
| T13 | Gateway compromise / identity spoofing | now the biggest concentration (door, DB creds, master key, auth). Small attack surface (mTLS from web + per-process sockets only), sanitizers/fuzzing, strips caller identity fields, identity from cert/socket only, route ACL deny-by-default, negative tests | a Gateway bug can expose everything it holds → consider splitting a Core out (D-03 revisit) |
| T14 | Controller (root) abused | minimal caps; no network; no DB; only inbound channel = private Watchdog channel with 3 operations on a static list; children drop all caps; small code, tests, sanitizers | a memory bug in the Controller is high impact → keep it tiny |
| T15 | Plugin reads secrets / other processes | distinct UIDs; secrets 0400 owned by the consumer on tmpfs; non-dumpable; /proc and kill denied across UIDs; volumes owned by single UIDs; DB not reachable (separate dbnet) |
kernel escapes |
| T16 | Credential lease abuse | grant + call context; every lease audited; short lease; instant grant revocation | compromised plugin can exfiltrate tokens it legitimately leases |
| T17 | Public media endpoint abuse | 256-bit hashed tokens, short TTL, revocable, single object; validated at the Gateway; no listing; rate limit; no cookies | — |
| T18 | OAuth callback abuse | single-use state bound to session user (TTL 10 min); BFF forwards raw params; allowlisted plugin IDs; fixed redirect targets |
— |
| T19 | Malicious descriptor content | restricted schema subset; escaped text only; SVG sanitized + size-limited; strict CSP | — |
| T20 | Approval abuse (spoofed approvals, approval fatigue, CSRF on the approval UI) | only admins may answer; same session/CSRF rules as all mutations; entries created only by the Executioner (route ACL); rate limit per plugin; timeout = deny; "allow always" is per capability and shown/revocable; every request and decision audited | a distracted admin can approve something harmful |
| T21 | Executioner compromise (holds CAP_SETUID/SETGID) |
only those two caps (ambient), no DB, no secrets, no volume write; rights verified after every spawn; plugin UID pool is fixed and non-root; route ACL still limits what it can ask the Gateway | can start processes under pool UIDs and forward requests as the Executioner |
| T22 | Gateway as single point of failure / DoS | Controller restarts it; clients reconnect with backoff; jobs are durable in the DB; per-sender rate limits | outage while the Gateway is down |
| T23 | File-System attacks (traversal, oversize, type confusion) | ID-derived paths, never client names; atomic writes; magic-byte + size checks; per-owner quota; volume owned by one UID | — |
- Reliability: clean install works from the docs; Gateway not ready until migrations done; restarts lose no jobs; a plugin crash never affects other processes (stack
DEGRADED, container healthy); restart backoff 1 s→60 s, crash loop = 5 in 5 min; shutdown drains ≤ 30 s with the Watchdog stopped first,stop_grace_period45 s; restore drill before release. - Performance baseline (not a scale promise): 10 concurrent users, 100 pending jobs, 5 concurrent attempts; authenticated read BFF→Gateway→DB p95 < 500 ms; one Gateway hop p95 < 20 ms (UDS) / < 100 ms (mTLS); upload limit 100 MiB per item (chunked 64 KiB, streamed through the Gateway to File-System); everything bounded (queues, bodies, uploads, tasks, storage quotas). Defaults:
server1 GiB / 512 pids,web512 MiB / 128 pids, plugin 256 MiB. - Security: only web published, default
127.0.0.1; production refuses to run without mTLS; Argon2id; no secrets in logs; authorization tests for every sensitive operation; C++ with warnings-as-errors, ASan/UBSan in CI, clang-tidy. - Observability: JSON logs with timestamp, process, severity, request ID, event type; redaction by construction (secret wrapper type); plugin stdout/stderr tagged by the Executioner; attempts log job/attempt/plugin/outcome/timing; audit = actor, action, target, outcome, time (no secret values); retention 14 days logs, 90 days audit/jobs.
- Data: stable IDs, UTC timestamps + IANA zone, constraints for ownership/state/idempotency; migrations tested from empty and from previous release; backup = DB + media + plugins volume, key separately.
- Portability: Docker Compose on one Linux host; production images pinned by digest, never
latest.
- Cross-user: read/write attempts per resource family; user cannot grant themselves admin/capabilities; admin cannot read others' content.
- Isolation (
security-smoke.sh): as a plugin UID: cannot read/run/secrets/*,/run/postflowx/secrets/*,/proc/<other>/{environ,mem,fd},media/pluginsvolumes; cannot signal/ptrace others; has no path to the Gateway or another plugin. From the web container the DB is unreachable. Each platform process shows its assigned UID/caps (Executioner: onlySETUID/SETGID). - Hub rules: a message between two processes arrives only through the Gateway; wrong sender for a route, forged identity fields, oversized, expired deadline, unknown route/version → rejected and logged; raw SQL from web or any process → rejected.
- Controller/Watchdog:
SIGSTOPon a process →UNRESPONSIVE→ restarted via the Controller; killing the Watchdog → restarted; Controller channel accepts only 3 operations on the static list. - Executioner: hash-mismatched plugin never starts; rights deviation after spawn → killed +
START_FAILED; unknown/expired/foreigncall_idrejected; trusted-without-grant denied. - Approval (local): untrusted request blocked until allowed; deny and 60 s timeout reject; "allow always" works and can be revoked; only admins can answer; works with the
egressnetwork detached and via CLI with the web container stopped; every decision audited. - Descriptor/UI: script/HTML in plugin strings and SVG icons does not execute.
- OAuth: reused, expired, foreign-session, missing
state→ rejected. Public media: expired, revoked, guessed, cross-object tokens → rejected. - File-System: path traversal, oversize, wrong magic bytes, foreign owner, quota → rejected.
- Secrets: ciphertext useless without the key; logs/audit contain no token values; restore drill with and without the key.
- Failure injection: plugin crash, resource exhaustion, Gateway/DB outage, Executioner crash and Gateway restart during a dispatched attempt → no corruption, no duplicates (
OUTCOME_UNKNOWNpath exercised); Controller crash → Docker restarts the container.
Host root sees everything · an approved or trusted plugin can misuse what it is granted/leases and has open egress · isolation depends on kernel/runtime · the Gateway holds the most (door + DB + key + auth) and the Controller is a small privileged component · a distracted admin can approve a harmful request · provider APIs may change or reject; ambiguous publish results can't always be resolved automatically.