Sécuriser le Cloud avec Tailscale : Mise en œuvre d'un VPN simplifiée

Sommaire

Lorsqu'on parle de sécurisation de l'accès aux ressources Cloud, l'une des règles d'or est d'éviter les expositions directes à Internet. La question qui se pose alors pour les Devs/Ops est : comment, par exemple, accéder à une base de données, un cluster Kubernetes ou un serveur via SSH sans compromettre la sécurité?

Les réseaux privés virtuels (VPN) offrent une réponse en établissant un lien sécurisé entre différents éléments d'un réseau, indépendamment de leur localisation géographique. De nombreuses solutions existent, allant de modèles en SaaS aux solutions que l'on peut héberger soi-même, utilisant divers protocoles et étant soit open source, soit propriétaires.

Parmi ces options, je souhaitais vous parler de Tailscale. Cette solution utilise le protocole WireGuard, réputé pour sa simplicité et sa performance. Avec Tailscale, il est possible de connecter des appareils ou serveurs de manière sécurisée, comme s'ils étaient sur un même réseau local, bien qu'ils soient répartis à travers le monde.

🎯 Nos objectifs

  • Comprendre comment fonctionne Tailscale
  • Mise en oeuvre d'une connexion sécurisée avec AWS en quelques minutes
  • Interragir avec l'API d'un cluster EKS via un réseau privé
  • Accéder à des services hébergés sur Kubernetes en utilisant le réseau privé

Pour le reste de cet article il faudra évidemment créer un compte Tailscale. A noter que l'authentification est déléguée à des fournisseurs d'identité tiers (ex: Okta, Onelogin, Google ...).

Lorsque le compte est crée, on a directement accès à la console de gestion ci-dessus. Elle permet notamment de lister les appareils connectés, de consulter les logs, de modifier la plupart des paramètres...

💡 Sous le capot

Terminologie

Mesh VPN: Un mesh VPN est un type de réseau VPN où chaque nœud (c'est-à-dire chaque appareil ou machine) est connecté à tous les autres nœuds du réseau, formant ainsi un maillage. À distinguer des configurations VPN traditionnelles qui sont conçues généralement "en étoile", où plusieurs clients se connectent à un serveur central.

Zero trust: Signifie que chaque demande d'accès à un réseau est traitée comme si elle venait d'une source non fiable. Une application ou utilisateur doit prouver son identité et être autorisée avant d'accéder à une ressource. On ne fait pas confiance simplement parce qu'une machine ou un utilisateur provient d'un réseau interne ou d'une certaine zone géographique.

Tailnet: Dès la première utilisation de Tailscale, un Tailnet est crée pour vous et correspond à votre propre réseau privé. Chaque appareil dans un tailnet reçoit une IP Tailscale unique, permettant une communication directe entre eux.

L'architecture de Tailscale est conçue de telle sorte que le Control plane et le Data plane sont clairement séparés:

  • D'une part, il y a le serveur de coordination. Son rôle est d'échanger des métadonnées et des clés publiques entre tous les participants d'un Tailnet (La clé privée étant gardée en toute sécurité son nœud d'origine).

  • D'autre part, les nœuds du Tailnet s'organisent en un réseau maillé (Mesh). Au lieu de passer par le serveur de coordination pour échanger des données, ces nœuds communiquent directement les uns avec les autres en mode point à point. Chaque nœud dispose d'une identité unique pour s'authentifier et rejoindre le Tailnet.

📥 Installation du client

La majorité des plateformes sont supportées et les procédures d'installation sont listées ici. En ce qui me concerne je suis sur Archlinux:

1sudo pacman -S tailscale

Il est possible de démarrer le service automatiquement au démarrage de la machine.

1sudo systemctl enable --now tailscaled

Pour enregistrer son ordinateur perso, lancer la commande suivante:

1sudo tailscale up --accept-routes
2
3To authenticate, visit:
4
5        https://login.tailscale.com/a/f50...

ℹ️ l'option --accept-routes est nécessaire sur Linux et permettra d'accepter les routes annoncées par les Subnet routers. On verra cela dans la suite de l'article

Vérifier que vous avez bien obtenu une IP du réseau Tailscale:

1tailscale ip -4
2100.118.83.67
3
4tailscale status
5100.118.83.67   ogenki               smainklh@    linux   -

ℹ️ Pour les utilisateurs de Linux, vérifier que Tailscale fonctionne bien avec votre configuration DNS: Suivre cette documentation.

Les sources

Toutes les étapes réalisées dans cet article proviennent de ce dépôt git

