AnsibleでDockerコンテナ起動とiptablesでアクセス制御をする

最近、Dockerをお勉強中なのですがAnsibleを使ってDockerコンテナのNATとアクセス制御をiptablesで直接制御してみた検証記録です。

# 1. 環境

|   項目  |          バージョン          |
|---------|------------------------------|
| OS      | RHEL7.5                      |
| Ansible | 2.4.2.0                      |
| Docker  | 1.13.1, build 6e3bb8e/1.13.1 |

# 2. 検証内容

ここでは、httpdが起動するコンテナイメージ(server:httpd)を作ってから、そのコンテナを起動した後にNATの設定と接続制御の設定するPlaybookを実行します。

```yaml:Dockerfile
FROM centos:centos7
RUN yum -y install httpd
CMD ["httpd","-D","FOREGROUND"]
```

```shell-session
[root@localhost ~]# docker image build -t server:httpd .
```

通常のポート指定をした起動だとソースはANY(0.0.0.0/0)の状態でコンテナが起動します。

```shell-session
[root@localhost ~]# docker run -d -it -p 8080:80 --name web_server01 server:httpd
[root@localhost ~]# iptables -nL
Chain INPUT (policy DROP)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
(snip)
Chain OUTPUT (policy DROP)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp spt:22

Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            172.17.0.2           tcp dpt:80
(snip)
```

これだとどこからでもアクセスできてしまうため、セキュリティ上好ましく無いと思いアクセス可能なソースIPを指定できるようAnsibleで自動化してみました。
<font color="#FF0000">k8sとかでやるとできるのかな？まだ、勉強中なので他のやり方もあるかと思います。</font>

# 3. Ansibleで自動化

ここでは、処理を実行するPlaybookと別ファイルで定義したExtra Varsファイルで構成しています。
Extra Varsファイルではコンテナ情報やアクセスNWを定義します。

## 3-1. Playbook

コンテナ起動時後に対象コンテナだけのNAT設定を削除しているのは、設定を全部入れ替えるためです。
Ansibleは「常にこの状態にあるべき」という冪等性を保つためにこの設定を入れています。
そのため、毎回入れる設定は前回入れた設定に追加したものか削除したものを指定する必要があります。

```yaml:example.yml
---
- name: TEST Docker Container NAT Playbook.
  hosts: localhost
  gather_facts: no
  tasks:
    # run countainer.
    - block:
      - name: run docker container.
        docker_container:
          name: "{{ container_name }}"
          image: "{{ image }}"
          state: "{{ state }}"
          tty: yes
          detach: yes

      - name: Get container IP.
        shell: "docker inspect -f '{% raw %}{{ .NetworkSettings.IPAddress }}{% endraw %}' {{ container_name }}"
        register: container_ip

      # Delete DNAT config.
      - shell: "iptables -t nat -S POSTROUTING | grep {{ container_ip.stdout }} | sed -e s/^-A/-D/ | xargs -L 1 iptables -t nat"
        ignore_errors: yes
      - shell: "iptables -t nat -S DOCKER | grep {{ container_ip.stdout }} | sed -e s/^-A/-D/ | xargs -L 1 iptables -t nat"
        ignore_errors: yes
      - shell: "iptables -S DOCKER | grep {{ container_ip.stdout }} | sed -e s/^-A/-D/ | xargs -L 1 iptables"
        ignore_errors: yes

      - iptables:
          table: nat
          chain: POSTROUTING
          source: "{{ container_ip.stdout }}/32"
          destination: "{{ container_ip.stdout }}/32"
          destination_port: "{{ item.container_port }}"
          protocol: "{{ protocol }}"
          match: "{{ protocol }}"
          jump: MASQUERADE
        with_items: "{{ dnats }}"

      - iptables:
          table: nat
          chain: DOCKER
          in_interface: "!docker0"
          protocol: "{{ protocol }}"
          match: "{{ protocol }}"
          destination_port: "{{ item.dnat_port }}"
          to_destination: "{{ container_ip.stdout }}:{{ item.container_port }}"
          jump: DNAT
        with_items: "{{ dnats }}"

      - iptables:
          chain: DOCKER
          source: "{{ item.src_ip | default('0.0.0.0/0') }}"
          destination: "{{ container_ip.stdout }}/32"
          in_interface: "!docker0"
          out_interface: docker0
          protocol: "{{ protocol }}"
          match: "{{ protocol }}"
          destination_port: "{{ item.container_port }}"
          jump: ACCEPT
        with_items: "{{ dnats }}"

      - shell: iptables-save > /etc/sysconfig/iptables
      when: state == "started"

    # stop container.
    - block:
      - name: Get container IP.
        shell: "docker inspect -f '{% raw %}{{ .NetworkSettings.IPAddress }}{% endraw %}' {{ container_name }}"
        register: container_ip

      - name: delete docker container.
        docker_container:
          name: "{{ container_name }}"
          state: "{{ state }}"

      # Delete DNAT config.
      - shell: "iptables -t nat -S POSTROUTING | grep {{ container_ip.stdout }} | sed -e s/^-A/-D/ | xargs -L 1 iptables -t nat"
        ignore_errors: yes
      - shell: "iptables -t nat -S DOCKER | grep {{ container_ip.stdout }} | sed -e s/^-A/-D/ | xargs -L 1 iptables -t nat"
        ignore_errors: yes
      - shell: "iptables -S DOCKER | grep {{ container_ip.stdout }} | sed -e s/^-A/-D/ | xargs -L 1 iptables"
        ignore_errors: yes
      - shell: iptables-save > /etc/sysconfig/iptables
      when: state == "absent"
```

## 3-2. Extra Vars

```yaml:extra_vars
container_name: web_server01
protocol: tcp
image: server:httpd
state: started #(e.g. started or absent)
dnats:
  - src_ip: 192.168.1.1/32
    dnat_port: 8080
    container_port: 80
```

## 3-3. 実行

実行します。

```shell-session
[root@localhost ~]# ansible-playbook example.yml -e @extra_vars
[root@localhost ~]# iptables -nL
Chain INPUT (policy DROP)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
(snip)
Chain OUTPUT (policy DROP)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp spt:22

Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  192.168.1.1          172.17.0.2           tcp dpt:80
(snip)
```

コンテナへのアクセス許可IPが `192.168.1.1` になっていることが確認できます。
`192.168.1.1` 以外から `8080` へアクセスしてもコンテナの `80` にはアクセスできないことを確認します。
次に他のIPも追加してみます。

```yaml:extra_vars
container_name: web_server01
protocol: tcp
image: server:httpd
state: started #(e.g. started or absent)
dnats:
  - src_ip: 192.168.1.1/32
    dnat_port: 8080
    container_port: 80
  - src_ip: 192.168.1.2/32
    dnat_port: 8080
    container_port: 80
```

実行します。

```shell-session
[root@localhost ~]# ansible-playbook example.yml -e @extra_vars
[root@localhost ~]# iptables -nL
Chain INPUT (policy DROP)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
(snip)
Chain OUTPUT (policy DROP)
target     prot opt source               destination
ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp spt:22

Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  192.168.1.1          172.17.0.2           tcp dpt:80
ACCEPT     tcp  --  192.168.1.2          172.17.0.2           tcp dpt:80
(snip)
```

`192.168.1.2` が追加されていることが確認できます。
`192.168.1.1` `192.168.1.2` 以外からはコンテナへアクセスできないことが確認できます。

# 4. 注意

このやり方は、一旦入っている設定を削除して上書きするやり方なので瞬断が発生します。
また、コンテナ側は1つのIPにしか対応していません。