OpenVPN-pod in Kubernetes stuurt niets door? Controleer ip_forward
Clients pingen de VPN-pod en niets verder. De namespace heeft doorsturen uit, en de oplossing moet admission, herstarts en de terugweg overleven.
Je laptop toont een groene tunnel, het clientlog meldt dat routes zijn gepusht, en het enige adres dat antwoordt is het tunnel-IP van de VPN-server zelf. Pings daarvoorbij sterven ongeacht waarop je richt, of dat een ClusterIP, het adres van een andere pod of een publieke hostnaam is. Die combinatie is diagnostisch en niet vaag: decapsulatie werkt, en de tweede hop niet.
Ik kwam dit tegen op een Debian-node met een via chart geïnstalleerde OpenVPN-image, en de tunnel was het makkelijke deel. De client kreeg een 10.8.0.x-lease, 10.8.0.1 antwoordde, en 10.96.0.10, de DNS-dienst van het cluster, liep in een timeout. Niets in het serverlog zag er ongezond uit, en dat is precies waarom mensen iptables-regels gaan herschrijven die ze nooit hadden hoeven aanraken.
Een VPN-pod is een router, en dat mag duidelijk gezegd worden. Versleutelde pakketten komen binnen, worden uitgepakt in de eigen netwerknamespace van de pod en moeten er dan via een andere interface uit. Linux weigert die tweede stap tenzij doorsturen binnen precies die namespace is ingeschakeld.
Wat de client wel en niet kan bereiken
Het adres dat antwoordt hoort bij het tunneleindpunt, en het VPN-proces bezit het lokaal, dus doorsturen is helemaal niet betrokken. Al het andere, inclusief een andere pod, een service-VIP of het internet, heeft de kernel nodig om een pakket van tun0 naar eth0 te verplaatsen. Wordt die verplaatsing geweigerd, dan is het symptoom gelijkmatige stilte en geen weigering, omdat niets een ICMP-fout de tunnel weer in stuurt. Je krijgt time-outs, geen onbereikbaar-meldingen, en dat is de aanwijzing dat niemand überhaupt routeert.
Twee minuten classificatie vernauwt de laag. Als DNS-namen falen terwijl ruwe pings naar een service-IP slagen, zit het probleem in de DNS-loodgieterij, niet in het doorsturen. Als elke bestemming voorbij het peer-adres in een timeout loopt, ook een kale IP, begin dan bij de namespace.
Lees de instelling binnen de namespace
Elke pod krijgt zijn eigen netwerknamespace, en net.ipv4.ip_forward is een sysctl per namespace. Die op de node wijzigen vertelt je niets over de container, en het is een betrouwbare manier om een middag op de verkeerde machine door te brengen.
kubectl exec -it deploy/openvpn -- sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0
Een nul bevestigt de diagnose. De waarde wordt ook niet geërfd van de runtime-configuratie van de node: hij begint bij de image-standaard telkens wanneer een container wordt gemaakt. Een handmatig met kubectl exec ... sysctl -w toegepaste oplossing verdwijnt daarom bij de volgende herstart en komt terug als een fout die sporadisch lijkt.
Inschakelen zodat het manifest het behoudt
De duurzame optie is een init-container die in dezelfde pod-sandbox draait en de waarde schrijft voordat de VPN-container start. Containers in een pod delen een netwerknamespace, dus de instelling gaat mee naar de hoofdcontainer.
initContainers:
- name: enable-forwarding
image: busybox:1.36
command: ["sysctl", "-w", "net.ipv4.ip_forward=1"]
securityContext:
privileged: true
Precies privileged: true is de regel waarop reviewers terugduwen, en terecht. Een sysctl schrijven vereist ofwel een privilege ofwel een nauw afgebakende capability, en een cluster met de beperkte Pod Security Standard kan een geprivilegieerde init-container botweg weigeren. Als de admission de pod weigert, weet je dat bij het aanmaken, wat tenminste eerlijk en makkelijk te lezen is.
Het schonere alternatief is de securityContext.sysctls-lijst op pod-niveau, waarmee de kubelet namespaced sysctls kan toepassen zonder enige container privileges te geven. Hij dekt alleen namen op de allowlist van het cluster. Of net.ipv4.ip_forward op die lijst staat, hangt af van hoe de kubelet is geconfigureerd, en een pod die een sysctl daarbuiten vraagt, wordt bij de admission geweigerd of blijft hangen in CreateContainerConfigError terwijl de sandbox opnieuw wordt geprobeerd. Controleer de allowlist voordat je aanneemt dat het veld stuk is.
Voordat je een van beide benaderingen met de hand bouwt, kijk of de chart de schakelaar al blootlegt, vaak als iets als ipForwardInitContainer: true. De chart-waarde gebruiken houdt je override-bestand klein en overleeft de volgende upgrade, wat handmatig gepatchte Deployment-wijzigingen niet doen.
De tweede helft: terugweg en beleid
Doorsturen aan is noodzakelijk, niet voldoende. Als clients een privépool gebruiken waarvoor het clusternetwerk geen route heeft, vinden antwoorden de weg terug de tunnel niet in, en de pod moet dat verkeer richting zijn bestemming maskeren.
Beleid ligt daar bovenop. NetworkPolicy-objecten en cloud-firewallregels worden geëvalueerd tegen het bronadres dat een pakket na vertaling draagt, dus een regel die voor de clientpool is geschreven kan stilzwijgend ophouden te passen zodra maskering is ingeschakeld.
Wanneer clients alleen diensten binnen het cluster hoeven te bereiken, is de VPN bij de service-VIP laten eindigen een kleiner ontwerp dan naar het podnetwerk routeren. Het houdt de clientpool ook volledig buiten de routingtabel van de CNI.
De falende hop van de client isoleren
Werk naar buiten en stop bij de eerste stille stap. Begin bij de VPN-peer, probeer dan het adres van een willekeurige pod, dan een service-VIP, en pas daarna een publieke bestemming. Elk succes verschuift de grens een hop, en elke fout benoemt de laag die het bekijken waard is. Na elke uitrol ga je naar binnen met exec en lees je de sysctl opnieuw, want een vervangen pod begint bij de image-standaard en een oplossing die nooit in het manifest stond is al weg.
De Kubernetes-documentatie over sysctls in een cluster gebruiken somt op welke namen via de beveiligingscontext van de pod kunnen worden gezet, en het is de eerste plek om te kijken wanneer de admission de wijziging weigert.