
上个月帮朋友维护一台老服务器网站挂了两周没人发现打开浏览器直接红字警告您的连接不是私密连接。查了下原因很简单证书过期了。问了一圈发现不少站长还在为SSL证书付费一年几百上千续费时还要填工单、等审核。其实免费SSL证书申请工具这个方向早就成熟到可以全程无人值守了问题在于大部分人卡在选型这一步工具那么多哪个靠谱申请完怎么替换替换完为什么不生效这篇文章就把我从选工具到踩坑、再到三台服务器全部自动化续期的完整过程拆开讲内容覆盖免费证书和付费证书的真实差距、主流申请工具的横向对比、acme.sh签发泛域名证书的实战流程以及Nginx替换证书不生效的完整排查链路。不管是第一次建站的新手还是被证书续期折磨过的老运维这篇文章都能让你少走弯路。1. 免费证书的真实力便宜不等于不可靠但有三个前提要认清1.1 免费证书和付费证书差在哪里很多人的第一反应是免费的东西肯定不如付费的稳。这句话放在别处可能成立但在SSL证书这里并不成立。证书的本质是证明这个域名是你的通信是加密的这一点上免费证书和付费证书用的是同一套技术栈公钥加密、CA数字签名、证书链校验。浏览器验证证书时不会因为你的证书是Lets Encrypt签发的就降低校验标准。真正有区别的是三点。第一验证级别。免费证书基本全是DV域名验证只验证你对域名的控制权。付费证书里有OV组织验证和EV扩展验证会核实企业主体身份浏览器地址栏会显示公司名。个人博客、SaaS后台、API服务用DV完全够用因为DV和OV在加密强度上没有差别差的只是浏览器里那个绿锁和企业名称的展示效果。第二服务保障。商业证书通常附带证书保险出了问题有赔付流程免费证书没有。对绝大多数网站来说这类保障基本用不上真正能救你的还是可靠的续期机制和日常监控。第三有效期策略。这是最容易忽略的一点。免费证书有效期通常只有90天或像阿里云免费证书那样几个月付费证书可以买一年甚至更长。听起来免费证书很麻烦但这反而是现代CA体系的设计核心——短有效期逼迫你自动化长有效期反而会放大运维惰性。1.2 长久免费SSL的本质把90天当成设计而不是缺陷如果你在搜索引擎里搜长久免费SSL会看到各种永久免费证书的说法。我的观点是没有任何证书是真正永久的付费证书到期后忘了续费照样挂免费证书只要自动化到位反而能做到实际上的长久免费。Lets Encrypt把证书有效期定为90天理由很明确私钥从泄露到吊销的时间窗口被压缩安全上限更高短周期让吊销列表CRL和OCSP的压力变小最关键的是90天强制你养成自动化习惯变相消灭部署后不管的传统运维方式。所以判断一个免费证书方案能不能长久不要看它是90天还是几个月要看它是否支持全自动续期。手动续期的免费证书无论有效期多长都是隐患自动续期的免费证书哪怕只有90天也比你手动续费一年的付费证书更安全。这个认知决定了后面所有工具选型的方向。2. 工具选型对比Certbot、acme.sh、阿里云免费证书到底用哪个2.1 主流方案的真实差异当前主流的免费SSL申请工具我实际用过或深度测试过的大致四类Certbot、acme.sh、阿里云免费证书、ZeroSSL。给一个对比表方案客户端形态证书有效期泛域名支持自动续期最适合谁CertbotPython/系统包90天支持需自行配置喜欢官方推荐、新系统acme.sh纯Shell脚本90天支持开箱即用老系统、多服务器、想省事阿里云免费证书控制台/API较短以控制台为准不支持控制台续期操作域名在阿里云、不想碰命令行ZeroSSLWeb后台/CLI90天支持后台可管理想要可视化后台的团队先说Certbot。它是EFF官方推荐的客户端生态成熟文档多Ubuntu上一条apt install certbot就能装。缺点是对Python环境有依赖老服务器比如CentOS 6或者一些精简的ARM设备装起来麻烦而且默认的webroot验证对目录权限要求严格配置稍微错一点就签发失败。acme.sh是我用了三年的方案。它是个纯Shell脚本任何有bash的机器都能跑不依赖Python连路由器上的Entware环境都能用。最大的杀手锏是内置了DNS API插件支持Cloudflare、阿里云等几十家DNS服务商签发泛域名证书时自动添加和删除TXT记录整个流程不用手动碰DNS控制台。阿里云免费证书适合的是不想折腾命令行的人。在控制台申请下载nginx格式的证书包手动上传到服务器改nginx配置。缺点是只支持单域名不带www也不支持泛域名有效期比较短需要定期手动替换。它的自动续期入口和规则也调整过几次具体以控制台页面为准别拿旧教程硬套。ZeroSSL是后起之秀网页后台可以看证书列表、下载、配置自动续期API也开放。但免费额度规则偶尔变动而且新版acme.sh默认对接的是ZeroSSL如果没有注册账号并完成邮箱验证签发会被卡住。这也是很多人装了acme.sh却签发失败的原因之一。2.2 我为什么最终选了acme.sh DNS API我的实际场景是三台服务器、十几个域名其中三个还是泛域名。如果全用Certbot手动webroot验证每次续期都要确认目录如果全用阿里云免费证书泛域名直接没法用单域名几个月换一次能累死。acme.sh DNS API的组合正好命中我所有需求泛域名一枚子域名随便加不用每加一个子域名就申请一次安装完自带cron定时任务每天检查一次证书到期前自动续期续期后可以通过部署钩子reloadcmd自动重载Nginx全程无人值守纯Shell脚本COPY到新机器就能用没有版本和Python依赖的锁死问题。当然代价是你要能拿到DNS服务商的API密钥。这需要一点权限配置但一次性做好长期受益。下面第3章就直接以这个方案为实战主线把申请、部署、续期的全过程写清楚。3. acme.sh 签发证书全流程从安装到拿到泛域名证书3.1 安装与默认CA设置安装acme.sh非常简单官方一行命令curl https://get.acme.sh | sh -s emailyouexample.com执行完会自动把脚本安装到~/.acme.sh并把自动续期的cron任务写进当前用户的crontab。装完重开一个终端执行下面命令确认版本acme.sh --version这里有个必须注意的坑新版acme.sh默认CA是ZeroSSL而不是Lets Encrypt。ZeroSSL对免费用户要求邮箱验证如果你没有完成验证签发时可能卡住。为了省事我建议装完立刻切回Lets Encryptacme.sh --set-default-ca --server letsencrypt如果你之前的默认CA已经切成了别家也可以在每次签发时追加--server letsencrypt参数指定两条路都能走。3.2 泛域名证书申请DNS验证方式泛域名证书只能用DNS验证。原因很简单HTTP验证只能证明你控制某个具体的域名没法证明你控制整个域名的DNS记录而*.example.com天然需要所有子域名都由你管理这一级别的证明。DNS验证的原理是让CA生成一串TXT记录值你去域名DNS解析服务里添加一条TXT记录。CA查到TXT记录匹配就认为你拥有该域名。手动做一次不难但每次续期都手动添加就太麻烦了。acme.sh的DNS API插件就是干这个的它会自动调DNS服务商API帮你添加TXT记录验证完再自动删除。以Cloudflare为例先创建一个API Token需要Zone.DNS:Edit权限然后导出环境变量并签发export CF_Token你的API Token export CF_Zone_ID你的Zone ID acme.sh --issue --dns dns_cf -d *.example.com -d example.com注意我同时签了*.example.com和example.com因为泛域名通配符不覆盖裸域名而大多数场景裸域名和子域名都要用。如果是阿里云DNS原理一样换成两个环境变量export Ali_Key你的AccessKey ID export Ali_Secret你的AccessKey Secret acme.sh --issue --dns dns_ali -d *.example.com -d example.com关于阿里云AccessKey建议单独创建一个RAM子用户只赋予DNS管理权限不要用主账号密钥。密钥一旦泄露主账号相当于裸奔。签发成功后证书文件在~/.acme.sh/*.example.com/目录下里面有证书、私钥和完整证书链。但注意千万不要直接从~/.acme.sh里复制证书文件去用这个目录由acme.sh管理内部结构可能在升级后变动。正确做法是用--install-cert把证书安装到正式位置这是第3.4节的内容。3.3 单域名证书的HTTP验证方式如果你的域名没有支持API的DNS服务商或者只是签一个临时证书可以用HTTP验证。HTTP验证的流程是CA给你一个随机文件名和内容要求你把它放到http://你的域名/.well-known/acme-challenge/路径下CA跑去访问这个URL能访问到就证明你控制了服务器进而证明域名归属。acme.sh支持--webroot方式它会自动把验证文件写到网站的webroot目录。假设你的网站根目录是/var/www/htmlacme.sh --issue -d example.com -w /var/www/html有两个经典坑要提醒你。第一webroot目录必须能被nginx/apache的当前配置正常访问。验证文件请求的路径是http://domain/.well-known/acme-challenge/xxxxx如果nginx有rewrite规则把它拦截了验证永远成功不了。第二如果你用了CDN验证请求会被CDN缓存到边缘节点最好的做法是把验证路径临时绕过CDN或者干脆用DNS验证。CDN缓存ACME验证文件导致签发失败是我见过最多的小白翻车现场。3.4 安装证书到Nginx别直接复制~/.acme.sh这里分享一个我认为最容易被忽视的细节。很多人签完证书喜欢这么干cp ~/.acme.sh/*.example.com/fullchain.cer /etc/nginx/ssl/短期没问题但acme.sh后续升级脚本或调整证书目录结构你的复制命令就可能失效而且手动复制完全破坏了自动化的闭环。官方推荐用--install-certacme.sh --install-cert -d *.example.com \ --key-file /etc/nginx/ssl/example.key \ --fullchain-file /etc/nginx/ssl/example.pem \ --reloadcmd nginx -s reload这条命令做了三件事把最新的私钥写到/etc/nginx/ssl/example.key把完整的证书链含中间证书写到/etc/nginx/ssl/example.pem证书更新后自动执行nginx -s reload让配置立即生效。后面你在nginx里引用这两个文件即可server { listen 443 ssl; server_name example.com www.example.com; ssl_certificate /etc/nginx/ssl/example.pem; ssl_certificate_key /etc/nginx/ssl/example.key; }注意我用的是fullchain而不是单独的站点证书文件这个细节直接关系到第4章要讲的替换证书不生效第一大坑。4. Nginx替换SSL证书不生效的完整排查链路4.1 如何确认证书真的没生效替换证书后不生效是高发问题。先说结论大多数情况下不是Nginx不听话而是你误判了生效。换完证书后不要只看浏览器。浏览器会有缓存、HSTS、OCSP缓存等一系列干扰因素今天看到的报错未必是证书的真实状态。正确做法分三步。第一步看本机直接请求源站的响应。这条命令强制以127.0.0.1连接请求example.com绕开DNS和CDN直接看源站返回什么curl --resolve example.com:443:127.0.0.1 -vI https://example.com第二步用openssl直接读取服务器实际返回的证书信息echo | openssl s_client -connect 127.0.0.1:443 -servername example.com 2/dev/null | openssl x509 -noout -dates -subject -issuer这个命令会显示服务器在当前连接里实际下发的证书有效期、颁发者和域名。重点核对两个地方证书到期日期是不是你刚签的新证书签发者是不是你预期的CA。如果显示的日期还是旧的说明Nginx加载的还是老证书如果日期已经是新的说明Nginx侧已生效问题出在浏览器或中间链路。第三步确认中间层。如果你的网站前面有CDN、负载均衡、云WAF客户端其实连的是中间层节点这些节点有自己的证书缓存。源站换证后要么去中间层控制台同步要么等待缓存自然过期。很多替换不生效其实是CDN节点还握着旧证书这个我在客户项目里遇到过好几次。浏览器端还有一个隐蔽的坑HSTS。如果你在上一个证书有效期内启用了HSTS浏览器会在一段时间内强制HTTPS并缓存旧证书状态。排查时建议用无痕窗口或另一台没访问过该域名的设备验证能排除大部分本地缓存问题。4.2 三个最常见的根因与对应解法第一证书链不完整。这是最高频原因。比如你引用的是cert.pem而不是fullchain.pem或者阿里云下载的证书包里只放了站点证书没放中间证书。后果很典型curl正常、PC浏览器正常但手机浏览器或某些API客户端报证书链不完整/无法验证签发者。解法是Nginx里统一使用fullchain文件或者手动把站点证书和中间证书按顺序拼接成一个文件。第二nginx -t通过但实际reload失败。这个坑比较隐蔽。nginx -t是用root身份检查配置和证书文件的但Nginx worker进程通常以nginx或nobody用户运行。如果你的证书文件权限是600且属主是rootworker进程根本读不到文件内容而Nginx master又已经加载了旧的证书表现就是证书没变。排查命令nginx -t sudo -u nginx head -n 1 /etc/nginx/ssl/example.pem如果第二行提示权限拒绝把证书文件权限改成640并确保目录可读即可chmod 640 /etc/nginx/ssl/example.pem第三多个server块或include重复导致的证书匹配错乱。Nginx按server_name匹配443端口配置如果你有两个server块都写了listen 443 ssl一个指向旧证书、一个指向新证书而实际访问的域名恰好匹配到旧证书那个块表面看起来就是替换没生效。排查时先列出所有监听SSL的server块确认每个域名的证书指向grep -rn ssl_certificate /etc/nginx/conf.d/ /etc/nginx/sites-enabled/还有一类客户端问题也值得提一下有时候源站Nginx侧明明换了新证书但API调用工具或开发框架本地缓存了证书信任栈比如Java的cacerts、Python的certifi包报错是SSL certificate problem: unable to get local issuer certificate。这种不是服务端问题清理客户端信任缓存即可。4.3 阿里云免费证书续期后替换的注意事项如果你的方案是阿里云免费证书替换流程和acme.sh不太一样在阿里云数字证书管理服务里重新签发新证书下载证书包再手动替换Nginx配置。这里要特别留意几点。阿里云免费证书有效期较短建议到期前一周就操作重新签发不要拖到过期当天才动手因为签发审核有几分钟到几小时不等的延迟下载的证书包是nginx格式里面会有.pem和.key可能还会有一个中间证书文件。如果下载包里是分开的文件记得把站点证书和中间证书按顺序拼接后再使用替换后同样要执行nginx -t nginx -s reload然后按第4.1节的步骤验证别信上传成功就完事。阿里云免费证书和acme.sh签发的Lets Encrypt证书可以共存不冲突先用阿里云证书顶上线上同时后台跑acme.sh等Lets Encrypt签发成功再切换减少空窗时间。我有一台客户服务器就是这么干的全程没停机。5. 把免费变成长久免费自动续期与兜底策略5.1 续期机制的核心cron 部署钩子acme.sh安装时会自动写入cron任务每天凌晨检查一次所有已签发的证书发现证书剩余有效期少于30天就去自动续期。用下面命令确认cron存在crontab -l | grep acme正常会看到类似下面这样的日志输出0 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh /dev/null续期成功后acme.sh会自动触发你在--install-cert时注册的--reloadcmd也就是自动重载Nginx。这个签发→安装→重载的闭环就是长久免费SSL真正的实现方式。你不需要每个月登录服务器也不需要去控制台手工点续期。这里有一个我要特别强调的操作习惯不要手动跑多次acme.sh --issue。如果你手动签了一次重复的证书cron可能检测到该域名已有证书而没有续期反而错过更新窗口。想测试续期流程是否正常用下面的命令强制续期一次acme.sh --renew -d *.example.com --force5.2 续期失败的常见原因与兜底策略自动化不是一劳永逸我三年来遇到过几次续期失败基本都是这几类原因。第一DNS API密钥失效。服务商那边重新生成过Token、账号权限变了或者Token过期acme.sh自动添加TXT记录失败续期直接挂掉。解法是定期检查环境变量配置或者给Token设置一个长期有效周期后再忘掉它。第二服务器时间漂移。ACME协议在签发验证时对时间很敏感服务器时间偏差超过几分钟会导致TLS握手失败、验证不通过。这个坑特别隐蔽因为大部分业务对时间误差不敏感。检查与修复方法timedatectl ntpdate -u ntp.aliyun.com # 或 systemctl restart chronyd/systemd-timesyncd第三CA服务触达不到。比如服务器防火墙限制出方向443端口或者网络出口异常导致acme.sh无法访问Lets Encrypt的ACME接口。排查时用一条命令确认链路curl -I https://acme-v02.api.letsencrypt.org/directory能返回HTTP 200说明链路正常否则就需要检查防火墙和路由。第四续期后reloadcmd失败。比如Nginx配置刚好被你改出了语法错误nginx -s reload执行失败出现证书其实续期了但Web服务还是旧证书的中间状态。这时候看acme.sh的日志就能发现日志路径在~/.acme.sh/acme.sh.log。兜底策略方面我建议加一道主动监控不要只依赖acme.sh自己的续期日志。最简单的方式是每周用cron跑一次检查把证书到期时间发到通知渠道。#!/bin/bash expire$(echo | openssl s_client -servername example.com -connect example.com:443 2/dev/null | openssl x509 -noout -enddate) echo $expire | mail -s SSL证书到期提醒 youexample.com我自己用的是Webhook方式在证书到期前14天把提醒推到手机上。之前有一台测试服务器就是因为我忘了加这道通知差点在半夜挂掉。多一道提醒自动化才不会变成自动出事。证书续期这种事前期把闭环搭好后面几乎不用管真正要管的反而是那些竟然没自动执行的意外时刻。