Comment sécuriser vos flux sortants en filtrant l'egress de votre serveur avec un Firewall

le mercredi 3 mars 2021

Bearstech | Dernière mise à jour : mercredi 3 mars 2021

Gérer une liste d'invité pouvant accéder à l'extérieur ou comment configurer son par-feu avec une liste de noms de domaine.

Notre prochain webinar

Le réseau de vos services

Les services que vous déployez vont utiliser du réseau, par définition. Dans un sens pour servir les utilisateurs, et dans l'autre sens pour utiliser lui même des services.

Pour limiter les surprises, vous allez appliquer les principes de l'autorisation minimale et de la défense en profondeur.

Ingress

On appelle ingress le flot entrant vers un serveur.

Ça fait longtemps que l'on déploie ses applications derrière un service spécialisé qui lui, affrontera les tumultes du grand Internet. Ce service (en level 7) peut être dans la place (Haproxy, Traefik, Nginx…) ou externalisé, en SAAS, comme Cloudflare. Les protections bas niveau (en level 3) comme l'anti DDOS sont maintenant considérés comme allant de soi.

Grâce à Let's Encrypt, le chiffrement via TLS est maintenant largement standardisé.

HTTP est fort pratique pour router plusieurs services derrière une seule paire IP/port. SNI permet d'avoir la même possibilité de routage à partir du nom de domaine que le virtualhosting d'HTTP. SNI est une extension TLS qui permet d'annoncer le domaine (en clair) avant le reste de la connexion qui sera chiffré. Ainsi, il est possible de router le trafic, sans pour autant être capable de le lire. Connaitre le nom de domaine peut être considéré comme indiscret, et une amélioration de SNI a été proposée, ESNI, mais cette technologie est peu diffusée, et certains pays la bloquent (Russie et Chine). SNI, pourtant indépendant d'HTTP reste malheureusement peu usité dans d'autres protocoles. C'est ballot, des proxys comme Traefik savent maintenant router du SNI sans exiger de l'HTTP.

Le monde UDP est plus diversifié, avec un usage plus massif des ports, et le chiffrage et autres protections systématiques sont plus compliqués à mettre en place.

Avoir un service dédié pour gérer le flot entrant est maintenant standard, les proxy en SAAS, souvent lié à une offre de CDN sont maintenant classique, et le monde du Cloud pousse à la notion de bastion et LAN privé.

Il est rare maintenant qu'une application ne s'appuie pas sur des services métiers, et je parle juste des bases de données ou de cache, avant même d'évoquer les microservices. Ces services métiers n'ont pas vocation à être accessibles depuis le grand Internet, ou alors vous allez très vite avoir des soucis.

Egress

Tout le monde fait attention à son Ingress (ou alors vous cherchez à passer chez Zataz), mais quid de son Egress, le flot sortant?

Les serveurs sans accès au réseau sont rapidement pénibles à utiliser : sans son apt-get update on n'est pas grand-chose. Et même si on a une procédure propre pour redéployer régulièrement des images fraiches de ses services (que ce soit des images disque comme le fait Netflix, ou des images de conteneurs), il est tout à fait légitime qu'un service utilise un service distant.

