
1. 很多设备根本没到被攻击的那一步就已经被“看穿”了我在做安全评估时见过太多这样的设备默认密码是admin/admin、开启了telnet、所有业务进程全用root跑、内核编译时连没用的驱动都一股脑编进去。这类设备不是“将来可能出问题”而是现在就等于脱光了站在公网上。嵌入式Linux的体量小看起来不起眼但偏偏是摄像头、路由器、网关、工控盒这些设备最容易成为攻击者的跳板。原因很简单它们常年在线、疏于维护、固件更新周期长一旦沦陷攻击者可以慢慢玩。这一讲要说的安全加固不是让你把系统当成银行核心系统来防——大部分嵌入式设备的算力、存储和运维条件根本撑不起那种重方案。真正的目标是在有限的资源里把攻击面砍到最小把权限锁到最紧把痕迹留到最全把进出流量管到最严。说白了就是四个动作最小化裁剪、权限硬化、日志审计、轻量防火墙。这四件事看起来各自独立实际是一条链路。裁剪让系统里没用的东西少一点权限硬化让有用的东西不那么容易被滥用日志审计让出了问题能回溯现场防火墙让该进的和不该进的流量各走各门。这篇内容我会把每一步的原理、操作和坑都拆开讲适合正在做嵌入式Linux产品开发、或者接手了旧设备想做一轮安全整改的工程师直接参考。在动手之前先想清楚一个前提加固不是装几个工具、跑几条命令就完事它是设计和运维层面的事情。你越是早一点把安全放进系统里后面付出的代价就越小。设备一旦批量出货你在几千台机器上做一次安全策略变更的成本和在开发板上改一个编译选项的成本完全不是一个量级。2. 最小化裁剪不是“删文件”是重新审视系统里每一个组件的存在理由很多人理解最小化裁剪就是删掉没用的软件包、关掉多余的服务。这个思路方向对但远远不够。真正的裁剪要从根上问一个问题这个组件在系统里提供了什么功能这个功能真的需要吗如果没有它系统还能不能跑2.1 内核裁剪裁剪的是攻击面不只是体积内核编译时默认配置会把大量驱动、文件系统、网络协议都编进去。对开发板来说这无所谓但对要部署到现场的设备来说每一行没有实际用途的代码都是潜在的攻击入口。比如你根本不用蓝牙但内核开了蓝牙协议栈那么蓝牙相关的CVE就砸到你头上了。内核裁剪的第一步是明确设备的硬件能力表和业务功能表。我曾经做过一个数据采集网关硬件上只有网口、串口、GPIO和几个传感器接口业务上只需要TCP/IP通信和简单的存储。那么裁剪的方向就非常清晰了网络协议保留TCP、UDP、ICMP关掉DCCP、SCTP、IRDA这些用不上的文件系统只保留实际用的ext4或squashfs关掉FAT、NTFS写入支持、NFS挂载驱动只编入本设备硬件对应的驱动其余一律不编或编成模块但不加载蓝牙、WiFi、USB Host相关如果不用直接关掉内核特性关了kexec、模块加载如果固件不允许动态加载模块、usb gadget相关内核配置里有一个挺实用的做法把不需要的功能从CONFIG_*层面关掉而不是仅仅不加载。比如CONFIG_MODULES如果不需要动态加载模块直接关掉这样即使攻击者拿到了设备也无法往内核里塞模块。类似地CONFIG_BPF_SYSCALL在大多数嵌入式场景下也可以关掉BPF功能强归强但对攻击者来说同样好用。内核裁剪完之后用make savedefconfig生成一份精简的defconfig放在工程里管理以后每次bump内核版本都能对照差异。这块我有过教训最初裁剪时直接改.config结果换内核版本时全乱了后来老老实实维护defconfig再配合脚本做diff事情就顺了很多。2.2 rootfs裁剪BusyBox、编译工具和文档都得走rootfs层面的裁剪核心原则是“运行时要什么就留什么”。开发环境里那些gcc、gdb、make、头文件统统不该出现在生产镜像里。我在实际项目中见过有人在生产设备上留了交叉编译工具链理由是“方便现场调试”。这个理由是假需求真需要调试用远程日志和单步跟踪就够了留编译工具链等于给攻击者送武器。文件系统的瘦身有几个常见手段用BusyBox代替完整的coreutils套件一条命令搞定几百个工具用musl libc代替glibc体积优势明显对静态编译的小程序尤其友好删除所有文档、man page、locale数据除非设备有多语言界面需求删除所有静态库和头文件对只读文件系统比如squashfs配合overlayfs做可写层该写的地方才写别小看这些操作。我做过一个实际对比同一个功能的应用用buildroot默认配置做一个镜像再按上述原则裁一遍镜像体积从300MB出头降到70MB左右这还是在保留了不少调试工具的情况下。如果连调试工具都不要压到40MB以内很轻松。不过有一点要提醒裁剪前一定要列清楚现场的运维需求。比如你需要远程排查问题那tcpdump、strace、top、ps这些诊断工具不能全裁否则出了问题只能干瞪眼。我的建议是保留一个最小诊断工具集但通过权限控制保证只有特定用户能执行。2.3 裁剪的边界什么是绝对不能省的这几年做加固最大的心得是裁剪不是越狠越好它要在“攻击面最小”和“可维护性正常”之间找平衡。有些东西省了后面要花十倍的代价补回来。绝对不能省的东西我列一下系统升级通道要预留A/B分区或者至少一个可用的恢复机制、最小日志能力出事没有日志一切白干、核心服务的依赖删了库导致业务崩溃属于重大事故、以及NTP或等价的时间同步机制。一个安全加固过的系统如果连时间都不准日志审计的价值直接打五折——到时候查问题发现日志时间对不上攻击行为的时间轴全乱套了。3. 权限硬化把系统和进程的“权力”压缩到只剩够用的程度权限硬化是安全加固里技术含量最高的一环也是最需要细心的一环。很多设备的漏洞利用链路从低权限的Web进程一路提权最终拿到root权限。如果权限硬化做到位这条利用链会在某个环节断掉。3.1 SUID权限硬化最先要处理的位置UNIX系的权限模型里SUID位是最危险的设计之一。一个带SUID位的程序执行时以文件属主身份运行。如果这个程序是root属主又有漏洞那么普通用户通过它就能直接提权。排查SUID文件的方式很直接find / -perm -4000 -type f 2/dev/null这个命令会列出所有带SUID位的文件。拿到列表后逐个确认它的用途和必要性。常见的合法SUID包括busybox如果配置了su/applet方式、passwd普通用户改密码要用、mount部分场景需要等但也仅此而已了。其余的全部去掉SUID位chmod u-s /path/to/executable我见过不少设备固件里躺着几十个带SUID的二进制大部分是从某个公共根文件系统直接搬过来的根本没人确认过它们为什么需要SUID。这种清理工作不复杂但需要细心每一项都要确认“这个程序的SUID位去掉之后业务还会不会正常”。尤其注意那些本身就具有提权能力的程序比如sudo如果业务不需要不如直接卸载。3.2 挂载选项与只读文件系统系统能改的地方越少攻击者能留后门的地方就越少。重点在于文件系统的挂载选项/dev/mmcblk0p2 / ext4 ro,nodev,nosuid 0 1 /dev/mmcblk0p3 /var ext4 rw,nodev,nosuid,noexec 0 2 tmpfs /tmp tmpfs nodev,nosuid,noexec,mode1777 0 0几个关键选项的意义ro根文件系统以只读方式挂载固件本身不被篡改nodev该分区上不解析设备文件防止攻击者创建块设备节点nosuid该分区上SUID位无效noexec该分区上不可执行程序尤其适合/tmp和/var这类可写分区只读根文件系统配合overlayfs做可写的/etc或/var是最经典的方案。但要注意/etc下有些关键配置文件需要可写比如hostname、网络配置你可以把单独的分区挂到/etc下对应的目录上而不是把整个/etc都弄成可写的。粒度越细被改的面就越小。3.3 进程权限不给业务进程root权限嵌入式设备里最常见的权限失控行为就是所有业务进程用root跑。开发方便调试省心但代价是攻击者只要拿下一个进程就等于拿到了整个系统。正确的做法是给每个业务进程创建独立用户按最小权限分配useradd -r -s /sbin/nologin -d /var/lib/myapp myapp_user-r创建系统账户-s /sbin/nologin禁止登录-d指定家目录。然后在启动脚本里用start-stop-daemon或systemd的User指令指定运行用户[Service] Usermyapp_user Groupmyapp_group NoNewPrivilegesyes ProtectSystemstrict ReadWritePaths/var/lib/myappNoNewPrivilegesyes这条尤其重要它禁止进程通过execve获得新的权限提升从内核层面断了提权这条路。配合ProtectSystemstrict即使进程被攻破也写不了系统目录写不了根目录。服务启动这块很多老派工程师习惯用socket/inetd方式启动但systemd带来的一堆安全选项不用太可惜。哪怕你的嵌入式系统用的是SysV init也建议逐步迁移到systemd或者至少想清楚不用它的理由。3.4 文件系统级强制访问控制SELinux还是AppArmor这个要看场景。SELinux的粒度最细但配置复杂度极高一堆policy文件要维护AppArmor基于路径配置简单直觉适合嵌入式设备的运维习惯。我倾向用一个词来建议如果团队里没有专职安全工程师选AppArmor如果有人能专职维护安全策略上SELinux。但说实话对大部分嵌入式产品严格的文件系统权限加挂载选项已经能挡住90%的常规攻击。SELinux和AppArmor是锦上添花不是雪中送炭。如果你的设备连SUID都没清挂载选项都是默认的先不要碰MAC。配置方式上AppArmor写一个profile文件指定某个程序的路径、可读路径、可写路径、可执行路径就够了。例如/path/to/myapp { /etc/myapp/* r, /var/lib/myapp/** rw, /path/to/binary ix, }Profile文件生效后可以通过aa-status确认加载状态。这个机制的实际拦截效果不错有一次我故意用业务进程越权读/etc/shadow直接就被拦截了日志里记得清清楚楚。4. 日志审计设备被攻破后日志就是唯一的现场安全加固做得再好的系统也无法保证百分百不被攻破。而日志审计的作用就是在攻破之后帮你还原攻击者的路径、时间、操作内容。没有日志的设备出了事等于没有侦探在场。4.1 嵌入式日志架构syslog、logrotate和远程日志嵌入式系统的日志方案要兼顾两点日志格式要统一日志容量要可控。老式的syslogd最大的问题是格式混乱、不支持结构化查阅困难。就算精简如BusyBox自带的syslogd做基本的事也够用但我更推荐在资源允许时上rsyslog或syslog-ng它们的过滤转发功能更完整。无论选哪个第一件事是设置远程日志。日志存本机攻击者拿到root权限后第一件事就是清日志。远程日志服务器哪怕只是一台内网机器能保证攻击者的行为至少有一份副本留在了设备之外。传输方式别用明文用TLS或者至少签个证书。远程日志的基础配置syslog-ngsource s_local { system(); internal(); }; destination d_remote { syslog(192.168.1.100 port(514) transport(tls)); }; log { source(s_local); destination(d_remote); };本地日志也别全丢留一份即可。注意/var/log通常放在可写分区上容量有限不做轮转的话几天就能把flash写满。用logrotate按大小或时间轮转保留最近N份/var/log/messages { size 1M rotate 5 compress missingok notifempty }这套配置下来设备哪怕本机存储全完蛋远程服务器上还有一份记录。4.2 auditd审计关键文件访问和执行行为syslog记录的是系统层面的日志事件但它不记录“谁读了哪个文件、谁执行了哪个命令”。这些细粒度行为需要auditd来做。auditd在内核层面打点性能开销可控但在资源极度紧张的MCU上慎用。典型场景设备上的配置文件被改了怀疑有人动过想知道谁改的。auditd的规则可以这样写auditctl -w /etc/myapp/config.ini -p wa -k config_change auditctl -a always,exit -F archb64 -S execve -k exec_events第一条规则监视/etc/myapp/config.ini的写入和属性修改第二条规则记录所有程序的执行。-k后面的字符串是自定义key用于检索。查询审计日志ausearch -k config_change --start today ausearch -k exec_events -c vi审计日志本身也要防篡改日志文件目录的权限要收死最好配合只读挂载或远程日志。4.3 日志时间对齐没有时间同步的日志等于废纸审计日志的价值依赖于时间线的准确性。试想攻击者凌晨三点干活你的设备时间停在两周前或者干脆没有RTC电池每次都回到1970年那日志上的时间戳就是彻头彻尾的误导。嵌入式设备必须强制启用NTP同步至少在内网架一台NTP服务器。在没有外部网络的隔离环境NTP服务器可以放在网关上。时间同步方面我踩过大坑一个设备在无外网环境下运行NTP一直失败我就把时间同步关了结果攻击发生后通过日志排查发现时间偏移了好几个小时最终只能靠手工比对多个设备的日志才勉强还原攻击路径。之后我的原则是哪怕NTP失败率高也要配置持久重试再给系统加一个定时任务定期检查时间偏移超过阈值就告警。4.4 实际案例一则通过日志发现的异常登录有一次我在巡检日志时注意到某台设备上出现了几十次失败的SSH登录尝试全部来自同一内网IP。光看这条消息可能有人觉得是误报但结合审计日志发现攻击者已经成功尝试了默认用户名root/admin登录随后执行了一系列命令。如果没有日志这事根本不会被发现。更关键的是我在远程日志上找到了攻击者修改配置文件之前的操作痕迹确认了入口是暴露到公网的SSH服务。公网SSH没做访问控制这是典型的配置失误。有了日志基础后续防火墙规则才能有的放矢地封禁。5. 轻量防火墙用iptables还是nftables以及怎么配才不死板嵌入式设备的防火墙不需要像企业级那样搞一堆zone和策略库但基础的方向过滤必须具备。常见的需求无非是只允许内网某个网段访问SSH业务端口只对特定IP开放禁止设备主动向外部发起非预期连接。5.1 iptables和nftables怎么选从功能上讲nftables是下一代框架语法更整洁性能更好且内核4.x之后就是标配。真正要考虑的是你的系统上是否自带nft命令以及团队成员熟不熟悉语法。如果设备长期不变更用iptables也完全没毛病。从实操角度我更推荐nftables理由很简单配置可以用一个文件搞定不用写一堆iptables -A命令拼起来review规则时一眼能看懂整条链的逻辑。举个例子table inet filter { chain input { type filter hook input priority filter; policy drop; iif lo accept ct state established,related accept ip saddr 192.168.1.0/24 tcp dport 22 accept ip saddr 192.168.1.0/24 tcp dport 8080 accept } chain forward { type filter hook forward priority filter; policy drop; } chain output { type filter hook output priority filter; policy drop; ct state established,related accept oif eth0 udp dport 123 accept oif eth0 tcp dport 443 accept } }这个配置做几件事入向默认丢弃只放行本地回环、已建立连接、内网SSH和业务端口转发默认丢弃设备不做路由转发出向默认丢弃只放行NTP和HTTPS出向限制是这个方案里最容易被忽略的一点。大部分设备的业务模式比较固定根本不需要任意出向流量。恶意程序一旦植入第一件事往往就是反向连接外部服务器。出向限制能直接把这个行为断掉。5.2 防火墙配置的坑别把业务流量一起丢了配置防火墙时最容易犯的错误是只想着“要放行什么”而忘了“默认策略是什么”。如果默认策略是ACCEPT那么漏掉的规则不会导致业务中断如果默认策略是DROP漏掉一条规则就是一次生产事故。我建议的策略是入向默认DROP、出向默认放行但在部署前切到白名单模式试跑一段时间。先用ACCEPT跑记录所有实际出现的连接可通过conntrack或日志观察再基于这个记录去收敛出向规则。另一个常见的坑是防火墙规则影响了设备的OTA升级。有些设备升级要连指定服务器但升级域名可能变出向白名单写死IP过一阵子升级服务器迁移了就全断了。这种场景建议在DNS层放开或定期更新规则但要在运维手册里写清楚。防火墙规则变更要有备份和回滚机制不能直接nft flush table完事。我自己会保留当前表配置再切换到新配置确认没问题后再删旧的。做法简单但关键时刻能救你一命。5.3 防火墙之外该封的端口要封该藏的端口要藏防火墙只能过滤网络层和传输层流量到了应用层的漏洞它是管不了的。嵌入式设备里最常见的暴露面就是各种Web管理界面和应用端口。有些设备默认开着Web管理端口只做了个密码认证而且密码还是默认的这种情况防火墙再强也没用。建议是能不开的管理端口就不开必须开的要么只监听在管理网段上要么配合防火墙白名单访问控制。SSH服务至少做到禁止密码登录、强制公钥认证监听端口如果不是特殊需求也可以改掉但别指望靠改端口来防扫描这只是多一层混淆不是安全措施。6. 第16讲课后思考题解析把前面的知识点串成实战链路上一讲的思考题有4道我挑出最典型的几道逐一解析。这些题的设计思路不单是回顾知识点更是引导你从整体视角去理解安全加固的取舍逻辑。6.1 思考题一最小化裁剪时为什么“看似没用”的文件反而不能随便删题目问的是既然做最小化裁剪为什么某些系统工具如ls还要保留很多人的第一反应是BusyBox已经提供ls了没必要保留完整版。但实际问题要更深一层这些基础命令不只服务于人类操作员还服务于各种脚本和诊断工具。如果你为了省那几百KB把ps换成残缺的实现运行时的监控脚本可能读到不完整的进程信息导致自动重启逻辑误判。更关键的是问题不在于“这项命令要不要”而在于“这项命令是否在系统已知功能的范围内”。一旦你删掉一个你“以为”没用的命令而系统某个角落的脚本仍在依赖它这个脚本就会陷入静默失败问题积累到上线之后才爆出来届时你连ps都跑不了排查手段都没了。所以裁剪的原则不是“每个文件都问一遍有没有用”而是“基于功能需求清单反向分析依赖关系”。先把业务功能和运维需求列全再通过依赖分析找出必需的组件剩余的可砍可不砍没有明确的保留理由就砍掉。有一个好习惯是裁剪后做一轮全量的冒烟测试把开机、连接、业务、日志、升级所有流程全走一遍。6.2 思考题二SUID位的清理为什么一边清一边还有新风险这道题想表达的是SUID清理不是一条find命令跑完就完事了。整个系统里动态链接的二进制的依赖库如果某个库本身以root权限加载并带有漏洞即使二进制本身没有SUID也一样可能被利用。例如某二进制没有SUID位但使用了一个带漏洞的共享库攻击者通过修改库的搜索路径或利用环境变量注入就有机会让这个二进制在加载库时执行恶意代码。SUID清理的本质是减少“以特权身份运行的代码路径”而不是简单地看文件属性位。这道题的完整思路是先做基线收集系统所有SUID文件列表一个个确认来源和必要性然后定期做diff新出现的SUID文件必须说明原因。配合NoNewPrivileges和只读挂载SUID清理的效果才真正稳固。6.3 思考题三日志审计方案中远程日志比本地日志好在哪这题表面问的是远程日志的优劣实际考察的是对“攻击者视角”的理解。攻击者拿到root权限后第一动作不是干坏事而是清理痕迹。本机日志就是最显眼的痕迹删掉它只需要一条rm或者更隐蔽的truncate且很难被察觉。远程日志的价值在于它把日志的存储位置移出了攻击者的控制范围。即使设备被拿下攻击者删光本机日志远程服务器上的原始记录还在。但这道题的延伸问题是远程日志和本地日志的内容、格式、保留策略要不要一样答案是否定的。远程日志应当更全本地日志可以只保留最近一段时间的摘要防止flash被写爆。数据安全性和设备可用性之间总要有一个平衡。6.4 思考题四防火墙默认策略选DROP好还是ACCEPT好这道题没有绝对正确答案但你的选择必须和产品场景匹配。如果设备在公网直接暴露默认DROP明显更合理如果设备只在受信任的内网运行默认ACCEPT也能接受但你需要清楚多了一层风险。我更倾向于默认DROP原因很简单默认拒绝的模型下业务流量要通过明确定义规则来放通这种显式声明的方式便于审查和审计。每次新增一条放行规则都必须回答“为什么这个流量需要放行”这种机制本身就是对攻击面的持续收敛。当然默认DROP的代价是调试不方便。开发阶段经常会有各种临时连接需求逐个开规则太繁琐。我的做法是开发环境用宽松策略发布前改成严格策略并跑完回归测试。关键点在于这个切换动作必须在产品流程里固化下来而不是靠某个工程师“记得”。7. 最后留一份加固自检清单按这个顺序做不会漏逐一做完前面的步骤之后我习惯用一份自检清单做最终验证。这份清单是根据多次处理实际设备安全事件沉淀下来的按顺序来能避免漏项。系统里是否有明确不需要的网络服务在监听端口所有服务进程是否以非root身份运行系统中是否还有不必要的SUID文件关键目录的写权限是否被限制到最小范围防火墙默认策略是否为DROP放行规则是否有注释说明用途系统日志是否同时保留了本地和远程副本远程日志的传输通道是否加密系统时间是否有可靠的同步机制根文件系统是否以只读方式挂载可写分区是否启用了nosuid,nodev,noexec固件升级通道是否已受签名保护最后一步的固件签名很多人会忽略。安全加固的每一项措施如果固件本身可以被任意刷写那前面做的所有努力都可以被直接覆盖。所以至少给升级包加上签名校验密钥要保存在安全的地方并且做到私钥不出安全环境。这十条都过了设备才算是有了底气。但也要记住安全不是静态的。内核漏洞不断被发现业务的访问模式也会变加固是一个持续的过程。我的习惯是每半年做一次全面的安全复盘对照最新的CVE库检查内核和组件版本。安全这件事最怕的就是“觉得已经加固完了”的心态。