Déployer Wireguard un peu partout🔗

Posted by Médéric Ribreux 🗓 In blog/ Sysadmin/

#debian #security #sysadmin

Introduction

Cela fait quelques temps que j'ai vu passer le logiciel Wireguard. Étant donné qu'il est créé et maintenu par zx2c4, dont j'utilise l'excellent cgit, je me suis dit que ça valait le coup d'y passer un temps certain.

Qu'est-ce-que Wireguard ? Pour faire simple, c'est un VPN. Il est écrit en C avec quelques lignes (5000) de code et semble prometteur pour l'avenir, tant du point de vue de la sécurité que des performances.

Dans cet article, nous allons simplement installer et configurer Wireguard sur une série de machines qui formeront notre VPN.

Installer Wireguard sur une machine Debian Buster

Wireguard est composé de deux éléments:

Sous une machine Debian classique, on peut utiliser la dernière version de Wireguard en installant:

# apt install wireguard-dkms wireguard-tools

Normalement, ces actions devraient vous conduire à installer de quoi compiler le module noyau en live (DKMS), c'est-à-dire le compilateur gcc en version 7 ainsi que les fichiers d'en-tête du noyau Linux. Sur une station de travail, ce n'est pas si lourd.

Installer WireGuard sur un Scaleway C1

J'ai un serveur Scaleway C1. Comme pour n'importe quel fournisseur de serveur dédié, vous ne pouvez pas forcément faire ce que vous voulez. Hé oui, ce n'est pas votre matériel. Forcément, ça s'en ressent aussi dans la partie logicielle.

Je me suis inspiré de ce document. Mais j'ai dû fortement améliorer mes compétences en gestion de noyau pour faire fonctionner réellement Wireguard sur cette machine. Car, il faut bien l'avouer, ma dernière compilation de kernel Linux doit dater d'il y a au moins 10 ans.

Si vous suivez la documentation sus-hyperliée, vous n'obtiendrez rien de satisfaisant. En effet, il existe des petites subtilités qui font que ça ne fonctionnera pas directement.

Le premier problème auquel j'ai dû me confronter est le fait que ma compilation de noyau ne donnait rien d'utilisable en termes de modules. J'ai même réussi à bloater une partie du C1 pendant quelques jours avec des modules qui ne se chargeaient plus. J'ai finalement pris le parti d'essayer de comprendre quelques options, notamment celle de CONFIG_MODULE_SRCVERSION

Ensuite, après avoir réglé ce premier problème et réussi à charger mes modules compilés avec modprobe, j'ai eu beaucoup de mal pour faire fonctionner le module Wireguard qui ne se chargeait tout simplement pas. Après quelques jours de repos (quand un truc ne marche pas, il faut le laisser se reposer, l'éviter pendant un temps. Quand on y revient, c'est souvent la voie du succès alors), je me suis rendu compte que le CPU/SOC du Scaleway C1 est un Marvell Armada 370 et que ce dernier ne gère pas les extensions NEON.

Malheureusement, Wireguard impose d'utiliser les extensions NEON dans le noyau si vous avez un CPU sous ARMv7, ce qui est le cas du CPU du Scaleway C1. J'ai donc dû modifier le fichier Kconfig de Wireguard pour modifier la ligne:

select KERNEL_MODE_NEON if CPU_V7

en

select KERNEL_MODE_NEON if (CPU_V7 && !MACH_ARMADA_370)

et le tour était joué. Je ne pensais pas devoir aller si loin mais j'avoue que quand j'ai réussi à faire un modprobe wireguard sans aucun message d'erreur, j'ai jubilé !

J'en remets le contenu à ma sauce:

# On récupère les éléments de version du Kernel de la machine
arch="$(uname -m)"
release="$(uname -r)"
upstream="${release%%-*}"
local="${release#*-}"

# On récupère les sources
mkdir -p /usr/src
wget -O "/usr/src/linux-${upstream}.tar.xz" "https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-${upstream}.tar.xz"
tar xf "/usr/src/linux-${upstream}.tar.xz" -C /usr/src/
ln -fns "/usr/src/linux-${upstream}" /usr/src/linux
ln -fns "/usr/src/linux-${upstream}" "/lib/modules/${release}/build"

