Un VPN permet de connecter des machines ou des réseaux ensemble via un tunnel sécurisé au travers d'un réseau auquel on ne fait pas forcément confiance. Il existe de multiples utilités, comme protéger notre activité sur un réseau WiFi non chiffré, accéder à des machines locales en étant à distance, ou encore accéder à des services depuis un point de sortie différent de notre endroit réel.

J'utilise depuis plusieurs années OpenVPN, qui convient parfaitement pour mon usage. Cependant, il est assez complexe à configurer et à maintenir, en partie à cause de la gestion des certificats SSL et de leur expiration, mais aussi parce que son fichier de configuration est assez imposant.

Récemment, un nouveau protocole de VPN a été intégré dans le noyau Linux : WireGuard. Il est annoncé comme étant très sécurisé en utilisant des algorithmes cryptographiques récents, rapide, léger, et très simple. Je m'y suis donc intéressé, pour voir s'il pourrait remplacer OpenVPN sur mes machines.

Différences par rapport à OpenVPN

Un cadre plus restreint

WireGuard est seulement un protocole de VPN. Contrairement à OpenVPN, il ne gère donc que l'établissement d'un lien VPN entre deux machines.

La configuration des interfaces VPN, l'envoi de configuration aux machines distantes (routes, serveur DNS, etc.) et le passage par un serveur proxy pour établir une connexion sont autant de fonctionnalités proposées par OpenVPN qui sortent du cadre de WireGuard. De plus, il se restreint au protocole UDP.

En contrepartie, sa configuration est bien plus simple, et chaque machine peut définir entièrement sa propre configuration sans devoir ignorer ou redéfinir certaines configurations envoyées par le serveur.

Un modèle d'infrastructure plus libre

Une autre différence par rapport à OpenVPN est que WireGuard ne force pas un modèle basé sur un serveur centralisant les connexions de toutes les autres machines. Il est possible de mettre en place un VPN WireGuard avec une machines jouant le rôle d'unique serveur, mais la présence de cette machine centrale n'est pas obligatoire.

En effet, WireGuard ne comporte vraiment pas de rôle client ou serveur, mais uniquement des pairs (peers), chacun pouvant se connecter à d'autres. Plus exactement, chaque machine peut agir à la fois en tant que client et/ou en tant que serveur pour chacun des autres pairs.

Ainsi, plusieurs machines présentes dans un même réseau local pourront communiquer directement au travers de ce réseau. On peut aussi imaginer deux machines serveur ne pouvant chacune contacter l'autre directement être liées par une troisième agissant comme client auprès des deux premières. Il faut cependant faire attention à ne pas rendre le réseau trop complexe.

Installation

WireGuard est composé d'un module du noyau Linux, intégré en standard depuis la version 5.6, ainsi que d'un ensemble d'utilitaires permettant de voir et modifier la configuration des interfaces VPN. Pour les systèmes utilisant un noyau plus vieux, il est possible d'ajouter le module wireguard au moyen de dkms.

$ # Installation du module pour les noyaux anciens (avant 5.6)
$ sudo apt install wireguard-dkms

Le réseau des machines sur lesquelles je l'utilise est géré par systemd-networkd, qui supporte nativement WireGuard depuis la version 237. Les utilitaires ne sont donc nécessaires que sur la machine depuis laquelle je vais réaliser la configuration, qui ne participera pas nécessairement au VPN, afin de générer les clés.

$ # Installation des utilitaires
$ sudo apt install wireguard-tools

Sous Debian Buster, il est nécessaire d'ajouter le dépôt backports pour pouvoir installer les paquets nécessaires.

Génération des clés

Afin de pouvoir établir une connexion entre les différentes machines, il est nécessaire de générer une paire de clés pour chacune. Les clés étant des éléments secrets, les droits des fichiers doivent être restreints.

$ umask 077
$ wg genkey | tee private | wg pubkey > public

Si vous souhaitez simplement générer des clés pour les coller dans un fichier de configuration, les afficher dans la console sera probablement plus pratique. La première affichée est la clé privée, la seconde est la clé publique correspondante.

$ wg genkey | tee /dev/stderr | wg pubkey

Enfin, pour améliorer la sécurité, il est possible de générer aussi une clé partagée (PreShared Key) pour chaque paire de machines :

$ umask 077
$ wg genpsk > peer1-peer2
$ wg genpsk > peer2-peer3
$ wg genpsk > peer1-peer3

Configuration des interfaces

Le support de WireGuard étant natif dans systemd-networkd, sa configuration se mêle avec celle de l'interface correspondante. Deux fichier seront nécessaires :

  • Un fichier .netdev, définissant la configuration de l'interface virtuelle.
  • Un fichier .network, définissant sa configuration réseau.

