ILLUMENTIS.
DE

Illumentis — Security Posture

Summary of all implemented security measures, gates and configurations — as the basis for an external security review.

As of: September 26, 2026 Phase: Beta (Strato, shared host) — production environment (Hetzner) not yet live Primary source: illumentis-security-testing-lawbook.md (binding, versioned rulebook)

What this document is: a consolidated self-disclosure of the current technical and organizational security posture — every measure genuinely exists in code or server configuration and is mostly covered by automated tests, not merely planned. Where a measure is still outstanding, that is explicitly marked as such rather than omitted.

What this document cannot replace: an actually performed, independent external penetration test. This overview is the basis for such a review, not its result.

Hosting
EU providers only, no US hyperscaler — CLOUD Act avoidance
Self-hosting
Git, error tracking, grammar checking, malware scanning, secrets — all self-operated instead of SaaS
Fail-closed
Security controls reject on uncertainty instead of silently letting requests through
Defense in depth
Voters + serializer groups + tenant filter + encryption as independent layers

1 Authentication & Access Control

Password hashing implemented

bcrypt/Argon2id via UserPasswordHasherInterface, minimum cost factor deliberately set and tested (PasswordHashingCostFactorTest) instead of trusting the framework default. Real finding 2026-09-25: the Argon2id dummy hash used for login anti-enumeration was, with the default time_cost on the real server hardware, measurably faster than a genuine bcrypt login — discovered via an external timing test, recalibrated empirically (not from table values) to near-equal timing.

Timing enumeration on forgot-password/registration implemented (2026-09-25)

The same external test showed a smaller but measurable residual difference on forgot-password/self-register (synchronous DB lookup only on a hit). Fixed structurally rather than by cost-equalizing: the entire asymmetric part (lookup, branching, sending the email) now runs inside an asynchronous message handler — the controller itself does exactly the same, constant amount of work for every email address. Re-verified live: response time for an existing vs. a made-up account is no longer distinguishable.

Two-factor authentication (TOTP) implemented

Mandatory for all admin roles, optional for regular accounts. Backup codes are hashed, shown once in plaintext. Own finding fixed: a TOTP test flake resolved via a frozen clock, production tolerance (leeway=0) deliberately left strict and unchanged.

Rate limiting implemented

Dedicated policy for login, 2FA, registration, AI endpoints — each with genuine symfony/lock synchronization (otherwise unsynchronized under load).

Invite/reset tokens implemented

Cryptographically secure random values (random_bytes()), never mt_rand(). Timing-safe comparison (hash_equals()), uniform error message regardless of hit rate.

Authorization exclusively via voters implemented

Never ad-hoc if checks. Full owner/co-author/guest/admin matrix, four graduated guest tiers, two-stage removal for co-authors.

API documentation not viewable anonymously implemented (2026-09-25)

External scan finding: /api/docs exposed the OpenAPI schema (field names such as canManageUsers) to anonymous visitors as well — not a data leak (the actual endpoints were already correctly protected), but unnecessary structural disclosure. The documentation endpoint now requires login.

Platform-owner continuity implemented

Co-owner, time-limited succession delegation, master recovery (two separate physical secrets + majority approval) — no single point of failure if the sole admin access is lost.

Admin reset & security freeze implemented

Every administrative 2FA/password reset creates an immutable (append-only) audit log entry. Four-eyes principle once ≥2 admin accounts exist. Email changes are exclusively self-service, never via an admin endpoint.

Session/device management implemented

Remote lock on device loss (remote-wipe signal), idempotency keys against duplicate processing on offline reconnect — enforced system-wide via #[IdempotencyStrategy], not optional per endpoint.

2 Encryption & Data Security

Field-level encryption implemented

Chapter content and version snapshots encrypted before being written (Sodium/XChaCha20-Poly1305), encapsulated in a dedicated Doctrine type.

Envelope encryption (KEK→DEK) implemented

Self-hosted HashiCorp Vault, genuine 2-of-3 unseal threshold (no solo key). Restore drill actually performed and passed, not just configured.

Email blind index implemented

