Skip to content

WireGuard gateways

Pre-release, v0.3.0 — not yet e2e-validated

The wireguard-protocol path has been proven against the reconciler/ render unit and envtest suites and the kernel prerequisite below has been live-verified on vpn01's worker nodes — but the full apply-loop has not yet run a live end-to-end tunnel validation pass. See the index page and Known Limitations.

VPNGateway{spec.protocol: wireguard} runs a WireGuard gateway pod instead of a strongSwan one — same unified VPNGateway/VPNPeer model, same exposure/authorization semantics, different dataplane. This page covers what's specific to the wireguard path: the kernel prerequisite, security-group differences, the absence of a metrics exporter, road-warrior addressing, and key handling.

Kernel prerequisite: the wireguard kernel module

WireGuard needs kernel support (CONFIG_WIREGUARD) on the node the gateway pod's NET_ADMIN/NET_RAW container runs on — it's not something a container image alone can provide. Verified present on Garden Linux 6.12 (vpn01's worker nodes, kernel 6.12.87): wireguard.ko ships as CONFIG_WIREGUARD=m with the rtnl-link-wireguard alias, which means it auto-loads the moment something runs ip link add type wireguard — the wireguard-apply container's own interface-creation step triggers the load; no shoot/node-pool change or manual modprobe is required on b'nerd's own estate. If you're running gateway nodes outside that verified environment, confirm the module is available (modinfo wireguard on the node, or check for CONFIG_WIREGUARD in the running kernel's config) before onboarding a wireguard-protocol gateway there — this operator cannot detect or work around a missing module; the interface creation simply fails.

Security-group semantics differ by peer shape

vpnSecurityGroupRules (internal/controller/vpngateway_controller.go) opens the gateway's spec.wireguard.listenPort (UDP) differently depending on what kind of peer is asking for it:

  • A site peer (one with wireguard.subnets set, i.e. a fixed remote network behind a static endpoint) gets its ingress rule scoped to its own endpoint host's /32 — same tight scoping as the ipsec path's per-peer IKE rules, never 0.0.0.0/0.
  • A road-warrior peer (one with wireguard.address set instead of subnets — no fixed subnet, no fixed source) gets the listen port opened to 0.0.0.0/0, because a roaming client has no stable source address to scope the rule to. This widening applies to the whole gateway's listen port the moment even one road-warrior peer exists — document this explicitly to anyone reviewing the gateway's security posture: one road-warrior peer on an otherwise all-site-peer gateway widens ingress for everyone, not just that one peer, because the SG rule is per-port, not per-peer-and-port.
  • A site peer whose endpoint is a hostname rather than a literal IPv4 address also falls through to the open 0.0.0.0/0 rule — a hostname can't be pinned to a /32 without a DNS lookup the SG-rule builder doesn't perform. This is the same low-risk posture as the road-warrior open rule (WireGuard's real gate is public-key crypto, not source scoping), and it's surfaced on the gateway's SecurityGroupScoped condition: False/HostnameEndpointOpensPort, naming the affected peer(s), when any site peer's endpoint opened the port this way; True/AllSourcesPinned when every peer's handshake source is either a pinned /32 or an intentional road-warrior open. Pin the peer to a literal IP endpoint to get the port scoped again.

No Prometheus exporter for WireGuard gateways

The ipsec-protocol gateway pod runs an ipsec-exporter sidecar (VICI → /metrics). WireGuard gateways have no equivalent exporter in v0.3.0 — WireGuard has no VICI-style RPC socket to scrape, and no exporter container is built into the wireguard container set (wireguard-apply, init-nat, status-agent; no fourth container). If you're used to alerting on the ipsec gateway's Prometheus metrics, that path does not exist yet for a wireguard gateway.

Monitoring a WireGuard gateway in v0.3.0 is via status.peers[] on the VPNGateway instead — the status-agent container (--protocol wireguard) reads wg show state and patches each peer's lastHandshake/bytesIn/bytesOut (and state) onto the gateway's status subresource, the same per-peer status shape a VPNPeer itself also carries:

kubectl get vpngateway acme-gw -o jsonpath='{.status.peers}' | jq .

A Prometheus exporter for WireGuard gateways is a known gap, not a design decision — see Known Limitations.

Road-warrior addressing

Road-warrior peers (those with wireguard.address set, subnets empty) get their tunnel /32 carved from the top /26 of the gateway's tenant /24 serviceCIDR — e.g. for 100.64.7.0/24, the road-warrior range is 100.64.7.192/26. Inside that range, the gateway's own wg0 interface takes the network address's first usable host (.193), and road-warrior peers are assigned from .194 upward. This default is configurable per gateway via spec.wireguard.roadWarriorCIDR — when pinned, it must be a canonical (no host bits set) IPv4 CIDR that sits inside the gateway's serviceCIDR (Validate() rejects an override outside that range).

A peer's wireguard.address field accepts either a literal IPv4 address inside the road-warrior range, or "auto" to let the allocator assign the next free one.

Key handling

  • The gateway's WireGuard private key lives in a Kubernetes Secret — either one you reference via spec.wireguard.privateKeySecretRef, or (when unset) one the operator generates itself, named <gateway-name> -wg-key. The key is mounted read-only into the pod and assembled into /run/wg/wg0.conf by the wireguard-apply container's apply script — that /run/wg mount is memory-backed (emptyDir{medium: Memory}), never node disk, so the assembled config (private key included) never touches the node's filesystem. The key never appears in container argv or logs — the apply script assembles the config via file redirection, not by interpolating the key into a shell command line.
  • The gateway's public key is derived from that private key and surfaced on status.wireguardPublicKey — safe to read and hand to a peer out-of-band, since a WireGuard public key carries no secrecy requirement by design.
  • Peer public keys (VPNPeer.spec.wireguard.publicKey) are likewise public by nature and live directly in the CR — no Secret indirection needed or provided for them.
  • A per-peer pre-shared key (wireguard.presharedKeySecretRef) is optional, additive defense-in-depth on top of the Curve25519 key exchange (standard WireGuard PSK semantics) — when set, it's mounted the same way a peer's PSK is on the ipsec side, one Secret volume per peer.