Comment utiliser `iptables` dans un conteneur Docker Discourse

Comment puis-je configurer iptables dans le conteneur Docker de Discourse ?

J’ai spécifiquement besoin des contrôles du pare-feu à l’intérieur du conteneur Docker, car je souhaite pouvoir ajouter des règles conditionnelles basées sur l’ID utilisateur.

Au départ, je bloquais tout accès à Internet pour le conteneur Docker (cela est possible si vous utilisez web.socketed.template.yml, car Nginx communiquera via des sockets de domaine Unix au lieu du réseau) en utilisant dockerd ... --iptables=false.

[root@osestaging1 templates]# grep ExecStart /usr/lib/systemd/system/docker.service
#ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --iptables=false
[root@osestaging1 templates]# 

…mais j’ai réalisé que le conteneur Docker devrait probablement avoir au moins un accès à Internet afin de pouvoir mettre à jour son système d’exploitation de base (actuellement Debian 10) avec des mises à jour de sécurité critiques via unattended-upgrades. Je préférerais donc accorder à root et à apt un accès à Internet et refuser tout le reste via iptables exécuté à l’intérieur du conteneur Docker de Discourse.

J’ai configuré cela. La partie la plus complexe était que — même en tant que root — vous obtiendrez toujours des erreurs « permission denied » en tentant de voir ou de modifier les règles iptables à l’intérieur du conteneur Docker Discourse par défaut.

root@24a1f9f4c038:/# iptables -L
# Warning: iptables-legacy tables present, use iptables-legacy to see them
iptables: Permission denied (you must be root).
root@24a1f9f4c038:/# 

Cela est dû au fait que, par défaut, le script launcher de Discourse, qui enveloppe les commandes Docker, exécute le conteneur Docker Discourse sans la capacité « NET_ADMIN ».

La méthode la plus robuste pour ajouter la capacité NET_ADMIN au conteneur Docker Discourse consiste à mettre à jour le fichier YAML de votre conteneur afin d’inclure l’argument nécessaire dans la commande docker run ... /sbin/boot via la chaîne YAML docker_args :

docker_args: "--cap-add NET_ADMIN"

Par exemple :

[root@osestaging1 discourse]# head -n15 containers/app.yml
## this is the all-in-one, standalone Discourse Docker container template
##
## After making changes to this file, you MUST rebuild
## /var/discourse/launcher rebuild app
##
## BE *VERY* CAREFUL WHEN EDITING!
## YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
## visit http://www.yamllint.com/ to validate this file as needed

docker_args: "--cap-add NET_ADMIN"

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
[root@osestaging1 discourse]# 

Ces arguments seront ajoutés à la commande docker run ... /sbin/boot exécutée par launcher via la variable $user_args :

[root@osestaging1 discourse]# grep -A2 -E '^\s*\$docker_path run' launcher
     $docker_path run --shm-size=512m $links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
        -e DOCKER_HOST_IP="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
        $run_image $boot_command
[root@osestaging1 discourse]# 

Comme il est très complexe d’apporter des modifications intégrées à l’image Docker de Discourse après un docker pull et avant l’exécution de docker run ... /sbin/boot, j’ai décidé d’installer et de configurer iptables via un simple script exécuté par runit au démarrage du conteneur.

La commande suivante créera le fichier modèle YAML nécessaire, qui générera le script runit lors du prochain ./launcher rebuild app :

cat << EOF > /var/discourse/templates/iptables.template.yml
run:
  - file:
     path: /etc/runit/1.d/01-iptables
     chmod: "+x"
     contents: |
        #!/bin/bash
        ################################################################################
        # File:    /etc/runit/1.d/01-iptables
        # Version: 0.2
        # Purpose: installs & locks-down iptables
        # Author:  Michael Altfield <michael@opensourceecology.org>
        # Created: 2019-11-26
        # Updated: 2019-12-17
        ################################################################################
        sudo apt-get update
        sudo apt-get install -y iptables 
        sudo iptables -A INPUT -i lo -j ACCEPT
        sudo iptables -A INPUT -s 127.0.0.1/32 -j DROP
        sudo iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo iptables -A INPUT -j DROP
        sudo iptables -A OUTPUT -s 127.0.0.1/32 -d 127.0.0.1/32 -j ACCEPT
        sudo iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo iptables -A OUTPUT -m owner --uid-owner 0 -j ACCEPT
        sudo iptables -A OUTPUT -m owner --uid-owner 100 -j ACCEPT
        sudo iptables -A OUTPUT -j DROP
        sudo ip6tables -A INPUT -i lo -j ACCEPT
        sudo ip6tables -A INPUT -s ::1/128 -j DROP
        sudo ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo ip6tables -A INPUT -j DROP
        sudo ip6tables -A OUTPUT -s ::1/128 -d ::1/128 -j ACCEPT
        sudo ip6tables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
        sudo ip6tables -A OUTPUT -m owner --uid-owner 0 -j ACCEPT
        sudo ip6tables -A OUTPUT -m owner --uid-owner 100 -j ACCEPT
        sudo ip6tables -A OUTPUT -j DROP

EOF

Notez qu’il s’agit d’une configuration iptables très basique. Avec le fichier web.socketed.template.yml (que j’utilise), le conteneur Docker n’a techniquement pas besoin d’accès à Internet, car le proxy inverse nginx sur l’hôte Docker communique avec le nginx de Discourse dans le conteneur Docker via un socket de domaine Unix. La principale raison pour laquelle j’autorise l’accès à Internet est de permettre au système d’exploitation de base du conteneur Docker de se mettre à jour avec des correctifs de sécurité critiques via unattended-upgrades. D’où l’ouverture pour l’utilisateur 100, qui est l’utilisateur apt.

Enfin, ajoutez le fichier templates/iptables.template.yml ci-dessus au fichier app.yaml de votre conteneur.

[root@osestaging1 discourse]# head -n15 /var/discourse/containers/discourse_ose.yml
## this is the all-in-one, standalone Discourse Docker container template
##
## After making changes to this file, you MUST rebuild
## /var/discourse/launcher rebuild app
##
## BE *VERY* CAREFUL WHEN EDITING!
## YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
## visit http://www.yamllint.com/ to validate this file as needed

docker_args: "--cap-add NET_ADMIN"

templates:
  - "templates/iptables.template.yml"
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
[root@osestaging1 discourse]# 

Vous devriez maintenant pouvoir exécuter :

./launcher rebuild app

Après environ 10 minutes d’attente, votre conteneur Docker Discourse aura iptables pleinement fonctionnel :slight_smile:

:warning: Qui que vous soyez, quoi que vous fassiez, s’il vous plaît, n’essayez pas cela à la maison, à l’école ou ailleurs !

Je suis assez sûr que vous handicapez votre instance Discourse en interdisant aux processus unicorn et sidekiq (appartenant à l’utilisateur discourse) d’accéder à Internet. Mais oui, je suis sûr que vous savez ce que vous faites.

Quel problème cela résout-il ?