users.email stored encrypted, plus an HMAC-SHA256 hash for login/uniqueness checks — no plaintext comparison on a sensitive column.

Tenant isolation implemented

A global Doctrine SQL filter automatically restricts every query to permitted projects — in addition to the voter check, not as a replacement. Fail-closed branch deliberately left unlogged (tripwire).

PostgreSQL least privilege implemented

Application role holds only DML rights, no SUPERUSER. Separate app/batch role, batch user has no access to email/password-hash columns.

Primary keys as ULID implemented

Instead of INT AUTO_INCREMENT — prevents enumeration of record counts. An invalid ID format returns 400, never 500.

No server-side zero-knowledge deliberate limit, openly communicated

Manuscripts are readable server-side (search, continuity checking, and collaboration require this) — honestly documented as an architectural decision, never marketed as "end-to-end encrypted."

3 Input Validation, File Uploads & Malware Protection

AST sanitization (editor content) implemented

Recursive allowlist instead of HTMLPurifier — TipTap content is JSON, not a raw HTML string. An unknown node type triggers full rejection, never silent stripping. Depth/size limits against payload bombs.

File upload validation implemented

Genuine content sniffing (finfo, never the client-supplied value), a hard MIME allowlist, size limits, self-generated filenames (structurally no path traversal possible).

Malware scanning on uploads implemented (2026-09-25)

Self-hosted ClamAV (no SaaS provider), fail-closed — scanner unreachable means the upload is rejected, never let through unscanned. Verified for real with an EICAR test file, memory limit set against resource exhaustion.

ZIP bombs / XXE (import) implemented

Every archive entry's target size is checked before extraction. LIBXML_NONET explicitly enabled, DTD loading disabled for every XML parse in the import path.

SSRF protection implemented

Every server-side outbound call is checked against a fixed domain allowlist — there is no "import from arbitrary URL" feature that could bypass this. The export rendering pipeline has no network access outside its own whitelist.

Mass-assignment protection implemented

Exclusively dedicated write DTOs, never generic entity hydration. No endpoint maps a request body directly onto an entity.

Security headers on file delivery implemented

X-Content-Type-Options: nosniff globally, Content-Disposition on every file endpoint — defense in depth on top of the MIME allowlist.

4 Network, Transport & Frontend

HTTPS/HSTS/CSP implemented

HTTPS enforced (not only at the application level), Strict-Transport-Security, a restrictive Content Security Policy (connect-src 'self' — prevents data exfiltration in a hypothetical XSS). No AI provider in the allowlist. Real finding 2026-09-25: this previously applied only to responses actually served through the backend application — the SPA shell (HTML/assets) served directly by the web server simply lacked the same headers. Both delivery paths now carry identical values.

Referrer-Policy / Permissions-Policy / X-Frame-Options implemented (2026-09-25)

External scan finding: not set at all previously. strict-origin-when-cross-origin instead of implicitly trusting the browser default; Permissions-Policy demonstrably disables unused browser features (camera, geolocation, payment, …); X-Frame-Options: DENY as a fallback for browsers without CSP Level 2 support (frame-ancestors already covers modern browsers).

CORS implemented

Exclusively exact, hardcoded domains for allow_credentials — never wildcard regexes across domain families.

CSRF protection implemented

SameSite cookie plus a mandatory X-Requested-With header on every mutating request.

No v-html in the frontend implemented

Never used anywhere in the code (deliberate, documented via comment) — editor rendering runs exclusively through TipTap's own, closed schema.

Client-side privacy shield implemented (optional)

Opt-in feature for shared devices — quickly hides sensitive content.

Minimum browser requirements communicated implemented (2026-09-25)

Explicit browserslist baseline instead of an implicit build-tool assumption. Feature-detection script (ES5, loaded before the actual bundle) shows a notice for an outdated browser — no hard block, purely a usability measure rather than a security decision.

5 Infrastructure & Operations (Strato beta, actually hardened today)

Host firewall implemented

A custom, Docker-compatible INPUT-chain firewall (default-deny with allowlist) — deliberately not the Plesk firewall, whose generated script was shown to cut off the entire container stack from the network.

