Como usar `iptables` dentro do contêiner Docker do Discourse

Como posso configurar o iptables no contêiner Docker do Discourse?

Eu preciso especificamente dos controles de firewall \u003cem\u003edentro\u003c/em\u003e do contêiner Docker, pois quero poder adicionar regras condicionais baseadas no ID do usuário.

Inicialmente, eu estava apenas negando qualquer acesso à internet ao contêiner Docker por completo (isso é viável se você usar web.socketed.template.yml, pois o nginx se comunicará por meio de sockets de domínio Unix em vez da rede) via 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]# 

…mas percebi que o contêiner Docker provavelmente deveria ter pelo menos acesso à internet para que possa atualizar seu sistema operacional base (atualmente Debian 10) com atualizações de segurança críticas via unattended-upgrades. Então, eu prefiro conceder acesso à internet ao root e ao apt e negar tudo o mais via iptables executando dentro do contêiner Docker do Discourse.

Eu configurei isso. A parte mais complicada foi que — mesmo sendo root — você ainda receberá erros de “permission denied” ao tentar visualizar ou editar regras do iptables dentro do contêiner Docker padrão do Discourse.

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

Isso ocorre porque, por padrão, o script launcher do Discourse, que envolve os comandos do Docker, executa o contêiner Docker do Discourse sem a capacidade “NET_ADMIN”.

A maneira mais robusta de adicionar a capacidade NET_ADMIN ao contêiner Docker do Discourse é atualizar o arquivo YAML do seu contêiner para incluir o argumento necessário no comando docker run ... /sbin/boot por meio da string YAML docker_args:

docker_args: "--cap-add NET_ADMIN"

Por exemplo:

[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]# 

Esses argumentos serão adicionados ao comando docker run ... /sbin/boot executado pelo launcher por meio da variável $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]# 

Como é extremamente complexo fazer alterações embutidas na imagem Docker do Discourse após um docker pull e antes do docker run ... /sbin/boot, decidi instalar e configurar o iptables por meio de um script simples executado pelo runit quando o contêiner inicia.

O seguinte comando criará o arquivo YAML de modelo necessário, que gerará o script runit na próxima execução de ./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

Observe que esta é uma configuração básica de iptables. Com o arquivo web.socketed.template.yml (que eu uso), o contêiner Docker tecnicamente não precisa de acesso à Internet, já que o proxy reverso nginx no host Docker se comunica com o nginx do Discourse no contêiner Docker por meio de um socket de domínio Unix. A principal razão para permitir o acesso à Internet é para que o sistema operacional base do contêiner Docker possa se atualizar com patches de segurança críticos por meio do unattended-upgrades. Por isso, abri o acesso para o usuário 100, que é o usuário apt.

Por fim, adicione o arquivo templates/iptables.template.yml acima ao arquivo app.yaml do seu contêiner.

[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]# 

Agora você deve ser capaz de executar:

./launcher rebuild app

E, após aguardar cerca de 10 minutos, seu contêiner Docker do Discourse terá o iptables totalmente funcional :slight_smile:

:warning: Quem quer que você seja, seja o que for que você faça, por favor, não tente isso em casa, na escola ou em nenhum lugar!

Tenho quase certeza de que você está prejudicando sua instância do Discourse ao impedir que os processos unicorn e sidekiq (pertencentes ao usuário discourse) acessem a internet. Mas sim, tenho certeza de que você sabe o que está fazendo.

Que problema isso está resolvendo?