ATHELOS
Settings

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 JavaScript
  • SameSite=Lax — protects against CSRF
  • Secure — 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.1 only, 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:

HeaderValuePurpose
X-Frame-OptionsDENYPrevent clickjacking
X-Content-Type-OptionsnosniffPrevent MIME sniffing
Referrer-Policystrict-origin-when-cross-originLimit referrer leakage
Permissions-Policycamera=(), microphone=(), geolocation=()Disable unused APIs
Content-Security-Policy(see config)Restrict script sources
Strict-Transport-Securitymax-age=63072000Force HTTPS (production)
Cross-Origin-Opener-Policysame-originIsolate browsing context
Cross-Origin-Resource-Policysame-originRestrict 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
  • .env files 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