开篇:这一篇,我们聊点真正能让服务“扛揍”的东西
做了这么久 Linux 运维和后台开发,我对 systemd 的感情其实挺复杂的。前几年它还背着“取代一切”的骂名,可到了今天,不管是云原生里的容器编排、裸机上的传统部署,还是嵌入式设备里的启动引导,systemd 几乎已经成了各发行版绕不开的底座。我们前面几篇聊了单元文件怎么写、服务怎么管理、日志怎么查,那些东西解决的是“服务能不能跑起来、挂了能不能拉起来”的生存问题。但这一篇我想把重心往另一个方向压一压——服务跑起来之后,它能不能安全地、隔离地、可控地一直跑下去。
这个 Part 5 的标题叫 Advanced Features, Sandboxing, and Security Best Practices,说白了就是三件事:第一,把 systemd 那些平时不太被注意到的高级能力翻出来讲讲;第二,用 systemd 自带的沙箱机制把服务“关进笼子”;第三,把安全加固做成一套可以复制到每个服务上的最佳实践。适合的读者不是第一次碰 systemd 的新手,而是已经写过几个 service 文件、遇到过几次诡异的启动失败、想在安全层面把服务逼到“默认拒绝一切”的人。
我在实际项目里见过太多这样的场景:服务能起来,业务能跑,但一查 systemd-analyze security,暴露等级拉满到 9.x 甚至 10 分。数据库容器恨不得映射整个宿主机的 /,应用进程以 root 身份跑着,网络命名空间和宿主完全共享。平时没出事大家也就睁一只眼闭一只眼,真出了安全事故,连最基本的隔离手段都拿不出来。这篇文章要做的,就是把这条底线往上拉一拉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 为什么这一篇要先聊“沙箱化”和“安全加固”
1.1 服务生命周期管理之外,真正容易翻车的是隔离与权限
很多人对 systemd 的认知停留在 systemctl start/stop/restart 和写一个 ExecStart 拉进程,这是“生命周期管理”的范畴。但 systemd 能做的远不止这些。它其实是一个能在运行前给进程设置好完整“运行环境”的机制:文件系统视图、挂载命名空间、网络命名空间、用户与组、系统调用过滤、内存写执行权限、capabilities 集合……这些能力组合起来,等于你在启动一个服务之前,就能像给容器设置安全上下文一样,把它的活动范围画好一个圈。
这个圈画得越紧,服务的攻击面就越小。比如一个 Nginx 前端进程,它真正需要的能力是什么?绑定 80/443 端口、读取站点的静态文件、写日志、可能还需要读取 TLS 私钥和证书。它完全不需要写 /etc,不需要访问 /home,不需要执行 mount,也不需要看到宿主上其他进程的 socket。如果你在 service 文件里不设任何限制,它就以一个 root 用户的全部权限运行在宿主的完整命名空间里。这个状态,等于把家门钥匙贴在了门框上。
所以我一直跟团队说,写 service 文件不能只做到“能拉起进程”,要做到“进程被迫以最保守的姿态运行”。这就是沙箱化的意义,也是这一篇文章先讲安全的原因。你只有先理解威胁边界,再看那些高级特性才会觉得豁然开朗。
1.2 热词里那些“诡异”报错,背后全是隔离带来的连锁反应
在准备这一篇时,我扫了一眼网上和 systemd 相关的一些高频搜索词,有几个很有意思:
systemd: wsl: failed to start the systemd user session for 'root'. see journalctl forcould not set file security for filesystemd 目录xinetd 和 systemd
先说第二个,could not set file security for file。这个报错我调过不止一次,典型的场景是你给某个服务配了 ProtectSystem=strict 或者 ReadOnlyPaths,但服务运行时要往某些路径写入文件,而且这些路径又不在你声明的可写列表里。内核的 mount 命名空间把路径变成只读之后,任何 open with write 的尝试都会撞墙。这个报错信息经常出现在服务的 stderr 或者 journal 里,但很多人第一次看到时会以为是 SELinux 在捣乱,其实根本原因是 systemd 在你启动进程前已经把文件系统挂载改成了只读。
第三个词“systemd 目录”就更基础了,但也是安全加固绕不开的坑。systemd 的单元文件分布在三个主要位置:/usr/lib/systemd/system(发行版自带)、/run/systemd/system(运行时生成)、/etc/systemd/system(管理员自定义)。很多人在 /etc/systemd/system 里改了配置发现不生效,就是没搞懂这三个目录的优先级和加载规则。后面讲最佳实践时我会专门提这个目录分层,因为安全加固往往就是靠向 /etc/systemd/system 里丢 override 文件来实现的。
第一个 WSL 报错则是 systemd 在 Windows 子系统里跑用户会话时的一组典型现象,我放在后面第 5 章专门讲,这里先埋个引子——隔离不仅作用于宿主机上的服务,在容器和 WSL 场景里,systemd 的用户会话机制同样有它自己的坑。
1.3 一个“最小权限”的思维模型
在做安全设计时,我习惯用一个很朴素的思维模型:假设服务下一秒就会被攻破,攻击者拿到了这个进程的完整控制权,那么他能做什么?他能不能往磁盘上写文件?能不能读 /etc/shadow?能不能通过某个 socket 探测内网?能不能调用 execve 再去启动一个 shell?
这个模型虽然听着吓人,但它能逼你把“假设安全”换成“假设已被入侵”。所有沙箱指令的取舍,本质上都是在回答这些“能不能”。systemd 提供了一大堆开关,但核心思路就一句话:默认拒绝,按需放行。
这句话落实下来,需要三样东西:一是文件系统视图隔离,二是进程能力收敛,三是系统调用限制。这三样做扎实了,即使服务真的被人利用,攻击者也拿不到多少有价值的东西。
2. systemd 高级特性:依赖编排、资源控制与单元组合
2.1 依赖关系不只是 After 和 Requires,还有 Wants、BindsTo 和 PartOf
写 service 文件时,最常用的是 After 和 Requires,但它们只解决了“启动顺序”和“强依赖”两个问题。真正进入高级用法后,你会发现还有三个指令非常关键:
Wants=: 弱依赖。目标服务启动失败,不影响当前服务启动。适合“顺带拉起但不强绑”的场景,比如我启动一个网关服务时,希望 sidecar 日志代理也起来,但日志代理挂了我不能把网关也拖下水。BindsTo=: 强绑定。不仅要求目标单元启动成功,而且目标单元一旦停止或失败,当前服务会被强制停止。这和Requires的差别在于:Requires只管启动顺序和失败传播,但不会因为目标服务后面手动停了就顺势停掉当前服务;BindsTo是真正意义上的“生死与共”。PartOf=: 单向依赖的批量操作。当目标单元被 stop 或 restart 时,当前单元也会跟着执行同样的动作,但反向不生效。这个在管理一组配套服务时非常方便,比如一个应用由 web、worker、beat 三个 service 组成,你只需要 restart web 这个主单元,写上PartOf=web.service的另外两个也会跟着 restart。
我记得有一次配置一个内部工单系统,前端、任务队列、定时调度三个服务必须一起启停。一开始我用 Requires 处理,结果手动 stop 前端时队列和调度还赖在后台跑,进程互相对不上状态。后来切成 BindsTo 加 PartOf 组合,一组 systemctl stop portal.service 就把三个进程全带停了,干净利落。