L'arrivée fracassante de Kubernetes a permis de changer d'échelle de problèmes avec le réseau. Avec k8s, vous allez avoir différentes applications (composés de multiples services) qui vont se partager un pool de ressource, permettant d'avoir de la scalabilité, de la résilience (quand un service tombe, un service sort de l'ombre à sa place), et meilleure utilisation des ressources matérielles (en dépassant les 10% de CPU de moyenne sur l'année). Comme les services sont dispersés sur plusieurs machines, l'utilisation, et la maitrise du réseau, va devenir très importante. Chaque application va vouloir avoir son propre LAN, et comme chaque machine va héberger des conteneurs de différentes applications, ça va tricoter sec.

Pour garantir la bonne utilisation des services (load balancing, chiffrage, authentification en mTLS, reprise sur incident, compression, métriques…) il peut être tentant d'utiliser un proxy ambassadeur comme Envoy, mais techniquement, on appelle ça un man in the middle, il va avoir besoin d'informations indiscrètes pour prendre ses décisions.

Comme il existe beaucoup d'approches pour gérer des LANs virtuels, la CNCF a abstrait ça avec la Container Network Interface. CNI permet de profiter de l'abstraction réseau de son fournisseur sans trop se soucier des choix et détails d'implémentation, ce qui est un argument de poids pour les gros du Cloud.

Cette utilisation frénétique du réseau a permis de valider ce qui marchait et marchait moins bien dans Linux, et de le faire évoluer pour que tout marche bien.

iptables s'est pris des cailloux, ce qui met en avant nftables qui essaye de lui succéder, mais surtout à eBPF de se généraliser dans le kernel, et dans le réseau en particulier avec XDP comme game changer comme on dit en Amérique.

XDP permet d'ajouter du code métier sur une carte ethernet (réelle ou virtuelle), et d'aller bien plus loin dans ce qui est possible pour filtrer et router du réseau, avec une vraie granularité, pas juste le kernel d'une VM.

Cilium et IOVisor se sont rapidement positionnés comme les caïds de l'eBPF.

Cilium se chargeant du réseau, avec une intégration fine de XDP, Envoy, CoreDNS, du eBPF pour voir ce qui se passe (et qui essaye de truander) et surtout pour gérer le réseau.

IOVisor se positionne sur le monitoring et l'analyse de performance.

Je veux juste filtrer ce qui sort

Voilà, on a une belle tempête d'acronymes, avec des technologies prometteuses et ambitieuses. Mais moi, ce que je veux, c'est ne permettre à un programme de n'avoir accès qu'à une liste de nom de domaines (avec des patterns, pour simplifier).

Cilium propose fièrement un filtrage de l'egress à base de whitelisting de noms de domaine. Mais est-il nécessaire de dégainer le roi de l'eBPF et k8s pour filtrer ce qui sort ?

iptables

iptables travaille au level 4, et n'a pas idée de ce qu'est un nom de domaine. Il est légitime pour un service de changer d'IP pour peu qu'il garde son nom de domaine, et certains se permettent d'avoir plusieurs IPs pour un seul nom (pour du loadbalancing, ou de la résolution géographique).

DNS

Pour filtrer par nom et blanchir des IPs (whitelister en VO), il faut donc utiliser la résolution de noms, et pour ça le plus simple est un DNS. Fournir un DNS qui ment est moralement douteux (vous savez, quand vous mettez des 127.0.0.1 dans votre /etc/hosts pour empêcher des applications de téléphoner à la maison), et il serait toujours possible de truander avec des appels illégitimes avec une IP en dur, ou en utilisant un DoH sur un domaine légitime.

Avec l'aide d'un DNS, il sera possible d'avoir un nom à filtrer, et la résolution de son IP, puis d'ouvrir l'accès à cette IP si le nom est accepté.

L'autre conséquence de k8s sur l'écosystème serveur est le retour en grâce du DNS dans le LAN, car la flemme et la facilité de travailler avec des IP lui avaient fait beaucoup de tort. Au début, dnsmasq avait été retenu, pour finalement créer CoreDNS, qui apporte une architecture contemporaine à base de plugin et beaucoup de dynamisme.

dnstap

CoreDNS a le bon goût de gérer dnstap, un protocole robuste pour écouter les échanges DNS sans pour autant interférer avec lui. Dnstap est implémenté par une ribambelle de serveurs (knot, unbound, bind, powerdns…), ce qui lui confère le statut de quasi-standard.

Network namespace

Pour ne pas brimer une machine complète, mais juste un service (ou une grappe de services), il faut utiliser un network namespace pour que le service ne voie qu'un seul réseau, et qu'une seule passerelle. Vous pouvez le faire avec ip netns ou confier cette tâche à Docker.

Stratégie

La stratégie est donc simple.

Il existe bien des outils pour informer des demandes d'ouverture des connections TCP demandées au kernel, grâce à eBPF, comme tcpconnect, qui va même faire le lien avec la résolution de nom, port et pid. Mais tcpconnect ne permet que de voir ce qu'il se passe, comme un netstat en temps réel, mais il ne va rien faire, et encore moins bloquer.

Il faut donc travailler au niveau du réseau, pas des syscalls.

Le service est dans un sous-réseau privé, son /etc/resolv.conf pointe sur un DNS qui fait juste relais, et qui cafte les demandes via dnstap.

Un service va écouter les demandes de résolution de nom (les CNAME). En fonction du nom demandé, cet outil va ouvrir (ou pas) via l'IP de destination, un passage à travers le par-feu.

Implémentation avec on-his-name

Côté implémentation, nous avons choisi, comme à notre habitude golang et des bibliothèques éprouvées sur des applications majeures. Au niveau DNS, c'est golang-dnstap qui permettra d'écouter les requêtes qui passent par CoreDNS.

Il existe un daemon firewalld, géré par CNI mais qui a envie de causer sur DBUS pour paramétrer son firewall sur un serveur? firewalld ne semble pas avoir beaucoup de fan hors RedHat (qui en assure le développement et la maintenance).

Nous allons rester sur le classique iptables avec la librairie go-iptables quie permet d'avoir rapidement un prototype fonctionnel (au moins pour GNU/Linux).

Pour éviter de perturber l'ensemble du daemon, le filtrage s'appliquera seulement sur les conteneurs attachés à un réseau dédié

docker network create on-his-name

Qui va créer, côté hôte, un bridge (le comportement par défaut de Docker)

18: br-e65ade98d3cb: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:ad:0d:b0:ae brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-e65ade98d3cb
       valid_lft forever preferred_lft forever
    inet6 fe80::42:adff:fe0d:b0ae/64 scope link
       valid_lft forever preferred_lft forever

IPTables

Côté Docker, les travaux sur les firewalls sont en friches. Docker est intrasèquement très lié à iptables via libnetwork et la tendance ne semble pas s'inverser. Malgré tout et grâce à Docker 17.06 des nouvelles chaînes iptables DOCKER et DOCKER-USER permettent d'isoler et interactions entre le daemon et le par-feu au sein du système hôte.

DOCKER est la chaîne réservée au deamon et il est préférable de ne pas y toucher. DOCKER-USER est celle dédiée aux règles utilisateurs. Celles-ci seront vérifiées en premier lors du transfert de paquets : c'est gagné.

Pour appliquer une whitelist, il faut nécessairement, dans un premier temps, bloquer tout le trafic. Le plus simple est de et de commencer à vider la chaîne pour y ajouter, en fin de table les règles qui bloque l'intégralité des flux passant par le bridge créé à l'étape précédente :

iptables -F DOCKER-USER
iptables -A DOCKER-USER -i br-e65ade98d3cb DROP

Avec la commande docker run et l'option --network il est possible d'isoler un conteneur dans un sous réseau dédié (ce que fait systématiquement docker-compose)

docker run --network=on-his-name --dns="172.18.0.1" --rm bearstech/debian:buster curl -I -m 2 https://golang.org
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (7) Failed to connect to golang.org port 443: Connection timed out
...

Comme prévu, le trafic est bloqué par iptables.

Démonstration

Par défaut, côté daemon Docker, les conteneurs héritent de la configuration DNS de l'hôte, piquée dans /etc/resolv.conf. Ce comportement change avec l'utilisation de l'option --network. C'est maintenant le résolveur interne du daemon qui se charge d'effectuer les requêtes DNS pour le conteneur.

Pour aller encore plus loin, il est possible de spécifier la configuration DNS par conteneur. C'est celui-ci qui sera interrogé par le daemon lors d'une requête DNS : Bingo !

Pour préparer le terrain, il faut dans un premier temps démarrer on-his-name

sudo SOCKET_UID=$(id -u) DOCKER_BRIDGE_NAME=on-his-name LISTEN=./tap.sock ./bin/on-his-name "bearstech.com."

Ici, les seuls flux autorisés seront http et https vers bearstech.com

on-his-name nécessite les droits root pour modifier le contenu des tables iptables.

Pour la résolution DNS, c'est CoreDNS qui servira la démo, l'interface d'écoute est le bridge lié au network on-his-name, pour la démo il se contente de transférer les requêtes vers 8.8.8.8, tout est décrit dans la configuration ci-dessous :

.:53 {
    bind 172.18.0.1
    forward . 8.8.8.8
    log
    errors
    cache
    dnstap ./tap.sock full
}

le fichier tap.sock étant la socket unix qui permet de cafter les requêtes DNS de CoreDNS vers on-his-name. L'option full pour dnstap est très importante : elle permet d'avoir l'intégralité du contenu de la requête DNS.

Une fois la stack en place, jouons avec différents conteneurs, ci-dessous l'état du par-feu au démarrage de on-his-name

Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DROP       all  --  br-e65ade98d3cb *       0.0.0.0/0            0.0.0.0/0
    0     0 DROP       all  --  *      br-e65ade98d3cb  0.0.0.0/0            0.0.0.0/0

En passant par le réseau Docker standard le trafic n'est pas perturbé :

docker run --rm bearstech/debian:buster curl -I -m 1 https://golang.org
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
HTTP/2 200
date: Tue, 02 Mar 2021 14:57:03 GMT
content-type: text/html; charset=utf-8
vary: Accept-Encoding
strict-transport-security: max-age=31536000; includeSubDomains; preload
via: 1.1 google
...

En spécifiant le réseau on-his-name et le DNS local à l'exécution du conteneur avec un domaine interdit

docker run -t -i --network=on-his-name --dns="172.18.0.1" --rm bearstech/debian:buster curl -I -m 2 https://golang.org
curl: (28) Connection timed out after 2000 milliseconds

Ça coince ! Et c'est visible dans les compteurs affichés par iptables :

Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    1    60 DROP       all  --  br-e65ade98d3cb *       0.0.0.0/0            0.0.0.0/0

Toutefois si on lance une requête vers un domaine autorisé, dans le réseau isolé :

docker run -t -i --network=on-his-name --dns="172.18.0.1" --rm bearstech/debian:buster curl -I -m 2 https://bearstech.com
HTTP/2 200
server: nginx/1.14.2
date: Tue, 02 Mar 2021 15:01:00 GMT
content-type: text/html; charset=UTF-8
content-length: 37776
last-modified: Fri, 26 Feb 2021 11:51:52 GMT
etag: "6038e0d8-9390"

Le trafic passe et les règles iptables sont dynamiquement mises à jour :

Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination
   15  1690 ACCEPT     tcp  --  br-e65ade98d3cb *       0.0.0.0/0            78.40.125.59         tcp dpt:443
    0     0 ACCEPT     tcp  --  br-e65ade98d3cb *       0.0.0.0/0            78.40.125.59         tcp dpt:80
   20  1200 DROP       all  --  br-e65ade98d3cb *       0.0.0.0/0            0.0.0.0/0

Les sources sont sur Github : on-his-name.

Conclusion

Il est possible avec des outils Linux standard (netns, dns, dnstap, iptables…) de mettre en place un filtrage de l'egress d'un service. Quand on est déjà dans un contexte de conteneur, c'est beaucoup plus simple à mettre en place (c'est quand même pour ça qu'ils ont été créés). Les promesses de XDP donnent clairement envie d'explorer au delà d'iptables avec xdp-filter par exemple.

Service Audit de Performance web docker

Bearstech vous propose ses services Audit de Performance web docker

Découvrir ce service

Partager cet article

Flux RSS

flux rss

Partager cet article :