Ccyriak.xyz

DNS 令牌的最小权限

目录

证书自动续期要改 DNS 记录,所以机器上必须放一个能改 DNS 的凭据。问题在于「能改 DNS」这个描述太粗——它可以是「能改我所有域名的一切记录」,也可以是「只能改这一个区域里的 TXT 记录」。这两者之间差的不是便利性,是一次事故的上限。

先想清楚它需要什么

DNS-01 验证的动作只有三件事:创建一条 _acme-challenge 的 TXT 记录、等它生效、删掉它。所以理想权限是:

  • 范围:单个区域,不是账号下所有域名
  • 动作:编辑 DNS 记录,不需要读账号、读账单、改 NS
  • 有效期:能设就设,到期重新生成

绝大多数 DNS 服务商都支持这种粒度的 API Token。以 Cloudflare 为例,创建时有三个下拉:权限、区域资源、可选的 IP 过滤。

text
Permissions   : Zone → DNS → Edit
Zone Resources: Include → Specific zone → example.com
Client IP     : (可选)填服务器出口 IP

注意不要选全局 API Key。那把钥匙能改账号下的一切,包括把整个域名的 NS 指到别处——一旦泄露,攻击者不需要你的服务器,直接接管域名解析。

验证它真的只有这么点权限

建完令牌别急着用,先探测一下边界。这个请求应该只返回一个区域:

bash
# 令牌从环境变量读,不要写在命令行里——它会进 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、属主是执行续期的用户、不进版本库。

bash
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 这件事要做两层

只靠自律不够,加一层机器检查更可靠。最省事的办法是在仓库根目录放一份白名单式的忽略规则,再装个提交前钩子扫一遍:

bash
# .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 记录」。前者意味着域名被接管、邮件被劫持、证书被冒签;后者最坏不过是一次续期失败,报错然后重试。