Les images de conteneurs Docker qui s'exécutent avec des droits non root ajoutent une couche de sécurité supplémentaire.

Comment lancer notre instance de Traefik dans un environnement Docker avec un utilisateur sans privilège?

Un container non-root c'est quoi ? Est-ce utile ?

Non-root container

Tout container qui se lance va exécuter un programme. Ce programme va s'exécuter avec le PID 1, par défaut Docker va exécuter cette tâche avec le compte root.  Cela pose un problème que tout Administrateur systèmes devrait avoir en tête : le principe du moindre privilège.

J'ai bien conscience qu'une partie d'entre vous, n'est pas SysAdmin, mais si vous vous définissez comme DevOps alors cette notion doit faire partie de vos mantras ! Elle n'est pas toujours applicable facilement, mais dès que possible il faut l'envisager !

En quoi cela pose problème ?

Actuellement lancer son container en tant que root est - hélas - normal. Mais si vous souhaitez sécuriser vos containers, alors il s'agit d'un élément à prendre en compte. Au moins, comme évoqué précédemment au nom du moindre privilège. Mais également pour des raisons de sécurité déjà évoquées sur ce blog :

Pourquoi passer aux containers Rootless ?
Effet de mode ou innovation ? Nous entendons de plus en plus parler du Rootless dans le monde des containers. Mais pourquoi ?

Je vous invite également à lire l'excellent article à ce sujet de Bryant Hagadorn :

Docker and Kubernetes — root vs. privileged
Most of us that are familiar with a Unix system, like macOS or Linux, are used to casually elevating our privileges to the root user through the use of sudo . Usually this happens when debugging…

Bien évidement cette étape ne sera peut-être pas votre priorité. Et il sera sûrement nécessaire d'effectuer quelques tests avec votre container avant d'y arriver.

Et dans le cas de Traefik ? Quel utilisateur est utilisé ?

Traefik

Regardons l'image de notre reverse proxy favori de plus près :

docker exec -it traefik ps aux

Vous constaterez en regardant la sortie de la commande, que l'utilisateur utilisé pour lancer l'exécutable Traefik est l'utilisateur root.

On pourrait se demander pourquoi Traefik utilise un compte root pour fonctionner ? Mais il existe de nombreuses raisons dont voici les plus simples :

  • L'utilisation de port privilégié ( < 1024 ).
  • L'accès à la socket Docker,

Et dans le cas de Nginx par exemple ? De base l'image est également lancée avec le compte root. Par contre les processus workers utilisent un compte non privilégié et nginx fournit des indications pour lancer son image avec un compte non-root :

It is possible to run the image as a less privileged arbitrary UID/GID...

Il faut se le dire très honnêtement : si ces deux images ne se lancent pas directement avec un compte sans privilège, c'est tout simplement qu'elles ne fonctionneraient pas partout - et pour tous - out-of-the-box !

Et oui, les utilisateurs finaux sont les premiers fautifs 😀

Mais existe t-il des solutions pour changer ça ?

Solution

Il existe même DES solutions ! Je vais en présenter une aujourd'hui, mais il en existe bien d'autres également.

Le but de cet article, et du blog, reste de vous sensibiliser à la sécurité de vos containers et de vous apporter des connaissances afin que vous puissiez réaliser vos propres choix/installations : je ne prétends pas avoir la meilleure solution !

Aujourd'hui, je vais utiliser une option présente dans Docker, le flag --user :

| Docker Documentation
Configure containers at runtime

Cette directive permet d’exécuter le processus lancé par votre container avec l'utilisateur passé en argument.

Pratique donc pour passer d'un utilisateur comme root à un compte sans privilège. Cette option est également disponible dans le format docker-compose.

Passons immédiatement à la pratique en créant un fichier docker-compose.yaml pour lancer Traefik :

version: "3.8"
services:
  reverse_proxy:
    image: traefik:2.2
    user: 1001:1001
    restart: unless-stopped
    command:
      --api.insecure=true 
      --log.level=DEBUG 
      --entrypoints.web.address=:80
      --providers.docker 
      --providers.docker.exposedbydefault=false
    ports:
      # The HTTP port
      - "80:80"
      # The Admin port
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - web

networks:
  web:
    name: traefik_web

🚩 Attention, ne passez un nom d'utilisateur à l'option user que si celui-ci existe sur votre image. J'utilise ici l'uid/gid 1001 qui n'existe pas sur mon hôte 🚩

Mais en lançant ce container on remonte très rapidement une erreur :

# sudo docker-compose up
Creating network "traefik_web" with the default driver
Creating traefik_reverse_proxy_1 ... done
Attaching to traefik_reverse_proxy_1
....
reverse_proxy_1  | 2020/07/15 11:37:23 traefik.go:72: command traefik error: error while building entryPoint web: error preparing server: error opening listener: listen tcp :80: bind: permission denied

Mon utilisateur n'a pas le droit de bind un port privilégié, encore une fois, il est possible de solutionner ce problème de plusieurs façons.

Je décide d'utiliser l'option sysctl de Docker pour autoriser cela dans mon fichier :

    sysctls:
      net.ipv4.ip_unprivileged_port_start: 0

Je relance, mais cette fois-ci j'ai une nouvelle erreur :

