引言:你的服务到底有多"裸奔"?
如果你管理的Linux服务器上跑着Nginx、PostgreSQL、Redis这些服务,先问你一个扎心的问题——你有没有检查过它们的安全暴露评分?
运行一下 systemd-analyze security,你大概率会看到一堆刺眼的红色数字。说实话,大多数默认安装的服务评分都在8到9之间(满分10分,分数越高越危险)。这意味着什么?简单说,如果其中任何一个服务被攻破,攻击者基本上可以为所欲为——读取整个文件系统、访问其他用户目录、加载内核模块,甚至提升到root权限。想想就后背发凉。
好消息是,systemd很早就内置了一套相当强大的安全沙箱机制。这些机制利用Linux内核的命名空间(namespaces)、能力限制(capabilities)、seccomp系统调用过滤等底层特性,让你可以把每个服务锁在一个精心定义的"笼子"里。更妙的是,你不需要改一行应用代码,也不用装额外的安全工具——这一切都内建在你每天打交道的服务管理器中。
接下来我会带你走一遍systemd服务加固的完整流程:从安全审计和评分解读,到核心加固指令详解,再到Nginx、PostgreSQL和Redis的生产级加固配置,最后聊聊SHH自动化工具。看完之后,你应该能把大部分服务的暴露评分从"危险"降到"安全"级别。
第一章:systemd安全审计——摸清你的暴露面
1.1 用systemd-analyze security做安全评分
systemd自带了一个非常实用的安全审计工具 systemd-analyze security。它会扫描系统中所有运行的服务单元,根据每个服务启用(或没启用)的安全指令,算出一个0到10的暴露评分。
# 查看所有服务的安全评分
sudo systemd-analyze security
# 按暴露分数排序,最危险的排最前面
sudo systemd-analyze security | sort -k 2 -rn
# 查看特定服务的详细安全分析
sudo systemd-analyze security nginx.service
评分含义如下:
- 0–3分(安全):服务已充分加固,攻击面最小化
- 4–6分(中等):已有部分加固,但仍有改进空间
- 7–10分(危险):几乎没有安全限制,被攻破后果很严重
不过要注意,这个评分本质上是个启发式指标,不是安全性的绝对证明。打个比方:如果你用它来扫描你的"房子",它会因为有门有窗而扣分——但你确实需要门窗啊。同理,很多服务必须保留一定的攻击面才能正常工作。重点是确保每个服务只拥有它真正需要的最小权限,而不是追求一个完美的零分。
1.2 详细审计报告解读
对单个服务运行详细分析时,systemd会输出一份逐项检查报告。每一项都标注了当前状态和对应的暴露权重。
# 示例:查看nginx的详细安全分析
sudo systemd-analyze security nginx.service
# 输出类似:
# NAME DESCRIPTION EXPOSURE
# ✗ PrivateNetwork= Service has access to host... 0.5
# ✗ PrivateTmp= Service has access to /tmp... 0.1
# ✗ PrivateUsers= Service has access to other... 0.2
# ✗ ProtectHome= Service has access to home... 0.2
# ✗ ProtectSystem= Service has full access to... 0.2
# ...
# → Overall exposure level for nginx.service: 9.2 UNSAFE 😨
报告中标记为 ✗ 的项目就是你要重点关注的加固目标。每项旁边的数字代表它在总分中的权重——权重越大,修复优先级自然越高。
1.3 确定加固优先级
并非所有服务都需要同等程度的加固。我个人建议按这个优先级来安排:
- 直接面向公网的服务(Nginx、Apache、SSH等)——优先级最高,没什么好犹豫的
- 处理不可信输入的服务(数据库、消息队列、邮件服务等)
- 内部基础设施服务(DNS、NTP、日志收集等)
- 系统管理服务(cron、systemd-journald等)——通常已有一定加固
第二章:核心加固指令详解
2.1 文件系统隔离
文件系统隔离是systemd安全加固的基石。通过控制服务能看到和修改文件系统的哪些部分,你可以极大地缩小攻击面。这也是我建议最先着手的加固方向。
ProtectSystem= 控制对系统目录的写入权限:
ProtectSystem=yes:将/usr和/boot挂载为只读ProtectSystem=full:在 yes 的基础上,把/etc也设为只读ProtectSystem=strict:将整个文件系统层次结构挂载为只读(除了/dev、/proc、/sys)。这是最推荐的选项
用了 strict 模式之后,你需要通过 ReadWritePaths= 显式授予服务写入权限的目录:
[Service]
ProtectSystem=strict
# 只允许写入服务确实需要的目录
ReadWritePaths=/var/log/nginx /var/cache/nginx /run/nginx
ProtectHome= 控制对用户主目录的访问:
ProtectHome=yes:让/home、/root和/run/user对服务完全不可见ProtectHome=read-only:允许读取但禁止写入ProtectHome=tmpfs:将这些路径替换为空的tmpfs文件系统
PrivateTmp= 为服务创建独立的 /tmp 和 /var/tmp 命名空间:
[Service]
PrivateTmp=yes
# 服务看到的/tmp实际上是
# /tmp/systemd-private-*-nginx.service-*/tmp
# 与其他进程完全隔离
这个指令相当重要。共享 /tmp 目录中的可预测文件名是一种经典攻击向量(TOCTOU竞态条件攻击),启用PrivateTmp基本上就堵住了这条路。
PrivateDevices= 限制服务对硬件设备的访问:
[Service]
PrivateDevices=yes
# 服务只能访问 /dev/null、/dev/zero、/dev/random 等伪设备
# 无法访问物理磁盘、USB设备等
2.2 权限与能力限制
Linux内核的能力(capabilities)机制把传统root的"全能"权限拆分成了多个细粒度权限。systemd让你可以精确控制服务拥有哪些能力——这比简单地"用root还是不用root"要灵活得多。
NoNewPrivileges= 大概是最简单也最有效的加固指令之一了:
[Service]
NoNewPrivileges=yes
# 确保服务及其所有子进程永远无法通过
# execve()获得新权限——即使执行了setuid程序也不行
就一行配置,效果却非常显著。强烈建议所有服务都加上。
CapabilityBoundingSet= 限制服务可使用的Linux能力集合:
[Service]
# 只允许绑定特权端口(如80/443),删除所有其他能力
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 完全清空能力集(最严格)
CapabilityBoundingSet=
常用的能力及其含义(这个列表值得收藏):
CAP_NET_BIND_SERVICE:绑定1024以下端口CAP_CHOWN:改变文件所有者CAP_SETUID/CAP_SETGID:切换用户/组身份CAP_DAC_OVERRIDE:绕过文件权限检查CAP_NET_RAW:创建原始套接字(ping等需要)CAP_SYS_ADMIN:几乎等同于root的"万能钥匙"——绝大多数服务都不该有这个
User= / Group= 以非root用户运行服务。坦白说,这往往比用root运行再通过 CapabilityBoundingSet 削减能力更安全、更简洁:
[Service]
User=www-data
Group=www-data
# 如果非root用户需要绑定特权端口,用AmbientCapabilities授予
AmbientCapabilities=CAP_NET_BIND_SERVICE
2.3 系统调用过滤(Seccomp)
系统调用过滤是另一道强有力的防线。通过限制服务可以调用的内核接口,你能进一步收紧安全边界。
SystemCallFilter= 使用seccomp过滤器限制系统调用:
[Service]
# 允许大多数标准服务需要的系统调用集
SystemCallFilter=@system-service
# 在此基础上,显式禁止危险的系统调用组
SystemCallFilter=~@mount @clock @reboot @swap @raw-io @debug
# 限制系统调用架构,防止通过32位兼容层绕过过滤
SystemCallArchitectures=native
systemd预定义了一些很方便的系统调用组,了解这些能帮你快速配置:
@system-service:适合大多数后台服务,包含常用调用,排除危险操作@mount:挂载相关调用——如果启用了文件系统隔离,务必禁止@clock:系统时钟操作@reboot:重启/关机相关@debug:调试相关(ptrace等)@privileged:需要特权的调用@raw-io:原始I/O操作
2.4 网络访问限制
对于不需要网络的服务,直接断网就完事了:
[Service]
# 完全禁止网络访问
PrivateNetwork=yes
# 或者只限制网络地址族
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# 或者使用IP地址白名单
IPAddressAllow=localhost 10.0.0.0/8
IPAddressDeny=any
2.5 内存与执行保护
这一组指令覆盖面比较广,每个都不复杂但加在一起效果很好:
[Service]
# 禁止同时可写可执行的内存区域(防止JIT攻击)
MemoryDenyWriteExecute=yes
# 限制可用的命名空间
RestrictNamespaces=yes
# 锁定进程执行域
LockPersonality=yes
# 禁止访问内核日志
ProtectKernelLogs=yes
# 禁止修改内核调优参数
ProtectKernelTunables=yes
# 禁止加载内核模块
ProtectKernelModules=yes
# 禁止修改控制组
ProtectControlGroups=yes
# 禁止创建SUID/SGID文件
RestrictSUIDSGID=yes
第三章:Drop-in覆盖文件——加固的正确姿势
3.1 为什么不能直接改服务文件
永远不要直接编辑 /usr/lib/systemd/system/ 下的原始服务文件。原因很简单:每次软件包更新,这些文件都会被覆盖,你辛辛苦苦写的加固配置全没了。(我早年吃过这个亏,升级一次Nginx配置就白做了。)
正确的做法是使用systemd的drop-in覆盖机制:
# 方法一:使用systemctl edit创建覆盖文件
sudo systemctl edit nginx.service
# 这会打开编辑器,保存后自动创建
# /etc/systemd/system/nginx.service.d/override.conf
# 方法二:手动创建覆盖文件
sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo nano /etc/systemd/system/nginx.service.d/hardening.conf
3.2 覆盖文件的优先级
systemd服务文件的加载优先级从低到高:
/usr/lib/systemd/system/——软件包安装的原始文件(最低优先级)/run/systemd/system/——运行时临时文件/etc/systemd/system/——管理员创建的覆盖文件(最高优先级)
覆盖文件中的指令会追加到(或替换)原始文件中的同名指令。改完之后别忘了重新加载:
# 重新加载systemd配置
sudo systemctl daemon-reload
# 重启服务使加固配置生效
sudo systemctl restart nginx.service
# 验证加固效果
sudo systemd-analyze security nginx.service
第四章:实战加固——Nginx、PostgreSQL与Redis
理论讲够了,下面进入实战环节。我会给出三个最常见服务的生产级加固配置,你可以直接拿去用(当然,根据你的实际环境微调一下路径和能力总是好的)。
4.1 Nginx Web服务器加固
Nginx是最常见的面向公网的服务之一,加固优先级自然最高。以下配置可以将典型的Nginx服务暴露评分从大约9.2降到2.5左右——效果还是相当明显的。
创建文件 /etc/systemd/system/nginx.service.d/hardening.conf:
[Service]
# === 文件系统隔离 ===
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ReadWritePaths=/var/log/nginx /var/cache/nginx /run/nginx
ReadOnlyPaths=/etc/nginx /etc/ssl
InaccessiblePaths=/root /boot /mnt /media
# === 权限限制 ===
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_KILL CAP_CHOWN CAP_DAC_OVERRIDE
AmbientCapabilities=CAP_NET_BIND_SERVICE
# === 系统调用过滤 ===
SystemCallFilter=@system-service
SystemCallFilter=~@mount @reboot @swap @clock @debug @raw-io
SystemCallFilter=~memfd_create
SystemCallArchitectures=native
# === 网络限制 ===
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# === 内存与执行保护 ===
MemoryDenyWriteExecute=yes
LockPersonality=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
RestrictNamespaces=yes
RestrictSUIDSGID=yes
ProtectHostname=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
应用配置并验证:
# 应用配置
sudo systemctl daemon-reload
sudo systemctl restart nginx
# 确认Nginx正常运行
curl -I http://localhost
# 检查加固后的安全评分
sudo systemd-analyze security nginx.service
# 预期结果:暴露评分从 ~9.2 降到 ~2.5
4.2 PostgreSQL数据库加固
数据库服务需要写入数据目录,但绝对不该访问大部分系统资源。PostgreSQL的加固有个坑需要特别注意——后面会说到。
创建文件 /etc/systemd/system/postgresql.service.d/hardening.conf:
[Service]
# === 文件系统隔离 ===
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
# PostgreSQL需要写入数据目录和日志
ReadWritePaths=/var/lib/postgresql /var/log/postgresql /var/run/postgresql
ReadOnlyPaths=/etc/postgresql
# === 权限限制 ===
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_CHOWN CAP_SETUID CAP_SETGID CAP_FOWNER CAP_DAC_OVERRIDE
RestrictSUIDSGID=yes
# === 系统调用过滤 ===
SystemCallFilter=@system-service
SystemCallFilter=~@mount @reboot @swap @debug
SystemCallArchitectures=native
# === 网络限制(仅允许TCP/Unix Socket) ===
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# === 内核与内存保护 ===
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
LockPersonality=yes
RestrictNamespaces=yes
ProtectHostname=yes
ProtectClock=yes
# === 注意:PostgreSQL使用共享内存,不能启用MemoryDenyWriteExecute ===
# MemoryDenyWriteExecute=yes # 不要启用!会导致PG崩溃
这里划重点:MemoryDenyWriteExecute=yes 千万不能用在PostgreSQL上。PG依赖动态共享内存(DSM)来实现并行查询和其他高级功能,启用这个选项会导致PG无法启动,或者在执行某些查询时直接崩掉。我见过不少人在这里踩坑,排查半天才发现是这个配置的问题。
4.3 Redis缓存服务加固
Redis相对简单,只需要网络访问和有限的文件系统写入。加固起来也比较顺畅。
创建文件 /etc/systemd/system/redis.service.d/hardening.conf:
[Service]
# === 文件系统隔离 ===
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ReadWritePaths=/var/lib/redis /var/log/redis /run/redis
ReadOnlyPaths=/etc/redis
# === 权限限制 ===
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_SETUID CAP_SETGID CAP_SYS_RESOURCE
RestrictSUIDSGID=yes
# === 系统调用过滤 ===
SystemCallFilter=@system-service
SystemCallFilter=~@mount @reboot @swap @clock @debug @raw-io
SystemCallArchitectures=native
# === 网络限制 ===
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# === 内核与内存保护 ===
MemoryDenyWriteExecute=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
LockPersonality=yes
RestrictNamespaces=yes
ProtectHostname=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid
顺便提一句,Redis通常需要 CAP_SYS_RESOURCE 来调整打开文件数限制(跟maxclients配置相关)。如果你已经在系统层面用ulimit设好了,这个能力其实可以去掉。
4.4 通用基线模板
如果你管的服务比较多,不想逐个分析,下面这个基线模板适用于大多数后台服务:
[Service]
# === 最小安全基线 ===
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
NoNewPrivileges=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
SystemCallFilter=@system-service
SystemCallArchitectures=native
# === 按需添加写入路径 ===
# ReadWritePaths=/var/lib/myservice /var/log/myservice /run/myservice
思路是先应用基线,启动服务,看日志。出错了再根据日志逐个放宽限制。这种"先锁紧再逐步放松"的策略比"默认全开再逐个限制"安全得多——毕竟你总不希望在加固的过程中漏掉什么。
第五章:SHH自动化加固工具
5.1 什么是SHH
手动加固确实有效,但面对大量服务时就有点力不从心了。这时候SHH(Systemd Hardening Helper)就派上用场了。这是安全研究公司Synacktiv用Rust开发的工具,能通过运行时剖析(runtime profiling)自动生成最优的systemd加固配置。
原理其实不复杂:先用strace跟踪服务在正常运行时实际用了哪些系统调用和资源,然后根据"最小权限原则"生成一套只允许已观察到的操作的加固配置。有点像是先偷偷观察服务的日常行为,再据此定制一个刚好够用的"笼子"。
5.2 安装与使用
# 前提条件:安装strace(≥6.4版本推荐)和Rust编译环境
sudo apt install strace
# 安装Rust(如果还没有)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 从源码安装SHH
cargo install systemd-hardening-helper
# 步骤1:开始服务剖析
# 服务会在strace跟踪下重启
sudo shh service start-profile nginx.service
# 此时正常使用服务一段时间
# 尽量覆盖各种功能和使用场景
# 例如:发送不同类型的HTTP请求、触发日志轮转等
# 步骤2:结束剖析并应用加固
# -a 标志表示自动应用生成的配置
sudo shh service finish-profile nginx.service -a
# 步骤3:验证服务正常运行
sudo systemctl status nginx.service
sudo systemd-analyze security nginx.service
5.3 SHH的局限性
SHH虽然好用,但有几个局限性你得清楚:
- 配置不可移植:生成的配置高度依赖运行环境(内核版本、系统库、发行版等),不能直接复制到其他机器上用
- 剖析覆盖率是个问题:生成的配置只反映剖析期间观察到的行为。如果某个功能在剖析时没被触发过,它在生产环境中就可能被阻止——所以剖析期间要尽量覆盖所有使用场景
- 有性能开销:strace跟踪期间服务性能会明显下降,别在高流量生产环境中直接搞,应该在预发布环境中进行
- 需要strace ≥6.4:旧版本可能无法正确捕获所有系统调用信息
第六章:systemd 257+新安全特性
6.1 PrivateUsers=managed模式
systemd 257引入了 PrivateUsers=managed 这个新选项,通过 systemd-nsresourced 自动为服务分配动态的UID/GID范围(65536个),实现了更强大的用户命名空间隔离。相比以前的 PrivateUsers=yes,managed模式在用户映射管理上灵活很多。
[Service]
# systemd 257+ 新特性
PrivateUsers=managed
# 自动获得独立的UID/GID范围
# 无需手动配置用户映射
6.2 cgroup v1淘汰与安全影响
从systemd 257起,cgroup v1(包括legacy和hybrid模式)已经被标记为过时,计划在v258中完全移除。所有现代安全加固都应该基于cgroup v2了。如果你的系统还在跑cgroup v1,是时候规划迁移了。cgroup v2提供了更安全的资源控制和更细粒度的权限管理,这对加固工作来说是实打实的好处。
6.3 RefreshOnReload凭证刷新
systemd 258将引入 RefreshOnReload= 选项,允许在服务reload时自动刷新凭证和扩展配置。对于需要定期轮换密钥或证书的安全敏感服务来说,这个功能还是挺值得期待的。
第七章:加固工作流最佳实践
7.1 渐进式加固策略
对生产服务做加固,急不得。建议遵循这个工作流:
- 审计现状:运行
systemd-analyze security获取所有服务的基线评分 - 确定优先级:先搞面向公网的高风险服务
- 预发布验证:在测试环境中应用加固配置,充分测试所有功能路径
- 逐步应用:先从低风险指令开始(ProtectHome、PrivateTmp),确认无误后再加更严格的限制
- 监控告警:加固后密切关注
journalctl -u service中的权限拒绝日志 - 定期复审:软件更新后重新跑安全审计,确认加固配置没被破坏或需要调整
7.2 故障排查思路
加固后服务启动不了或者表现异常?别慌,按这个流程排查:
# 1. 查看服务的详细错误日志
sudo journalctl -u nginx.service -n 50 --no-pager
# 2. 如果怀疑是seccomp过滤导致的问题
# 检查审计日志中的SECCOMP条目
sudo journalctl -k | grep SECCOMP
# 3. 使用strace手动跟踪服务启动过程
sudo strace -f -p $(systemctl show -p MainPID nginx.service --value) 2>&1 | head -100
# 4. 临时禁用加固配置进行对比测试
sudo systemctl revert nginx.service
sudo systemctl restart nginx.service
根据我的经验,最常见的几类问题和对应的解决方案:
- 服务无法写入日志/数据:检查
ReadWritePaths=是否包含了所有需要写入的目录 - 服务无法绑定端口:确认
CapabilityBoundingSet=里有CAP_NET_BIND_SERVICE - 服务崩溃或段错误:八成是
MemoryDenyWriteExecute=yes跟JIT编译或动态共享内存冲突了 - 网络连接失败:看看
RestrictAddressFamilies=是不是漏了需要的地址族
7.3 与其他安全机制的协同
systemd加固不是孤立的安全措施,它应该是纵深防御体系的一环。几个值得关注的组合:
- 与AppArmor/SELinux配合:systemd沙箱做进程级隔离,MAC框架做系统级强制访问控制。两者互补,并不冲突
- 与auditd配合:在启用systemd加固的同时配置auditd审计规则,可以记录被沙箱阻止的操作尝试,对安全分析很有帮助
- 与防火墙配合:systemd的
IPAddressAllow/IPAddressDeny做服务级网络限制,nftables/iptables做主机级网络过滤,各管一层 - 与CIS基线配合:CIS Benchmark中不少服务加固建议可以直接通过systemd指令实现
常见问题解答(FAQ)
systemd-analyze security评分能降到0吗?
理论上可以,但对大多数服务来说没什么意义。评分0意味着服务几乎不能做任何事——没有网络、没有文件系统写入、没有设备访问。实际目标应该是面向公网的服务降到3分以下,内部服务降到5分以下。关键不是追求分数,而是确保每个服务只拥有完成其功能所需的最小权限。
systemd加固会影响性能吗?
大多数指令对性能的影响可以忽略不计。文件系统隔离(ProtectSystem、ProtectHome)用的是内核挂载命名空间,几乎没有运行时开销。seccomp过滤在首次加载时有微小的设置成本,之后每次系统调用的检查开销极低(纳秒级别)。唯一可能有感知的是 PrivateUsers=,因为它需要做UID/GID映射转换,但一般场景下也不明显。
加固配置会在系统更新后丢失吗?
不会——只要你用了正确的drop-in覆盖文件方式。放在 /etc/systemd/system/service.d/ 目录下的覆盖文件不受软件包更新影响。但要注意,软件更新可能引入新功能,需要额外的系统调用或文件系统权限。所以每次大版本更新后重新测试一下服务是否正常,是个好习惯。
MemoryDenyWriteExecute为什么会让某些服务崩溃?
这个指令禁止进程创建同时可写和可执行的内存映射。有些应用偏偏需要这个能力:PostgreSQL用动态共享内存(DSM),部分Java应用依赖JIT编译器,Python某些C扩展也可能需要。如果服务用了这类特性,就不能开启这个选项。遇到段错误或"Operation not permitted"时,第一个怀疑对象就是它。
容器环境中怎么用systemd加固?
如果容器内跑的是systemd(比如systemd-nspawn或某些Docker镜像),加固指令可以正常用。但在Kubernetes之类的编排平台中,建议优先使用平台原生的安全机制(SecurityContext、Pod Security Standards),把systemd加固当补充手段。另外,PrivateUsers= 和 ProtectKernelModules= 在嵌套命名空间环境中可能会有兼容性问题,部署前最好测试一下。