Partie 13: Sidecar d'un pod sur k3s
Février 2026 · 15 minutes de lecture · Niveau intermédiaire
Introduction
Certaines applications nécessitent que leur trafic réseau passe par un VPN :
- Accès à des APIs géo-restreintes
- Protection de l'IP source pour des raisons de confidentialité
- Conformité avec des politiques réseau d'entreprise
- Services de médias qui préfèrent une certaine... discrétion 😏
Dans Kubernetes, la solution élégante est le pattern sidecar : regrouper plusieurs containers dans un même pod pour qu'ils partagent le même namespace réseau. Ainsi, un container VPN peut servir de "passerelle" pour tous les autres.
Architecture cible
┌─────────────────────────────────────────────────────────────────────┐
│ vpn-pod │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Gluetun │ │ Service A │ │ Service B │ ... │
│ │ (VPN Client) │ │ (port 8080) │ │ (port 9000) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┴─────────────────┘ │
│ Namespace réseau partagé │
│ Interface: tun0 (VPN) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ VPN Provider │
│ (ProtonVPN, │
│ Mullvad...) │
└─────────────────┘
│
▼
Internet
(IP = IP du VPN)
Pourquoi cette architecture ?
Avantages :
- Un seul container gère la connexion VPN (Gluetun)
- Les autres containers n'ont aucune configuration VPN
- Tout le trafic sortant passe automatiquement par le tunnel
- Isolation : si le VPN tombe, le trafic est bloqué (kill switch)
Gluetun : Le couteau suisse du VPN dans Docker/K3s
Gluetun est un container qui supporte de nombreux fournisseurs VPN (ProtonVPN, Mullvad, NordVPN, etc.) et protocoles (OpenVPN, WireGuard).
Fonctionnalités clés :
- Kill switch intégré : Bloque le trafic si le VPN est déconnecté
- Firewall configurable : Autorise certains sous-réseaux locaux
- Port forwarding : Support NAT-PMP pour certains providers
- Health check : Endpoint
/v1/healthpour les probes K3s
Implémentation Helm
Structure du chart
helm-charts/vpn-services/
├── Chart.yaml
├── values.yaml
└── templates/
├── _helpers.tpl
├── deployment.yaml
├── service.yaml
└── sealed-secret.yaml
values.yaml
# Configuration globale
global:
timezone: Europe/Paris
# Namespace
namespace: secure-services
# ===================================
# GLUETUN - VPN Container
# ===================================
gluetun:
enabled: true
image:
repository: qmcgaw/gluetun
tag: latest
vpn:
provider: protonvpn # ou mullvad, nordvpn, etc.
type: wireguard
# Port forwarding (si supporté par le provider)
portForwarding:
enabled: true
# Autoriser le réseau local et les réseaux K3s
firewall:
outboundSubnets: "192.168.1.0/24,10.42.0.0/16,10.43.0.0/16"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
# ===================================
# SERVICE A - Exemple d'application
# ===================================
serviceA:
enabled: true
image:
repository: mon-image
tag: latest
port: 8080
# ===================================
# SERVICE B - Autre application
# ===================================
serviceB:
enabled: true
image:
repository: autre-image
tag: latest
port: 9000
deployment.yaml (le cœur du pattern)
apiVersion: apps/v1
kind: Deployment
metadata:
name: vpn-pod
namespace: {{ .Values.namespace }}
spec:
replicas: 1
selector:
matchLabels:
app: vpn-pod
template:
metadata:
labels:
app: vpn-pod
spec:
# ================================
# CONTAINER 1 : GLUETUN (VPN)
# ================================
containers:
- name: gluetun
image: qmcgaw/gluetun:latest
# Capabilities nécessaires pour créer le tunnel
securityContext:
capabilities:
add:
- NET_ADMIN
env:
- name: VPN_SERVICE_PROVIDER
value: "protonvpn"
- name: VPN_TYPE
value: "wireguard"
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: vpn-credentials
key: wireguard-private-key
- name: WIREGUARD_ADDRESSES
valueFrom:
secretKeyRef:
name: vpn-credentials
key: wireguard-addresses
- name: SERVER_COUNTRIES
value: "Switzerland"
- name: VPN_PORT_FORWARDING
value: "on"
- name: FIREWALL_OUTBOUND_SUBNETS
value: "192.168.1.0/24,10.42.0.0/16,10.43.0.0/16"
# IMPORTANT : Gluetun déclare TOUS les ports
# (les autres containers ne peuvent pas le faire)
ports:
- name: health
containerPort: 9999
- name: service-a
containerPort: 8080
- name: service-b
containerPort: 9000
# Health check
livenessProbe:
httpGet:
path: /v1/openvpn/status
port: 9999
initialDelaySeconds: 30
periodSeconds: 30
resources:
{{- toYaml .Values.gluetun.resources | nindent 12 }}
# ================================
# CONTAINER 2 : SERVICE A
# ================================
- name: service-a
image: "{{ .Values.serviceA.image.repository }}:{{ .Values.serviceA.image.tag }}"
# PAS de section "ports" ici !
# Les ports sont déclarés dans gluetun
env:
- name: TZ
value: "{{ .Values.global.timezone }}"
resources:
requests:
memory: "128Mi"
cpu: "50m"
# ================================
# CONTAINER 3 : SERVICE B
# ================================
- name: service-b
image: "{{ .Values.serviceB.image.repository }}:{{ .Values.serviceB.image.tag }}"
env:
- name: TZ
value: "{{ .Values.global.timezone }}"
resources:
requests:
memory: "128Mi"
cpu: "50m"
# Configuration DNS optimisée
dnsConfig:
options:
- name: ndots
value: "1"
Points critiques à retenir
- Seul Gluetun déclare les ports — Les autres containers écoutent, mais c'est Gluetun qui expose
- NET_ADMIN capability — Nécessaire pour créer l'interface
tun0 - dnsConfig ndots:1 — Évite les problèmes de résolution DNS (voir section troubleshooting)
Gestion des secrets avec SealedSecrets
Les credentials VPN ne doivent jamais être en clair dans Git.
# 1. Créer le secret (dry-run)
kubectl create secret generic vpn-credentials \
--from-literal=wireguard-private-key="CLE_PRIVEE_ICI" \
--from-literal=wireguard-addresses="10.x.x.x/32" \
--namespace=secure-services \
--dry-run=client -o yaml > /tmp/secret.yaml
# 2. Chiffrer avec kubeseal
kubeseal --format yaml < /tmp/secret.yaml > templates/sealed-secret.yaml
# 3. Supprimer le fichier non chiffré !
rm /tmp/secret.yaml
Problèmes rencontrés et solutions
1. DNS : Le piège du ndots:5
Symptôme : Les requêtes DNS vers des domaines externes échouent ou sont très lentes.
Cause : Kubernetes configure par défaut ndots:5, ce qui signifie que tout domaine avec moins de 5 points est d'abord recherché dans les search domains du cluster.
Requête: api.example.com
K3s essaie d'abord:
- api.example.com.secure-services.svc.cluster.local
- api.example.com.svc.cluster.local
- api.example.com.cluster.local
- api.example.com (enfin !)
Solution :
spec:
template:
spec:
dnsConfig:
options:
- name: ndots
value: "1"
2. Application qui utilise la mauvaise interface
Symptôme : L'application fonctionne, mais son trafic ne passe pas par le VPN.
Cause : Certaines applications (notamment celles utilisant libtorrent... pour du téléchargement légal bien sûr) se bindent par défaut sur eth0 au lieu de tun0.
Diagnostic :
# Vérifier l'IP publique vue par le container
kubectl exec -n secure-services deployment/vpn-pod -c gluetun -- wget -qO- https://ipinfo.io/ip
# Comparer avec l'IP vue par l'application
kubectl exec -n secure-services deployment/vpn-pod -c service-a -- wget -qO- https://ipinfo.io/ip
Si les IPs diffèrent, l'application n'utilise pas le tunnel.
Solution : Configurer l'application pour utiliser l'interface tun0 (via son fichier de config ou son API).
3. Ports firewall bloqués
Symptôme : Connexions timeout vers certains services externes.
Cause : Gluetun a un firewall intégré qui bloque par défaut le trafic non-VPN.
Solution : Configurer les ports de sortie autorisés :
env:
- name: FIREWALL_VPN_INPUT_PORTS
value: "12345" # Port entrant (si port forwarding)
- name: FIREWALL_OUTBOUND_SUBNETS
value: "192.168.1.0/24,10.42.0.0/16" # Réseaux locaux autorisés
4. Port forwarding dynamique
Certains VPN (ProtonVPN, AirVPN) supportent le port forwarding via NAT-PMP. Le port assigné est dynamique et peut changer.
Récupérer le port :
kubectl exec -n secure-services deployment/vpn-pod -c gluetun -- cat /tmp/gluetun/forwarded_port
Automatiser la configuration :
#!/bin/bash
PORT=$(kubectl exec -n secure-services deployment/vpn-pod -c gluetun -- cat /tmp/gluetun/forwarded_port)
echo "Port forwardé: $PORT"
# Configurer l'application avec ce port (via son API par exemple)
kubectl exec -n secure-services deployment/vpn-pod -c service-a -- \
curl -s -X POST "http://localhost:8080/api/settings" \
-d "json={\"listen_port\":$PORT}"
Commandes de diagnostic
# Vérifier l'état du VPN
kubectl exec -n secure-services deployment/vpn-pod -c gluetun -- wget -qO- https://ipinfo.io
# Logs Gluetun
kubectl logs -n secure-services deployment/vpn-pod -c gluetun -f
# Logs d'un service
kubectl logs -n secure-services deployment/vpn-pod -c service-a -f
# Vérifier les interfaces réseau
kubectl exec -n secure-services deployment/vpn-pod -c gluetun -- ip addr
# Test de connectivité depuis un service
kubectl exec -n secure-services deployment/vpn-pod -c service-a -- wget -qO- https://example.com
# Redémarrer le pod
kubectl rollout restart deployment/vpn-pod -n secure-services
Cas d'usage légitimes
Ce pattern est utile pour de nombreux scénarios :
- Agrégation de prix : Comparer les prix depuis différentes régions
- Tests de géolocalisation : Vérifier le comportement d'une app selon le pays
- Services P2P légitimes : Jeux en ligne, visioconférence, partage de fichiers autorisés
- Accès à des ressources d'entreprise : VPN corporate pour des services internes
- Gestion de bibliothèques multimédias personnelles : Pour les collectionneurs de Linux ISOs, évidemment
Conclusion
Le pattern sidecar avec Gluetun permet d'intégrer proprement un VPN dans Kubernetes :
✅ Séparation des responsabilités : Un container gère le VPN, les autres se concentrent sur leur métier
✅ Simplicité : Pas de configuration VPN dans chaque application
✅ Sécurité : Kill switch intégré, credentials gérés via SealedSecrets
✅ GitOps compatible : Tout est déclaratif et versionné
Ce pattern m'a permis de déployer plusieurs services nécessitant une connexion VPN sur mon cluster K3s Raspberry Pi, le tout géré automatiquement par ArgoCD.
Ressources
Cet article fait partie de ma série sur le déploiement d'un homelab K3s sur Raspberry Pi. Retrouvez les autres articles sur mon blog.