Skip to main content

Standardizing Homelab IaC: Ansible Hostname Automation & Zero-Trust VLAN Architecture

Creeping depths of the deep
Author
Creeping depths of the deep
Architecting resilient infrastructure, autonomous AI workflows, and self-hosted tools.

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 IDSubnetWorkloadsAccess Control & Isolation Rules
VLAN 10 (MGMT)10.0.10.0/24Proxmox hypervisors, Switch MGMT, IPMIStrictly Isolated: Blocked from all subnets. Accessible only via Tailscale or Admin Workstations.
VLAN 14 (SERVERS)10.0.14.0/24docker VM, n8n VM, ollama LXC, NASCore Services: Inter-server traffic permitted; web ingress routed via Traefik reverse proxy.
VLAN 20 (TRUSTED)10.0.20.0/24Admin PCs, trusted laptops, personal phonesFull Access: Outbound internet permitted; full access to SERVERS and MGMT.
VLAN 30 (IOT)10.0.30.0/24Smart TVs, IP cameras, Home Assistant plugsRestricted: Blocked from initiating connections to MGMT & SERVERS. Outbound WAN only.
VLAN 40 (GUEST)10.0.40.0/24Transient visitor devicesZero 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: draken

2. 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: true

Because 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
#

  1. Decouple Host Identity from Network Topology: Standardizing single-word hostnames across Ansible inventory files eliminates ambiguity in logs and metrics dashboards.
  2. 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.
  3. 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!