多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Docker容器IP封禁不生效?正确入口是DOCKER-USER链

Docker容器IP封禁不生效?正确入口是DOCKER-USER链 接到一个运维任务部署在Docker里的服务被某个恶意IP疯狂刷接口量不大但很烦人老板要求立刻封掉那个IP。你手速很快一条iptables -A INPUT -s x.x.x.x -j DROP甩下去然后——发现根本没生效。别急这几乎每个玩Docker的运维都踩过。问题出在Docker的网络模型上它默认走的是NAT转发包根本不是从INPUT链进来的你封错了地方。这篇文章就来把这个坑彻底填平说清楚Docker环境下的iptables策略为什么不一样、正确的封禁入口在哪、命令怎么敲、怎么验证、重启后怎么不失效以及各种能让你怀疑人生的边角案例。## 1. 问题定位为什么对Docker容器直接封IP总是不生效 ### 1.1 Docker网络模型的本质 Docker装好之后默认会创建一个名叫docker0的虚拟网桥。所有容器只要不特别指定网络模式都会通过一对veth虚拟网卡挂到这个网桥上。网桥本身是一个二层设备负责把容器发出的数据包转发到宿主机协议栈再由宿主机的路由和iptables来做三层转发和NAT。 这里最关键的点出现了外部客户端访问一个映射了端口的容器服务比如-p 8080:80数据包进到宿主机网卡后会先经过PREROUTING链被DNAT规则把目的IP和端口改写成容器的IP和端口然后走路由判定发现目标地址是容器网段于是交给FORWARD链最终经docker0转发进容器。 注意整个过程中数据包的“目的IP”已经被改写成容器IP通常是个172.17.0.x之类的私网地址它从头到尾都不涉及本机的INPUT链。所以你辛辛苦苦在INPUT链上封禁源IP内核压根就不看这条链规则自然成了摆设。 ### 1.2 为什么Docker会给iptables加这么多规则 你如果运行iptables -L -n -t nat和iptables -L -n -t filter会看到docker自动塞了几条自定义链DOCKER、DOCKER-USER、DOCKER-ISOLATION-STAGE-1/2。这是Docker的端口映射机制在背后操作iptables它要在PREROUTING里做DNAT在FORWARD里做放行在POSTROUTING里做SNAT/MASQUERADE。 很多人不知道的是Docker每次创建容器或重启容器时都会动态调整这些链。如果你把规则加到FORWARD链上或者自己定义了一条链但插在了Docker管理的链之前很可能被Docker重启容器时清理掉或者因为规则顺序问题根本不生效。 ### 1.3 封禁入口选错会导致的连锁反应 常见错误是两类一类是上面说的只封INPUT完全无效另一类是直接在FORWARD链里全局限制结果把正常的容器互通、docker exec访问宿主机、跨容器调用全部搞挂。我见过有人加了一条iptables -I FORWARD -s 1.2.3.4 -j DROP后其他容器访问外网通的但新起的容器网络不通了排查了很久发现是FORWARD链顺序问题Docker在FORWARD链里插入了自己的动态规则位置在你那条DROP之后数据包已经被丢弃了连锁都没走到。 所以核心结论先给出来**给Docker环境下的服务封IP正确的入口是DOCKER-USER链**。这条链是Docker官方专门为用户自定义过滤规则预留的天然在Docker自动生成的动态规则之前被检查而且Docker不会覆盖或清空它。 ## 2. 动手实践通过DOCKER-USER链封禁容器服务的IP ### 2.1 先确认当前环境与数据链路 动手之前先做三件事确认系统里iptables存在且不是nftables纯模式、确认Docker服务正常、确认业务流量真实经过宿主机转发。 第一件事用iptables -L -n看输出如果有DOCKER-USER链说明Docker在filter表里建好了这条链如果没看到多半是Docker版本太老或者用了--iptablesfalse启动daemon。第二件事systemctl status docker确认服务处于running。第三件事在宿主机上用tcpdump -i docker0 host 1.2.3.4观察目标IP是否有流量经过这一步非常重要能帮你确认恶意IP的流量到底走的是哪个接口、哪个方向。 我通常在验证阶段同时开两个终端一个跑tcpdump一个执行封禁命令。如果tcpdump没看到包说明流量可能根本没到宿主机可能在更前面就被云安全组挡了这时候你要么规则写错要么源IP观察错了别急着往下走。 ### 2.2 封禁单个IP的准确命令 确认链路之后执行真正的封禁 bash iptables -I DOCKER-USER -s 1.2.3.4 -j DROP这里用-I插入到最前面比用-A追加到末尾更合理。原因很简单Docker自己会在DOCKER-USER链里追加一些规则吗不会但你的规则如果放在后面一旦有其他管理员手滑加了宽松规则前面已经把流量放过去了DROP规则就形同虚设。放最前面保证恶意IP的第一个包就被丢掉。如果恶意IP是个动态范围可以直接封一个CIDRiptables -I DOCKER-USER -s 1.2.3.0/24 -j DROP如果还要限制具体服务端口比如只封对一个容器的8080端口访问iptables -I DOCKER-USER -s 1.2.3.4 -p tcp --dport 8080 -j DROP这里有个细节值得说透--dport写在DOCKER-USER链里匹配的是DNAT之前的目的端口还是DNAT之后的实际上DOCKER-USER链是在FORWARD链里被调用的而Docker的DNAT规则在PREROUTING里所以当包到达DOCKER-USER链时DNAT已经做完了。换句话说如果你容器映射的是8080:80外部客户端实际上访问的是宿主机的8080端口你能挡住的--dport 8080这个条件但在DOCKER-USER链里看到的--dport可以是8080因为DNAT改的是目的IP端口从8080映射到80是Docker userland-proxy或iptables DNAT同时做的。实测中两者都存在的情况是进入链时端口已经被改写所以你更稳妥的做法是用IP维度去封或者配合-m conntrack配合--ctorigdstport来匹配原始目的端口。后面我会详细说这个坑。2.3 封禁后如何验证真的生效加完规则不是结束必须验证三件事规则确实在链里、流量确实被丢弃、业务容器本身没受影响。第一步iptables -L DOCKER-USER -n --line-numbers查看规则列表确认你加的规则排在第一位。第二步在宿主机上tcpdump -i docker0 host 1.2.3.4能看到来自该IP的TCP SYN反复重传但始终没有ACK回来——这个现象说明包已经被宿主机丢弃但因为TCP重传机制客户端还在傻等。如果用curl --interface eth0 --connect-timeout 3 http://你的容器服务地址模拟会直接超时如果客户端在远端跑表现就是卡住、转圈、然后连接超时。第三步换一个不受影响的IP访问相同服务确认服务正常响应。这一步是为了排除“封过头”的风险比如误伤到代理出口IP或者公司专线IP。我吃过一次亏封攻击IP时顺手把阿里云SLB的健康检查IP段封了结果整组容器被认为不健康被摘流量差点酿成事故。从那以后封任何IP之前我都会先看一眼云控制台里有哪些健康检查、NAT网关、堡垒机的IP段。3. 深入拓展端口、CIDR与防误伤的组合封禁策略3.1 按原始目的端口封禁的正确写法前面提到DNAT导致--dport匹配混乱的问题这里展开说。假设容器映射关系是宿主机的8081端口 - 容器的3306端口一个攻击者从5.6.7.8访问http://宿主机IP:8081你希望只封这个源IP对8081的访问而不能误伤它访问别的服务。在DOCKER-USER链里如果直接写-p tcp --dport 8081实测有时能匹配到有时匹配不到取决于Docker版本和数据包在NETFILTER里的处理顺序。最稳的写法是用conntrack模块匹配原始目的端口iptables -I DOCKER-USER -s 5.6.7.8 -p tcp -m conntrack --ctorigdstport 8081 -j DROP这种写法会直接查连接跟踪表里的原始方向目的端口不受DNAT改写影响语义明确这个包最初的目标就是宿主机的8081。我把这条命令用在生产环境后再没出现过“规则加了两条结果一条都不生效”的怪象。3.2 让白名单IP永远排在黑名单之前的规则顺序如果你的服务对内开放对外只允许特定IP访问比如一个管理后台只让公司办公网出口IP访问同时黑名单里又有一些伪装成公司IP的攻击者这时候规则顺序就特别敏感。推荐的顺序是先放行白名单CIDR再丢弃其他所有IP的特定端口最后丢弃明确的黑名单IP。iptables -I DOCKER-USER -s 42.186.0.0/16 -p tcp --dport 8443 -j ACCEPT iptables -I DOCKER-USER -p tcp --dport 8443 -j DROP iptables -I DOCKER-USER -s 42.186.7.8 -j DROP注意这里-I每次都是插到最前面所以第三条黑名单规则会跑到最前面黑名单先丢然后白名单放行最后默认丢其他流量。执行顺序要按“先加最后生效的策略”也就是把黑名单放最前面不然白名单放行规则一旦在前伪装成公司IP的攻击者也跟着放行了。3.3 封禁容器访问外部特定IP的场景还有一种情况是容器主动连外部某个恶意地址比如中了挖矿木马容器不断向外联。这种封法方向相反要在DOCKER-USER链里把“出去的路”也断了iptables -I DOCKER-USER -d 6.7.8.9 -j DROP这条规则对来自任何容器、发往目标IP为6.7.8.9的包生效。如果只是某个容器往外连还想放行其他容器就得结合-i docker0和-o eth0这样的接口维度来做方向匹配iptables -I DOCKER-USER -i docker0 -o eth0 -d 6.7.8.9 -j DROP写这种规则时我习惯在注释里写明用途因为三个月后你自己都可能忘了这条规则是防谁的。-m comment --comment block outbound to miner pool加上去排查的时候能省半小时。4. 重启失效问题规则持久化与自动化方案4.1 为什么规则会丢以及怎么保住Docker容器重启不会清空DOCKER-USER链但宿主机重启、iptables服务重启或者Firewalld接管时规则就会全部丢失。很多人坑在写好规则后没保存半年后机房断电重启服务恢复了但攻击者的IP也恢复访问了。最简单的方案是安装iptables-persistent工具在Debian/Ubuntu上执行apt install -y iptables-persistent netfilter-persistent save它会读取当前活动规则并写入/etc/iptables/rules.v4和rules.v6下次开机时自动恢复。CentOS/RHEL系用iptables-services包service iptables save写入/etc/sysconfig/iptables。4.2 把封禁逻辑写进启动脚本的容错设计实际生产环境中要比这个更谨慎一点。我用得最多的方案是写一个独立脚本block_ips.sh里面包含所有黑名单和白名单逻辑开机后由systemd服务拉起来。这样做的好处是脚本本身就是文档谁接手都能看懂封了哪些IP。脚本开头要加一个保护机制先flush掉DOCKER-USER链里自己曾经加过的规则再重新加载。比如用iptables -F DOCKER-USER但这会把别人手动加的规则也清掉。更精细的做法是只删除带特定comment的规则或者用iptables-save和iptables-restore整体原子替换。我分享一套简单可靠的流程当前规则备个份iptables-save /root/iptables-backup-$(date %F).rules写好新的规则文件开头用*filter结尾用COMMIT执行iptables-restore /root/rules-docker-user.rules验证一遍验证没问题后再把它落成开机脚本这套方法我用了很久从来没有因为规则加载顺序问题导致容器网络起不来。4.3 结合Fail2ban实现自动封禁如果你嫌手撸黑名单太累可以接入Fail2ban。它本身支持iptables和nftables后端针对Nginx日志里的401、404刷接口行为自动拉黑IP。核心配置是让Fail2ban把封禁规则写到DOCKER-USER链里而不是默认的INPUT链。方法是在/etc/fail2ban/action.d/iptables.conf里自定义actionban参数actionban iptables -I DOCKER-USER -s ip -j DROP actionunban iptables -D DOCKER-USER -s ip -j DROP这样Fail2ban自动封禁的IP会直接作用在Docker转发链路上容器服务立刻断连。我实际用下来比默认的INPUT链方案靠谱太多因为INPUT链对Docker容器流量压根不起作用。但要注意Fail2ban自己的ignoreip白名单一定要配好否则误封了自家运维IP连登录都难。5. 常见问题与排查技巧实录5.1 封了IP但容器日志里还能看到该IP的请求这个现象非常迷惑人。你明明封了容器里却还在打印攻击者的请求日志。原因是容器日志打印的是应用层收到的请求如果你的服务前面还有一层负载均衡比如Nginx代理、SLB攻击者打到LB上LB再转发给容器时源IP往往被改成了LB的内网IP。你在宿主机封的是攻击者原始IP但到容器这一跳的源IP已经变成LB地址了所以容器还能“看到”请求。解法是在LB层封禁或者使用X-Forwarded-For头在应用层做防护。宿主机iptables封禁只对“攻击者直连宿主机映射端口”的场景生效。我遇到过好几个同事在这个地方纠结半天最后发现封源IP根本没意义因为流量过了四层负载均衡。5.2 DOCKER-USER链不存在怎么办低版本Docker可能不自动创建DOCKER-USER链还有些人将Docker配置成了--iptablesfalse这时你可以手动新建这条链iptables -N DOCKER-USER iptables -I FORWARD -j DOCKER-USER但更推荐的做法是检查Docker版本升级到20.10以上并确认daemon配置里没有禁用iptables。--iptablesfalse这个参数在生产环境要慎用关掉它之后端口映射会失效你得自己管所有网络规则复杂度直线上升。5.3 容器内外访问结果不一致的问题排查有时候你封了外部IP容器内部通过docker exec去ping该IP却是通的这不奇怪。docker exec跑命令的时候数据包是容器自己发出去的走的路由是容器到docker0再到宿主机协议栈它不经过宿主机的eth0入口所以DOCKER-USER链里针对从外进入方向的规则不会触发。如果想让容器内也断掉这个IP需要加出方向限制也就是前面提到的-d写法。还有一种情况是宿主机本机访问容器服务不受控制。比如你在宿主机上curl localhost:8080这个包走的是回环接口根本不上FORWARD链DOCKER-USER拦不到它。这种流量要封的话得在INPUT链上针对lo接口的dport做限制但一般没人这么干因为属于本机信任访问。5.4 规则性能和误伤后果DOCKER-USER链是所有经过宿主机转发的容器流量都要查一遍的链所以在该链上不要堆太多规则。我见过有人把几千个IP全塞进去结果容器间互通延迟明显上升Nginx转发效率掉了近一成。这时候该上IPsetipset create blacklist hash:ip ipset add blacklist 1.2.3.4 iptables -I DOCKER-USER -m set --match-set blacklist src -j DROPIPset的哈希查询是O(1)几千个IP基本无感而且支持CIDR聚合管理起来也方便。封禁解封直接改IPset就行不用反复改iptables规则ipset save/restore同样能持久化。6. 踩坑与经验总结做运维这几年和Docker iptables打交道下来最大的体会是一定要先搞清楚流量路径再动手封禁。很多人iptables熟得不行但对Docker网络转发的特殊性没有概念眉毛胡子一把抓封错链、规则顺序没排对、重启丢规则各种坑走一遍才老实。我在实际工作里养成了几个习惯写在这里供你参考第一所有自定义iptables规则必须加-m comment写明加规则的原因和操作人。半夜被叫起来排查攻击的时候看到一条没有注释的DROP规则真的会想骂人。第二每次封禁操作前先备份规则文件iptables-save /root/backup/iptables-$(date %s).rules。封错IP、封错网段、封到自家出口IP的事情我都遇到过一键回滚的依赖就是这份备份。第三不要依赖防火墙规则来防应用层攻击。iptables在四层以下做过滤很高效但对HTTP CC攻击、慢速攻击这些七层的东西无能为力该上WAF、Nginx限流还是得上。第四定期清理过时的封禁规则。很多IP是临时攻击封三天就该解封长期堆在链里除了拖慢匹配速度还可能造成不可预知的结果。我一般每月做一次iptables -L DOCKER-USER -n --line-numbers逐条审核还能不能看懂、还成不成立看不懂的一律清掉重建。如果你现在正对着不生效的DROP规则发愁别急按着这个思路来先跑iptables -L DOCKER-USER -n确认链里有规则再tcpdump -i docker0 host 攻击IP确认流量路径最后用conntrack确认NAT关系大概率能在十分钟内定位问题。Docker和iptables的组合拳说穿了就是一层窗户纸捅破之后你会觉得一切都很清晰。
返回列表