Skip to content

WireGuard Mesh

Topology

Full mesh with the bastion as the central hub. All peers connect to the bastion; Kubernetes nodes and Proxmox hosts are spokes.

mermaid
graph TB
    subgraph Bastion["Bastion (OVH VPS)"]
        WG0_B[wg0 - Hub]
    end

    subgraph K8s["Kubernetes Nodes"]
        CP[Control Plane]
        W1[Worker 1]
        W2[Worker 2]
    end

    subgraph Proxmox["Proxmox Hosts"]
        ED[EliteDesk]
        TC[ThinkCentre]
    end

    subgraph LXCs["LXCs (behind NAT)"]
        CT101[SigNoz - 10.0.0.201]
        CT100[OneUptime - 10.0.0.51]
    end

    WG0_B --- CP
    WG0_B --- W1
    WG0_B --- W2
    WG0_B --- ED
    WG0_B --- TC
    ED -->|NAT forward| CT101
    TC -->|NAT forward| CT100

Peer Configuration

PeerWireGuard IPPSKNotes
bastion10.10.0.1Hub node
talos-cp10.10.1.1NoTalos v1.13 does not support PresharedKey
talos-w110.10.1.2NoTalos v1.13 does not support PresharedKey
talos-w210.10.1.3NoTalos v1.13 does not support PresharedKey
EliteDesk10.10.0.150YesNAT to SigNoz LXC 10.0.0.201
ThinkCentre10.10.0.50YesNAT to OneUptime LXC 10.0.0.51
Mac10.10.0.10Client
ER706W router10.10.0.20Home site-to-site

Proxmox Host Forwarding

Each Proxmox host forwards traffic to its LXCs via:

  1. IP forwarding enabled in sysctl (managed by the common Ansible role)
  2. nftables MASQUERADE rule for WireGuard-to-LXC traffic

NAT rules are now persistent via nftables (not iptables). The bastion and Proxmox hosts use a table ip nat with MASQUERADE for traffic from the WireGuard interface to LXC destinations. This is configured via the firewall_nat_destinations Ansible variable.

nft
# Example nftables NAT rule (bastion/Proxmox hosts)
table ip nat {
  chain postrouting {
    type nat hook postrouting priority 100; policy accept;
    oifname "wg0" ip daddr 10.0.0.0/24 masquerade
  }
}

INFO

The WireGuard template no longer includes redundant sysctl PostUp commands -- IP forwarding is handled by the common Ansible role.

Bastion Routing

The bastion uses PostUp routes to reach LXC IPs through the respective Proxmox host WireGuard addresses:

ini
[Peer]  # EliteDesk
AllowedIPs = 10.10.0.150/32, 10.0.0.201/32

[Peer]  # ThinkCentre
AllowedIPs = 10.10.0.50/32, 10.0.0.51/32

MetalLB VIP Routing

MetalLB L2 mode assigns VIP 10.10.1.200 to the Traefik LoadBalancer service. However, MetalLB L2 uses ARP announcements which do not cross WireGuard tunnels. The bastion must have the VIP in its WireGuard AllowedIPs for the node that owns the VIP.

ini
[Peer]  # K8s node holding MetalLB VIP
AllowedIPs = 10.10.1.1/32, 10.10.1.200/32

The bastion Ansible variable k8s_traefik_upstream controls which IP Caddy uses as the Traefik upstream. With MetalLB, this is set to 10.10.1.200 (the VIP) instead of individual node IPs.

WARNING

If MetalLB moves the VIP to a different node (e.g., after a node failure), update the AllowedIPs in the WireGuard config for the new node peer. This is managed via inventories/production/hosts.yml.

OTel Relay Pattern

K8s nodes have NO route to 10.0.0.x (home LAN). Telemetry from the K8s OTel DaemonSet is sent to the bastion relay (10.10.0.1:4317), which forwards to SigNoz at 10.0.0.201:4317.

K8s DaemonSet → 10.10.0.1:4317 (bastion relay) → 10.0.0.201:4317 (SigNoz)

Ansible Management

Two Jinja2 templates handle WireGuard configuration:

TemplateTarget
wg0.conf.j2Bastion — generates full peer list
wg0-proxmox.conf.j2Proxmox hosts — generates bastion peer + NAT rules

WARNING

Running the Ansible WireGuard playbook removes any peers not defined in the inventory. Always ensure new peers are added to the inventory before running the playbook.

Quinza Infrastructure