docker + ufw 在 Ubuntu 下的最佳实践是什么

What is the best practice of docker + ufw under Ubuntu

我刚试过 Docker。它很棒,但似乎不能很好地与 ufw 一起使用。默认情况下,docker 会稍微操纵 iptables。结果不是错误,但不是我所期望的。 有关详细信息,您可以阅读 The dangers of UFW + Docker

我的目标是建立一个像

这样的系统
    Host (running ufw) -> docker container 1 - nginx (as a reverse proxy)
                       -> docker container 2 - node web 1
                       -> docker container 3 - node web 2
                       -> .......

我想通过 ufw 管理传入流量(例如限制访问),因此我不想 docker 触及我的 iptables。这是我的测试

环境:

第一次尝试

docker run --name ghost -v /home/xxxx/ghost_content:/var/lib/ghost -d ghost
docker run --name nginx -p 80:80 -v /home/xxxx/nginx_site_enable:/etc/nginx/conf.d:ro --link ghost:ghost -d nginx

运气不好。第一个命令没问题,但第二个命令会抛出错误

Error response from daemon: Cannot start container

第二次尝试

然后我发现了这个:unable to link containers with --iptables=false #12701

运行以下命令后,一切正常。

sudo iptables -N DOCKER

但是,我注意到我无法在容器内建立任何出站连接。例如:

xxxxg@ubuntu:~$ sudo docker exec -t -i nginx /bin/bash
root@b0d33f22d3f4:/# ping 74.125.21.147
PING 74.125.21.147 (74.125.21.147): 56 data bytes
^C--- 74.125.21.147 ping statistics ---
35 packets transmitted, 0 packets received, 100% packet loss
root@b0d33f22d3f4:/# 

如果我从 Docker 守护程序中删除 --iptables=false,那么容器的互联网连接将恢复正常,但 ufw 将无法工作 'properly'(好吧......通过我的定义)。

那么,docker + ufw 的最佳实践是什么?谁能提供一些帮助?

据我所知,您希望更好地控制谁可以在 Docker 中访问您的应用程序 运行?我在这里回答了一个类似的问题,通过前端代理而不是 IP 表来控制流量:

编辑

通过上述方法,您可以使用 UFW 仅允许传入连接到端口 80(即代理)。这可以将任何端口暴露保持在最低限度,并且您可以通过代理配置和 DNS 控制流量。

几个月前我遇到过这样的问题,最近决定在我的博客上描述这个问题以及解决方案。这是快捷方式。

使用 --iptables=false 对您描述的情况没有多大帮助。这里根本不够。默认情况下,none 个容器可以进行任何传出连接。

在此处将容器置于 UFW 后面的过程中,您忽略了一小步。您可以使用 --iptables=false 或创建 /etc/docker/daemon.json 文件,内容如下

{
  "iptables": false
}

结果是一样的,但是后一个选项要求你用 service docker restart 重启整个 docker 服务,或者如果 docker 有机会添加 iptables 甚至重启禁用此功能之前的规则。

完成后,再做两件事:

$ sed -i -e 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/g' /etc/default/ufw
$ ufw reload

因此您在 UFW 中设置默认转发策略以接受,并使用:

$ iptables -t nat -A POSTROUTING ! -o docker0 -s 172.17.0.0/16 -j MASQUERADE

这样你所实现的是在你的 iptables 规则中禁用 docker 混乱行为,同时 docker 提供了必要的路由,因此容器可以很好地进行传出连接。不过,UFW 规则从此时起仍将受到限制。

希望这能为您和任何来到这里寻找答案的人解决问题。

我在 https://www.mkubaczyk.com/2017/09/05/force-docker-not-bypass-ufw-rules-ubuntu-16-04/

更全面地描述了问题和解决方案

这里有一个 的附录,用于整个设置中涉及更多桥接网络的情况。这些可能由 Docker-Compose 项目提供,这里是如何生成正确的规则,因为这些项目由 systemd.

控制

/etc/systemd/system/compose-project@.service

[Unit]
Description=Docker-Compose project: %I
After=docker.service
BindsTo=docker.service
AssertPathIsDirectory=/<projects_path>/%I
AssertFileNotEmpty=/<projects_path>/%I/docker-compose.yml

[Service]
Type=simple
Restart=always
WorkingDirectory=/<projects_path>/%I
ExecStartPre=/usr/bin/docker-compose up --no-start --remove-orphans
ExecStartPre=+/usr/local/bin/update-iptables-for-docker-bridges
ExecStart=/usr/bin/docker-compose up
ExecStop=/usr/bin/docker-compose stop --timeout 30
TimeoutStopSec=30
User=<…>
StandardOutput=null

