Hetzner Networking on Proxmox: A Per-Distro Guide

Share
Hetzner Networking on Proxmox: A Per-Distro Guide

Hetzner's dedicated server networking trips up everyone the first time. The gateway isn't in the same subnet as the IPs you're assigning. Additional IPs are routed individually as /32s, not as a contiguous on-link subnet. The upstream switch MAC-filters by default, so naïve bridged VM setups fail silently.

The fix is conceptually the same on every distro — a routed setup with on-link gateway directives — but every distro decided to do networking differently. This guide covers Proxmox host configuration and per-distro VM configuration for Ubuntu 22.04, Ubuntu 24.04, Debian 12, AlmaLinux 9, and CentOS 7.

Examples use documentation IP space (RFC 5737). Substitute your real values from Hetzner Robot:

  • Server primary IP: 192.0.2.10
  • Server gateway: 192.0.2.1
  • Additional IPs for VMs: 198.51.100.20 through 198.51.100.25

Why Hetzner is different

In a typical hosting setup, additional IPs come from the same subnet as the host's primary IP, and they share a gateway. On Hetzner:

  • Additional IPs are routed individually to your server as /32s, or as a small subnet (e.g., /29) routed via your server's primary IP — not directly attached.
  • The gateway address is the host's primary gateway, which is outside the additional IP block entirely.
  • The upstream switch performs MAC filtering. Frames with a MAC the switch wasn't told to expect are dropped at the port.

Two ways to deal with MAC filtering for VMs:

  1. Routed setup (what this guide covers). Proxmox host routes packets to VMs via host routes. VMs use on-link to reach the gateway through the host. The switch only ever sees the host's MAC. Works for any number of IPs, no extra Hetzner config needed.
  2. vMAC setup. Request a virtual MAC per additional IP from Hetzner Robot, assign it to the VM, and the switch will accept frames from that MAC. Allows fully bridged networking but adds per-IP operational overhead.

Routed is what most production deployments use. Everything below assumes routed.

Step 1: Proxmox host configuration

Tell the host that your additional IPs are reachable through vmbr0. This causes the kernel to proxy-ARP for them on the host side, which is what gets traffic from the upstream switch into the VMs.

nano /etc/network/interfaces

Add up route add -host lines for each additional IP under the vmbr0 block:

auto vmbr0
iface vmbr0 inet static
    address 192.0.2.10/26
    gateway 192.0.2.1
    bridge-ports eno1
    bridge-stp off
    bridge-fd 0
    # Hetzner additional IPs routed to VMs
    up route add -host 198.51.100.20 dev vmbr0
    up route add -host 198.51.100.21 dev vmbr0
    up route add -host 198.51.100.22 dev vmbr0
    up route add -host 198.51.100.23 dev vmbr0
    up route add -host 198.51.100.24 dev vmbr0
    up route add -host 198.51.100.25 dev vmbr0

For a routed subnet ordered from Hetzner (e.g., a /29), use a single network route instead of per-IP host routes:

    up route add -net 198.51.100.16/29 dev vmbr0

Apply without rebooting:

ifreload -a

Verify the routes are active:

ip route | grep vmbr0

Confirm IP forwarding is enabled:

sysctl net.ipv4.ip_forward

It should return 1. If not:

echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-ip-forward.conf
sysctl -p /etc/sysctl.d/99-ip-forward.conf

⚠️ Verify forwarding survives reboot. PVE enables it by default, but hardened systems or sysctl edits can disable it.

Step 2: Disable the Proxmox firewall on Hetzner VMs

⚠️ Uncheck "Firewall" on every VM network interface. PVE's firewall layer interacts badly with Hetzner's switch MAC enforcement and the routed setup. Symptoms range from intermittent packet loss to total outbound failure. Disable it at the VM NIC level and do firewalling inside the guest with nftables or firewalld.

UI path: VM → Hardware → Network Device → Edit → uncheck Firewall.

CLI:

qm set <vmid> --net0 model=virtio,bridge=vmbr0,firewall=0

Step 3: VM configuration per distro

