Dockerの特定コンテナにアクセスできるPCを制限する

## はじめに
職場で ~~遊んで~~ 仕事している最中に、
**Dockerで立ち上げた特定のコンテナへのアクセスを特定のPCに制限したい**
という状況が発生しました。
最初はコンテナ起動のオプションでいい感じにできるだろうと思っていたのですが、
どうやらDockerのオプションだけでは細かいアクセス制御はできないようでした。

最終的にはパケットフィルタ（iptables）の設定をいい感じにすることで当初の目的を達成できたので、備忘録として残しておこうと思います。
想定環境は Linux OS です（RaspberryPi を含む）。

## 結論（TL;DR）
以下の設定で、指定したIPからのみ特定のコンテナ（ポート）へのアクセスを許可することができます。当たり前ですが、ルールを複数使う場合は ```RETURN``` の後に ```REJECT``` が来るようにしてください。

```shell
sudo iptables -I DOCKER-USER -p tcp --dport コンテナが開放しているポート番号 -m conntrack --ctorigdstport ホストPCの開放ポート番号 -j REJECT
sudo iptables -I DOCKER-USER -s 接続を許可するIPアドレス -p tcp --dport コンテナが開放しているポート番号 -m conntrack --ctorigdstport ホストPCの開放ポート番号 -j RETURN
```


