Ubuntu 防火墙 UFW 对 Docker 容器端口的策略问题处理

原因

UFW(Uncomplicated Firewall)是 Debian 和 Ubuntu 系统自带的一个前端工具,便于用户管理防火墙规则。然而,由于 Docker 和 UFW 在操作 iptables 时采取不同的方式,二者之间存在不兼容的问题。

当 Docker 公开容器端口时,容器的流入和流出流量会在经过 UFW 防火墙规则之前被重定向。具体而言,Docker 使用 nat 表对容器流量进行路由,使得数据包在到达 UFW 监控的 INPUT 链之前就被处理。同样,流出的 OUTPUT 数据包也在 UFW 应用规则之前完成了路由操作,导致 UFW 的防火墙配置被绕过。

现在的目标是让 UFW 生效,同时尽可能少的修改 Docker 网络。

方案一

https://github.com/chaifeng/ufw-docker?tab=readme-ov-file#solving-ufw-and-docker-issues

该方案的优点是不需要修改 Docker 的网络配置。缺点是无论内网还是外网,想要访问容器暴露的端口,都需要进行一些额外的操作。

  1. /etc/ufw/after.rules 末尾添加以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    # BEGIN UFW AND DOCKER
    *filter
    :ufw-user-forward - [0:0]
    :ufw-docker-logging-deny - [0:0]
    :DOCKER-USER - [0:0]
    -A DOCKER-USER -j ufw-user-forward

    # [1]允许同一网络的容器可以互相访问,但也导致内网可以访问容器暴露的端口,如需更高安全性可注释
    -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

    # [2]允许容器进行 DNS 查询
    -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

    # [3]记录并拒绝所有发往内网 IP 的新 TCP 连接(SYN 包)和 UDP 流量
    -A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
    -A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
    -A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
    -A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
    -A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
    -A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

    # [4]默认返回规则
    -A DOCKER-USER -j RETURN

    # [5]记录并丢弃非法连接尝试
    -A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
    -A ufw-docker-logging-deny -j DROP

    COMMIT
    # END UFW AND DOCKER
  2. 重载配置:

    1
    2
    3
    sudo systemctl daemon-reload && sudo ufw reload
    # 如果不生效,执行以下命令
    sudo /usr/lib/ufw/ufw-init flush-all && sudo service ufw restart && sudo service docker restart
配置后的效果
  • 宿主机可以访问容器内端口。(与新增配置无关)
  • 容器内无法访问宿主机的端口。(与新增配置无关)
  • 同一 Docker 网络内的容器可以互相访问端口(如果注释了 [1] 部分的规则,则此功能将失效,解决方法请参考问题1)。
  • 内网可以访问容器暴露的端口(如果注释了 [1] 部分的规则,则此功能将失效,解决方法请参考问题2)。
  • 外网无法访问容器暴露的端口。(解决方法请参考问题2)

问题及解决方案

1. 容器无法访问同一 Docker 网络中的其他容器端口

1
sudo ufw route allow from 172.18.0.0/16 to 172.18.0.0/16

或者直接在 /etc/ufw/after.rules-A DOCKER-USER -j ufw-user-forward 后面添加一行:

1
2
3
-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -s 172.18.0.0/16 -d 172.18.0.0/16 -j RETURN

2. 无法访问容器映射的端口

当使用 -p 8080:80 映射端口时,应允许路由到容器端口 80,而不是主机端口 8080

1
sudo ufw route allow proto tcp from any to <容器ip> port <容器端口>

同时需要确保主机的 8080 端口能够被外部访问:

1
sudo ufw allow proto tcp from any to <主机ip> port <主机端口>

方案二

虽然该方案需要修改 Docker 的网络配置,但不会有方案一中出现的问题,推荐使用

  1. 禁用 Docker 的 iptables 管理:

    1
    sudo vim /etc/docker/daemon.json

    添加以下配置:

    1
    { "iptables": false }
  2. 修改 Docker 启动配置:

    1
    sudo vim /etc/default/docker

    添加 -iptables=false

    1
    DOCKER_OPTS="-iptables=false"
  3. 修改 UFW 配置:

    1
    sudo vim /etc/default/ufw

    把 DEFAULT_FORWARD_POLICY 改为 ACCEPT

    1
    DEFAULT_FORWARD_POLICY="ACCEPT"
  4. 修改 UFW 规则:

    1
    sudo vim /etc/ufw/before.rules

    在 *filter 前面添加下面内容:

    1
    2
    3
    4
    5
    6
    7
    *nat
    :POSTROUTING ACCEPT [0:0]
    # 访问外部网络时,将源地址改为主机的 IP 地址,只影响发往 docker0 网卡之外的流量
    -A POSTROUTING ! -o docker0 -s 172.17.0.0/16 -j MASQUERADE5COMMIT
    -A POSTROUTING ! -o [网卡名] -s [网卡所在网段] -j MASQUERADE5COMMIT
    ...
    COMMIT
  5. 重启 UFW 和 Docker:

    1
    sudo /usr/lib/ufw/ufw-init flush-all && sudo service ufw restart && sudo service docker restart