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

我刚试过 Docker。这是令人敬畏的,但似乎不能很好地工作与 ufw。默认情况下,docker 会稍微操作一下 iptables。结果不是一个错误,但不是我所期望的。 欲了解更多细节,请阅读《 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。这是我的测试

环境:

  • 新安装的 Ubuntu 14.04(内核: 3.13.0-53)
  • Docker 1.6.2
  • 启用 UFW 转发。([启用 UFW 转发] 2)
  • 向 Docker 守护进程添加了 --iptables=false

第一次尝试

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

第二次尝试

然后我发现了这个: 无法将容器与—— 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 将不能“正常”工作(嗯... 根据我的定义)。

那么,Docker + ufw 的最佳实践是什么? 有人能提供一些帮助吗?

125744 次浏览

据我所知,你希望更好地控制谁可以访问你在 Docker 内部运行的应用程序?我在这里回答了一个类似的问题,通过前端代理而不是 IP 表来控制流量: 阻止对码头集装箱的外部访问

剪辑

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

我几个月前就遇到过这样的问题,最近决定在我的博客上描述这个问题并提出解决方案。这是捷径。

使用 --iptables=false对您描述的情况没有多大帮助。这里还不够。默认情况下,所有容器都不能执行任何传出连接。

有一个小步骤,你忽略了你的方式有后面 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/中更全面地描述了问题和解决方案

值得一提的是,这里有一个对 @ mkubaczyk 的回答的附录,用于在整个设置中涉及到更多的桥接网络的情况。这些可能是由 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 网络相关的规则。
  • 如果修改了 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。现在公共网络不能访问任何已发布的码头端口,集装箱和专用网络可以定期互访,集装箱也可以从内部访问外部网络。

例如,如果希望允许公共网络访问 Docker 容器提供的服务,则容器的服务端口为 80。运行以下命令以允许公共网络访问此服务:

ufw route allow proto tcp from any to any port 80

此命令允许公共网络访问容器端口为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

它是如何工作的?

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

-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 管理是否允许公共网络访问 Docker 容器提供的服务。这样我们就可以在一个地方管理所有的防火墙规则。

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

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

例如,若要允许公众访问容器端口为 8080的发布端口,请使用以下命令:

ufw allow 8080

缺点:

它不仅公开容器的端口,而且还公开主机的端口。

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

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

ufw allow proto tcp from any to 172.16.0.3 port 8080

使用 ufw-user-forward

赞成:

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

例如,如果我们希望发布容器的端口 8080,请使用以下命令:

ufw route allow 8080

公共网络可以访问所有集装箱港口为 8080的已发布港口。

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

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 Swarm。请参阅更多的最新代码 https://github.com/chaifeng/ufw-docker

安装 Docker Swarm 模式

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

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

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

我花了两个小时尝试上面和其他帖子中的建议。 唯一有效的解决办法是来自 Tsuna 在 Github 线中的帖子:

/etc/ufw/after.rules的末尾附加以下内容(替换 使用外部面向界面的 eth0) :

# 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

撤销所有:

  • 从/etc/docker/daemon.json 中删除“ iptables”: “ false”
  • 恢复到/etc/DEFAULT/ufw 中的 DEFAULT _ FORWARD _ POLICY = “ DROP”
  • 删除对/etc/ufw/before. rules 的任何与 docker 相关的更改
  • 请确保在重新启动后测试一切正常。 我仍然相信多克的不按常理出牌的行为是危险的,更多的人将继续无意中暴露内部 对外界的服务,因为多克打孔 其他安全的 iptables 配置。

我不喜欢 iptables 所需的操作开销: docker 守护进程中的假标志。事实上,从我所看到的,请纠正我,如果我错了,所有的解决方案都太复杂了黑客。

只需在/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

没有必要搞乱码头网络或不必要的黑客攻击。

并不是说这里的解决方案是错误的,但是对于寻找 快速一步指令的人来说,它们看起来有点“可怕”和错误修剪。我最近也遇到了这个问题,我在网上看了所有类似的答案,在写作的时候还没有找到任何快速而清晰的答案。 令人惊讶的是,我的替代解决方案很容易理解和管理,而且很有效: 只需在主机外部实现防火墙即可

  • 数字海洋有惊人的 防火墙零额外成本,所见即所得的风格。
  • AWS 提供 保安组
  • 等等。

把防火墙当作第一类物件似乎有很多好处。

不好意思,我翻出了这条旧线索。我有同样的问题,它帮助我只是约束 ufw 到特定的 IP 和接口。因为默认情况下 ufw 应用于所有网络接口,也是从 Docker 的内部接口。这就是为什么所有这个漂亮的码头港口转发故事(喜欢 -p80:8080)不工作。为了克服这个问题,只需要指定一个特定的接口和应用 ufw 的 ip。在我的例子中,它是在服务器上向全世界公开的。

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 或 iptables: false in/etc/docker/daemon.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 的文章摘要:

告诉 Docker 远离我的防火墙

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


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

改变未来的政策

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) 由于默认/标准的 Docker 桥接网络在172.17.0.0/16上工作(见 docker inspect bridge子网) ,最直接的 IMHO 是:

ufw allow from 172.17.0.0/16

UFW 非常简单,如果不是迫不得已,我不想深入研究 iptables。对我来说,关于 iptables/ufw 的 Docker 行为也是可以的,尽管没有足够的文档记录。我的观点是,在启动集装箱时,人们应该准确地了解暴露的端口发生了什么。然后,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: port,这意味着它的端口是可用的 从主机和主机网络

让我们运行最后一个 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: port 3306在本地发布,这意味着可以使用 只能来自主机,而不能来自主机网络

因此,是的,Docker 必须调整 UFW,但这只是为了达到所要求的目的: 在本地或向网络公开一个端口。所以,只要您知道如何使用端口发布,就应该是安全的。

此外,虽然我不是一个网络安全专家,但在我的服务器上做了一些 全端口扫描让我放心: 我得到的结果与预期的一致。

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

参考文献:

我也有过类似的案子。

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

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 用户时,通过指定% 作为 db host 而不是 user@localhost,允许它从任何主机进行连接。