网站装好之后,很多站长把域名解析、首页能打开就当完成,直到某天 Chrome 地址栏弹出“不安全”提示,或者搜索结果里展示的还是 http 链接,才发现 HTTPS 这一步漏了。现在免费证书的获取方式已经很成熟,Let's Encrypt 几乎零成本,真正麻烦的不是申请,而是续期——90 天有效期,手动续几次就会忘。这篇文章用 acme.sh 把签发、部署、自动续期一次配好,后面基本不用再管。
一、先搞清楚:免费 SSL 证书的现状
Let's Encrypt 是公益 CA,证书免费、有效期 90 天,支持单域名、多域名和泛域名。它的加密强度与付费 DV 证书没有区别,差别只在于没有商业赔付和人工客服。对绝大多数个人站、企业官网、博客来说完全够用。
有两点需要提前了解:一是国内云厂商的免费 DV 证书大多已经缩短到 3 个月,一年要手动换三四次,反而更麻烦;二是泛域名(*.example.com)必须走 DNS-01 验证,不能用文件验证。
另外,Let's Encrypt 近年一直在推进更短的有效期方案,所以“自动续期”不是可选项,而是必选项。
二、准备工作
一台有公网 IP 的 VPS(1 核 1G 足够),系统以 Debian 12 为例;域名已经解析到服务器 IP;安全组和系统防火墙放行 80、443 端口;Nginx 已经装好并能正常访问。三者缺一个,后面签发就会卡住。
三、安装 acme.sh
acme.sh 是纯 Shell 编写的 ACME 客户端,不依赖 Python,也不需要额外运行环境,一条命令就能装完:
curl https://get.acme.sh | sh -s email=you@example.com
安装完成后重新加载环境变量,确认版本:
source ~/.bashrc
acme.sh --version
acme.sh 默认使用 ZeroSSL 作为 CA,而 ZeroSSL 需要注册账户,偶尔会出现注册失败、签发卡住的情况。更稳妥的做法是切换回 Let's Encrypt:
acme.sh --set-default-ca --server letsencrypt
四、签发单域名证书(HTTP-01 验证)
HTTP-01 的原理是让 CA 访问 http://你的域名/.well-known/acme-challenge/ 下的验证文件,以此确认域名归属,所以 80 端口必须通。网站根目录在 /var/www/html 时:
acme.sh --issue -d example.com -d www.example.com -w /var/www/html
如果 80 端口被 Nginx 占用、又没有可写的网站目录,可以改用 standalone 模式,签发前临时停掉 Nginx:
systemctl stop nginx
acme.sh --issue -d example.com -d www.example.com --standalone
systemctl start nginx
签发成功后证书会放在 ~/.acme.sh/example.com/ 目录下,但不要把 Nginx 直接指向这里。正确做法是用 install-cert 复制到固定目录,否则重装 acme.sh 后路径会失效。
五、签发泛域名证书(DNS-01 验证)
泛域名只能走 DNS 验证。acme.sh 内置了各家 DNS 服务商的 API,以阿里云为例,先在 RAM 里创建一个只拥有云解析权限的子账号,拿到 AccessKey:
export Ali_Key="你的AccessKeyId"
export Ali_Secret="你的AccessKeySecret"
然后一条命令签发,注意 example.com 和 *.example.com 要分别写,泛域名不包含主域名本身:
acme.sh --issue --dns dns_ali -d example.com -d *.example.com
腾讯云把 dns_ali 换成 dns_dp,Cloudflare 换成 dns_cf 并设置 CF_Token。AccessKey 会被 acme.sh 保存到 ~/.acme.sh/account.conf,之后自动续期直接复用,不用再手动输入。建议这个子账号只给云解析权限,别用主账号 AccessKey。
六、把证书装到 Nginx 并开启 HTTPS
先建一个固定目录,再用 install-cert 把证书装过去,同时登记 reload 命令——这一步很关键,续期后能不能自动生效全靠它:
mkdir -p /etc/nginx/ssl/example.com
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.com/key.pem \
--fullchain-file /etc/nginx/ssl/example.com/fullchain.pem \
--reloadcmd "systemctl reload nginx"
Nginx 配置分两段:80 端口全部 301 到 HTTPS,443 端口加载证书。
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com/key.pem;
root /var/www/html;
index index.php index.html;
location / { try_files $uri $uri/ /index.php?$args; }
}
检查语法并重载,再用 curl 看返回:
nginx -t && systemctl reload nginx
curl -I https://example.com
返回 200 且证书链正常,就说明 HTTPS 已经生效。如果浏览器报错,先看证书是不是只装了 cert.pem 而没有装 fullchain.pem。
七、自动续期到底靠不靠谱
acme.sh 安装时会自动往 crontab 写一条任务,每天检查一次,证书剩余 30 天以内就自动续期,续期成功后执行 install-cert 时登记的 reloadcmd。先确认任务是否存在:
crontab -l | grep acme
想验证整条续期链路,可以强制续一次(每月别超过 5 次,避免触发 CA 限流):
acme.sh --renew -d example.com --force
续期日志在 ~/.acme.sh/acme.sh.log,出问题先看这里。另外建议再加一层兜底:用 Uptime Kuma、监控宝或自己的脚本检查证书到期时间,剩余天数小于 15 天就告警,避免 cron 挂了却没人知道。
八、HTTPS 之后必须做的三件事
1. 修复混合内容。页面里只要还有 http:// 的图片、JS、CSS,浏览器就会判定“部分不安全”,锁标变灰。把站内资源改成 https:// 或协议相对的 //,数据库里的旧地址可以 SQL 批量替换。
2. 开 HSTS。在 443 的 server 块里加:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
第一次上线建议先用 300 秒的 max-age 试跑,确认全站没有 http 资源再改成一年,否则回退很麻烦。
3. 全站统一到 https。80 跳 443 已经解决大部分问题,但还要检查网站后台的站点地址、CDN 回源协议、sitemap 里的链接是否都是 https。搜索引擎会把 http 和 https 视为两个地址,重复内容会分散权重。建议重新提交 https 的 sitemap,并保留 301 至少半年。
九、常见问题排查
签发报 403 或 timeout:九成是 80 端口没放行,或者域名开着 CDN 代理,CA 访问到的是 CDN 节点而不是源站。先把 CDN 切成“仅 DNS”再签。
提示证书链不完整:Nginx 里必须用 fullchain.pem,只配 cert.pem 会导致部分安卓机和微信内置浏览器报错。
续期成功但浏览器还是旧证书:检查 reloadcmd 是否执行,或者 Nginx 配置里的证书路径是不是还指着旧目录。
泛域名不生效:*.example.com 不包含 example.com 本身,两个都要写进 -d 参数。
acme.sh 卡在注册 ZeroSSL:直接执行 --set-default-ca --server letsencrypt 切回 Let's Encrypt。
十、总结
SSL 证书这件事,签发只要几分钟,难的是让它一直有效。用 acme.sh 把验证方式、安装路径、reload 命令一次性配好,后面就是全自动。真正需要人工关注的只有两点:DNS API 的密钥别过期,证书监控别失联。把这两件事做好,HTTPS 就可以从待办清单里彻底划掉了。