Je parlerai ici en priorité de mon expérience pour monter des VPN IPsec, soit un protocole spécifique, car c’était le besoin que j’ai été amené à couvrir.
AWS ne supporte pas encore, à l’heure où j’écris cet article, d’autre protocole pour une connexion VPN managée.
Je vous donne ici une configuration Terraform d’exemple, mais vous pouvez très bien lire l’article (et le schéma réseau) et vous en inspirer pour créer les mêmes ressources à la main, que ce soit via la console, la ligne de commande ou le SDK.
- Un petit schéma réseau
- Quelques variables Terraform utiles
- Les passerelles
- Monter la connexion côté AWS
- Router le trafic
- Quelques notes supplémentaires
Un petit schéma réseau
Avant toute chose, et pour s’assurer de bien comprendre comment les flux réseaux vont voyager, un petit schéma peut aider à partir sur de bonnes bases.
Quelques variables Terraform utiles
Avant de commencer, j’ai l’habitude de définir quelques variables communes à tous mes runs Terraform (que je peux importer via un fichier commun, chargé par grâce à un lien symbolique) :
variable "project_name" {
description = "Nom du projet"
type = string
default = "corentinhatte"
validation {
condition = length(var.project_name) <= 12 && length(regexall("[^a-zA-Z0-9-]", var.project_name)) == 0
error_message = "The project tag must be no more than 12 characters, and only contain letters, numbers, and hyphens."
}
}
variable "environment" {
description = "Nom de l'environnement actuel"
type = string
default = "test"
validation {
condition = length(var.environment) <= 12 && length(regexall("[^a-zA-Z0-9-]", var.environment)) == 0
error_message = "The environment tag must be no more than 12 characters, and only contain letters, numbers, and hyphens."
}
}
Ces variables me permettent de définir une local pour appliquer un préfixe à tous mes noms de ressource. Pratique pour éviter les conflits de noms entre les environnements.
locals {
prefix = "${var.project_name}-${var.environment}"
}
On peut également définir une variable pour le nom du VPN à monter (par exemple en y mettant le nom du client concerné), si on veut faire un module réutilisable :
variable "vpn_name" {
description = "Nom du VPN"
type = string
default = ""
validation {
condition = length(var.vpn_name) >= 3 && length(var.vpn_name) <= 20 && length(regexall("[^a-zA-Z0-9-]", var.vpn_name)) == 0
error_message = "The vpn_name tag must be 3 to 20 characters long, and only contain letters, numbers, and hyphens."
}
}
Les passerelles
Comme vous pouvez le voir sur le schéma, avant de lancer la connexion il faut donc définir les passerelles de chaque côté : c’est elles qui vont faire passer les flux réseaux.
La passerelle privée est gérée par AWS et peut être partagée entre plusieurs tunnels, là où la passerelle client est une ressource virtuelle qui permet de définir l’IP de connexion distante spécifique à chaque client.
Récupérer le VPC
Pour se simplifier la vie, on peut récupérer notre VPC via une data source aws_vpc
.
data "aws_vpc" "default" {
default = true
}
On filtre ici très simplement pour récupérer le VPC par défaut du compte, mais vous pouvez tout à faire changer de sélecteur pour filtrer sur des étiquettes (tags) par exemple.
Notre passerelle privée
resource "aws_vpn_gateway" "main" {
vpc_id = data.aws_vpc.default.id
tags = {
Name = "${local.prefix}-main"
}
}
Comme vous le voyez rien d’extraordinaire : on spécifie uniquement le VPC auquel on veut attacher notre passerelle privée. Bien que ce soit facultatif (la ressource est créée dans le VPC par défaut si vous ne spécifiez pas son identifiant), ça évite de la monter dans le mauvais VPC par erreur.
La passerelle client
resource "aws_customer_gateway" "main" {
bgp_asn = 65000
device_name = "${local.prefix}-${var.vpn_name}"
ip_address = var.gateway_ip_address
type = "ipsec.1"
tags = {
Name = "${local.prefix}-${var.vpn_name}"
}
}
Le principe est simple : on définit l’IP de la passerelle distante à laquelle on souhaite se connecter, on précise le type de VPN (ici ipsec.1
)… et c’est tout.
Si vous avez besoin d’attacher un certificat, pour pouvez utiliser le paramètre certificate_arn
.
Monter la connexion côté AWS
resource "aws_vpn_connection" "main" {
vpn_gateway_id = aws_vpn_gateway.main.id
customer_gateway_id = aws_customer_gateway.main.id
type = "ipsec.1"
static_routes_only = true
tags = {
Name = "${local.prefix}-${var.vpn_name}"
}
}
Il existe beaucoup plus de paramètres pour configurer par exemple les clés pré-partagées, les algorithmes autorisés, le lancement automatique de la connexion… je vous laisse parcourir la documentation Terraform aws_vpn_connection
pour trouver ceux qui vous conviennent.
À noter qu’une connexion VPN AWS crée deux tunnels par défaut, avec chacun leur IP et leurs paramètres dédiés, pour des raisons de redondance. Vous n’êtes pas obligé d’utiliser les deux tunnels si vous n’avez pas besoin de haute disponibilité (ou que votre client ne veut pas monter/gérer deux tunnels).
Félicitations, vous avez créé une connexion VPN !
Vous pouvez vérifier qu’elle est opérationnelle dans la console AWS : le statut devrait être UP (s’il est DOWN c’est que la connexion n’est pas opérationnelle, il faudra peut-être adapter la configuration des passerelles).
Router le trafic
Pour diriger le trafic depuis votre sous-réseau jusqu’au réseau de votre client il faut gérer le routage à deux niveaux :
- Dans votre réseau interne, via une table de routage, pour diriger vers votre passerelle privée
- Au niveau de la passerelle privée, pour diriger vers la bonne connexion VPN
Router en interne vers la passerelle privée
Cette ressource peut être redondante à créer si vous ciblez plusieurs IP ou blocs d’IP distants. Pour me faciliter la tâche j’ai défini une variable egress_routes
dans mon module :
variable "egress_routes" {
description = "Routes/Adresses à diriger vers le VPN"
type = list(string)
default = ["8.8.8.8"]
}
Je peux alors boucler sur cette liste pour créer les routes utilisées par mes serveurs/instances privées :
resource "aws_route" "routes_az_a" {
count = length(var.egress_routes)
route_table_id = data.aws_route_table.private.id
destination_cidr_block = var.egress_routes[count.index]
gateway_id = aws_vpn_gateway.main.id
}
N’oubliez pas d’importer votre table de routage via une data source si ce n’est pas déjà fait :
data "aws_route_table" "private" {
filter {
name = "tag:Name"
values = ["${local.prefix}-private"]
}
}
Envoyer le trafic vers le bon client
Une fois que le trafic est arrivé dans la passerelle privée, il faut qu’elle sache vers quelle connexion l’envoyer (histoire que vous n’envoyiez pas des données au mauvais client). Pour cela on réutilise notre variable egress_routes
pour boucler :
resource "aws_vpn_connection_route" "main" {
count = length(var.egress_routes)
destination_cidr_block = var.egress_routes[count.index]
vpn_connection_id = aws_vpn_connection.main.id
}
Vous pouvez tester que votre connexion fonctionne dans un sens (le routage dans l’autre sens étant à définir sur le réseau du client) :
# Remplacez `8.8.8.8` par l'IP du serveur client à contacter
ping 8.8.8.8
Quelques notes supplémentaires
Si vous utilisez un équilibreur de charge
Vous pouvez voir dans le schéma en début d’article que j’ai ajouté un équilibreur de charge dans le réseau privé, mais que je n’ai pas défini de ressource pour ça. Je vous laisse libre d’en définir une si besoin, car chaque besoin est unique.
Quelques notes/suggestions néanmoins si vous en utilisez un :
- Si le trafic doit rester privé, pensez à rendre l’équilibreur interne pour lui attribuer uniquement une IP privée joignable uniquement via le VPN et/ou votre réseau interne
- Votre client aura besoin de définir une route de sortie vers votre load balancer, pas vers vos instances
- Le trafic reçu par votre client viendra en revanche de vos instances (qui devront être autorisées en entrée, donc), pas de l'ALB
Pour stocker les clés pré-partagées
Si vous utilisez des clés pré-partagées et que vous ne laissez pas AWS les définir pour vous (par exemple si votre client les fournit), vous pouvez les stocker dans un Parameter Store ou dans Secrets Manager, puis en importer la valeur dans votre module (la clé est de toute façon stockée en clair dans le state Terraform) par exemple via une data source.
Ce billet est une copie d’un article publié sur mon blog.
Vous pouvez retrouver la version originale ici