⚠  DRAFT

Security Whitepaper

Product: Exceptao / paraKSCol / Cyberzgodnošć EDU  ·  Last updated: 2026-05-15

Audience: Prospective customers, security reviewers, auditors, compliance officers, pen-testers.

This document is a public-facing summary of the platform’s security architecture. Every claim here is something we expect to be asked about during a security review; every answer should be verifiable through the audit log, the verification endpoint, or the DPA annexes.

We believe in not overclaiming. The final section (“What we don’t do yet”) is honest about the gaps.

1. Security philosophy

This product is sold to security professionals — CISOs, security engineers, compliance officers — who will scrutinise it before trusting it with their organisation’s compliance data. Three principles guide every design decision:

1. Fail closed. If a security control cannot run — Vault unreachable, audit chain broken, backup provider down, RLS misconfigured — the system refuses to operate the affected surface and surfaces the failure explicitly. It does not silently degrade, skip the control, or log a warning and continue. Compliance software that silently degrades is worse than no software.

2. The database is the security boundary. Tenant isolation is enforced by Postgres Row-Level Security at the database layer, not by application-layer filters. A bug in application code that forgets to filter by tenant is harmless: the database returns zero rows because the RLS policy blocks the query. Application-layer filtering is defence-in-depth, not the primary control.

3. Tamper-evidence over tamper-prevention. Any operator with sufficient database access can in theory modify logs and backups. The architecture does not assume this is impossible — it makes any tampering detectable and traceable through the hash chain, anchor timestamps, and the verification endpoint. An organisation can verify the integrity of their own audit log at any time without trusting the Operator.

2. Tenant isolation

2.1 Postgres FORCE ROW LEVEL SECURITY

Every tenant-scoped table has the following applied at migration time:

ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;
ALTER TABLE <table> FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON <table>
  USING (tenant_id::text = current_setting('app.current_tenant_id', true));

FORCE ROW LEVEL SECURITY is the critical line: it applies the policy even to the table owner. The application runtime Postgres role (app) is not a superuser and is not the table owner (tables are owned by migrator). The app role is constrained by RLS on every query — it cannot bypass RLS even if an application bug constructs a query that omits the tenant filter.

The GUC pair (app.current_tenant_id, app.current_org_unit_id) is set within a SET LOCAL block at the start of each request’s database transaction:

# apps/tenants/context.py
with connection.cursor() as cur:
    cur.execute("SET LOCAL ROLE app")
    cur.execute("SET LOCAL app.current_tenant_id = %s", [str(tenant.id)])
    cur.execute("SET LOCAL app.current_org_unit_id = %s", [str(org_unit.id)])

SET LOCAL means the GUC values expire at transaction end. pgbouncer runs in transaction pooling mode — connections are returned to the pool at transaction end, ensuring no GUC value leaks into a subsequent request’s transaction.

2.2 Database role split

RolePrivilegesUsed by
migratorDDL; owns all tables; BYPASSRLSDjango migrations only — never used at runtime
appSELECT, INSERT, UPDATE, DELETE on business tables; constrained by RLSApplication runtime (Django ORM, DRF views)
audit_writerINSERT only on audit_log; no UPDATE, no DELETEAudit service (writes only)
superadminBYPASSRLS; SELECT on all tablesOperator super-admin panel — every action audit-logged

The app role cannot read, update, or delete audit log rows written by audit_writer. The superadmin role is used only by the Operator’s cross-tenant administrative panel, and every action taken under superadmin is itself audit-logged.

2.3 Organisation unit (sub-tenant) isolation

A second isolation dimension — org_unit_id — scopes access for site-bound roles in multi-institution deployments (e.g. a JST tenant with nested school units). An ITSO assigned to School A sees only School A’s ControlTasks, incidents, and samoidentyfikacja records. This scoping is enforced by RLS at (tenant_id, org_unit_id), not application code.

2.4 Region isolation

Region is set at Tenant creation and is an immutable attribute. Customer business data does not cross regions. The EU region (Frankfurt) and the PL region (Warsaw — when active) are physically and logically separate deployments. Control-plane metadata (tenant routing, subscription records) is stored in the EU region and contains no customer business data.

3. Authentication

3.1 Local accounts

3.2 WebAuthn passkeys

3.3 OIDC SSO

3.4 SAML 2.0 SSO

3.5 Sessions

4. Authorisation

5. Audit log integrity

5.1 Append-only enforcement

The Postgres audit_writer role has INSERT privilege on audit_log and no UPDATE or DELETE. The CI/CD pipeline verifies this privilege set on every deployment by running:

SELECT has_table_privilege('audit_writer', 'audit_log', 'UPDATE');
-- must return false
SELECT has_table_privilege('audit_writer', 'audit_log', 'DELETE');
-- must return false

If either check fails, the deployment is blocked.

5.2 SHA-256 hash chain

Each row in audit_log contains a row_hash field computed as:

row_hash_n = sha256( prev_hash_(n-1) || canonical_json(row_n) )