# On récupère la configuration du kernel utilisé
zcat /proc/config.gz > /usr/src/linux/.config
printf 'CONFIG_LOCALVERSION="%s"\nCONFIG_CROSS_COMPILE=""\n' "${local:+-$local}" >> /usr/src/linux/.config
echo "CONFIG_MODULE_SRCVERSION=y" >> /usr/src/linux/.config
wget -O /usr/src/linux/Module.symvers "http://mirror.scaleway.com/kernel/${arch}/${release}/Module.symvers"

# On prépare la configuration
make oldconfig

# On patche Wireguard in-tree 
/home/medspx/WireGuard/contrib/kernel-tree/create-patch.sh | patch -p1
sed 
make menuconfig

make prepare modules_prepare
make CFLAGS_MODULE=-fno-pic -j9
make CFLAGS_MODULE=-fno-pic -j9 modules
make modules_install

Configurer le VPN

Ok, nous avons maintenant deux bécanes avec Wireguard. Construisons donc un VPN avec. Je pars du principe que vous n'avez pas de firewall entre les deux, pour simplifier les commandes.

Avec Wireguard, ce qui est bien, c'est qu'on peut construire le VPN en live puis balancer la configuration finale dans un fichier de configuration avec une seule commande. Tout se passe avec les commandes classiques ip et wg.

-- Génération des clefs privées/publiques
# wg genkey > /etc/wireguard/private

-- Ajout de l'interface wireguard (wg0)
# ip link add dev wg0 type wireguard

-- Affectation d'une IP virtuelle sur l'interface wireguard
# ip addr add 192.168.3.4/24 dev wg0

-- On ajoute notre clef privée
# wg set wg0 private-key /etc/wireguard/private

-- On peut maintenant voir la configuration en dynamique
# wg showconf wg0

-- On va maintenant ajouter la définition d'un autre client
-- Vous avez besoin de sa clef publique que vous pouvez
-- obtenir avec wg show wg0
# wg set wg0 peer LHqF7DAsJoRftEWmj+b2HnBKonZqLwJwdO9eLQYx+lI=
-- allowed-ips 192.168.3.1/32 endpoint medspx.fr:54003

-- Ensuite, il suffit d'activer l'interface réseau
ip link set wg0 up

-- Sur l'autre machine, il faut également ajouter ce nouveau peer
# wg set wg0 peer clef_publique allowed-ips 192.168.3.4/32

-- et maintenant, la connexion devrait être effective
# wg show wg0

-- On peut maintenant stocker la configuration en dur
# wg showconf wg0 > /etc/wireguard/wg0.conf

-- Un peu plus tard, vous pouvez recharger cette configuration
# wg setconf wg0 /etc/wireguard/wg0.conf

Configuration avec systemd-networkd

Une fois la configuration dynamique passée, il peut être intéressant d'utiliser systemd pour garder cette configuration. Pour ma part, je n'utilise que systemd pour la configuration de mes interfaces réseaux. Wireguard est géré par systemd-networkd depuis systemd 237. Cela signifie que c'est présent dans Debian Buster mais également dans Debian Stretch si vous avez activé les dépôts backports.

Vous devez créer un fichier /etc/systemd/networkd/30-wg0.netdev:

[NetDev]
Name = wg0
Kind = wireguard
Description = Wireguard

[WireGuard]
ListenPort = auto
PrivateKey = [CLIENT PRIVATE KEY]

[WireGuardPeer]
PublicKey = [SERVER PUBLIC KEY]
PresharedKey = [PRE SHARED KEY]
AllowedIPs = 192.168.3.1/32
Endpoint = medspx.fr:54003
PersistentKeepalive = 60

Le ListenPort à auto indique que le port change chaque fois que vous lancez l'interface. C'est une configuration plutôt intéressante pour des stations en mode client sans configuration spécifique de pare-feu.

Si vous avez un serveur public avec une IP fixe, je vous conseille de déclarer un port spécifique et de l'ouvrir dans votre pare-feu.

