Docker コンテナ内の Discourse で `iptables` を使用する手順

Discourse Docker コンテナ内で iptables を設定するにはどうすればよいですか?

特に、ユーザー ID に基づく条件付きルールを追加できるようにするため、Docker コンテナ 内部 でファイアウォールの制御を行いたいと考えています。

当初は、Docker コンテナへのインターネットアクセスを完全に遮断していました(web.socketed.template.yml を使用すれば、nginx がネットワークではなく Unix ドメインソケットを介して通信するため、これは可能です)。そのために 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]# 

しかし、Docker コンテナは少なくとも、unattended-upgrades を介してベース OS(現在は Debian 10)に重要なセキュリティアップデートを適用するためにインターネットアクセスが必要だと気づきました。そのため、Discourse Docker コンテナ内で iptables を実行し、root と apt にはインターネットアクセスを許可し、それ以外はすべて拒否する方がよいと考えました。

これを設定しました。最も複雑だった点は、root ユーザーであっても、デフォルトの Discourse Docker コンテナ内で iptables ルールを表示または編集しようとすると、「permission denied(権限が拒否されました)」エラーが発生することです。

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

これは、Docker コマンドをラップする Discourse の launcher スクリプトが、デフォルトでは Docker コンテナを「NET_ADMIN」権能(capability)なしで実行するためです。

Discourse Docker コンテナに NET_ADMIN 権能を追加する最も堅牢な方法は、コンテナの YAML ファイルを更新して、docker_args YAML 文字列経由で docker run ... /sbin/boot コマンドに必要な引数を追加することです。

docker_args: "--cap-add NET_ADMIN"

例:

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

これらは、launcher によって実行される docker run ... /sbin/boot コマンドに、$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]# 

docker pull 後、docker run ... /sbin/boot 実行前に、Discourse Docker イメージに組み込まれた変更を加えるのは非常に複雑なため、コンテナ起動時に runit によって実行される単純なスクリプトを通じて iptables をインストールおよび設定することにしました。

以下のコマンドを実行すると、必要なテンプレート YAML ファイルが作成され、次の ./launcher rebuild app 実行時に runit スクリプトが生成されます。

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

これは非常に基本的な iptables 設定です。私が使用している web.socketed.template.yml ファイルを使用する場合、Docker ホスト上の nginx リバースプロキシが Unix ドメインソケットを介して Docker コンテナ内の Discourse nginx と通信するため、技術的には Docker コンテナにインターネットアクセスは不要です。インターネットアクセスを許可している主な理由は、Docker コンテナのベース OS が unattended-upgrades を介して重要なセキュリティパッチを自動的に更新できるようにするためです。そのため、apt ユーザーであるユーザー 100 に対してアクセスを開放しています。

最後に、上記の templates/iptables.template.yml ファイルをコンテナの app.yaml ファイルに追加します。

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

これで、

./launcher rebuild app

を実行できるようになります。

約 10 分待機すると、Discourse Docker コンテナ内で iptables が完全に機能するようになります :slight_smile:

:warning: あなたがいずれ、何をしていようとも、自宅、学校、あるいはどこでもこれを試さないでください!

あなたが Discourse インスタンスを discourse ユーザーが所有する unicorn および sidekiq プロセスへのインターネットアクセスを禁止することで機能不全に陥らせていることは間違いありません。しかし、あなたが何をしているか理解していることは確かでしょう。

これはどのような問題を解決するのでしょうか?