问题背景
某台 Linux 服务器上运行着一个内部证书申请工具。该工具通过 Web 页面发起 ACME 证书申请,并使用 DNS TXT 记录完成域名验证。
应用采用 Python 编写,通过 Gunicorn 运行,仅监听本机回环地址,再由 Web 服务器进行反向代理。
其目录结构大致如下:
/srv/example-cert-tool/ 应用代码和虚拟环境
/tmp/example-cert-tool/ 证书申请期间的临时工作目录
临时目录中可能出现:
config/
work/
logs/
challenges/
approvals/
download/
state.json
证书签发完成后,还可能短暂保存:
fullchain.pem
privkey.pem
certificate.zip
这些文件包含证书和私钥,因此不适合长期保存在服务器上。证书下载完成后,应尽快清除服务器副本。
故障现象
服务器重启后,证书工具无法启动。查看 systemd 状态时发现服务不断退出并自动重启:
Failed to set up mount namespacing:
/tmp/example-cert-tool: No such file or directory
Main process exited, status=226/NAMESPACE
由于服务配置了自动重启,短时间内重复失败,最终形成了持续的重启循环,累计重启次数达到数万次。
需要注意的是,此时 Gunicorn 和 Python 应用实际上还没有开始执行。
故障发生在 systemd 为服务建立挂载命名空间的阶段。
根本原因
服务的 unit 文件中包含类似配置:
ReadWritePaths=/srv/example-cert-tool/instance /tmp/example-cert-tool
这项配置用于限制服务可写入的位置,是一种合理的 systemd 安全加固措施。
问题在于:
/tmp/example-cert-tool
在服务启动前必须已经存在。
/tmp 本身是临时文件系统。系统重启后,其中的自定义目录可能被正常清除。当 systemd 尝试根据 ReadWritePaths= 建立服务的挂载命名空间时,发现目录不存在,于是直接返回:
226/NAMESPACE
因此,真正的问题并不是使用了 /tmp,而是:
服务依赖一个可能消失的临时目录,却没有建立可靠的自动创建机制。
为什么继续使用 /tmp
最初容易产生一种直觉:既然 /tmp 重启后会被清空,是不是应该将目录迁移到项目目录或持久化目录?
对于这个证书工具而言,并不需要。
该目录保存的是证书签发过程中的短期状态、验证文件、下载包和私钥副本。证书下载完成后,这些内容本来就应该尽快删除。
将其改到持久化目录反而可能带来新的问题:
- 私钥在服务器上长期残留;
- 证书下载包被备份系统意外收录;
- 运行数据与应用代码混在一起;
- 系统重启后仍保留已经失去用途的敏感文件;
- 应用漏洞可能读取历史任务留下的私钥。
因此,保留:
/tmp/example-cert-tool
是符合该工具用途的设计。
需要修复的只是目录创建和权限管理。
为什么不能只使用 ExecStartPre=mkdir
一种看似直接的解决方法是:
ExecStartPre=/usr/bin/mkdir -p /tmp/example-cert-tool
但在本次故障中,这种方式不可靠。
因为 systemd 会先处理:
ReadWritePaths=
BindPaths=
ReadOnlyPaths=
PrivateTmp=
等命名空间和沙箱配置,然后才执行普通的 ExecStartPre=。
当 /tmp/example-cert-tool 不存在时,服务会在运行 mkdir 之前就因 226/NAMESPACE 失败。
因此,需要一个独立于主服务的准备步骤,在主服务建立命名空间之前创建目录。
修复方案
最终采用两层机制:
- 使用
systemd-tmpfiles定义目录的所有者和权限; - 使用独立的 oneshot 服务,在主服务启动前执行 tmpfiles 规则。
一、定义临时目录
创建:
/etc/tmpfiles.d/example-cert-tool.conf
内容:
d /tmp/example-cert-tool 0700 appuser appgroup -
各字段含义如下:
d 创建目录
/tmp/example-cert-tool 目录路径
0700 仅服务用户可访问
appuser 所有者
appgroup 所属组
- 不设置按时间自动删除
这里没有设置自动过期时间,因为证书申请可能持续一段时间。过于激进的定时清理可能在任务尚未完成时删除工作文件。
目录中的证书和私钥应由应用在任务结束后主动清理。
二、创建准备服务
创建:
/etc/systemd/system/example-cert-tool-prepare.service
内容:
[Unit]
Description=Prepare temporary directory for certificate tool
Before=example-cert-tool.service
[Service]
Type=oneshot
ExecStart=/usr/bin/systemd-tmpfiles --create /etc/tmpfiles.d/example-cert-tool.conf
该服务只负责一件事:
确保 /tmp/example-cert-tool 存在,并具有正确权限。
它执行完成后显示为:
inactive (dead)
这是正常的 oneshot 行为,不代表执行失败。
三、让主服务依赖准备服务
在主服务的 [Unit] 段中加入:
Requires=example-cert-tool-prepare.service
After=network-online.target example-cert-tool-prepare.service
StartLimitIntervalSec=300
StartLimitBurst=5
这样,每次启动主服务之前,systemd 都会先运行目录准备服务。
启动限制表示:
300 秒内最多允许 5 次启动失败
即使以后再次出现配置错误,也不会无限高速重启。
四、设置应用临时目录
在 [Service] 段中加入:
Environment=TMPDIR=/tmp/example-cert-tool
Restart=on-failure
RestartSec=10s
其中:
Environment=TMPDIR=/tmp/example-cert-tool
确保 Python、Gunicorn及其调用的临时文件库使用指定目录。
Restart=on-failure
仅在异常退出时重启,不在正常停止后自动拉起。
RestartSec=10s
则避免失败后立即形成高频循环。
原来的安全限制继续保留:
ReadWritePaths=/srv/example-cert-tool/instance /tmp/example-cert-tool
不需要放宽服务权限,也不需要删除 systemd 沙箱配置。
修改后的工作流程
修复后,服务启动顺序变为:
启动主服务
↓
运行目录准备服务
↓
systemd-tmpfiles 创建临时目录
↓
设置所有者和 0700 权限
↓
systemd 建立主服务挂载命名空间
↓
启动 Gunicorn
↓
Web 服务器继续反向代理请求
即使服务器刚刚重启,或者临时目录被手动删除,主服务也能在启动时自动恢复。
验证方法
为了验证修复不是依赖已有目录,可以先停止服务,再删除整个临时目录:
systemctl stop example-cert-tool.service
rm -rf /tmp/example-cert-tool
随后只启动主服务:
systemctl start example-cert-tool.service
不提前执行 mkdir,也不手动调用 tmpfiles。
然后检查:
systemctl status example-cert-tool-prepare.service
systemctl status example-cert-tool.service
stat /tmp/example-cert-tool
预期结果:
准备服务执行成功
主服务为 active (running)
临时目录被自动重新创建
目录权限为 0700
目录所有者为应用服务用户
再检查监听端口和健康状态:
ss -lntp
curl http://127.0.0.1:<内部端口>/health
实际验收中:
健康检查返回 200
应用首页返回 200
Gunicorn 正常监听本机地址
最新日志中不再出现 226/NAMESPACE
最后观察服务重启次数:
systemctl show example-cert-tool.service \
-p NRestarts \
-p ActiveState \
-p SubState \
-p StartLimitHit
等待一段时间后再次检查,确认:
NRestarts 没有增加
ActiveState=active
SubState=running
StartLimitHit=no
关于临时证书和私钥
将工作目录放在 /tmp 并不意味着文件会在下载后立即自动消失。
tmpfiles.d 在本次方案中只负责:
- 创建目录;
- 设置目录所有者;
- 设置目录权限。
证书、私钥和下载包是否在任务结束后删除,仍取决于应用自身的清理逻辑。
对于一次性证书申请工具,推荐的使用方式是:
完成证书申请
↓
下载并确认文件完整
↓
尽快删除服务器上的任务和私钥副本
至少应保证:
临时目录权限:0700
私钥文件权限:0600
证书压缩包权限:0600
服务只监听本机回环地址
证书下载后尽快清理
系统重启导致 /tmp 内容消失,在这种使用场景下属于安全特性,而不是缺陷。
总结
本次故障并不是因为 /tmp 不适合作为证书工具的工作目录。
真正的问题是:
systemd 安全配置要求目录存在
+
系统重启清除了临时目录
+
服务没有在启动前重新创建目录
+
自动重启没有设置合理上限
最终方案没有迁移应用,没有放宽安全限制,也没有将敏感证书改为持久保存。
修复内容只有三项:
使用 tmpfiles.d 定义临时目录
使用独立 oneshot 服务在主服务前创建目录
增加合理的失败重启限制
修复后,即使临时目录在系统重启后消失,服务也能够自动创建所需目录并正常启动,同时继续保持临时证书和私钥不长期存储的安全设计。