ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CentOS 7.9下certbot自动续期SSL证书:从安装到nginx完整接入指南

CentOS 7.9下certbot自动续期SSL证书:从安装到nginx完整接入指南 1. 为什么选择certbot证书过期恐惧症的终极解药先交代一下背景。我手头维护着几台CentOS服务器上面跑着nginx、Tomcat之类的服务前几年一直用阿里云的免费证书。阿里云免费证书本身没什么毛病一年申请一次但痛点在于你得记着续期这件事。每年到时间了登录控制台重新申请、下载、上传服务器、改nginx配置、reload。这套流程走下来运气好十分钟赶上控制台改版或者证书类型变更折腾半小时也不是没可能。更麻烦的是多台服务器的情况。我有个客户三台服务器每台域名还不一样每年到了续期月就得挨个处理。有一次我出差在外其中一台服务器证书到期没来得及换结果客户那边小程序直接白屏因为接口全是https的。那次之后我就决定必须上certbot这种自动续期的方案。certbot是Lets Encrypt官方推荐的客户端工具作用是自动申请、部署、续期SSL证书。它跟传统证书体系最大的区别在于证书有效期只有90天但通过定时任务可以做到全自动续期一旦配置好理论上这辈子不用再手动碰证书。用生活类比的话普通证书像是一年一换的纸质年检标certbot像是ETC走一次流程后面自动扣费、自动更新你只管开车就行。这篇文章我会把整个流程拆开讲从环境准备、安装、签发证书到nginx接入、自动续期再到几个容易踩的坑比如离线安装、nginx替换证书不生效这些实际工作中一定会遇到的问题。适合刚接触Linux运维的人也适合已经用nginx有一段时间但证书管理还靠手动的老手。2. 安装前的三个关键判断域名、解析、端口2.1 域名解析必须提前搞定certbot签发证书时Lets Encrypt服务器会尝试访问你的域名来验证你对该域名的控制权。验证方式主要有两种HTTP-01访问域名下的特定路径和DNS-01在DNS记录里加TXT记录。默认情况下certbot用的是HTTP-01验证。这意味着什么你的域名必须已经正确解析到这台服务器上。我见过不少人卡在这一步域名还在旧服务器指向或者DNS记录刚改还没生效然后跑certbot报错Failed to connect to domain for verification就开始怀疑是防火墙的问题、是certbot的问题其实根本原因是解析都没通。检查解析很简单dig short example.com curl -I http://example.com/.well-known/acme-challenge/test第二条命令是模拟Lets Encrypt的验证请求。如果服务器上没配nginx站点可能会返回404但只要能连上、有HTTP响应就说明网络链路通了。如果连404都没有先排查解析和防火墙。2.2 80端口必须暴露HTTP-01验证需要Lets Encrypt服务器通过80端口访问你的服务器。很多服务器为了安全默认只开了443和2280端口是关的这会导致验证失败。前面提到的热词里有“gpt网络配置问题ssl证书”其实不少人遇到的所谓“网络配置问题”排查到最后就是80端口没放行。在阿里云这类云平台上除了服务器本身要开放端口安全组规则里也要放行这两层是独立的任何一层没开都连不上。检查端口是否监听ss -lnt | grep :80如果nginx还没装上或者站点没配置这一条可能看不到输出这是正常的因为端口还没被占用。但安全组和系统防火墙层面必须允许入站80。2.3 certbot运行方式的选择certbot支持两种主要的签发方式这个选择会影响后面的安装配置nginx插件模式--nginxcertbot自动修改你的nginx配置注入证书路径和443监听最省事但要求nginx是标准安装且配置结构规范。webroot模式--webroot -w /path/to/webrootcertbot在指定目录下创建验证文件由已经运行的nginx提供服务不修改nginx配置适合配置比较复杂、不想让工具乱动的场景。我个人的建议是第一次用certbot别用nginx插件模式。原因后面会详说但简单讲就是它改配置的方式有时候跟你的预期不一致改完了你还要去检查、可能还要改回来反而多一道工序。用webroot模式收发全在自己掌控里。3. CentOS 7.9安装certbot在线与离线两条路3.1 在线安装EPEL源是唯一正路CentOS 7默认的yum源里没有certbot必须启用EPELExtra Packages for Enterprise Linux源。yum install -y epel-release yum install -y certbot装完验证一下certbot --version这里有个老生常谈但值得再提的坑如果你的服务器在国内EPEL源下载可能很慢甚至超时。解决方案是换成国内的镜像源比如阿里云镜像。方法mv /etc/yum.repos.d/epel.repo /etc/yum.repos.d/epel.repo.bak curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo yum clean all yum makecache切换之后速度会有质的提升。另外注意certbot在EPEL里是一个大而全的包依赖挺多的安装过程拉几十个依赖包是正常的别看到一堆依赖就怀疑卡住了。3.2 离线安装在一台能联网的机器上做搬运工热词里特别提到了“centos7.9 离线安装certbot”这个场景在政企内网、专网环境非常常见。思路其实一句话找一台同系统版本的、能联网的机器把rpm包都下载下来拷贝到内网机器上本地安装。具体步骤第一步联网机器上配置好EPEL源然后mkdir /opt/certbot-rpms yum install --downloadonly --downloaddir/opt/certbot-rpms certbot这条命令会下载certbot以及所有依赖的rpm包但不会安装。下载完检查一下目录ls /opt/certbot-rpms正常会有几十个rpm包括python2-certbot、python2-acme、python2-mock等等。注意这些包的依赖链条很长缺一个装不上所以全部拷过去最稳妥。第二步把整个目录打包传进内网机器tar czvf certbot-rpms.tar.gz /opt/certbot-rpms第三步在内网机器上进入目录直接cd /opt/certbot-rpms rpm -Uvh *.rpmrpm会自动按照依赖顺序安装。如果提示依赖缺失大概率是漏拷了package回联网机器重新执行第二步的下载命令补齐即可。第四步离线环境下certbot运行还需要处理证书更新时的网络可达性问题。内网环境如果无法访问Lets Encrypt的服务器离线安装certbot的意义主要在签发本机构建的自签证书或私有CA签发的证书或者通过内网ACME服务器签发。如果是纯内网场景还需要配置certbot的ACME目录地址certbot --server https://your-internal-acme-server/directory这个选项官方文档有提到但实际用的人少这里单独拿出来讲是因为离线环境的核心瓶颈就在网络层。3.3 为什么不直接推荐snap安装搜certbot安装教程会看到不少文章推荐用snap安装官方文档也把snap作为首选方案。但在CentOS 7.9上我实测snap安装的certbot是python3版本运行起来内存占用更高而且snap本身在CentOS 7上需要额外装epel源里的snapd两三层套娃折腾的时间不如直接用yum源里的python2版本。Python 2版本的certbot虽然官方已停止新功能开发但核心的签发、续期功能完全够用稳定得很。4. 签发证书与nginx接入的完整演示范例4.1 webroot模式签发先建好验证目录假设我的域名是blog.example.com网站根目录在/var/www/blog。先建一个ACME验证用的目录mkdir -p /var/www/blog/.well-known/acme-challenge然后执行签发certbot certonly --webroot -w /var/www/blog -d blog.example.com第一次运行时certbot会问你两个问题一个是邮箱地址用于过期提醒一个是同意服务条款。邮箱建议填真实邮箱证书快过期时Lets Encrypt会发提醒邮件万一自动续期哪天失效了你还有时间手动补救。签发成功后证书文件在/etc/letsencrypt/live/blog.example.com/目录下四个文件cert.pem域名证书本身chain.pem中间证书链fullchain.pemcert.pem和chain.pem合并nginx里配这个privkey.pem证书私钥4.2 nginx配置的关键细节证书文件别用错nginx的ssl配置长这样server { listen 443 ssl; server_name blog.example.com; ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem; # 其他配置... }这里有个很多人第一次用certbot容易踩的细节ssl_certificate必须用fullchain.pem不是cert.pem。如果你只用cert.pem浏览器会报错提示“服务器证书链不完整”因为中间证书没有合进去。另外/etc/letsencrypt/live/blog.example.com/这个路径其实是个软链接指向../../archive/blog.example.com/下的具体版本文件。这个机制的好处是每次续期后live目录下的文件名不变nginx配置不用改。但坏处是如果你不小心删了archive目录live下的链接就断了证书直接失效。配置完nginx测试一下语法然后reloadnginx -t systemctl reload nginx4.3 80端口跳转443顺手做掉用Lets Encrypt证书的站点一般都会顺带把http访问强制跳到https。这里有个容易忽略的点如果80端口没做跳转、网站又能正常通过http访问搜索引擎会认为你有两个重复内容站点影响SEO。跳转配置server { listen 80; server_name blog.example.com; return 301 https://$host$request_uri; }注意这个配置和webroot验证不冲突。certbot已经完成验证后.well-known/acme-challenge路径其实可以不用保留。但建议不要删因为自动续期默认走同一种验证方式下次续期还要用到这个路径。如果你把80的跳转声明放在最前面且恰好有location /.well-known/acme-challenge的优先匹配就不会有冲突问题。5. nginx替换SSL证书不生效完整的排查链路这个场景我想单独拉出来好好讲讲因为热词里明晃晃写着“nginx替换ssl证书不生效”而且这个坑我踩过不止一次。5.1 现象描述某天你拿到了新证书替换了fullchain.pem和privkey.pem执行了nginx -s reload浏览器里一看证书还是旧的。或者更诡异电脑上Chrome显示新证书手机上Safari显示旧证书。5.2 原因一nginx reload时节流导致worker进程还占用旧证书nginx reload并不是所有worker进程立刻退出。它会启动新配置的新worker进程同时让旧worker进程处理完手头的连接再退出。**在旧连接还没处理完时旧worker仍然使用旧证书。**最典型的就是HTTP/2长连接浏览器和服务器之间一个连接挂很久旧worker一直不退新的连接进来走了新worker、新证书而旧连接还占着老证书。排查方法ps -ef | grep nginx看有没有多个worker进程的启动时间不一致。如果确实有老进程可以用nginx -s reload sleep 3 nginx -s reload连续reload两次把上一次的worker进程全部换掉。更粗暴的办法是nginx -s stop再nginx直接重启整个服务不过这会造成瞬间断连生产环境慎用。5.3 原因二证书文件路径被软链接误导/etc/letsencrypt/live/目录是软链接这个事前面提过。如果你手动替换文件时直接vi编辑了live目录下的cert.pem其实你改的是软链接指向的那个目标。假设你vi保存时把软链接的三元结构cert.pem、privkey.pem、chain.pem其中之一变成了普通文件或者删掉重建live目录的链接结构就乱了。遇到过最典型的场景有人为了图方便把新证书直接复制到/etc/letsencrypt/live/blog.example.com/cert.pem覆盖了软链接本身。结果原链接指向的archive目录还在而live下变成了一个独立文件。下次续期时certbot默认去写archive目录但nginx读的还是那个值两边各写各的证书彻底分叉。这种问题的排查方法ls -l /etc/letsencrypt/live/blog.example.com/如果输出里没有-符号说明软链接被干掉了。修复办法重新建立链接关系或者干脆删掉这个目录让certbot重新生成。我建议清理掉重新签干净利落。5.4 原因三客户端缓存骗过了你的眼睛这也是一个容易让人白忙活半天的坑。很多浏览器对证书有本地缓存尤其是HSTSHTTP Strict Transport Security开启后浏览器强制走HTTPS且会缓存证书状态。你这边证书其实已经换成功了但浏览器还是显示旧证书。排查时不要只看浏览器用命令行工具验证最可靠openssl s_client -connect blog.example.com:443 -servername blog.example.com 2/dev/null | openssl x509 -noout -dates这个命令直接跟服务器握手拿到的是真实证书的生效时间、过期时间不经过浏览器缓存。如果这个输出已经显示新证书那问题就在浏览器缓存强制刷新CtrlF5或者换个无痕窗口验证即可。5.5 原因四nginx的ssl证书配置被include覆盖有人说“我明明改了nginx conf为什么reload后还是旧证书”。一种情况是同一个server块被多次定义nginx默认取最后一个匹配的。另一种情况是配置里用了include动态加载比如ssl_certificate在别处的文件中被再次赋值。排查手段nginx -T这条命令会把nginx最终生效的完整配置输出出来。直接搜ssl_certificate看到底生效的是哪个文件、哪一行。如果报错提示“unknown directive”通常是配置文件语法问题也能一并暴露。5.6 一个验证的方案把这串脚本固化下来为了以后不再重复排查我自己把检查步骤写成了一个shell脚本每次换完证书就跑一遍#!/bin/bash DOMAINblog.example.com echo 证书有效期 openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2/dev/null | openssl x509 -noout -dates echo live目录软链接 ls -l /etc/letsencrypt/live/${DOMAIN}/ echo nginx生效配置 nginx -T 2/dev/null | grep -A2 ssl_certificate输出一目了然到底是浏览器缓存、nginx worker没换、还是配置指向错了十秒内定位。6. 自动续期配置与阿里云免费证书的取舍分析6.1 dry-run和定时任务certbot续期命令本身certbot renew --dry-run--dry-run是演练模式会完整走一遍续期流程但不真正替换证书。第一次配置完建议先跑一遍dry-run确认流程通畅。确认没问题后添加定时任务。certbot装好后默认会在/etc/cron.d/certbot里生成一条定时任务内容大致是每天执行两次0 */12 * * * root test -x /usr/bin/certbot /usr/bin/certbot renew -q --post-hook systemctl reload nginx这条任务默认在但有个前提nginx的reload这个post-hook不一定在里面。旧版本的certbot自动生成的cron可能只有certbot renew -q没有reload动作。证书文件虽然更新了但nginx没重载等于白更新。检查一下你的cron文件内容没有就补上。crontab -l我的习惯是单独写一条自己的cron不依赖certbot生成的15 3 * * * /usr/bin/certbot renew -q --renew-hook systemctl reload nginx注意我把时间设置在凌晨3点15分避开Lets Encrypt服务器的高峰时段也避免白天用户在访问时突然reload。--renew-hook跟--post-hook的区别renew-hook只在证书确实续期成功之后执行而post-hook每次跑完都执行哪怕这次没续期。用renew-hook更精准减少无意义的nginx reload。6.2 续期失败的常见原因80端口被占、DNS解析漂移自动续期并不是100%成功尤其是运行久了之后。最常见的失败原因80端口被其他服务占用比如你后来在服务器上挂了别的Web服务占用了80导致ACME验证无法通过。域名解析变了比如把站点从这台服务器迁到了另一台但certbot还在这台跑验证自然失败。防火墙规则变更安全组调整时顺手封了80但443还开着用户访问正常唯独续期悄悄失败。我的建议是每月手动看一眼续期日志。日志位置在/var/log/letsencrypt/letsencrypt.log月底翻一翻看看有没有renewal failed字样。虽然证书到期前Lets Encrypt会发邮件提醒但邮件万一进了垃圾箱呢。6.3 阿里云免费证书 vs Lets Encrypt一张表看清差异热词里“阿里云ssl证书免费续期”出现频率很高我索性把两者的对比写透。阿里云免费证书额度也够用一年一签但它的自动化程度和certbot根本不在一个量级。用一张表总结对比项阿里云免费证书Lets Encrypt证书certbot管理有效期通常3个月或1年政策变动频繁90天签发方式控制台申请手动部署命令行自动无需登录网页自动续期不支持到期需重新申请支持cron定时任务自动续部署工作量每年一次约15分钟首次配置30分钟之后零维护多服务器管理每台单独申请配置同一定时任务可管多域名多服务器支持域名数量一般单域名或仅一级域名单域名、多域名、泛域名通吃失败恢复难度手动下载更换失败可随时再来自动续期失败需查日志定位有一定门槛如果只是个人博客、测试站点我强烈建议直接上certbot。如果是企业业务、对证书的品牌和信任链有特殊要求比如某些App接口强制要求OV证书那阿里云或其它云厂商的证书仍然必要但那些证书的部分到期需要人工确认没有绕过的办法。6.4 手动迁移已有一张证书怎么转到certbot统一管理有读者可能会问“我现在用的是阿里云证书想换成certbot但又不敢贸然迁怕出问题。”这个迁移有个稳妥的流程先在服务器上装好certbot用webroot模式签发新证书不要动nginx的配置。确认签发新证书成功然后在nginx里把ssl_certificate和ssl_certificate_key指向/etc/letsencrypt/live/目录。nginx -t systemctl reload nginx这时候切换就完成了且整个过程没有证书空窗期。把阿里云那张旧证书解绑删除先别急着删过一周看没什么问题再说。这个流程的核心思路新老证书并存切换只是改配置随时可以回滚。7. 兜底经验那些教程不会告诉你的certbot真相7.1 live目录下的软链接是“花架子”别有依赖不少教程会让你直接在nginx配置里写死/etc/letsencrypt/live/domain/fullchain.pem然后说“续期后无需修改配置”。这句话听起来很美但有一个前提整个/etc/letsencrypt目录结构不能被破坏。我碰到过一次某运维为了清理磁盘把/etc/letsencrypt/archive目录整个删了以为证书都下载过不需要了。结果第二天所有https站点直接挂了因为nginx读的软链接指向的文件不存在了。certbot证书文件虽然不大但archive目录里存的是历史上所有版本的证书每个证书文件不到5KB一个域名十年累积也不到200KB真没必要动它。7.2 证书私钥文件权限必须收紧certbot签发的privkey.pem默认权限是600属主是root。这个默认值是对的千万别手欠改成644。如果私钥文件允许其他用户读取等于是把服务器大门钥匙挂在了门口。如果你在配置时遇到nginx无法启动、报错提示“cannot load certificate key”检查一下权限ls -l /etc/letsencrypt/live/blog.example.com/privkey.pem chmod 600 /etc/letsencrypt/live/blog.example.com/privkey.pem7.3 证书过期后更换浏览器中间的“信任断裂期”怎么避免Lets Encrypt证书有效期90天自动续期通常会在到期前30天开始尝试。但如果你中途做了服务器迁移、域名更换自动续期链条断了证书过期了你才知道此时站点已经无法访问。这时候再跑certbot certonly --webroot -w /var/www/blog -d blog.example.com --force-renewal强制续签能救回来但中间那段时间用户访问就是报错状态。避免这种尴尬的最笨但最有效的方法给“证书到期前第14天”设一个日历提醒定期打开/var/log/letsencrypt/letsencrypt.log扫一眼。真正接管certbot之后每年花在证书上的精力基本就是这几眼日志的时间。8. 一次真实踩坑复盘certbot续期成功但网站还是打不开写到最后分享一次我实际遇到的诡异问题希望能帮读者省去一次深夜加班。某客户的站点证书过期了按常规操作跑到服务器上续期日志显示Congratulations, all renewals succeeded: /etc/letsencrypt/live/blog.example.com/fullchain.pem续期成功nginx也reload了但客户的浏览器还是显示证书过期。我用openssl命令验证openssl s_client -connect blog.example.com:443 -servername blog.example.com 2/dev/null | openssl x509 -noout -dates notBeforeJan 1 00:00:00 2025 GMT notAfterJan 1 00:00:00 2026 GMT证书确实是旧的。这就很奇怪了renewals succeeded却还是旧证书。最后排查到原因这台服务器上装了多个nginx实例一个在宿主机上一个在Docker容器里。certbot续期没问题post-hook reload的宿主机的nginx也没问题但客户访问的域名实际透过宿主机反代到Docker里的另一个nginx而Docker里的nginx没有被reload还在用旧证书。这个案例想说的不是Docker的问题而是续期链条的最后一环——reload动作必须覆盖实际对外提供服务的那个进程。如果你用了任何形式的反向代理、负载均衡、Docker映射都要确认证书续期后的reload动作能穿透到最终提供TLS终结的那个节点。否则任何一环断了外面看起来“证书过期”但certbot日志里一切都正常。检查方法不复杂续期后主动访问一下站点用命令行确认证书日期别只看certbot的renew日志。眼见为实这句老话在运维领域是铁律。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进