Personal infrastructure-as-code for home network devices.
Four Devices managed via Ansible:
| Device | Role | Key Services |
|---|---|---|
| NSA (Debian) | Home server | Pi-hole DNS, Home Assistant, Plex, WireGuard VPN, Zigbee, ntopng, OpenClaw |
| MKT (MikroTik) | Router | PPPoE WAN, DHCP, WiFi, Guest network |
| Mini (Mac) | Backup hub + LLM | Syncthing, iCloud backup, Ollama |
| MB4 (Mac) | Workstation + LLM | Syncthing, Docker dev, LM Studio |
Three Networks: managed via Ansible:
| Network | Subnet | Purpose |
|---|---|---|
| LAN | 192.168.1.0/24 | Main network, Pi-hole DNS |
| Guest | 192.168.10.0/24 | Isolated, public DNS (1.1.1.1) |
| VPN | 10.0.0.0/24 | WireGuard remote access |
Services (LAN & VPN): all via nginx reverse proxy — no port numbers:
| Service | URL |
|---|---|
| Home Assistant | http://ha |
| Pi-hole Admin | http://pihole/admin |
| Plex | http://plex/web |
| Cockpit | http://nsa |
| OpenClaw | https://openclaw |
| ntopng | http://ntopng |
| Static sites | http://laya, http://hopo, http://docs |
| SSH | ssh richardbell@nsa |
| Syncthing | ~/Sync/ folder sync |
| GitHub Actions | systemd service (self-hosted runner) |
| Device | Hostname | Type | Chip | RAM | Storage | OS |
|---|---|---|---|---|---|---|
| NSA | nsa | Beelink SEi8 | Intel i3-8109U | 16GB DDR4 | 238GB NVMe + 1TB SATA | Debian 13 |
| Mac Mini | mini | Mac Mini | Apple M1 | 16GB | 256GB | macOS 15 |
| MacBook Pro | mb4 | MacBook Pro 14" | Apple M4 | 36GB | 512GB | macOS 15 |
| iPhone | ios | iPhone 17 Pro Max | A18 Pro | - | 512GB | iOS 18 |
| Router | mkt | MikroTik hAP ax³ | ARM64 | 1GB | 128MB | RouterOS 7.19.6 |
| Device | Role | Services |
|---|---|---|
| NSA | Home server | Docker, VPN, DNS, Media, MQTT, Zigbee, Syncthing, Cockpit, OpenClaw AI |
| Mini | Backup hub / LLM server | Syncthing, iCloud backup, Ollama |
| MB4 | Daily workstation | Syncthing, Docker (Colima) |
| iOS | Mobile | Syncthing, WireGuard VPN |
| Router | Network gateway | PPPoE, DHCP, WiFi, Guest VLAN, Firewall |
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | SSH access from LAN | ✅ Done | Port 22, key-only auth |
| 2 | SSH access from VPN | ✅ Done | Via WireGuard tunnel |
| 3 | Docker containers running | ✅ Done | 10 containers: Pi-hole, Home Assistant, Plex, nginx, Mosquitto, Zigbee2MQTT, ntopng, OpenClaw, Matter Server |
| 4 | WireGuard VPN server | ✅ Done | Port 51820, 3 peers configured (MB4, Mini, iOS) |
| 5 | Home Assistant accessible | ✅ Done | http://ha (via nginx proxy) |
| 6 | Plex media server | ✅ Done | http://plex (via nginx proxy) |
| 7 | Cockpit admin panel | ✅ Done | http://nsa (via nginx proxy) |
| 8 | nginx reverse proxy | ✅ Done | Port 80/443, all services via hostname |
| 9 | MQTT broker | ✅ Done | Port 1883 |
| 10 | Zigbee2MQTT | ✅ Done | Sonoff ZBDongle-P, firmware 20240710 |
| 11 | Firewall (nftables) | ✅ Done | Default deny, explicit allow |
| 12 | Weekly Docker backup | ✅ Done | Sun 3am → Syncthing |
| 13 | Pi-hole DNS (LAN) | ✅ Done | MikroTik pushes Pi-hole as DNS (2026-01-20) |
| 14 | Pi-hole DNS (VPN) | ✅ Done | Works via 10.0.0.1 |
| 15 | Ad-blocking (LAN) | ✅ Done | Pi-hole blocks ads network-wide |
| 16 | Ad-blocking (VPN) | ✅ Done | Works when VPN active |
| 17 | Local DNS names | ✅ Done | Pi-hole custom.list + /etc/hosts on Macs |
| 18 | OpenClaw AI assistant | ✅ Done | https://openclaw (via nginx HTTPS proxy), Ollama backend on Mini |
| 19 | Tapo cameras (3x) | ✅ Done | Kitchen, Master Bedroom, Office — RTSP via generic camera integration |
| 20 | Matter smart plugs (4x) | ✅ Done | Meross WiFi plugs — shared from Apple Home via Matter multi-admin |
| 21 | Nanoleaf bulb | ✅ Done | Thread/Matter — shared from Apple Home via Companion App |
| 22 | Matter Server | ✅ Done | python-matter-server container, --primary-interface enp1s0 |
| 23 | GitHub Actions runner | ✅ Done | Self-hosted CI/CD, systemd service, Playwright + AWS CLI deps |
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | SSH access | ✅ Done | Key-only auth |
| 2 | Syncthing running | ✅ Done | Syncs with NSA, MB4, iOS |
| 3 | iCloud backup | ✅ Done | Daily 3am rsync to iCloud Drive |
| 4 | Homebrew packages | ✅ Done | Managed via Ansible |
| 5 | /etc/hosts entries | ✅ Done | NSA service names |
| 6 | Ollama LLM server | ✅ Done | LAN-accessible on port 11434, OpenClaw backend (qwen2.5:7b-16k) |
| 7 | Auto-login after reboot | ✅ Done | LaunchAgents (Ollama, Syncthing) start without manual login |
| 8 | Always-on power settings | ✅ Done | No sleep, Wake on LAN, auto-restart after power failure |
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | SSH access | ✅ Done | Key-only auth |
| 2 | Syncthing running | ✅ Done | Syncs with NSA, Mini, iOS |
| 3 | Homebrew packages | ✅ Done | Managed via Ansible |
| 4 | /etc/hosts entries | ✅ Done | NSA service names |
| 5 | WireGuard client | ✅ Done | Split tunnel for DNS |
| 6 | Docker (Colima) | ✅ Done | ~/docker/, Ansible managed |
| 7 | PostgreSQL container | ✅ Done | Port 5432, PostGIS 16 |
| 8 | DynamoDB Local container | ✅ Done | Port 8000, AWS emulator |
| 9 | k6 load testing | ✅ Done | On-demand (profiles: tools) |
| 10 | OpenVAS vulnerability scanner | ✅ Done | Security scanning (profiles: security) |
| 11 | nmap network scanner | ✅ Done | Network reconnaissance (profiles: security) |
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | DHCP server | ✅ Done | Pool 192.168.1.100-200 |
| 2 | DHCP reservations | ✅ Done | NSA, Mini, 3x Tapo cameras, Aqara doorbell |
| 3 | Port forward 51820 | ✅ Done | WireGuard VPN |
| 4 | DNS to Pi-hole | ✅ Done | Pi-hole primary, 1.1.1.1 fallback |
| 5 | PPPoE (Plusnet) | ✅ Done | Replaced Plusnet Hub Two |
| 6 | SSH access | ✅ Done | ssh admin@mkt |
| 7 | WiFi (2.4/5GHz) | ✅ Done | WPA2/WPA3-PSK with PMF |
| 8 | Guest WiFi | ✅ Done | SSID: guestexpress |
| 9 | Guest isolation | ✅ Done | 192.168.10.0/24, blocked from LAN |
| 10 | Ansible managed | ✅ Done | ansible-playbook mkt.yml |
INTERNET (Plusnet ISP)
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ ROUTER (MikroTik hAP ax³) │
│ 192.168.1.1 │
│ WAN: PPPoE │ LAN: 192.168.1.0/24 │ Guest: 192.168.10.0/24 │
│ DHCP: .100-.200 │ DNS: Pi-hole (LAN) / 1.1.1.1 (Guest) │
└───────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ NSA │ │ Mini │ │ MB4/iOS │
│ 192.168.1.183 │ │ 192.168.1.116 │ │ DHCP Pool │
│ │ │ │ │ │
│ Pi-hole DNS │ │ Syncthing │ │ Syncthing │
│ Docker │ │ iCloud Backup │ │ WireGuard │
│ WireGuard VPN │ │ │ │ │
│ Home Assistant │ └─────────────────┘ └─────────────────┘
│ Plex, nginx │
└─────────────────┘
│ ┌─────────────────┐
│ WireGuard VPN (10.0.0.0/24) │ Guest WiFi │
▼ │ 192.168.10.x │
┌─────────────────┐ │ (isolated) │
│ Remote Clients │ │ DNS: 1.1.1.1 │
│ 10.0.0.2-254 │ └─────────────────┘
└─────────────────┘
| Setting | Value |
|---|---|
| WAN IP | 81.174.139.34 (static) |
| LAN Subnet | 192.168.1.0/24 |
| Guest Subnet | 192.168.10.0/24 (isolated) |
| Gateway | 192.168.1.1 (MikroTik) |
| DNS Primary | 192.168.1.183 (Pi-hole) |
| DNS Fallback | 1.1.1.1 (Cloudflare) |
| VPN Subnet | 10.0.0.0/24 |
| ISP | Plusnet FTTC |
| Device | IP | MAC |
|---|---|---|
| NSA | 192.168.1.183 | 7c:83:34:b2:c1:33 |
| Mini | 192.168.1.116 | 14:98:77:78:d6:46 |
| Router | 192.168.1.1 | 04:f4:1c:d1:38:84 |
| Tapo Kitchen (C210) | 192.168.1.103 | e4:fa:c4:b5:3e:86 |
| Tapo Bedroom (C210) | 192.168.1.104 | e4:fa:c4:b5:4b:5e |
| Tapo Office (TC70) | 192.168.1.102 | 40:ed:00:36:a3:58 |
| Aqara Doorbell | 192.168.1.113 | 54:ef:44:5a:6c:15 |
| Device | VPN IP |
|---|---|
| NSA | 10.0.0.1 |
| iOS | 10.0.0.2 |
| MB4 | 10.0.0.3 |
| Mini | 10.0.0.4 |
All DNS queries go through Pi-hole for ad-blocking and local name resolution.
Local DNS Records (Pi-hole custom.list):
192.168.1.183 nsa ha pihole plex laya hopo docs openclaw ntopng
192.168.1.116 mini
Service Discovery:
| Method | Format | Accessible From |
|---|---|---|
| Pi-hole DNS | nsa, ha, mini |
LAN + VPN |
| mDNS/Avahi | nsa.local, mini.local |
LAN only |
| Port | Service | URL | Access |
|---|---|---|---|
| 22 | SSH | ssh richardbell@nsa |
LAN + VPN |
| 53 | Pi-hole DNS | - | LAN + VPN |
| 80 | nginx proxy | http://ha, http://pihole, http://plex, etc | LAN + VPN |
| 443 | nginx HTTPS | https://openclaw (self-signed cert) | LAN + VPN |
| 1883 | MQTT | - | LAN + VPN |
| 8081 | Pi-hole Admin | http://pihole/admin (via proxy) or direct | LAN + VPN |
| 51820 | WireGuard | - | Anywhere |
Many public networks use 192.168.1.0/24 which conflicts with home LAN. Use VPN IP (10.0.0.1) instead:
# Add to /etc/hosts on Mac when remote
sudo sh -c 'echo "10.0.0.1 laya hopo docs ha plex pihole nsa openclaw ntopng" >> /etc/hosts'| Service | URL |
|---|---|
| Home Assistant | http://ha |
| Plex | http://plex/web |
| Pi-hole Admin | http://pihole/admin |
| Cockpit | http://nsa |
| OpenClaw | https://openclaw |
| ntopng | http://ntopng |
| Static sites | http://laya, http://hopo, http://docs |
| SSH | ssh root@10.0.0.1 |
| From | To | Command |
|---|---|---|
| MB4/Mini (LAN) | NSA | ssh nsa or ssh richardbell@192.168.1.183 |
| MB4/Mini (LAN) | NSA root | ssh root@nsa |
| MB4 (Remote) | NSA | ssh nsa (via WireGuard) |
| MB4 (Remote) | Mini | ssh mini (via WireGuard + ProxyJump) |
NSA ←──→ MB4
↑ ↑
│ │
↓ ↓
Mini ←──→ iOS
All peers sync ~/Sync bidirectionally.
| Source | Method | Destination | Schedule |
|---|---|---|---|
| NSA /srv/docker | tar + Syncthing | Mini, MB4 | Weekly (Sun 3am) |
| Mini ~/Sync | rsync | iCloud Drive | Daily (3am) |
| MB4 ~/Sync | Syncthing | NSA, Mini | Real-time |
Data flow:
NSA Docker backup → ~/Sync/backups/nsa/ → Syncthing → Mini → iCloud
# Configure everything
ansible-playbook site.yml
# Single machine
ansible-playbook nsa.yml
ansible-playbook mini.yml
ansible-playbook mb4.yml
ansible-playbook mkt.yml
# Dry run
ansible-playbook site.yml --check --diff
# Specific tags
ansible-playbook nsa.yml --tags docker
ansible-playbook nsa.yml --tags pihole
ansible-playbook nsa.yml --tags plex
ansible-playbook mini.yml --tags icloud-backup
ansible-playbook mb4.yml --tags docker
ansible-playbook mkt.yml --tags dhcp
ansible-playbook mkt.yml --tags wifiSecrets stored encrypted in vault.yml. Password retrieved from macOS Keychain automatically via ~/.ansible/vault-pass.sh.
Two git hooks prevent accidental commit/push of unencrypted secrets:
| Hook | Trigger | Protection |
|---|---|---|
pre-commit |
Before commit | Blocks if staged vault.yml is not encrypted |
pre-push |
Before push | Scans all commits being pushed for unencrypted vault |
Hooks are in .githooks/ (tracked by git). After cloning, tell git where to find them:
git config core.hooksPath .githooksTo bypass in emergency (NOT recommended): git commit --no-verify
# Edit vault
ansible-vault edit vault.yml
# View vault
ansible-vault view vault.yml
# Re-encrypt with new password
ansible-vault rekey vault.yml| Variable | Purpose |
|---|---|
| vault_wireguard_private_key | VPN server key |
| vault_wireguard_peers | VPN peer configs |
| vault_mqtt_password | MQTT broker auth |
| vault_pihole_password | Pi-hole admin |
| vault_plex_claim | Plex setup token |
| vault_macos_ssh_key | Mac SSH key |
| vault_ssh_authorized_keys | SSH public keys |
| vault_mikrotik_admin_password | Router admin |
| vault_mikrotik_pppoe_username/password | ISP credentials |
| vault_mikrotik_wifi_ssid/password | Main WiFi |
| vault_mikrotik_guest_ssid/password | Guest WiFi |
| vault_openclaw_token | OpenClaw API token |
| vault_tapo_camera_user | Tapo camera RTSP username |
| vault_tapo_camera_password | Tapo camera RTSP password |
| vault_mini_login_password | Mini macOS login password (auto-login) |
# Quick smoke test (~30 seconds)
./tests/quick-check.sh
# Full test suite — MB4, NSA, MikroTik, nmap (auto-logged)
./tests/run-all.sh
# MikroTik router tests only
./tests/test-mkt.sh# nmap — port/service scan (auto-runs in full suite, or standalone)
docker compose -f ~/docker/docker-compose.yml --profile security run --rm nmap -sV 192.168.1.0/24
# OpenVAS — vulnerability scanner (manual, web UI)
docker compose -f ~/docker/docker-compose.yml --profile security up -d openvas
open http://localhost:9392 # Login: admin / admin| Log | Location | Notes |
|---|---|---|
| Full suite | tests/results/YYYY-MM-DD_HHMMSS.log |
Auto-saved by run-all.sh, colors stripped |
| nmap reports | ~/docker/nmap/reports/ |
nsa-YYYY-MM-DD.txt, mini-YYYY-MM-DD.txt |
| OpenVAS | ~/docker/openvas/data/ |
Web UI managed, exportable as PDF/CSV |
See tests/README.md for details or browse http://docs/testing.html on LAN.
| Date | Test | Result | Notes |
|---|---|---|---|
| 2026-02-02 | Tapo cameras (3x) | ✅ Pass | RTSP streams in HA via generic camera integration |
| 2026-02-02 | Matter smart plugs (4x) | ✅ Pass | Meross plugs commissioned via Companion App, multi-admin with Apple Home |
| 2026-02-02 | Nanoleaf Thread bulb | ✅ Pass | Commissioned via Companion App, Thread mesh via Apple border router |
| 2026-02-02 | Zigbee coordinator FW | ✅ Pass | Sonoff ZBDongle-P flashed 20210708 → 20240710 via Zigbee2MQTT OTA |
| 2026-02-02 | Matter Server | ✅ Pass | python-matter-server with --primary-interface enp1s0, HA integration connected |
| 2026-01-29 | Comprehensive LAN test | ✅ Pass | DNS (9/9), SSH (3/3), HTTP services (8/8), Ollama, MikroTik |
| 2026-01-29 | Plex HTTPS requirement | HTTP returns empty reply; HTTPS works. Bookmarks updated to https:// |
|
| 2026-01-29 | Ollama LAN access | ✅ Pass | http://192.168.1.116:11434/ responds, qwen2.5:7b-16k active for OpenClaw |
| 2026-01-21 | iOS WireGuard VPN | ✅ Pass | 10.0.0.2, Pi-hole/Plex accessible from mobile |
| 2026-01-21 | Guest network isolation | ✅ Pass | 192.168.10.x, internet works, LAN blocked |
| 2026-01-21 | WireGuard full tunnel | ✅ Pass | Remote access to ha, pihole, plex |
| 2026-01-21 | Tests via VPN | ✅ Pass | 25/25 MikroTik tests from offsite |
| 2026-01-20 | Pi-hole DNS from LAN | ✅ Pass | dig @192.168.1.183 google.com works |
| 2026-01-20 | MikroTik Ansible tests | ✅ Pass | 25/25 tests passed |
| 2026-01-20 | Guest WiFi | ✅ Pass | SSID guestexpress working |
| 2026-01-16 | WireGuard split tunnel DNS | ✅ Pass | dig @10.0.0.1 google.com resolves correctly |
| 2026-01-16 | Pi-hole ad-blocking via VPN | ✅ Pass | ads.google.com → 0.0.0.0 (blocked) |
Before running Ansible, NSA needs:
- Debian 13 (trixie) installed
- User
richardbellwith sudo access - SSH enabled, key copied
- Static IP or DHCP reservation
- 1TB SATA SSD mounted at
/mnt/data
| Port | Protocol | Service | Access |
|---|---|---|---|
| 22 | TCP | SSH | LAN + VPN |
| 53 | TCP/UDP | DNS (Pi-hole) | LAN + VPN |
| 80 | TCP | HTTP (nginx) | LAN + VPN |
| 443 | TCP | HTTPS (OpenClaw) | LAN + VPN |
| 1883 | TCP | MQTT | LAN + VPN |
| 8123 | TCP | Home Assistant | LAN + VPN |
| 9090 | TCP | Cockpit | LAN + VPN |
| 32400 | TCP | Plex | LAN + VPN |
| 3000 | TCP | ntopng | LAN + VPN |
| 18789 | TCP | OpenClaw | LAN + VPN |
| 5580 | TCP | Matter Server (WebSocket) | Localhost only (HA→matter-server) |
| 51820 | UDP | WireGuard | Anywhere |
| Item | Path |
|---|---|
| Docker compose | /srv/docker/docker-compose.yml |
| Docker data | /srv/docker/{service}/ |
| Media library | /mnt/data/media/ |
| Backups | /mnt/data/backups/ |
| Firewall | /etc/nftables.conf |
| WireGuard | /etc/wireguard/wg0.conf |
| Item | Path |
|---|---|
| Sync folder | ~/Sync/ |
| Backup script | ~/bin/sync-backup.sh |
| Backup log | ~/Library/Logs/sync-backup.log |
| LaunchAgents | ~/Library/LaunchAgents/ |
| Device | Server | Model | Context | Size | Notes |
|---|---|---|---|---|---|
| Mini | Ollama | qwen2.5:7b-16k | 16K | ~5GB | Active OpenClaw backend (~12s response) |
| Mini | Ollama | qwen2.5:14b | 4K default | ~9GB | Available but too slow for OpenClaw agentic mode |
| MB4 | LM Studio | Qwen2.5-32B-Instruct | - | 25GB | Local dev |
OpenClaw LLM setup: OpenClaw gateway (NSA) connects to Ollama (Mini, 192.168.1.116:11434) via explicit provider config. Custom Ollama model qwen2.5:7b-16k created with num_ctx: 16384 to meet gateway's 16K minimum context requirement. The 14B model works but is too slow for agentic chat on M1 16GB (~84s vs ~12s for 7B).
| Item | Path |
|---|---|
| Docker compose | ~/docker/docker-compose.yml |
| PostgreSQL data | ~/docker/postgres/data/ |
| DynamoDB data | ~/docker/dynamodb/data/ |
| k6 scripts | ~/docker/k6/scripts/ |
| Environment | ~/docker/.env |
NSA has a two-tier storage setup balancing speed and capacity:
| Drive | Size | Type | Mount | Purpose |
|---|---|---|---|---|
| NVMe | 256GB | PCIe NVMe | / |
Fast tier - OS, Docker, databases |
| SATA SSD | 1TB | 2.5" SATA | /mnt/data |
Capacity tier - media, backups |
| Path | Contents | Why NVMe |
|---|---|---|
/ |
Debian OS | Fast boot |
/srv/docker/ |
Docker Compose + container configs | Fast container startup |
/var/lib/docker/ |
Container images, volumes | I/O intensive |
| Pi-hole SQLite | DNS query database | Frequent writes |
| Home Assistant DB | Event/state history | Frequent writes |
| Syncthing index | File metadata | Random I/O |
| Path | Contents | Why SATA |
|---|---|---|
/mnt/data/media/ |
Plex library (movies, TV, music) | Large files, sequential reads |
/mnt/data/backups/ |
Docker backup archives | Weekly writes, large files |
/mnt/data/ntopng/ |
Network traffic data (30 day retention) | Moderate writes, searchable |
/mnt/data/transcode/ |
Plex transcoding temp | Can be slow, disposable |
/mnt/data/downloads/ |
Staging area for new media | Temporary storage |
| Drive | Used | Available | Headroom |
|---|---|---|---|
| NVMe | ~40GB | ~200GB | Plenty for Docker growth |
| SATA | TBD | ~1TB | Media library expansion |
Note: Monitor with df -h - if NVMe exceeds 80%, consider moving large Docker volumes to SATA.
- Install Debian 13 minimal (server, SSH only, no desktop)
- Create user
richardbellwith sudo - Enable root SSH:
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config - Copy SSH key from Mac:
ssh-copy-id root@192.168.1.183 - Mount data drive at
/mnt/data(UUID: 56380a4f-8876-4b77-9dc0-7d0d8ab7d948) - Remove brltty if Zigbee dongle not detected:
apt remove brltty - Run from Mac:
ansible-playbook nsa.yml - Restore Docker data from backup (see
docs/nsa-migration.md)
- Cockpit: http://nsa (via nginx proxy) or https://192.168.1.183:9090 (direct)
- Physical access to Beelink
| Item | Value |
|---|---|
| Username | richardbell |
| SSH key type | ed25519 |
| Sync folder | ~/Sync/ |
| Admin group | sudo (Linux), wheel (macOS) |
| Timezone | Europe/London |