Il va permettre de créer l'ensemble des composants qui ont pour objectif d'obtenir un cluster EKS de Lab et font suite à un précédent article sur Cilium et Gateway API.

☁️ Accéder à AWS en privé

Subnet router

Afin de pouvoir accéder de manière sécurisée à l'ensemble des ressources disponibles sur AWS, il est possible de déployer un Subnet router.

Un Subnet router est une instance Tailscale qui permet d'accéder à des sous-réseaux qui ne sont pas directement liés à Tailscale. Il fait office de pont entre le réseau privé virtuel de Tailscale (Tailnet) et d'autres réseaux locaux.

Nous pouvons alors router des sous réseaux du Clouder à travers le VPN de Tailscale.

⚠️ Pour ce faire, sur AWS, il faudra bien entendu configurer les security groups correctement pour autoriser les Subnet routers.

🚀 Déployer un Subnet router

Entrons dans le vif du sujet et deployons un Subnet router sur un réseau AWS!
Tout est fait en utilisant le code Terraform présent dans le répertoire opentofu/network. Nous allons analyser la configuration spécifique à Tailscale qui est présente dans le fichier tailscale.tf avant de procéder au déploiement.

Le provider Terraform

Il est possible de configurer certains paramètres au travers de l'API Tailscale grâce au provider Terraform. Pour cela il faut au préalable génerer une clé d'API 🔑 sur la console d'admin:

Il faudra conserver cette clé dans un endroit sécurisé car elle est utilisée pour déployer le Subnet router

1provider "tailscale" {
2  api_key = var.tailscale.api_key
3  tailnet = var.tailscale.tailnet
4}

les ACL's