Note : Si le réseau de votre machine n'est pas géré par systemd-networkd, il est quand-même possible de l'utiliser pour configurer une connexion WireGuard. Dans ce cas, il suffira simplement de lui indiquer d'ignorer les interfaces qu'il ne doit pas gérer. Par exemple, dans /etc/systemd/network/10-ignored.network :

[Match]
Name=!en* wl*

Définition de l'interface virtuelle

La configuration de l'interface virtuelle est placée dans /etc/systemd/network/50-wg0.netdev :

[NetDev]
Name=wg0
Kind=wireguard

[WireGuard]
# Clé privée
PrivateKey=<local_peer_private_key>
# Port d'écoute, si la machine est joignable directement par d'autres
ListenPort=<port>

[WireGuardPeer]
# Clé publique du pair
PublicKey=<remote_peer_public key>
# Clé partagée
PresharedKey=<shared_key>
# Adresse de la machine distante, si elle est joignable directement
Endpoint=<remote_peer_public_ip>:<port>
# Durée maximum d'inactivité avant l'envoi d'un paquet permettant de conserver la connexion
PersistentKeepalive=<seconds>
# Adresses IP ou sous-réseaux accessibles depuis ce pair
AllowedIPs=<remote_peer_ip>/32
AllowedIPs=<remote_subnet1>/<mask>
AllowedIPs=<remote_subnet2>/<mask>

Les clés privées et partagées étant des éléments secrets, il faut bien s'assurer que les droits du fichier .netdev sont restreints, afin que tout le monde ne puisse pas les lire. Une bonne pratique est de définir le couple root:systemd-network comme propriétaire et appliquer les permissions 0640.

Note : Depuis la version 242 de systemd, des paramètres PrivateKeyFile et PresharedKeyFile ont été ajoutés, permettant de stocker les clés privées et partagées dans des fichiers. Lorsque l'un de ces paramètres est utilisé, le paramètre correspondant (PrivateKey et PresharedKey) est ignoré.

Configuration réseau de l'interface

Une fois l'interface virtuelle définie, il faut donner sa configuration réseau dans /etc/systemd/network/50-wg0.network :

[Match]
Name=wg0

[Network]
Address=<local_peer_ip>/<mask>

[Route]
Destination=<remote_subnet1>/<mask>
Scope=link

[Route]
Destination=<remote_subnet2>/<mask>
Scope=link

Note : Comme n'importe quelle interface réseau, il est possible de lui attribuer plusieurs adresses IP si nécessaire. WireGuard ne s'occupant que de l'établissement d'un lien sécurisé, cela n'a aucune incidence sur son fonctionnement.

Contrairement à d'autres gestionnaires de réseau, systemd-networkd ne définit pas automatiquement de route pour chaque adresse IP ou sous-réseau autorisé. Il est donc nécessaire de les définir explicitement dans le fichier .network.

Notes générales

Le paramètre Endpoint n'est nécessaire que dans un sens, mais il peut très bien être présent des eux cotés si les deux pairs sont joignables directement. De cette manière, n'importe laquelle des deux machines pourra initier la connexion.

Le paramètre AllowedIPs permet à WireGuard de savoir vers quel pair envoyer les paquets que l'interface reçoit selon leur destination. En cas de chevauchement entre les adresses IP et sous-réseaux déclarés, seul l'un des pairs en conflit aura la configuration. Il faut donc bien faire attention au masque appliqué, pour éviter qu'un pair ne soit associé à un subnet complet, interdisant ainsi de contacter des machines de ce subnet via d'autres pairs. C'est pour cette raison qu'il est courant d'utiliser le masque /32 pour les IP du réseau WireGuard lui-même.

Il est possible de configurer des interfaces WireGuard dans des containers non-privilégiés sans configuration spécifique. Le seul prérequis pour utiliser WireGuard est que le module soit chargé sur le noyau de l'hôte.

Lorsqu'une machine agit en tant que client au niveau VPN, mais que ses services doivent pouvoir être contactés par les autres pairs, il est nécessaire d'ajouter le paramètre PersistentKeepalive. En effet, la connexion VPN n'est établie entre deux pairs que lors de la première communication. Tant que cette machine n'aura rien envoyé, les autres pairs ne pourront donc pas la contacter, par exemple après une coupure de la connexion.

Conclusion

WireGuard est maintenant utilisé à la place d'OpenVPN depuis trois semaines sur mes serveurs, et je ne vois pas de différence au quotidien. Il présente pour moi l'avantage d'avoir une configuration beaucoup plus simple, que j'ai donc pu intégrer dans mes playbooks Ansible.

J'ai eu l'occasion de tester les déconnexions lors du redémarrage des pairs client et/ou serveur séparément, et le lien s'est rétablit automatiquement à chaque fois sans action particulière de ma part.

Mon cas d'utilisation étant très simple, je n'ai pas eu l'occasion de comparer les performances entre WireGuard et OpenVPN.