Security
Threat model, hardening checklist, and known limitations.
Security
ATHELOS is a self-hosted tool that handles sensitive client data. This page documents the threat model, the hardening measures in place, and the known limitations.
Threat model
What ATHELOS defends against
- Credential theft — Argon2id password hashing, signed sessions, optional TOTP and WebAuthn
- Client data leakage — self-hosted, no cloud services, no telemetry
- XSS and clickjacking — Content Security Policy, X-Frame-Options, React escaping
- CSRF — SameSite cookies, NextAuth CSRF tokens
- Data loss — encrypted offsite backups with tested restores
- Unauthorized access — VPN-only access, single-user by design
- Malicious dependencies — lockfile pinning, dependency auditing
What ATHELOS does not defend against
- Physical theft of an unlocked, powered-on machine — full-disk encryption helps, but an unlocked laptop can be read
- Nation-state adversaries with hardware implants — out of scope
- Compromise of the underlying OS — if your machine is rooted, nothing in the app layer helps
- Insider threats — there's only one user, so this doesn't apply
- Supply chain attacks on Ollama models — the models are trusted based on where you pulled them from
Access control
Network
ATHELOS is designed to run on your local machine or a VPN-only server. It's not intended for public internet exposure.
Production deployment:
- Bind to Tailscale or WireGuard only
- No public IP exposure
- SSH key-only, no password authentication
- UFW firewall with only the required ports open
Authentication
- Single user — one owner account
- Argon2id — memory-hard password hashing (19 MB, 2 iterations)
- JWT sessions — signed with
AUTH_SECRET, 30-day expiry - Optional TOTP — 6-digit time-based codes
- Optional WebAuthn — hardware-backed second factor
Cookies
Session cookies are set with:
HttpOnly— not accessible to JavaScriptSameSite=Lax— protects against CSRFSecure— only sent over HTTPS (in production)
Authorization
Every route and server action checks the session. Server actions call a
requireUser() helper that throws if no valid session exists. There's no
client-side trust.
Data protection
At rest
- Disk — full-disk encryption (LUKS on Linux)
- Database — sensitive columns encrypted (TOTP secrets, WebAuthn keys)
- Backups — restic AES-256 + ChaCha20-Poly1305
- Backup storage — B2 private bucket with encryption
In transit
- Network — WireGuard (via Tailscale) for remote access
- HTTPS — TLS terminated by Caddy in production
- Internal services — bound to
127.0.0.1only, never exposed
Isolation
- No third-party cloud services
- No telemetry
- No analytics
- Local AI only — no prompts leave your machine
HTTP security headers
Every response includes:
| Header | Value | Purpose |
|---|---|---|
| X-Frame-Options | DENY | Prevent clickjacking |
| X-Content-Type-Options | nosniff | Prevent MIME sniffing |
| Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leakage |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | Disable unused APIs |
| Content-Security-Policy | (see config) | Restrict script sources |
| Strict-Transport-Security | max-age=63072000 | Force HTTPS (production) |
| Cross-Origin-Opener-Policy | same-origin | Isolate browsing context |
| Cross-Origin-Resource-Policy | same-origin | Restrict resource loading |
X-Powered-By is removed — no version fingerprinting.
Password policy
Minimum 8 characters. No maximum. No complexity requirement.
Rationale: length matters more than complexity. A 20-character passphrase beats a 12-character "P@ssw0rd!".
Use a password manager. Generate the password if possible.
Rate limiting
Login attempts are rate-limited:
- 5 attempts per 15 minutes, per IP + email combination
- Redis-backed sliding window
- Fails open (if Redis is down, the limiter is bypassed to preserve availability)
This is designed for a VPN-only deployment where the risk of brute-force is low. If exposed publicly, add additional protection.
Session management
Sessions are JWT-based, signed with AUTH_SECRET.
- 30-day expiry
- Rotated on password change — all other sessions invalidated
- Manual revocation — sign out individual sessions from Settings
For higher security, rotate AUTH_SECRET — this invalidates all sessions
at once. Store the new secret in your password manager.
Audit log
Every write operation is logged to audit_log:
- Who did it (user ID)
- What action
- What entity
- When
The table is append-only. No UPDATE or DELETE operations are performed on it. The log is a permanent record of system activity.
Read operations are not logged by default. If you need read logging, configure it in Settings → Security → Audit.
Known limitations
Documented honestly:
CSP uses unsafe-inline for scripts
Required for Next.js hydration. Tightening to nonce-based CSP is possible but complex and slow. The VPN-only access model makes this acceptable.
No rate limiting on most endpoints
Only login is rate-limited. Other endpoints rely on the single-user model and VPN-only access.
Reads are not audited
Only writes are logged. If you need to know who read what, that's not tracked.
No automatic secret rotation
AUTH_SECRET, TOTP_ENCRYPTION_KEY, and other long-lived secrets must
be rotated manually. See Reference: environment variables.
Dependencies not scanned continuously
pnpm audit runs manually. Dependabot integration is available but
requires GitHub Actions configuration.
Bcrypt not used
Argon2id is the modern recommendation. Bcrypt is fine but older. No reason to use both.
Hardening checklist
For a production deployment:
- Full-disk encryption enabled
- SSH key-only, password auth disabled
- UFW firewall enabled
- Only port 22 (or 22 + Tailscale) open
- Data services bound to 127.0.0.1
- TOTP or WebAuthn enabled
- Backups configured and tested
-
.envfiles at 600 permissions - Secrets stored in password manager + paper
- Access via Tailscale only, no public exposure
- OS updates applied regularly
- Ollama bound to localhost only
When to be concerned
Signs that something is wrong:
- Login attempts you didn't make (check audit log)
- Sessions you don't recognize (check Settings → Sessions)
- Missing data (check backups)
- Backup failures (check journalctl)
- Emails you didn't send (SMTP logs)
The audit log is your first stop. It records every write with a timestamp.
Where to go next
- Two-factor — enabling TOTP
- WebAuthn — hardware keys
- Scheduled backups — protecting data