DNS 令牌的最小权限
证书自动续期要改 DNS 记录,所以机器上必须放一个能改 DNS 的凭据。问题在于「能改 DNS」这个描述太粗——它可以是「能改我所有域名的一切记录」,也可以是「只能改这一个区域里的 TXT 记录」。这两者之间差的不是便利性,是一次事故的上限。
先想清楚它需要什么
DNS-01 验证的动作只有三件事:创建一条 _acme-challenge 的 TXT 记录、等它生效、删掉它。所以理想权限是:
- 范围:单个区域,不是账号下所有域名
- 动作:编辑 DNS 记录,不需要读账号、读账单、改 NS
- 有效期:能设就设,到期重新生成
绝大多数 DNS 服务商都支持这种粒度的 API Token。以 Cloudflare 为例,创建时有三个下拉:权限、区域资源、可选的 IP 过滤。
Permissions : Zone → DNS → Edit
Zone Resources: Include → Specific zone → example.com
Client IP : (可选)填服务器出口 IP
注意不要选全局 API Key。那把钥匙能改账号下的一切,包括把整个域名的 NS 指到别处——一旦泄露,攻击者不需要你的服务器,直接接管域名解析。
验证它真的只有这么点权限
建完令牌别急着用,先探测一下边界。这个请求应该只返回一个区域:
# 令牌从环境变量读,不要写在命令行里——它会进 shell 历史
export CF_TOKEN=$(cat /root/.secrets/cf-dns.token)
curl -s -H "Authorization: Bearer $CF_TOKEN" \
"https://api.cloudflare.com/client/v4/zones" \
| python3 -c 'import sys,json; print([z["name"] for z in json.load(sys.stdin)["result"]])'
如果输出里出现了第二个域名,说明 Zone Resources 那一步选成了 All zones,回去改。
别用 curl -v 或 set -x 调试这类请求。详细模式会把 Authorization 头原样打到终端和日志里,一次调试就能让令牌进入 shell scrollback、syslog 和你的截图。
令牌放在哪
三个原则:文件权限 600、属主是执行续期的用户、不进版本库。
install -d -m 700 /root/.secrets
printf '%s' 'REDACTED' > /root/.secrets/cf-dns.token
chmod 600 /root/.secrets/cf-dns.token
chown root:root /root/.secrets/cf-dns.token
acme.sh 用 Cloudflare 插件时读的是环境变量 CF_Token。它会把用过的变量写进 ~/.acme.sh/account.conf,那个文件的权限本身就是 600,可以接受——但要记得它也是明文存储,备份机器镜像时会被一起带走。
不进 Git 这件事要做两层
只靠自律不够,加一层机器检查更可靠。最省事的办法是在仓库根目录放一份白名单式的忽略规则,再装个提交前钩子扫一遍:
# .git/hooks/pre-commit
#!/bin/sh
if git diff --cached | grep -qEi \
'CF_Token|api[_-]?key|BEGIN [A-Z ]*PRIVATE KEY|Bearer [A-Za-z0-9_-]{20,}'; then
echo "疑似凭据,提交已阻止" >&2
exit 1
fi
它不完美,会漏掉新格式的令牌,但能挡住 90% 的手滑。要更严就上专门的扫描工具。
轮换比保管更重要
无论多小心,令牌总有暴露的可能:日志、备份、某次 ps 的输出、同事的截图。所以真正的防线是能快速换掉它。
- 给令牌起一个能看出用途的名字,比如
acme-renew-2026 - 记录它在哪台机器、哪个文件里,换的时候不会漏
- 换掉之后立刻吊销旧的,别留着「以防万一」
- 一次一令牌,别在多台机器上共用一个
如果服务商支持设置过期时间,就设成略长于最长的续期周期,比如一年。到期强制重来一次,比「永远有效」安全得多。
小结
权限收敛这件事的收益不是「更安全」这种抽象说法,而是把最坏情况的损失从「整个域名」压到「一条 TXT 记录」。前者意味着域名被接管、邮件被劫持、证书被冒签;后者最坏不过是一次续期失败,报错然后重试。