Security
Defense in Depth
Section titled “Defense in Depth”Sanctum uses multiple layers of security:
- 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
- Automatic rotation — 8 secrets rotated monthly
- 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 (18789), dashboard dev (3000, 3004), XTTS (8020), MLX (8899), Firewalla bridge (18094), and others.
Secret Rotation
Section titled “Secret Rotation”Monthly via com.sanctum.rotate-secrets LaunchAgent (1st of month, 3:30am).
Rotates 8 secrets:
- Home Assistant main + Windu tokens
- Firewalla bridge token
- Gateway token
- Network control token
- Backup encryption key
- OpenRouter API key
- Deepgram API key
Plus verifies the Cloudflare tunnel token.
# Manual rotationbash ~/Backups/rotate-secrets.sh
# Dry runbash ~/Backups/rotate-secrets.sh --dry-runSSH 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)
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.
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.
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.
How It Works
Section titled “How It Works”The guardrail injector proxy (port 4000) sits in front of LiteLLM (port 4001). When a request targets an OpenRouter model — either directly or via fallback routing — the proxy:
- Extracts text from user and assistant messages (system prompts are left untouched)
- Sends the text to a local Presidio analyzer container to detect PII entities
- Sends detected entities to a local Presidio anonymizer container to replace them with placeholders
- Forwards the scrubbed request to the external provider
- De-anonymizes the response before returning it to the caller
What Gets Scrubbed
Section titled “What Gets Scrubbed”| Entity | Example | Replacement |
|---|---|---|
| Person names | John Smith | <PERSON> |
| Email addresses | john@example.com | <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-27B 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 → Guardrail Injector (port 4000) → LiteLLM (port 4001) → 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