• Docker
  • Bonnes pratiques pour un hôte Docker en "prod"

Banip Merci pour l'info, j'avais effectivement vu quelque chose dans le genre. Par contre j'ai du mal à saisir l'interêt de le passer derrière Traefik si c'est pour ouvrir quand même un port supplémentaire ?!

    Salut,

    Pour une utilisation en production, j'aurais tendance à dire :

    • Utilisation des user-namespace, comme ça si dans le conteneur tu es root, sur l'hôte tu ne l'ai pas. Cela complexifie la gestion des volumes
    • éviter au maximum le partage du socket docker
    • Utiliser ces propres images, ou des images de confiance (linuxserver, binami, mondedie etc ...)
    • Separer les reseaux des conteneurs au strict minimum, si un conteneur n'a pas besoin du net, on lui mets un réseau que local
    • Utiliser iptables au lieu de docker-proxy
    • Limiter les ressources de chaque conteneur

    Pour moi c'est le minimum, après y'a des tweaks :

    • Utilisation d'un autre runtime comme firecracker ou kata-container, voir gvisor même si c'est un ptrace degueulasse
    • Utiliser docker en rootless voir podman qui est aussi daemonless

    Et je dirais que c'est deja pas mal.

      xataz Merci à toi 🙂 pour le minimum, tu as traité ces points dans ton tuto sur Docker ? Dans un autre ? Pas tous ?
      Il faut que je regarde les user-namespace que je ne connais pas et que je regarde ce qu'il faut configurer sur iptables que je ne connais pas très bien.
      Le partage du socket docker il faut que je regarde les containers qui l'utilisent, je n'ai que traefik et portainer il me semble.
      J'utilise les images officielles ou celles de linuxserver le plus souvent.
      Tous les containers derrière traefik sont sur un réseau traefik (+ un réseau mariadb pour les containers avec une BDD) et mon VPN est sur un réseau vpn.

      Merrick Merci 😉 j'ai lu le tuto mais par contre je n'ai rien trouvé concernant les user-namespaces et iptables

      C'est dans la configuration du daemon pour les deux.
      Tout est bien expliquer dans la doc officiel.

      Pour iptables, sur debian y'a un tweak à faire pour modifier l'alternative iptables-legacy au lieu de iptables-nftable

        NicCo Je ne comprends pas de quel port supplémentaire tu parles ? J'ai un teamspeak derrière 9987 et l'autre derrière 9988.

        Traefik pour une infra de prod c'est une des sécurisations évidentes, on masque les ports qui n'ont pas besoin d'être exposés de l’hôte vers le docker, dans mon exemple les ports 9987 et 9988 ne sont pas vraiment ouverts :

        root@machine:~# netstat -ntpl |egrep "9987"
        root@machine:~# netstat -ntpl |egrep "9988"
        root@machine:~#

        C'est simplement une entrée SRV dans le DNS qui indique que ts.domain.tld et ts2.domain.tld mènent aux ports 9987 et 9988 et c'est traefik qui dirige les requêtes vers le docker en question.

          Banip Merci, j'ai lu trop vite tes fichiers et j'ai cru que tu avais exposé les ports. Je vais regarder ta config, merci 😉

          xataz Merci, je vais regarder ça. Du coup avec ce type de config tu n'ouvres aucun port ? Même le 80/443 ?

          xataz J'ai lu la doc officielle et j'ai mis en place les user-namespaces (soit c'est hyper simple, soit mon environnement docker est simple, soit j'ai oublié de faire quelque chose). J'ai bien vérifié et le root dans le container ne permet plus de modifier les fichiers nécessitant une élévation sur l'hôte. Pour le moment je ne rencontre pas de problèmes, qu'est-ce qui est plus complexe sur la gestion des volumes (pour voir si je n'ai rien oublié) ? Je peux toujours utiliser un dossier docker (/home/myuser/docker) et une arborescence pour stocker les données permanentes ? Il faut quand même déclarer un PUID/GUID dans la déclaration docker-compose ? Et le PUID/GUID ne sont pas ceux de l'utilisateur utilisé pour le user-namespace ?

          J'ai lu aussi la doc pour iptables mais à part restreindre l'accès à une ou plusieurs IP (ce que je ne veux pas vu que je veux accéder à mon hôte depuis plusieurs connexions qui peuvent changer d'IP), je ne vois pas quoi faire avec ?!

          Merci

          Aerya La partition dédiée n'est pas forcément utile parce que sauf à te planter dans le montage, normalement, on container n'a pas accès à tout ton serveur.

          NON, un stockage séparé est indispensable en production, notamment par ce qu'en cas de saturation de l'espace disque le système hôte plante. Et pour d'autres raisons variable suivant la situation (fiabilité, performance, ...).

          Effectivement. Après... je suis du genre à surveiller mon espace disque 😉

          Merci pour les infos, je vais partir sur des partitions séparées ça ne me coûtera pas plus cher 😃
          Par contre j'ai toujours un peu de mal à saisir si je comprends bien la différence entre un user-namespace, un "user: PID:GID" déclaré dans le fichier docker-compose et un "environment: PUID=/PGID=". Si quelqu'un a une explication un peu claire je suis preneur 😉 merci !

          Si je ne me trompe pas, l'ARG n'est valide que pour la construction de l'image. Quand elle tourne c'est seulement l'ENV qui est pris en compte.

          Merci déjà pour cette réponse.
          Je ne sais pas si j'ai bien compris mais le user-namespace et l'arg "user: PID:GID" semblent correspondre à la même chose ? Sauf qu'avec le user-namespace tu ne déclares pas à chaque fois un user ? Et ce serait juste l'utilisateur qui exécute l'image ?
          Par contre pour l'env ce serait l'utilisateur qui est utilisé dans l'image ?

          Je plussoie le message de xataz, et je rajouterais pour le socket, dans le cas où tu as besoin de l'utiliser, d'avoir recours à un proxy granulaire comme celui-là : https://github.com/Tecnativa/docker-socket-proxy

          Pour gérer l'isolation réseau des conteneurs utiliser des networks bridge que tu définis toi-même (plutôt que d'utiliser le bridge par défaut et d'utiliser des links, dépréciés d'ailleurs). Utilise le flag --internal si tu veux isoler un réseau de l'Internet par exemple (doit y avoir quelques précisions en plus).

          Pour iptables, depuis les versions récentes de Docker, utilise la chaîne DOCKER-USER, puis dedans ajoute une chaîne de filtre qui accepte le traffic prévu vers l'extérieur, puis rejette le reste.

          Enfin pour les runtimes je trouve que c'est un peu compliqué encore de conseiller ça, même si le niveau d'isolation est step up encore davantage. Je joue d'ailleurs avec actuellement pour voir comment bien le gérer (a priori c'est surtout niveau réseau que ça se corse car la résolution DNS par Docker ne fonctionne pas sans la stack réseau de l'hôte - mais tu peux y parer en définissant des adresses statiques).

          Je ne sais pas si j'ai bien compris mais le user-namespace et l'arg "user: PID:GID" semblent correspondre à la même chose ? Sauf qu'avec le user-namespace tu ne déclares pas à chaque fois un user ? Et ce serait juste l'utilisateur qui exécute l'image ?

          Alors y a plusieurs différences.

          • Le paramètre --user définit le user (UID/GID) par défaut du conteneur. Par défaut, sauf si USER est déclaré dans un Dockerfile, ça sera root.
          • Le user namespace est une feature du kernel qui permet de mapper différents user pour faire en sorte, par exemple, que root dans le conteneur =/= root sur l'hôte (ce qui empêche des catastrophes en cas de compromission d'un conteneur).
          • Enfin, certaines images proposent des variables d'environnement (ENV dans le Dockerfile) pour effectivement configurer un utilisateur. Ca ne garantit pas qu'il n'y a pas de root process dans le conteneur, par contre, mais c'est flexible et ça utilise la stratégie du "degrading privileges" (qui a ses défauts aussi).

          En gros il faut utiliser un peu de tout ça selon ton besoin, tes images. Idéalement le namespace pour pas qu'un root escape soit dangereux pour la sécurité de l'hôte (d'ailleurs considère que n'importe quel user non-root qui a accès à Docker sans namespace sur l'hôte a tous les pouvoirs....), le paramètre user pour correspondre à tes besoins, et certaines images avec les variables qui "font tout".

          Ce ne sont pas vraiment les mêmes choses mais ce sont des moyens mis en oeuvre pour plus ou moins le même but. Et c'est assez compliqué, j'en conviens...

          Merci beaucoup @Wonderfall, c'est déjà un peu plus clair pour la partie userns, l'argument user et les variables user. Après comme tu dis reste à réussir à les mettre en pratique vu que c'est un peu compliqué surtout pour un débutant, j'ai ma VM de test je vais pouvoir faire des essais 😉

          Je vais regarder aussi pour le docker-socket-proxy qui avait déjà été abordé un peu plus haut par @xataz ainsi que pour la config UDP de Traefik proposée par @Banip (je me fais un mémo en même temps avant de noter tout ça proprement)

          Pour gérer l'isolation des containers tu ne conseilles pas de passer l'ICC à false donc ? Si l'option est dépréciée, je n'aurais pas la possibilité de faire communiquer les containers entre eux... Par contre dans le cas d'un Traefik v2 en front de plusieurs containers, je suis obligé de tous les mettre dans le même réseau bridge (que j'ai créé avant, par exemple nommé frontend) ou il y a une autre solution ?

          Pour iptables, la chaine DOCKER-USER est à configurer soi-même ou par défaut elle laisse passer le trafic de/vers l'extérieur ?

          Le runtime et le rootless pour le moment je n'y touche pas, on verra un jour 😃

          Tu utilises un daemon.json pour configurer le daemon Docker ? Si oui, tu pourrais le partager pour avoir une idée ?

          Merci !

          C'est une excellente question quant à l'ICC et l'usage de --link.
          Le link est effectivement déprécié : https://docs.docker.com/network/links/

          Du coup pour ICC, je ne sais pas trop si c'est encore d'actualité, j'attendrais le retour de quelqu'un de plus expérimenté comme xataz. Je ferai des recherches approfondies dessus, mais il me semble que l'ICC aujourd'hui ne s'applique qu'au bridge par défaut.

          L'idée c'est effectivement de gérer tes propres réseaux désormais. Par exemple imaginons tu un conteneur Postgres, il est inutile qu'il soit dans le réseau "frontend", et aussi inutile qu'il soit connecté à l'extérieur (d'où --internal). Tu pourras créer un réseau isolé dédié à l'app en question (app <-> db <-> redis par exemple).

          Effectivement pour Traefik le plus commun est de créer un gros réseau dédié, c'est le plus simple. Par contre, toutes les apps de ce réseau peuvent communiquer entre "elles" donc il faut effectivement faire attention si par exemple la sécurité d'une app repose sur le proxy.

          Alors il me semble (à vérifier) que Traefik doit simplement pouvoir router vers ces différents conteneurs, donc tu peux voir le sujet à l'envers et faire des réseaux uniques pour chaque lien Traefik <-> conteneur. Mais ça demande un peu plus de configuration, par contre c'est nécessaire pour vraiment bien isoler entre des conteneurs qui n'ont pas besoin de communiquer entre eux (et c'est la bonne pratique).

          Pour iptables, la chaine DOCKER-USER est à configurer soi-même ou par défaut elle laisse passer le trafic de/vers l'extérieur ?

          Elle est là par défaut, et oui elle laisse passer le traffic. Après pour bloquer spécifiquement l'accès d'un réseau Docker à l'extérieur, je n'ai pas utilisé iptables personnellement.

          Tu utilises un daemon.json pour configurer le daemon Docker ? Si oui, tu pourrais le partager pour avoir une idée ?

          Oui j'utilise daemon.json, le mien est très simple (enfin c'est relatif) et sur mon serveur perso je n'utilise pas encore le rootless. J'ai juste mis btrfs, le live restore activé, les runtimes alternatifs (gvisor, kata), etc. Rien de spécial ! 😄

          {
              "data-root": "/docker",
              "debug": false,
              "live-restore": true,
              "userland-proxy": false,
              "iptables": true,
              "icc": false,
              "runtimes": {
                  "runsc": {
                      "path": "/usr/bin/runsc"
                  },
                  "kata": {
                      "path": "/snap/bin/kata-containers.runtime"
                  }
              },
              "storage-driver": "btrfs"
          }

          (Pourquoi je n'utilise pas les user namespaces : pas compatibles avec kata et gvisor, et c'est redondant avec eux. gvisor par exemple fonctionne avec son namespace séparé de l'hôte. Sinon, je conseille de l'utiliser, c'est une isolation bonne à prendre avec runc.)

          hydrog3n mouaif bouffer xGB d'espace libre pour ça je trouve ça un peu con ^^ Une machine ça se monitore et comme une des règles de base c'est 50% d'espace libre minimum c'est assez tôt que t'as une alerte qui te permet d'éviter un disque plein !
          Je préfère de la surveillance (de toute façon il y a d'autres choses à surveiller donc il en faut) à une bidouille.