SSH, mode utilisateur, TCP / IP et WireGuard

Quiconque héberge une application d'un fournisseur comme Fly.io (ci-après simplement Fly) peut bien avoir besoin de se connecter au serveur exécutant cette application via SSH.



Mais Fly est un peu comme un mouton noir parmi d'autres plates-formes similaires. Notre matériel fonctionne dans des centres de données disséminés dans le monde. Nos serveurs sont connectés à Internet via le réseau Anycast, et ils sont connectés les uns aux autres via le réseau WireGuard. Nous prenons les conteneurs Docker des utilisateurs et les transformons en microvirtuels Firecracker. Et lorsque nous avons commencé, nous l'avons fait pour donner à nos clients la possibilité d'exécuter des «applications de périphérie». Ces applications sont généralement des morceaux de code autonomes relativement petits et très sensibles aux performances du réseau. Par conséquent, ces extraits de code doivent s'exécuter sur des serveurs situés aussi près que possible des utilisateurs. Dans un tel environnement, la possibilité de se connecter au serveur via SSH n'est pas si importante.







Mais maintenant, tous nos clients n'utilisent pas Fly de cette manière. De nos jours, dans l'environnement Fly, vous pouvez facilement exécuter tout le code lié à une application. Nous avons simplifié la procédure de démarrage d'un ensemble de services dans un environnement en cluster. Ces services peuvent, à l'aide de canaux de communication sécurisés, interagir entre eux, ils peuvent stocker des données de manière permanente, ils peuvent, via le réseau WireGuard, communiquer avec leurs opérateurs. Si je continue l'histoire de notre système dans le même esprit, alors je devrai fournir des liens vers tous les documents que nous avons écrits au cours des deux derniers mois.



Mais, dans tous les cas, nous n'avions pas de support SSH normal.



Il est clair, bien sûr, que vous pouvez simplement créer un conteneur avec un service SSH auquel vous pouvez vous connecter via SSH. La plate-forme Fly prend en charge le travail avec les ports TCP courants (et les ports UDP également). Si le client, à l'aide du fichier fly.toml



, "informe" notre réseau Anycast de son étrange port SSH, le système organisera le routage de ses connexions SSH, après quoi tout fonctionnera comme il se doit.



Mais ceux qui créent des conteneurs ne le font généralement pas, et nous ne leur suggérons pas de le faire. En conséquence, nous avons équipé Fly du support SSH. Ce que nous avons fait est organisé d'une manière assez inhabituelle. Dans cet article, qui se compose de deux parties, j'en parlerai.



Partie 1: 6PN et Hallpass



J'ai beaucoup écritsur l'organisation des réseaux privés dans Fly. Pour résumer, il s'avère que ce que nous avons peut être comparé à une version IPv6 simplifiée des «clouds privés virtuels» GCP ou AWS. Nous appelons ce système 6PN. Lorsqu'une instance d'application (machine microvirtuelle Firecracker) est lancée dans Fly, nous attribuons un préfixe IPv6 spécial à cette instance. Plusieurs identifiants sont encodés dans le préfixe: l'identifiant de l'application, l'organisation propriétaire de l'application et les ressources matérielles sur lesquelles l'application s'exécute. Nous utilisons un peu de code eBPF pour acheminer statiquement ces paquets IPv6 sur notre réseau WireGuard interne et pour nous assurer que les clients ne peuvent pas se connecter aux systèmes des organisations avec lesquelles ils ne sont pas impliqués.



Vous pouvez également utiliser WireGuard pour relier les réseaux IPv6 privés que nous créons avec d'autres réseaux. Notre API est capable de créer des configurations WireGuard qui peuvent être utilisées, par exemple, sur des hôtes EC2 pour le proxy RDS Postgres . Ou, si nécessaire, vous pouvez utiliser des clients WireGuard (sous Windows, Linux ou macOS) pour connecter l'ordinateur de développement à votre propre réseau privé.



Vous savez probablement déjà à quoi je veux en venir.



Nous avons écrit un serveur SSH très petit et très simple dans Go appelé Hallpass. Il peut être comparé à "Hello, World!" Créé à l'aide de la bibliothèque Go x/crypto/ssh



