Skip to content

Security: PostFlowX/deploy

Security

docs/SECURITY.md

Security and quality targets

Threat model + non-functional requirements in one place. Decisions behind it: DECISIONS.md.

1. Assets and trust model

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.

2. Threats (T) → mitigations

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 —

3. Targets (NFR) — measure, don't over-engineer

  • 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_period 45 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: server 1 GiB / 512 pids, web 512 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.

4. Must-pass tests (run in CI or scripts/*-smoke.sh)

  1. Cross-user: read/write attempts per resource family; user cannot grant themselves admin/capabilities; admin cannot read others' content.
  2. Isolation (security-smoke.sh): as a plugin UID: cannot read /run/secrets/*, /run/postflowx/secrets/*, /proc/<other>/{environ,mem,fd}, media/plugins volumes; 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: only SETUID/SETGID).
  3. 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.
  4. Controller/Watchdog: SIGSTOP on a process → UNRESPONSIVE → restarted via the Controller; killing the Watchdog → restarted; Controller channel accepts only 3 operations on the static list.
  5. Executioner: hash-mismatched plugin never starts; rights deviation after spawn → killed + START_FAILED; unknown/expired/foreign call_id rejected; trusted-without-grant denied.
  6. 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 egress network detached and via CLI with the web container stopped; every decision audited.
  7. Descriptor/UI: script/HTML in plugin strings and SVG icons does not execute.
  8. OAuth: reused, expired, foreign-session, missing state → rejected. Public media: expired, revoked, guessed, cross-object tokens → rejected.
  9. File-System: path traversal, oversize, wrong magic bytes, foreign owner, quota → rejected.
  10. Secrets: ciphertext useless without the key; logs/audit contain no token values; restore drill with and without the key.
  11. Failure injection: plugin crash, resource exhaustion, Gateway/DB outage, Executioner crash and Gateway restart during a dispatched attempt → no corruption, no duplicates (OUTCOME_UNKNOWN path exercised); Controller crash → Docker restarts the container.

5. Residual risks

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.

There aren't any published security advisories