Salut à tous !

Aujourd'hui j'ai été confronté à un problème que je vais essayer d'expliquer le plus simplement possible. Je n'ai pas trouvé la solution sur mondedie mais j'ai peut être mal cherché. Si ce que je vais dire là fait doublon avec un autre sujet je m'en excuse par avance.

Avant propos
Je me suis lancé récemment sur docker (je suis pas fan mais j'admets quelques aspects pratiques..). Donc mes connaissances sur le sujet sont maigres et très certainement perfectibles.

Le contexte
Serveur : debian 10
Parefeu : ufw / iptables (pas taper..)
Dans mon cas de figure tous les ports en entrées sont bloqués hormis les ports 22, 80, 81, 443 que j'ai ouvert volontairement. Et en sortie tout est ouvert.

J'ai donc mis en place un fichier docker-compose.yml comprenant nginx proxy manager et adminer. Voici le contenu de celui-ci pour les curieux.

version: "3"
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: always
    ports:
      - '80:80'
      - '443:443'
      - '81:81'
    networks:
      - webservices
    volumes:
      - ./config.json:/app/config/production.json
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - db
  db:
    image: jc21/mariadb-aria:10.4
    container_name: mariadb
    restart: always
    networks:
      - webservices
    environment:
      MYSQL_ROOT_PASSWORD: 'xxx'
      MYSQL_DATABASE: 'xxx'
      MYSQL_USER: 'xxx'
      MYSQL_PASSWORD: 'xxx'
    volumes:
      - ./data/mysql:/var/lib/mysql
  adminer:
    image: adminer
    container_name: adminer
    restart: always
    ports:
      - 8080:8080
    networks:
      webservices:
        aliases:
          - adminer
networks:
  webservices:

l'objet ici n'étant pas de critiquer le contenu de ce docker-compose.yml mais si vous avez des conseils ou autre je suis preneur bien évidemment

Rien de bien difficile jusque là.

Au lancement du docker-compose, je m'empresse de tester l'accès à http://ip_du_serveur:8080 qui va, en toute logique, m'emmener sur adminer. SAUF QUE, si vous avez lu le début du contexte, je n'ai pas ouvert le port 8080. MAIS (vous qui êtes fan de docker vous le savez et riez déjà), DOCKER est un peu le maître à bord et outrepasse les règles iptables standards. Enfin, je ne sais pas par quelle magie vaudou ce fichu port 8080 est accessible et ce n'est pas non plus mon but ici de vous expliquer la théorie du fonctionnement de la baleine. Quoi qu'il en soit, mon adminer est bien accessible via un port que je n'ai pas autorisé et cela me gêne.

Après quelques recherches je m'aperçois qu'effectivement les conteneurs docker ne sont pas des services d'hôte mais qu'ils ont leur propre réseau virtuel (sauf que ça, avant de me lancer, je l'avais plus ou moins compris). Du coup, le trafic routé n'est pas géré par l'input mais par le forward. DE PLUS, docker s'amuse à créer plein de règles iptables (moi ça m'a cassé la tête) pour permettre une bonne communication entre les conteneurs. Cela parait logique quand on connait un peu l'engin ou qu'on a de bonnes notions de réseau mais quand on débarque un peu au milieu de l'océan comme moi, qu'on s'embarque sur la baleine, on manque de se noyer à chaque manipulation.

Bref !

L'objectif
Mon objectif était donc simple : trouver un moyen de faire en sorte que docker respecte mes fichues règles écrites via ufw.

Le mauvais tips
Il existe plusieurs façons de contourner le "problème" (qui n'en est pas un, je sais). Voici ce qu'il ne faut pas faire 🙂
Il est possible d'ajouter des options à DOCKER, dès qu'il se lance, comme lui ajouter des dns ou lui dire qu'on veut gérer nous même nos règles iptables.
J'ai donc était touché au fichier de lancement de docker en y ajoutant ce qui suit :

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --iptables=true

je ne dis volontairement pas ici où j'ai été modifié ça, même si vous voyez bien que c'est dans le .service de docker, puisque c'est LA CHOSE à ne pas faire

J'ai donc ajouté en bout de ligne --iptables=false, ce qui a pour bénéfice de faire respecter les règles que j'ai écris via ufw à docker mais qui a pour effet de bord de faire perdre la connexion internet à mes conteneurs. J'entends par là qu'ils ne peuvent plus voir l'extérieur, donc un ping vers mondedie.fr renvoie ce bon vieux "ping: bad address mondedie.fr" (qui m'aura tout de même fait perdre une bonne heure).
Alors bien évidemment, n'étant pas un pro docker je ne me suis pas rendu compte de suite de ma stupidité. En fait c'est en voulant générer un certificat ssl via nginx proxy manager que je me suis rendu compte que.. d'une part ça ne fonctionnait pas et que d'autre part j'allais passer un mauvais moment.
J'ai pesté pendant longtemps sur nginx proxy manager, j'ai fouillé sur leur github tout un moment j'étais même prêt à écrire une issue et puis... j'ai réfléchi (ce sont des choses qui arrivent mais ça fait très mal à la tête donc pas trop souvent non plus).
Ma réflexion a donc été : peut être que le conteneur n'arrive pas à joindre lets encrypt ! Non ?!

J'ai donc viré le param dont je parle précédemment (--iptables) et la magie opère, mon certificat ssl se génère à merveille.

Le bon tips
Reste donc à trouver comment faire pour :

  1. faire respecter le code de la route à une baleine
  2. rendre internet accessible en pleine mer
  3. déconfiner sans se faire chopper par le virus (je vous laisse voir avec le premier ministre pour ce point là)

Voici donc la solution, qui va se passer à la quasi toute fin du fichier /etc/ufw/after.rules, juste avant le COMMIT

:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

Puis on redémarre ufw et si ça ne fonctionne pas on redémarre le serveur (dans mon cas j'ai été obligé de redémarrer le serveur, ne me demandez pas pourquoi).

Cela aura pour effet de bloquer l'accès aux ports ouverts par les conteneurs docker et de permettre à ces derniers de communiquer avec le monde extérieur.

Alors bien évidemment, cela n'est pas à recopier tel quel et vous aurez peut être à adapter en fonction des ip de votre réseau local. Je n'ai rien inventé, j'ai trouvé la solution ici et je vous invite à aller sur ce lien pour obtenir de plus complètes explications sur le fonctionnement (sauf si vous savez lire tout ce charabia bien entendu).

Et là vous allez me dire qu'il manque quelque chose. Oui vous avez raison, il manque dorénavant de quoi faire entrer nos invités sur nos conteneurs dockers. Si vous souhaitez par exemple rendre de nouveau disponible votre conteneur nginx proxy manager qui tourne sur le port 81, je vous invite à saisir ceux-ci sans copier/coller (parce que c'est pas très compliqué cette fois-ci) :

ufw route allow proto tcp from any to any port 81

Alors bien évidemment les plus paranos d'entre vous vont dire "oh mer il et fou il a ouvert le port 81 en accès à tous les conteneurs". Et pourquoi pas après tout ? Bien évidemment je n'ai pas fais cela, c'est juste un exemple. Sur le lien que je cite plus haut vous avez de quoi écrire des règles un peu moins open si vous le désirez.

Conclusion
Vive les baleines mais attention ! “Un sous-marin, pour une baleine, c'est un gros suppositoire.”

Merci pour cette info, je prends.
Pour ma part, j'ai fait plus simple : un seul container expose ses ports (le reverse proxy, soit traefik, soit nginx-reverse-proxy), et AUCUN autre container n'expose ses ports de façon publique. C'est le reverse proxy qui fait le routage.
Dans le cas improbable où l'hôte aurait besoin d'accéder au container, au lieu de faire

  ports:
  - 8080:8080

Je fais

  ports:
  - 172.17.0.1:8080:8080

ce qui fait que le port est exposé uniquement sur l'interface par défaut de docker, et non routé sur toutes les interfaces.

    Merrick c'est en effet une excellente alternative à tout mon blabla car ça a le mérite de remplir l'objectif souhaité et de ne pas toucher aux règles iptables 😁. Je vais tester ça merci.

    Ouais c'est plus simple mais c'est cool aussi d'expliquer comment interagir avec IPtables et Docker 🙂

    Répondre…