I put internal NTP on real hosts — not containers — because an unprivileged container can’t discipline the clock (it shares the host kernel’s), and a time server that can’t correct its own clock is just a repeater. Those hosts sit on the network-services VLAN so every other VLAN can reach them.
Then I noticed the trap, before it bit: those hosts are multi-homed, and that quietly breaks a time server.
The trap#
Take a host with a management interface on one VLAN and its NTP service on another, with its default route on the management side. A client on a third VLAN asks it for the time:
- The request is routed in and arrives on the NTP interface.
- The host builds the reply — source = the NTP address — and routes it by destination. The client’s subnet matches no connected route, so it takes the default route → out the management interface.
The reply comes in one door and leaves by another, carrying a source address that doesn’t belong to the interface it exited. That’s asymmetric routing, and it’s a real problem the moment you’re running a zone-based firewall: the reply now hits the gateway on the “wrong” interface with a source from another zone, and reverse-path filtering or the zone classifier can drop or mis-file it. Relying on “the firewall’s connection tracking will sort it out” is exactly the kind of hope you don’t want under a security-focused firewall.
The part that isn’t a problem (and why it matters)#
The same hosts also run VMs (my DNS boxes) bridged onto that service VLAN. It’s tempting to think those VMs inherit the host’s routing mess. They don’t: a bridged VM is its own L3 host with its own IP and its own default gateway set inside the guest. The bridge is pure layer 2 — the VM ARPs for the gateway and its frames go straight to it, regardless of what the host does. A single-homed VM is symmetric like any physical box.
That’s the whole distinction: the DNS service was never at risk (single-homed VMs), only NTP — because NTP has to run on the physical, multi-homed hosts to touch the clock.
The fix, in order of preference#
- Single-home where you can. Some of my time servers had no reason for a second interface — so I removed it. One interface = its own default route = symmetric by construction. Problem gone, zero config.
- Where you can’t, route by source. A host that genuinely needs two legs gets source-based policy routing: a routing table per interface plus a rule that says “traffic from this address leaves via this interface’s gateway.” Four lines on Linux. (Plus loose reverse-path filtering and a bit of ARP tuning so each leg only answers for its own subnet.)
Going over-opinionated: a management VRF#
For the hosts that stay multi-homed, I went a step past policy routing to a management
VRF — a hard layer-3 boundary. The management interface lives in its own routing domain
(vrf-mgmt); the service interface stays in the default one. It’s not just “replies leave
the right door” — it’s that a compromised service daemon literally cannot route to the
management network at all. Defence in depth, not merely correct routing.
The honest caveat there: it’s clean on plain Debian, but on a Proxmox host you’re binding its web UI (and, in a cluster, corosync) into the VRF, which fights the platform. Since my Proxmox nodes are standalone (no cluster), there’s no corosync to worry about and only the web UI needs the binding — so the VRF is feasible, with source-routing as the safe fallback.
What I’d tell past-me#
- Any server that answers cross-subnet requests on a non-default interface is a multi-homing bug waiting for a stateful firewall to expose it.
- The cleanest fix is usually to delete an interface, not add config.
- Hosting VMs does not make a host multi-homed — the guests route themselves. Keep the host’s own homing as simple as its job allows.