... (Si je recommençais, j'utiliserais probablement simplement le package Glider Labs pour créer des serveurs SSH. En utilisant ce package, notre serveur serait littéralement un "Hello, World!" L' initialisation de toutes les instances de machines microvirtuelles Firecracker est effectuée et Hallpass est lancé avec la liaison à leurs adresses 6PN.



Si vous êtes en mesure de fonctionner sur le réseau 6PN de votre organisation (par exemple, via une connexion WireGuard), cela signifie que vous pouvez vous connecter à l'instance microvirtuelle à l'aide de Hallpass.



Il n'y a qu'un seul détail intéressant sur le fonctionnement de Hallpass. Il s'agit d'authentification. Les éléments d'infrastructure de notre réseau de production n'ont généralement pas d'accès direct à nos API ou à leurs bases de données sous-jacentes. Et les instances de Firecracker elles-mêmes, bien sûr, n'ont pas non plus cet accès. Cela entraîne certaines difficultés associées à la modification des paramètres de communication. Comment, par exemple, pouvez-vous répondre à la question du type de clés dont vous avez besoin pour vous connecter à certaines instances de machines microvirtuelles?



Nous avons trouvé une solution de contournement à ce problème en recourant aux certificats clients SSH. Au lieu d'avoir à gérer la remise des clés chaque fois qu'un utilisateur souhaite se connecter à partir d'un nouvel hôte, nous créons un certificat racine pour organiser cet utilisateur. La clé publique de ce certificat racine est hébergée dans notre système DNS privé et Hallpass contacte le DNS pour obtenir ce certificat chaque fois qu'une connexion est tentée. Notre API signe de nouveaux certificats pour les utilisateurs, ces certificats peuvent être utilisés pour se connecter au système.



Vous avez peut-être des questions sur cette solution. Par conséquent, je vais révéler quelques détails supplémentaires sur lui.



Parlons d'abord des certificats. Des décennies de folie X.509«Peut-être que le mot« certificat »vous donne un arrière-goût désagréable. Et je ne vous en veux pas. Mais les certificats doivent être utilisés lors de l'organisation des connexions SSH, car de tels certificats dans ce cas sont une bonne solution. Cependant, les certificats SSH ne sont pas des certificats X.509. Il utilise son propre format OpenSSH et, en général, rien de spécial ne peut être dit à propos de ces certificats. Comme tous les autres certificats, ils ont une "date d'expiration", ce qui vous permet de créer des clés de courte durée (et c'est presque toujours, exactement ce dont vous avez besoin). Et, bien sûr, ils vous permettent d'attribuer une clé publique à un groupe entier de serveurs, ce qui peut autoriser un nombre arbitraire de clés privées. Il n'est pas nécessaire de constamment mettre à jour les serveurs correspondants.



Vient ensuite notre API et la signature de certificats. Bien! Nous sommes très prudents, mais ces certificats sont généralement aussi sécurisés que les jetons d'accès Fly. Pour le moment, les certificats ne peuvent pas être mieux protégés que les jetons, car le jeton permet le déploiement de nouvelles versions de conteneurs d'applications. Travailler avec Web PKI X.509 CA implique de nombreuses formalités. Nous faisons sans eux.



Et enfin, notre DNS. Elle, je suis d'accord, ressemble à un non-sens complet. Mais ce n'est vraiment pas si grave. Chaque hôte exécutant des instances microvirtuelles Firecracker exécute une version locale de notre serveur DNS privé (un petit programme écrit en Rust). Le code eBPF garantit que les machines Firecracker ne peuvent interagir qu'avec ce serveur DNS, en s'y référant à partir de l'adresse 6PN de leur serveur. (D'un point de vue technique, un utilisateur peut uniquement effectuer des requêtes auprès de l'API DNS privée de ce serveur, et toutes les requêtes des autres utilisateurs seront traitées de manière récursive.) Un serveur DNS peut (je sais que cela semble inhabituel) identifier de manière fiable une organisation en analysant les demandes d'adresses IP source. En général, c'est ainsi que nous travaillons.



Tout cela se passe dans les profondeurs de notre système, les utilisateurs ne peuvent pas voir tout cela. Les utilisateurs n'ont vu qu'une commande flyctl ssh issue -a



qui demandait un nouveau certificat à notre API, puis l'ont passé à l'agent SSH local, après quoi les connexions SSH, en général, se sont avérées opérationnelles. Tout cela a été assez bien arrangé. Mais toute entreprise peut toujours être menée avec plus de précision qu'auparavant.



Partie 2: Travailler sur un réseau WireGuard à partir du mode utilisateur en utilisant TCP / IP



Il y a un problème avec le schéma ci-dessus d'utilisation de SSH, c'est que ce n'est pas tout le monde qui a installé WireGuard. Cependant, le programme correspondant doit être installé par tout le monde. WireGuard est une excellente technologie qui aide beaucoup à gérer les applications exécutées sur la plate-forme Fly. Quoi qu'il en soit, certains de nos utilisateurs ne disposent pas de WireGuard.



Certes, ces utilisateurs doivent également travailler avec leurs systèmes via SSH.



À première vue, le fait que quelqu'un n'ait pas installé WireGuard peut sembler un obstacle insurmontable. Comment fonctionne WireGuard? Une nouvelle interface réseau est créée sur l'ordinateur de l'utilisateur. Il s'agit soit d'une interface WireGuard au niveau du noyau (sous Linux), soit d'un tunnel auquel est attaché un service WireGuard en mode utilisateur (dans tous les autres systèmes d'exploitation). Sans cette interface réseau, vous ne pouvez pas travailler avec le réseau WireGuard.



Mais si vous regardez WireGuard sous le bon angle, vous pouvez voir que, d'un point de vue technique, ce n'est pas le cas. À savoir, des privilèges au niveau du système d'exploitation sont nécessaires pour configurer une nouvelle interface réseau. Mais pour envoyer des paquets à 51820/udp



aucun privilège n'est nécessaire. Tout ce qui est nécessaire pour faire fonctionner le protocole WireGuard peut être démarré en tant que processus non privilégié s'exécutant en mode utilisateur. C'est ainsi que fonctionne le package wireguard-go .



Cela vous permettra uniquement de suivre la procédure d'établissement de liaison WireGuard. Mais en même temps, nous ne parlons pas d'échange d'informations avec les nœuds du réseau WireGuard, car vous ne pouvez pas simplement prendre et envoyer des données arbitraires à un autre système connecté à ce réseau. Un tel système écoute les paquets qui seraient normalement transmis sur les réseaux TCP / IP. Les outils système standard qui prennent en charge les sockets UDP ne sont d'aucune utilité pour établir une connexion TCP à l'aide de ces sockets.



Serait-il difficile d'écrire un petit morceau de code qui active TCP en mode utilisateur, uniquement conçu pour prendre en charge la communication sur le réseau WireGuard, toujours en mode utilisateur? Un tel code permettrait aux utilisateurs de Fly de se connecter à leurs systèmes via SSH sans avoir à installer le logiciel qui alimente WireGuard.



J'ai été imprudent en discutant de tout cela sur la chaîne Slack sur laquelle était Jason Donenfeld. À savoir, après avoir réfléchi à voix haute, je suis allé me ​​coucher. Quand je me suis réveillé, Jason avait déjà implémenté tout cela en utilisant gVisor et inclus dans la bibliothèque WireGuard.



La chose la plus intéressante ici est gVisor. Nous avons déjà écrit à ce sujet ... Si quelqu'un ne sait pas, gVisor est essentiellement un système d'exploitation Linux de l'espace utilisateur, Linux implémenté dans Golang, utilisé en remplacement runc



de l'exécution de conteneurs. C'est en fait un projet complètement insensé. Et si vous l'utilisez, alors je suppose que vous pouvez en parler fièrement aux autres, car c'est juste une chose magnifique. Dans ses profondeurs, il existe une implémentation TCP / IP complète, écrite en Go, qui fonctionne sur des données d'entrée et de sortie représentées comme des tampons ordinaires []byte



.



Ensuite, quelques tweets ont été tweetés, puis quelques heures plus tard, j'ai reçu un très bon e-mail de Ben Barkert... Ben avait déjà travaillé sur diverses tâches liées au sous-système de réseau gVisor, il était intéressé par ce sur quoi nous travaillions, il voulait savoir si nous aimerions coopérer avec lui. Nous avons aimé son idée de travailler ensemble sur ce projet. Et maintenant, sans entrer dans les détails, nous avons une implémentation SSH basée sur des certificats qui passe par l'implémentation TCP / IP de gVisor en mode utilisateur. Tout cela interagit avec le réseau WireGuard via un package de mode personnalisé wireguard-go



. Et enfin, cette chose est intégrée au flyctl



.



Pour utiliser SSH en utilisant flyctl



- entrez simplement une commande comme celle-ci:



flyctl ssh shell personal dogmatic-potato-342.internal

      
      





Et maintenant, pour que vous puissiez réaliser l'incroyable de ce qui se passe, je vais vous parler un peu de cette commande. Donc - dogmatic-potato-342.internal



est un nom DNS interne qui n'est résolu que par un serveur DNS privé sur le réseau 6PN. Tout cela est efficace car dans le mode, l' ssh shell



utilitaire flyctl



utilise le mode utilisateur de la pile TCP / IP gVisor. Mais il n'y a pas de code dans gVisor pour effectuer une recherche DNS. C'est juste une bibliothèque Go standard que nous avons trompée en y glissant notre interface TCP / IP spéciale.



Flyctl



, au fait, c'est un projet open source(Il devrait en être ainsi, car les clients doivent l'utiliser sur leurs propres ordinateurs sur lesquels ils sont engagés dans le développement). Par conséquent, si vous êtes intéressé, vous pouvez simplement lire son code. Ben a écrit un bon code dans le dossier pkg . Et le reste du code, horrible, j'ai écrit. En Go, fournir des communications IP sur le réseau WireGuard est étonnamment simple. Si vous avez déjà fait de la programmation TCP / IP de bas niveau, vous pourriez trouver cette simplicité incroyable. Les objets de la pile TCP gVisor se connectent directement au code réseau de la bibliothèque standard.



