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.

Defense in Depth
Section titled “Defense in Depth”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.
- Network isolation — VM has no internet access (host-only networking)
- Firewall — pf rules on LAN interface block unauthorized port access
- SSH hardening — Key-only auth, no root login, AllowUsers restriction
- Encrypted secrets — SOPS+age on VM, macOS Keychain on Mac (FileVault disabled — see below)
- Automatic rotation — Daily age-check, auto-rotate where the API allows
- Service binding — Services bound to specific interfaces (not 0.0.0.0)
- PII anonymization — Personal data scrubbed from all external LLM requests
pf Firewall
Section titled “pf Firewall”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.
Secret Rotation
Section titled “Secret Rotation”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.
| Schedule | Agent | Behavior |
|---|---|---|
| 06:00 ET daily | com.sanctum.secret-rotation-scan | Greps git remotes + files for exposed token patterns. Detect-only. |
| 06:15 ET daily | com.sanctum.secret-age-check | Alerts via Force Flow when any tracked secret is past its rotation_max_age_days. |
| 06:30 ET daily | com.sanctum.openrouter-auto-rotate | Mints + propagates + revokes via OpenRouter’s management API once past 85 days. No human. |
SSH Hardening
Section titled “SSH Hardening”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.
VM Isolation
Section titled “VM Isolation”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.
Secrets Storage
Section titled “Secrets Storage”| Location | Purpose |
|---|---|
| macOS Keychain | Runtime token access |
| 1Password | Backup 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 — Deliberately Disabled
Section titled “FileVault — Deliberately Disabled”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.
Why It Is Off
Section titled “Why It Is Off”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.
Compensating Controls
Section titled “Compensating Controls”FileVault is one layer of seven. Removing it shifts weight to the others:
- Physical security — Locked haus, behind Firewalla, on a private LAN segment.
- Secret rotation — Daily age-check + auto-rotation where the API allows (details). Physical compromise has a bounded blast radius.
- 1Password backup — All secrets have copies off-disk.
- UPS — Handles short blips. FileVault removal handles the extended-outage case where nobody is home to type a password.
- Bootstrap LaunchDaemons —
com.sanctum.vmnet+com.sanctum.bootstrapstart every service at boot, no GUI login. The VM runs viasocket_vmnet+ bare QEMU. Touch ID preserved.
Re-enabling FileVault
Section titled “Re-enabling FileVault”If the threat model changes (Mini moves to a co-lo, the operator becomes a person of interest):
sudo fdesetup enableBackground encryption takes hours. This re-introduces the remote-reboot problem — keep fdesetup authrestart handy.
Remote Reboot Protocol
Section titled “Remote Reboot Protocol”With FileVault off, remote reboots just work:
# Standard reboot (SSH)sudo reboot
# Graceful reboot with service drainsudo 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.
PII Anonymization
Section titled “PII Anonymization”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.
How It Works
Section titled “How It Works”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:
- Extracts text from user and assistant messages (system prompts left untouched)
- Routes through the local Presidio analyzer + anonymizer containers, which detect PII and replace each entity with a typed placeholder
- Forwards the scrubbed request to the external provider
- De-anonymizes the response on the way back
What Gets Scrubbed
Section titled “What Gets Scrubbed”| Entity | Example | Replacement |
|---|---|---|
| Person names | John Smith | <PERSON> |
| Email addresses | support@sanctum.run | <EMAIL_ADDRESS> |
| Phone numbers | 514-555-1234 | <PHONE_NUMBER> |
| Credit cards | 4111-1111-1111-1111 | <CREDIT_CARD> |
| SSN / bank numbers | 123-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.
What Is Not Affected
Section titled “What Is Not Affected”- 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
Architecture
Section titled “Architecture”Client → Sanctum Proxy (port 4040) → Provider | | +-- Presidio Analyzer (Docker, port 5002) +-- Presidio Anonymizer (Docker, port 5001) | | +-- PII scrubbed before exit ◄──────────+ +-- PII restored on responsePresidio Containers
Section titled “Presidio Containers”The analyzer and anonymizer run as local Docker containers, bound to localhost only:
# Check statusdocker ps --filter name=presidio
# Restart if neededdocker restart presidio-analyzer presidio-anonymizer