Les ACL's permettent de définir qui est autorisé à communiquer avec qui (utilisateur ou appareil). À la création d'un compte, celle-cis sont très permissives et il n'y a aucune restriction (tout le monde peut parler avec tout le monde).

 1resource "tailscale_acl" "this" {
 2  acl = jsonencode({
 3    acls = [
 4      {
 5        action = "accept"
 6        src    = ["*"]
 7        dst    = ["*:*"]
 8      }
 9    ]
10...
11}
Note

Pour mon environnement de Lab, j'ai conservé cette configuration par défault car je suis la seule personne à y accéder. De plus les seuls appareils connectés à mon Tailnet sont mon laptop et le Subnet router. En revanche dans un cadre d'entreprise, il faudra bien y réfléchir. Il est alors possible de définir une politique basée sur des groupes d'utilisitateurs ou sur les tags des noeuds.

Consulter cette doc pour plus d'info.

Les noms de domaines (DNS)

Il y a différentes façons possibles de gérer les noms de domaines avec Tailscale:

Magic DNS: Lorsqu'un appareil rejoint le Tailnet, il s'enregistre avec un nom et celui-ci peut-être utilisé directement pour communiquer avec l'appareil.

1tailscale status
2100.118.83.67   ogenki               smainklh@    linux   -
3100.115.31.152  ip-10-0-43-98        smainklh@    linux   active; relay "par", tx 3044 rx 2588
4
5ping ip-10-0-43-98
6PING ip-10-0-43-98.tail9c382.ts.net (100.115.31.152) 56(84) bytes of data.
764 bytes from ip-10-0-43-98.tail9c382.ts.net (100.115.31.152): icmp_seq=1 ttl=64 time=11.4 ms

AWS: Pour utiliser les noms de domaines internes à AWS il est possible d'utiliser la deuxième IP du VPC qui correspond toujours au serveur DNS. Cela permet d'utiliser les éventuelles zones privées sur route53 ou de se connecter aux ressources en utilisant les noms de domaines.

La configuration la plus simple est donc de déclarer la liste des serveurs DNS à utiliser et d'y ajouter celui de AWS. Ici un exemple avec le DNS publique de Cloudflare.

1resource "tailscale_dns_nameservers" "this" {
2  nameservers = [
3    "1.1.1.1",
4    cidrhost(module.vpc.vpc_cidr_block, 2)
5  ]
6}

La clé d'authentification ("auth key")

Pour qu'un appareil puisse rejoindre le Tailnet au démarrage il faut que Tailscale soit démarré en utilisant une clé d'authentification. Celle-ci est générée comme suit

1resource "tailscale_tailnet_key" "this" {
2  reusable      = true
3  ephemeral     = false
4  preauthorized = true
5}
  • reusable: S'agissant d'un autoscaling group, il faut que cette même clé puisse être utilisée plusieurs fois.
  • ephemeral: Pour cette démo nous créons une clé qui n'expire pas. En production il serait préférable d'activer l'expiration.
  • preauthorized: Il faut que cette clé soit déjà valide et autorisée pour que l'instance rejoigne automatiquement le Tailscale.

La clé ainsi générée est utilisée pour lancer tailscale avec le paramètre --auth-key

1sudo tailscale up --authkey=<REDACTED>

Annoncer les routes pour les réseaux AWS

Enfin il faut annoncer le réseau que l'on souhaite faire passer par le Subnet router. Dans notre exemple, nous décidons de router tout le réseau du VPC qui a pour CIDR 10.0.0.0/16.

Afin que cela soit possible de façon automatique, il y a une règle autoApprovers à ajouter. Cela permet d'indiquer que les routes annoncées par l'utilisateur smainklh@gmail.com sont autorisées sans que cela requiert une étape d'approbation.

1    autoApprovers = {
2      routes = {
3        "10.0.0.0/16" = ["smainklh@gmail.com"]
4      }
5    }

La commande lancée au démarrage de l'instance Subnet router est la suivante:

1sudo tailscale up --authkey=<REDACTED> --advertise-routes="10.0.0.0/16"

Le module Terraform

J'ai créé un module très simple qui permet de déployer un autoscaling group sur AWS et de configurer Tailscale. Au démarrage de l'instance, elle s'authentifiera en utilisant une auth_key et annoncera les réseaux indiqués. Dans l'exemple ci-dessous l'instance annonce le CIDR du VPC sur AWS.

 1module "tailscale_subnet_router" {
 2  source  = "Smana/tailscale-subnet-router/aws"
 3  version = "1.0.4"
 4
 5  region = var.region
 6  env    = var.env
 7
 8  name     = var.tailscale.subnet_router_name
 9  auth_key = tailscale_tailnet_key.this.key
10
11  vpc_id                = module.vpc.vpc_id
12  subnet_ids            = module.vpc.private_subnets
13  advertise_routes      = [module.vpc.vpc_cidr_block]
14...
15}

Maintenant que nous avons analysé les différents paramètres, il est temps de démarrer notre Subnet router 🚀 !!

Il faut au préalable créer un fichier variable.tfvars dans le répertoire opentofu/network.

 1env                 = "dev"
 2region              = "eu-west-3"
 3private_domain_name = "priv.cloud.ogenki.io"
 4
 5tailscale = {
 6  subnet_router_name = "ogenki"
 7  tailnet            = "smainklh@gmail.com"
 8  api_key            = "tskey-api-..."
 9}
10
11tags = {
12  project = "demo-cloud-native-ref"
13  owner   = "Smana"
14}

Puis lancer la commande suivante:

1tofu plan --var-file variables.tfvars

Après vérification du plan, appliquer les changements

1tofu apply --var-file variables.tfvars

Quand l'instance est démarrée, elle apparaitra dans la liste des appareils du Tailnet.

1tailscale status
2100.118.83.67   ogenki               smainklh@    linux   -
3100.68.109.138  ip-10-0-26-99        smainklh@    linux   active; relay "par", tx 33868 rx 32292

Nous pouvons aussi vérifier que la route est bien annoncée comme suit:

1tailscale status --json|jq '.Peer[] | select(.HostName == "ip-10-0-26-99") .PrimaryRoutes'
2[
3  "10.0.0.0/16"
4]

⚠️ Pour des raisons de sécurité, pensez à supprimer le fichier variables.tfvars car il contient la clé d'API.

👏 Et voilà ! Nous sommes maintenant en mesure d'accéder au réseau sur AWS, à condition d'avoir également configuré les règles de filtrage, comme les ACL et les security groups. Nous pouvons par exemple accéder à une base de données depuis le poste de travail

1psql -h demo-tailscale.cymnaynfchjt.eu-west-3.rds.amazonaws.com -U postgres
2Password for user postgres:
3psql (15.4, server 15.3)
4SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off)
5Type "help" for help.
6
7postgres=>

💻 Une autre façon de faire du SSH

Traditionnellement, nous devons parfois nous connecter à des serveurs en utilisant le protocole SSH. Pour ce faire, il faut générer une clé privée et distribuer la clé publique correspondante sur les serveurs distants.

Contrairement à l'utilisation des clés SSH classiques, étant donné que Tailscale utilise Wireguard pour l'authentification et le chiffrement des connexions il n'est pas nécessaire de ré-authentifier le client. De plus, Tailscale gère également la distribution des clés SSH d'hôtes. Les règles ACL permettent de révoquer l'accès des utilisateurs sans avoir à supprimer les clés SSH. De plus, il est possible d'activer un mode de vérification qui renforce la sécurité en exigeant une ré-authentification périodique. On peut donc affirmer que l'utilisation de Tailscale SSH simplifie l'authentification, la gestion des connexions SSH et améliore le niveau de sécurité.

Les autorisations pour utiliser SSH sont aussi gérées au niveau des ACL's

 1...
 2    ssh = [
 3      {
 4        action = "check"
 5        src    = ["autogroup:member"]
 6        dst    = ["autogroup:self"]
 7        users  = ["autogroup:nonroot"]
 8      }
 9    ]
10...

La règle ci-dessus autorise tous les utilisateurs à accéder à leurs propres appareils en utilisant SSH. Lorsqu'ils essaient de se connecter, ils doivent utiliser un compte utilisateur autre que root. Pour chaque tentative de connexion, une authentification supplémentaire est nécessaire (action=check). Cette authentification se fait en visitant un lien web spécifique

1ssh ubuntu@ip-10-0-26-99
2...
3# Tailscale SSH requires an additional check.
4# To authenticate, visit: https://login.tailscale.com/a/f1f09a548cc6
5...
6ubuntu@ip-10-0-26-99:~$

Pour que cela soit possible il faut aussi démarrer Tailscale avec l'option --ssh

Les logs d'accès à la machine peuvent être consultés en utilisant journalctl

1ubuntu@ip-10-0-26-99:~$ journalctl -aeu tailscaled|grep ssh
2Oct 15 15:51:34 ip-10-0-26-99 tailscaled[1768]: ssh-conn-20231015T155130-00ede660b8: handling conn: 100.118.83.67:55098->ubuntu@100.68.109.138:22
3Oct 15 15:51:56 ip-10-0-26-99 tailscaled[1768]: ssh-conn-20231015T155156-b6d1dc28c0: handling conn: 100.118.83.67:44560->ubuntu@100.68.109.138:22
4Oct 15 15:52:52 ip-10-0-26-99 tailscaled[1768]: ssh-conn-20231015T155156-b6d1dc28c0: starting session: sess-20231015T155252-5b2acc170e
5Oct 15 15:52:52 ip-10-0-26-99 tailscaled[1768]: ssh-session(sess-20231015T155252-5b2acc170e): handling new SSH connection from smainklh@gmail.com (100.118.83.67) to ssh-user "ubuntu"
6Oct 15 15:52:52 ip-10-0-26-99 tailscaled[1768]: ssh-session(sess-20231015T155252-5b2acc170e): access granted to smainklh@gmail.com as ssh-user "ubuntu"
7Oct 15 15:52:52 ip-10-0-26-99 tailscaled[1768]: ssh-session(sess-20231015T155252-5b2acc170e): starting pty command: [/usr/sbin/tailscaled be-child ssh --uid=1000 --gid=1000 --groups=1000,4,20,24,25,27,29,30,44,46,115,116 --local-user=ubuntu --remote-user=smainklh@gmail.com --remote-ip=100.118.83.67 --has-tty=true --tty-name=pts/0 --shell --login-cmd=/usr/bin/login --cmd=/bin/bash -- -l]

ℹ️ Avec Tailscale SSH il est possible de se connecter en SSH peu importe où est situé l'appareil. En revanche dans un contexte 100% AWS, on préferera probablement utiliser AWS SSM.

Logs

💾 En sécurité il est primordial de pouvoir conserver les logs pour un usage ultérieur. Il existe différents types de logs:

Logs d'audit: Ils sont essentiels pour savoir qui a fait quoi. Ils sont accessibles sur la console d'admin et peuvent aussi être envoyés vers un SIEM.

Logs sur les appareils: Ceux-cis peuvent être consultés en utilisant les commandes appropriées à l'appareil. (journalctl -u tailscaled sur Linux)

Logs réseau: Utiles pour visualiser quels appareils sont connectés les uns aux autres.

☸ Qu'en est-il de Kubernetes?

Sur Kubernetes il existe plusieurs options pour accéder à un Service:

  • Proxy: Il s'agit d'un pod supplémentaire qui transfert les appels à un Service existant.
  • Sidecar: Permet de connecter le pod au Tailnet. Donc la connectivité se fait de bout en bout et il est même possible de communiquer dans les 2 sens. (du pod vers les noeuds du Tailnet).
  • Operator: Permet d'exposer les services et l'API Kubernetes (ingress) ainsi que de permettre aux pods d'accéder aux noeuds du Tailnet (egress). La configuration se fait en configurant les ressources existantes: Services et Ingresses

Dans notre cas, nous disposons déjà d'un Subnet router qui route tout le réseau du VPC. Il suffit donc que notre service soit exposé sur une IP privée.

L'API Kubernetes

Pour accéder à l'API Kubernetes il est nécessaire d'autoriser le Subnet router. Cela se fait en définissant la règle suivante pour le security group source.

 1module "eks" {
 2...
 3  cluster_security_group_additional_rules = {
 4    ingress_source_security_group_id = {
 5      description              = "Ingress from the Tailscale security group to the API server"
 6      protocol                 = "tcp"
 7      from_port                = 443
 8      to_port                  = 443
 9      type                     = "ingress"
10      source_security_group_id = data.aws_security_group.tailscale.id
11    }
12  }
13...
14}

Nous allons vérifier que l'API est bien accessible sur une IP privée.

 1CLUSTER_URL=$(TERM=dumb kubectl cluster-info | grep "Kubernetes control plane" | awk '{print $NF}')
 2
 3curl -s -o /dev/null -w '%{remote_ip}\n' ${CLUSTER_URL}
 410.228.244.167
 5
 6kubectl get ns
 7NAME                STATUS   AGE
 8cilium-secrets      Active   5m46s
 9crossplane-system   Active   4m1s
10default             Active   23m
11flux-system         Active   5m29s
12infrastructure      Active   4m1s
13...

Accéder aux services en privé

Un Service Kubernetes exposé est une resource AWS comme une autre 😉. Il faut juste s'assurer que ce service utilise bien une IP privée. Dans mon exemple j'utilise Gateway API pour configurer la répartition de charge du Clouder et je vous invite à lire mon précédent article sur le sujet.

Il suffirait donc de créer un NLB interne en s'assurant que le Service ait bien l'annotation service.beta.kubernetes.io/aws-load-balancer-scheme ayant pour valeur internal. Dans le cas de Gateway API, cela se fait via la clusterPolicy Kyverno.

1          metadata:
2            annotations:
3              external-dns.alpha.kubernetes.io/hostname: gitops-${cluster_name}.priv.${domain_name},grafana-${cluster_name}.priv.${domain_name}
4              service.beta.kubernetes.io/aws-load-balancer-scheme: "internal"
5              service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
6          spec:
7            loadBalancerClass: service.k8s.aws/nlb

Il y a cependant un prérequis supplémentaire car nous ne pouvons pas utiliser Let's Encrypt pour les certificats internes. J'ai donc généré une PKI interne qui génère des certificates auto-signés avec Cert-manager.

Ici je ne détaillerai pas le déploiement du cluster EKS, ni la configuration de Flux. Lorsque le cluster est créé et que toutes les ressources Kubernetes ont été réconcilié, nous avons un service qui est exposé via un LoadBalancer interne AWS.

1NLB_DOMAIN=$(kubectl get svc -n infrastructure cilium-gateway-platform -o jsonpath={.status.loadBalancer.ingress[0].hostname})
2dig +short ${NLB_DOMAIN}
310.0.33.5
410.0.26.228
510.0.9.183

Une entrée DNS est également créée automatiquement pour les services exposés et nous pouvons donc accéder en privé grâce à Tailscale.

1dig +short gitops-mycluster-0.priv.cloud.ogenki.io
210.0.9.183
310.0.26.228
410.0.33.5

💭 Dernières remarques

Il y a quelques temps, dans le cadre professionnel, j'ai mis en place Cloudflare Zero Trust. Je découvre ici que Tailscale présente de nombreuses similitudes avec cette solution. La décision entre les deux est loin d'être triviale et dépend grandement du contexte. Pour ma part, j'ai été particulièrement convaincu par la simplicité de mise en œuvre de Tailscale, répondant parfaitement à mon besoin d'accéder au réseau du Clouder. Bien entendu il existe d'autres solutions comme Teleport, qui offre une approche différente pour accéder à des ressources internes.

Cela dit, focalisons-nous sur Tailscale.

Une partie du code de Tailscale est open source, notamment le client qui est sous license BSD 3-Clause. La partie propriétaire concerne éssentiellement la plateforme de coordination. À noter qu'il existe une alternative open source nommée Headscale. Celle-ci est une initiative distincte qui n'a aucun lien avec la société Tailscale.

Pour un usage personnel, Tailscale est vraiment généreux, offrant un accès gratuit pour jusqu'à 100 appareils et 3 utilisateurs. Ceci-dit Tailscale est une option sérieuse à considérer en entreprise et il est important, selon moi, d'encourager ce type d'entreprises qui ont une politique open source claire et un produit de qualité.

Traductions: