前提

  • パブリック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の各チェーンの処理フローはhttp://sawara.me/linux/828/の絵がわかりやすい。

普通のホストへの通信フローだと、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