Threat model
Audience
PLANA staff, customer CTOs, security reviewers performing due diligence.
This document captures the attacker model PLANA Pulse is built against, the assets we protect, and the layered defences in place. It is updated when new threats emerge or when defences materially change.
What we protect
| Asset | Priority |
|---|---|
| Customer business data in tenant Odoo databases | P0 |
| Customer authentication credentials (passwords, TOTP secrets, OIDC sessions) | P0 |
| Customer financial / banking data | P0 |
| PLANA platform secrets (SOPS-encrypted; API keys to third parties) | P0 |
| PLANA staff credentials | P0 |
| Customer audit trails (who did what, when) | P1 |
| Customer documents in filestore | P1 |
| Operational telemetry (logs, metrics) | P2 |
| Source code | P2 (most is open source policy; some PLANA-specific) |
What we don't protect against (out of scope)
| Threat | Why out of scope |
|---|---|
| Nation-state-level adversary with persistent zero-days | Different magnitude of investment than our budget supports |
| Physical access to Exoscale's Sofia data centre | Exoscale's responsibility, contractual |
| Compromise of the customer's own endpoint / phone | Customer's responsibility (we provide 2FA, audit logs) |
| Compromise of upstream Odoo or OCA source code | We monitor; we cannot prevent |
Attacker categories
1. Opportunistic external attacker
Goal: profit (extortion, credential theft, crypto-mining), no specific target. Capability: known CVEs, off-the-shelf exploit kits, low patience.
Defences:
- TLS-only ingress (Let's Encrypt, no fallback to HTTP) at the Envoy Gateway
- CrowdSec ExtAuthz blocking known-bad IPs at the gateway (fail-closed)
- Coraza WAF (OWASP CRS) catching injection / traversal / XSS patterns before they reach the workload (fail-closed)
- Up-to-date base images via the Forgejo registry mirror; CI rebuild on base image change
- Read-only root filesystem on every workload pod; no shell binary in production images
2. Authenticated customer (looking for cross-tenant data)
Goal: access another tenant's data through the platform. Capability: valid login on their own tenant, possibly some technical sophistication with Odoo's developer mode, possibly a workspace API key.
Defences:
- DB-per-tenant on pg01;
dbfilter=^%h$ties hostname to DB - HTTPRoute keyed on hostname; cross-tenant URL doesn't reach a different DB
- Filestore SubPath mount; cannot escape into another tenant's directory
/web/database/*endpoints blocked at the Envoy Gateway level (since 2026-05-20)- Redis session and bus channel prefixes per DB (
plana_session_redis,plana_bus_redis) - Workspace API keys scoped to one workspace; cannot enumerate or read others
- See Tenant isolation for the layered guarantee model
3. Authenticated customer (looking for privilege escalation within their own tenant)
Goal: a non-admin tenant user gains admin within the same workspace. Capability: knowledge of Odoo internals, possibly XSS payload.
Defences:
- Odoo's standard record rules + groups + multi-company filters
base_user_role+plana_user_rolesfor role-based ACL above Odoo groups- CSP headers via Envoy Gateway response filter (limiting inline scripting)
- Strict file-type / size validation on attachment uploads in
plana_*modules - 2FA optional but recommended per workspace; admins should require it on their own accounts
4. Compromised CI / supply chain
Goal: insert malicious code via a CI build. Capability: compromise of a maintainer's account or of an upstream package.
Defences:
- Self-hosted Forgejo CI — no public-internet pipelines run our builds
- Restricted CI runners (no privileged mode, no docker.sock)
- Image signing via Kaniko's Cosign attestations (in pilot)
- Dependency pinning +
oca-syncweekly cron audits OCA upstreams; large unexpected changes generate a PR for manual review - 2FA mandatory on every Forgejo account
- SOPS for secrets — CI cannot decrypt SOPS files without the age key, which is not in CI env
5. Compromised staff account
Goal: extract data or sabotage. Capability: valid staff session, possibly elevated.
Defences:
- Authentik enforces TOTP on all staff accounts
- Tenant impersonation requires membership in the
tenant-impersonatorsgroup; every impersonation event is audit-logged with the source user - Loki retains every K8s API write for 90 days via the audit-webhook
- Vaultwarden requires Authentik SSO with TOTP — no static password access
- SOPS age key is per-machine, NOT shared between staff; revoking a staff member requires re-encrypting affected secrets (procedure documented)
- Most production changes go through Flux GitOps PRs, with at least one reviewer; direct cluster access is rarely needed
6. Insider threat
Same as compromised staff but the actor is deliberate. Same defences plus:
- All work in shared repos with PR review
- Production access is logged
- No single staff member has all SOPS keys + Authentik admin + cluster admin simultaneously
- Termination procedure removes access from Authentik, Forgejo, the SKS cluster, Vaultwarden, and the SOPS age key list in the same hour
7. Network attacker (on-path)
Goal: intercept or modify traffic. Capability: passive observation, active injection of TCP/TLS, possibly DNS hijacking.
Defences:
- TLS 1.2+ enforced at the gateway; weak ciphers disabled
- HSTS header with long max-age + preload
- DNSSEC enabled on planapulse.com and planapulse.app (in progress)
- Internal traffic (pod-to-pod) is on Calico's CNI overlay — not on the public internet
- pg01 and Redis listen on private IPs only; reachable through the Exoscale Private Network, not via the public LB
Common attack chains we monitor for
| Chain | Where caught |
|---|---|
Credential stuffing on auth.planapulse.com | Authentik throttling + Loki anomaly query |
| SQL injection in Odoo URL parameters | Coraza WAF + Odoo's parametrized queries |
Path traversal in /web/binary/saveas | Coraza CRS rules |
| Session hijack via leaked JWT | Token TTL 15 min; refresh requires the refresh cookie which is HttpOnly + Secure |
Exfiltration via large XML-RPC read | pulse-account-api rate-limits per workspace |
| Lateral movement after pod compromise | NetworkPolicies + restricted Pod Security Standard |
| Persistent backdoor in a base image | Image rebuild on every push; CI inspects pip list and apt list --installed diff |
Disclosure
If you find a vulnerability, please email security@plana.solutions. We acknowledge within 1 business day and aim for a fix within 7 days for critical issues, 30 days for high, 90 days for medium.
We do not currently run a bug bounty programme. We will credit responsible disclosures by name in the Compliance page unless asked to remain anonymous.
Defence depth in one diagram
┌─────────────────────────────────────┐
│ Customer │
│ (2FA optional, recommended) │
└────────────────┬────────────────────┘
│ HTTPS, HSTS, CSP
▼
┌─────────────────────────────────────┐
│ CrowdSec ExtAuthz │
│ (fail-closed, IP reputation) │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Coraza WAF (OWASP CRS) │
│ (fail-closed, payload inspection) │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Envoy Gateway │
│ (TLS termination, route matching) │
└────────────────┬────────────────────┘
│ HTTP, internal
▼
┌─────────────────────────────────────┐
│ Application │
│ (Odoo / pulse-* / ai-agents) │
│ - Pod Security: Restricted │
│ - read-only rootfs │
│ - Drop CAP_*, non-root │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ NetworkPolicies │
│ (egress default-deny, allow-list) │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Database / Redis / NFS │
│ (private network, auth required) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Audit log (Loki, 90d) │
└─────────────────────────────────────┘Where to read more
- Network policies — the per-namespace catalogue
- WAF and CrowdSec — the edge defences in detail
- Secrets management — SOPS, Vaultwarden, rotation
- Audit logging — what we capture and for how long
- Tenant isolation — the cross-tenant cousin of this page
- Compliance — GDPR, retention, DPA