fail2ban implemented

Real nftables integration (a Docker-host-specific pitfall found and fixed), verified for real with a dummy IP — not merely configured, an actual network rule confirmed on a ban.

SSH hardening implemented

Key auth only, no password login.

UID isolation between services implemented

The web app and the Git/CI server initially ran under the same host UID (an RCE in the larger, more exposed app would have automatically granted access to the Git server) — fixed, dedicated system users per service, kept as a general principle going forward.

Filesystem permissions implemented

The repository directory was accidentally world-readable (to other, unrelated accounts on the shared host) — demonstrated for real with a foreign account and closed. Backup encryption code moved out of the crontab into its own 0600 file.

Branch protection implemented

No direct push to main — tested for real, an actual push attempt was rejected. Mandatory CI checks before every merge.

Fail-closed CI/CD deploy implemented

Automatic staging deploy only on green CI — the negative path tested for real (red checks demonstrably triggered no deploy). Deploy token is least-privilege (read:repository, no write access, no SSH key).

Backup & restore implemented, tested for real

Encrypted backups (Vault, Git server, database), restore drills actually performed and passed — not merely present as a script.

OOM hardening (malware scanner) implemented

Explicit memory limit on the ClamAV container, so a resource outlier never takes down the shared host instead of just this one service.

Messenger worker (background queue) implemented (2026-09-25)

Real finding during the timing-fix rollout: no process had been consuming the asynchronous message queue — multiple messages were sitting unprocessed in the database for real. A dedicated worker container (same image, messenger:consume) is now automatically carried along by the same compose file on every deploy.

Deploy automation: config changes reliably go live implemented (2026-09-25, self-audit finding)

Found while re-verifying that day's own changes: a changed web-server configuration (bind mount) was checked out by the automatic deploy but never actually reloaded without an extra manual step — the purely memory-rule nature of that step led to exactly one instance of forgetting it. Now a fixed, unconditional part of the deploy script, no manual step required anymore. Shows the project's own review discipline: the finding came from re-verifying its own work, not from an external source.

6 Self-Hosting Principle & Cloud Act Avoidance

A consistent architectural principle, not limited to a single service: EU hosting only (Hetzner/Strato), no US hyperscalers (AWS/Google Cloud/Azure) and no US SaaS providers for security-relevant functions — because of the US CLOUD Act (an extraterritorial data-access claim, independent of physical storage location).

7 Privacy & Data Subject Rights (GDPR)

Full data export implemented

Technical fulfillment of GDPR Art. 20 — free on every plan, and by principle must never become a paid feature. Account-level and project-level data are kept separate, each complete (repeatedly checked for real against all ~170 entity types).

Access transparency implemented

Every guest access to shared content is logged and visible to the owner/co-authors.

Deletion exception rule implemented

GDPR Art. 17(3)(b) — a narrowly scoped exception only for an open accountability matter, never a blanket delay of the entire deletion.

Data minimization implemented

Guest accounts receive only what the specific share requires. Never-accepted invitations are automatically anonymized after 14 days.

No third-party trackers implemented

No advertising/analytics cookies, no cross-site tracking. Performance telemetry is never sent silently in the background — every transmission produces a visible notice.

8 Governance & Testing Discipline

Security requirements are consolidated into a single, binding rulebook (illumentis-security-testing-lawbook.md, ~1000 lines) — additive-only: nothing is ever shortened or weakened without an explicit check-in. Every security-relevant measure is validated by an automated test class, not by manual review alone.

9 Monthly Red-Team Validation Report (September 2026)

14 attack scenarios tested — 13 of them verified with concrete automated tests. Identified vulnerabilities were fixed and retested; one known limitation is documented rather than omitted. Current test run: 3,896 automated tests, 2 test failures in the last run, 1 known independent risky test tracked separately.

Attack scenarioResult
Compromised Userpassed
Cross-Tenant IDORpassed
Compromised Co-Authorpassed
Compromised Editorpassed
Compromised Adminpassed — with documented limits
Session Hijackingpassed — with documented limits
Race Conditionspassed
API Manipulationpassed
Vault Policy Bypasspassed
Authenticated Scrapingpassed
Branch/Merge Manipulationpassed
Permission Escalationpassed
Compromised Symfony Runtimepassed — with documented limits
Deployment Integrityknown remaining measure

