downuuship

acme.sh 换用 webroot 模式后踩到的权限坑

2026-10-14

acme.sh 默认可以用 standalone 模式签证书:临时起一个 80 端口的服务完成验证,签完就关。但这要求 80 端口当时是空闲的,如果机器上已经有 Nginx,就得先停掉,很别扭。

webroot 模式更省事:把验证文件写进站点根目录,让现有的 Nginx 去提供服务。

acme.sh --issue --webroot /var/www/site -d example.com --keylength ec-256

坑一:验证目录必须真的能被访问到

acme.sh 会把文件写到 /var/www/site/.well-known/acme-challenge/。如果你的 Nginx 里给这个域名写的是:

location / { return 301 https://$host$request_uri; }

那验证请求也会被 301 到 https,而 Let's Encrypt 只走 http。正确写法是把 challenge 路径单独拿出来,放在跳转之前:

location /.well-known/acme-challenge/ {
    root /var/www/site;
    default_type text/plain;
}
location / { return 301 https://$host$request_uri; }

Nginx 的 location 匹配里,^~ 和精确匹配 = 优先于普通前缀匹配,所以这种写法是安全的,不依赖书写顺序。

坑二:目录权限

如果站点目录是 root:root 755,而 Nginx worker 以 www-data 或 nginx 身份运行,读 .well-known 下面的文件是没问题的(文件默认 644)。

但如果这一层里有一级目录是 700,worker 就进不去,返回 403。验证失败时先看 Nginx 的 error.log,里面会明确写 Permission denied 还是 No such file。

坑三:重载后立刻测

改完配置要等 reload 真正完成再测。快速验证:

echo test > /var/www/site/.well-known/acme-challenge/t.txt
curl -H "Host: example.com" http://127.0.0.1/.well-known/acme-challenge/t.txt

返回 test 才算通。很多人在这里拿到 404,其实是 reload 还没生效就去测了。

续期

签完之后用 --install-cert 把证书装到固定路径,并指定 reload 命令。acme.sh 的 cron 每天跑一次,到期前一个月会自动续。