Skip to content

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

AssetPriority
Customer business data in tenant Odoo databasesP0
Customer authentication credentials (passwords, TOTP secrets, OIDC sessions)P0
Customer financial / banking dataP0
PLANA platform secrets (SOPS-encrypted; API keys to third parties)P0
PLANA staff credentialsP0
Customer audit trails (who did what, when)P1
Customer documents in filestoreP1
Operational telemetry (logs, metrics)P2
Source codeP2 (most is open source policy; some PLANA-specific)

What we don't protect against (out of scope)

ThreatWhy out of scope
Nation-state-level adversary with persistent zero-daysDifferent magnitude of investment than our budget supports
Physical access to Exoscale's Sofia data centreExoscale's responsibility, contractual
Compromise of the customer's own endpoint / phoneCustomer's responsibility (we provide 2FA, audit logs)
Compromise of upstream Odoo or OCA source codeWe 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_roles for 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-sync weekly 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-impersonators group; 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

ChainWhere caught
Credential stuffing on auth.planapulse.comAuthentik throttling + Loki anomaly query
SQL injection in Odoo URL parametersCoraza WAF + Odoo's parametrized queries
Path traversal in /web/binary/saveasCoraza CRS rules
Session hijack via leaked JWTToken TTL 15 min; refresh requires the refresh cookie which is HttpOnly + Secure
Exfiltration via large XML-RPC readpulse-account-api rate-limits per workspace
Lateral movement after pod compromiseNetworkPolicies + restricted Pod Security Standard
Persistent backdoor in a base imageImage 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

© PLANA Digital Ltd.