做嵌入式Linux这些年,我最大的感受是:大多数设备在出厂时几乎没有任何安全防线。功能跑通、能联网、Web后台能打开,就算交付了。直到产品被甲方扫描出漏洞,或者被拉进僵尸网络、固件被逆向提取,才想起来做安全加固。第17讲正好是把这件事讲透了:想要从系统层面真正把设备“焊死”,不是装个杀毒软件的事,而是要把最小化裁剪、权限硬化、日志审计、轻量防火墙四件事一起做好。这篇文章我会按实战思路把四块内容拆开讲清楚,再把第16讲的课后思考题完整解析补上,适合正在做产品化嵌入式Linux开发的工程师,也适合想从应用开发往底层系统方向转的同学。
1. 为什么要做系统级安全加固:先懂威胁再动手
嵌入式设备的安全问题和云服务器完全不一样,很多通用的加固方案直接照搬过来反而会“水土不服”。想做好加固,第一步不是敲命令,而是先搞明白你的设备到底面临什么威胁、哪些环节最容易被打穿。
1.1 嵌入式设备的安全现状与常见攻击路径
嵌入式设备有几个鲜明的特征:常年无人值守、算力和存储有限、固件更新困难、出厂后往往很难打补丁。这些特征决定了它和高性能服务器面对的安全模型不同。服务器被攻破还能及时人工介入,设备被攻破往往要等几个月才能发现,甚至永远发现不了。
从攻击路径看,嵌入式设备被入侵的方式其实比较固定。最典型的几个入口:
- 开放的管理端口,比如telnet、SSH用了默认口令或者弱口令,这是最容易被扫描器命中的。
- Web管理后台的漏洞,很多设备内置了一个轻量级HTTP服务,逻辑简单但缺少输入校验,容易被命令注入或越权访问。
- 调试接口暴露,比如串口UART控制台没有登录保护,或者JTAG调试口没被禁用。
- 固件被提取后逆向分析,攻击者拿到rootfs就能翻出密钥、硬编码账号和隐藏的后门逻辑。
- 网络服务进程自身的安全漏洞,比如老版本dropbear、lighttpd、busybox httpd的已知漏洞。
我见过最典型的案例是一个智能设备,出厂固件里保留了telnetd,密码固定且无法修改,扫描工具几秒钟就能进去,然后直接被拉进僵尸网络发起DDoS流量。事后复盘发现,就是rootfs打包时少做了一次清理,把开发阶段的调试服务留在了量产固件里。这种事情不是个例。
1.2 加固思路:从攻击面收敛到纵深防御
既然清楚了攻击路径,加固思路就明确了。我习惯把安全加固拆成四个层级,这也是第17讲的核心框架:
- 最小化裁剪:把不需要的组件、服务、驱动全部移除,从源头上缩小攻击面。
- 权限硬化:即使某个进程被攻破,也要让攻击者拿不到系统最高权限,限制横向移动。
- 日志审计:让每次异常行为都留下痕迹,出事后可回溯,同时能用于入侵检测的输入。
- 轻量防火墙:在网络边界做流量过滤,把不必要的访问直接挡在门外。
这四个层级不是孤立的,而是递进关系。可以类比成一间房子:门越少越好(最小化裁剪),门锁越结实越好(权限硬化),门口要装监控(日志审计),院子外还要有围墙和门禁(防火墙)。只有四者同时起作用,才叫纵深防御,缺一环都会有明显短板。
2. 最小化裁剪:缩小攻击面的第一步
最小化裁剪是系统级安全加固里收益最高、但也最容易翻车的一步。它的核心思想不复杂:凡是业务不需要的东西,一律不出现在最终固件里。攻击者打不进来的前提,是根本没有“门”可以打。
2.1 内核裁剪:关掉不用的模块和功能
内核是设备最底层、权限最高的软件,也是攻击者最喜欢的目标。内核算力大、攻击面自然也大,所以内核裁剪在嵌入式Linux安全加固里是优先级最高的任务。
实际操作时,我会先拿到官方或板级厂商的defconfig,跑通一次完整构建,然后在make menuconfig里逐项排查。重点关注这几个方向:
- 文件系统支持:如果产品用jffs2或ubifs,那ext4、vfat这类模块完全可以去掉。
- 网络协议:不需要IPv6就关掉CONFIG_IPV6;不需要蓝牙、无线协议就移除对应协议栈;如果设备只有有线以太网,无线驱动和安全协议一个都不要留。
- 驱动领域:USB storage驱动如果永远不会用到,关掉;GPU、声卡、摄像头驱动同理,按产品BOM逐一核对。
- 调试与动态加载:CONFIG_KALLSYMS会导出内核符号地址,攻击者可能利用它辅助提权,量产版建议关闭。CONFIG_DEBUG_FS、CONFIG_KPROBES、CONFIG_BPF_SYSCALL等调试和执行特性,如果不做动态追踪分析,关闭。CONFIG_MODULE_SIG签名校验打开,配合模块签名,防止内核被注入未授权模块。
- 安全特性开启:CONFIG_RANDOMIZE_BASE开启内核地址随机化,CONFIG_STACKPROTECTOR开启栈保护,CONFIG_HARDENED_USERCOPY开启用户拷贝加固。
一个常见误区是,觉得关掉这些功能会影响性能或稳定性。实际只要产品功能测试覆盖到,关闭这些特性通常不会产生业务影响,反而因为内存占用下降、启动速度提升,体验更好。我这边一个网关项目,裁剪内核后镜像从4MB降到约1.6MB,启动时间缩短了差不多30%。代价是前期排查配置项花了半天时间,但后续收益非常可观。
2.2 文件系统裁剪:去掉用不到的组件和工具
内核裁剪解决的是底层攻击面,而用户态rootfs里的工具越多,风险越高。很多固件压缩包解压出来有几十个可执行文件,里面一半以上和业务无关,纯粹是开发调试时留下的。
用Buildroot构建时,我会在make menuconfig里做一次“反向清理”:
- Target packages里把tcpdump、gdb、strace这类调试工具去掉。注意,不是所有调试工具都不能留,如果产品现场需要定位网络问题,tcpdump可以留在内测版,量产版一定要去掉。
- BusyBox里按applet逐个审查。telnetd、ftpget、ftpd这些远程访问类applet,没有业务依赖就直接关掉。HTTP服务如果不需要,也去掉。
- 不要把交叉编译工具链打进rootfs,更不要把GCC编译器放进去。这类文件极大拉伸了攻击者逆向和编译恶意代码的成本,能不放坚决不放。
- 删除文档和示例文件,/usr/share/doc、/usr/share/man这些对运行没有任何意义,留着只会提供信息泄露的线索。
- 检查rootfs里是否残留内核符号表或System.map,这个文件同样属于敏感信息。
我个人习惯是在rootfs打包前加一个清理脚本,比如Buildroot的post-build脚本,把历史遗留文件、日志、临时文件、用户目录、shell history统统清掉。这样每次构建的产物都是干净的,不用手动清理。
2.3 rootfs中的残留风险与清理要点
文件系统裁剪常常会漏掉一些“看不见”的风险点,这些点比多余的二进制工具更隐蔽:
- /etc/shadow里的密码哈希:开发阶段的测试密码可能留在里面,一定要在打包前重置或删除不需要的用户。
- SSH主机密钥:如果固件里带了固定的dropbear host key,所有同型号设备的通信都可以被解密。量产固件应该去掉主机密钥,让每个设备首次启动自动生成。
- /home目录下的.bash_history:里面可能有用户敲过的命令,包含路径、IP、账号信息。
- /tmp目录没有限制:任何用户都可以写文件,可能导致符号链接攻击或恶意文件执行。挂载时建议带noexec、nosuid、nodev选项。
- 系统日志残留:开发阶段的日志可能包含堆栈、内存地址、业务数据,打包前清空/var/log。
一个很实用的经验:裁剪工作做完后,把整个rootfs挂载到qemu或目标设备上,用find命令扫一遍所有setuid/setgid文件、所有可执行文件列表、所有监听端口,逐项确认是否和业务白名单一致。运气好能多揪出几个被忽略的后门或调试服务。
3. 权限硬化:让每个进程都“够用就好”
最小化裁剪解决的是暴露面问题,权限硬化解决的是“打进去之后能做什么”的问题。很多嵌入式系统一启动就直奔root shell,所有进程都拥有最高权限,这对攻击者来说等于直接送了一座城。权限硬化的核心就一句话:让每个用户、每个进程、每个文件,只拥有完成工作所必需的最小权限。
3.1 用户与权限规划:ROOT权限的下放与隔离
嵌入式Linux里最常见的权限问题是“一root通吃”。业务进程用root跑,Web服务用root跑,日志服务用root跑,任何被攻破的漏洞都会立刻导致系统完全沦陷。
我一般会这样做用户规划:
- 首先禁用root直接登录。SSH服务如果用dropbear,设置PermitRootLogin no,日常维护使用一个普通用户,需要管理员操作时再通过sudo切换到root。设备上没有图形界面,维护人员使用普通用户加sudo足够了。
- 其次创建独立业务账户。比如一个设备有采集服务和Web服务,分别创建acq和web用户,它们只对自己需要访问的文件和目录有权限。即使Web服务被攻破,攻击者拿到的也是web用户权限,无法直接读取采集数据或篡改配置。
- 使用systemd的User=和Group=指令把服务运行身份降到非特权用户。systemd还有PrivateTmp、ProtectSystem、NoNewPrivileges、RestrictAddressFamilies这些参数,可以再压缩服务进程的系统调用面。
- sudo授权要做细粒度控制。不要给主维护用户一个sudo ALL权限,尽量用sudoers文件精确到命令,比如只允许重启服务、查看状态、调整日志级别。
有一个容易被忽视的点:创建业务用户时,shell字段要谨慎设置。如果业务进程的守护用户设置了/bin/sh,一旦进程被注入shell命令,攻击者就获得了一个可交互Shell。建议将业务账户的shell设为/sbin/nologin或/bin/false,尤其对于纯守护进程使用的系统账户。
3.2 文件系统权限与挂载选项加固
文件系统层面的权限,是权限硬化中最容易被测试出来、也最容易被忽略的部分。很多工程师只改了配置文件权限,却忘了挂载选项,导致攻击者可以在/tmp里放一个可执行文件,然后通过某个开启了写权限的Web服务去执行它。
在做文件系统挂载时,我通常会在/etc/fstab或构建脚本里对分区做明确的挂载属性设置:
- /tmp挂载为nodev,nosuid,noexec,因为正常业务不会在/tmp里执行二进制文件。
- /var/tmp同样处理,防止攻击者利用临时目录写入恶意脚本再执行。
- 只读分区要果断用ro挂载。比如rootfs本身如果设计为只读挂载,攻击者就算改了文件也无法持久化。
- 可写分区例如数据分区,单独挂到/data这样的目录,不要和系统文件混在一起。
- 关键配置文件权限收紧。/etc/passwd、/etc/shadow、/etc/inittab、/etc/sudoers原则上只允许root用户写,其他同类用户组只读。
此外,setuid/setgid程序是整个文件系统权限里最需要注意的雷区。默认的busybox里很多applet可能带setuid,但绝大多数业务根本不需要。我建议在构建完成后扫描一遍系统里所有setuid文件,然后逐个确认,不需要的全部清除setuid位。一个攻击者如果能找到有问题的setuid程序,往往就能用它完成权限提升。
3.3 内核参数与系统服务加固
内核参数也是权限硬化的一部分,虽然很多工程师容易把它归到“性能调优”里,但实际上net.ipv4系列和kernel系列参数和安全直接相关。我常用的内核加固参数如下:
# 临时生效 sysctl -w kernel.randomize_va_space=2 sysctl -w kernel.kptr_restrict=1 sysctl -w kernel.dmesg_restrict=1 sysctl -w net.ipv4.conf.all.accept_redirects=0 sysctl -w net.ipv4.conf.all.send_redirects=0 sysctl -w net.ipv4.tcp_syncookies=1这些参数的作用分别说明一下:kernel.randomize_va_space=2开启完整的地址空间随机化,降低缓冲区溢出攻击成功率;kernel.kptr_restrict=1限制非特权用户查看内核符号地址,让内核泄露不那么容易变成提权跳板;kernel.dmesg_restrict=1禁止普通用户读取内核日志,避免中间层信息泄露;net.ipv4.tcp_syncookies=1可以缓解SYN洪水。要注意这些参数需要写入/etc/sysctl.conf或启动脚本,保证重启后依然生效。
服务层面对应要做的,是把所有不需要的监听服务关掉。netstat -tlnp扫一遍,除了业务需要的端口,其他端口一律不留。同时检查有没有老旧的rsh、telnet、NFS等不安全服务,这类服务在嵌入式产品里早该退出历史舞台。如果用systemd,用systemctl list-unit-files把所有开机自启单元过一遍,逐一确认。
4. 日志审计:让每一次异常行为留痕
很多Linux开发者在做安全加固时,最不重视的就是日志审计。通信、配置、防火墙都调好了,觉得万事大吉;等到设备出问题需要排查时,才发现日志是空的、轮转没做好、远程日志没配,只能对着一块裸板怀疑人生。日志审计在安全体系里的作用不是阻止攻击,而是让攻击“无处遁形”,同时为事后回溯提供证据链。
4.1 日志系统的选型与配置
嵌入式设备资源有限,日志方案不能照搬服务器上的ELK那套,需要在轻量和可扩展之间做取舍。我见过三种主流的做法:
- 使用BusyBox自带的syslogd:最简单,适合日志需求极低的场景,支持本地文件和简单的远程转发,但过滤功能弱,格式可定制性差。
- 使用rsyslog:模块化设计,支持多种输入输出模块,过滤规则灵活;缺点是稍微占内存,对几十MB内存级别的设备可能有点吃力。
- 使用syslog-ng:配置语法清晰,流量控制比rsyslog更好,很适合资源受限但对日志转发稳定性要求高的设备。
我这边一个典型的采集网关用的是syslog-ng,因为它可以做基于程序名、优先级、内容关键字的复杂过滤,还能在断网时用磁盘队列缓存日志。一个参考配置如下:
source s_sys { system(); internal(); }; filter f_auth { facility(auth, authpriv); }; filter f_kernel { facility(kern); }; destination d_auth { file("/var/log/auth.log"); }; destination d_kernel { file("/var/log/kern.log"); }; log { source(s_sys); filter(f_auth); destination(d_auth); }; log { source(s_sys); filter(f_kernel); destination(d_kernel); };需要注意,单独的/var/log分区最好挂成可写,但不能是tmpfs,否则重启日志全丢。建议把日志放到独立的数据分区或专用的log分区,并且挂载时带上noexec选项,防止攻击者往日志目录里放恶意脚本后尝试执行。
4.2 日志轮转与远程日志同步
嵌入式设备的flash寿命有限,日志如果无限增长,除了撑爆存储,还会加速flash磨损。所以日志轮转是刚需,不是可选。即使只用busybox syslogd,也要配好logrotate或者脚本化轮转策略。
用logrotate时,一个典型的策略是“按大小轮转+固定保留份数”:
/var/log/*.log { size 512K rotate 4 compress delaycompress copytruncate notifempty }这里的copytruncate对写日志的进程比较友好,不需要重启服务;delaycompress是为了给正在写入的日志留一份未压缩的最近文件,调试时读取方便。
远程日志方面,最稳妥的方案是让设备把关键日志实时转发到集中的日志服务器。syslog-ng的转发简单直接:
destination d_remote { udp("192.168.1.100" port(514)); }; log { source(s_sys); filter(f_auth); destination(d_remote); };远程日志可以防止攻击者入侵设备后“清场”——即使他把本地日志文件删除,远端服务器还是保留着入侵痕迹。但要注意,转发通道如果不加密,核心业务数据可能泄露。如果条件允许,优先使用TLS方式转发;如果不允许,至少要过滤掉业务敏感数据,只转发安全事件类日志。
4.3 关键操作审计:从登录到提权
日志审计的一个更高要求,是能追踪到每一次登录、每次sudo提权和每次关键文件操作。单纯靠syslog的记录粒度往往不够细,很多远程登录行为只记录了IP和用户名,缺少命令序列和文件访问记录。
如果有条件启用内核的audit子系统,效果会好很多。加载auditd之后,可以用auditctl添加规则:
# 监听密码文件访问 auditctl -w /etc/passwd -p wa -k passwd_watch auditctl -w /etc/shadow -p wa -k shadow_watch # 监听sudo和su auditctl -w /usr/bin/sudo -p x -k sudo_exec auditctl -w /bin/su -p x -k su_exec每条审计记录都会写入/var/log/audit/audit.log,配合ausearch和aureport工具,可以快速查出来什么时候、哪个进程、访问过哪些敏感文件。不过auditd对系统性能有一定开销,在资源受限设备上建议只审计最核心的少数文件,不要全局铺开。
如果内核没有开audit支持,退而求其次的做法是在应用层做精细记录:配置dropbear记录登录成功/失败日志,配合pam的pam_tty_audit或者sudo自带的日志回显,以及shell脚本里记录history,组合起来也能覆盖大部分安全审计需求。
5. 轻量防火墙:为资源受限设备定制流量防线
很多嵌入式开发者觉得防火墙是服务器才需要的东西,嵌入式设备只要能ping通、能连服务器就够了。这个想法风险很大。设备暴露在局域网上,如果没有防火墙,任何端口只要处于监听状态,就等于对全网开放。轻量防火墙不是为了对付高级黑客,而是为了挡住自动化扫描工具和误打误撞的攻击请求。
5.1 选型:iptables还是nftables?
嵌入式场景下,选nftables还是iptables,我给的结论是:如果内核版本在3.13以上且空间允许,优先用nftables。原因有几点:
- nftables的内存占用和规则匹配效率通常比iptables好,规则集也更紧凑。
- 配置语法更清晰,不容易写出又乱又难维护的规则链。
- 内核里netfilter的nf_tables子系统在持续演进,新特性都往这边堆。
不过现实是,很多存量设备的内核版本较低,或者团队对iptables已经有大量脚本积累,这时候硬迁到nftables反而增加风险。我的建议是:新项目能用nftables就用nftables,老项目继续用iptables,并计划在下一个大版本升级时平滑切换。
使用nftables前,确认内核配置里打开了CONFIG_NF_TABLES,然后通过BusyBox或独立安装nft命令来管理规则。提权风险不用太担心,nft命令本身有不错的语法检查,错误规则会直接拒载,不容易把系统搞到完全断网。
5.2 最小规则集设计:默认拒绝、按需放行
防火墙规则的原则,和最小化裁剪完全一致:默认拒绝,显式放行。很多工程师习惯先写“允许所有”,再补几条“拒绝”,这种思路在安全加固里是反面教材。正确做法是,input链默认策略drop,然后对每个真正需要的流量单独写放行规则。
一个nftables的最小规则集示例:
#!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; # 环回接口必须放行 iif "lo" accept # 已建立连接的数据流放行 ct state established,related accept # 无效连接直接丢弃 ct state invalid drop # 放行SSH管理端口,限速防暴力破解 tcp dport 22 accept tcp dport 22 ct state new limit rate 5/minute accept # 放行业务端口,例如8080 tcp dport 8080 accept # ICMP仅放行ping,限速 icmp type echo-request limit rate 5/second accept # 其余全部丢弃 drop } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } }默认拒绝会自动拦截所有没有明确允许的入站流量,比如telnet、SNMP、以及设备上偶尔残留的调试端口。要注意,output链默认放行一般是合理的,嵌入式设备主动向外的连接不能误伤;如果你对设备有强控制需求,可以再细致约束output链,但初期不建议做太死,否则业务调试会非常痛苦。
5.3 常见场景的防火墙规则示例
除了最基础的最小规则集,实际产品里还会有几个高频场景,需要额外配置:
第一个场景是限定管理地址。设备既然开启SSH远程维护,就不应该允许任意IP访问。用nftables限源IP:
# 只允许10.20.0.0/24网段访问SSH ip saddr 10.20.0.0/24 tcp dport 22 accept第二个场景是防止端口扫描和暴力破解。除了SSH限速,还可以用connlimit限制同一IP建立的新连接数:
# 同一IP最多允许10个新连接 tcp dport 22 ct state new ip saddr limit rate 10/minute accept第三个场景是保护业务端口。设备如果对内网提供数据服务,通常只允许业务网段访问,配置方法和管理端口相同。可以配合白名单机制,把其他网段全部丢掉。
配置完规则后,一定要记得把规则持久化。nftables用nft list ruleset > /etc/nftables.conf保存;iptables用iptables-save > /etc/iptables.rules保存,然后在启动脚本里加载。忘了持久化是最常见的问题,重启后设备又回到“裸奔”状态。
6. 第16讲课后思考题完整解析
按专栏的进度,第16讲讲的是嵌入式Linux从启动到服务运行的基础链路安全,涵盖内核启动参数、设备树、init进程与systemd基础加固。课后思考题看着简单,但实际做起来有不少坑,我挑了四道代表性问题展开解析。
6.1 第16讲内容回顾与思考题分布
第16讲的主线是从硬件上电到用户态服务启动这条完整链路:U-Boot传递启动参数、内核解压、设备树解析、根文件系统挂载、init进程和systemd拉起服务。这个过程中每一个环节都可能成为安全缺口,所以课后思考题的分布也集中在这些环节:如何识别攻击面、网络拓扑设计逻辑、内核日志的信息泄露风险、功能开发与裁剪的平衡。
这些题目不要求你背命令,而是逼着你去盘点自己设备的“家底”。我在布置思考题时的本意,就是让读者带着问题去把整个启动流程和安全配置过一遍,等做到第17讲的实战加固时,才会理解每一步动作到底是在防什么。
6.2 思考题1:如何识别系统中的最大攻击面?
这道题的答案不是“开放了哪些端口”这么简单,而是要从数据输入角度去分析。攻击面是所有可接受外部输入并产生执行、存储或转发行为的路径。
实际操作中,我会分四步盘查:
- 网络输入面:用netstat -tlnp或ss -tlnp列出所有监听端口,对每个端口问三个问题——这个服务是必需的吗?它有身份认证吗?它的输入数据可信吗?
- 本地输入面:检查sysfs、procfs、debugfs里是否有可写的节点;检查/tmp、/var/tmp等临时目录是否可以任意读写;检查所有setuid/setgid文件。
- 物理输入面:串口是否需要登录,JTAG/SWD调试口是否已禁用,存储介质是否可以直接拆下来读取。
- 固件更新面:升级包的签名校验是否真的开启了,固件包解密密钥是否硬编码在镜像里。
最大攻击面通常不是某一个端口,而是“默认口令+调试服务+未签名升级”的组合。答这道题时,要能输出一张攻击面清单,标出每个入口的威胁等级,再和加固措施一一对应。
6.3 思考题2:为什么要区分业务网络与管理网络?
这道题考的是网络分域思想。业务网络指设备对外正常提供服务的网络,管理网络指设备接收配置、维护、升级指令的网络。两者如果不做区分,所有流量都混在同一个平面内,意味着任何一个业务网络里的攻击者都能直接访问设备的SSH、Web管理、SNMP等管理接口。
我举个例子:一个局域网里跑着N台摄像头,同时还有办公电脑。如果摄像头的管理口在内网无限制开放,办公电脑被植入恶意软件后,就能直接在局域网里扫描摄像头管理端口,用默认口令登进去。如果管理面和业务面分开,办公电脑所在的业务网段根本访问不到管理接口,攻击路径就断了。
落地时不用想得太复杂,至少做到两点:管理网段的源IP白名单,管理端口不暴露到业务网段。如果设备只有一个网口,可以在防火墙层面对管理接口实施来源IP限制,只允许运维人员所在网段访问,配合802.1Q VLAN做逻辑隔离更好。
6.4 思考题3:内核日志泄露了哪些敏感信息?
这道题考察的是信息泄露意识。内核日志(dmesg)里实际上包含了大量对攻击者有用的信息:内核版本号、编译时间、编译工具链路径、内核基地址、设备寄存器地址、驱动加载顺序和模块列表。
攻击者拿到内核版本号后,很可能去匹配对应的公开漏洞,比如某个老版本内核的提权CVE。拿到基地址后,结合内核镜像布局知识,可以绕过KASLR做内核态攻击。所以量产固件通常需要把dmesg限制为root用户可读,或直接关闭普通用户的dmesg输出。
具体做法是设置kernel.dmesg_restrict=1。更激进一点,把内核启动参数里的loglevel调低,减少内核输出到控制台和串口的信息量,或者把最终发布固件的串口日志完全关闭。这块容易踩的坑是:开发阶段日志打得太爽,量产固件忘记收口,结果攻击者通过串口就能看到一堆内部地址信息。
6.5 思考题4:最小化裁剪与功能开发的平衡点
这道题没有标准答案,考察的是工程权衡能力。裁剪过头,业务功能起不来;裁剪不足,攻击面又太大。我建议从流程上解决,而不是靠某一次裁剪操作解决问题。
第一,建立模块化配置基线。把rootfs配置拆解为基础版、标准版、全功能版,基础版只留下网络通信、日志、核心业务,其它组件按需增补。这样既保证最小化,又不影响功能迭代。
第二,裁剪前先做依赖分析。列清楚每个业务进程依赖的动态库、内核模块、设备节点、配置文件。很多裁剪问题都是依赖链断裂导致的,比如某个程序需要/dev/watchdog,但内核裁剪掉watchdog驱动,应用就异常退出。
第三,保留可观测性。裁剪不是把所有的调试工具都删光,而是在“调试能力”和“攻击面”之间找一个折中,比如只留一个带授权控制的抓包工具入口,日常默认关闭。
如果产品有CE或等保合规需求,最小化裁剪的基线还可以和合规清单挂钩,既能安全出货,又能应对审计检查。
7. 实战踩过的坑与排查技巧
安全加固这套东西,理论和落地之间的距离比想象中大。我在不同项目里翻过几次车,把几个典型问题写下来,给后面做同类工作的人提个醒。
7.1 裁剪过头导致业务功能异常
这个问题最常发生在我们裁剪内核时。第一次做裁剪的同事,狠起来把所有不认识的驱动和协议全部关掉,最终固件下载到设备里,应用起来后一部分正常的硬件功能不工作。排查时发现,某个采集模块依赖内核里的spi-gpio驱动,而这个驱动在裁剪时恰好被默认配置里的某个依赖条件误关掉了。
解决方案是拿裁剪前后的内核配置做一次diff,把每个被关闭的选项列出来,和业务依赖表逐一核对;同时把业务功能测试和回归测试纳入裁剪验收流程。裁剪有风险,保险起见,每次裁剪只做一小步,然后跑一次基础功能测试,不要一次性改几十个内核选项。
7.2 权限硬化导致应用无法启动
有一次我把系统里所有服务都降权运行,结果一个数据采集程序启动后立刻崩溃。查日志发现,它要写一个配置文件,但该文件目录的owner是root,降权后的用户没有写权限。还有一次是应用需要访问某个设备节点,而udev规则没给对应用户组授权,导致open设备失败。
权限硬化的坑基本都是“最小权限做得太狠”,没有给正常业务留出空间。我的经验是:先保持默认权限跑通业务,再逐项收紧权限,每收紧一项都做一轮回归测试。遇到应用无法启动,第一时间查audit日志或dmesg,看是哪一步系统调用被拒绝,再针对性地补权限或改规则。
7.3 日志量骤增导致存储耗尽
设备在正式环境里跑了一个月,突然所有功能变慢,连SSH都登录不进去。登录串口一看,/var/log分区已经100%满了,罪魁祸首是一个第三方库疯狂打印告警日志,每秒几十条,把flash空间瞬间打满。
这个问题的根源是我在量产固件里没有配置日志轮转和大小限制。现在我的做法是:日志存储必须设置上限,单文件大小和保留份数都要明确,而且要对重点进程做日志速率限制。另外,量产设备日志级别默认设为info,debug级别的输出全部关掉,除非现场远程调试才临时打开。
7.4 防火墙规则导致设备无法远程维护
防火墙规则上线后,我最担心的情况就是把自己关在外面。早期一个项目里,我在iptables里配了一条允许远程维护网段的规则,结果忘了维护专线IP不固定这件事,服务器换了IP之后再也连不上设备,只能去现场连串口救急。
后续我自己的规矩是:防火墙规则先在测试环境完整模拟一遍,验证所有管理通道和业务通道都正常后再上生产;同时在规则注释里写清维护网段和变更联系人的信息,避免后续维护人员改配置时不知所措。最重要的是,保留至少一条串口备用通道,确保规则的最后一道保险永远是物理访问途径。
我个人在实际操作中的一个体会是:安全加固不是一次性的项目,而是一条持续演进的流水线。每一个新功能、每一个新驱动、每一版固件,都应该顺手再过一遍“裁剪、权限、日志、防火墙”这四道工序。形成习惯后,你不会觉得这是额外成本,反而会觉得,设备交给客户之后自己心里踏实了。最后再分享一个小技巧:每次发布正式固件前,把整个rootfs关键文件的hash指纹列表导出一份存档,后续一旦怀疑设备被篡改,直接重新计算比对,几分钟就能确认是否被动过手脚。