ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Shell脚本实战:用iptables打造端口放行与IP黑白名单防火墙

Shell脚本实战:用iptables打造端口放行与IP黑白名单防火墙 搞服务器运维这些年我最大的体会就是能用一行命令解决的问题千万别去装一堆花里胡哨的面板。最近正好在整理内部安全加固文档写到了第64章主题是用Shell开发一套简易防火墙脚本核心就是端口放行和IP黑白名单控制。很多人一听防火墙就想到硬件设备、商业软防火墙实际上在轻量业务、内网网关、临时测试环境这些场景里一个写得规矩的iptables脚本比什么都实用。这篇东西适合刚入门Shell、准备给服务器做基础安全加固的人也适合手里攒了一堆iptables命令想彻底脚本化的老手。我会把设计思路、关键代码、踩坑过程都摊开讲照着操作半小时内能跑通第一版。1. 为什么用Shell写防火墙脚本项目背景与设计思路1.1 这个脚本到底解决什么问题先说场景。我之前负责一批内网业务服务器既有Nginx、Redis、MySQL这类常见服务也有一些临时开的测试端口。团队十几个人每个人都用自己的方式开放防火墙有人直接关掉防火墙有人随手写几条iptables规则等出问题了一查规则乱成一团。我需要一套统一入口来管理“放行什么端口”“允许哪些IP访问”最好还能让新人半小时内看懂、改得动。这套脚本就围绕两个核心需求来设计端口放行指定TCP、UDP端口支持批量配置。IP黑白名单黑名单直接拒绝白名单直接放行且优先级可控。这两个需求听着简单落地时却不省心。比如黑名单和放行端口同时命中一个IP时谁先生效IPv6流量会不会绕过IPv4的DROP策略重启服务器之后规则还在不在这些细节都会在后面的章节里逐个说到。脚本本身不求功能全但求“行为可预期”。这也是我把这一章当作64章系列里一个重要里程碑的原因能用最朴素的Shell实现清晰可控的防火墙逻辑说明基础功底已经过关了。1.2 为什么选Shell加iptables而不是图形面板有人问为什么不装个图形化管理工具或者直接用云平台的安全组答案很简单不是所有机器都在云上也不是所有环境都有条件装额外的管理程序。图形面板看着方便但多数时候会引入自己的依赖、端口和代理逻辑反而扩大了攻击面。裸机、虚拟机、容器宿主机里iptables几乎是无处不在的基础设施它不依赖额外服务性能也足够扛住大部分中小业务。Shell的优势在于可审阅、可版本管理。整个防火墙行为就是几个文本文件加一个主脚本放到Git里就能跟踪每一次规则变更。出问题时直接把脚本和规则文件丢给同事不用远程连上去一个一个敲命令排查。再加上Shell脚本本身对系统管理员来说就是日常语言维护成本低得很。相比之下nftables语法更现代有些新版发行版也开始默认使用但从“最通用”角度来说iptables命令还是更容易迁移到旧机器和云主机的初始化脚本里。我在这套方案里用的是iptables命令但留有扩展nftables的空间后面会提。1.3 设计原则规则文件与脚本逻辑分离写这个脚本我给自己定了三条规矩。第一条脚本里不硬编码任何IP和端口。所有策略参数都放配置文件修改策略就是改配置不动脚本逻辑。第二条每个函数只做一件事命令执行失败要能定位到行号直接用set -e会把整个脚本搞得很脆我宁愿自己写检查函数。第三条必须支持安全回滚。防火墙脚本最大的风险就是把自己锁在门外没有回滚方案等于给生产环境埋雷。最终目录结构大概长这样/etc/fw-script/ ├── fw.sh # 主脚本 ├── conf/ │ ├── ports.conf # 放行端口配置 │ ├── whitelist.conf # 白名单IP │ └── blacklist.conf # 黑名单IP ├── backup/ # 执行前的规则备份 └── logs/ # 操作日志配置文件和脚本逻辑分开之后像“临时给某个IP放行”这种需求只需要往whitelist.conf里加一行再执行一次脚本。哪怕是新人来操作也不太容易把脚本改坏。2. 脚本整体结构与核心功能拆解2.1 主脚本的执行流程这套脚本按照“先检查环境再清理旧规则再加载新策略”的顺序执行。听起来简单但顺序选错很致命。比如一上来就清空所有iptables规则当前SSH连接立刻中断后续规则还没写入机器已经失联。所以我的流程是检查是否root权限检查iptables命令是否存在。加载配置文件解析端口和IP列表。将当前规则备份到backup目录。先设置回环接口和已建立连接放行保证现有连接不断。应用黑名单规则再应用白名单规则。应用端口放行规则最后设置默认策略。打印最终规则摘要方便确认。主脚本会用case语句接收命令行参数比如fw.sh start、fw.sh stop、fw.sh status、fw.sh --add-port 8080/tcp。这样运维同事可以直接用参数修改策略而不需要亲手改配置文件。参数解析里有个细节shift命令要放在每个case分支内部处理好否则参数会被吞掉。我第一次写时就踩过这个坑。2.2 配置文件的格式设计端口配置文件用“端口/协议”的格式每行一条规则#开头的是注释# ports.conf 22/tcp 80/tcp 443/tcp 3306/tcp 10050/tcp 10051/tcpIP黑白名单文件就一个IP或者一个网段一行同样支持注释# whitelist.conf 10.10.20.0/24 192.168.1.100配置文件越简单越好尽量不要在文件里写复杂语法。因为解析逻辑是用Shell的while read循环逐行读取的格式一旦花哨起来脚本要么增加依赖要么遇到各种转义问题。这个设计会让后续扩展变得非常舒服。比如想加一个“保护端口”直接在端口配置文件加一行脚本逻辑完全不用动。2.3 函数拆分与日志输出我把整个脚本拆成了load_config、backup_rules、apply_base、apply_blacklist、apply_whitelist、apply_ports、set_default_policy、show_summary这些函数。每个函数开头打印一行带时间戳的日志方便追踪执行过程。日志函数我会这样写log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* } log_error() { echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] $* 2 }这里的date命令格式很讲究%Y-%m-%d %H:%M:%S组合起来就是约数字串形式的时间戳。配合tee -a logs/fw.log写进日志文件出问题的时候能精确定位是哪一步执行失败。很多人写脚本时懒得打日志真到了故障现场就抓瞎这就是典型的省小钱亏大钱。3. 核心功能落地端口放行与黑白名单的代码实现3.1 环境检查与旧规则清理环境检查函数要处理一个很常见的坑假如脚本以普通用户身份执行了iptables命令根本用不了这就要直接报错退出。我的写法是这样check_env() { if [ $(id -u) -ne 0 ]; then log_error 请用root身份执行本脚本 exit 1 fi command -v iptables /dev/null 21 || { log_error 未找到iptables命令 exit 1 } }清理旧规则的时候我建议分步清理而不是直接执行iptables -F。默认INPUT链策略如果是ACCEPT-F只清规则不会断连但一旦之前有人设置过DROP策略-F也得先看清楚。我习惯先做备份再清理备份命令如下backup_rules() { local time_tag time_tag$(date %Y%m%d_%H%M%S) mkdir -p backup iptables-save backup/iptables_${time_tag}.rules log_info 规则备份到 backup/iptables_${time_tag}.rules }iptables-save成功与否直接决定了回滚是否可行所以这里一定要检查命令返回值。我见过有人把备份命令扔到脚本末尾结果规则早就被清空了备份了个寂寞。3.2 黑名单规则应该放在什么位置黑名单要做到“只要是黑名单里的IP即使端口在白名单里也进不来”所以黑名单规则必须插在端口放行规则之前。我自己用的函数是apply_blacklist() { local ip while read -r ip; do case $ip in |\#*) continue ;; esac iptables -A INPUT -s $ip -j DROP log_info 已封禁IP: $ip done conf/blacklist.conf }注意这里的case语句它的作用就是过滤空行和注释行。你没看错Shell里判断字符串是不是以#开头最简洁的方式就是case $ip in \#*)。这个写法比grep加if判断干净得多而且不依赖额外工具。黑名单支持填写网段比如1.2.3.0/24iptables会自动识别CIDR格式非常方便。白名单规则要加在黑名单后面但要在业务端口规则前面。白名单的本质是“源地址一旦命中就直接放行不再走后面的端口判断”所以它天然拥有最高优先级。如果光是放行IP不放行端口那白名单也进不来。3.3 端口放行的参数化解析端口配置的解析我用两层循环做外层循环读文件内层用:分割端口和协议。为什么不用awk因为文件格式太简单了Shell自己的字符串切割就够了少装一个工具脚本就更通用一点。代码如下apply_ports() { local line port proto while read -r line; do case $line in |\#*) continue ;; esac port${line%%:*} proto${line#*:} iptables -A INPUT -p $proto --dport $port -j ACCEPT log_info 已放行端口: $port/$proto done conf/ports.conf }${line%%:*}取冒号左边的内容${line#*:}取冒号右边的内容这两个是Shell里非常典型的参数扩展。如果端口配置写成了8080不带协议脚本会默认用tcp可以让用户在配置里用tcp或udp显式指定。实际上我后来还加了协议检查遇到既不是tcp也不是udp的值直接跳过并告警防止手滑把配置文件写坏。默认策略这里要单独说一句。很多教程喜欢在脚本结尾直接iptables -P INPUT DROP但这样做对服务器远程连接风险很大。我自己的做法是先放行SSH再设置DROP策略并且留一个带sleep的回滚窗口。这部分在下一个实操章节展开因为它是很多初学者的噩梦。4. 从零实操搭建脚本并安全上线4.1 手把手写第一版完整脚本我习惯先建目录再写文件。创建目录的命令很简单mkdir -p /etc/fw-script/{conf,backup,logs}顺序是先把主脚本fw.sh写好再把三个配置文件准备好第一次正式执行前先在测试机器上完整跑一遍。完整脚本大概长这样我精简过代码里的注释都对应当前这套目录结构#!/bin/bash BASE_DIR/etc/fw-script CONF_DIR${BASE_DIR}/conf BK_DIR${BASE_DIR}/backup LOG_FILE${BASE_DIR}/logs/fw.log log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* | tee -a $LOG_FILE } log_error() { echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] $* 2 | tee -a $LOG_FILE } load_config() { BLACK_LIST$(grep -v ^\s*# ${CONF_DIR}/blacklist.conf | grep -v ^\s*$) WHITE_LIST$(grep -v ^\s*# ${CONF_DIR}/whitelist.conf | grep -v ^\s*$) } backup_rules() { local time_tag time_tag$(date %Y%m%d_%H%M%S) iptables-save ${BK_DIR}/iptables_${time_tag}.rules [ $? -eq 0 ] log_info 备份完成: ${BK_DIR}/iptables_${time_tag}.rules || log_error 备份失败 } apply_base() { iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT log_info 回环接口与已建立连接放行 } apply_blacklist() { local ip for ip in $BLACK_LIST; do iptables -A INPUT -s $ip -j DROP log_info 黑名单封禁: $ip done } apply_whitelist() { local ip for ip in $WHITE_LIST; do iptables -A INPUT -s $ip -j ACCEPT log_info 白名单放行: $ip done } apply_ports() { while IFS: read -r port proto; do [ -z $port ] continue [ -z $proto ] prototcp iptables -A INPUT -p $proto --dport $port -j ACCEPT log_info 放行端口: $port/$proto done ${CONF_DIR}/ports.conf } set_default_policy() { iptables -P INPUT DROP iptables -P FORWARD DROP log_info 默认策略设置为DROP } show_summary() { iptables -L INPUT -n --line-numbers } main() { case $1 in start) check_env load_config backup_rules apply_base apply_blacklist apply_whitelist apply_ports set_default_policy show_summary ;; stop) iptables -P INPUT ACCEPT iptables -F log_info 防火墙已清空默认策略为ACCEPT ;; status) iptables -L INPUT -n --line-numbers ;; *) echo 用法: $0 {start|stop|status} exit 1 ;; esac } main $第一次执行前务必检查conf/ports.conf里有没有遗漏SSH端口。如果当前服务器远程SSH端口是22就确保第一行是22/tcp。第一次执行的命令建议这样chmod x /etc/fw-script/fw.sh /etc/fw-script/fw.sh start执行完立刻在另一个终端窗口测试新连接。很多人第一次跑就把自己锁在门外就是因为没有在“另一个终端”做好准备以为当前连接不断就万事大吉结果当前连接指的是新规则加载前已经建立的连接默认策略一变新连接全被拦了。4.2 手动放行与参数扩展前面脚本里只包含start、stop、status三个参数但实际运维过程中临时放行端口的需求非常多。我后来给脚本加了一个--add-port参数用到了热词里提到的shift命令。下面这段代码可以附加到main函数之前add_port() { local port proto port${1%%/*} proto${1##*/} [ -z $proto ] prototcp iptables -A INPUT -p $proto --dport $port -j ACCEPT echo ${port}/${proto} ${CONF_DIR}/ports.conf log_info 手动放行端口: ${port}/${proto} } while [ $# -gt 0 ]; do case $1 in --add-port) shift add_port $1 ;; *) ;; esac shift done这里shift命令的作用是让$1变成当前参数的下一个参数。比如执行fw.sh --add-port 8080/tcp时第一次$1是--add-portshift之后$1变成了8080/tcp这时正好是我们要的端口参数。如果不做shift就永远只能拿到--add-port这个字符串函数里解析出来的端口多半是空的。这个坑在我刚写参数解析时至少踩过三次。4.3 安全上线与回滚方案不管脚本写得多仔细防火墙脚本都怕一个致命问题把当前远程连接搞断。为此我强烈建议做“自动回滚”机制。思路很简单在设置默认DROP策略之前先用后台任务启动一个延时回滚guardian_timer() { local rollback_file$1 sleep 60 iptables-restore $rollback_file log_info 60秒内未收到确认已自动回滚 }主流程改成在备份完规则后立刻启动这个后台守护任务然后正常应用新规则。脚本运行结束后等待30秒如果管理员确认规则正常可以执行fw.sh confirm取消回滚。这个方案虽然土但非常实用。我在很多生产环境都用这一招尤其操作远程机房服务器时它救过我至少两次。自动回滚时间不要设太长我一般设60秒。太短的话人还没反应过来就滚回去了太长的话防火墙处于一个“可能回滚”的不确定状态对业务影响更大。写进生产环境前先在自己的虚拟机里把整个流程演练一遍再把回滚时间调到5分钟练熟了再上生产。5. 常见问题与排查技巧实录5.1 刚执行完脚本就被踢下线怎么办这是所有防火墙脚本最容易引发的事故我处理过很多次。原因几乎都是同一个默认策略设置成了DROP但脚本里没有放行当前SSH端口或者放行了却因为规则顺序不对没生效。比如有人把22/tcp写到了apply_ports的最后而那时iptables -P INPUT DROP已经执行完了当前连接虽然还在但新连接一个都进不来。遇到这种情况机房里有物理控制台或远程管理卡就直接进后台执行iptables -F iptables -P INPUT ACCEPT。如果没有物理控制台只能依靠提前配置的定时回滚任务。这也是为什么我反复强调任何防火墙变更都必须带定时回滚不能裸奔。监控告警里也应该加一条“SSH探活”比如每分钟从另一台机器探测一下22端口连续三次失败就自动执行回滚。5.2 配置文件中明明写了放行端口却还是不通出现这个问题先看规则顺序再看默认策略最后看是不是有其他规则抢先DROP了。用iptables -L INPUT -n --line-numbers查看规则列表确认目标端口规则确实在默认策略之前。如果是白名单方式还要确认客户端的源IP没有变。内网环境里NAT很容易让源IP变成网关地址导致白名单匹配不上。另一个容易被忽略的坑是“协议写错”。有些服务是TCP但配置文件里只写了端口没写协议脚本默认按tcp处理那没问题。但如果你写的是53/udp而服务实际在监听TCP的53端口那照样不通。排查顺序一定是服务监听状态、端口协议、防火墙规则、安全组或局域网设备一层层筛下来。5.3 IPv6绕过规则导致放行失效或漏封很多机器同时监听IPv4和IPv6如果只写了iptables规则IPv6流量走的是ip6tables完全不受IPv4 INPUT链影响。这就造成一个很典型的场景白名单只允许了10.10.20.100访问但由于IPv6没有配置任何限制攻击者改用IPv6地址照样能访问服务。解决办法有两种。要么在服务器上明确禁用IPv6要么给ip6tables写一套对应的规则。我的做法是如果业务不需要IPv6就在sysctl.conf里关闭如果确实需要IPv6那么脚本里必须同时执行ip6tables的清理和策略设置。不要让IPv6成为一个规则盲区这很重要。5.4 重启服务器后规则全部丢失这是iptables的传统艺能。默认情况下iptables规则只存在于内存重启后清空。很多不懂的人配置完防火墙重启了一下机器发现端口全开了以为自己脚本有问题其实只是规则没持久化。解决方式有几种。最传统的是执行iptables-save /etc/iptables/rules.v4然后用iptables-restore恢复。Debian系可以装iptables-persistentCentOS 6用service iptables saveCentOS 7之后更建议让脚本作为开机自启任务执行。我自己更喜欢把fw.sh做成systemd服务或者直接在rc.local里调用一次这样规则来自配置文件和脚本始终是可维护的状态。注意持久化文件也要纳入备份否则重启后恢复出来的是一份旧规则。5.5 Shell脚本本身的几个细节坑写这套脚本时我顺手整理了几个Shell语法上值得注意的点新手十有八九会碰到。第一个是for循环里读文件的问题。如果直接用for ip in $(cat whitelist.conf)遇到注释行、空行、通配符、空格都会出问题。更稳妥的是while read逐行读取配合case过滤注释。第二个是set -e和管道组合的坑。比如iptables-save backup || log_error比set -e更可控因为set -e遇到命令返回非零会直接退出干扰整体执行流程。第三个是日期转数字串的小技巧$(date %Y%m%d_%H%M%S)在生成备份文件名时很好用但日期字符串里不能有空格否则文件名会被拆成两个。第四个是shift命令在参数解析里的位置前面已经讲过不赘述。这些坑看起来都特别小但在线上环境里小坑才最致命。我见过有人因为一个没引号的变量把整个网络策略弄崩的。Shell脚本里能加引号的地方尽量加引号能用read循环就别用for in cat这是两条保命原则。6. 几个我自己用下来很顺手的小习惯写到最后分享几个实操经验。第一所有配置文件和脚本都要纳入Git管理每次变更都提交一次注释里写明是谁、什么时候、因为什么改的。出问题的时候看一眼提交记录就知道是哪个IP或端口引入的排查速度快很多。第二规则文件里统一用“最小权限”原则默认不放行任何未明确指定的端口。哪怕团队里有人喊“临时开放下”也要走配置文件和Git提交的流程这也是拒绝乱改防火墙的最好理由。第三每次执行完脚本顺手保存一份iptables-save输出到logs目录形成变更前后对比哪怕是脚本自动执行的也留个痕迹。这套方案不是银弹它解决的是中小规模、规则变化不频繁的服务器场景。如果你的业务天天都在变端口或者有大量自动扩缩容的节点那应该考虑更自动化的安全组方案。不过对于大部分团队来说一个结构清晰、带黑白名单和端口管理能力的Shell防火墙脚本已经能承担起基础安全防护的重任。把它写明白、维护好比装十个半吊子安全工具都有用。
返回列表