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.

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 (ForbiddenUploadExtension denylist)

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

AreaBehavior
PasswordsHashed; excluded from Spatie attribute logs on User
2FA / passkeysOptional; project can require MFA — Passkeys
API keysStored hashed; hash excluded from activity logs
Activity Log UI / AI toolPermission can-show-activity-logs
Collections / filesSpatie permissions + field / item ACL rules
WysiwygWysiwygHtmlSanitizer strips dangerous markup
File Manager uploadsExtension denylist (ForbiddenUploadExtension); plain-text strip on name/metadata/tags
Activity retentionDefault 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

ControlWhy
HTTPS (APP_URL)Cookies, passkeys, basic confidentiality in transit
Unique APP_KEY, APP_DEBUG=falseSession/crypt integrity; no debug leaks
Encrypted DB volume + backupsData at rest if the disk or backup is stolen
Locked-down .env / SSHKeys and secrets live on the host
Who gets can-show-activity-logsAudit UI shows PII and chat previews
Queue + schedulerRetention 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 body plaintext in chat_messages so 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, unique APP_KEY
  • [ ] HTTPS on the public origin; APP_URL matches 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-logs to trusted operators
  • [ ] Scheduler running (activitylog:clean and other ops jobs) — Operations
  • [ ] Public API CORS / keys / IP policy reviewed — Public CMS API
  • [ ] Optional: two_factor_required for 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.

Previous
Deployment