你写的规则,与 Docker 写的规则
有两个程序在用不同的假设,编辑着同一套防火墙规则,而只有其中一个会告诉你它做了什么。ufw 把自己的规则写进 filter 表的 INPUT 链——这是一个数据包发往主机本身时会走的路径。Docker 则写进 nat 表,以及 FORWARD 链——这是一个数据包发往其他地方时会走的路径。
顺着单个数据包走一遍,这道缺口就一目了然了。某个身处另一个国家的人,向你地址上的 5432 端口发起一个连接。数据包抵达后,最先遇到的是 nat PREROUTING,它把数据包送进 Docker 的 DOCKER 链。在那里,一条 DNAT 规则把目的地址改写成 172.17.0.2:5432——也就是那个容器。这个数据包已经不再是发给你服务器本身的了,所以内核会转发它,而不是把它投递到本机:它会经过 FORWARD 链,Docker 早已在那里为发往已发布端口的流量装好了一条 ACCEPT 规则。在这整段旅程里,这个数据包没有一刻经过 INPUT——而那才是 ufw 唯一在过滤的链。你的规则从来没有错。它只是从来没有被问过。
这也是为什么这个症状会如此让人摸不着头脑。ufw status verbose 一直报告着 Default: deny (incoming),除了 80、443 和你的 SSH 端口之外什么都不放行,与此同时,另一个大陆上的扫描器却正握着一个通向你数据库的开放会话。这两个说法都是真的。防火墙确实在照你配置的那样工作,处理着分给它的那部分流量——而真正要紧的那部分流量,根本没有分给它。
这些都不是什么 bug,也不是 Docker 粗心大意。一个容器引擎必须编写 NAT 和转发规则,否则容器根本没法触达网络,而它又没法安全地猜出,你主机防火墙里的哪些规则原本是想套用在容器身上的。所以它做了件老实事:只管好自己的那些链,同时交给你一条专属的链——DOCKER-USER,在 FORWARD 里排在其他一切规则之前被评估——并承诺绝不会覆盖这条链的内容。缺口不在于这套机制本身的存在。缺口在于 -p 5432:5432 读起来像是"让它可以被访问到",而它的真实意思却是"把这个发布到这台机器所应答的每一个地址上,绕过你花了一个小时写好的防火墙"。
从外面看,一个暴露的端口是什么样子
互联网发现问题的速度,比大多数人预期的要快。托管服务商的地址段会被持续、地毯式地扫描——干这件事的不是哪个特意盯上你的人,而是商业爬虫、研究项目,以及见机行事的僵尸网络:它们扫过每一个可路由地址上的每一个有意思的端口,然后把结果发布出来或者卖掉。从 docker compose up -d 到第一个不请自来的连接找上刚刚发布的数据库端口,间隔时间以小时计。没有人需要猜你的主机名。没有人需要知道你的名字。你的地址就在被扫描的范围之内。
他们能找到什么,完全取决于你发布了什么,而常见的情况都不太好看。一个从镜像快速上手示例里起来的 PostgreSQL 或 MySQL 容器,示例里那个敷衍了事的密码原封不动地留在那里。一个从来没配置过要求身份验证的 Elasticsearch 或 MongoDB 实例,因为当初设想它只会被应用容器访问到。一个来者不拒、谁都能连上的 memcached——这不仅是数据泄露,还是一个可以被人拿去对准别人的放大器,把你的服务器变成我们DDoS 攻击第一小时应对手册里描述的那种洪泛攻击的参与者。一个原本假设自己处在私有网络里的管理面板或消息队列看板。一个悄悄列出你运行的每一项内部服务、主机名和版本号的指标端点。
有必要把第二层伤害也说清楚,因为第一层伤害并不是账单的全部。一个可以从互联网访问到的数据库,不只是可读——在大多数引擎里,它同样可写,这意味着入侵者不需要再找一个漏洞就能植入持久化后门,而有几款引擎,从一个具备特权的会话里,还能被哄骗着去写文件、在主机上执行命令。在一台你当初选中正是因为它不带任何身份信息的机器上,一个未经身份验证的立足点,同样也是通向这台机器所能触及的一切的一条后路:它够得到的备份目标、它环境变量里的密钥、它所在网桥上的其他容器。
让人不安的地方在于,这一切都不会自己发出声响。不会有哪一行日志写着"你的防火墙被绕过了"。服务照常运作,应用照常连接,网站照常在线,唯一的外部信号是一个没有人在盯着的连接数。这次暴露,要么被你自己在接下来的十分钟里主动发现,要么被别人,按他们自己的时间表发现。
SP·03看清你到底发布了什么
先看引擎自己以为在做什么。docker ps 打印出一列 PORTS,这一列里的区别正是本指南通篇要讲的东西:0.0.0.0:5432->5432/tcp 意味着机器上的每一个地址,127.0.0.1:5432->5432/tcp 意味着仅限回环地址,而一个没有箭头、光秃秃的 5432/tcp 意味着这个端口只对其他容器暴露,没有对外发布。在动手改任何东西之前,把每个容器这一列里的每一行都读一遍。
接着用 ss -tulpen 看一眼套接字。在默认安装下,你会看到 docker-proxy 占着已发布的端口,因为 Docker 至今仍然会为每一个已发布端口起一个小小的用户态进程。这里有一个能让人白折腾一下午的坑:如果你的守护进程上关掉了 userland-proxy——这是个常见的调优改动,有些发行版甚至默认就是这么配的——主机上就根本不会有监听套接字。ss 什么都看不到,lsof 也什么都看不到,可端口依然大开着,因为内核的 DNAT 规则自己就把活干了,不需要任何进程占住那个地址。一个安安静静的 ss 输出,并不能证明端口是关着的。
所以还是得看规则本身。iptables -t nat -S DOCKER 会为每一个已发布端口列出一行 DNAT 规则,每一行都写着你想要的答案:带 -d 127.0.0.1/32 的规则是回环发布,没有目的地址限制的规则则适用于这台机器持有的每一个地址。对 ip6tables 也做一遍同样的事,因为这两个协议族是各自独立配置的,一台机器完全可能一边收得很紧,另一边却大敞着门。
最后一步——也是唯一真正能证明点什么的一步——从别的地方看这台机器。以上所有命令都是在主机本身上跑的,继承的是主机自己对其网络的那一套认知。回环流量会绕开那些真正要紧的链,所以 curl 127.0.0.1:5432 成功了,并不能说明陌生人是否也能做到同样的事;它失败了,能说明的就更少。真正有说服力的测试,是从另一台机器、在另一个网络上,针对两个 IP 协议族分别发起的扫描。在那之前的一切,都只是个假设。
回环绑定,以及 ports 与 expose 的区别
最小的有效修复,只有十一个字符。-p 127.0.0.1:5432:5432 告诉 Docker,写 DNAT 规则的时候带上一个目的地址限制,这样改写就只会套用在原本就已经是本地的流量上。一个冲着你公网地址来的远程数据包,从此不再匹配这条规则,不会被转发进容器,而是终于抵达了你一直以为它会去的地方:INPUT,在那里被 ufw 挡下。在 Compose 文件里,同样的事情写作 ports: ["127.0.0.1:5432:5432"],而引号在这里很关键——一个带冒号却没加引号的值,迟早会闹出一次解析事故。
但更值得问的问题是,这个端口究竟为什么要被发布出来。挂在同一个用户自定义网络上的容器,彼此之间可以直接通过服务名、用服务自己的端口互相触达,完全不需要任何形式的发布。你的应用不会去连 127.0.0.1:5432;它连的是 postgres:5432,由 Docker 内置的 DNS 解析到私有网桥上的一个地址。处在这种安排里的数据库,根本不需要 ports: 这一行——不需要回环那种写法,什么写法都不需要。最安全的已发布端口,就是那个被你删掉的端口。把 ports: 留给那一两个真正要面向公网的服务,其余的一切都放到私有网络上去交谈。
这里也正是 expose: 常常被误读的地方。它什么都不发布,也什么都不开放;它只是一份记录着某个服务监听哪个端口的文档说明,对防火墙不产生任何方向上的影响。人们添上它,是希望它是 ports: 的安全版本——从某种意义上说确实是,就像一段注释是代码的安全版本那样。如果你想要一个服务只能被它的邻居触达,你需要的不是 expose:;你需要的是压根不写 ports:。
回环绑定有两条诚实的局限。第一,它守住的是主机这个边界,而不是邻里之间:同一个网桥网络上的容器,彼此依然畅通无阻,所以一个被攻陷的前端容器,通往一个什么都没发布的数据库,仍然有一条现成的路。当爆炸半径真的要紧时,把服务拆到不同的网络上,并把后端那个标成 internal: true。第二,127.0.0.1 是一个 IPv4 地址,只能约束 IPv4;如果这台主机有一个可路由的 /64——这里的每个套餐都自带一个——就要把 v6 单独拿出来想清楚,也要单独测试。
DOCKER-USER:专为此而生的链
回环绑定,修好的是你还记得住的那些容器。DOCKER-USER 才是让你不再一个 -p 之差就撞上下一次事故的办法。Docker 把它装成 FORWARD 里最先被跳转到的一环,排在自己那些放行规则之前,而且——和它其他的链不一样——不管重启、升级还是新建容器,都不会去动这条链里的内容。这是它留给你的、用来放那些引擎自己推断不出的策略的官方位置:到底哪些来源,才被允许触达这台主机上的容器。
这套模式是公网接口上的三条规则,而顺序就是全部关键所在。第一条,放行已建立和相关的流量,这样你容器发起的连接,回包才能继续畅通。第二条,放行你真正想放进来的那些来源——一个办公室地址、一台监控主机、一台对等服务器。第三条,把其余一切从互联网上进来的流量统统丢弃。把 conntrack 规则弄错,是这里最经典的翻车方式:要是把 DROP 放在最前面,每一个容器发起的出站连接都会死在返回包上,表现出来就是"Docker 把 DNS 和软件包安装搞坏了",把人引到完全错误的方向去排查。
有两个操作层面的细节,决定了这套规则能不能扛住现实的考验。它必须按名字限定在公网接口上——-i eth0,或者 ip route get 1.1.1.1 在你这台机器上报出来的那个名字——否则你会连自己网桥之间的流量都一并丢掉。而且它必须每次重启之后都重新应用一遍,并且要在 Docker 守护进程建好这条链之后才应用。一个 oneshot systemd 单元、顺序设为 After=docker.service,是靠得住的做法;用 netfilter-persistent 保存下来也可以,前提是你得接受这样一个事实:恢复一整套规则,而 Docker 同时又在重建它自己那一套,这是一场你应该去验证、而不是想当然认定结果的竞速。
接下来是 IPv6 的那一半。如果 ip6tables -S DOCKER-USER 打印出了一条链,就把每一条规则原样镜像过去。如果它报错,说明你的守护进程压根没在管理 v6 规则——这说明不了 v6 流量能不能到达你的容器,只说明 Docker 没有在过滤它。这里不要靠推理去猜答案;不同版本、不同守护进程设置、不同发行版之间,路径都不一样,一个自信满满的错误结论,比没有结论更糟。用 nmap -6 扫描你自己,然后相信这个结果。
有一件事不要做:在 /etc/docker/daemon.json 里伸手去改 "iptables": false。这会让 Docker 不再碰防火墙,但同时也会让容器的 NAT、出站伪装和网络间隔离统统失去配置的对象。你并没有消除这个问题,你只是把这份工作继承了下来——手动地,为每一个容器,永远地。在一台单独的 VPS 上,诚实的选择是让 Docker 继续管好自己的链,而你负责管好 DOCKER-USER。
另一扇门:套接字,以及什么在以 root 身份运行
前面说的都是数据包如何抵达。这一节要说的是它们抵达之后会发现什么,其中有一项比其余所有加在一起都更糟,值得单独拎出来讲清楚:把 /var/run/docker.sock 挂进一个容器,等同于把这台主机的 root 权限交给了那个容器。不是"接近于"。是完全等同。任何能跟这个套接字对上话的东西,都能起一个新的特权容器,把主机的整个文件系统挂进去,然后从那里读走每一把密钥、改写每一个文件、装上它想装的任何东西。不少图省事的镜像都会要求挂载它——仪表盘、自动更新工具、带服务发现的反向代理。把这种请求当成一个决定来看待:你愿不愿意像信任自己的 root shell 一样信任这个镜像;对通过 TCP 暴露的 Docker API 也要一样小心——2375 端口上一个未经身份验证的守护进程,是同一扇门,只不过对着整个互联网敞开着。
除此之外,容器运行时还给了你四种成本很低的收缩面,一个都不需要你重新设计架构。用 user: "1000:1000" 以非 root 用户身份运行,因为命名空间内部默认就是 root,而这正是每一次越狱的起点。设置 security_opt: ["no-new-privileges:true"],让镜像里的某个 setuid 二进制文件永远没法拿到比启动它的那个进程更多的权限。用 cap_drop: [ALL] 丢掉全部 capabilities,只把服务确凿需要的那几项加回来——大多数 Web 应用其实一项都不需要。把根文件系统挂成 read_only: true,再给它配一小块 tmpfs 当草稿空间,这一下就把"在应用目录里丢一个 Web shell"从一个可行步骤变成了死路一条。
同样的逻辑也适用于交到容器手上的东西。以环境变量形式传入的密钥,对任何能读到进程环境的东西都是可见的,而且会被原样复制进 docker inspect 的输出,以及任何会把配置整个吐出来的日志或崩溃报告里;换成一个以只读方式挂载在固定路径上的文件,没那么方便,但泄漏面也小得多。而且一个容器需要的网络权限,不该超过它的工作本身:一个只跟数据库打交道的工作进程,根本没理由能对互联网发起连接。
如果这一切读起来像是在跟默认设置对着干,这个感受很公道,而这也正是无 root Docker 或者 Podman 的立论所在——在那种模式下,守护进程和容器都以一个非特权用户的身份运行,容器逃逸只会把你带到那个普通用户的身份,而不是 root,已发布端口也是由主机上一个普通的用户态进程占着,这意味着你的 ufw 规则能像平常那样套用在它们身上。这笔交易是真实存在的:一些 capabilities、一些存储驱动、一些网络花招,要么表现不一样,要么干脆用不了。值得先知道这个选项存在、它能换来什么,而不是等出了事之后才发现,默认设置本身也是一种选择。
SP·07不是你写的镜像,跑在一台你输不起的机器上
一个容器镜像,就是别人的文件系统,跑在你的机器上,由你从没读过的那些层拼装而成。这不是反对使用它们的理由——而是提醒你,得弄清楚自己在跑的是哪些镜像,它们又有多旧了。在小型基础设施上真正会发生的两种失败,说穿了都很平常:要么这个镜像从来就不值得信任,要么它三月份的时候还值得信任,只是后来再没有人重新构建过它。
第一种失败,主要靠对镜像来源的自律来解决。优先选择官方或供应商发布的仓库,而不是随手找一个只有三颗星的分支;尤其要提防那些卖点就是把六个服务塞进一行 YAML 里的镜像。在真正要紧的地方,按摘要而不是按标签锁定版本:postgres:17 是个会动的靶子,两次 docker compose pull 之间就可能在你脚下悄悄变掉,而 postgres@sha256:… 才是你测试过的那个确切文件系统。锁定版本,是用自动获得修复换来可重现性,如果你有重新构建的习惯,这笔交易是对的;如果没有,这笔交易就是错的。
第二种失败,咬起人来悄无声息。无人值守升级碰不到你的容器。你在第一小时里配置好的那套自动安全更新,打的是主机自身软件包的补丁,看不到镜像内部的用户态世界——所以一台自称已经完全打好补丁的机器,跑的 Web 服务器背后那个基础镜像,完全可能带着一年都没修的漏洞。容器不是被更新的,而是被替换的:拉取、重建、把不再被引用的东西清掉,按一个你真正能坚持下来的节奏去做。写下来的每月一次,胜过一个完美的打算。
这就让 Compose 文件成了这台服务器上最有价值的一个对象。它是这台机器是什么这件事唯一完整的描述,从它重建应该是一项日常操作,而不是一场考古项目——把它放进版本控制,把环境变量文件跟它放在一起,再把两者一起放进保存着你数据的那份异地加密备份里。检验一台容器主机的标准,不是它是不是正在运行,而是你能不能在一台全新的 VPS 上,原样把它重新搭起来,大概只需要 15 min。
SP·08日志、卷,以及比容器活得更久的状态
容器被宣传成可以随意丢弃的东西,这话对进程本身是对的,但对它留下的一切都是错的。一台 Docker 主机上会积累两种状态,而这两种状态,都会在最糟糕的时刻让人措手不及。
第一种是日志。默认的 json-file 驱动,会把你的容器写往 stdout 和 stderr 的每一行都记录下来,而且——除非你另外交代过——它从来不会做日志轮转。一个话多的应用,靠这个方式就能在几周之内把一块磁盘写满,而一台数据库主机上磁盘写满,本身就是一次事故。在 /etc/docker/daemon.json 里设一次上限,这样它就会套用到你之后启动的每一个容器上。
同一个设置也有一层隐私上的解读,而在一台你正是为了隐私才选中的服务器上,这一层解读恐怕才是更要紧的那个。一份从不轮转的访问日志,是每一个访客的 IP 地址、User-Agent 和请求路径的永久、未加索引的记录,躺在一块你并不实际掌控的磁盘上。这不是你决定要保留的东西。是默认设置替你做了这个决定。刻意去做这个决定,意味着给保留期限设一个上限,而更好的做法是,从源头上就少写一点:在反向代理这一层,用一种省略或截断客户端地址的日志格式,能保住运维价值,同时甩掉责任负担。你从没写下的数据,没法被查封、传唤或泄露——这跟促使人们从一开始就不让服务器留下你的名字的道理,其实是同一套道理。
第二种是卷,而这里的锋利之处在于一个参数。docker compose down 会停止并删除容器,但不会碰命名卷;docker compose down -v 则会带着同样的干脆劲儿,把它们永久删掉。数据库就活在那些卷里。你会想念的其他一切东西,多半也是。把数据放进命名卷,而不是匿名卷;搞清楚凌晨两点你正敲下的到底是哪条命令;还要记得,docker system prune 存在的意义就是回收空间,它很乐意把你的空间也一并回收掉。
剩下的就是备份本身,而容器这层抽象在这里最后再误导你一次。直接拷贝一个正在运行的数据库的卷目录,算不上备份;那只是一份在你读取的同时还在被写入的文件拷贝,而它在你最需要的那一刻,会以一个损坏的数据库的样子被还原出来。要通过引擎本身来备份——pg_dump、mysqldump,或者引擎自带的快照 API——然后把结果加密,送到一个你的服务器够不着的地方。最后这一句,在入侵者已经身处容器内部时才真正见分晓:一份连你被攻陷的主机都能删掉的备份,根本算不上备份。
分步操作
-
01
盘点这台机器眼下到底发布了什么
在改动任何东西之前,先把当前状态记录下来——之后你会需要拿它来做对比。把每个容器的
PORTS列都读一遍,再看看 Docker 实际装了哪些 NAT 规则,两个 IP 协议族都要看。凡是打印出来时没有带127.0.0.1这个目的地址的,不管ufw status怎么说,都是能从互联网上连到的。docker ps --format 'table {{.Names}}\t{{.Ports}}' ss -tulpen | grep -E 'docker|LISTEN' sudo iptables -t nat -S DOCKER # -d 127.0.0.1/32 = loopback only sudo ip6tables -t nat -S DOCKER # may not exist; that is an answer too sudo ufw status verbose # what you believed was true顺手把公网接口的名字也记下来——后面写防火墙规则时用得着,而且并不是每个镜像上它都叫
eth0。ip route get 1.1.1.1 | awk '{print $5; exit}' -
02
从别处扫描你自己
主机没法审计它自己:回环流量根本不会经过你正想测试的那些链。要从另一台机器、在另一个网络上运行这些命令——另一个地区、一台 $8.00/月的 VPS,就是最老实的测试台,用完之后它也还派得上其他用场。两个协议族都要扫,因为它们是分开配置的。
# from ANOTHER machine, against your server nmap -Pn -sS -p- --min-rate 1000 203.0.113.10 nmap -Pn -6 -p 22,80,443,3306,5432,6379,9200,27017 2001:db8::1 # no nmap? one port at a time is enough to settle an argument nc -zv 203.0.113.10 5432
凡是不属于 80、443 或者你 SSH 端口的开放端口,都是一项发现。把这份输出留着;它就是你最后想拿出来的那份证据里,"之前"的那一半。
-
03
不该公开的,就别再发布
现在从源头上修,也就是在 Compose 文件里动手。后端服务搬到一个私有网络上,彻底去掉它们的
ports:这一行——应用通过服务名去找它们。任何必须能从主机本身访问到的东西,改成一个明确的回环绑定,而不是一个光秃秃的端口。把后端网络标成internal,这样上面的任何东西都不能未经允许就跟互联网说上话。services: db: image: postgres:17 # ports: ["5432:5432"] # deleted: the app reaches it as db:5432 networks: [back] volumes: [dbdata:/var/lib/postgresql/data] app: image: myapp:1.4 environment: DATABASE_URL: postgres://app@db:5432/app ports: ["127.0.0.1:8080:8080"] # loopback only; proxy sits in front networks: [back, front] networks: front: {} back: internal: true volumes: dbdata: {}重新创建这套服务栈,确认 NAT 规则的样子变了——应用那一行 DNAT 规则现在应该带着一个回环目的地址,而数据库应该干脆一行都没有了。
docker compose up -d docker ps --format 'table {{.Names}}\t{{.Ports}}' sudo iptables -t nat -S DOCKER -
04
在公网前面,只留一样东西
在所有东西都改到回环之后,一个反向代理就成了唯一的公网门面——唯一终结 TLS 的地方,唯一能看到客户端地址的地方,也是唯一决定日志格式的地方。把它跑在主机上,或者放进一个只发布 80 和 443、别的什么都不发布的容器里;如果它跑在容器里,就让它加入
front网络,代理到服务名,而不是代理到回环地址。# in the http{} block of /etc/nginx/nginx.conf — no client address recorded log_format privacy '- - [$time_local] "$request" $status $body_bytes_sent'; # /etc/nginx/conf.d/app.conf (proxy on the host) server { listen 443 ssl; listen [::]:443 ssl; server_name example.com; access_log /var/log/nginx/app.log privacy; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }如果这么做的重点在于不让任何人知道这台机器在哪里,那这个代理就该整个放到另一台机器上——这正是把源 IP 隐藏在反向代理背后里讲的那套做法,而且它能和这里的一切组合起来用。
-
05
用 DOCKER-USER 补上缺口,两个协议族都要
回环绑定修的是你今天已经有的容器;这一步修的是你还没写出来的那些。公网接口上三条规则,conntrack 放在最前面,好让容器的出站流量继续畅通,然后是你允许的那些来源,最后是一条丢弃规则。应用之后马上测试一下容器的出站连通性——这正是人们最容易搞反的那条规则。
PUB=$(ip route get 1.1.1.1 | awk '{print $5; exit}') sudo iptables -I DOCKER-USER 1 -i "$PUB" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN sudo iptables -I DOCKER-USER 2 -i "$PUB" -s 203.0.113.7/32 -j RETURN # your admin address sudo iptables -I DOCKER-USER 3 -i "$PUB" -j DROP sudo iptables -S DOCKER-USER docker run --rm alpine sh -c 'apk update' >/dev/null && echo 'egress OK'如果守护进程也在管理 v6 规则,就把这套规则原样镜像一份给 IPv6,然后用一个在 Docker 重建好它的链之后才运行的单元,让这一切能扛过重启。
sudo ip6tables -S DOCKER-USER || echo 'no v6 chain: verify v6 reachability by scanning' # /etc/systemd/system/docker-user-rules.service [Unit] After=docker.service Requires=docker.service [Service] Type=oneshot ExecStart=/usr/local/sbin/docker-user-rules.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target
-
06
收回容器内部的权限,给日志设个上限
缩小一个被攻陷的镜像能做的事,同时别让主机被一份没有人主动选择要留下的记录塞满。这四项容器设置,对一个普通的 Web 服务来说不花任何代价;日志上限则会套用到守护进程重新加载之后启动的每一个容器上。
services: app: image: myapp:1.4 user: "1000:1000" read_only: true tmpfs: [/tmp] cap_drop: [ALL] security_opt: ["no-new-privileges:true"] # never: volumes: ["/var/run/docker.sock:/var/run/docker.sock"]# /etc/docker/daemon.json { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }sudo systemctl reload docker docker inspect --format '{{.HostConfig.LogConfig}}' app -
07
从外部验证,再写进运维手册
找个没什么东西依赖结果的时候,主动重启一次,证明你搭好的状态,就是重启回来之后的那个状态。然后把第二步里的外部扫描再跑一遍,和你保留的那份输出做个对比——真正的交付物是这份差异,不是那些命令本身。
sudo reboot # after it returns: sudo iptables -S DOCKER-USER # rules reapplied? docker ps --format 'table {{.Names}}\t{{.Ports}}' # from ANOTHER machine again: nmap -Pn -p- 203.0.113.10 nmap -Pn -6 -p- 2001:db8::1找个你自己一定能找得到的地方,写下五行字:规则绑定在哪个接口上、那个 unit 文件放在哪里、哪些服务是故意公开的、镜像上一次重建是什么时候,以及怎么恢复那些卷。一台容器主机的好坏,取决于那份能让你在最糟的一天之后把它重建回来的说明,仅此而已。


