こんにちは、SSTでWeb脆弱性診断用のツール(スキャンツール)開発をしている坂本(Twitter, GitHub)です。
先日、Dockerコンテナのinbound/outbound通信をiptablesで制限する方法について調べたので、簡単に紹介していきたいと思います。
動機としてはタイトルに書いた通りSSRF対策を目的としています。 特に内部ネットワーク宛の攻撃を想定し、Dockerコンテナからの private network 宛のoutboundを禁止したい。 でもDockerホスト側ではメールGWなど一部内部NW上のサーバへのoutboundは許可する必要があるので、Dockerコンテナだけに限定したoutboundルールを設定したい・・・そんな状況に対応するにはどうするか調べてみた次第です。
結論から書くと Docker Engine 公式ドキュメントで解説されているとおり DOCKER-USER
chain をカスタム します。
それだけですと記事としてなんの新規性も無いので、公式ドキュメントには無い以下のトピックスに挑戦してみました。
- RHEL/CentOS系の
firewall-cmd
コマンドからiptablesのルールを追加してみる。 - systemd で docker 起動時に
DOCKER-USER
chain にルールを追加してみる。
検証環境:
- CentOS Stream 8
- Docker Engine Community Version 23系
- Amazon Linux 2
- Docker Engine Version 20系
- 1. Docker の iptables と DOCKER-USER chain
- 2. firewall-cmd コマンドから DOCKER-USER chain にルールを追加
- 3. systemd で docker 起動時に DOCKER-USER chain をカスタマイズ
- 4. outboundを制限したい private/local network いろいろ
- 5. bridge network 内の通信と outbound 制限の衝突について
- 6. うまく動かなくなったときは
- 7. Docker の iptables 機能を無効化するアプローチについて
- 8. その他 docker network におけるセキュリティの話題について
- 9. まとめ
1. Docker の iptables と DOCKER-USER chain
まず Docker の iptables と DOCKER-USER
chain について簡単に紹介します。
- Docker はコンテナのネットワーク管理のために iptables を操作しています。
- Docker は
DOCKER-USER
とDOCKER
という2つのカスタム chain をFORWARD
chain の一部として追加し、Dockerコンテナのinbound/outboundはこの2つのカスタムchainを通るように設定します。 - Docker 側で追加する iptables ルールは
DOCKER
chain に追加されます。 - ユーザ側でカスタマイズしたい場合は
DOCKER-USER
chain に iptables ルールを追加します。
詳細は Docker 公式ドキュメントや iptables の資料を参照してください。
2. firewall-cmd コマンドから DOCKER-USER chain にルールを追加
Docker 公式ドキュメントでは iptables コマンドでルールを追加する例が紹介されています。
坂本自身は普段 CentOS Stream 8 を使っており、ディストリビューションとの親和性から firewall-cmd
コマンドを使ったやり方を本記事では紹介します。
例として 10.0.0.0/8
(class A private network) 宛のTCP outbound をREJECTするルールを DOCKER-USER
chain に追加するには、以下のコマンドを実行します。
$ sudo firewall-cmd --direct \ --add-rule ipv4 \ filter DOCKER-USER 10 \ -m state --state NEW \ -m tcp -p tcp \ -d 10.0.0.0/8 \ -j REJECT
--direct
オプションを使うのがポイントです。
RHEL/CentOS系のファイアウォールである firewalld では iptables よりも簡易的かつ分かりやすい形でパケットフィルタ設定を行えるようになっています。
そのトレードオフとして、通常の設定方法では今回のような細かい指定ができません。
それを補うが --direct
オプションであり、iptablesのルール設定をほぼそのまま指定することが可能となります。
3. systemd で docker 起動時に DOCKER-USER chain をカスタマイズ
firewall-cmd
でoutbound設定を行う例を紹介しましたが、これをシステムが起動する度に手動で実行するのは非効率です。
Linux ディストリビューションにおいては Docker Engine (dockerd) は systemd により起動されるので、DOCKER-USER
chain のカスタマイズも systemd で管理してくれると便利です。
これを実現するため試行錯誤してみた結果として、本記事では docker.service
の systemd 設定を override.conf
でカスタマイズし、 ExecStartPost
を使うことで dockerd 起動後に firewall-cmd
を自動実行する方法を紹介します。
例として class A/B/C および 169.254.0.0/16
(RFC3927, link-local address) への TCP outbound を REJECT するルールを追加してみます。
/etc/systemd/system/docker.service.d/override.conf
を作成して以下の内容を保存します。[Service] ExecStartPost=/usr/bin/firewall-cmd --direct --add-rule ipv4 filter DOCKER-USER 20 -m state --state NEW -m tcp -p tcp -d 10.0.0.0/8 -j REJECT ExecStartPost=/usr/bin/firewall-cmd --direct --add-rule ipv4 filter DOCKER-USER 21 -m state --state NEW -m tcp -p tcp -d 172.16.0.0/12 -j REJECT ExecStartPost=/usr/bin/firewall-cmd --direct --add-rule ipv4 filter DOCKER-USER 22 -m state --state NEW -m tcp -p tcp -d 192.168.0.0/16 -j REJECT ExecStartPost=/usr/bin/firewall-cmd --direct --add-rule ipv4 filter DOCKER-USER 23 -m state --state NEW -m tcp -p tcp -d 169.254.0.0/16 -j REJECT
- システムを再起動します。
RHEL/CentOS系では docker.service
という名前で dockerd の systemd サービスが登録されます。
systemd では /etc/systemd/system/<service名>.d/(任意のファイル名).conf
という設定ファイルを作ることで、元の service 設定にマージする仕組みが提供されています。1
その仕組みを使って、 docker.service
が起動したあと (= ExecStartPost
) に firewall-cmd
を使って DOCKER-USER
chain をカスタマイズするコマンドを実行するというのが、上記の例の中身になります。
余談: firewalld 側で DOCKER-USER
chain のカスタマイズを永続化できないか?というのも試してみたのですが、どうも DOCKER-USER
chain 自体は永続化されておらず、dockerd起動時に毎回作成してるようです。そしてサービスの起動順序としては firewalld のほうが先なので・・・ firewalld 側で DOCKER-USER
chain のカスタマイズを永続設定にしてしまうと、「まだ dockerd が起動して無くて DOCKER-USER
chain が存在しない状況でそれを操作しようとする」状況となるため、設定が無視されてしまう現象を確認しました。
このため、DOCKER-USER
chain をカスタムするなら docker が起動した後が確実なので、上記のように systemd の override 機能を使って ExecStartPost
を使ってみた次第です。
と、ここまで書いてて気づいたのですが、 firewall-cmd
コマンドを単発実行するだけの service を作成して、docker サービスの起動後に実行するよう設定するアプローチもありかもしれません。
さらに厳密を期するなら、dockerが起動して DOCKER-USER
chain が追加されたところまで確認してから firewall-cmd
コマンドを実行するよう health check の仕組みを導入する必要もありそうです。
興味ある方はお試しください。
4. outboundを制限したい private/local network いろいろ
「SSRF対策として private network へのoutboundを制限したい」と聞いて、すぐ思いつくIPアドレスとしては class A/B/C でしょうか。最近ではそれに加えて AWS EC2 メタデータURL2が含まれる 169.254.0.0/16
も必須と思われます。
最近見かけた記事では、以下のURLを参考資料として他にもいろいろなIP範囲を制限していて勉強になりました。 https://github.com/letsencrypt/boulder/blob/d6cd589795735d876ad62e40b6adf19655e8f7f0/bdns/dns.go#L32-L124
ここに挙げられている全ての範囲が本当に outbound 制限が必要かどうかまでは、本記事ではスコープ外とします。 SSRF対策を目的とした outbound 制限対象の private network については、ベストプラクティスとなるような一覧があると便利かと思いますので、機会があれば調べてみたいと思います。
5. bridge network 内の通信と outbound 制限の衝突について
Docker ではコンテナを配置するネットワークを複数種類サポートしています。3
ここでは DOCKER-USER
chain の outbound 制限と、bridge network のIP範囲が衝突したため、同じ bridge network に配置したコンテナ間で通信ができなくなった例を紹介します。
(1) 本記事で紹介したように /etc/systemd/system/docker.service.d/override.conf
で以下のように 172.16.0.0/12
へのTCP outbound のREJECTルールを DOCKER-USER
chain に追加。
[Service] (...) ExecStartPost=/usr/bin/firewall-cmd --direct --add-rule ipv4 filter DOCKER-USER 21 -m state --state NEW -m tcp -p tcp -d 172.16.0.0/12 -j REJECT (...)
(2) 以下のように 172.16.0.0/12
レンジで独自に bridge nework の mytestbr0
を作成。
$ docker network create \ --driver=bridge \ --subnet=172.30.230.0/24 \ --ip-range=172.30.230.0/27 \ --gateway=172.30.230.30 \ mytestbr0
(3) mytestbr0
に nginx コンテナを2つ配置して起動。
$ docker run -d --rm -p 8080:80 --network mytestbr0 --name nginx8080 nginx:1.23-alpine $ docker run -d --rm -p 8081:80 --network mytestbr0 --name nginx8081 nginx:1.23-alpine
(4) Docker ホストからコンテナへの疎通確認 → 正常
$ curl http://localhost:8080/ $ curl http://localhost:8081/
(5) それぞれのコンテナに入って、お互いに通信できるか確認 → 失敗
$ docker exec -it nginx8080 /bin/sh / # curl http://nginx8081/ curl: (7) Failed to connect to nginx8081 port 80 after 1003 ms: Couldn't connect to server / # exit $ docker exec -it nginx8081 /bin/sh / # curl http://nginx8080/ curl: (7) Failed to connect to nginx8080 port 80 after 1001 ms: Couldn't connect to server / # exit
原因としては単純で、DOCKER-USER
chain で 172.16.0.0/12
宛のTCP outboundをREJECTしてます。 mytestbr0
の設定上、各コンテナにもその範囲のIPが割り当てられますので、お互いにTCP通信をしようとすれば当然 172.16.0.0/12
宛のTCP outbound となるのでREJECTされてしまった・・・というオチになります。
このような状況を避けるため、自分としては以下のように対処してみました。
mytestbr0
を172.16.0.0/12
レンジまるごとカバーする形で作成する。- その代わり
DOCKER-USER
chain からは172.16.0.0/12
レンジのルールを削除する。
mytestbr0
に配置されたコンテナからすれば、 172.16.0.0/12
宛の通信は全て同じネットワーク上の他のコンテナ宛となるはずで、外には出ていかない・・・と思います。なので、DOCKER-USER
chain のルールからも外してしまって支障はない、という判断での対処になります。(この部分の理解や前提が間違ってたらご指摘ください)
なお Docker ホスト側が所属するIPと被る状況だとどうなるか、自分も理解しきれておりません。そのため、Dockerホストが所属するIPどは別のレンジで bridge network を作成し、そのレンジを除外しておくなど細かい設定が必要となるかと思います。4
6. うまく動かなくなったときは
これは自分の体験談ですが、docker network
コマンドでネットワークの作成/削除を繰り返していたり、iptablesのカスタム設定に試行錯誤しているうちに、どう頑張っても設定した通りの挙動にならないときがありました。
その時どうしたか・・・ OSを再起動するとアッサリ解消しました。
docker自体も iptables をいろいろ操作してるので、何かの拍子に設定がずれたり、壊れたりする状況があるのだと思われます。 ただ docker の良いところというか、たまたま今現在の仕様がそうなってるので助かってるところとして、iptablesに対する操作が iptables のレイヤーで永続化されておらず、dockerが起動してから設定されるところです。 もしも docker 関連でiptablesの設定が壊れてしまっても、OSを再起動すれば壊れた設定がクリアされ、きれいな状態で再挑戦できます。
みなさんがもし docker network
コマンドやiptablesで試行錯誤をしているうちに「あれ?」と思うような現象に遭遇したら、ダメ元でもいいのでOS再起動を検討してみてください。
それでも思ったように動いてくれないときは、 iptables -L -n
などで実際にiptablesの設定状況をダンプしてみて、どういう設定になっているか目視で解析してみるのを推奨します。
7. Docker の iptables 機能を無効化するアプローチについて
最近見かけた記事で、そもそも docker の daemon.json
で iptables
キーに false
を設定してdockerによるiptables操作を無効化し、マニュアルでiptablesのrouting設定を構築して outbound 制限を組み込む記事5がありました。
こちらの記事も読み応え十分で、iptablesの勉強になります。 また outbound で制限したいIPアドレスとして class A/B/C 以外にも様々なものがあることをこちらの記事で知り、大変勉強になりました。
一方で、Docker の iptables 操作を無効化することの副作用について注意が必要な印象も受けました。 公式ドキュメントでは Prevent Docker from manipulating iptables で次のように説明しています。
Setting
iptables
tofalse
will more than likely break container networking for the Docker engine.
iptables
を false
に設定すると、高い確率で docker のコンテナネットワークを壊す可能性がある、とのことです。
非常に副作用が大きい設定変更と思われますので、こちらのアプローチを採用する際は本当にそれが適切か、入念な事前検証やメリット/デメリットの検討が必要と思われます。
8. その他 docker network におけるセキュリティの話題について
最近見かけた記事で、Docker Desktop の port mapping が Docker ホストのファイアウォールのルールも書き換えていて、公衆WiFi上で動かしたら外部からアクセスされてしまった事例がありました。
コンテナサービスが提供するネットワーク機能は便利な点もありますが、裏側でどういう仕組で動いているのか、どういう副作用があるのか公式ドキュメントなどを通じてきちんと仕組みを理解する必要性を改めて感じました。
9. まとめ
- docker は独自に iptables のルールを設定している。
- コンテナに対する iptables のルールをカスタマイズしたいときは
DOCKER-USER
chain に設定する。 - システム(docker)起動時に
DOCKER-USER
chain へのルール設定を行いたい場合は、systemd によるカスタマイズ機能を使う。- 本記事の例では
/etc/systemd/system/docker.service.d/override.conf
ファイルのExecStartPost
からfirewall-cmd --direct
で詳細なiptablesルールを設定。
- 本記事の例では
- class A/B/C の他にも特殊なIPアドレスレンジがいろいろある。SSRF対策として outbound を制限したいIPアドレスについて、それらも含めてRFCなど調べて検討すると良さそう。
- outbound 制限をするときは、 bridge network などコンテナ自身が所属するIPレンジとの衝突に注意。
- うまく動かなくなったときはシステム再起動。
iptables -L -n
などで実際の設定状況の確認も忘れずに。 - Docker の
daemon.json
でiptables
キーをfalse
にするアプローチは、副作用が大きい設定変更。入念な事前検証やメリット/デメリットの検討を。 - コンテナ環境での便利なネットワーク機能について、裏側の仕組みや公式ドキュメントの確認を忘れずに。(セキュリティ関連の記載は特に要チェック)
SSRF対策に限らず、コンテナネットワークの routing や filtering 設定をカスタマイズしたい、調べてみたいユースケースはたくさんあると思います。本記事の内容がそのようなときにお役に立てれば幸いです。
- https://www.freedesktop.org/software/systemd/man/systemd.unit.html↩
- https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-metadata.html↩
- https://docs.docker.com/network/drivers/↩
-
自分の場合、VirtualBoxによる仮想VM環境に CentOS Stream 8 を入れてその中で docker を動かしています。VirtualBox のVMにデフォルトで設定されるNATネットワークなので、VMのIPレンジは
10.0.0.0/8
となることが確定しているので、それ以外のレンジで bridge network を作成しています。もし Docker Desktop などで物理PC上に直接Dockerを導入していて、なおかつ WiFi などでIPレンジが時と場所によって変わるような使い方ですと、これについてはより詳細に検証する必要があるかと思います。↩ - https://developers.prtimes.jp/2023/06/13/prevent-from-ssrf-with-docker-and-firewalld/↩