Guillaume Fenollar DevOps et SysAdmin Freelance

Guillaume FENOLLAR

Ingénieur Linux/DevOps Indépendant

− Montpellier −

Le VPN Wireguard dans un cluster Kubernetes

Avec l'adoption de Wireguard dans le noyau Linux 5.6, le monde entier (et moi y compris) s'est un peu plus intéressé à ce petit outsider dans le domaine des VPN on-premise. L'occasion parfaite de remplacer mon OpenVPN un peu surdimensionné pour mon usage perso (et vieillissant disons-le) en l'installant dans mon cluster Kubernetes. Voici comment rapidement mettre en place cette solution.

Wireguard est un “serveur” VPN léger et simpliste (du moins en comparaison de ses compétiteurs). Pour ceux qui arriveraient là par erreur, sachez que ce n’est pas un service VPN à la demande tel que NordVPN ou autre, c’est à vous de l’installer et de le maintenir vous-même sur un serveur ou un cloud, mais cela vous donne l’avantage de contrôler par où passent vos paquets.

En fait, on ne peut pas le considérer comme un serveur au sens service du terme au même titre que OpenVPN par exemple, parce que son code s’exécute au sein du noyau Linux (kernelspace), pour le moment par le biais d’un module en attendant son intronisation. L’utilisateur communique avec lui grâce à une cli simple en userspace qui se limite à l’essentiel.

Je vais aller au plus simple et décrire basiquement le mode de fonctionnement d’une architecture client/serveur simple, où chaque partie fait tourner Wireguard de la même façon, la configuration seulement faisant la différence entre un client simple (ne forwardant aucun paquet) et un serveur (NATant toute requête en provenance des clients, vers l’extérieur).

Tout d’abord, il faut savoir que Wireguard utilise une authentification à chiffrement asymétrique, vous allez donc devoir générer des sets de clés sur chaque peer, que ce soit un client ou un serveur. Créez les vôtres pour chaque peer avec les indications à la page en lien (doc officielle).

Mise en place du serveur

J’utilise là l’image la plus populaire d’un système packagé avec Wireguard, cmulk/wireguard-docker. Il s’agit d’un système Debian, et comme Wireguard s’exécute au sein du kernel, ça ne fonctionnera que si vous utilisez le même. Le processus d’installation du module utilise DKMS pour le construire dynamiquement avec le noyau trouvé sur la machine hôte, puis l’installe dans le dossier /lib/modules monté au sein de conteneur. Au sein de Kubernetes, un initContainer sera utilisé pour accomplir cette tâche. Vous pouvez utiliser une autre image déjà disponible pour coller à votre distribution/kernel, où la générer vous même si le coeur vous dit (vous pourriez même apprendre des choses ;).

Commençons par générer la configuration serveur que vous placerez dans un Secret Kubernetes, et qui sera du nom de votre future interface réseau avec le suffixe .conf, par exemple ici wg.conf:

cat > /tmp/wg.conf << EOF
[Interface]
Address = 10.66.0.1/24
PrivateKey = sGDvYx+OX7fuALQVUt0TuhnJzVSscNfYsVouQR/HU1U=
ListenPort = 5555
# publickey: fa4YCJNIlhBEStWMSl8usvFp41lKTC7O/Tu6abvYDQ4=

[Peer]
PublicKey = A7rD6b+WyL0dG8TDaCqWZyjgQiegnmX5l/vHobv2LT8=
AllowedIPs = 10.66.0.2/32
EOF
kubectl create secret generic wireguard --from-file=/tmp/wg.conf

N’utilisez, évidemment pas les valeurs de PrivateKey et PublicKey que j’ai générés ici pour l’occasion, c’est le moment d’utiliser celles que vous avez créé à l’instant.

Le sous-réseau du canal de communication créé entre les deux hôtes utilise ici comme réseau 10.66.0.0/24, et comme adresses d’hôtes .1 sur le serveur, et .2 sur le client, changez également ça à votre guise. Dans le cas où votre client (ici la partie Peer) est un simple client qui ne forward aucun paquet ni ne gère aucun réseau lui-même, il est très important de mettre sa future adresse avec un masque en /32, et non pas le sous-réseau l’englobant. AllowedIPs ne nous indique pas les IPs autorisées sur cet hôte, mais sur quels IPs il va être maître, c’est une erreur que j’ai faite et que m’a fait perdre pas mal de temps.

Vous pouvez alors créer le reste du déploiement.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: wireguard
  name: wireguard
