如何用udev规则给工业Linux系统加一道“USB防火墙”?
你有没有遇到过这样的场景:一台部署在工厂车间的工控机,平时跑得好好的,结果某天突然宕机、数据异常,排查半天发现是有人插了个U盘拷走了生产日志?更糟的是,那个U盘根本不是公司配发的——它是个“陌生人”,没人知道从哪儿来。
在工业自动化、边缘计算和嵌入式控制领域,这类问题比想象中更常见。USB接口本是为了方便维护与调试而存在,但恰恰也成了安全链条上最脆弱的一环。恶意设备(比如BadUSB)、未经认证的存储介质、甚至是开发人员遗留的串口转接工具,都可能成为系统失稳甚至被入侵的突破口。
那么,能不能在设备刚一插入的瞬间,就判断它是“朋友”还是“敌人”,并立即做出响应?答案是肯定的——我们不需要复杂的EDR或硬件加密模块,只需利用Linux系统自带的一个机制:udev。
为什么选udev?因为它够“近”
要理解udev的价值,得先搞清楚一件事:大多数安全策略都是“事后补救”。比如:
- 防火墙拦IP包 → 数据已经进来了;
- 杀毒软件扫描文件 → 文件已经被挂载读取了;
- SELinux限制进程权限 → 进程已经开始运行了。
但udev不一样。它是内核和用户空间之间的桥梁,在设备物理接入的第一时间就能介入处理。换句话说,当一个U盘刚插进去、还没来得及注册驱动的时候,udev就已经看到了它的“身份证”——厂商ID、产品型号、设备类别……这些信息统称为“设备描述”。
只要我们在这一层设好规则,就可以做到:“你不在我名单里?抱歉,连门都不让你进。”
udev不是脚本引擎,而是设备守门人
很多人以为udev只是用来自动挂载U盘或者创建符号链接的小工具。其实不然。systemd-udevd是现代Linux系统中真正意义上的热插拔事件处理器,它的职责远不止命名设备那么简单。
当你把一个USB设备插进主机时,整个流程快得几乎察觉不到:
- 内核检测到新设备,生成
/sys/bus/usb/devices/1-2这样的路径; - 把设备属性通过netlink通知给udev;
- udev开始遍历
/etc/udev/rules.d/下的所有规则文件; - 一旦匹配成功,立刻执行动作:授权、拒绝、打标签、写日志、调外部程序……
全程毫秒级完成,而且发生在任何文件系统操作之前。
这意味着什么?意味着你可以在这个阶段直接对设备说“不”。不是等它挂载后杀掉进程,也不是等它写入文件后再报警——而是从根子上切断它的初始化流程。
设备是怎么被识别的?靠的就是“设备描述”
每个USB设备都有独一无二的“指纹”,主要包括以下几个关键字段:
| 字段 | 示例值 | 含义 |
|---|---|---|
idVendor | 1a86 | 厂商ID,全球唯一分配 |
idProduct | 7523 | 产品ID,由厂商自定义 |
bDeviceClass | 08 | 设备大类,08表示大容量存储 |
iManufacturer | "WinChipHead" | 制造商字符串 |
iProduct | "USB2.0-Serial" | 产品名称 |
这些信息都可以通过命令查看:
lsusb -v或者深入 sysfs 目录:
cat /sys/bus/usb/devices/1-2/idVendor更重要的是,udev 规则可以直接引用这些属性进行匹配。例如:
ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523"这行规则就能精准锁定某款CH340芯片的USB转串设备。
实战:三步构建你的USB白名单防火墙
真正的防御不是“看到坏人再抓”,而是“只放行已知的好人”。这就是所谓的“白名单 + 默认拒绝”模型。下面我们就一步步实现这个机制。
第一步:收集所有合法设备的“身份证”
在正式启用屏蔽规则前,必须先把当前环境中所有正常使用的USB设备登记一遍。可以用这条命令快速列出:
lsusb | grep -i usb然后针对每一个设备,提取其详细属性:
udevadm info -a -p $(udevadm info -q path -n /dev/sda) | grep -E "idVendor|idProduct|bDeviceClass"记录下每台设备的 VID/PID 组合,并确认用途是否合规。比如:
| 用途 | 厂商 | VID | PID |
|---|---|---|---|
| 加密狗 | SafeNet | 0529 | 0001 |
| U盘 | SanDisk | 0781 | 5567 |
| 调试串口 | FTDI | 0403 | 6001 |
把这些整理成文档,作为后续规则编写的依据。
第二步:允许已知设备通行
创建一个高优先级规则文件,专门用于放行白名单设备。注意编号要小,确保最先加载。
# 文件: /etc/udev/rules.d/10-allowed-usb.rules ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0781", ATTRS{idProduct}=="5567", GOTO="allowed_end" ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0529", ATTRS{idProduct}=="0001", GOTO="allowed_end" ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GOTO="allowed_end" # 如果以上都没匹配,则跳过放行段落 GOTO="block_unknown" LABEL="allowed_end" # 可选:为放行设备添加标记便于监控 TAG+="systemd", ENV{LOG_LEVEL}="info"这里用了GOTO和LABEL控制流程跳转,避免冗余执行。
第三步:兜底拦截所有“未知”设备
接下来是最关键的部分——默认拒绝策略。我们要在最后设置一条“铁闸”,拦住所有未被列入白名单的可疑设备。
# 文件: /etc/udev/rules.d/99-block-unknown-usb.rules LABEL="block_unknown" # 情况1:已识别为usb-storage但缺乏明确标识(可能是伪装设备) ACTION=="add", SUBSYSTEM=="scsi", KERNEL=="sd[a-z]", \ ENV{ID_USB_DRIVER}=="usb-storage", \ ENV{ID_VENDOR}=="?*", ENV{ID_MODEL}=="?*", \ RUN+="/bin/logger 'ALERT: Suspicious USB storage device detected (no vendor/model)'", \ RUN+="/bin/sh -c 'echo 0 > /sys$DEVPATH/authorized'", \ GOTO="block_finish" # 情况2:设备类型为大容量存储(bDeviceClass==08),但不在白名单中 ACTION=="add", SUBSYSTEM=="usb", ATTRS{bDeviceClass}=="08", \ ATTRS{idVendor}!="0781", ATTRS{idProduct}!="5567", \ ATTRS{idVendor}!="0529", ATTRS{idProduct}!="0001", \ RUN+="/bin/logger 'BLOCKED: Unknown USB mass storage device [%s{idVendor}:%s{idProduct}]'", \ RUN+="/bin/sh -c 'echo 0 > /sys$DEVPATH/authorized'" LABEL="block_finish"其中最关键的指令是这一句:
echo 0 > /sys$DEVPATH/authorized这是 Linux 提供的原生接口,用于动态禁用某个 USB 设备。写入0后,系统会立即停止对该设备的枚举过程,相当于“断电拔线”。设备端表现为反复连接失败,无法被操作系统识别。
同时配合logger记录事件,可用于后续审计或联动告警系统。
更进一步:让规则变得更智能
如果你的现场设备种类多、更换频繁,静态规则维护起来会很麻烦。这时候可以引入外部逻辑判断。
动态查询数据库决定是否放行
ACTION=="add", SUBSYSTEM=="usb", \ PROGRAM="/usr/local/bin/check_usb_whitelist %s{idVendor} %s{idProduct}", \ RESULT!="allow", \ RUN+="/bin/sh -c 'echo 0 > /sys$DEVPATH/authorized'"这里的PROGRAM会调用一个自定义脚本,传入VID和PID作为参数。脚本可以从 SQLite、Redis 或配置文件中查询该设备是否在册,返回allow或其他字符串。
示例脚本/usr/local/bin/check_usb_whitelist:
#!/bin/bash VID="$1" PID="$2" # 查询白名单数据库 if sqlite3 /etc/usb_whitelist.db "SELECT 1 FROM devices WHERE vid='$VID' AND pid='$PID';" | grep -q 1; then echo "allow" exit 0 else logger "USB device $VID:$PID not in whitelist" exit 1 fi这样就能实现集中化管理,适合大型工厂或多站点部署。
部署时必须注意的五个坑
再好的方案,落地时也可能踩雷。以下是工业现场常见的几个陷阱及应对建议:
1. 别误杀了自己的设备!
上线前务必全面扫描现有设备。特别是那些“隐形”的USB设备——比如某些工控板载的CAN卡、GPS模块、甚至TPM芯片,它们也可能走USB总线。
建议做法:
在测试环境中模拟真实连接状态,逐个验证规则行为。
2. 留一条“紧急逃生通道”
万一哪天工程师在现场需要临时调试,却发现所有U盘都被封了,怎么办?
可以在规则中加入一个“开关条件”:
KERNEL=="sd[a-z]", IFACE{eth0}/arp_filter=="1", GOTO="block_unknown"意思是:只有当网络接口启用了ARP过滤(代表处于生产模式)时才启用拦截。否则跳过。
或者更简单的办法:检测某个隐藏文件是否存在:
TEST!="/tmp/usb_debug_mode", GOTO="block_unknown"运维人员只需创建/tmp/usb_debug_mode文件即可临时放开限制。
3. 日志别丢了
一定要确保日志能留存。否则出了事都不知道谁动过设备。
推荐配置 rsyslog 将相关消息转发到远程日志服务器:
kern.* @logs.example.com:514并在规则中加入详细的上下文信息:
RUN+="/bin/logger 'Blocked USB: VID=%s{idVendor}, PID=%s{idProduct}, Port=%p'"4. 不同发行版略有差异
虽然udev标准基本统一,但在 Yocto、Buildroot、Debian、CentOS 等系统上仍有些许差别:
- 某些嵌入式系统默认不启用
authorized接口; ENV{}变量名可能不同(如旧版本用ID_VENDOR_ENC);RUN+=执行超时时间较短,复杂脚本需异步处理。
建议:在目标平台上充分测试,使用udevadm test模拟事件流程。
5. 别忘了HID类攻击设备
除了U盘,还有更危险的:伪装成键盘的HID设备(如Rubber Ducky)。它们不走存储路径,所以不会触发sd[a-z]规则。
应对方法是单独过滤HID类设备:
ACTION=="add", SUBSYSTEM=="usb", ATTRS{bDeviceClass}=="00", \ ATTRS{bInterfaceClass}=="03", \ ATTRS{idVendor}!="045e", ATTRS{idProduct}!="07a2", \ RUN+="/bin/sh -c 'echo 0 > /sys$DEVPATH/authorized'", \ RUN+="/bin/logger 'Blocked potential HID attack device: %s{idVendor}:%s{idProduct}'"它真的有效吗?看看实际效果
部署完成后,你可以做几个简单测试:
- 插入一个不在白名单中的U盘 → 系统无反应,
dmesg显示设备被拒绝; - 查看
/var/log/syslog→ 出现类似日志:Jul 10 14:22:33 gateway kernel: usb 1-2: authorized to connect Jul 10 14:22:33 gateway logger: BLOCKED: Unknown USB mass storage device [1a86:7523] - 使用
lsblk→ 没有新增块设备; - Windows主机显示“设备无法识别”或“供电不足”。
整个过程耗时通常小于200ms,用户体验接近物理禁用,但灵活性更高。
结语:轻量级防护也能扛起工业安全大旗
这套基于udev的USB访问控制方案,没有依赖任何第三方软件,也不需要额外硬件投入,完全依托Linux内核原生能力实现。它像一道“隐形防火墙”,默默守护着每一台无人值守的工控设备。
更重要的是,这种方法体现了一种正确的安全思维:越靠近威胁源头的防御,效率越高;越早中断攻击链的动作,代价越小。
未来,我们还可以将这一机制与其他技术结合:
- 联动TPM模块,验证设备数字签名;
- 使用机器学习分析设备行为模式,自动学习“正常”设备特征;
- 结合容器隔离,为不同用途的USB设备划分执行域。
但对于今天绝大多数工业场景来说,一条精心设计的udev规则,已经足够把你从“担心谁又插了U盘”的焦虑中解放出来。
如果你正在负责某个嵌入式系统的安全加固工作,不妨现在就打开终端,试试写下第一条规则:
echo 'ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="ffff", RUN+="/bin/sh -c \"echo 0 > /sys\$DEVPATH/authorized\""' \ > /etc/udev/rules.d/99-deny-test.rules然后插一个测试设备,看看它是不是“啪”一下就被踢出去了?
欢迎在评论区分享你的实战经验。