问题背景

某台 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 失败。

因此,需要一个独立于主服务的准备步骤,在主服务建立命名空间之前创建目录。

修复方案

最终采用两层机制:

  1. 使用 systemd-tmpfiles 定义目录的所有者和权限;
  2. 使用独立的 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 服务在主服务前创建目录
增加合理的失败重启限制

修复后,即使临时目录在系统重启后消失,服务也能够自动创建所需目录并正常启动,同时继续保持临时证书和私钥不长期存储的安全设计。

Leave a Reply

Your email address will not be published. Required fields are marked *