在Linux服务器上做IP访问控制,这个需求我前后经手了不下十次。每次都有几台服务器要封某个恶意IP、给办公网段放行SSH、或者限制某个应用只允许指定来源访问。标题里“禁止或允许指定IP的访问”看似简单,但实际情况远比一条命令复杂:规则写在哪个表、插在哪个位置、重启之后还在不在、会不会把自己锁在门外,这些问题我都踩过。这篇文章就把我多年实操下来验证过的方案讲透,从原理到命令再到排查,适合刚接触Linux的运维新人,也适合被这个问题反复折腾过的老手。
1. 需求拆解与方案选型:先想清楚再动手
1.1 这个需求到底在解决什么问题
“禁止或允许指定IP的访问”这句话拆开看,至少要回答三个问题:禁的是谁、禁的哪种访问、打算在哪一层禁。
“禁的是谁”最容易理解,就是源IP地址,比如某个攻击者的IP、某个抓数据的爬虫IP、某个临时接入的陌生网段。“禁的哪种访问”就要区分了:是干脆完全不能碰这台机器,还是只不能访问某个端口、某个服务?这两者的含义完全不同,防火墙规则写起来也是两套思路。“在哪一层禁”是很多人忽略的关键点——Linux下面有网络层的iptables/nftables、有服务层面的TCP Wrapper(/etc/hosts.allow和hosts.deny)、还有Nginx或Apache这类应用层的deny配置。网络层拦截发生在数据包进入系统早期,资源开销小、拦截范围广;应用层拦截则能识别更细的信息,比如URL路径、User-Agent,但会让请求先到达服务进程,消耗更多的CPU和内存。
我见过不少同事一上来就写Nginx的deny,结果发现爬虫根本不走Nginx,或者直接打到其他端口;也见过有人随手加一条iptables DROP规则,把整个办公网段的正常开发和测试流量全断了。所以做访问控制之前,先花两分钟确认这三个维度,比急着敲命令有价值得多。以最常见的运维场景来说,绝大多数需求都是“针对某个源IP或网段,控制对SSH、Web、数据库等具体端口的访问”,这种场景首选Linux内核自带的netfilter框架,也就是iptables或它的继任者nftables。
1.2 访问控制的三个层面
搞清楚技术选型之前,先认识一下Linux上做IP访问控制经常涉及的三个层面。
第一个层面是网络层和传输层,由netfilter在内核里实现,对外暴露iptables、nftables、firewalld等工具。这一层的拦截点是所有网络数据包必经之路,无论数据是发给Nginx、MySQL还是任何其他进程,只要规则命中就会被丢弃或拒绝。这也是我最推荐的处理方式:效率高、覆盖面广、不依赖具体业务进程。
第二个层面是TCP Wrapper,通过hosts.allow和hosts.deny两个文件控制对某些服务的访问。需要注意,这个机制只对编译时链接了libwrap库的服务有效,比如早期的telnet、部分版本的ssh,而现在大部分服务都是独立实现网络逻辑,不读这两个文件,所以这个方案天然存在覆盖不全的问题,我在现代发行版上基本不推荐依赖它。
第三个层面是应用层面的访问控制,Nginx有deny和allow指令,Apache有mod_authz_host,SSH本身有AllowUsers、AllowGroups,MySQL有user表中的host字段。应用层控制粒度最细,比如可以做到“只允许某个IP访问某个URL”,但代价是请求已经进入业务栈,没有网络层那么节省资源。合理的做法是分层配合:网络层做粗粒度拦截,应用层做细粒度控制,而不是只用其中一层。
1.3 工具选型对比:iptables、firewalld、nftables、hosts.allow
Linux下能完成IP访问控制的工具不少,我把它们放一起做了个对比,方便你按场景选择。
| 工具 | 底层框架 | 特点 | 适合场景 | 主要缺点 |
|---|---|---|---|---|
| iptables | netfilter | 规则直观、资料多、几乎所有Linux版本可用 | 老系统、习惯命令行直接操作的运维场景 | 规则成百上千后管理混乱;被舍弃但长期维护 |
| firewalld | netfilter(动态) | 支持zone和rich-rule、支持运行时和永久配置分离 | RHEL/CentOS/Fedora等默认系统,日常快速管理 | 概念较多,学习曲线比iptables略陡 |
| nftables | netfilter | 语法更简洁、支持批量规则集、性能更好 | 新安装的Debian 12+/Ubuntu 22.10+/RHEL 9+ | 资料相对少,老教程多为iptables写法 |
| hosts.allow/hosts.deny | libwrap | 配置文件简单、改完即时生效 | 仅兼容libwrap的旧服务 | 覆盖范围太小,新服务基本不管用 |
如果你正在用CentOS 7、Ubuntu 18.04这类还在支持期的老版本系统,iptables依然是最稳的选择。如果你用的是RHEL 9、Ubuntu 22.04或者更新的系统,我的建议是把nftables作为主要理解方向,但因为兼容性和习惯原因,不少人还是继续用iptables命令,系统也会做兼容。我的经验是:不要把工具之争上升到信仰层面,先保障需求能解决,再逐步向新工具平滑过渡。
2. netfilter原理速览:规则到底是怎么匹配的
2.1 数据包进入Linux后的完整路径
我一直跟新同事说,理解iptables不用背所有细节,只要图景清晰,命令随手就能写出来。当一个数据包从网卡进入Linux系统后,它会沿着一条固定路径移动:先是网卡驱动收包,进入内核协议栈,然后经过路由决策决定这个包是发给本机的、还是要转发的。如果目标地址是本机,数据包就会经过INPUT链,最终交给对应的用户态进程,比如sshd或nginx;如果目标地址不是本机且开启了转发,则经过FORWARD链继续转发。
这个“本机包走INPUT、转发包走FORWARD”的区别非常关键。我遇到过不止一次:在云服务器上明明用iptables放行了某个源IP的80端口访问,但请求还是进不来,查到最后发现服务跑在Docker容器里,数据包先经过Docker的DNAT规则转到容器IP,前面的INPUT规则根本没有参与“主角”的身份。这种场景的处理逻辑完全不同,后面会专门聊。
netfilter框架在这条路径上安排了多个钩子点,每个点对应一条链:数据包刚进来还没做路由决策时是PREROUTING,发给本机时是INPUT,本机发出时是OUTPUT,转发时是FORWARD,最后临出门前是POSTROUTING。你写“禁止或允许指定IP访问”时,绝大多数情况下只需要关心INPUT链。
2.2 四表五链:别被吓到,你只需要两条
网上讲iptables都会提到“四表五链”,这四个表分别是raw、mangle、nat、filter,每条链可以挂多个表,并且顺序固定。这个顺序我不建议死记,只要记住一个原则:做IP访问控制只需要filter表,其余三个表各有专攻——raw表用来处理连接跟踪、mangle表用来修改数据包头、nat表用来做地址转换。
filter表天然挂载在INPUT、FORWARD和OUTPUT三条链上,我们绝大多数封禁和放行动作都发生在INPUT链上。比如下面这条命令:
iptables -A INPUT -s 192.168.1.100 -j DROP它的意思就是在filter表的INPUT链上追加一条规则:如果源地址是192.168.1.100,就直接丢弃。理解这条命令只需要知道三个部件:-s指定源地址、-j指定动作、INPUT指定链。动作除了DROP还有REJECT,两者的区别是DROP直接沉默丢掉数据包,客户端表现像连接超时;REJECT会回一个拒绝消息,客户端会立刻看到“Connection refused”之类的报错。从隐蔽性角度,抵御扫描时DROP更好,因为它让攻击者难以判断端口是关了还是被防火墙拦了;从排障方便角度,REJECT更友好,能快速区分规则拦截和网络不通。
filter表最常用的动作还有ACCEPT,就像它的名字一样,直接放行。三段合一就构成了访问控制的最小完整逻辑:先放行你信任的、再拒绝你不信任的、最后处理其他一切。
2.3 规则匹配顺序:为什么有时候“明明写了却没用”
iptables规则是按插入位置从上到下逐条匹配的,一条规则命中后,除非动作明确不终止匹配,否则数据包就不会再看后面的规则。这个特性引出了两个高频坑。
第一个坑是“先DROP后ACCEPT等于没有ACCEPT”。如果INPUT链有这样的顺序:
iptables -A INPUT -s 192.168.1.100 -j DROP iptables -A INPUT -s 192.168.1.100 -p tcp --dport 22 -j ACCEPT那第二条放行规则永远不会执行,因为第一条匹配到源IP就DROP了。想要“只封某IP的80端口、但允许它访问22端口”,必须先写精确的端口规则、再写大范围的封禁规则,顺序反过来就是给自己挖坑。
第二个坑是“默认策略”的作用。iptables链的策略(policy)是所有规则都没匹配时兜底生效的:策略是ACCEPT,那就放行;策略是DROP,那就拒绝。很多安全加固教程会让你把INPUT默认策略改成DROP,这种模式下你必须在前面显式放行SSH、80、443等必要端口,否则重启规则后你就彻底连不上了。我见过有同学照着教程执行到最后,回车一敲,远程会话立刻卡死——因为默认策略改成DROP后,已有的ESTABLISHED连接没放行,SSH被自己砍了。
在规则顺序上,我总结了一套稳定可靠的习惯,也是之后所有命令组合的基础:在顶部先放行所有已建立连接,再放行明确信任的源IP,然后拒绝不想要的,最后兜底策略。
3. iptables实操:禁用/放行指定IP的完整命令
3.1 最常用命令拆解
先看最基础的三条命令,它们覆盖了80%的日常需求。
# 禁止指定IP访问本机所有服务 iptables -I INPUT -s 192.168.1.100 -j DROP # 允许指定IP访问本机SSH服务 iptables -I INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT # 放行已建立的连接和关联连接 iptables -I INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT逐条解释。第一条用了-I参数,表示把规则插入到链的最前面,这个细节很多人不留意,后面会讲为什么必须插前面。第二条用-p tcp限定协议、--dport 22限定端口,再加上-s指定的源网段,组合起来就是“只允许192.168.1.0/24网段访问22端口”,这是做SSH白名单最标准的写法。第三条是护身符:不放行ESTABLISHED状态的话,你主动出去的连接返回的数据包可能被视为新连接被拦掉,表现就是“能连出去但收不到响应”,这条规则应该常年待在规则列表最顶部。
不少教程里还会用到-s参数配合!来排除,比如“不允许某个IP,但其他都允许”,写成:
iptables -A INPUT -s ! 192.168.1.100 -j ACCEPT我实际工作中会用得比这个少,因为否定匹配很容易让人读规则时产生误解,而且如果后面还叠加其他规则,排查难度翻倍。我倾向于把所有允许的都摆在前面,让人一眼能看出白名单,末尾统一拒绝,这种规则的“可读性”比“精简性”重要得多。
3.2 三个高频业务场景的命令组合
场景一:SSH服务只允许办公网段访问。这算是最常见也最刚需的访问控制。完整做下来建议按这个顺序执行:
# 先备份现有规则,出问题能回滚 iptables-save > /root/iptables-backup-$(date +%F).rules # 放行已建立的连接 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 放行办公网段访问SSH iptables -A INPUT -s 192.168.10.0/24 -p tcp --dport 22 -j ACCEPT # 放行本机回环接口,很多服务之间的本地通信依赖它 iptables -A INPUT -i lo -j ACCEPT # 把所有其他INPUT流量默认丢弃 iptables -P INPUT DROP这套组合的关键在于顺序。默认策略DROP放在最后一步执行,前面所有ACCEPT都准备好之后,才“关门”。如果把DROP提前,你后面再添加ACCEPT时会有一瞬间的断连风险,我实际操作中尽量避免这种窗口。另外记得加回环接口放行,曾经有个同事没放行lo口,数据库本地连接全部失败,排查了很久才发现是这条规则的问题。
场景二:封禁某个恶意IP访问Web服务。生产环境的临时封禁我通常这样写:
# 封禁恶意IP访问80和443端口 iptables -I INPUT -s 203.0.113.8 -p tcp -m multiport --dport 80,443 -j DROP # 或者更严格的封禁:该IP完全不能碰这台服务器 iptables -I INPUT -s 203.0.113.8 -j DROP如果只是防攻击者打网站,用第一种就够了,精确到端口可以减少误伤;如果是扫描器、漏洞探测这类行为,直接整机封禁更省心。用-I插入而不是-A追加,是因为如果当前规则列表里已有其他ACCEPT规则覆盖了这个IP,追加在后面的DROP永远匹配不上,插到前面才能确保新规则最先生效。这个细节曾经让我排查了大半天,现在写封禁命令时已经形成肌肉记忆。
场景三:临时放行某个IP做远程调试。常见于让合作伙伴联调或者临时给异地同事开权限:
# 放行指定IP访问MySQL端口,只给1小时 iptables -I INPUT -s 203.0.113.55 -p tcp --dport 3306 -j ACCEPT # 一小时之后手动删除该规则 iptables -D INPUT -s 203.0.113.55 -p tcp --dport 3306 -j ACCEPT删除规则时,最稳妥的方式是完整复制添加时的命令,把-I改成-D,这样能保证删除的规则与被删的规则参数完全一致。若只按行号删除,比如先iptables -L -n --line-numbers再按编号删,一旦期间有人动过规则,编号就错位了,很容易误删其他重要规则。
3.3 规则持久化:重启不丢
在CentOS 6/7时代,大家习惯把规则写到/etc/sysconfig/iptables,然后通过iptables-save和iptables-restore来回导。到了systemd时代,不同发行版做法略有差异。
CentOS/RHEL 7及以上,如果你装了iptables-services这个包,可以用:
iptables-save > /etc/sysconfig/iptables systemctl enable iptables systemctl restart iptablesDebian/Ubuntu上更常见的是安装iptables-persistent:
apt install -y iptables-persistent netfilter-persistent save netfilter-persistent reload这里我要提醒一个很多教程不讲的点:用firewalld作为默认防火墙的发行版上,直接用iptables命令添加的规则在firewalld reload后会被清掉。原因在于firewalld重启时会根据自身的配置重新生成本身管理的那套iptables/nftables规则,你手动加的规则不在它的配置体系里,自然就被冲掉了。解决办法有两个:要么统一用firewalld的配置方式管理,后面第四章详细讲;要么确认系统firewalld是停止状态,再用纯iptables规则。最怕的是同时开两个管理工具,规则互相覆盖,排查起来让人头大。
4. firewalld与nftables:新系统上的简单姿势
4.1 firewalld的两种配置方式
firewalld相比传统iptables,一个很大的改进是区分“运行时规则”和“永久规则”。运行时规则立即生效、重启后消失,适合临时改;永久规则需要配合--permanent参数,reload之后生效,适合长期配置。初次接触的人最容易犯的错是加了--permanent就以为立刻生效,结果发现连接还在,实际是忘了reload。
firewalld里做IP访问控制,最直观的方式是富规则(rich rule)。允许和禁止指定IP分别是这样:
# 允许指定IP访问22端口 firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.50" port port="22" protocol="tcp" accept' # 禁止指定IP访问本机所有端口 firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.8" drop' # 重载使永久规则生效 firewall-cmd --reload富规则的语法初看复杂,其实结构固定:family指定协议族、source address指定源地址、port和protocol指定端口协议、最后是动作。多规则之间支持逻辑组合,比如限定指定源IP只能访问指定服务端口,这在传统iptables里要写好几条,富规则一句话就能表达清楚。
还有一种方式是直接管理源和端口。比如把某IP加进信任区,它就能访问几乎所有被放行的服务:
firewall-cmd --permanent --add-source=192.168.10.0/24 --zone=trusted firewall-cmd --reload用zone做管理,适合业务网段和公网IP分层级处理的场景。trusted区是白名单思路,public区是默认对外区,把不同来源的流量丢进不同zone,比在一个链里堆几十条规则清晰得多。
4.2 从iptables平滑迁移到nftables
新版系统上netfilter的默认命令已经逐渐切换为nft,RHEL 9、Debian 12、Ubuntu 22.04之后的版本都支持。nftables的语法和iptables差异不小,但表达同样逻辑时其实更简洁。封禁和放行指定IP对应的nft命令大体长这样:
# 禁止指定IP nft add rule inet filter input ip saddr 203.0.113.8 drop # 允许指定IP访问22端口 nft add rule inet filter input ip saddr 192.168.10.50 tcp dport 22 acceptnftables里“表”和“链”需要提前创建,在例子中假定已经存在一张inet族的filter表和input链。inet族是个非常实用的设计,它同时覆盖IPv4和IPv6,不需要像iptables时代那样ipv4写一套、ipv6再写一套。如果你接收到一台新装的Linux,不确定默认链在哪,可以先nft list ruleset看看当前规则集,这是nftables的推荐调试方式,和iptables-save的作用类似。
从iptables迁移的路径上,很多人会直接用iptables-nft这个兼容层,命令照旧,底层翻译成nftables。短期应急没问题,但我还是会建议逐步把关键规则用nft原生语法重写,因为兼容层的翻译有时会产生一些不易阅读的规则,排障时增加理解成本。
4.3 怎么选:我的建议
工具越来越多,选型没必要纠结到失眠。根据我的实操经验,给出如下建议:如果服务器是CentOS 7/8、Ubuntu 20.04及更早版本,默认iptables就行,资料多、踩坑经验写出来的人也多;如果是RHEL 9、Rocky Linux 9、Ubuntu 22.10以上系统,直接学nftables,一步到位;如果你管理的是云主机且官方提供了安全组,那么第一道防线放在云安全组上,第二道防线再在系统里用firewalld或nftables做精细化控制,多层校验才能避免单点失误。
我个人的习惯是,云上服务器优先用云平台的安全组做粗粒度白名单,比如只放行办公网段访问SSH和管理面端口;到了操作系统内部再用firewalld做细粒度的临时封禁和放行。云安全组相当于小区门口的保安,防火墙规则相当于房间的门锁,两层职责不同,配合才是完整的访问控制方案。
5. 常见问题与排查技巧实录
5.1 封了没效果的五个原因
第一条排查顺序,这是最常见的坑。如果你用-A追加规则,而规则列表里恰好已有一条同源IP的ACCEPT在它之前,那么追加的DROP永远不会被匹配。用iptables -L -n --line-numbers查行号,把DROP规则插入到匹配规则之前即可。我习惯所有封禁规则都优先用-I,就是为了省这个心。
第二条要确认封的是不是IP协议栈的另一面。很多服务器对外有IPv4地址,但监控、内网管理也可能走IPv6。你用iptables只封了IPv4,攻击者用IPv6地址一样能进来。解决方法是同步处理ip6tables,或者在支持nftables的新系统上用inet族的表一网打尽。封禁前先检查服务器的IPv6启用情况和流量情况,这个步骤很多人会漏。
第三条是Docker等容器环境。Docker会自动向iptables的DOCKER链和FORWARD链注入规则,而且容器端口映射依赖DNAT规则。只写INPUT链的规则往往拦不住到容器服务的流量,因为流量已经在PREROUTING阶段被做了目的地址转换,送达阶段根本不属于INPUT链的管辖范围。遇到容器场景,需要操作DOCKER-USER链,或者直接用Docker的发布端口控制、配一个前端Nginx做访问控制,这些方式都比硬写iptables有效。
第四条是服务自身先拒绝了连接。有些服务有独立的ACL机制,比如MySQL的user表host字段、SSH的Match Address、Nginx的allow/deny指令。如果防火墙规则已放行但连接还是失败,先查服务日志,很可能请求在应用层被拦了,防火墙规则看起来“没生效”其实是被冤枉的。
第五条是规则被覆盖或清空。前面讲过的firewalld reload覆盖手工iptables规则就是典型。排查时观察规则是否还在:iptables -L -n -v看一下规则计数器的Packet列,如果长期为0,说明这条规则从未匹配到任何包,优先怀疑规则顺序或链不对。这是我每次排障必看的数据,比猜来猜去快得多。
5.2 误封自己被锁在门外的急救
这个话题很痛,但不得不讲。远程改防火墙规则,最怕的就是把自己锁在门外。我见过有人在办公室远程操作生产服务器,一条默认策略DROP命令发出去,人立刻就慌了——机器还在,但所有SSH连接都断了。这种情况下,真正的急救路径有这么几条,按便利程度排序:第一,如果服务器有带外管理(如IPMI、iDRAC、云平台VNC),通过它登录进去把规则改回来,这是最可靠的方式;第二,如果是云主机,云厂商控制台一般有网页终端,相当于带外管理;第三,如果都没有,只能物理接触机器了。所以平时就要把这些路径摸清楚,别等出事再找。
更实用的是提前预防。我常年养成的习惯是,任何添加封禁规则的操作之后,立刻用定时任务做自动回滚:
# 5分钟后自动删除该封禁规则,作为误封保险 echo "iptables -D INPUT -s 203.0.113.8 -j DROP" | at now + 5 minutes如果规则确实要长期保留,5分钟内再手动取消这个定时任务或者用cron做一个更复杂的确认脚本就行。用at命令来回滚,是我推荐给所有新人的“保命招”,成本极低,效果极大。另外建议每次批量变更前先iptables-save备份,把备份文件命名带上日期,出问题随时iptables-restore还原。有同行还习惯用screen或tmux开着会话,里面保持一个root shell,这样SSH断开后规则还能通过残留会话操作,也是一个思路。
5.3 排查工具箱:连不通和没拦住分别怎么查
判断“某个IP到底能不能访问某个端口”,我不建议一上来就上防火墙命令,先做网络连通性判断更清晰。在客户端机器上,可以用nc或telnet测试端口通不通:nc -zv 目标IP 22这种写法很直观,返回success说明网络层没问题;telnet目标IP 22连上再断开则说明TCP握手成功。ping可以测基础连通性但很容易被防火墙策略干扰,所以ping通不等于端口通、ping不通也不代表服务有故障。
如果确认端口连不通,回到服务器上,按这个顺序排查:先iptables -L -n -v看规则顺序和计数器,再把默认策略、相关链的规则都过一遍;然后iptables-save看完整规则集,检查有没有冲突;接着看服务是否监听在正确端口,ss -lntp输出里主动监听列表会告诉你一切;最后看服务日志,很多被应用层拒绝的连接在日志里有明确记录。
反过来,如果“没拦住”,比如明明加了DROP规则但IP还能访问,优先查三件事:规则是否在正确链上(INPUT还是FORWARD)、是否存在更靠前的ACCEPT规则、IPv6规则是否漏了。另外还有一种隐蔽情况是IP冲突,局域网内别的机器占用了目标IP,从你的服务器看流量行为就会异常混乱。排查IP冲突可以arping目标IP看是否有多个MAC响应,这是网络环境里很容易被忽略的因素。
5.4 易错点速查表
把我在实际项目中碰到的高频问题整理成一张表,贴在服务器上或者收藏起来,写规则之前过一遍能少踩很多坑。
| 易错点 | 现象 | 正确做法 |
|---|---|---|
| 使用-A追加封禁规则 | 封禁不生效 | 封禁规则优先使用-I插入到顶部 |
| 只封IPv4不封IPv6 | 攻击者改用IPv6访问成功 | 同步配置ip6tables或使用inet族表 |
| 默认策略改为DROP后未放行现有连接 | SSH立刻断开 | 先加ESTABLISHED,RELATED放行规则,最后再改默认策略 |
| 在firewalld环境手工加iptables规则 | 重启或reload后规则丢失 | 使用firewalld的永久规则+reload管理 |
| 容器服务只改INPUT链 | Docker端口映射流量依旧放行 | 使用DOCKER-USER链或应用层访问控制 |
| 忘记放行lo回环接口 | 本机服务之间连接异常 | 添加-i lo -j ACCEPT规则 |
| 规则参数错误导致删除语句不匹配 | 删除规则失败或误删其他规则 | 复制添加命令,仅将-I改为-D |
最后分享一个我个人的体会:访问控制最终拼的不是命令记得多熟,而是对数据包路径的理解、对规则顺序的敬畏、以及一套稳定的操作习惯。每次变更之前做好备份和回滚预案,变更之后立刻验证连通性,这两步做到了,防火墙这块基本稳了。这套方法论我用了好几年,不管在自建机房还是云环境都没出过大岔子。