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/health pour 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

  1. Seul Gluetun déclare les ports — Les autres containers écoutent, mais c'est Gluetun qui expose
  2. NET_ADMIN capability — Nécessaire pour créer l'interface tun0
  3. 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.

Read more