spec:
  selector:
    matchLabels:
      app: wireguard
  strategy:
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: wireguard
      name: wireguard
    spec:
      initContainers:
      - name: install-module
        image: cmulk/wireguard-docker:buster
        args:
        - install-module
        volumeMounts:
        - mountPath: /lib/modules/
          name: modules
        securityContext:
          capabilities:
            add:
            - SYS_MODULE
      containers:
      - image: cmulk/wireguard-docker:buster
        name: wireguard
        securityContext:
          capabilities:
            add:
            - SYS_MODULE
            - NET_ADMIN
        volumeMounts:
        - mountPath: /etc/wireguard
          name: conf
      volumes:
      - name: conf
        secret:
          secretName: wireguard
          defaultMode: 0400
      - name: modules
        hostPath:
          path: /lib/modules
          type: "Directory"
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: wireguard
  name: wireguard
spec:
  externalTrafficPolicy: Cluster
  ports:
  - nodePort: 32555
    port: 5555
    protocol: UDP
    targetPort: 5555
  selector:
    app: wireguard
  type: LoadBalancer

L’initContainer devrait compiler et installer le module sur la machine hôte. C’est de loin l’opération qui prend le plus de temps lors de ce déploiement, il serait alors plus judicieux de faire tourner cette opération une seule fois via un DaemonSet, c’est à vous de choisir la méthode qui vous sied le mieux. Enfin, le container principal activera les routes et règles iptables nécessaires au bon fonctionnement de Wireguard.

Mise en place du client

Voici une configuration de client simple dont vous pouvez vous inspirer. Bien entendu, installez d’abord les outils Wireguard grâce à votre gestionnaire de paquets préféré.

cat > /etc/wireguard/wg.conf << EOF
[Interface]
Address = 10.66.0.2/24
PrivateKey = uPoWquE6Ftx1L6Re2L6SPtJDqpfZl00aUc8hhDZ8klQ=
ListenPort = 0 #needed for some clients to accept the config

[Peer]
PublicKey = fa4YCJNIlhBEStWMSl8usvFp41lKTC7O/Tu6abvYDQ4=
Endpoint = mon-cluster-kube.fr:32555
#AllowedIPs = 10.66.0.0/24
AllowedIPs = 0.0.0.0/0,::/0 #makes sure ALL traffic routed through VPN
PersistentKeepalive = 25
EOF

La partie Interface est alors votre machine cliente, et le Peer, votre machine serveur. Encore une fois, le contenu de AllowedIps détermine le contenu de votre VPN en spécifiant sur quels réseaux le pair en question sera la passerelle. 0.0.0.0/0 indique donc de transférer tout paquet vers cet hôte là, mais vous pouvez limiter la communication à un sous-réseau plus petit, par exemple pour garder votre connexion Internet en local. Enfin vous m’avez compris.

Côté, client, un outil pratique peut être utilisé pour monter l’interface en question :

wg-quick up wg # Monter l'interface et créer les routes correspondantes
wg-quick down wg # Arrêter l'interface

Cette commande prend en deuxième argument le nom du fichier que vous aurez placé dans /etc/wireguard/

Usage, debug et troubleshooting

On peut pas dire que la documentation de Wireguard soit à l’état de l’art. C’est très concis, notamment concernant les configurations possible. Fort heureusement, un repo non officiel s’avère beaucoup plus complet, je vous invite à aller y faire un tour.

Sur une machine ayant le module activé et en utilisation, vous pouvez lancer la commande suivante pour avoir des informations sur l’état de connexions des peers, par exemple (résultat d’une commande lancée sur un client).

$ wg
interface: wg
  public key: A7rD6b+WyL0dG8TDaCqWZyjgQiegnmX5l/vHobv2LT8=
  private key: (hidden)
  listening port: 51373
  fwmark: 0xca6c

peer: fi3lZUf+1XBCc5FLBt/xALXfoESDbJNRZm/3SD3pbkg=
  endpoint: xxx.xxx.xxx.xxx:32555
  allowed ips: 10.66.0.0/24, 0.0.0.0/0, ::/0
  latest handshake: 42 seconds ago
  transfer: 5.17 KiB received, 6.10 KiB sent
  persistent keepalive: every 25 seconds

Concernant le debug, étant donné qu’il s’agit d’un module du noyau, il ne sera pas possible d’avoir un joli fichier de log, il va falloir passer par une instruction à communiquer au module, de la façon suivante :

# Activer le débug
echo 'module wireguard +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# Affichier le buffer de logs du noyau
dmesg -T -w
# Désactiver le debug
echo 'module wireguard -p' | sudo tee /sys/kernel/debug/dynamic_debug/control

Android

L’application Android est très simple et efficace, je n’aurai pas besoin d’expliquer quoi que ce soit à son propos, je vous laisse la découvrir !