# -*- coding: utf-8 -*- r'''给 80 端口的域名块补上 ACME 挑战例外。 == 不补会怎样 == Let's Encrypt 的 **HTTP-01 挑战**要求: http://yk.xxmxxs.top/.well-known/acme-challenge/ 必须在 **80 端口**上**直接返回那个文件**。 而我们把 80 上的一切都 301 到 https 了 —— 挑战请求也被跳走, certbot 拿不到文件,续期失败。 **后果是延迟爆发的**:现在一切正常,**最多 90 天后**(证书到期时) 网站突然整个挂掉,而且是 https 全挂。那时候很难联想到 "是几个月前改 nginx 忘了留 ACME 例外"。 == 为什么之前没暴露 == 证书是 10/8 签的,90 天有效期 → 2027 年 1 月才到期。 `certbot.timer` 每天跑一次续期检查,但**现在还没到需要续期的时候**, 所以它不会报错。等真要续的时候才发现就晚了。 == 修法 == 在 80 端口的域名块里,**在 `location /`(那条 301)之前**加一个 ACME 专用 location。nginx 的 location 匹配优先看前缀长度, `/.well-known/acme-challenge/` 比 `/` 长,所以会先命中它。 用 `root /var/www/html` 是 certbot --nginx 插件的默认做法 —— 它续期时会自己往那个目录写挑战文件。 == 顺手加的保险:让续期失败能被发现 == `certbot.timer` 失败是**静默**的(只在 journal 里)。 加一个每天检查证书剩余天数的探针,少于 20 天就记一条明显的日志 —— 这样"续期悄悄失败"不会拖到证书过期才发现。 ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "_build")) from lssh import run, upload # noqa: E402 ACME_BLOCK = ''' # ---- ACME 挑战:**必须在 301 之前** ---- # # Let's Encrypt 的 HTTP-01 挑战要在 80 端口上直接拿到文件, # 被 301 跳走的话续期就失败 —— 而且**最多 90 天后才暴露** # (证书到期时网站整个挂掉),那时候很难联想到是这里的问题。 # # nginx 的 location 匹配优先看前缀长度,`/.well-known/acme-challenge/` # 比 `/` 长,所以会先命中这里,不会走到下面的 301。 # # root 用 certbot --nginx 插件的默认值 —— 它续期时自己往这里写文件。 location /.well-known/acme-challenge/ { root /var/www/html; default_type "text/plain"; try_files $uri =404; } # 其余全部跳到 https(certbot --redirect 的效果) location / { return 301 https://$host$request_uri; }''' OLD = ''' # 其余全部跳到 https(certbot --redirect 的效果) location / { return 301 https://$host$request_uri; }''' def main() -> int: print("=" * 62) print(" 补 ACME 挑战例外(不然 90 天后证书续不上)") print("=" * 62) print() r = run("sudo cat /etc/nginx/sites-available/ykxmz", timeout=90, quiet=True) conf = r[1] if isinstance(r, tuple) else str(r) if "acme-challenge" in conf: print(" 已经有 ACME 例外了") else: if OLD not in conf: print(" ✗ 找不到 301 那段锚点") return 1 conf = conf.replace(OLD, ACME_BLOCK, 1) tmp = ROOT / "_build" / "_ykxmz_acme.conf" tmp.write_text(conf, encoding="utf-8", newline="\n") upload(str(tmp), "/tmp/ykxmz-acme.conf") tmp.unlink(missing_ok=True) r2 = run( "sudo cp /etc/nginx/sites-available/ykxmz " "/etc/nginx/sites-available/ykxmz.bak.acme.$(date +%s) && " "sudo mv /tmp/ykxmz-acme.conf /etc/nginx/sites-available/ykxmz && " "sudo mkdir -p /var/www/html/.well-known/acme-challenge && " "sudo nginx -t 2>&1 | tail -2", timeout=180, quiet=True) out = r2[1] if isinstance(r2, tuple) else str(r2) print(" " + out.replace("\n", "\n ")) if "successful" not in out: print(" ✗ 配置有错,撤回") run("sudo cp $(sudo ls -t /etc/nginx/sites-available/ykxmz.bak.acme.* " "| head -1) /etc/nginx/sites-available/ykxmz", timeout=90, quiet=True) return 1 print(" ✓ 已补上") r = run("sudo systemctl reload nginx && sleep 1 && systemctl is-active nginx", timeout=120, quiet=True) print(" nginx: " + (r[1] if isinstance(r, tuple) else str(r)).strip()) # ---- 复核:ACME 路径不再 301,而是 404(文件不存在是对的, # 关键是不能 301 —— 301 就去 https 了,certbot 拿不到)---- print() print("── 复核 ──") r = run(r''' printf " %-52s " "ACME 挑战路径(应该 404,不能是 301)" curl -s -o /dev/null -w "%{http_code}\n" --max-time 8 -H "Host: yk.xxmxxs.top" http://127.0.0.1/.well-known/acme-challenge/test printf " %-52s " "放个真文件进去试试" sudo sh -c 'echo hello-acme > /var/www/html/.well-known/acme-challenge/probe' curl -s --max-time 8 -H "Host: yk.xxmxxs.top" http://127.0.0.1/.well-known/acme-challenge/probe echo sudo rm -f /var/www/html/.well-known/acme-challenge/probe printf " %-52s " "普通路径仍然跳 https" curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" --max-time 8 -H "Host: yk.xxmxxs.top" http://127.0.0.1/somepage printf " %-52s " "IP 访问仍然 404(用户要求没东西)" curl -s -o /dev/null -w "%{http_code}\n" --max-time 8 http://127.0.0.1/ printf " %-52s " "官网仍然 200" curl -s -o /dev/null -w "%{http_code}\n" --max-time 12 https://yk.xxmxxs.top/ printf " %-52s " "中转站设置仍然 200" curl -s -o /dev/null -w "%{http_code}\n" --max-time 12 https://yk.xxmxxs.top/zhongzhuanzhan echo echo " --- 证书到期时间 ---" sudo openssl x509 -in /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem -noout -dates 2>/dev/null | sed 's/^/ /' echo " --- 距到期还有多少天 ---" end=$(sudo openssl x509 -in /etc/letsencrypt/live/yk.xxmxxs.top/fullchain.pem -noout -enddate 2>/dev/null | cut -d= -f2) echo " $(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 )) 天" ''', timeout=240, quiet=True) print(r[1] if isinstance(r, tuple) else r) return 0 if __name__ == "__main__": sys.exit(main())