使用iptables管控docker容器
docker与iptables说明
某些项目考虑到安全问题,需要启用iptables来进行加固。根据官方文档介绍(https://dockerdocs.cn/network/iptables/):
在Linux上,Docker操纵iptables
规则以提供网络隔离。尽管这是实现的详细信息,并且您不应修改Docker在iptables
策略中插入的规则,但是如果您想要拥有自己的策略(而不是由Docker管理的策略),它确实会对您需要执行的操作产生一些影响。
如果您在暴露于Internet的主机上运行Docker,则可能需要适当的iptables策略,以防止未经授权访问您主机上运行的容器或其他服务。此页面描述了如何实现此目标以及需要注意的注意事项。
Docker安装了两个名为DOCKER-USER
和的自定义iptables链DOCKER
,它确保始终由这两个链首先检查传入的数据包。
Docker的所有iptables
规则都已添加到DOCKER
链中。请勿手动操作此链条。如果您需要添加在Docker规则之前加载的规则,则将它们添加到DOCKER-USER
链中。在Docker自动创建任何规则之前,将应用这些规则。
在这些链之后,FORWARD
将评估添加到链中的规则(手动添加或通过另一个基于iptables的防火墙)。这意味着,如果您通过Docker公开端口,则无论防火墙配置了什么规则,该端口都会公开。如果您希望即使在通过Docker公开端口时也要应用这些规则,则必须将这些规则添加到 链中。DOCKER-USER
通过如上资料已经确定,只需要在 DOCKER-USER
链上进行改动即可。
iptables规则设置
注意:设定规则时,请带上 -i [网卡名] ,否则会发生误差的情况。
规则设定
默认情况下,允许所有外部源IP连接到docker主机。查看DOCKER-USER
链
root@localhost(192.168.199.41)~>iptables -nvL
Chain INPUT (policy ACCEPT 71 packets, 5105 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DOCKER-USER all -- * * 0.0.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
0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 33 packets, 4924 bytes)
pkts bytes target prot opt in out source destination
Chain DOCKER (1 references)
pkts bytes target prot opt in out source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
pkts bytes target prot opt in out source destination
0 0 DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.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)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * docker0 0.0.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) #重点关注该链
pkts bytes target prot opt in out source destination
0 0 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
禁止访问docker
既然要做限制,第一条肯定是全部禁用,然后再添加需要放开的IP及端口。
#禁止外部访问docker容器
root@localhost(192.168.199.41)~>iptables -I DOCKER-USER -i eth0 -j REJECT
root@localhost(192.168.199.41)~>iptables -nvL DOCKER-USER
Chain DOCKER-USER (1 references)
pkts bytes target prot opt in out source destination
0 0 REJECT all -- eth0 * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable
0 0 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
启动一个容器
root@localhost(192.168.199.41)~>docker run -d --name ngx -p 80:80 nginx:alpine
尝试从外部访问
root@localhost(192.168.199.108)~>curl 192.168.199.41
curl: (7) Failed connect to 192.168.199.41:80; Connection refused
开启192.168.199.108访问容器80端口
root@localhost(192.168.199.41)~>iptables -R DOCKER-USER 1 -i eth0 -s 192.168.199.108 -p tcp --dport 80 -j ACCEPT
root@localhost(192.168.199.41)~>iptables -nvL DOCKER-USER --line-number
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT tcp -- eth0 * 192.168.199.108 0.0.0.0/0 tcp dpt:80
2 32 1472 REJECT all -- eth0 * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable
3 24 3170 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
再次尝试从192.168.199.108访问
root@localhost(192.168.199.108)~>curl 192.168.199.41
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
访问成功。
参考规则
仅特定IP访问容器,其他全部禁止。
iptables -I DOCKER-USER -i eth0 ! -s 192.168.199.108 -j DROP
开放一段ip及端口访问(开放192.168.199.1-192.168.199.10可以访问80-90及8080-9090端口)
iptables -I DOCKER-USER -i eth0 -j REJECT
iptables -I DOCKER-USER -m iprange -m multiport -i eth0 -p tcp --src-range 192.168.199.1-192.168.199.10 --dport 80:90,8080:9090 -j ACCEPT
实际案例-1
环境说明
操作系统:CentOS Linux release 7.9.2009 (Core)
docker版本:26.1.0
使用docker-compose启动的容器:nginx、tomcat、mysql
docker-compose ps -a
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
mysql mysql:5.7.18 "docker-entrypoint.s…" mysql 11 minutes ago Up 11 minutes 3306/tcp
nginx nginx:alpine "/docker-entrypoint.…" nginx 5 minutes ago Up 5 minutes 0.0.0.0:80->80/tcp
tomcat tomcat "catalina.sh run" tomcat 11 minutes ago Up 11 minutes 0.0.0.0:8080->8080/tcp
注意:每个容器对外都有映射端口。
安装iptables服务
yum install iptables-services -y
启动并保存配置
systemctl enable --now iptables
service iptables save #保存配置
配置规则
接下来,进行规则的配置。首先要考虑不要影响业务的前提下将规则配置好。
需要做以下限制:
- mysql不允许任何人在外部访问,仅允许
tomcat
访问; - tomcat 只允许
192.168.199.108
访问; - nginx开放给所有人访问。
第一条:mysql不允许任何人在外部访问,仅允许 tomcat
访问
分析:这里服务所指的是服务所在容器,每个容器都有IP地址,我们也知道容器的IP是随时都有可能发生变化(例如:mysql容器重建,则会生成一个不同IP的容器),这里就需要在启动时给定容器IP,然后通过 iptables 来对容器IP进行限制即可达到目的。
第一步:启动时,绑定IP
more docker-compose.yml
services:
mysql:
container_name: mysql
image: mysql:5.7.18
environment:
- MYSQL_ROOT_PASSWORD=123123
- MYSQL_ROOT_HOST=%
- TZ=Asia/Shanghai
restart: always
volumes:
- ./data:/var/lib/mysql
networks:
test_net:
ipv4_address: 172.100.0.2 #为mysql绑定IP
tomcat:
container_name: tomcat
image: tomcat
restart: always
ports:
- 8080:8080
networks:
test_net:
ipv4_address: 172.100.0.3 #为tomcat绑定IP
nginx:
container_name: nginx
image: nginx:alpine
restart: always
ports:
- 80:80
networks:
- test_net
networks:
test_net:
name: test_net
driver: bridge
ipam:
config:
- subnet: "172.100.0.0/16"
第二步:已知 mysql
和 tomcat
的IP以后,就可以定制 iptables
规则
iptables -I DOCKER-USER ! -s 172.100.0.3 -d 172.100.0.2 -p tcp --dport 3306 -j REJECT
--仅允许tomcat(172.100.0.3)访问mysql(172.100.0.2) 的 3306端口,其他访问全部禁止,为了测试方便,我这里直接使用 REJECT
查看 iptables规则列表
iptables -nvL DOCKER-USER --line-numbers
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT tcp -- * * !172.100.0.3 172.100.0.2 tcp dpt:3306 reject-with icmp-port-unreachable
2 13049 22M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
第三步:进入tomcat 容器 和 nginx容器进行 telnet mysql 3306
进行测试。
docker exec -it nginx sh
/ # ping -c1 mysql
PING mysql (172.100.0.2): 56 data bytes
64 bytes from 172.100.0.2: seq=0 ttl=64 time=0.360 ms
--- mysql ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.360/0.360/0.360 ms
/ # telnet mysql 3306
telnet: can't connect to remote host (172.100.0.2): Connection refused
--- 进入 nginx 容器 telnet mysql容器3306端口失败,符合预期。---
docker exec -it tomcat bash
root@4b57c5896221:/usr/local/tomcat# ping -c1 mysql
PING mysql (172.100.0.2) 56(84) bytes of data.
64 bytes from mysql.test_net (172.100.0.2): icmp_seq=1 ttl=64 time=0.304 ms
--- mysql ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.304/0.304/0.304/0.000 ms
root@4b57c5896221:/usr/local/tomcat# telnet mysql 3306
Trying 172.100.0.2...
Connected to mysql.
Escape character is '^]'.
J
5.7.18U%yMYk:
@C%(U^K_mysql_native_password
--- 进入 tomcat 容器 telnet mysql容器3306端口连接成功,符合预期。 ---
到此,第一个要求已经完成了。
第二条:tomcat 只允许192.168.199.108
访问;
第一步:需要获得tomcat容器的IP才能做 iptables
规则限制
通过上面的操作,已知tomcat IP:172.100.0.3
第二步:定制规则
iptables -I DOCKER-USER ! -s 192.168.199.108 -d 172.100.0.3 -p tcp --dport 8080 -j REJECT
iptables -nvL DOCKER-USER --line-numbers
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT tcp -- * * !192.168.199.108 172.100.0.3 tcp dpt:8080 reject-with icmp-port-unreachable
2 2 120 REJECT tcp -- * * !172.100.0.3 172.100.0.2 tcp dpt:3306 reject-with icmp-port-unreachable
3 13069 22M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
第三步:进行测试
root@localhost(192.168.199.108)~>curl -I 192.168.199.41:8080
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 24 Jul 2024 11:17:05 GMT
root@shate(192.168.199.10)~>curl -I 192.168.199.41:8080
curl: (7) Failed connect to 192.168.199.41:8080; Connection refused
--- 测试结果:108访问正常,10访问被拒绝,符合预期 ---
到此,第二个要求已经完成了。
第三条:nginx开放给所有人访问。
因为默认是全部放开,所有第三条规则原本就满足。
上面的示例只是为了演示,如何在单机当中对容器做限制,如果使用网络插件或者名称空间的方式会更优雅。
实际案例-2
在某些严格的环境中,单台服务器作为一个单位来进行管控,粒度并没有到容器的级别,如果物理服务器使用了容器的方式,则可以通过 iptables
的 DOCKER-USER
链来进行管控。
例如:对于192.168.199.41
服务器,仅开放22 、80、3306、8080 端口,其他全部禁用。
问题分析: 22端口作为ssh服务,都是在本地直接进行开放,然后 80、3306、8080
都以容器的方式运行,接下来实操:
第一步:在 INTPUT
链添加 目标端口22、80、3306、8080
的允许,否则就被关在外面,无法连接服务器了!!!
iptables -R INPUT 1 -m multiport -p tcp --dport 22,80,3306,8080 -j ACCEPT
iptables -nvL INPUT
Chain INPUT (policy DROP 6 packets, 539 bytes)
pkts bytes target prot opt in out source destination
38 2808 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 multiport dports 22,80,3306,8080
iptables -nvL INPUT
Chain INPUT (policy ACCEPT 137 packets, 13238 bytes)
pkts bytes target prot opt in out source destination
272 18784 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
第二步:设置 INPUT
默认规则为 DROP
iptables -P INPUT DROP
第三步:启动一个没有做管控的容器访问测试
docker run --name ngx-1 -d -p 8888:80 nginx:alpine
--- 启动一个nginx容器,端口映射为 8888 ,很明显 8888 并没有在规则限制中 ---
在别的主机中访问 http://192.168.199.41:8888
root@localhost(192.168.199.108)~>curl -I http://192.168.199.41:8888
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Wed, 24 Jul 2024 11:42:43 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 28 Dec 2021 18:48:00 GMT
Connection: keep-alive
ETag: "61cb5be0-267"
Accept-Ranges: bytes
由此得知,容器并没有走 INPUT 链,如果要对容器进行管控必须在 DOCKER-USER链进行规则设定!
因此,修改 第一步 规则,仅限制 22
端口
iptables -R INPUT 1 -p tcp --dport 22 -j ACCEPT
iptables -nvL INPUT --line-numbers
Chain INPUT (policy DROP 8 packets, 846 bytes)
num pkts bytes target prot opt in out source destination
1 36 2692 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
再次,在 DOCKER-USER
中,对容器进行限制。
### 1.设置默认规则为:REJECT ###
iptables -I DOCKER-USER -i eth0 -j REJECT
iptables -nvL DOCKER-USER --line-numbers
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT all -- eth0 * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable
2 13253 22M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
### 2.开放80、8080、3306端口 ###
iptables -I DOCKER-USER -i eth0 -m multiport -p tcp --dport 80,3306,8080 -j ACCEPT
iptables -nvL DOCKER-USER --line-numbers
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 48 11070 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 multiport dports 80,3306,8080
2 68 6657 REJECT all -- eth0 * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable
3 13294 22M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
进行测试
root@localhost(192.168.199.108)~>curl -I http://192.168.199.41:80
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Wed, 24 Jul 2024 13:42:57 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 28 Dec 2021 18:48:00 GMT
Connection: keep-alive
ETag: "61cb5be0-267"
Accept-Ranges: bytes
root@localhost(192.168.199.108)~>curl -I http://192.168.199.41:8080
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 24 Jul 2024 13:43:01 GMT
root@localhost(192.168.199.108)~>curl -I http://192.168.199.41:8888 # 为什么没有开放的 8888 也可以访问到?
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Wed, 24 Jul 2024 13:43:05 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 28 Dec 2021 18:48:00 GMT
Connection: keep-alive
ETag: "61cb5be0-267"
Accept-Ranges: bytes
注意:这里为什么没有开放的 8888
也可以被访问到?
启动这个容器时的命令为:
docker run --name ngx-1 -d -p 8888:80 nginx:alpine
注意这个端口映射 容器内80 -> 物理机 8888 端口,这里提出质疑,我们在 DOCKER-USER 添加规则是否只是限制容器内端口,而无法限制映射到物理机的端口?
修改该 nginx
的配置文件,使其启动为容器后也监听到 8888
端口
cat default.conf
server {
listen 8888;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
启动容器并映射该配置文件
root@localhost(192.168.199.41)/root>docker rm -f ngx-1
ngx-1
root@localhost(192.168.199.41)/root>docker run --name ngx-1 -d -v /root/default.conf:/etc/nginx/conf.d/default.conf -p 8888:8888 nginx:alpine
本地测试及远程主机测试
### 本地测试 ###
root@localhost(192.168.199.41)/root>curl -I 192.168.199.41:8888
HTTP/1.1 200 OK
Server: nginx/1.21.5
Date: Wed, 24 Jul 2024 13:51:21 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 28 Dec 2021 18:48:00 GMT
Connection: keep-alive
ETag: "61cb5be0-267"
Accept-Ranges: bytes
### 远程主机测试 ###
root@localhost(192.168.199.108)~>curl -I 192.168.199.41:8888
curl: (7) Failed connect to 192.168.199.41:8888; Connection refused
由此验证了我们的猜想,DOCKER-USER
仅是对容器内监听端口进行管控,而不会对映射后的端口进行控制。