Operations
Threat model & hosting
Externa is self-hosted. The app ships RBAC, hashed passwords, optional MFA, and an admin-gated activity log. Transport encryption, database disk encryption, and backup hygiene are the operator’s job.
Related
Repo file: externa-core/SECURITY.md · App gates: Pre-release security checklist · Deployment · Activity & notifications · Passkeys · Roles & permissions
Goals
Protect against:
- Unauthenticated access to admin UI and APIs
- Authenticated users exceeding their Spatie / collection / file permissions
- Accidental logging of passwords, 2FA secrets, and API key material
- XSS from rich text (TipTap HTML sanitizer) and from File Manager plain-text fields (name / metadata / tags via
PlainTextSanitizer) - Upload of common executable / script extensions into the Files pool (
ForbiddenUploadExtensiondenylist)
Not designed to protect against:
- An attacker with shell access to the app host or a copy of the database and
APP_KEY - Network sniffing when the operator runs plain HTTP in production
- End-to-end confidentiality of chat (no client-held keys)
Trust boundaries
[Browser] --TLS (operator)-- [App / PHP]
|
+-- DB (chat bodies, activity JSON, users)
+-- Object storage (files)
+-- .env / APP_KEY
Anyone who can read the DB (or app storage + key) can read chat message bodies and activity log properties, including short chat body_preview fields. That matches a normal collaborative CMS, not a Signal-style messenger.
Laravel encrypted casts (e.g. AI sync bearer tokens) use APP_KEY on the server. Useful against raw DB dumps without the key; useless if the app server is compromised.
What the application covers
| Area | Behavior |
|---|---|
| Passwords | Hashed; excluded from Spatie attribute logs on User |
| 2FA / passkeys | Optional; project can require MFA — Passkeys |
| API keys | Stored hashed; hash excluded from activity logs |
| Activity Log UI / AI tool | Permission can-show-activity-logs |
| Collections / files | Spatie permissions + field / item ACL rules |
| Wysiwyg | WysiwygHtmlSanitizer strips dangerous markup |
| File Manager uploads | Extension denylist (ForbiddenUploadExtension); plain-text strip on name/metadata/tags |
| Activity retention | Default 365 days; activitylog:clean on the scheduler |
Team chat attachments are not covered by the Files extension denylist at upload time; see Chat.
What operators must provide
| Control | Why |
|---|---|
HTTPS (APP_URL) | Cookies, passkeys, basic confidentiality in transit |
Unique APP_KEY, APP_DEBUG=false | Session/crypt integrity; no debug leaks |
| Encrypted DB volume + backups | Data at rest if the disk or backup is stolen |
Locked-down .env / SSH | Keys and secrets live on the host |
Who gets can-show-activity-logs | Audit UI shows PII and chat previews |
| Queue + scheduler | Retention cleanup and async jobs actually run |
Full env/deploy steps: Deployment.
Activity log & chat privacy
- Activity log stores plaintext properties (IP, event metadata, settings keys, lean chat
body_preview≤ 120 characters). It does not encrypt columns. - Chat stores full
bodyplaintext inchat_messagesso hub search, replies, and moderation work. Product docs: Chat (hub & items). - Do not expect application-level encryption of chat or activity as a substitute for host security.
If you need stronger confidentiality (regulated data, E2EE messaging), that is a different product shape — not the Externa default.
Deploy checklist (host)
- [ ]
APP_ENV=production,APP_DEBUG=false, uniqueAPP_KEY - [ ] HTTPS on the public origin;
APP_URLmatches what browsers use - [ ] Production database with volume encryption and encrypted backups
- [ ] Strong bootstrap admin credentials; rotate after first login if seeded from env
- [ ] Restrict
can-show-activity-logsto trusted operators - [ ] Scheduler running (
activitylog:cleanand other ops jobs) — Operations - [ ] Public API CORS / keys / IP policy reviewed — Public CMS API
- [ ] Optional:
two_factor_requiredfor all users — Passkeys
Then run the application gates in Pre-release security checklist.
Reporting issues
See SECURITY.md in externa-core: prefer private disclosure, not a public issue.