在上一篇里,Marinara 跑在 127.0.0.1:7860,通过 Pingap 用 http://IP 对外提供访问。这样能用,但有一个难受的地方:Basic Auth 的账号密码是在明文 HTTP 里传的。所以目标很明确:给它配上 https://marinara.icyyan.com。
要做的事情拆开来就两件:证书从哪来,证书怎么挂到反向代理上。好消息是这台服务器上已经有两个站点(cloudside.icyyan.com、silly.icyyan.com)跑着 HTTPS,整套体系是现成的——新站点照抄就行。这篇文章把整个过程走一遍,包括中间踩掉的几个坑。
1. 先看现成的体系长什么样
照抄之前先弄清楚抄的是什么。服务器入口是 Pingap,HTTPS 这一块的结构是:
- 443 端口只监听一次,靠 SNI 分流。SNI 是 TLS 握手时客户端告诉服务器「我要访问哪个域名」,服务器按这个域名选对应的证书、再按域名找到对应的 location 转发。所以多个 HTTPS 站点可以共用同一个 443。
- 每个域名一张 Let’s Encrypt 证书,统一放在
/etc/letsencrypt/live/<域名>/下,都是 certbot 签的。 - 签证书用的验证方式是
dns-aliyun插件,这是整个过程里最关键的一环,值得单独说。
2. 为什么用 DNS-01 挑战
CA 签证书之前,必须先确认「你确实控制这个域名」,这个确认动作叫挑战(challenge)。最常见的 HTTP-01 挑战是让 CA 从公网访问 http://<域名>/.well-known/... 下的一个文件——这要求域名已经解析到服务器、80 端口可用。
DNS-01 换了一条路:只要能在这个域名的 DNS 里写下一条指定的 TXT 记录,就证明你控制了整个域。阿里云的 DNS 有 API,certbot 的 dns-aliyun 插件会自动完成「写 TXT 记录 → 等它生效 → 让 CA 来查 → 删掉记录」的全过程,人不用插手。
对这个场景它有三个实打实的好处:
- 不需要 A 记录先存在。
marinara.icyyan.com还没做解析,证书照样能签——验证查的是 TXT 记录,和网站指向哪无关。这意味着证书可以先备好,解析后补。 - 不占 80 端口。服务器上 80 已经被 Pingap 占着做跳转,不用折腾。
- 以后能签泛域名。
*.icyyan.com这种证书只能用 DNS-01 签,想偷懒一劳永逸的话有这条路。
插件用的阿里云 AccessKey 存在 /etc/letsencrypt/aliyun/credentials.ini,注意这个文件不能外泄。
3. 签发:一条命令,两个坑
照着 silly 的续期配置(/etc/letsencrypt/renewal/ 里能看到当初签发的完整参数),签发命令是:
sudo /usr/local/bin/certbot certonly -n -a dns-aliyun \
--dns-aliyun-credentials /etc/letsencrypt/aliyun/credentials.ini \
--dns-aliyun-propagation-seconds 60 \
--key-type rsa -d marinara.icyyan.com
--dns-aliyun-propagation-seconds 60 是写完 TXT 记录后等它在 DNS 里生效的时间,所以这条命令要跑一分多钟,不是卡住了。看着简单,但直接照抄会踩两个坑。
坑一:sudo certbot 和 certbot 不是同一个程序。 直接跑会报 unrecognized arguments: --dns-aliyun ...,像是插件没装。实际上服务器上有两个 certbot:系统自带的旧版 /usr/bin/certbot(没有 aliyun 插件),和装在 /usr/local/bin/certbot 的新版(插件齐全)。日常 shell 里 PATH 把 /usr/local/bin 排在前面,所以 certbot plugins 能看到插件;但 sudo 的 secure_path 不含 /usr/local/bin,于是找到的是旧版。确认方法是在 sudo 下也跑一次 plugins 对比。教训:sudo 跑 certbot 一律写全路径。
坑二:--dns-aliyun 不能用来选插件。 写成 --dns-aliyun 会报 ambiguous option,因为它是 --dns-aliyun-credentials、--dns-aliyun-propagation-seconds 的公共前缀。选认证器要用 -a dns-aliyun(或 --authenticator dns-aliyun)。
跑完产物就在约定的位置:
/etc/letsencrypt/live/marinara.icyyan.com/
├── cert.pem
├── chain.pem
├── fullchain.pem
└── privkey.pem
4. 挂到 Pingap:三个文件
Pingap 的配置在 /etc/pingap/,改动就三处。先是 certificates.toml 注册证书:
[certificates.marinara]
domains = "marinara.icyyan.com"
tls_cert = "/etc/letsencrypt/live/marinara.icyyan.com/fullchain.pem"
tls_key = "/etc/letsencrypt/live/marinara.icyyan.com/privkey.pem"
再是 locations.toml 加转发规则(upstream marinara 指 127.0.0.1:7860,之前已存在):
[locations.marinaraHttps]
host = "marinara.icyyan.com"
upstream = "marinara"
client_max_body_size = "100mb"
enable_reverse_proxy_headers = true
最后把它挂进 servers.toml 的 443 server:
[servers.https]
addr = "0.0.0.0:443"
locations = ["cloudsideHttps", "sillyHttps", "marinaraHttps"]
global_certificates = true
enabled_h2 = true
global_certificates = true 就是让 Pingap 按 SNI 自动挑证书的开关,不需要把证书和 location 手动绑在一起。
改完先校验再生效:
sudo /usr/local/bin/pingap -c /etc/pingap -t # 输出 Validate config success
sudo systemctl restart pingap
5. 大坑:不要用 systemctl reload pingap
这里单独用一节说,因为它把全部站点搞挂了一次。
pingap.service 这个 systemd unit 里写着 ExecReload=/bin/kill -HUP $MAINPID,看起来 SIGHUP 是「平滑重载配置」。实际效果是:进程收到 SIGHUP 直接退出,systemctl is-active 变成 inactive,所有站点全部下线。好在 systemctl start pingap 立刻拉了回来(unit 的 ExecStartPre 会先校验配置,有问题起不来,也算一层保护)。
所以在这台机器上的安全操作是:改完配置先 pingap -t 校验,然后 systemctl restart pingap。一秒以内的中断,个人站点可以接受。ExecReload 这个雷先记在这里,回头把它修成优雅重启或者干脆删掉。
6. 应用层:origin 白名单和容器重建
代理通了还不算完。Marinara 自己会校验浏览器来源:origin 不在 CSRF_TRUSTED_ORIGINS / CORS_ORIGINS 白名单里的请求会被拒。所以在 /opt/marinara/.env 里把新 origin 加上:
CSRF_TRUSTED_ORIGINS=https://marinara.icyyan.com
CORS_ORIGINS=https://marinara.icyyan.com
多个值用英文逗号分隔。改完 .env 要注意一个细节:它是通过 compose 的 env_file 在创建容器时注入的,docker compose restart 不会重新读。正确姿势是:
cd /opt/marinara && docker compose up -d
compose 发现环境变了会自动重建容器。应用启动要十几秒,刚起来时 curl 不通别慌,等一下再试。看到 401 Unauthorized 反而是好消息——说明容器活着,且 Basic Auth 在正常工作。
7. DNS 还没配,怎么验证
签证书不需要 A 记录,但浏览器访问需要。在加解析之前,可以先用 curl --resolve 把整条链路验掉:
curl -I --resolve marinara.icyyan.com:443:127.0.0.1 https://marinara.icyyan.com/
--resolve 跳过 DNS 查询、强制把域名解析到指定地址,SNI 用的还是真实域名,所以 TLS 握手、证书匹配、反代转发全是真实路径。返回 401 就说明「证书 → Pingap → 容器」整条链通了。
再看一眼证书本身:
echo | openssl s_client -connect 127.0.0.1:443 -servername marinara.icyyan.com 2>/dev/null \
| openssl x509 -noout -issuer -subject -enddate
签发者是 Let’s Encrypt、subject 是 marinara.icyyan.com、有效期 90 天,都对得上。这之后才是去阿里云控制台加一条 A 记录:marinara 指向服务器公网 IP,等解析生效就能用 https://marinara.icyyan.com 访问了。
8. 续期:自动是自动,但要确认钩子真的在
证书续期不用操心:certbot-renew.timer 会定期跑 certbot renew,到期前 30 天用同样的 dns-aliyun 方式自动续。续完以后 Pingap 要重新加载才能用上新证书,这个「续期后动作」就是容易有尾巴的地方。
钩子有两个可以放的位置,检查时两个都要看:
/etc/sysconfig/certbot里的DEPLOY_HOOK(全局生效)/etc/letsencrypt/renewal-hooks/deploy/目录下的可执行脚本(每次续期成功后逐个执行)
我一开始只看了前者,差点得出「没有钩子」的结论;实际上后者的目录里已经躺着一个重启脚本。certbot 的 renew 钩子机制藏在这个目录里,不看文档很容易漏掉。最终落下的钩子很朴素:
#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/restart-pingap.sh
systemctl restart pingap
注意别学 systemd unit 里那样用 SIGHUP(前面第 5 节的坑),直接 restart。写完 chmod 755,再手动执行一次验证 Pingap 能正常回来,这个钩子才算真的落地。
另外一个顺手的全面体检:sudo /usr/local/bin/certbot renew --dry-run 会对着 Let’s Encrypt 的 staging 环境把每张证书的续期流程完整跑一遍(真的会走阿里云 API 建删 TXT 记录),三张证书全部通过,才算确认自动续期没有暗伤。
收尾
整体回顾一遍,给已有服务补 HTTPS 的完整路径是:确认现有体系(SNI + certbot)→ DNS-01 签证书(不占 80、不等解析)→ Pingap 三处配置 → 应用层 origin 白名单 → --resolve 提前验证 → 最后才加 DNS A 记录。坑主要在人机交界的地方:sudo 的 PATH、systemd 的 reload 语义、容器环境变量的注入时机,每一个都值得亲自试一次再写进肌肉记忆。
HTTPS 通了之后,locations.toml 里那个 http://IP 的明文入口(marinaraIpHttp)就没有存在必要了,建议删掉,让所有访问都走加密通道。