What these tests do not prove

The tests described here are not a guarantee against future vulnerabilities. They verify defined security properties under the documented assumptions — the four cases below were deliberately documented with their respective limits rather than presented as fully solved.

Compromised Admin documented limit

The principle "no admin can see book content" holds fully for the app admin (can_manage_users) via the application itself (voters, serializer groups, field-level encryption). It does NOT hold against a server root/infrastructure admin: they can read the KEK from Symfony Secrets/the environment variable. Genuine effectiveness even against a compromised root access would require an external KMS/Vault that decrypts itself (ciphertext in, plaintext out, the key never leaves the key server) — already implemented for content/media/email-hash (see Vault Policy Bypass/Compromised Symfony Runtime), but not yet for every data path.

Session Hijacking documented limit

Detection deliberately compares only the user agent, not the IP address (a network change in the middle of a legitimate session is normal). An attacker who exactly replicates both the victim's IP AND user agent goes undetected.

Compromised Symfony Runtime documented limit

A compromised, actively running Symfony process can, at the time of access, reach plaintext keys or already-decrypted data present in process memory. The Vault architecture (envelope encryption) specifically reduces the risk of a permanently available, static application key — it does not prevent a full live RCE.

Deployment Integrity known open measure

File-integrity monitoring (detecting subsequent code tampering) exists and is tested. Automated deployment hardening (signed manifest, SHA pinning, external out-of-band verification) is documented knowledge, but not yet a running measure.

10 Honest Stocktake — What an External Auditor Would Additionally Check

Carried over from the lawbook's "external verifiability" section — self-disclosure is not external confirmation. This table deliberately distinguishes between the two.

AreaStatus
Auth, authorization, IDOR, encryption, input validationFully covered, automated tests
Network/transport, DNS/email authentication (SPF/DKIM/DMARC)Covered
Backup/disaster recovery, incident response planCovered, restore tested for real
Secret scanning in CI (Gitleaks)Implemented (2026-09-25) — a real setup misconfiguration was found and fixed (a missing useDefault=true would have silently disabled every rule)
License checking against an allowlist (dependencies)Implemented (2026-09-25), backend + frontend, fail-closed on a missing license declaration
Automated coverage-threshold enforcementImplemented (2026-09-26) — PCOV in CI, thresholds raised to 90%/80% after a real measurement run. All 101 gaps found in the security group systematically closed with real tests (not just reaching the threshold) — current actual state 99.4–99.6%/87.5–87.7% (small measurement variance between runs, see section 8), both values well above the threshold. Four consistently reproduced, documented lines remain open: two genuine dead code, two due to an external Vault rate limit
Rate-limit backoff for the Vault email-hash migrationImplemented (2026-09-26) — self-throttling + retry with exponential backoff against Vault's Transit rate limit (60/60s), previously entirely unaccounted for. Fully tested via an injectable test seam, without consuming the real rate-limit budget
Mutation testing (voter classes + ChapterContentSanitizer)Implemented (2026-09-26) — monthly reporting cron (Infection), not a CI gate. First real run: 596/773 mutants killed, covered-code MSI 77%
/.well-known/security.txt + vulnerability disclosure policy (safe harbor)Implemented (2026-09-29) — real contact address, dedicated policy page
Malware scanning for JSON-import ZIP contentsDeliberately out of scope — a dedicated zip-bomb guard already exists
Dedicated production server (currently: shared Strato host for beta)Hetzner migration planned, not yet live
Independent external penetration testNot yet performed
Hosting provider certification (ISO 27001 or similar)Only implicit via the EU-location decision

Assessment

  1. Systematically, all industry-standard measures are implemented and backed by automated tests — that is what this overview documents.
  2. An independent external penetration test can only be delivered by an actually performed review — no internal document, however thorough, can substitute for that. That is the next sensible step before a broader onboarding.