[Install]
WantedBy=multi-user.target

/usr/local/bin/update-iptables-for-docker-bridges

#!/bin/sh

for network in $(docker network ls --filter 'driver=bridge' --quiet); do
  iface=$(docker network inspect --format '{{index .Options "com.docker.network.bridge.name"}}' ${network})
  [ -z $iface ] && iface="br-${network}"
  subnet=$(docker network inspect --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' ${network})
  rule="! --out-interface ${iface} --source ${subnet} --jump MASQUERADE"
  iptables --table nat --check POSTROUTING ${rule} || iptables --table nat --append POSTROUTING ${rule}
done

显然,这不会很好地扩展。

还值得注意的是,整个基本概念将伪装容器中应用程序 运行 的任何连接源。

问题

这个问题已经存在很长时间了。

在 Docker 中禁用 iptables 会带来其他问题。

先回滚更改

如果您根据我们在互联网上找到的当前解决方案修改了您的服务器,请先回滚这些更改,包括:

  • 启用 Docker 的 iptables 功能。 删除 --iptables=false 等所有更改,包括配置文件 /etc/docker/daemon.json.
  • UFW 的默认 FORWARD 规则改回默认的 DROP 而不是 ACCEPT
  • 在UFW配置文件/etc/ufw/after.rules中移除与Docker网络相关的规则/etc/ufw/after.rules
  • 如果您修改了Docker 配置文件,请先重新启动Docker。后面我们会修改UFW的配置,然后重启即可。

解决 UFW 和 Docker 问题

该方案只需要修改一个UFW配置文件,所有Docker配置和选项保持默认。不需要禁用 docker iptables 功能。

修改UFW配置文件/etc/ufw/after.rules,在文件末尾添加如下规则:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

更改文件后使用命令sudo systemctl restart ufw重启UFW。现在public网络无法访问任何公开的docker端口,容器和私有网络可以正常互访,容器内部也可以访问外网。

如果想让public个网络访问Docker容器提供的服务,比如某个容器的服务端口是80。 运行 以下命令允许 public 网络访问此服务:

ufw route allow proto tcp from any to any port 80

此命令允许public网络访问容器端口为80的所有已发布端口。

注意:如果我们使用选项 -p 8080:80 发布端口,我们应该使用容器端口 80,而不是主机端口 8080

如果有多个服务端口为80的容器,但我们只想让外网访问某个容器。例如容器的私有地址为172.17.0.2,使用如下命令:

ufw route allow proto tcp from any to 172.17.0.2 port 80

如果服务的网络协议是UDP,比如DNS服务,可以使用如下命令让外网访问所有已发布的DNS服务:

ufw route allow proto udp from any to any port 53

同理,如果只是针对特定的容器,比如IP地址172.17.0.2:

ufw route allow proto udp from any to 172.17.0.2 port 53

它是如何工作的?

以下规则允许私有网络能够相互访问。通常,专用网络比 public 网络更受信任。

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

以下规则允许UFW管理是否允许public网络访问Docker容器提供的服务。这样我们就可以在一个地方管理所有的防火墙规则。

-A DOCKER-USER -j ufw-user-forward

以下规则阻止所有public网络发起的连接请求,但允许内部网络访问外部网络。对于 TCP 协议,它阻止从 public 网络主动建立 TCP 连接。对于 UDP 协议,所有小于 32767 的端口的访问都被阻止。为什么是这个端口?由于UDP协议是无状态的,所以不可能像TCP那样阻塞发起连接请求的握手信号。对于 GNU/Linux,我们可以在文件 /proc/sys/net/ipv4/ip_local_port_range 中找到本地端口范围。默认范围是 32768 60999。当从运行容器访问UDP协议服务时,本地端口会在端口范围内随机选择一个,服务器会return将数据发送到这个随机端口。因此,我们可以假设所有容器内部UDP协议的监听端口都是小于32768的。这就是我们不希望public网络访问小于32768的UDP端口的原因。

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

更多

https://github.com/chaifeng/ufw-docker

sudo wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
chmod +x /usr/local/bin/ufw-docker

用法

ufw-docker help
ufw-docker install
ufw-docker status
ufw-docker allow webapp
ufw-docker allow webapp 80
ufw-docker allow webapp 53/udp
ufw-docker list webapp
ufw-docker delete allow webapp 80/tcp
ufw-docker delete allow webapp

更新时间:2018-09-10

选择ufw-user-forward的原因,不是ufw-user-input

使用ufw-user-input

专业版:

易于使用和理解,支持旧版本 Ubuntu。

例如,要允许public访问容器端口为8080的已发布端口,请使用命令:

ufw allow 8080

缺点:

不仅暴露了容器的端口,还暴露了主机的端口。

例如,如果主机上的服务是 运行,端口是 8080。命令 ufw allow 8080 允许 public 网络访问该服务和容器端口为 8080 的所有已发布端口。但是我们只想在主机上公开服务运行,或者只是在容器内公开服务运行,而不是两者。

为避免此问题,我们可能需要对所有容器使用类似于以下的命令:

ufw allow proto tcp from any to 172.16.0.3 port 8080

使用ufw-user-forward

专业版:

无法通过同一命令同时在主机和容器上公开服务 运行。

比如我们要发布容器的8080端口,使用如下命令:

ufw route allow 8080

public网络可​​以访问容器端口为8080的所有已发布端口。

但是public网络仍然无法访问主机的端口8080。如果我们想这样做,执行以下命令允许public单独访问主机上的端口:

ufw allow 8080

缺点:

不支持旧版本的Ubuntu,而且命令有点复杂。但是你可以使用我的脚本 https://github.com/chaifeng/ufw-docker.

结论

如果我们使用旧版本的 Ubuntu,我们可以使用 ufw-user-input 链。但要注意避免暴露不该暴露的服务。

如果我们使用支持ufw route子命令的较新版本的Ubuntu,我们最好使用ufw-user-forward链,并使用ufw route命令管理容器的防火墙规则。


更新:2018 年 10 月 6 日

脚本 ufw-docker 现在支持 Docker Swarm。更多请看最新代码,https://github.com/chaifeng/ufw-docker

安装 Docker Swarm 模式

在 Swarm 模式下使用时,我们只能在管理器节点上使用此脚本来管理防火墙规则。

  • 修改所有节点上的所有 after.rules 文件,包括管理器和工作器
  • 正在管理器节点上部署此脚本

运行在 Docker Swarm 模式下,此脚本将添加一个全局服务 ufw-docker-agent。图像 chaifeng/ufw-docker-agent 也是从该项目自动构建的。

我花了两个小时尝试上面的建议和其他 post 的建议。 唯一有效的解决方案来自 Tsuna 的 post Github thread:

Append the following at the end of /etc/ufw/after.rules (replace eth0 with your external facing interface):

# Put Docker behind UFW
*filter
:DOCKER-USER - [0:0]
:ufw-user-input - [0:0]

-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
-A DOCKER-USER -i eth0 -j ufw-user-input
-A DOCKER-USER -i eth0 -j DROP
COMMIT

And undo any and all of:

  • Remove "iptables": "false" from /etc/docker/daemon.json
  • Revert to DEFAULT_FORWARD_POLICY="DROP" in /etc/default/ufw
  • Remove any docker related changes to /etc/ufw/before.rules
  • Be sure to test that everything comes up fine after a reboot. I still believe Docker's out of the box behavior is dangerous and many more people will continue to unintentionally expose internal services to the outside world due to Docker punching holes in otherwise safe iptables configs.

我不喜欢 iptables 所需的操作开销:docker 守护程序中的 false 标志。事实上,据我所知,如果我错了请指正,所有的解决方案都太复杂了。

只需将其插入 /etc/ufw/after.rules 中 *filter 部分之前:

*mangle
# Allow a whitelisted ip to access postgres port
-I PREROUTING 1 -s <whitelisted_ip> -p tcp --dport 5432 -j ACCEPT
# Allow everyone to access port 8080
-I PREROUTING 2 -p tcp --dport 8080 -j ACCEPT
# Drop everything else
-I PREROUTING 3 -p tcp -j DROP
COMMIT

无需扰乱 docker 网络或不必要的黑客攻击。

并不是说这里的解决方案是错误的,但对于正在寻找快速一步说明的人来说,它们看起来有点"terrifying"和错误修剪。我最近也遇到了这个问题,在线阅读了所有类似的答案,但在撰写本文时还没有发现任何快速清晰的内容。 令人惊讶的是,我的替代解决方案易于理解和管理,并且有效:只需在主机外部实施防火墙

将 Firewall 视为 first-class citizen 似乎有很多好处。

抱歉挖掘这个旧线程。我遇到了同样的问题,它帮助我将 ufw 限制到特定的 ip 和接口。因为默认情况下 ufw 应用于所有网络接口,也是来自 docker 的内部接口。这就是为什么所有这些 docker 端口转发故事(如 -p80:8080)都不起作用的原因。要克服这个问题,只需指定一个特定的接口和 ip 应该应用什么 ufw。在我的例子中,它是在服务器上暴露给世界的那个。

ufw allow in on eth0 to ip_of_eth0 port 22 proto tcp
ufw allow in on eth0 to ip_of_eth0 port 80 proto tcp
ufw allow in on eth0 to ip_of_eth0 port 443 proto tcp

将 eth0 更改为您想要的接口。

有了这个解决方案,现在就可以不用弄乱 iptables 或 /etc/docker/daemon 中的 iptables:false。json 标志只公开真正需要的端口。

从外部计算机输出的nmap:

Starting Nmap 7.91 ( https://nmap.org ) at <time>
Nmap scan report for <domain> (ip)
Host is up (0.042s latency).
Not shown: 997 filtered ports
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 11.44 seconds

总结来自@mkubaczyk 的post:

告诉docker远离我的防火墙

cat << EOF >> /etc/docker/daemon.json
{
     "iptables": false
}
EOF

echo "DOCKER_OPTS=\"--iptables=false\"" >>  /etc/default/docker
service docker restart

更改 ufw 转发策略

sed -i -e 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/g' /etc/default/ufw 

添加针对容器的 nat 规则

cat << EOF >> /etc/ufw/before.rules
# NAT table rules
*nat
:POSTROUTING ACCEPT [0:0]

# Forward traffic through eth0 - Change to match your out-interface
-A POSTROUTING -s 10.66.66.0/24 -o ens0 -j MASQUERADE

# don't delete the 'COMMIT' line or these nat table rules won't
# be processed
COMMIT

EOF
ufw reload

免责声明:此回复适用于 ufw(即 Ubuntu) 由于 default/standard Docker 桥接网络在 172.17.0.0/16 上工作(参见 docker inspect bridge 子网),恕我直言,最直接的做法是:

ufw allow from 172.17.0.0/16

UFW 非常简单,如果不是被迫,我不想深入研究 iptables。 Docker 关于 iptables / ufw 的行为对我来说似乎没问题,尽管没有足够的记录。我的观点是,在启动容器时,应该准确了解暴露的端口发生了什么。然后 docker ps 命令给出了关于正在发生的事情的良好反馈。

让我们运行一个MariaDb容器:

$ docker run --detach --env MARIADB_ROOT_PASSWORD="superSecret" mariadb:10.4

$ docker ps --format "table {{.Names}}\t{{.Ports}}"
NAMES           PORTS
happy_jackson   3306/tcp

此处 PORTS 列显示 3306/tcp:端口 3306 可能可用但实际上未发布,这意味着 3306 端口不可访问既不能从主机也不能从主机网络访问.

让我们 运行 另一个 MariaDb 容器 :

$ docker run --detach --env MARIADB_ROOT_PASSWORD="superSecret" -p 3306:3306 mariadb:10.4

$ docker ps --format "table {{.Names}}\t{{.Ports}}"
NAMES              PORTS
trusting_goodall   0.0.0.0:3306->3306/tcp

现在 PORTS 列显示 0.0.0.0:3306->3306/tcp:端口已发布,这意味着它端口可用 来自主机和来自主机网络

让我们 运行 最后一个 MariaDb 容器 :

$ docker run --detach --env MARIADB_ROOT_PASSWORD="superSecret" -p 127.0.0.1:3306:3306 mariadb:10.4

$ docker ps --format "table {{.Names}}\t{{.Ports}}"
NAMES             PORTS
quizzical_gauss   127.0.0.1:3306->3306/tcp

现在 PORTS 列显示 127.0.0.1:3306->3306/tcp:端口 3306 是本地发布的,这意味着它只能从主机使用,不能从主机网络使用

所以是的 Docker 必须调整 UFW,但这只是为了实现所要求的:在本地或网络上公开一个端口。所以只要你知道你在用端口发布做什么,你就应该是安全的。

此外,虽然我不是网络安全专家,但在我的服务器上做了一些 full ports scans 让我放心:我得到的结果符合预期。

如果网络隔离对您的应用程序不是很重要,您也可以选择将容器附加到主机网络。

参考:

我有一个类似的案例。

我解决它的方法是创建一个自定义网络并将其定义为外部网络。

docker network create my_app_net

# put this in all the project related containers' docker compose files.
networks:
      - my_app_net

networks:
  my_app_net:
    external: true

然后我可以通过我定义的主机名连接到容器(在 docker compose 中)。

主机名:“my_app_db” container_name:“my_app_db”

然后我能够从其中一个容器连接到数据库服务器。我还确保服务器绑定到所有 IP,例如0.0.0.0(我为此使用自定义 my.cnf 文件) mysql -uUSER -pPASS -hDOCKER_HOST --port 3306 --protocol=tcp DB_NAME

另一个重要的细节是在创建 mysql 用户时允许它通过将 % 指定为数据库主机而不是 user@localhost 来从任何主机连接。