这条报错我先放这儿:wsl: failed to start the systemd user session for 'root'. see journalctl for details。很多人看到它第一反应是"坏了,systemd又出问题了",然后跑去看日志,折腾半天发现其实是用户会话没起来。Systemd就是这样,表面上是"开机启动服务"的进程管理器,实际越往下挖越发现它是一整套操作系统管理框架。尤其是到了沙箱化和安全加固这一层,很多人根本没意识到,自己天天用的systemd其实自带了一整套安全机制,可以用最简单的方式把一个服务锁死。
这篇是Systemd系列的第五篇,前面几篇如果都跟下来了,你现在应该能用unit文件写服务、用timer替代cron、用journalctl查日志、用socket activation管理依赖。这一篇我们换个视角——不讲怎么让服务跑起来,讲怎么让服务跑起来之后不被人搞。主题是Systemd的安全沙箱机制和最佳实践,核心内容包括:文件系统隔离、权限限制、能力裁剪、系统调用过滤,以及一条命令给服务安全打分的systemd-analyze security工具。
如果你维护过任何暴露在公网的服务,或者你在公司里负责中间件部署,这篇文章值得看完。哪怕你只是在自己服务器上跑了几个Docker容器和自写脚本,这套东西也能帮你把攻击面压缩到一个很小的范围。
1. 为什么Systemd是安全加固的隐藏利器
1.1 从威胁模型说起:服务被攻破之后会发生什么
先说一个容易被忽略的事实:我们做安全加固,重点不是防止服务被攻破,而是假设服务已经被攻破之后,怎么让攻击者什么都干不了。
举个例子。你在一台服务器上跑了一个Nginx,nginx.conf里写错了某个配置,或者PHP代码里存在一个文件包含漏洞,攻击者拿到了Web服务的执行权限。这时候他会做什么?大概率是这几件事:读/etc/shadow提取密码哈希、扫描内网、下载工具、尝试提权、甚至直接删库跑路。
如果你只是裸跑一个nginx.service,那这些操作几乎是畅通无阻的。因为默认情况下,systemd启动的进程继承的是完整的权限集合,能读写哪些文件、能调用哪些系统调用、能访问哪些内核接口,完全没有限制。
但如果你给这个服务配置了ProtectSystem=strict、PrivateTmp=true、NoNewPrivileges=true,情况就完全不一样了。攻击者拿到的那个shell,会发现自己连/etc目录都写不进去,/tmp也不是原来的/tmp,想执行/usr/bin/curl下载工具,结果系统调用被拦了。这就是沙箱化的价值——它不是让你的服务更强大,而是让你的服务被攻破之后,攻击者寸步难行。
这里有一个经常被误解的点:Systemd的沙箱不是虚拟机,也不是容器。它没有独立的网络栈,没有独立的PID命名空间(除非你开PrivatePids),它做的事情本质上是"限制",而不是"隔离"。同一个内核,同一套文件系统,但通过权限裁剪、命名空间隔离、系统调用过滤等手段,把进程能碰到的东西限制在最小范围内。这种思路和Docker的--security-opt、Kubernetes的SecurityContext是同源的——大家都走的是Linux内核那一套机制,只是Systemd把这些机制包装成了unit文件里一行行的配置。
1.2 Systemd沙箱的能力范围
Systemd的沙箱参数主要定义在systemd.exec手册里,服务进程在execve之前,systemd会按顺序完成一系列准备工作:创建新的挂载命名空间、设置文件系统保护、裁剪capabilities、应用系统调用过滤器。这些能力大致可以分成四层:
| 层次 | 典型参数 | 作用 |
|---|---|---|
| 文件系统 | ProtectSystem、ProtectHome、PrivateTmp、ReadOnlyPaths、InaccessiblePaths | 控制进程能读写哪些路径 |
| 进程隔离 | PrivateDevices、PrivateNetwork、PrivateUsers、ProtectKernelTunables、ProtectKernelModules、ProtectControlGroups | 限制对设备、网络、内核接口的访问 |
| 权限限制 | NoNewPrivileges、CapabilityBoundingSet、AmbientCapabilities、RestrictSUIDSGID | 控制进程能获得哪些特权 |
| 系统调用 | SystemCallFilter、RestrictAddressFamilies、MemoryDenyWriteExecute、LockPersonality | 限制进程能调用哪些内核API |
这四层可以叠加使用,配合systemd-analyze security给出的安全等级评估,你能很直观地看到每个配置项对整体安全的影响。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安全沙箱核心参数拆解:原理与实战
2.1 文件系统隔离:ProtectSystem与PrivateTmp
文件系统隔离是沙箱里最常用、效果最明显的一类参数。
ProtectSystem有三个级别。默认是no,不做任何限制。ProtectSystem=yes表示/usr、/boot、/etc变成只读。ProtectSystem=full在前面基础上再加上/efi和/etc。ProtectSystem=strict则是整个文件系统除了/dev、/proc、/sys这几个虚拟文件系统之外全部变成只读。
实际使用中,绝大部分服务根本不需要写/usr和/etc,所以ProtectSystem=strict是代价最低收益最高的配置之一。但有个问题要注意:如果你某个服务确实需要写/etc下的某个配置文件,strict模式会直接让这个写入失败,而且报错通常是权限问题,排查起来容易懵。这时候你需要用ReadWritePaths=把特定路径重新放回可写状态。
再来看PrivateTmp=true。这个参数很妙,它会给服务进程创建一个私有的/tmp和/var/tmp挂载命名空间。什么意思呢?就是服务进程看到的/tmp和宿主机上的/tmp不是同一个。攻击者就算在/tmp下放了恶意文件,别的进程也看不到——反之亦然,别人也看不到服务自己的临时文件。这个功能用的就是Linux的namespace机制,和容器是同一套底层技术。
我自己的习惯是:除了极少数需要跨进程共享临时文件的服务,一律开PrivateTmp=true。有个坑是,如果你用/tmp来传递Unix socket,开了PrivateTmp之后别的进程可能连不上。这时候优先考虑换/run目录,因为/run本身就是设计用来放运行时文件的。
ProtectHome=true则更直接:把/home、/root、/run/user整个锁掉,进程完全不可访问。对绝大多数服务来说,它完全没有任何理由去读用户的家目录。如果嫌这个还不够狠,ProtectHome=read-only是只读版本,ProtectHome=tmpfs则是给这三个目录挂一个空的tmpfs,进程能访问但内容不存在。
2.2 内核接口保护:ProtectKernelTunables与相关参数
内核接口是很多人会忽略的重灾区。
/proc/sys和/sys这两个目录看起来只是普通文件,实际上它们映射的是内核的运行时
