This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
See README.md for full requirements and status per machine.
Personal infrastructure-as-code (Ansible) managing a home network with four devices:
- NSA (
192.168.1.183): Debian 13 home server running Docker containers (Pi-hole, Home Assistant, Plex, WireGuard, nginx, Mosquitto, Zigbee2MQTT, ntopng, OpenClaw, Matter Server) + Cockpit web admin - Mini (
192.168.1.116): Mac Mini M1 for Syncthing, iCloud backup, and Ollama LLM (available for local inference) - MB4 (
mb4): MacBook Pro M4 workstation with Syncthing and Docker (Colima) for local dev - MKT (
192.168.1.1): MikroTik hAP ax³ router (PPPoE WAN, DHCP, firewall, WiFi)
# Run playbooks (vault password auto-retrieved from macOS Keychain)
ansible-playbook site.yml # All hosts
ansible-playbook nsa.yml # NSA only
ansible-playbook mini.yml # Mini only
ansible-playbook mb4.yml # MB4 only
ansible-playbook mkt.yml # MikroTik router only
# Dry run with diff
ansible-playbook nsa.yml --check --diff
# Run specific tags
ansible-playbook nsa.yml --tags docker
ansible-playbook nsa.yml --tags pihole
ansible-playbook nsa.yml --tags wireguard
ansible-playbook mb4.yml --tags hosts # Update /etc/hosts entries
ansible-playbook mb4.yml --tags docker # Set up Colima + dev containers
ansible-playbook mkt.yml --tags dhcp # DHCP server config
ansible-playbook mkt.yml --tags firewall # NAT and filter rules
ansible-playbook nsa.yml --tags github-runner # CI/CD runner + deps
ansible-playbook mini.yml --tags cloudflared # Cloudflare Tunnel (always-on)
ansible-playbook mb4.yml --tags cloudflared # Cloudflare Tunnel config (on-demand)
# Available tags: common, ssh, cockpit, avahi, docker, colima, ssl, https, nftables, pihole, wireguard, syncthing, backup, plex, openclaw, ollama, homeassistant, ha, github-runner, ci, power, autologin, homebrew, icloud-backup, mackup, hosts, dns, identity, bridge, network, ip, pppoe, wan, dhcp, nat, filter, wifi, wireless, services, security, traffic-flow, monitoring, dashboard, cleanup, disk-stats, cloudflared
# Vault operations
ansible-vault view vault.yml
ansible-vault edit vault.yml
# Testing
./tests/quick-check.sh # Fast smoke test
./tests/run-all.sh # Full test suite (auto-logs to tests/results/)
# Security scanning (MB4, Docker/Colima required)
docker compose -f ~/docker/docker-compose.yml --profile security run --rm nmap -sV 192.168.1.0/24
docker compose -f ~/docker/docker-compose.yml --profile security up openvas # http://localhost:9392
# Test logs
# tests/results/ # Auto-saved by run-all.sh
# ~/docker/nmap/reports/ # nmap scan reports
# ~/docker/openvas/data/ # OpenVAS data (web UI managed)site.yml # Master playbook - imports all host playbooks
├── nsa.yml # Linux server: Docker services, firewall, VPN
├── mini.yml # macOS: Syncthing, iCloud backup, Homebrew
├── mb4.yml # macOS: Syncthing, Homebrew
└── mkt.yml # MikroTik router: PPPoE, DHCP, firewall, WiFi
tasks/ # Reusable task modules
├── common.yml # SSH dirs, shell config, Sync folder
├── ssh.yml # SSH keys, hardening (Linux)
├── cockpit.yml # Web admin interface (Linux)
├── avahi.yml # mDNS for .local resolution (Linux)
├── docker.yml # Docker install, compose deployment
├── docker-macos.yml # Colima + Docker on macOS
├── nftables.yml # Linux firewall rules
├── pihole.yml # Pi-hole config, disable dnsmasq
├── wireguard.yml # VPN server config
├── syncthing-{linux,macos}.yml
├── backup.yml # Docker backup script + cron
├── icloud-backup.yml # rsync to iCloud (Mini only)
├── homebrew.yml # Homebrew packages/casks
├── mackup.yml # App settings backup (macOS)
├── hosts-macos.yml # /etc/hosts entries for NSA services
├── plex.yml # Media server directories
├── ssl.yml # Self-signed cert for OpenClaw HTTPS
├── openclaw.yml # OpenClaw AI assistant directories
├── ollama.yml # Ollama LLM server (Mini)
├── homeassistant.yml # HA config deployment (configuration.yaml, secrets.yaml)
├── github-runner.yml # GitHub Actions self-hosted runner + CI deps
├── monitoring.yml # health-check, daily-status, docker-backup scripts
├── monitoring-mini.yml # NSA liveness monitor on Mini
├── dashboard.yml # Build + deploy Vue dashboard to docs site
├── cleanup-macos.yml # Remove unwanted startup items (macOS)
├── power-macos.yml # Auto-login, sleep settings (macOS)
├── disk-stats-macos.yml # Disk usage stats for dashboard
├── cloudflared-macos.yml # Cloudflare Tunnel config (Mini: always-on, MB4: on-demand)
└── mikrotik/ # MikroTik router tasks
├── identity.yml # Router name
├── bridge.yml # LAN bridge config
├── ip-address.yml # LAN IP
├── dhcp-server.yml # DHCP pool, leases, DNS
├── pppoe.yml # WAN PPPoE connection
├── firewall-nat.yml # NAT masquerade, port forwards
├── firewall-filter.yml # Input/forward chain rules
├── wifi.yml # WiFi config
├── guest-network.yml # Guest isolation (192.168.10.0/24)
├── traffic-flow.yml # NetFlow export to ntopng
└── services.yml # Enable/disable services
files/nsa/ # Static config files deployed to NSA
├── docker-compose.yml # All Docker service definitions
├── nftables.conf # Firewall rules (IPv4 + IPv6)
├── nginx.conf # Reverse proxy config (all services)
├── mosquitto.conf # MQTT broker config
└── pihole/05-local-dns.conf # Local DNS entries (dnsmasq address= format)
files/scripts/ # Monitoring scripts deployed to NSA
├── health-check # 5min cron — alerts on service issues
├── daily-status # 7:30am cron — HTML email + status.json
└── docker-backup # Sunday 3am — backup Docker volumes
templates/nsa/ # Jinja2 templates
├── wg0.conf.j2 # WireGuard config (uses vault peers)
├── docker.env.j2 # Docker environment vars
├── ha-configuration.yaml.j2 # Home Assistant configuration.yaml
└── ha-secrets.yaml.j2 # Home Assistant secrets.yaml
dashboard/ # Vue 3 + TypeScript dashboard (deployed to http://docs)
├── src/
│ ├── views/ # StatusView, ServicesView, NetworkView, DataView,
│ │ # OperationsView, TestingView
│ ├── components/ # StatusDot, DiskBar, ServiceRow, SyncthingPanel
│ ├── composables/ # useStatusData, useServiceCheck, useSyncthing
│ ├── types/status.ts # StatusData type definition
│ └── router/index.ts # SPA routes
└── dist/ # Built output, deployed by tasks/dashboard.yml
group_vars/ # Variables by group
├── linux_servers.yml # Linux-specific (apt, systemd)
├── macos.yml # macOS-specific (homebrew paths)
└── network_devices.yml # MikroTik connection settings
host_vars/ # Variables by host (override group_vars)
├── nsa.yml # NSA-specific config
├── mini.yml # Mini-specific config
├── mb4.yml # MB4-specific config
└── mkt.yml # MikroTik router config
docs/ # Documentation
├── guest-wifi-qr.png # Guest WiFi QR code
├── nsa-migration.md # NSA rebuild runbook
└── iot-vlan-design.md # IoT VLAN isolation design (planned)
URLs that work on both LAN and VPN (use short hostnames, not .local):
All browser services are accessible via nginx reverse proxy — no port numbers needed.
| Service | URL | Notes |
|---|---|---|
| Home Assistant | http://ha | WebSocket supported |
| Pi-hole Admin | http://pihole/admin | Direct: http://192.168.1.183:8081/admin |
| Plex | http://plex/web | |
| Cockpit | http://nsa | Proxies to Cockpit HTTPS on 9090 |
| OpenClaw | https://openclaw | HTTPS required (WebSocket secure context) |
| ntopng | http://ntopng | |
| Laya | http://laya | Static site |
| Hopo | http://hopo | Static site |
| Docs | http://docs | Vue 3 dashboard (status, services, network, data, ops, testing) |
| Minecraft | - | 3 servers: Laya (UDP 19132, survival), Richard (19133, creative), Mara (19134, creative). Data: /mnt/data/minecraft-*/ |
| Mosquitto | - | Port 1883 (not browser) |
| Matter Server | - | Port 5580 (localhost only, HA connects via WebSocket) |
| WireGuard | - | Port 51820 (not browser) |
| GitHub Actions | - | Self-hosted runners: nsa-bkd-{1,2,3,4} (Buckden), nsa-fdz-{1,2} (Fourdotzero) |
Note: Short hostnames (e.g., ha, pihole) are resolved by Pi-hole DNS and work over both LAN and WireGuard VPN. .local variants (e.g., ha.local) use mDNS (Avahi) and only work on LAN.
Dev servers exposed via Cloudflare Tunnels for QA testing before launch:
| Slot | URL | Machine | Notes |
|---|---|---|---|
| 1 | https://1.vulkitron.com | Mini | Always-on |
| 2 | https://2.vulkitron.com | Mini | Always-on |
| 3 | https://3.vulkitron.com | Mini | Always-on |
| 4 | https://4.vulkitron.com | MB4 | On-demand: cloudflared tunnel run mb4 |
| 5 | https://5.vulkitron.com | MB4 | On-demand: cloudflared tunnel run mb4 |
Slot-to-port mapping configured in host_vars/{mini,mb4}.yml (cloudflared_slots). Config deployed to /etc/cloudflared/config.yml.
OpenClaw gateway runs on NSA (Docker), connects to Groq cloud API for fast LLM inference.
Config: Gateway config stored in /srv/docker/openclaw/state/openclaw.json. Key settings:
- Provider:
groqwith OpenAI-compatible API (api: "openai-responses") - Model:
llama-3.3-70b-versatile(131K context, 32K max tokens) - Control UI origins:
https://openclaw,https://openclaw.(trailing dot for Chrome 144+)
Access: https://openclaw/?token=<OPENCLAW_TOKEN> — token required for WebSocket auth. First-time access requires device pairing (approved via paired.json).
Performance: ~250-400ms response time (vs ~12s with local Ollama on M1).
Pairing new devices: When "pairing required" error appears, move pending device to paired:
ssh nsa
docker exec openclaw-gateway cat /home/node/.openclaw/devices/pending.json # Get device info
# Edit paired.json to include the device (key by deviceId, not requestId)Fallback: Ollama on Mini (192.168.1.116:11434) still available with qwen2.5:7b-16k for local/offline use.
Two-tier storage balancing speed and capacity:
| Drive | Size | Mount | Use |
|---|---|---|---|
| NVMe | 256GB | / |
OS, Docker, databases |
| SATA SSD | 1TB | /mnt/data |
Media, backups |
Key paths:
/srv/docker/- Docker Compose and configs (NVMe)/mnt/data/media/- Plex library (SATA)/mnt/data/backups/- Docker backup archives (SATA)/mnt/data/ntopng/- Network traffic data, 30 day retention (SATA)/mnt/data/minecraft-{laya,richard,mara}/- Minecraft world data (SATA)
Password retrieved automatically from macOS Keychain via ~/.ansible/vault-pass.sh.
Key variables in vault.yml:
vault_wireguard_private_key,vault_wireguard_peers- VPN configvault_pihole_password- Pi-hole adminvault_plex_claim- Plex setup token (expires in 4 min, get from plex.tv/claim)vault_ssh_authorized_keys- SSH public keysvault_mikrotik_admin_password- Router admin passwordvault_mikrotik_pppoe_username,vault_mikrotik_pppoe_password- ISP credentialsvault_mikrotik_wifi_ssid,vault_mikrotik_wifi_password- WiFi configvault_mikrotik_guest_ssid,vault_mikrotik_guest_password- Guest WiFi configvault_openclaw_token- OpenClaw API tokenvault_groq_api_key- Groq cloud LLM API key (OpenClaw backend)vault_tapo_camera_user,vault_tapo_camera_password- Tapo camera RTSP credentialsvault_mini_login_password- Mini macOS login password (for auto-login after reboot)
- WAN:
81.174.139.34(static IP from Plusnet) - LAN IPv4:
192.168.1.0/24, Gateway:192.168.1.1(MikroTik hAP ax³) - Guest IPv4:
192.168.10.0/24, Gateway:192.168.10.1(isolated, public DNS) - LAN IPv6:
fd7a:94b4:f195:7248::/64 - VPN:
10.0.0.0/24(WireGuard on NSA, endpoint: 81.174.139.34:51820) - DNS: Pi-hole at
192.168.1.183:53(LAN), 1.1.1.1/8.8.8.8 (guest) - mDNS: Avahi for
.localresolution (e.g.,nsa.local) - Router: MikroTik hAP ax³ (replaced Plusnet Hub Two on 2026-01-20)
- All services accessible from LAN or VPN only (except WireGuard port 51820)
- Guest network isolated: can reach internet, blocked from LAN (192.168.1.0/24)
| Issue | Status | Notes |
|---|---|---|
| VPN not connected on MB4 (LAN) | ℹ️ Info | WireGuard VPN shows disconnected when MB4 is on LAN — expected, not needed on home network. |
| OpenClaw self-signed cert | ℹ️ Info | https://openclaw uses self-signed cert. Browser shows warning on first visit — click "Proceed" once, then it's remembered. |
| iCloud Private Relay incompatible | ℹ️ Info | Guest WiFi shows "not compatible with Private Relay" - expected for IPv4-only networks. |
| Issue | Resolution | Date |
|---|---|---|
| mb4.yml missing ssh task import | Added import_tasks: tasks/ssh.yml with tags: [ssh] to mb4.yml. SSH authorized keys were never deployed to MB4 via Ansible. |
2026-02-24 |
| mini.yml common/ssh imports untagged | Added tags: [common] and tags: [ssh] to imports — --tags ssh was silently skipping all tasks. |
2026-02-24 |
| vault_groq_api_key missing from vault | Recovered key from live openclaw config (/srv/docker/openclaw-gateway/state/openclaw.json) and added to vault. |
2026-02-24 |
| m365 zsh warning on machines without m365 | Made source ~/.config/m365/m365.zsh conditional with [[ -f ... ]] guard in .zshrc. |
2026-02-24 |
| OpenClaw slow with local Ollama | Switched from Ollama (Mini) to Groq cloud API. Response time improved from ~12s to ~300ms. Config in /srv/docker/openclaw/state/openclaw.json. |
2026-02-09 |
| OpenClaw "origin not allowed" error | Added controlUi.allowedOrigins to config including https://openclaw. (trailing dot for Chrome 144 single-label hostname behavior). |
2026-02-09 |
| Matter server-side commissioning fails | Meross plugs only accept commissioning from phone proxy. Use HA Companion App instead of server-side commission_with_code. |
2026-02-02 |
matter-server --primary-interface None |
Added --primary-interface enp1s0 to docker-compose.yml command. Without it, CHIP SDK picks wrong/no interface. |
2026-02-02 |
| Pi-hole DNS not working on Mac | Removed legacy /etc/hosts entries (10.0.0.1) that were overriding Pi-hole DNS. Pi-hole now handles local hostnames. |
2026-01-21 |
| Network issues (browsers/NSA failing) | Removed duplicate bridge-lan - must use existing bridge (defconf). Set bridge_name: bridge in host_vars/mkt.yml. |
2026-01-20 |
| Browsers not loading (curl works) | Added MSS clamping for PPPoE (MTU 1492). Without it, large TCP packets (TLS handshakes) fail silently. | 2026-01-20 |
| Date | Test | Result |
|---|---|---|
| 2026-02-24 | Termius iOS SSH key auth | ✅ Pass - Key deployed to NSA, Mini, MB4 via Ansible. Passwordless SSH from Termius iOS confirmed. |
| 2026-02-24 | SSH Mini→NSA, MB4→Mini | ✅ Pass - Cross-machine SSH working with key auth on all three hosts |
| 2026-02-09 | OpenClaw + Groq | ✅ Pass - Switched to Groq API (llama-3.3-70b-versatile), ~300ms responses, device pairing working |
| 2026-02-03 | Full test suite (run-all.sh) | ✅ Pass - Quick: 11/11, MikroTik: 25/25. Fixed stale tests (etc→docs, nginx 8080→80, Pi-hole 443→8081) |
| 2026-02-02 | Tapo cameras (3x RTSP) | ✅ Pass - Kitchen, Bedroom, Office streams in HA |
| 2026-02-02 | Matter smart plugs (4x) | ✅ Pass - Meross plugs via Companion App, multi-admin with Apple Home |
| 2026-02-02 | Nanoleaf Thread bulb | ✅ Pass - Commissioned via Companion App, Thread mesh |
| 2026-02-02 | Zigbee coordinator FW | ✅ Pass - Sonoff ZBDongle-P 20210708 → 20240710 |
| 2026-02-02 | Matter Server | ✅ Pass - python-matter-server with --primary-interface enp1s0 |
| 2026-01-29 | Comprehensive network test | ✅ Pass - 9/9 DNS, 3/3 SSH, 8/8 HTTP services, Ollama LAN access |
| 2026-01-29 | Plex HTTPS requirement | https:// |
| 2026-01-29 | Ollama LAN access | ✅ Pass - http://192.168.1.116:11434/ responds, qwen2.5:7b-16k active for OpenClaw |
| 2026-01-29 | MikroTik router health | ✅ Pass - RouterOS 7.19.6, uptime 9+ days, 2% CPU |
| 2026-01-21 | Guest WiFi isolation | ✅ Pass - Internet works, LAN blocked (192.168.1.x unreachable) |
| 2026-01-21 | WireGuard full tunnel (mobile) | ✅ Pass - ping 10.0.0.1, http://ha:8123, https://pihole/admin |
| 2026-01-21 | Pi-hole DNS (LAN) | ✅ Pass - All hostnames resolve via 192.168.1.183 |
| 2026-01-20 | MikroTik Ansible (25 tests) | ✅ Pass - ./tests/test-mkt.sh |
| 2026-01-20 | Guest WiFi connection | ✅ Pass - SSID guestexpress working |
| 2026-01-20 | WiFi PMF (management-protection) | ✅ Pass - Apple security warning resolved |
| 2026-01-16 | WireGuard split tunnel DNS | ✅ Pass - dig @10.0.0.1 google.com resolves |
| 2026-01-16 | Pi-hole ad-blocking via VPN | ✅ Pass - ads.google.com returns 0.0.0.0 |