最近、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を実行します。
FROM centos:centos7
RUN yum -y install httpd
CMD ["httpd","-D","FOREGROUND"]
[root@localhost ~]# docker image build -t server:httpd .
通常のポート指定をした起動だとソースはANY(0.0.0.0/0)の状態でコンテナが起動します。
[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で自動化してみました。
k8sとかでやるとできるのかな?まだ、勉強中なので他のやり方もあるかと思います。
3. Ansibleで自動化
ここでは、処理を実行するPlaybookと別ファイルで定義したExtra Varsファイルで構成しています。
Extra Varsファイルではコンテナ情報やアクセスNWを定義します。
3-1. Playbook
コンテナ起動時後に対象コンテナだけのNAT設定を削除しているのは、設定を全部入れ替えるためです。
Ansibleは「常にこの状態にあるべき」という冪等性を保つためにこの設定を入れています。
そのため、毎回入れる設定は前回入れた設定に追加したものか削除したものを指定する必要があります。
---
- 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
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. 実行
実行します。
[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も追加してみます。
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
実行します。
[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にしか対応していません。