The hash is computed by the application before the INSERT and stored alongside the row. The audit_writer role inserts both the row data and the hash in a single atomic operation.

5.3 Verification endpoint

GET /api/audit/verify walks the full hash chain for the requesting Tenant and returns:

{
  "ok": true,
  "broken_at": null,
  "total_rows": 47291,
  "first_row_at": "2026-01-15T09:00:00Z",
  "last_row_at": "2026-05-15T14:23:11Z",
  "last_anchor_at": "2026-05-15T14:00:00Z",
  "schema_versions_seen": [1]
}

If the chain is broken, ok is false and broken_at contains the ID and timestamp of the first broken row. This endpoint is available to all Users with the auditor role and to the Tenant Admin.

5.4 Hourly anchor snapshots

A Celery beat job runs every hour and records the current row_hash of the latest audit log row per Tenant into an audit_anchor table. This table is written by the superadmin role (not audit_writer) and is read-only from the application’s perspective. Anchors provide a lightweight checkpoint: to verify the chain from the last anchor point, you only need to walk rows since the last anchor, not the full chain.

5.5 Boot-time chain integrity check

On application startup, the system: (1) reads the latest row_hash from audit_log for each Tenant; (2) reads the latest stored anchor from audit_anchor for each Tenant; (3) compares them. A mismatch means rows were added, modified, replaced, or the database was restored to a previous state since the last anchor. On mismatch, a chain_discontinuity anchor event is written and the Operator’s on-call is alerted. This is a deliberate design: any restore or tampering is detectable.

5.6 Right-to-erasure pseudonymisation

On a verified GDPR Art. 17 erasure request, the actor-identity fields in audit log rows (actor_email, actor_ip) are replaced with stable tombstones (actor_tombstone_<uuid>). The hash chain is recomputed over the tombstone value, preserving chain integrity. The event record (action, timestamp, target) is retained. Legal basis: legitimate interest in audit integrity (GDPR Art. 6(1)(f)).

6. Encryption

6.1 In transit

6.2 At rest

6.3 HTTP security headers

HeaderValue
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload
Content-Security-PolicyNonce-based; default-src 'self'; script sources require nonce or hash; no unsafe-inline or unsafe-eval
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Cross-Origin-Opener-Policysame-origin
Cross-Origin-Resource-Policysame-origin
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=() (deny by default)

7. Secret management

8. Network architecture

9. Backups and disaster recovery

10. Supply chain security

11. Vulnerability management

SeverityCVSS scorePatch SLA
Critical≥ 9.072 hours from confirmed vulnerability
High7.0–8.914 days
Medium4.0–6.930 days
Low< 4.0Next minor release

12. Incident response

The Operator maintains a documented Incident Response (IR) runbook covering: detection and initial triage, containment, eradication, recovery, post-mortem, and regulatory notification.

Jurisdiction / lawSupervisory authority notificationAffected individual notification
GDPR / EU≤ 72 hours from awareness (Art. 33)Without undue delay for high-risk breaches (Art. 34)
UK GDPR≤ 72 hours from awarenessWithout undue delay for high-risk breaches
Poland (KSC / NIS2)24h early warning; 72h incident notification; 30d final report (Art. 23 NIS2)Per applicable law
Australia (Privacy Act 1988, NDB Scheme)As soon as practicableAs soon as practicable
Canada (PIPEDA)As soon as feasibleAs soon as feasible

The Operator’s obligation to notify the Controller (Tenant) of a personal data breach is ≤ 72 hours from awareness, per the DPA at /legal/dpa §8. This enables the Tenant to meet its own supervisory authority notification deadline.

13. What we don’t do yet

We believe in not overclaiming security certifications or capabilities. The following are planned but not yet delivered:

ItemStatusPlan
SOC 2 Type II certificationNot yet — audit engagement not startedArchitecture designed for SOC 2 readiness from day one. All control evidence (access reviews, change management, backups, audit logs) is auto-generated. Audit engagement planned post-GA.
ISO 27001 certificationNot yetPlanned after SOC 2 Type II.
FedRAMPNot applicable in v1No US federal customers targeted in v1.
RFC 3161 external timestamp authority (TSA) for audit anchorsArchitecture supports it; not yet wired to a production TSAPhase 2. Would provide externally verifiable timestamps for audit anchors that are not under the Operator’s control.
Recurring external penetration testFirst test planned pre-first-paying-customerAnnual cadence once revenue supports the cost of a quality engagement.
Bug bounty programNot yetPhase 2/3 — Intigriti or HackerOne.
Warm-standby DR (RTO ≤ 30 min)Not yet — current RTO ≤ 4 hoursPhase 3 — requires a hot-standby database replica and automated failover.
LUKS remote attestation / automated unsealing[TO BE COMPLETED: document production LUKS unlock mechanism]Currently requires manual key entry on VPS reboot.

14. Contact

PurposeContact
Security disclosuressecurity@exceptao.com
Privacy / DPAprivacy@exceptao.com
.well-known/security.txtPublished at each brand domain
PGP public key[TO BE COMPLETED: publish at .well-known/security.txt]