Every example below assumes:

  • The VM's primary interface is ens18 (KVM/virtio default; sometimes enp6s18 depending on machine type)
  • The VM gets a single IP: 198.51.100.20/32
  • The gateway is 192.0.2.1 — the host's gateway, deliberately off-link from the VM's IP

Universal pattern: /32 address, off-link gateway, explicit on-link directive.

Ubuntu 22.04 — netplan

Edit netplan (filename varies; check ls /etc/netplan/):

nano /etc/netplan/50-cloud-init.yaml
network:
  version: 2
  ethernets:
    ens18:
      addresses:
        - 198.51.100.20/32
      nameservers:
        addresses:
          - 8.8.8.8
          - 1.1.1.1
        search: []
      routes:
        - to: 0.0.0.0/0
          via: 192.0.2.1
          on-link: true

Disable cloud-init network rewriting so it doesn't clobber this on reboot:

cat > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg <<EOF
network: {config: disabled}
EOF

Apply:

netplan apply

Ubuntu 24.04 — systemd-networkd (skip netplan)

Ubuntu 24's netplan has quirks with on-link routes on point-to-point /32 setups. Skip netplan entirely and write a systemd-networkd unit:

mv /etc/netplan/50-cloud-init.yaml /etc/netplan/50-cloud-init.yaml.disabled
nano /etc/systemd/network/10-ens18.network
[Match]
Name=ens18

[Network]
Address=198.51.100.20/32
DNS=8.8.8.8
DNS=1.1.1.1

[Route]
Gateway=192.0.2.1
GatewayOnLink=yes

Disable cloud-init network management as in the Ubuntu 22 section. Then:

systemctl enable --now systemd-networkd
networkctl reload

Verify with networkctl status ens18 — link state should be routable, route configured.

Debian 12 — systemd-networkd

Debian 12 ships with ifupdown by default. Switch to systemd-networkd for cleaner on-link handling:

systemctl stop networking
systemctl disable networking
nano /etc/systemd/network/10-ens18.network

For multiple IPs on the same interface (e.g., assigning a /29 routed subnet to one VM):

[Match]
Name=ens18

[Network]
Address=198.51.100.20/32
Address=198.51.100.21/32
Address=198.51.100.22/32
DNS=8.8.8.8
DNS=1.1.1.1

[Route]
Gateway=192.0.2.1
GatewayOnLink=yes
systemctl enable --now systemd-networkd
systemctl enable --now systemd-resolved
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

⚠️ Without systemd-resolved running, the DNS= entries in the .network file are ignored. Either enable resolved (recommended) or write /etc/resolv.conf manually.

AlmaLinux 9 / Rocky 9 — NetworkManager keyfile

NetworkManager stores per-connection config under /etc/NetworkManager/system-connections/. Edit the connection file (interface name on RHEL 9 family is usually enp6s18):

nano /etc/NetworkManager/system-connections/enp6s18.nmconnection

Replace the [ipv4] section with:

[ipv4]
method=manual
address1=198.51.100.20/32
gateway=192.0.2.1
dns=8.8.8.8;1.1.1.1;
route1=192.0.2.1/32,0.0.0.0,0
route1_options=onlink=true
route2=0.0.0.0/0,192.0.2.1,0
route2_options=onlink=true
ignore-auto-routes=true
ignore-auto-dns=true

⚠️ Keyfile permissions matter — NetworkManager refuses to load files with loose permissions:

chmod 600 /etc/NetworkManager/system-connections/enp6s18.nmconnection
chown root:root /etc/NetworkManager/system-connections/enp6s18.nmconnection

Apply:

nmcli connection reload
nmcli connection up enp6s18

Verify with nmcli connection show enp6s18 | grep -E 'IP4|ROUT'.

CentOS 7 — legacy network-scripts

CentOS 7 is EOL but still common in long-lived deployments. Two files to edit:

nano /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
BOOTPROTO=none
ONBOOT=yes
IPADDR=198.51.100.20
PREFIX=32
DNS1=8.8.8.8
DNS2=1.1.1.1
nano /etc/sysconfig/network-scripts/route-eth0
default 192.0.2.1 dev eth0 onlink