reverse_proxy_1  | time="2020-07-15T11:40:11Z" level=error msg="Provider connection error Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get \"http://%2Fvar%2Frun%2Fdocker.sock/v1.24/version\": dial unix /var/run/docker.sock: connect: permission denied, retrying in 943.910051ms" providerName=docker

Encore et toujours, plusieurs solutions !

Si tout simplement je n'utilisais pas ma socket Docker sur un service qui est disponible sur internet ? 🤣

Et en plus, j'ai déjà évoqué ce sujet ici :

Sécuriser l’accès à sa socket Docker...
Nous avions déjà vu ensemble comment sécuriser son daemon et son hôte Docker. Mais quand est-il de l’API disponible au travers de la socket Docker ?

Je vais donc modifier mon fichier docker-compose.yaml et ma configuration de Traefik pour utiliser ce service :

version: "3.8"
services:
  dockerproxy:
    image: tecnativa/docker-socket-proxy
    environment:
      - CONTAINERS=1
    networks:
      - socket_docker
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"

  reverse_proxy:
    image: traefik:2.2
    user: 1001:1001
    restart: unless-stopped
    sysctls:
      net.ipv4.ip_unprivileged_port_start: 0
    command:
      --api.insecure=true
      --log.level=DEBUG
      --entrypoints.web.address=:80
      --providers.docker.endpoint="tcp://dockerproxy:2375"
      --providers.docker.exposedbydefault=false
    ports:
      # The HTTP port
      - "80:80"
      # The Admin port
      - "8080:8080"
    networks:
      - socket_docker
      - web


networks:
  web:
    name: traefik_wan

  socket_docker:

Pour Traefik, je modifie mon endpoint Docker :

providers.docker.endpoint="tcp://dockerproxy:2375"

Et on relance ! Cette fois-ci, aucune erreur !

Et si on validait le fonctionnement avec un exemple concret ?

Exemple 🚀

Un simple exemple pour valider le bon fonctionnement de la solution avec une instance whoami.

Je vais donc créer un fichier docker-compose.yaml :

version: '3.8'
services:
  whoami:
    image: containous/whoami
    labels:
      traefik.enable: true
      traefik.docker.network: traefik_wan
      traefik.http.routers.whoami.entrypoints: web
      traefik.http.routers.whoami.rule: 'Host(`whoami.mydomain.com`)'
      traefik.http.services.whoami.loadbalancer.server.port: 80
    networks:
      - traefik_wan

networks:
  traefik_wan:
    external: true

Il ne reste plus qu'à lancer cette instance :

# docker-compose up -d

et ... :

# curl http://whoami.mydomain.com
Hostname: 3d8322e24526
IP: 127.0.0.1
IP: 172.27.0.3
RemoteAddr: 172.27.0.2:50726
GET / HTTP/1.1
Host: whoami.mydomain.com
User-Agent: curl/7.68.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: X.X.X.X
X-Forwarded-Host: whoami.mydomain.com
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: 598416f3d937
X-Real-Ip: X.X.X.X

Et le HTTPS dans tout ça ?!

HTTPS everywhere

Traefik sans HTTPS, c'est comme un repas sans fromage !

Raclette ?

Voici donc un fichier docker-compose.yaml avec l'intégralité de la configuration et une redirection HTTPS :

Créez au préalable le fichier acme.json et donnez les droits à l'uid 1001 :

touch acme.json
sudo chown 1001:1001 acme.json

Il serait d'ailleurs préférable - surtout dans ce cas précis - de stocker le certificat dans une base de type KV ( cela permet d'éviter la création et les droits à donner au fichier acme.json ) !

Bonus stage 🔥

Voici également une solution équivalente, l'article date un peu, mais globalement la solution reste envisageable :

How to run Træfik as a non-root user ? Part 1
The goal of this tutorial is to explain an easy way to run Træfik, a modern HTTP reverse proxy and load balancer, as a non-privileged user. If you are here, it is possibly because you want to use…

Enfin, et si il y a encore des sceptiques dans l'utilisation d'un flag comme --user , je terminerai avec cette citation d'un article de Dan Walsh.

The --user option is still very necessary and adds a lot of security even when using rootless Podman, and users should still use it to be as secure as possible.

Même avec un container runtime en mode Rootless comme Podman, utilisez cette option est recommandée !

Article complet à retrouver ici :

Should you use the --user flag in rootless containers?
Take a deep dive and discover the power of the --user option for rootless Podman containers in Linux.

Vous savez maintenant comment exécuter votre instance Traefik avec un utilisateur non privilégié !

Faut-il franchir le pas rapidement ? J'utilise finalement moi-même cette option depuis assez peu de temps car je n'avais pas fait de ce sujet ma priorité. Tout dépendra donc de votre volonté à avancer sur le terrain du DevSecOps ( oui encore un joli mot marketing 😂 ).

En tout cas, les containers de demain devront forcément intégrer ce type de mécanisme de sécurité !

N'hésitez pas à permettre au blog de continuer à exister et à fournir un contenu de qualité - enfin je l'espère - au travers de vos dons sur : buymeacoff.ee/lfache
Et n'hésitez pas à m'apporter des remarques ou des commentaires sur Twitter, ou ici 👇