Le PersistentKeepalive est également intéressant si vous êtes derrière un NAT (donc plutôt réservé aux stations de type client). Avec cette configuration, Wireguard envoi un petit paquet réseau de temps en temps (selon l'intervalle indiqué) pour ré-ouvrir l'interface vers l'extérieur depuis le réseau interne. Cela permet de laisser la connexion ouverte au niveau du routeur NAT et donc de laisser votre client disponible pour le reste du VPN.

Si vous configurez une sorte de serveur VPN, vous devez indiquer tous les clients enregistrés en ajoutant autant de directives [WireGuardPeer] que de clients, afin de partager les clefs publiques.

Puis, vous aurez besoin d'un fichier pour configurer votre interface dans /etc/systemd/networkd/30-wg0.network:

[Match]
Name = wg0

[Link]
RequiredForOnline = no

[Network]
Address = 192.168.3.4/24

[Route]
Gateway = 192.168.3.1
Destination = 192.168.3.0/24

J'ai indiqué la directive RequiredForOnline à no car, pour certaines machines (typiquement un portable avec une connexion erratique), cela permet de démarrer le système sans attendre que Wireguard soit activé.

Aller un peu plus loin que le test du ping

Nous allons mettre en place ssh derrière une machine du VPN. L'astuce consiste simplement à mettre la ligne suivante dans /etc/ssh/sshd_config:

# L'adresse publique
ListenAddress 10.0.0.1:2222

# On écoute sur l'adresse Wireguard
ListenAddress 192.168.3.1:22

À partir de cet instant, vous pouvez utiliser un client ssh depuis une des machines du VPN pour attaquer directement le serveur via Wireguard.

On le voit, la grande force de Wireguard est d'utiliser une interface réseau virtuelle dédiée. De fait, n'importe quelle configuration classique d'un logiciel serveur peut être utilisée sans commandes spéciales.

Si vous avez un firewall

Pour ma part, mon C1 a un pare-feu configuré avec nftables. J'ai choisi de laisser passer tout ce qui vient ou va vers wg0.

Globalement ma configuration est la suivante:

#!/usr/sbin/nft -f

# Configuration NFT de debianplug

# On vide toutes les règles
flush ruleset

# Table pour un serveur public un peu fermé
table inet public_server {
  # Chaîne de gestion de tout ce qui rentre
  chain entree {
	# Chaîne de type filtre, en entrée
	type filter hook input priority 0;

	# Accepter tout ce qui vient de l'interface loopback
	meta iif lo accept

	# Accepter tout ce qui vient de Wireguard
	meta iif wg0 accept

	# Accepte le trafic qui vient de la machine
	ct state established,related accept

	# Réponse au ping IPv4
	ip protocol icmp accept

	# Ouverture des ports publics
	# Le port 54003 est celui de Wireguard
	tcp dport { 25,53,80,143,443,1376,4416,4444,5222,5269,9714,54003 } accept
	udp dport { 53, 9714, 54003 } accept

	# Ouverture du port nbd
	tcp sport { 4416 } accept

	# Compter et journaliser ce qui est rejeté
	counter log prefix "Connexion entrante rejetée: " level warn drop
  }

  # Chaîne de gestion de tout ce qui sort
  chain sortie {
	type filter hook output priority 0;

	# On accepte les sorties vers l'interface loopback
	meta oif lo accept

	# On accepte les sorties vers Wireguard
	meta oif wg0 accept

	# Réponse au ping IPv4
	ip protocol icmp accept

	# Ouverture des ports publics
	tcp sport { 25,53,80,143,443,1376,4444,5222,5269,9714,54003  } accept
	udp sport { 53,9714,54003 } accept

	# Ouverture des ports externes classiques utilisés par le système
	tcp dport { 25,53,80,143,443,995,4416,8443 } accept
	udp dport { 53,67,68,123 } accept

	# count and drop any other traffic
	counter log flags skuid prefix "Connexion sortante rejetée: " level warn drop
  }
}

Dans cette configuration, j'ouvre le port 54003 de Wireguard sur cette machine et j'autorise tout ce qui passe par l'interface wg0. De cette manière, tout marche !

Conclusions

Ce truc est vraiment extra ! D'abord, ça marche et ça passe pas mal de caca. Le code est super light. J'ai eu l'occasion de le parser rapidement pour trouver mes erreurs de symboles de Kernel et j'ai été étonné du nombre ultra limité de fichiers et de lignes (5000 seulement, sachant que mon programme de base pgMitm en fait près de 1000 à lui tout seul pour pas grand-chose). L'implémentation est vraiment élégante et fine. Je ne parle pas de la configuration en live qui témoigne d'un vrai réalisme dans la mise en place de réseaux qui se solde généralement par un nombre impressionnant de systemctl restart systemd-networkd.

Comme d'habitude, Jason Donenfield et zx2c4 sortent un truc de qualité et je suis fier de leur rendre un hommage appuyé.