## 詳細説明
### iptables とは
[Wiki](https://ja.wikipedia.org/wiki/Iptables)より

>iptablesは、Linuxに実装されたパケットフィルタリングおよびネットワークアドレス変換 (NAT) 機能であるNetfilter(複数のNetfilterモジュールとして実装されている)の設定を操作するコマンドのこと。Netfilterは、いわゆるファイアウォールやルータとしての役割を果たす。

誤解を恐れずに言えば、Linux のファイアウォールです。
iptables に設定したルールに基づいてパケットの受信や破棄が行われます。
フィルタルールは上から順に適用され、最初に適用されたルールが最優先となります。

### Docker と iptables

Docker は iptables に ```DOCKER``` と ```DOCKER-USER``` という独自の チェイン（Chain）を持っています。チェインとは通信フィルタルールのグループだと思ってください。
publish オプション（-p）でコンテナを外部公開した場合、Docker は ```DOCKER``` チェインへ自動的にフィルタルールとアドレス変換（NAT）ルールを追加します。
これが良いかどうかは[各方面で議論されている](https://www.ryotosaito.com/blog/?p=492)ようですが、この機能のおかげで何も考えずとも公開されたポートにアクセスすれば、コンテナで立ち上げたサービスを利用することができるようになっています。

しかし、publish オプション（-p）だけでは、
「パソコンAはアクセスしていいけど、パソコンBからはアクセスできないようにする」
といったコンテナへのアクセス制限（IPアドレス制限）をすることはできません。
このような複雑なパケット処理のルールを追加するためには、```DOCKER-USER``` チェインに独自ルールを追加する必要があります。

### 基本となるフィルタルールとNAT
特定PCのみアクセス許可するためには、次の2つのルールを追加する必要があります。

1. パソコンAからのアクセスだったら、パケットを通すルール
2. パソコンA以外からのアクセスだったら、パケットを破棄するルール

##### 1. パソコンAからのアクセスだったら、パケットを通すルール（不完全）
特定のポート（コンテナ）へパソコンAからアクセスがあった場合は、パケットを通すルールです。プロトコルがUDPの場合は、```-p udp``` としてください。

```shell
sudo iptables -I DOCKER-USER -s パソコンAのIPアドレス -p tcp --dport ポート番号 -j RETURN
```

##### 2. パソコンA以外からのアクセスだったら、パケットを破棄するルール（不完全）
特定のポート（コンテナ）へのアクセスを全て拒否（パケットを破棄）するルールです。
このルールは上のルールよりも下に置く必要があります。

```shell
sudo iptables -I DOCKER-USER -p tcp --dport ポート番号 -j REJECT
```

**しかしこのままでは問題があります。**
実は ```--dport``` で指定するポート番号は**コンテナ側で受け付けるポート番号**になります。
例えば、nginx を以下のようなコマンドで起動したとします。

```shell
docker run -it -d -p 8080:80 --name my_nginx nginx
```

この場合、```--dport``` には **ポート8080 ではなく ポート80** を指定しなければなりません。
これはフィルタルールを適用する前に、アドレス変換（NAT）の処理が入っているためです。
このままでは下記のような構成だとうまくいかなくなります。

|サーバ名|ホスト側のポート|コンテナ側のポート|ルール|
|:---:|:---:|:---:|---|
|nginx1|**8080**|**80**|パソコンAからのみアクセス可|
|nginx2|**8081**|**80**|すべてのPCからアクセス可|

したがって、ホスト側のポート番号も考慮したフィルタを構成する必要があります。

### ホスト側のポートも考慮したフィルタルール
iptables の ```conntrack``` というオプションを利用します。

##### 1. パソコンAから特定のホストポートへのアクセスだったら、パケットを通すルール
特定のポート（コンテナ）へパソコンAからアクセスがあった場合は、パケットを通すルールです。プロトコルがUDPの場合は、```-p udp``` としてください。

```shell
sudo iptables -I DOCKER-USER -s パソコンAのIPアドレス -p tcp --dport コンテナ側のポート番号 -m conntrack --ctorigdstport ホスト側のポート番号 -j RETURN
```

##### 2. パソコンA以外から特定のホストポートへのアクセスだったら、パケットを破棄するルール
特定のポート（コンテナ）へのアクセスを全て拒否（パケットを破棄）するルールです。
このルールは上のルールよりも下に置く必要があります。

```shell
sudo iptables -I DOCKER-USER -p tcp --dport コンテナ側のポート番号 -m conntrack --ctorigdstport ホスト側のポート番号 -j REJECT
```

これで当初目的としていた動作が実現できました。

## 設定例
nginx サーバを2つ立ち上げて、片方はすべてのPCからアクセスを許可し、もう一方は特定のPCからのみアクセスを許可するように設定してみます。検証環境は以下の通りです。

|項目|バージョンなど|
|:---:|---|
|マシン|RaspberryPi4 4GB|
|OS|Linux raspberrypi 5.10.17-v8+ #1421 SMP PREEMPT Thu May 27 14:01:37 BST 2021 aarch64 GNU/Linux|
|Docker|Docker version 20.10.7, build f0df350|

|サーバ名|ホスト側のポート|コンテナ側のポート|ルール|
|:---:|:---:|:---:|---|
|nginx1|**8080**|80|```192.168.1.24``` からのみ<br>アクセス可|
|nginx2|**8081**|80|すべてのPCからアクセス可|

初期の iptables 設定状態

```shell
$> iptables -L
（中略）
Chain DOCKER-USER (1 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

Chain DOCKER (2 references)
target     prot opt source               destination
```

#### 1. iptables へのルール追加

```shell
sudo iptables -I DOCKER-USER -p tcp --dport 80 -m conntrack --ctorigdstport 8080 -j REJECT
sudo iptables -I DOCKER-USER -s 192.168.1.24 -p tcp --dport 80 -m conntrack --ctorigdstport 8080 -j RETURN
```

#### 2. コンテナの立ち上げ

```shell
$> docker run -it -d -p 8080:80 --name nginx1 nginx
$> docker run -it -d -p 8081:80 --name nginx2 nginx
$> docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                                   NAMES
73fed61ceb4f   nginx     "/docker-entrypoint.…"   5 seconds ago    Up 5 seconds    0.0.0.0:8081->80/tcp, :::8081->80/tcp   nginx2
c987876549e0   nginx     "/docker-entrypoint.…"   12 seconds ago   Up 11 seconds   0.0.0.0:8080->80/tcp, :::8080->80/tcp   nginx1
```

```shell
$> iptables -L
（中略）
Chain DOCKER-USER (1 references)
target     prot opt source               destination
RETURN     tcp  --  192.168.1.24         anywhere             tcp dpt:http ctorigdstport 8080
REJECT     tcp  --  anywhere             anywhere             tcp dpt:http ctorigdstport 8080 reject-with icmp-port-unreachable
RETURN     all  --  anywhere             anywhere

Chain DOCKER (2 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             172.17.0.2           tcp dpt:http
ACCEPT     tcp  --  anywhere             172.17.0.3           tcp dpt:http
```

#### 3. アクセス検証
##### 192.168.1.24 からのアクセス結果
![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580996/6cb573e0-2d2a-651f-a74a-0cb6efdd0865.png)

##### 192.168.1.26 からのアクセス結果
![図3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/580996/ec56476b-d504-e241-8b95-f806de83dec4.png)

狙い通り、特定のコンテナへのアクセスが制限されています。

## おわりに
今回は Docker で立ち上げた特定のコンテナへのアクセスを特定のPCに制限する方法についてまとめました。
注意点として、今回の設定は **PCを再起動すると全て消えます**。設定の永続化をする場合は、```iptables-persistent``` などを使って永続化設定を行ってください。やり方は参考資料のURLを参照してください。

## 参考資料
- [How to Whitelist IP Addresses to Access Desired Docker Containers? | by Divya Mamgai | The Startup | Medium](https://medium.com/swlh/how-to-whitelist-ip-addresses-to-access-desired-docker-containers-5f6c8fcfa7f6)
- [Docker and iptables | Docker Documentation](https://docs.docker.com/network/iptables/)
- [iptablesのnatでリダイレクト先ポートを外から直接アクセスできないようにする - 人間ハニーポット作戦](https://junk-coken.hatenablog.com/entry/2018/06/16/041047)
- [Steps for limiting outside connections to docker container with iptables? - Server Fault](https://serverfault.com/questions/704643/steps-for-limiting-outside-connections-to-docker-container-with-iptables)
- [Ubuntuでiptablesを再起動後にも保持する方法 - Qiita](https://qiita.com/yas-nyan/items/e5500cf67236d11cce72)
- [goto-linux.com | Linuxでの再起動後にiptablesルールを永続化する方法](https://goto-linux.com/ja/2019/11/21/linux%E3%81%A6%E3%81%AE%E5%86%8D%E8%B5%B7%E5%8B%95%E5%BE%8C%E3%81%ABiptables%E3%83%AB%E3%83%BC%E3%83%AB%E3%82%92%E6%B0%B8%E7%B6%9A%E5%8C%96%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/)