Cómo usar `iptables` dentro del contenedor Docker de Discourse

¿Cómo puedo configurar iptables en el contenedor Docker de Discourse?

Necesito específicamente los controles del firewall dentro del contenedor Docker porque quiero poder agregar reglas condicionales basadas en el ID de usuario.

Al principio, simplemente negaba cualquier acceso a Internet al contenedor Docker por completo (esto es posible si usas web.socketed.template.yml porque nginx se comunicará a través de sockets de dominio Unix en lugar de la red) mediante 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]# 

…pero me di cuenta de que el contenedor Docker probablemente debería tener al menos acceso a Internet para poder actualizar su sistema operativo base (actualmente Debian 10) con actualizaciones de seguridad críticas mediante unattended-upgrades. Por lo tanto, preferiría simplemente conceder acceso a Internet a root y apt, y denegar todo lo demás mediante iptables ejecutándose dentro del contenedor Docker de Discourse.

Lo configuré así. La parte más complicada fue que, incluso si eres root, seguirás obteniendo errores de “permiso denegado” al intentar ver o editar las reglas de iptables dentro del contenedor Docker predeterminado de Discourse.

root@24a1f9f4c038:/# iptables -L
# Advertencia: existen tablas iptables-legacy, usa iptables-legacy para verlas
iptables: Permiso denegado (debes ser root).
root@24a1f9f4c038:/# 

Esto se debe a que, por defecto, el script launcher de Discourse, que envuelve los comandos de Docker, ejecuta el contenedor Docker de Discourse sin la capacidad “NET_ADMIN”.

La forma más robusta de agregar la capacidad NET_ADMIN al contenedor Docker de Discourse es actualizar el archivo yaml de tu contenedor para incluir el argumento necesario en el comando docker run ... /sbin/boot mediante la cadena yaml docker_args:

docker_args: "--cap-add NET_ADMIN"

Por ejemplo:

[root@osestaging1 discourse]# head -n15 containers/app.yml
## esta es la plantilla del contenedor Docker de Discourse todo-en-uno, independiente
##
## Después de realizar cambios en este archivo, DEBES reconstruir
## /var/discourse/launcher rebuild app
##
## TEN *MUCHO* CUIDADO AL EDITAR!
## ¡LOS ARCHIVOS YAML SON SUPER SENSIBLES A ERRORES EN LOS ESPACIOS EN BLANCO O EN LA ALINEACIÓN!
## visita http://www.yamllint.com/ para validar este archivo según sea necesario

docker_args: "--cap-add NET_ADMIN"

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

Estos se agregarán al comando docker run ... /sbin/boot ejecutado por launcher a través de 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]# 

Dado que es muy complejo realizar cambios integrados en la imagen Docker de Discourse después de un docker pull y antes del docker run ... /sbin/boot, decidí instalar y configurar iptables mediante un script simple ejecutado por runit cuando el contenedor se inicia.

El siguiente comando creará el archivo yaml de plantilla necesario, que generará el script runit en el próximo ./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
        ################################################################################
        # Archivo:    /etc/runit/1.d/01-iptables
        # Versión: 0.2
        # Propósito: instala y bloquea iptables
        # Autor:  Michael Altfield <michael@opensourceecology.org>
        # Creado: 2019-11-26
        # Actualizado: 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

Ten en cuenta que esta es una configuración de iptables muy básica. Con el archivo web.socketed.template.yml (que uso), el contenedor Docker técnicamente no necesita acceso a Internet, ya que el proxy inverso nginx en el host de Docker se comunica con el nginx de Discourse en el contenedor Docker a través de un socket de dominio Unix. La razón principal por la que permito el acceso a Internet es para que el sistema operativo base del contenedor Docker pueda actualizarse con parches de seguridad críticos mediante unattended-upgrades. De ahí que abra el acceso para el usuario 100, que es el usuario apt.

Finalmente, agrega el archivo templates/iptables.template.yml anterior al archivo app.yaml de tu contenedor.

[root@osestaging1 discourse]# head -n15 /var/discourse/containers/discourse_ose.yml
## esta es la plantilla del contenedor Docker de Discourse todo-en-uno, independiente
##
## Después de realizar cambios en este archivo, DEBES reconstruir
## /var/discourse/launcher rebuild app
##
## TEN *MUCHO* CUIDADO AL EDITAR!
## ¡LOS ARCHIVOS YAML SON SUPER SENSIBLES A ERRORES EN LOS ESPACIOS EN BLANCO O EN LA ALINEACIÓN!
## visita http://www.yamllint.com/ para validar este archivo según sea necesario

docker_args: "--cap-add NET_ADMIN"

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

Ahora deberías poder ejecutar:

./launcher rebuild app

Y después de esperar unos 10 minutos, tu contenedor Docker de Discourse tendrá iptables completamente funcional :slight_smile:

:warning: ¡Sea quien sea usted y haga lo que haga, por favor no intente esto en casa, en la escuela o en ningún otro lugar!

Estoy bastante seguro de que está inutilizando su instancia de Discourse al prohibir que los procesos unicorn y sidekiq (propiedad del usuario discourse) accedan a Internet. Pero sí, estoy seguro de que sabe lo que está haciendo.

¿Qué problema está resolviendo esto?