Skip to content

Security

Somewhere a security auditor is reading this page and trying to decide whether to be impressed or alarmed that this level of defense-in-depth has been applied to a haus. The answer is both. It should be both.

The security fortress — seven layers deep and counting

Sanctum uses seven layers of security, because if action movies have taught us anything, it’s that every single layer will eventually be breached by someone with enough determination. The game isn’t making the wall perfect — it’s making the attacker tired.

  1. Network isolation — VM has no internet access (host-only networking)
  2. Firewall — pf rules on LAN interface block unauthorized port access
  3. SSH hardening — Key-only auth, no root login, AllowUsers restriction
  4. Encrypted secrets — SOPS+age on VM, macOS Keychain on Mac (FileVault disabled — see below)
  5. Automatic rotation — Daily age-check, auto-rotate where the API allows
  6. Service binding — Services bound to specific interfaces (not 0.0.0.0)
  7. PII anonymization — Personal data scrubbed from all external LLM requests

Rules in /etc/pf.anchors/sanctum block external access to internal ports on the LAN interface (en1):

Blocked ports include: gateway (1977), dashboard (1111), Sanctum TTS (8008), MLX (1337), Firewalla bridge (1984), and others.

The philosophy: if a service doesn’t need to talk to the LAN, it doesn’t get to. Every open port is a decision, not a default.

Three LaunchAgents, one doctrine: scan and notify daily, auto-rotate only where the provider has a real management API, paste-and-propagate everywhere else. Mechanism, audit log shape, and per-secret schedules live in Secret Rotation.

ScheduleAgentBehavior
06:00 ET dailycom.sanctum.secret-rotation-scanGreps git remotes + files for exposed token patterns. Detect-only.
06:15 ET dailycom.sanctum.secret-age-checkAlerts via Force Flow when any tracked secret is past its rotation_max_age_days.
06:30 ET dailycom.sanctum.openrouter-auto-rotateMints + propagates + revokes via OpenRouter’s management API once past 85 days. No human.

Both Mac and VM have hardened SSH configs:

  • Key-only authentication (no passwords)
  • No root login
  • AllowUsers restriction
  • Post-quantum key exchange (on VM)

Yes, post-quantum key exchange. For a VM that can’t access the internet. Because when the quantum computers arrive, they’ll find this particular door already bolted from the inside.

The Ubuntu VM runs on host-only networking (bridge100, 10.10.10.0/24). It can only reach the Mac at 10.10.10.1 — no direct internet access. All external communication goes through the Mac.

This means a compromised VM can’t phone haus, can’t exfiltrate data, can’t join a botnet. The worst it can do is send a strongly worded message to the Mac, which is already watching.

LocationPurpose
macOS KeychainRuntime token access
1PasswordBackup copies of all secrets
VM SOPS (age)Encrypted secrets for VM services

Secrets are never stored in config files, git repos, or environment variables on disk.

FileVault is off on the Mac Mini. This was a council decision, not an oversight.

The Jedi Council convened on 2026-03-29 to evaluate whether full-disk encryption made sense for a stationary home server. The verdict was unanimous: availability beats encryption for a device that never leaves the haus.

FileVault protects against one threat: someone physically stealing the Mini or pulling its SSD. It creates a different problem: every power outage turns the Mini into a $600 paperweight until someone walks over and types a password. The pre-boot screen runs before the network stack exists — no remote unlock, no VPN trick. The whole haus automation stack goes dark while the disk sits encrypted and useless.

The council reasoning:

  • Windu (Security): A deaf security agent is worse than an unencrypted disk. Mac down means Signal alerts stop and Alarmo goes blind — a powered-off security system has more attack surface than an unencrypted SSD behind Firewalla.
  • Qui-Gon (Infrastructure): The Mini is the backbone of 30+ services across two nodes. FileVault turned a recoverable event (power restored) into a manual one (drive to the haus, type a password).
  • Yoda (Grand Master): The Force flows through availability, not through encryption of a stationary box.

FileVault is one layer of seven. Removing it shifts weight to the others:

  1. Physical security — Locked haus, behind Firewalla, on a private LAN segment.
  2. Secret rotation — Daily age-check + auto-rotation where the API allows (details). Physical compromise has a bounded blast radius.
  3. 1Password backup — All secrets have copies off-disk.
  4. UPS — Handles short blips. FileVault removal handles the extended-outage case where nobody is home to type a password.
  5. Bootstrap LaunchDaemonscom.sanctum.vmnet + com.sanctum.bootstrap start every service at boot, no GUI login. The VM runs via socket_vmnet + bare QEMU. Touch ID preserved.

If the threat model changes (Mini moves to a co-lo, the operator becomes a person of interest):

Terminal window
sudo fdesetup enable

Background encryption takes hours. This re-introduces the remote-reboot problem — keep fdesetup authrestart handy.

With FileVault off, remote reboots just work:

Terminal window
# Standard reboot (SSH)
sudo reboot
# Graceful reboot with service drain
sudo shutdown -r +1 "Sanctum maintenance reboot"

The Mac boots, LaunchDaemons fire, services start (including the VM via socket_vmnet), and the council reconvenes. No GUI login. No drive to the haus. Touch ID stays.

All requests routed to external LLM providers (OpenRouter) pass through a Presidio-based anonymization layer before leaving the network. This ensures personal data never reaches third-party inference APIs.

Your AI agents know your name. The cloud doesn’t need to.

The Sanctum Proxy (port 4040) handles all LLM request routing. When a request targets an OpenRouter model — either directly or via fallback routing — the proxy:

  1. Extracts text from user and assistant messages (system prompts left untouched)
  2. Routes through the local Presidio analyzer + anonymizer containers, which detect PII and replace each entity with a typed placeholder
  3. Forwards the scrubbed request to the external provider
  4. De-anonymizes the response on the way back
EntityExampleReplacement
Person namesJohn Smith<PERSON>
Email addressessupport@sanctum.run<EMAIL_ADDRESS>
Phone numbers514-555-1234<PHONE_NUMBER>
Credit cards4111-1111-1111-1111<CREDIT_CARD>
SSN / bank numbers123-45-6789<US_SSN>

Only entities above 0.7 confidence score are anonymized. IP addresses, locations, and code identifiers are intentionally excluded to avoid breaking technical context.

  • Anthropic (Claude) — direct API, no PII scrubbing (trusted provider, privacy policy reviewed)
  • Local models (LM Studio, Council MLX) — never leave the network
  • Gemini — routed via Google AI Studio API (separate trust decision)
  • System prompts — contain instructions, not personal data
Client → Sanctum Proxy (port 4040) → Provider
| |
+-- Presidio Analyzer (Docker, port 5002)
+-- Presidio Anonymizer (Docker, port 5001)
| |
+-- PII scrubbed before exit ◄──────────+
+-- PII restored on response

The analyzer and anonymizer run as local Docker containers, bound to localhost only:

Terminal window
# Check status
docker ps --filter name=presidio
# Restart if needed
docker restart presidio-analyzer presidio-anonymizer