やりたかったこと
- コンテナをDockerホストの外部に公開するときのアクセス制御設定(ファイアーウォール設定)
やったこと
- Dockerのポートフォワーディングでコンテナをホスト外部に公開してiptablesでアクセス制限
- 外部ネットワークにはコンテナ443/tcpのみをアクセスを許可
- 内部ネットワークにはコンテナのすべてのポートを許可
- Docker再起動時にiptablesルールを再設定するようにDockerのsystemdで設定
前提
- Docker 1.12.3
- CentOS7.2
- iptables 1.4.21
Dockerのポートフォワーディング設定
Dockerでコンテナをホストの外と通信させたい時、-p
オプションでポートフォワーディングを設定すると思いますが、このポートフォワーディングを設定するとiptablesに以下のような設定が追加されます。
$ docker run -p 443:443 -d nginx
$ iptables -nL
~~ 省略 ~~
Chain DOCKER (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.2 tcp dpt:443
$ iptables -t nat -nL
~~ 省略 ~~
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:443
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 to:172.17.0.2:443
上の設定ではnginxコンテナの443ポートをホストの443ポートにフォワードして公開してるのですが、これ、ホストでのINPUTの設定にかかわらず、**ホストのもつすべてのネットワークに公開されてます。**なのでIPやポートで制限を行いたいのです。
iptablesでコンテナへのアクセスを制限する
Docker Docsに以下のような記載があります。
Docker のデフォルト転送ルールは、全ての外部ソースの IP アドレス対して許可しています。コンテナを特定の IP アドレスやネットワークに対してのみ接続したい場合には、 DOCKER フィルタ・チェーンの一番上にネガティブ・ルールを追加します。例えば、コンテナが外部の IP アドレス 8.8.8.8 をソースとするもの しか 許可しない場合には、次のようなルールを追加します。
$ iptables -I DOCKER -i ext_if ! -s 8.8.8.8 -j DROP
というわけで、iptablesのDocker Chainにルールを追加してあげればホスト外からコンテナへのアクセスを制御できます。
うちはリバースプロキシコンテナとAPコンテナが載ってるDockerホストがあって、ホストサーバには外部ネットワークと内部ネットワークが接続されています。そこでリバースプロキシコンテナを外部に公開します。
こんな感じで設定しました。
- 外部ネットワーク(eth0)には443/tcpから許可
- 内部ネットワーク(eth1)はすべて許可(明示的な設定はありません)
$ iptables -I DOCKER -i eth0 -j DROP
$ iptables -I DOCKER -i eth0 -p tcp --dport 443 -m state --state NEW -j ACCEPT
Chain DOCKER (1 references)
pkts bytes target prot opt in out source destination
0 0 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 state NEW
0 0 DROP all -- eth0 * 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.2 tcp dpt:443
0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.3 tcp dpt:8080
172.17.0.2
がリバースプロキシコンテナで、443/tcpを、172.17.0.3
がAPコンテナで、8080/tcpを公開してますが、上位のルールで外部ネットワーク(eth0)では443/tcpしか通さないようになっています。
172.17.0.0/16
はDockerが設定するコンテナネットワークで、コンテナから外のアクセスは許可しています。
またこの設定はコンテナの443/tcpポートのみへの接続を許可してますが、ポートバインディングでコンテナの443/tcpをホストの別のポートに割り当てた場合も許可の対象です。つまり
$ docker run -p 10443:443 -d nginx
とした場合はホストの10443/tcpポートを経由したコンテナアクセスは許可されます。
Docker起動時のiptables設定
iptablesのDocker Chainは、Docker Engineが動的にiptablesルールを追加するものなので、iptablesサービスを利用している場合、iptablesを再起動するとコンテナのポートフォワーディングのiptablesルールが吹き飛び、ホストの外からコンテナへアクセスできなくなります。
(issueにも上がってます。Docker networking fails after iptables service is restarted #12294)
ポートフォワーディングのiptablesを戻すにはDocker Engineの再起動か、自力でルールを書き直すしかありません。なのでiptablesの再起動は厳禁です。
とりあえずコンテナへのアクセス制御ルールはDocker Engineの起動をトリガーに設定したいところ。
そのため、僕の環境ではsystemdでDocker Engineの起動後にiptablesルールを追加しています。(この辺のベストプラクティスがあったら教えてほしいです。)
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd $OPTIONS
ExecStartPost=/sbin/iptables -I DOCKER -i eth0 -j DROP
ExecStartPost=/sbin/iptables -I DOCKER -i eth0 -p tcp --dport 80 -m state --state NEW -j ACCEPT
他のアプローチ
自分でDocker用のiptables設定をすべて書く
Docker Engineの起動オプションで--iptables=false
を指定してあげれば、Dockerはiptablesを全く書き換えません。そのため、自分でiptablesのルールを書いてあげればより柔軟に高度なアクセス制限ができます。
しかしその反面、いままでdocker run
の-p
オプションがやってくれたポートフォワーディングのルールも自分で書かなければいけないのでかなり面倒です。
--net=host
でコンテナをホストのネットワークに直接繋いで、ホストのiptablesで制御する
デフォルトだとコンテナはdocker0
というブリッジネットワークに接続しますが、オプションでホストのネットワークに直接つなぐことができます。しかし使用するポートは全てホストのものを使うため、1つのコンテナでポートをたくさん使うようなものや、ホストにたくさんコンテナを立てる場合はポートが不足する可能性もあります。また、同じコンテナを複数立てる場合はコンテナ内のミドルウェアの設定でポートを競合しないように変えなくてはならないので大変です。
おわりに
これ書いてて、そもそもちゃんとしたサービスを公開する時はだいたい前段にそれなりのファイアーウォール機器やロードバランサをいれるから、Dockerホストでアクセス制限考えることは少ないからあんまり意味ないな、とも思いました。
Dockerにおけるiptables設定の知見ってあんまり見ない気がしますが、どうなんでしょう。それかdocker network
でうまく設定しろよってことなんですかね。
追記(2016/12/6)
docker run
の-p
オプションで、公開するIPアドレスを指定できることをすっかり見落としてました…。こんな感じでコンテナ作れば公開するネットワークは1つに制限できますね。
$ docker run -p 192.168.11.5:443:443 -d nginx