Jetez un œil à ce code:



tunDev, gNet, err := netstack.CreateNetTUN(localIPs, []net.IP{dnsIP}, mtu)
if err != nil {
    return nil, err
}

// ...

wgDev := device.NewDevice(tunDev, device.NewLogger(cfg.LogLevel, "(fly-ssh) "))

      
      





CreateNetTUN



Est une partie wireguard-go



. C'est là que les capacités de gVisor sont utilisées. Tout d'abord, nous avons à notre disposition un dispositif tunnel synthétique qui peut être utilisé pour lire et écrire des paquets ordinaires qui fournissent un fonctionnement WireGuard. Deuxièmement, nous avons la fonction net.Dialer , un wrapper pour gVisor, qui peut être utilisé dans le code Go et à travers lui interagir avec le réseau WireGuard correspondant.



C'est tout? En général, oui. Par exemple, voici comment nous utilisons ces mécanismes pour travailler avec DNS:



resolv: &net.Resolver{
    PreferGo: true,
    Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
        return gNet.DialContext(ctx, network, net.JoinHostPort(dnsIP.String(), "53"))
    },
},

      
      





Il s'agit d'un code réseau normal écrit en Go. En général, cela s'est bien passé.



De toute évidence, tout le monde devrait le faire.



Grâce à quelques centaines de lignes de code (c'est - à part le code d'implémentation en mode utilisateur Linux que nous obtenons de gVisor; mais que faire - il n'y a pas d'échappatoire aux dépendances), vous pouvez obtenir un nouveau réseau avec cryptographie authentification à votre disposition. Un réseau accessible à tout moment et depuis presque tous les programmes.



Il est clair qu'un tel réseau est nettement plus lent que celui basé sur l'implémentation TCP / IP de base. Mais est-ce souvent vraiment important? Et, en particulier, a-t-il souvent un sens lors de la résolution de problèmes périodiques, pour la solution desquels étranges, inconnus de quoi, les tunnels TLS sont généralement construits? Lorsque la vitesse compte, vous pouvez simplement passer à l'implémentation WireGuard standard.



En tout cas, ce que j'ai dit a résolu notre énorme problème. Après tout, ce système convient non seulement pour organiser le travail de SSH. Nous hébergeons également des bases de données Postgres. C'est très pratique lorsqu'il est possible, en exécutant une simple commande, d'ouvrir littéralement un shell de n'importe où psql



, qu'il soit possible, au bon moment, d'installer WireGuard pour macOS.



Utilisez-vous WireGuard?






All Articles