Apply:

systemctl restart network

⚠️ CentOS 7 reached EOL in June 2024. No more upstream security updates. New deployments should land on AlmaLinux 9 or Rocky 9. Plan migration timelines for any CentOS 7 VMs you're still maintaining.

Verification

Run from inside the VM after configuration, regardless of distro:

ip addr show              # confirm IP is assigned to the right interface
ip route                  # confirm default route via gateway, onlink
ping -c 3 192.0.2.1       # gateway reachability
ping -c 3 1.1.1.1         # internet connectivity
dig +short google.com     # DNS resolution

From outside (your workstation):

ping -c 3 198.51.100.20
ssh root@198.51.100.20

Troubleshooting

VM has correct config but no connectivity

Most common cause: the VM's tap interface isn't attached to vmbr0 on the host. Happens after migrations, manual qm commands, or PVE firewall toggles.

On the host:

brctl show vmbr0
# or with iproute2:
bridge link show | grep vmbr0

Look for tap<VMID>i0. If missing:

ip link set tap<VMID>i0 master vmbr0

Replace <VMID> with the actual VM ID. For a permanent fix, restart the VM cleanly through PVE — the issue usually means PVE thinks the NIC is detached.

Gateway unreachable from VM

ping <gateway> fails from VM, host can reach gateway fine. Walk the chain:

  1. Host route present? ip route | grep 198.51.100.20 on the host.
  2. IP forwarding on? sysctl net.ipv4.ip_forward returns 1.
  3. VM tap on bridge? brctl show vmbr0.
  4. PVE firewall disabled on the VM NIC? Check VM hardware tab.
  5. Host iptables FORWARD chain? iptables -L FORWARD -n -v — look for DROP rules. Common after CSF/UFW configuration on the host.
  6. Inside the VM, is on-link actually applied? ip route show should show the gateway with onlink flag.

DNS resolves nothing

The DNS client isn't honoring your config:

  • systemd-networkd hosts: confirm systemd-resolved is active and /etc/resolv.conf symlinks to /run/systemd/resolve/stub-resolv.conf.
  • NetworkManager hosts: nmcli dev show <iface> | grep DNS should list your servers.
  • Anyone: cat /etc/resolv.conf. If it lists nothing or stale entries, fix the resolver before debugging anything else.

Intermittent packet loss

Almost always one of:

  • PVE firewall enabled on the VM NIC (re-check, it sometimes re-enables after PVE upgrades)
  • A second IP on the VM that wasn't routed on the host (VM tries to source-route via the wrong interface)
  • vMAC conflict — if you partially enabled vMAC mode in Robot but didn't apply the MAC to the VM

A note on IPv6

Hetzner provides a /64 IPv6 subnet with every server. IPv6 setup is much simpler than IPv4 — no on-link gymnastics, the gateway is the standard link-local fe80::1. Worth setting up alongside IPv4 since dual-stack is essentially free here.

For a VM with IPv6 2001:db8:abcd::20:

[Network]
Address=2001:db8:abcd::20/64

[Route]
Gateway=fe80::1

The host needs net.ipv6.conf.all.forwarding=1 and proxy-ndp configured for the VM's address, or a route directive equivalent to the IPv4 host routes above.

Summary

Distro Network tool Config path
Ubuntu 22.04 netplan /etc/netplan/50-cloud-init.yaml
Ubuntu 24.04 systemd-networkd /etc/systemd/network/10-ens18.network
Debian 12 systemd-networkd /etc/systemd/network/10-ens18.network
AlmaLinux 9 NetworkManager keyfile /etc/NetworkManager/system-connections/enp6s18.nmconnection
CentOS 7 network-scripts (legacy) /etc/sysconfig/network-scripts/{ifcfg,route}-eth0

Host-side configuration is identical across distros. Only the in-guest tooling differs. The pattern — /32 address + off-link gateway + explicit on-link — is what makes Hetzner's routing model work, and it's the one detail every Hetzner setup tutorial needs to get right.