As homelabs grow from single-board experiment platforms into multi-node hypervisor clusters, managing host identity, network routing, and security isolation manually becomes a bottleneck.
In this post, I detail how I refactored my homelab infrastructure: moving from a flat network broadcast domain to a Zero-Trust VLAN architecture, while enforcing declarative Infrastructure-as-Code (IaC) using Ansible across a 3-node Proxmox VE hypervisor cluster and guest workloads.
🏗️ The Infrastructure Blueprint#
The core compute engine is a 3-node Proxmox VE cluster named squad, hosting a mix of Debian QEMU Virtual Machines and unprivileged LXC containers:
- Baremetal Hypervisors (
proxmox_cluster):lansen(PVE Node 1),draken(PVE Node 2),viggen(PVE Node 3). - Workload VMs (
vms):docker(Primary Traefik reverse proxy & service host),n8n(Workflow automation engine). - LXC Containers (
lxcs):ollama(Dedicated AI LLM inference host).
🌐 Network & VLAN Topology Architecture#
To eliminate lateral attack paths between IoT gadgets, personal devices, and core servers, the network is segmented into functional VLANs managed by a UniFi Firewall/Gateway:
+----------------------------------+
| Gateway / Firewall |
| 10.0.1.1 |
+----------------+-----------------+
|
+-------------------+---------------+-------------------+-------------------+
| | | |
▼ ▼ ▼ ▼
+--------------+ +---------------+ +---------------+ +---------------+
| VLAN 20 | | VLAN 30 | | VLAN 40 | | Tailscale |
| TRUSTED | | IOT | | GUEST | | VPN Mesh |
| 10.0.20.0/24 | | 10.0.30.0/24 | | 10.0.40.0/24 | | Subnet Router |
+------+-------+ +-------+-------+ +-------+-------+ +-------+-------+
| | | |
(Full Access) (Internet Only) (Zero Access) (Admin Access)
| | | |
+-------------------+---------------+-------------------+-------------------+
|
+---------------------------------------+---------------------------------------+
| Proxmox VE Cluster ("squad") |
| |
| [VLAN 10: MGMT (10.0.10.0/24)] [VLAN 14: SERVERS (10.0.14.0/24)] |
+-----------------------+-----------------------+-------------------------------+
| | | |
| +-----------------+ | +-----------------+ | +-------------------------+ |
| | lansen (PVE1) | | | draken (PVE2) | | | viggen (PVE3) | |
| | 10.0.10.10 | | | 10.0.10.11 | | | 10.0.10.12 | |
| +--------+--------+ | +--------+--------+ | +------------+------------+ |
| | | | | | |
| (Trunk | vmbr0) | (Trunk | vmbr0) | (Trunk | vmbr0) |
| | | | | | |
| | (QEMU) | | (LXC) | | (QEMU) |
| +--------v--------+ | +--------v--------+ | +------------v------------+ |
| | docker | | | ollama | | | n8n | |
| | VM | | | Container | | | VM | |
| | 10.0.14.20 | | | 10.0.14.22 | | | 10.0.14.21 | |
| | (VLAN 14 tag) | | | (VLAN 14 tag) | | | (VLAN 14 tag) | |
| +-----------------+ | +-----------------+ | +-------------------------+ |
| | | |
+-----------------------+-----------------------+-------------------------------+🔒 Zero-Trust VLAN Segmentation Matrix#
| VLAN ID | Subnet | Workloads | Access Control & Isolation Rules |
|---|---|---|---|
| VLAN 10 (MGMT) | 10.0.10.0/24 | Proxmox hypervisors, Switch MGMT, IPMI | Strictly Isolated: Blocked from all subnets. Accessible only via Tailscale or Admin Workstations. |
| VLAN 14 (SERVERS) | 10.0.14.0/24 | docker VM, n8n VM, ollama LXC, NAS | Core Services: Inter-server traffic permitted; web ingress routed via Traefik reverse proxy. |
| VLAN 20 (TRUSTED) | 10.0.20.0/24 | Admin PCs, trusted laptops, personal phones | Full Access: Outbound internet permitted; full access to SERVERS and MGMT. |
| VLAN 30 (IOT) | 10.0.30.0/24 | Smart TVs, IP cameras, Home Assistant plugs | Restricted: Blocked from initiating connections to MGMT & SERVERS. Outbound WAN only. |
| VLAN 40 (GUEST) | 10.0.40.0/24 | Transient visitor devices | Zero Access: Client isolation enabled. Blocked from all RFC1918 private subnets. |
⚙️ Declarative Host Identification via Ansible#
To ensure uniform host identity, I built an Ansible inventory structure separating baremetal hypervisors from guest workloads, enforcing clean, single-word hostnames (lansen, draken, viggen, docker, n8n, ollama).
TIL: RFC 1123 Hostname Conventions
I was today years old when I learned about the official Internet hostname standards defined in RFC 1123 (Section 2.1) (updating the legacy RFC 952 specifications). Adhering to RFC 1123 conventions—using strictly lowercase alphanumeric characters and hyphens while avoiding underscores or special characters—ensures seamless compatibility across DNS resolvers, systemd-hostnamed, and Ansible inventory parsing!
1. Hierarchical Inventory Layout (ansible/inventory.yaml)#
all:
vars:
ansible_python_interpreter: auto_silent
homelab_domain: "example.internal"
children:
# Baremetal Proxmox VE Cluster
proxmox_cluster:
vars:
pve_cluster_name: squad
ansible_user: root
hosts:
lansen:
ansible_host: 10.0.10.10
draken:
ansible_host: 10.0.10.11
viggen:
ansible_host: 10.0.10.12
# Virtual Machines
vms:
vars:
ansible_user: zorg-agent
guest_type: vm
hosts:
docker:
ansible_host: 10.0.14.20
pve_parent: lansen
n8n:
ansible_host: 10.0.14.21
pve_parent: viggen
# LXC Containers
lxcs:
vars:
ansible_user: zorg-agent
guest_type: lxc
hosts:
ollama:
ansible_host: 10.0.14.22
pve_parent: draken2. Dynamic Jinja2 /etc/hosts Templating (hosts.j2)#
Debian guests require 127.0.1.1 loopback aliases for zero-latency local IPC, whereas physical Proxmox hypervisors require binding their FQDN directly to their LAN IP address (10.0.10.x):
# Managed by Ansible - Do NOT edit manually
127.0.0.1 localhost
{% if 'proxmox_cluster' in group_names %}
# Physical Hypervisor Setup
{{ ansible_default_ipv4.address }} {{ system_hostname }}.{{ homelab_domain }} {{ system_hostname }}
{% else %}
# Guest VM / LXC Setup
127.0.1.1 {{ system_hostname }}.{{ homelab_domain }} {{ system_hostname }}
{% endif %}
# IPv6 default local entries
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters🛡️ Traefik Reverse Proxy & SSL Termination#
External web traffic routes through Traefik v3 hosted on the docker VM (10.0.14.20). Traefik handles TLS termination using Let’s Encrypt / Cloudflare DNS-01 challenges and proxies internal web UIs seamlessly.
For example, Proxmox hypervisor UI traffic is routed via a dynamic Traefik configuration (proxmox.yaml):
http:
routers:
proxmox-ui:
rule: "Host(`proxmox.example.com`)"
entryPoints:
- "websecure"
service: "proxmox-ui-service"
tls:
certResolver: "cloudflare"
services:
proxmox-ui-service:
loadBalancer:
servers:
- url: "https://10.0.10.10:8006" # Target PVE node IP
serversTransport: "pve-transport"
serversTransports:
pve-transport:
insecureSkipVerify: trueBecause Traefik routes by explicit backend IP (https://10.0.10.10:8006) and handles SSL termination independently, updating internal OS hostnames or /etc/hosts resolution introduces zero breaking changes to web ingress or SSL certificates.
🚀 Key Takeaways & Lessons Learned#
- Decouple Host Identity from Network Topology: Standardizing single-word hostnames across Ansible inventory files eliminates ambiguity in logs and metrics dashboards.
- Segment Before Scaling: Isolating IoT hardware and guest devices into dedicated VLANs (
VLAN 30&VLAN 40) drastically reduces the blast radius in case of device compromise. - Always Dry-Run (
--check --diff): Running dry runs before executing state changes ensures zero unexpected downtime across critical services.
Questions or suggestions about homelab networking and Ansible automation? Feel free to reach out via GitHub or Twitter!


