前提
- パブリックIPを持つサーバ
- iptablesで疎通設定をしている
- AWSのセキュリティグループのようにサーバの外側で別途疎通設定をしていない
動作確認versionは以下の通り。
- CentOS 7.5
- Docker version 18.03.1-ce
問題
docker run -p ホストOSポート:Dockerポート
のように-p
でポート設定を行うと、Dockerがiptablesに追加設定を行う。
Nginxを起動して確認する。ホストOSはiptablesで80ポートを社内からのみ許可しているとする。NginxをDockerで起動すると、80ポートが想定外に社外からアクセスできてしまうことを確認する。
docker run -it -d --privileged --name nginx -p 80:80 centos:centos7 /sbin/init
docker exec -it nginx /bin/bash
cat << 'EOF' > /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1
EOF
yum -y install nginx
systemctl start nginx
exit
原因--iptablesに追加される設定--
iptablesの各チェーンの処理フローはhttps://oxynotes.com/?p=6361の絵がわかりやすい。
普通のホストへの通信フローだと、PREROUTING -> INPUT -> ローカルのプロセス -> OUTPUT -> POSTROUTING
となる。DockerだとホストへのアクセスがDockerに転送されるのでPREROUTING -> FORWARD -> POSTROUTING
となる。
PREROUTING
docker runしたあとのiptablesには以下のnat設定が追加される。
$ sudo iptables -nL -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
中略
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:80
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80
PREROUTINGでDOCKERチェーンが呼ばれている。DOCKERチェーンでは、ホストの80にきたアクセスすべてをDOCKERの80にDNAT(送信先IPアドレスを変更)している。
FORWARD
PREROUTINGの次はFORWARDなのでnatテーブルではなくfilterテーブルを見る。
$ sudo iptables -nL
Chain INPUT (policy ACCEPT)
中略
Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-1 all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT)
中略
Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:80
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
target prot opt source destination
DROP all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
FORWARDのなかではDOCKER-USER -> DOCKER-ISOLATION-STAGE-1 -> DOCKER-ISOLATION-STAGE-2 -> DOCKER
の順にチェーンが呼ばれる。
DOCKER-ISOLATION-STAGEはDockerネットワーク間の通信を制限するためのルールとなり、今はdocker0ブリッジネットワークしか存在しないのできにしなくていい。(iptables -nvL
とvオプションをつけるとinputインタフェースとoutputインタフェースが表示されるのでわかる)
DOCKER-USERに自分でカスタマイズしたい内容を登録する。現在は何もしていない。
DOCKERではANYからDockerのIP(172.17.0.2)の80ポートへの通信をACCEPTしている。Dockerを起動する前はINPUTチェーンで80ポートへの通信を社内のみに制限していたのに、起動後に80ポートへアクセスしてもINPUTチェーンではなくFORWARDチェーンを通るので社外からもアクセスできてしまう。
解決策その1: iptables -I DOCKER-USER
DOCKERチェーンのルールを上書きするにはDOCKERチェーンより前に呼ばれているDOCKER-USERチェーンに書く。
/sbin/iptables -I DOCKER-USER -i ens160 -p tcp --dport 80 -d 172.18.0.0/16 -s 0.0.0.0/0 -j DROP
/sbin/iptables -I DOCKER-USER -i ens160 -p tcp --dport 80 -d 172.18.0.0/16 -s 許可するIP -j ACCEPT
追加したルールは、パブリックIPのネットワークインターフェースens160を通ってDockerのIPである172.17.0.0/16($ ip addr show docker0 | grep inet
で確認)の80ポートにくるアクセスについて、「許可するIP」がsourceであればACCEPTする。許可するIPに合致しなければANYでDROPしている。
$ sudo iptables -nL | grep -A7 'Chain DOCKER-USER'
Chain DOCKER-USER (1 references)
target prot opt source destination
ACCEPT tcp -- XXX.XXX.XXX.XXX 172.17.0.0/16 tcp dpt:80
ACCEPT tcp -- YYY.YYY.YYY.YYY 172.17.0.0/16 tcp dpt:80
DROP tcp -- 0.0.0.0/0 172.17.0.0/16 tcp dpt:80
RETURN all -- 0.0.0.0/0 0.0.0.0/0
※許可するIPが一つだけあるいはIPのRangeできれいに一行で表現できれば、/sbin/iptables -I DOCKER-USER -i ens160 -p tcp --dport 80 -d 172.17.0.0/16 ! -s XXX.XXX.XXX.XXX -j DROP
のように-s
を否定!
することで指定のIP以外をDROPしてもいい。
OSが再起動してiptablesが元に戻ってしまうことを考慮して、Dockerのsystemd起動設定ファイルにExecStartPost=
でiptablesへのルール追加を書いておくといい。
解決策その2: --net=host
そもそもdocker run -p ホストOSポート:Dockerポート
のように-p
でポート設定を行わないというのが二つ目の解決策。--net=host
としてdocker runするとホストのネットワークに直接つなぐことができ、ポートも共通となる。非常に簡潔だし、AWSのVPCセキュリティグループのようなものを使用せずにiptablesだけで疎通を制御しているのであれば、安全面を重視してこちらがおすすめ。ただしホストのポートが共通になるがゆえに同じコンテナを複数立てられないデメリットがある。
docker run -it -d --privileged --name nginx_net_host --net=host centos:centos7 /sbin/init
# 以下nginx起動まで略
iptablesを見てみると、PREROUTINGでもFORWARDでもDOCKERチェーンで何もしていない。PREROUTINGで何もしていないのでINPUTチェーンに入る。もともとINPUTチェーンで80への疎通を絞っていれば、その設定が適用される。
$ sudo iptables -nL -t nat | grep -A2 'Chain DOCKER '
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
$ sudo iptables -nL | grep -A2 'Chain DOCKER '
Chain DOCKER (1 references)
target prot opt source destination
ちなみにnetstatしてみても直接80ポートを使っていることがわかる。
$ sudo netstat -antp | grep :80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1529/nginx: master
-p
を使っているときは以下のようにdocker-proxyとなっている。
$ sudo netstat -antp | grep :80
tcp6 0 0 :::80 :::* LISTEN 1659/docker-proxy