iptablesでDockerコンテナへのアクセス制限をする

## やりたかったこと
* コンテナを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](http://docs.docker.jp/engine/userguide/networking/dockernetworks.html)に以下のような記載があります。
>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](https://github.com/docker/docker/issues/12294)）

ポートフォワーディングのiptablesを戻すにはDocker Engineの再起動か、自力でルールを書き直すしかありません。なので**iptablesの再起動は厳禁**です。

とりあえずコンテナへのアクセス制御ルールはDocker Engineの起動をトリガーに設定したいところ。
そのため、僕の環境ではsystemdでDocker Engineの起動後にiptablesルールを追加しています。（この辺のベストプラクティスがあったら教えてほしいです。）

```/etc/systemd/system/docker.service.d/docker.conf
[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
```