LAN
10.0.0.0/24| Action | Source | Destination | Note |
|---|---|---|---|
| Block | LAN | mgmt | Isolated |
| Block | LAN | services | Isolated |
| Block | LAN | automation | Isolated |
| Pass | LAN | * | Internet OK |
mgmt
10.0.10.0/24| Action | Port | Destination | Note |
|---|---|---|---|
| Pass | 53 | * | AdGuard bootstrap DNS (resolves DoH upstream hostname) |
| Pass | 53 | AdGuard | DNS (internal — mgmt devices → AdGuard) |
| Block | * | RFC1918 | No internal |
| Pass | 443 | * | HTTPS — updates / Ubiquiti cloud / AdGuard DoH upstream |
| Pass | 80 | * | HTTP — apt repos |
| Block | * | * | Default deny |
services
10.0.20.0/24| Action | Port | Destination | Note |
|---|---|---|---|
| Pass | 53 | AdGuard | DNS |
| Block | * | RFC1918 | No internal access |
| Pass | 443 | * | HTTPS — updates, metadata |
| Pass | 80 | * | HTTP — apt repos |
| Block | * | * | Deny everything else |
automation
10.0.30.0/24| Action | Port | Destination | Note |
|---|---|---|---|
| Pass | 53 | AdGuard | DNS only |
| Block | * | RFC1918 | No internal access |
| Pass | 443 | * | HTTPS (Claude, Notion, GCal) |
| Pass | 80 | * | HTTP (apt repos) |
| Pass | 465 | * | SMTP/SSL (SMTP provider) |
| Block | * | * | Deny everything else |
iot
10.0.40.0/24Reserved for IoT / smart home devices. Firewall rules not yet configured.
Scheduled Reports
Monday Brief
0 10 * * 1Preview of the week ahead: personal calendar highlights + 7-day weather forecast.
Daily Brief
0 10 * * 2-7Today's calendar events, weather forecast, and outfit/prep tip.
Friday Report
0 23 * * 5Weekly wrap-up of online coding class notes transcribed in Notion — topics covered, concepts learned, follow-ups.
Report Pipeline
Auto-Deploy Pipeline
Symptom
msmtp: permission denied when scripts pass --file= pointing to a path inside the repo.
Cause
AppArmor restricts msmtp to reading files only from the home directory.
Fix
Copy config to ~/.msmtprc and remove --file= from all scripts.
cp /opt/agent60/.msmtprc ~/.msmtprc
chmod 600 ~/.msmtprc
Symptom
Cron job exits with code 127. Log shows command not found: claude.
Cause
Cron runs with a stripped PATH. /home/user/.local/bin (where claude lives) is not included.
Fix
Add this as the very first line of crontab -e:
PATH=/home/user/.local/bin:/usr/local/bin:/usr/bin:/bin
Symptom
msmtp silently hangs or shows connect failed. Automation VLAN is IPv4-only but msmtp resolves SMTP hostnames to IPv6 first.
Fix
Disable IPv6 on agent60 VM permanently:
echo "net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1" | sudo tee /etc/sysctl.d/99-no-ipv6.conf
sudo sysctl --system
Symptom
msmtp: authentication failed even with the correct password.
Fix
Use an app password — not your regular account password. For the SMTP provider: Account Security → App Passwords.
Symptom
msmtp reports success, logs look clean, but the report email never lands in the inbox.
Cause
The test message body is just the word test with no Subject: header or real body. Providers (Gmail, etc.) silently drop or spam-filter these as malformed / suspicious mail — msmtp still reports success because the SMTP handhake completed.
Fix
Send test messages with a proper subject and body, e.g.:
printf "Subject: msmtp test\n\nThis is a test email from the homelab.\n" | msmtp you@example.com
Once a real subject and body are present, the message arrives normally.
Fix
Remove tls_trust_file from ~/.msmtprc to let msmtp use the system default CA bundle. Setting it manually to a nonexistent path causes certificate errors.
Diagnose
Test MCP connectors interactively and verify auth:
echo "List my Google Calendar events for this week" | claude -p
echo "List my online coding class notes transcribed in Notion from the past week" | claude -p
claude whoami
If connectors return empty, verify they are active at claude.ai/connectors.
Cause
The agent60 VM runs UTC. local = UTC offset. Cron times must be offset accordingly.
Check & fix
timedatectl
# To set: sudo timedatectl set-timezone America/Chicago
Example: 5:00 AM local → 0 10 * * 1 in UTC
Debug
Run with --dry-run to generate the full report and print HTML without sending email:
./friday_report.sh --dry-run
./monday_brief.sh --dry-run
./daily_brief.sh --dry-run
Cause
VLAN 30 only allows port 443 outbound. SSH default port 22 is blocked by firewall rules.
Fix
Route SSH via ssh.github.com:443. Add to ~/.ssh/config:
Host github.com
Hostname ssh.github.com
Port 443
Diagnose
systemctl status webhook ngrok
journalctl -fu webhook
journalctl -u webhook --no-pager | grep -E "Pull|Rejected|Webhook"
If ngrok is down, verify the static domain in ~/.config/ngrok/ngrok.yml matches the GitHub webhook URL. If the webhook is rejected, check the HMAC secret matches index.js.
By design
Proxmox (10.0.0.XX:8006) is only reachable from the LAN (10.0.0.0/24). The OPNsense Tailscale plugin advertises all subnets (LAN + VLAN 10/20/30/40), so any tailnet device has full L3 reach — connect via Tailscale and browse to 10.0.0.XX:8006. Same path reaches Homarr (10.0.10.XX), AdGuard (10.0.10.XX), Unifi OS (10.0.10.XX), and agent60 VM (10.0.30.XX).
Symptom
TOTP code rejected at login screen — usually caused by clock drift.
Fix
Verify NTP sync. Re-generate TOTP secret under user settings if needed. 2FA locations: Proxmox → Datacenter → Users → Edit; OPNsense → System → Access → Users.
Diagnose
systemctl status cron
crontab -l
tail -f /opt/agent60/logs/friday_report.log
Simulate the exact cron environment to reproduce issues locally:
env -i PATH=/home/user/.local/bin:/usr/local/bin:/usr/bin:/bin \
HOME=$HOME USER=$USER \
/opt/agent60/friday_report.sh --dry-run
Symptom
msmtp: connect failed when scripts pass --file= pointing to a path inside the repo.
Scripts fail with Unable to connect to Anthropic services — ECONNREFUSED. curl shows Could not resolve host: api.anthropic.com. DNS broken across all VLANs.
Cause
AdGuard uses DoH upstream (https://dns10.quad9.net/dns-query) and requires a bootstrap DNS lookup on plain port 53 to resolve the Quad9 hostname before it can connect. Adding a "deny everything else" rule to mgmt blocked this outbound port 53 request. AdGuard's cached IP kept things working briefly, then DNS failed entirely once the cache expired.
Fix
Add a rule in mgmt above the RFC1918 block allowing AdGuard's IP to reach any destination on port 53:
Pass TCP/UDP 10.0.10.XX → *:53 — AdGuard bootstrap DNS
Rule order matters: this rule must come before the RFC1918 block, otherwise AdGuard's bootstrap queries to public resolvers are caught and dropped.
Homelab Documentation
Single-node Proxmox VE homelab on a Mini PC, running OPNsense as the core firewall & router. Four VLANs segment management, services, automation, and IoT. AdGuard Home provides network-wide DNS plus .homelab URL rewrites. Tailscale (via the OPNsense exit-node plugin) advertises every subnet for secure remote access.
🎯 Goals
- Self-host services securely — if an LXC or VM gets compromised, it stays contained
- Securely remote-access the homelab from anywhere
- Learn Proxmox, Docker, Linux, networking
🖥️ Hardware
- Mini PC — 16GB RAM · 256GB SSD
- Unifi Flex Mini 2.5G Switch
- Ugreen USB-C to Ethernet Adapter (2.5G)
- Ugreen USB-to-USB-C Adapter (10Gbps, 3A)
- 5-pack Cable Matters 1ft 10Gbps Cat6 patch cables
🔌 Physical Wiring
- WAN: Cat6 from wall → Mini PC nic0 (vmbr0)
- LAN: Mini PC USB port → USB/USB-C adapter → USB-C to Ethernet adapter → Cat6 patch → Port 2 of Flex Mini (vmbr1)
🌐 Network Architecture
Home gateway is 192.168.X.X (ISP router, DHCP range 192.168.X.x). OPNsense runs as a Proxmox VM with vmbr0 as WAN (192.168.X.XX/24) and vmbr1 as LAN (10.0.0.1/24). Proxmox itself sits on 10.0.0.XX with gateway 10.0.0.1 — only reachable through the LAN. The OPNsense Tailscale plugin runs as an exit node and advertises every VLAN subnet for remote access.
Division of labor
🛡️ OPNsense VM — does all the actual networking
- Creates + enables each VLAN on the vmbr1 parent (Interfaces → Other Types → VLAN)
- Configures each VLAN interface (subnet, gateway IP, MTU) under Interfaces → Assignments
- Runs DHCP on every VLAN (advertises AdGuard 10.0.10.XX as DNS)
- Default gateway for every subnet (.1 of each /24)
- All firewall rules between VLANs and to/from WAN
- Forwards LAN DNS → AdGuard, handles NAT for WAN egress
🖧 Unifi OS Server — dumb-pipe configurator
- Defines the VLAN tag numbers (10 / 20 / 30 / 40) so the switch knows what to forward
- Port 2 native VLAN = LAN, trunks tags 10/20/30/40 (uplink to OPNsense vmbr1)
- Assigns access VLAN per remaining port as devices are added
- Does NOT do subnets, gateways, DHCP, DNS, firewall, or routing — all OPNsense
🚫 AdGuard Home — DNS + ad blocking
- DNS resolver for every LXC, VM, and tailnet client
- Upstream: Quad9 DoH (https://dns10.quad9.net/dns-query) — malware-blocking endpoint over HTTPS:443
- Hosts .homelab URL rewrites
- OPNsense DHCP and the Tailscale plugin both advertise AdGuard as DNS
🔀 VLAN Layout
| VLAN | Tag | Subnet | DHCP Range | Purpose |
|---|---|---|---|---|
| LAN | — | 10.0.0.0/24 | 10.0.0.50 – 200 | Proxmox host, trusted wired devices |
| mgmt | 10 | 10.0.10.0/24 | 10.0.10.50 – 200 | Proxmox, OPNsense, Unifi OS, AdGuard, Homarr |
| services | 20 | 10.0.20.0/24 | 10.0.20.50 – 200 | Self-hosted services — homelab-site, Jellyfin (planned) |
| automation | 30 | 10.0.30.0/24 | 10.0.30.50 – 200 | agent60 VM |
| iot | 40 | 10.0.40.0/24 | 10.0.40.50 – 200 | IoT / smart home (future) |
All DHCP servers advertise 10.0.10.XX (AdGuard) as DNS.
🛡️ Firewall Rules
LAN
| Action | Protocol | Source | Destination | Description |
|---|---|---|---|---|
| Block | IPv4 * | LAN network | mgmt network | Block LAN → mgmt |
| Block | IPv4 * | LAN network | services network | Block LAN → services |
| Block | IPv4 * | LAN network | automation network | Block LAN → automation |
| Pass | IPv4 * | LAN network | * | Allow LAN to internet |
LAN devices use OPNsense (10.0.0.1) for DNS, which forwards to AdGuard internally. Mgmt UIs (Homarr, Proxmox, OPNsense) reached via Tailscale when on LAN or remotely.
mgmt (VLAN 10)
| Action | Protocol | Source | Destination | Port | Description |
|---|---|---|---|---|---|
| Pass | TCP/UDP | 10.0.10.XX | * | 53 | AdGuard bootstrap DNS — must be first; resolves DoH upstream hostname (dns10.quad9.net) before RFC1918 block |
| Pass | TCP/UDP | mgmt network | 10.0.10.XX | 53 | DNS to AdGuard (mgmt devices) |
| Block | * | mgmt network | RFC1918 | * | Block all internal nets |
| Pass | TCP | mgmt network | * | 443 | HTTPS — updates, Ubiquiti cloud, Proxmox community repo, AdGuard DoH upstream |
| Pass | TCP | mgmt network | * | 80 | HTTP — apt package repos |
| Block | * | mgmt network | * | * | Default deny |
services (VLAN 20)
| Action | Protocol | Source | Destination | Port | Description |
|---|---|---|---|---|---|
| Pass | TCP/UDP | services network | 10.0.10.XX | 53 | DNS to AdGuard |
| Block | * | services network | RFC1918 | * | Block lateral access to other internal networks |
| Pass | TCP | services network | * | 443 | HTTPS — updates, Jellyfin metadata (TMDB / TVDB), GitHub SSH-via-443 |
| Pass | TCP | services network | * | 80 | HTTP — apt package repos |
| Block | * | services network | * | * | Deny everything else |
Inbound: LAN → services stays blocked. Jellyfin (and any future service) is reached via Tailscale only — the OPNsense Tailscale plugin already advertises 10.0.20.0/24 to the tailnet, so tailnet devices have L3 reach without a per-port pass rule.
automation (VLAN 30)
| Action | Protocol | Source | Destination | Port | Description |
|---|---|---|---|---|---|
| Pass | TCP/UDP | automation network | 10.0.10.XX | 53 | DNS to AdGuard |
| Block | * | automation network | RFC1918 | * | No access to any internal network |
| Pass | TCP | automation network | * | 443 | HTTPS — Anthropic, Notion, Google, Open-Meteo |
| Pass | TCP | automation network | * | 80 | HTTP — apt package repos |
| Pass | TCP | automation network | * | 465 | SMTP SSL — SMTP provider |
| Block | * | automation network | * | * | Deny everything else |
iot (VLAN 40)
Rules not yet configured — no devices on this VLAN.
WAN / Tailscale
No inbound rules (default deny). Tailscale subnet routing handled by the OPNsense Tailscale plugin.
📊 LXC + VM Resources
| Service | Type | IP(s) | RAM | Cores | VLAN |
|---|---|---|---|---|---|
| OPNsense | VM | WAN 192.168.X.XX · LAN 10.0.0.1 · mgmt 10.0.10.1 | 2GiB / 4GiB swap | 4 (1 socket) | — |
| AdGuard Home | LXC | 10.0.10.XX | 2GiB / 2GiB swap | 1 | mgmt |
| Unifi OS Server | LXC | 10.0.10.XX | 4GiB / 512MiB swap | 2 | mgmt |
| Homarr | LXC | 10.0.10.XX | 2GiB / 512MiB swap | 2 | mgmt |
| homelab-site | LXC | 10.0.20.XX | 256MiB / 256MiB swap | 1 | services |
| agent60 VM | VM | 10.0.30.XX | 2GiB / 512MiB swap | 2 | automation |
📦 Service Summary
🛡️ OPNsense (VM)
Core firewall, router, DHCP, DNS forwarder, NAT. WAN on vmbr0, LAN + VLANs on vmbr1. Tailscale plugin enabled as exit node advertising every VLAN subnet.
🖧 Unifi OS Server (LXC)
Controller for the Flex Mini switch (the switch has no UI of its own). Defines VLAN tag numbers and Port 2 trunk. OPNsense handles all the actual networking.
🚫 AdGuard Home (LXC)
DNS resolver and ad/tracker blocker for every device. Quad9 DoH upstream (dns10.quad9.net — malware-blocking endpoint). Hosts .homelab URL rewrites so memorable names work across LAN / VLANs / Tailscale.
📊 Homarr (LXC)
Homelab dashboard — service links and status. Reachable from mgmt VLAN directly or via Tailscale (LAN → mgmt blocked at the firewall). Pending: move to services VLAN.
📖 homelab-site (LXC, services VLAN)
Static nginx serving this very page (homelab_reorganization.html) at http://map.homelab for personal viewing. Tailscale-only inbound (LAN → services blocked). Private GitHub repo cloned via SSH using a read-only deploy key tunneled over HTTPS:443 (services VLAN doesn't allow :22). Manual git pull to update. If the LXC is ever compromised, blast radius is read access to this one repo — nothing else.
🤖 agent60 VM (Ubuntu, automation VLAN)
Isolated automation VM — internet-only egress, no internal access. Default user account. Hosts agent60 and the ngrok / webhook auto-deploy stack. Strong random password generated by and stored in a password manager — password-based SSH safe as a fallback alongside the ed25519 key.
🏷️ .homelab URL Rewrites
AdGuard rewrites these hostnames to internal IPs so memorable URLs work from any device on LAN, any VLAN, or Tailscale:
| Hostname | IP | Service |
|---|---|---|
| proxmox.homelab | 10.0.0.XX | Proxmox VE |
| opnsense.homelab | 10.0.10.1 | OPNsense admin |
| adguard.homelab | 10.0.10.XX | AdGuard Home |
| unifi.homelab | 10.0.10.XX | Unifi OS Server |
| homarr.homelab | 10.0.10.XX | Homarr dashboard |
| map.homelab | 10.0.20.XX | homelab-site LXC (this dashboard) |
| agent.homelab | 10.0.30.XX | agent60 VM (automation) |
📅 agent60
Three cron-scheduled scripts on the agent60 VM. VM runs UTC, so cron times are offset to match local time (UTC offset):
- Monday Brief · 0 10 * * 1 = 5:00 AM local · upcoming week's personal calendar + 7-day weather
- Daily Brief · 0 10 * * 2-7 = 5:00 AM local Tue–Sun · today's calendar + weather + outfit tip
- Friday Report · 0 23 * * 5 = 6:00 PM local · weekly wrap-up of online coding class notes transcribed in Notion
Each script runs Claude CLI headless (claude -p), queries MCP connectors (Notion / Google Calendar) plus Open-Meteo via WebFetch, fills an HTML email template, and sends via msmtp to SMTP provider (smtp.example.com:465, implicit TLS).
See the agent60 tab for the full pipeline and auto-deploy flow diagram.
🔐 GitHub SSH (on agent60 VM)
- SSH key ~/.ssh/id_ed25519 (ed25519, no passphrase) — added to a GitHub account
- Automation VLAN only allows port 443 outbound, not 22 — SSH routes via ssh.github.com:443 in ~/.ssh/config
- keychain installed — SSH agent auto-starts on login; silent git pull with no passphrase prompt
🪝 Auto-Deploy (GitHub Webhook + ngrok)
Two systemd services on the agent60 VM, both enabled and auto-starting on boot. Pulls latest code to /opt/agent60 on every push to main:
webhook.service
Node.js HTTP server at localhost:3000 (/opt/webhook/index.js). Verifies HMAC-SHA256 signature, then runs git -C /opt/agent60 pull. 5s execSync timeout. Per-IP 5s rate limit (429 on repeats). Logs to journald.
ngrok.service
Exposes localhost:3000 via a static ngrok-free.app domain over HTTPS. Config at ~/.config/ngrok/ngrok.yml (chmod 600). Outbound on port 443 — the only port allowed off VLAN 30.
GitHub webhook payload URL: https://<static-domain>.ngrok-free.app/webhook · content-type JSON · push events only · HMAC secret matches SECRET in index.js.
🔒 Security Hardening
Completed
- 2FA on Proxmox (TOTP) — Datacenter → Users → Edit
- 2FA on OPNsense (TOTP) — System → Access → Users
- Secret file permissions tightened on agent60 VM
/opt/agent60/.env chmod 600 /opt/agent60/.msmtprc chmod 600 ~/.config/ngrok/ngrok.yml chmod 600 /opt/webhook/index.js chmod 640
- Webhook execSync 5s timeout — prevents hanging git pull from blocking listener
- Webhook per-IP rate limiting — 5s cooldown, 429 on repeats
- agent60 LXC → VM migration — stronger kernel-level isolation; webhook + ngrok services carried over
- Dedicated SMTP provider account for agent60 — isolated from personal email
- VLAN segmentation — automation VLAN internet-only, no internal access
- Proxmox management off WAN — host only reachable via internal LAN (10.0.0.XX)
- Default deny on WAN + Tailscale — no inbound rules on either
- IPv6 disabled on agent60 VM — prevents msmtp routing issues on IPv4-only VLAN
- Strong randomized password for agent60 VM — password-manager-generated long random string on the default user account, stored in the same vault. Makes password-based SSH safe as a fallback alongside the ed25519 key.
- Read-only deploy key for homelab-site repo clone — ed25519 key generated on the LXC and added to the GitHub repo under Settings → Deploy keys with "Allow write access" unchecked. Repo-scoped (not tied to my user account), pull-only, easy to revoke independently. If the LXC is ever compromised the attacker only gets read access to this one repo — no push, no other repos, no account-wide blast radius.
- Proxmox host firewall enabled — Datacenter → Firewall turned on as a second layer beyond OPNsense, so the Proxmox host itself filters traffic regardless of what's happening inside the guest network — 2026-05-17.
Pending
- Tailscale ACLs — restrict which devices can reach which subnets
- Move Homarr to services VLAN (currently on mgmt)
📚 See Also
- Topology — interactive node map; click any node or VLAN chip for full detail + connections
- VLANs — per-VLAN cards with assigned devices and firewall rules at a glance
- agent60 — cron schedule + report and auto-deploy pipeline diagrams
- Troubleshooting — common issues and fixes (AppArmor, IPv6, cron PATH, etc.)