背景
Docker コンテナで WEB アプリケーションを公開する際、docker-compose を用いてポートを ports オプションで publish し、別途用意してあるリバースプロキシを介して外部公開させました。
外部からコンテナへの通信は publish されたポートのみアクセスできるよう制限されているものの、コンテナから外部への通信は制限されていません。
そこで、万一アプリケーションの脆弱性によりアプリケーションから悪意のある通信が行われた場合に備えて、コンテナが通信できるプライベート IP は任意のもののみに制限することにしました。
本記事はその際に調べた Docker ネットワークと iptables の詳細と、コンテナから外部への通信を制限した際の設定内容を記述します。
Docker ネットワークとは
Docker ネットワークは Docker コンテナがホストと通信をするため、または別のコンテナ、そして異なるネットワークと通信するために利用されます。
Docker ネットワークには 3 種類の動作が異なるドライバが用意されており、bridge, host, none が存在します。
公式マニュアルでの Networking や、Docker Tutorials and Labs の Docker Networking 資料による具体例を元にした説明が役に立ちます。
参考情報: [GitHub - docker/labs](https://github.com/docker/labs/tree/master/networking
参考情報: docker docs > Networking overview
Docker ネットワークの詳細
bridge, host, none ネットワークの詳細を説明します。
尚、書かれている内容は Docker v18.03 における情報を参考にしました。
bridge ネットワークドライバの詳細
bridge はデフォルトのネットワークドライバです。
ブリッジネットワークはアプリケーションがスタンドアロンコンテナで動作する際に、コミュニケーションをとるために使われます。(参考情報: Use bridge networks)
- ネットワーク用語において bridge とは、ネットワーク間のトラフィックをフォワードする Link Layer デバイスである
- bridge はハードウェア又は kernel で動作するソフトウェアとして実装される
- Docker 用語において bridge はソフトウェア bridge を指す
- 同一 bridge ネットワークに接続されているコンテナが通信できる
- bridge ネットワークに接続していないコンテナとは通信できない
- 上記ルールは自動でインストールされる(Linux では Docker host の iptables にルールが追加される)
- bridge ネットワークは同一 Docker ホスト上で適用され、異なる Docker ホスト間は OS レベルのルーティング又は overlay ネットワークで通信させることが出来る
-
--link
オプションを使わない限り、ブリッジネットワーク内のコンテナはお互いを IP アドレスでアクセスする - ユーザ定義の bridge ネットワークではコンテナは名前又はエイリアスで互いに解決できる
- コンテナ実行中にユーザ定義のネットワークへ接続・切断が出来る
- デフォルトの bridge ネットワークは iptables と MTU を共有しており、それらの設定を行うことが出来るがコンテナ外部で設定されるため、Docker を再起動する必要があります
-
docker network create
でユーザ定義ネットワークを作成することが出来る - デフォルトの bridge ネットワークでは環境変数を共有できない
-
--link
オプションを使うことで共有することが出来る - ユーザ定義 bridge ネットワークでは環境変数を共有できない
- 複数コンテナで共有できる方法は次の 3 つです
- Docker volume でファイルやディレクトリ等の共有情報を複数のコンテナでマウントさせる
- docker-compose で複数のコンテナを一緒に開始させる
- スタンドアロンコンテナの代わりに swarm サービスを使う
-
[
{
"Name": "bridge",
"Id": "ad7fcb93799ce4bf7cd65db271feacdfd16e62e8579d2c14f3b3026f1d94ed3a",
"Created": "2018-09-30T14:27:42.551956224Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"e02ca3ee8d40b03cfa4503c0b5bc6a103909b5c2d13bbfc0c03cbf0a4005ab6a": {
"Name": "gallant_volhard",
"EndpointID": "e885851033138b7748d252afb0da13826360fb8802445536a80c0e97de44138b",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
host ネットワークの詳細
スタンドアロンコンテナの場合、host ネットワークはコンテナと Docker ホスト間のネットワークを共有します。
例えばポート 80 にバインドするコンテナを実行してホストネットワークを使用する場合、コンテナのアプリケーションはホストの IP アドレスのポート 80 で使用できる。
host ネットワークドライバは Linux ホストでのみ動作し、Docker for MAC, Docker for Windows または Docker EE for Windows Server ではサポートされていません。
[
{
"Name": "host",
"Id": "84e49cbca104637ee19edf06c6f15c0171efcadc2a08da2d4e6d6daaaccef26c",
"Created": "2018-01-18T02:08:37.994752736Z",
"Scope": "local",
"Driver": "host",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
none ネットワークの詳細
none ネットワークはネットワーク機能が全く必要としないコンテナに対して設定するためのネットワークドライバです。
none ネットワークを使用してコンテナを起動すると、ネットワーク機能は完全に無効化されます。
[
{
"Name": "none",
"Id": "3793b20ebb28b8d256f042f449023750de9569fda9dec5e512434d9e65491080",
"Created": "2018-01-18T02:08:37.988729433Z",
"Scope": "local",
"Driver": "null",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
Docker デーモン起動時に追加される iptables の設定
Docker デーモンが起動すると次のように iptables に Docker コンテナ用の設定が追加されていることが分かります。
Chain INPUT (policy ACCEPT 36612 packets, 271M bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
24 2960 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
24 2960 DOCKER-ISOLATION all -- * * 0.0.0.0/0 0.0.0.0/0
12 2040 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
12 920 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 26829 packets, 1329K bytes)
pkts bytes target prot opt in out source destination
Chain DOCKER (1 references)
pkts bytes target prot opt in out source destination
Chain DOCKER-ISOLATION (1 references)
pkts bytes target prot opt in out source destination
24 2960 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
24 2960 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
docker-compose におけるネットワーク
docker-compose では docker-compose.yml に記述された services が所属する bridge network が作成されます。
そのため、コンテナへのアクセスを許可するには -p
オプションと同様に ports オプションでポートを公開する必要があります。
また、bridge ネットワークに所属しているためコンテナに割り当てられる IP アドレスは bridge ネットワークのセグメントのものとなります。
[
{
"Name": "app_default",
"Id": "388e266f99936126b073c0e6d66a1e6ecc0eaf7e8c96ff5dc5f608eb26f71e8c",
"Created": "2018-09-30T17:07:24.415316566Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"2fb05af8bd06597631f8dba72a7530b7762b9cc7a740a3b0115015121bfd1970": {
"Name": "app_app_1",
"EndpointID": "e5921d8f559503282906a2b6856d3d96745b382b05ed590c96207b97cc2b52dd",
"MacAddress": "02:42:ac:12:00:04",
"IPv4Address": "172.18.0.4/16",
"IPv6Address": ""
},
"61072fd28a1c0e8783ad44d878a7d52924da6d3437ad72edfd014e632db4f1ef": {
"Name": "app_elasticsearch_1",
"EndpointID": "f097dfaec1a0310df215dd9faa3b9e0e817e1c32a942536aa6ef560c4abdaf12",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"dd4588c652dd96254f90953be7dc835393a259f8ad61f235fa8eabce3496d1d6": {
"Name": "app_mongo_1",
"EndpointID": "6ebe62c253492d9a170d3985b71a9e8cbfcdf252269792d63365ffc40004ee03",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "app"
}
}
]
docker-compose によりコンテナを起動させた後の iptables を見ると次のとおり★が付いた箇所が変化しています。
Chain INPUT (policy ACCEPT 355 packets, 19816 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
1391 1084K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
1391 1084K DOCKER-ISOLATION all -- * * 0.0.0.0/0 0.0.0.0/0
1278 1078K ACCEPT all -- * br-388e266f9993 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED (★1)
19 1140 DOCKER all -- * br-388e266f9993 0.0.0.0/0 0.0.0.0/0 (★2)
94 4566 ACCEPT all -- br-388e266f9993 !br-388e266f9993 0.0.0.0/0 0.0.0.0/0 (★3)
19 1140 ACCEPT all -- br-388e266f9993 br-388e266f9993 0.0.0.0/0 0.0.0.0/0 (★4)
18 2544 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
18 1424 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 202 packets, 19112 bytes)
pkts bytes target prot opt in out source destination
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT tcp -- !br-388e266f9993 br-388e266f9993 0.0.0.0/0 172.18.0.4 tcp dpt:3000 (★5)
Chain DOCKER-ISOLATION (1 references)
pkts bytes target prot opt in out source destination
0 0 DROP all -- docker0 br-388e266f9993 0.0.0.0/0 0.0.0.0/0 (★6)
0 0 DROP all -- br-388e266f9993 docker0 0.0.0.0/0 0.0.0.0/0 (★7)
1427 1088K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
1427 1088K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
まず、br-388e266f9993 は Docker ホストに追加されたインタフェースです。
コンテナのデフォルトゲートウェイとなります。
以下、★x についての説明を記述します。
尚、説明において br-388e266f9993 に所属するコンテナネットワークをコンテナネットワークと呼ぶことにします。
- (★1) 外部からコンテナネットワークへの通信で、ステータスが RELATED 又は ESTABLISHED のものを許可する (コンテナと接続完了した通信は許可する)
- (★2) DOCKER チェーンが許可する通信であれば、外部からコンテナネットワークへの通信を許可する
- (★3) コンテナネットワークからコンテナネットワーク以外への通信を許可する
- (★4) コンテナネットワーク間の通信を許可する
- (★5) 外部からコンテナネットワークへの 172.18.0.4:3000 に対する TCP 通信を許可する
- (★6) Docker ホストからコンテナネットワークへの通信を拒否する
- (★7) コンテナネットワークから Docker ホストへの通信を拒否する
★5に書かれている設定が docker-compose.yml に ports で書かれた内容が反映された設定です。
version: '3'
services:
app:
ports:
- 3000:3000
: <snip>
このように、docker-compose によりコンテナを起動すると、既存の FORWARD チェーンにルールが追加され、その設定によってコンテナと外部ネットワーク間の通信が制御されます。
ここで、FORWARD チェーンに追加されるルールは DOCKER-USER, DOCKER-ISOLATION よりも下で docker0 に関する設定より上に位置するように挿入されるようでした。
iptables について
参考情報:Linux 2.4 Packet Filtering HOWTO
参考情報:Iptables Tutorial 1.2.2
参考情報:man iptables
iptables はパケット処理(パケット送受信やルーティング、NAT 等)をする際に、特定のフィルタ処理(パケット破棄・DNAT/SNAT・パケットロギング 等)を行うことが出来るソフトウェアです。
iptables ではパケット処理を行う目的に応じて、複数のフィルタルールをまとめてテーブルと呼ばれる名前で管理する。(正しくはフィルタルールではなく、それらを複数まとめて処理フェーズの名前で管理するチェーンを管理するのがテーブルである)
テーブルは既定されているもののみ扱うことができ、テーブルの追加や削除はできない。
先に紹介した通り、Linux の場合 Docker コンテナの通信は iptables によって制御されています。
テーブルには次のものがある。(iptables v1.6 にて確認)
- filter テーブル(オプションでテーブルを指定しなかった場合のデフォルト値)
- 受信・送信・ルーティングするパケットに対して設定するフィルタルールを管理するテーブルである。
- 受信後フィルタとして INPUT チェイン、送信後フィルタとして OUTPUT チェイン、ルーティング時フィルタとして FORWARD チェインが既定で用意されている。
- nat テーブル
- ネットワークアドレス変換(DNAT / SNAT / MASQURADE / REDIRECT)やポートフォワード等、パケットの送信元・送信先情報を変更する目的で、パケットに対して設定するフィルタルールを管理するテーブルである。
- 受信後フィルタとして PREROUTING チェイン、変更後のパケットの送信前フィルタとして OUTPUT チェイン、変更後のパケットの送信後フィルタとして POSTROUTING チェインが既定で用意されている。
- nat テーブルでは DROP アクションを持つフィルタルールは登録できない。(あくまでネットワークアドレスの書き換えに使う)
- mangle テーブル
- TOS / TTL 等を書き換えるような特別な目的でパケットに対して設定するフィルタルールを管理するテーブルである。
- POSTROUTING チェーン、OUPUT チェーン、INPUT チェーン、FORWARD チェーン、PREROUTING チェーンが既定で用意されている。(Linux Kernel 2.4.18 以降)
- Linux Kernel 2.4.17以前:PREROUTING チェーン、OUTPUT チェーン のみであった
- Linux Kernel 2.4.18以降:INPUT チェーン、FORWARD チェーン、POSTROUTING チェーン が追加された
- raw テーブル
- コネクション追跡を行わないよう NOTRACK ターゲットを適用するためのフィルタルールを管理するテーブルである。NOTRACK ターゲットが適用されると、conntrack は該当のパケットに対するコネクション追跡を行わなくなる。(参考情報:Iptables Tutorial 1.2.2 - The conntrack entries)
- PREROUTING チェーンと、OUTPUT チェーンしかない。(conntrack が処理される前段階のものがこの 2 つだけ)
- security テーブル
- 強制アクセスコントロール(Mandatory Access Control)を行う目的でパケットに対して設定するフィルタルールを管理するテーブルである。SELinux で実装されている。
- INPUT チェーン、OUTPUT チェーン、FORWARD チェーンが既定で設定されている。
上記テーブルのフィルタ処理は次のフローで処理されます。
(Iptables Tutorial 1.2.2 より引用)
docker-compose で起動させた Docker コンテナから送出するパケットを制限する
これまで見てきた内容を踏まえて、Docker コンテナから送出するパケットを制御する方法として次の手法を取ることにしました。
- docker-compose は systemd の Unit として自動起動させる (参考 URL)
- 但し、docker network は停止時に破棄しないよう設定する (
ExecStop=/usr/local/bin/docker-compose down
は指定しない) - ※ docker network が作成されるタイミングで iptables の FORWARD チェーンに最優先のルールが追加されるため、パケット制御するルールを優先させるために network を破棄しないようにする
- 但し、docker network は停止時に破棄しないよう設定する (
- docker-compose で作成する network の IP アドレスは subnet で指定する (仮に 172.20.0.0/16 とします)
- ※ subnet を指定しない場合、172.18.0.0/16, 172.19.0.0/16, ... と自動設定されます
- ※ subnet が既に使われている場合は、その IP アドレスを避けて自動設定されます
- ※ 固定指定された場合に subnet が同じ場合はコンテナが起動しません
- Docker デーモンが起動した後に FORWARD チェーンにルールを追加する
- ルールが最優先されるように 1 番目に挿入する
- ※ Docker デーモンが起動する前にルールを追加すると、Docker デーモンが起動した際に追加されるルールが優先されます
- ルールを追加するため、Systemd で docker service に対して
ExecStartPost
を設定する
- ルールが最優先されるように 1 番目に挿入する
- FORWARD チェーンに挿入するルールは IP アドレスをベースとする
以下、手順です。
$ sudo systemctl edit docker
下記内容を記述して保存する。(/etc/systemd/system/docker.service.d/override.conf に保存される)
[Service]
ExecStartPost=/sbin/iptables -N DOCKER-OUTBOUND-FILTER
ExecStartPost=/sbin/iptables -A DOCKER-OUTBOUND-FILTER -s 172.20.0.0/16 -d 172.20.0.0/16 -j ACCEPT -m comment --comment "Accept packets between Docker containers"
ExecStartPost=/sbin/iptables -A DOCKER-OUTBOUND-FILTER -s 172.20.0.0/16 -d 192.168.100.1 -j ACCEPT -m comment --comment "Accept packets between Docker containers and HTTP Proxy Server"
ExecStartPost=/sbin/iptables -A DOCKER-OUTBOUND-FILTER -s 172.20.0.0/16 -d 192.168.100.2 -p udp --dport 53 -j ACCEPT -m comment --comment "Accept packets between Docker containers and DNS Server"
ExecStartPost=/sbin/iptables -A DOCKER-OUTBOUND-FILTER -s 172.20.0.0/16 -d 192.168.0.0/16 -j DROP -m comment --comment "Drop packets from Docker containers to Private Network Address"
ExecStartPost=/sbin/iptables -A DOCKER-OUTBOUND-FILTER -j RETURN
ExecStartPost=/sbin/iptables -I FORWARD 1 -j DOCKER-OUTBOUND-FILTER
$ sudo systemctl start docker-compose@some-compose-app
$ sudo systemctl enable docker-compose@some-compose-app
以上で次回 OS 起動時に Docker コンテナから送出するパケットを制御できます。
尚、OS 起動後に docker-compose で 2 種類のコンテナ群が起動した際の iptables の結果を参考に記載します。
Chain INPUT (policy ACCEPT 45 packets, 4833 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
23 1172 DOCKER-OUTBOUND-FILTER all -- * * 0.0.0.0/0 0.0.0.0/0
23 1172 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
23 1172 DOCKER-ISOLATION all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
12 512 ACCEPT all -- * br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABL
ISHED
11 660 DOCKER all -- * br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-ccaef8da7d18 !br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
11 660 ACCEPT all -- br-ccaef8da7d18 br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- * br-3b054d11a74d 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABL
ISHED
0 0 DOCKER all -- * br-3b054d11a74d 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-3b054d11a74d !br-3b054d11a74d 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-3b054d11a74d br-3b054d11a74d 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 27 packets, 4481 bytes)
pkts bytes target prot opt in out source destination
Chain DOCKER (3 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT tcp -- !br-ccaef8da7d18 br-ccaef8da7d18 0.0.0.0/0 172.19.0.4 tcp dpt:3000
Chain DOCKER-ISOLATION (1 references)
pkts bytes target prot opt in out source destination
0 0 DROP all -- br-ccaef8da7d18 docker0 0.0.0.0/0 0.0.0.0/0
0 0 DROP all -- docker0 br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
0 0 DROP all -- br-3b054d11a74d docker0 0.0.0.0/0 0.0.0.0/0
0 0 DROP all -- docker0 br-3b054d11a74d 0.0.0.0/0 0.0.0.0/0
0 0 DROP all -- br-3b054d11a74d br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
0 0 DROP all -- br-ccaef8da7d18 br-3b054d11a74d 0.0.0.0/0 0.0.0.0/0
23 1172 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-OUTBOUND-FILTER (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT all -- * * 172.20.0.0/16 172.20.0.0/16 /* Accept packets between Docke
r containers */
0 0 ACCEPT all -- * * 172.20.0.0/16 192.168.100.1 /* Accept packets between Docke
r containers and HTTP Proxy Server */
0 0 ACCEPT udp -- * * 172.20.0.0/16 192.168.100.2 udp dpt:53 /* Accept packets be
tween Docker containers and DNS Server */
0 0 DROP all -- * * 172.20.0.0/16 192.168.0.0/16 /* Drop packets from Docker con
tainers to Private Network Address */
23 1172 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
23 1172 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
最後に
紹介した方法では docker-compose が作成する network が再作成されたタイミングでルールの優先度が変わり、送出パケットの制御が出来なくなる点が問題です。(下記のように docker-compose コンテナ用に作成した network 用のルールが DOCKER-OUTBOUND-FILTER より優先されます)
Chain FORWARD (policy DROP 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 DOCKER-ISOLATION all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- * br-c4d330f8b3c3 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * br-c4d330f8b3c3 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-c4d330f8b3c3 !br-c4d330f8b3c3 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-c4d330f8b3c3 br-c4d330f8b3c3 0.0.0.0/0 0.0.0.0/0
1688 189K DOCKER-OUTBOUND-FILTER all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
1669 188K ACCEPT all -- * br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
19 1140 DOCKER all -- * br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- br-ccaef8da7d18 !br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
19 1140 ACCEPT all -- br-ccaef8da7d18 br-ccaef8da7d18 0.0.0.0/0 0.0.0.0/0
コンテナのバージョンアップ時などは docker-compose down を実施せずに、docker-compose stop, rm でコンテナのみを削除して network は削除しないようにするなど配慮が必要である。
もっと良い方法があれば追記します。