IKEv2 and WireGuard running in separate Kubernetes namespaces

Published: 2026-06-12

Both VPN protocols run inside the k0s cluster — not as bare processes on the host, but as Kubernetes pods. This is unusual (most guides run VPN directly on the host), but it makes secrets management and deployment consistent with everything else.


IKEv2 / strongSwan

04-setup-vpn.sh deploys hwdsl2/ipsec-vpn-server in the vpn namespace. This image wraps strongSwan and provides auto-configuration via environment variables.

Key deployment settings:

yamlspec:
  template:
    spec:
      hostNetwork: true
      containers:
        - name: vpn
          image: hwdsl2/ipsec-vpn-server
          securityContext:
            privileged: true
            capabilities:
              add: ["NET_ADMIN", "SYS_MODULE"]
          env:
            - name: VPN_IPSEC_PSK
              valueFrom:
                secretKeyRef:
                  name: vpn-secrets
                  key: VPN_IPSEC_PSK
            - name: VPN_USER
              valueFrom:
                secretKeyRef:
                  name: vpn-secrets
                  key: VPN_USER
            - name: VPN_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: vpn-secrets
                  key: VPN_PASSWORD
          volumeMounts:
            - name: vpn-data
              mountPath: /opt/vpn-data
      volumes:
        - name: vpn-data
          hostPath:
            path: /opt/vpn-data
            type: DirectoryOrCreate

Why hostNetwork: true: strongSwan must bind to the host's UDP 500 and 4500 ports for IKE negotiation. These ports must be reachable directly by clients — they can't go through a Kubernetes Service, because IKE uses the client's source IP as part of the handshake.

Why privileged: true: needed for ipsec kernel modules. This is the biggest security tradeoff: a privileged pod can escape to the host. The alternative — running strongSwan directly on the host — would require managing it outside Kubernetes, which breaks the operational model.

Generated client configs (.mobileconfig for Apple, .sswan for Android) are written to /opt/vpn-data by the init container and can be retrieved from there.


WireGuard

08-setup-wireguard.sh deploys linuxserver/wireguard in the wireguard namespace. Unlike strongSwan, WireGuard runs entirely in user space via the wireguard-go implementation in the container.

yamlspec:
  template:
    spec:
      hostNetwork: true
      containers:
        - name: wireguard
          image: linuxserver/wireguard
          securityContext:
            capabilities:
              add: ["NET_ADMIN", "SYS_MODULE"]
          env:
            - name: PEERS
              value: "phone,laptop,tablet"
            - name: SERVERURL
              value: "91.184.248.13"
            - name: SERVERPORT
              value: "51820"
            - name: PEERDNS
              value: "1.1.1.1"
            - name: INTERNAL_SUBNET
              value: "10.0.0.0/24"
          volumeMounts:
            - name: wg-data
              mountPath: /config
      volumes:
        - name: wg-data
          hostPath:
            path: /opt/wireguard-data

The PEERS variable generates a client config and QR code for each peer automatically. Retrieve configs:

bash# Get WireGuard config for laptop peer
kubectl exec -n wireguard deploy/wireguard -- cat /config/peer_laptop/peer_laptop.conf

# Get QR code for phone peer
kubectl exec -n wireguard deploy/wireguard -- cat /config/peer_phone/peer_phone.conf | qrencode -t ansiutf8

WireGuard uses UDP 51820. Since UDP traffic can't be load-balanced by kube-proxy's iptables rules, hostNetwork: true is also needed here.


Secrets management

VPN credentials are stored as SealedSecrets:

yamlapiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: vpn-secrets
  namespace: vpn
spec:
  encryptedData:
    VPN_IPSEC_PSK: AgB...
    VPN_USER: AgB...
    VPN_PASSWORD: AgB...

To rotate credentials:

  1. Update .env
  2. Re-seal: kubectl create secret generic vpn-secrets --from-env-file=.env --dry-run=client -o yaml | kubeseal ...
  3. Commit the new SealedSecret
  4. Flux reconciles and the pod restarts with new credentials

Firewall rules

Both VPN protocols require specific ports open:

bash# IKEv2
ufw allow 500/udp
ufw allow 4500/udp

# WireGuard
ufw allow 51820/udp

These are global allows (not restricted by IP) because VPN is meant for use from anywhere.


Why run VPN inside k8s at all?

Consideration Kubernetes pod Bare systemd service
Secret management Kubernetes Secrets (same as everything else) Config file on host
Auto-restart restartPolicy: Always Restart=always (can fail permanently)
Observability kubectl logs, pod metrics journalctl + custom exporter
Security Privileged pod (IKEv2) Process on host
Deployment kubectl apply + Flux SSH + systemctl

The real tradeoff is privileged: true on the IKEv2 pod. WireGuard is cleaner — only NET_ADMIN and SYS_MODULE, and on Linux 5.6+ the kernel module is available so SYS_MODULE isn't even needed. This could be tightened further with a custom seccomp profile.


Checking VPN status

bash# IKEv2 status
kubectl exec -n vpn deploy/ipsec-vpn-server -- ipsec status

# WireGuard status
kubectl exec -n wireguard deploy/wireguard -- wg show

# Pod logs
kubectl logs -n vpn deploy/ipsec-vpn-server
kubectl logs -n wireguard deploy/wireguard

What can go wrong

IKEv2 pod can't bind to UDP 500/4500. Another process on the host may already be listening on those ports. Check with ss -ulpn | grep -E "500|4500". The pod requires hostNetwork: true and the ports to be free.

WireGuard peer can't connect after pod restart. The linuxserver/wireguard image regenerates peer configs on startup if /config is empty. If the hostPath volume was deleted or the data directory changed, peers need new configs. Back up /opt/wireguard-data/ before any infrastructure changes.

privileged: true pod won't start. Pod Security Admission in restricted or baseline mode blocks privileged pods. The vpn namespace needs pod-security.kubernetes.io/enforce: privileged label, or the pod needs a specific exemption.


Summary

  • Both IKEv2 (strongSwan) and WireGuard run as Kubernetes pods with hostNetwork: true — necessary because VPN protocols require direct access to host-level UDP ports
  • IKEv2 needs privileged: true; WireGuard needs only NET_ADMIN + SYS_MODULE
  • VPN credentials are stored as SealedSecrets and rotated by re-sealing and committing a new manifest
  • WireGuard peer configs and QR codes are auto-generated from the PEERS env variable at startup
  • Firewall rules for UDP 500/4500 (IKEv2) and UDP 51820 (WireGuard) must be added to the host's ufw/iptables