Certbot自动管理免费SSL证书:申请、续期与排错实战
这段时间我陆陆续续帮几台服务器接过SSL证书也踩过几次坑最常用的一套方案其实到现在还是Lets Encrypt加Certbot。如果你有网站、接口服务或者各种运维面板需要上HTTPS别急着去云控制台手动折腾证书申请Certbot这套自动化工具基本能把你从“生成CSR、提交验证、下载证书、手动续期”这条繁琐流程里解放出来。市面上关于Certbot的文章很多但大部分只能覆盖最好走的那种“Nginx自动签发”一到多域名、续期失败、证书链不完整、以及把证书套用到MySQL、FTP这些非Web服务上就各种报错直冒。这篇就是把我实际在CentOS 7这类系统上操作Certbot的完整过程记录下来包含每一步的选型逻辑、常用参数、踩坑点和不建议做的操作希望能让没配过的人少走弯路也能让已经配过的对上号查漏补缺。1. 免费SSL证书的基础逻辑为什么是Certbot1.1 SSL证书到底在解决什么问题先说原理。SSL证书本身干的事情就是两件加密传输和身份认证。加密传输很好理解浏览器和服务器之间走HTTPS数据在链路上被加密中间人抓包抓到也是一堆乱码。身份认证呢是让访问者确认“这台服务器确实是你想访问的那台”避免DNS劫持或中间人伪造服务器。两者配合才构成了现代互联网传输信任的基础。很多人会把“SSL证书”当成一个单纯的文件其实它由两部分组成私钥和公钥证书。私钥保存在服务器上绝不能泄露公钥证书里包含了域名信息、颁发机构、有效期、公钥等内容。当客户端发起HTTPS请求时服务器会把公钥证书发给客户端客户端用内置的信任锚去验证证书链验证通过后再协商会话密钥。这个过程是TLS握手阶段完成的也是很多“SSL连接错误”出现的根源。理解了这一点你就能明白为什么证书申请工具要帮你做这么多事情它不只是生成一对密钥还要把公钥部分提交给CA机构去签名CA机构验证你对域名的控制权之后才会签发正式的证书。这套验证和签发流程如果是手动操作第一次起码折腾一两个小时而Certbot把这些自动化了从申请到配置一条命令就能完成。1.2 免费证书与付费证书的差异说到免费证书很多人第一反应是“免费的是不是不靠谱”。实际上Lets Encrypt是目前全球用户量最大的CA机构之一它签发的DVDomain Validation域名验证证书在浏览器兼容性上没有任何问题手机端、桌面端、各种API客户端都能正常信任。付费证书和它的核心区别不在加密强度而在验证级别和售后服务付费证书可以做到OV组织验证甚至EV扩展验证会在证书里体现企业名称适合金融、政务、品牌官网这类需要展示主体身份的站点免费证书则只验证域名控制权证书里不包含组织信息。从我实际使用的经验来看个人博客、中小型业务系统、API接口、测试环境用Lets Encrypt完全够用。唯一需要留意的只是有效期是90天不是付费证书常见的一年甚至两年所以必须依赖自动续期。这也正好是Certbot的强项。如果你图省事在云厂商控制台申请免费证书很多其实也是一年有效期到期手动续期域名多了之后非常痛苦。1.3 为什么选Certbot而不是其他工具同类工具其实还有acme.sh、lego、Caddy的内置自动HTTPS等。Certbot的最大优势是EFF官方维护社区案例极多遇到问题基本都能搜到答案。另外它和Nginx、Apache的集成非常好可以自动修改站点配置也能用webroot模式只签发证书不碰配置。对于生产环境来说操作越少意味着出错概率越低。acme.sh也很有名它是个纯Shell脚本部署轻量适合那种不想装一堆Python依赖的机器。但我遇到过一个情况在CentOS 7的Python 2.7环境下Certbot老版本偶尔会有依赖兼容问题这时候acme.sh反而更顺。所以我的建议是默认优先试Certbot如果你机器环境特殊、装不上或者依赖冲突严重再退回acme.sh。工具是死的思路要活。2. 安装与前置检查CentOS 7完整过程2.1 安装前必须确认的几件事在敲安装命令之前先确认三件事否则后面大概率白忙活。第一域名解析必须已经指向这台服务器。Lets Encrypt在验证域名控制权时会通过HTTP方式请求你的服务器如果DNS解析还没生效或者指向了别处验证必失败。这个看起来是最基础的但我见过太多人DNS刚添加TTL还没过期就开始申请然后反复报错。第二服务器80端口必须能被外部访问。Certbot默认的HTTP-01验证方式CA会访问http://你的域名/.well-known/acme-challenge/xxx这个路径来确认你是否控制域名。如果80端口被防火墙挡了或者Nginx配置里把所有HTTP请求都301跳转到HTTPS了验证同样会失败。这里有个细节如果做了强制跳转需要让.well-known路径例外放行或者直接用DNS验证方式。第三确认系统的包管理源可用。CentOS 7默认源里没有certbot需要先装EPELExtra Packages for Enterprise Linux源。如果机器在内网或者镜像源没有同步后面安装会卡在依赖解析上。2.2 用EPEL源安装CertbotCentOS 7下的安装过程分两步。先装EPEL源yum install -y epel-release然后安装certbot。这里要注意如果你用的是Nginx可以顺便装nginx插件用Apache就装apache插件。我个人更推荐先装certbot本体等需要自动改配置时再按需安装对应插件避免插件之间互相干扰。yum install -y certbot装上之后先确认版本certbot --version如果你的CentOS 7比较老装出来的可能是0.31版本年代比较久远。这版本也能用但部分新参数不支持。想用新版的话官方推荐用snap安装。不过说句实话在CentOS 7这种老系统上纠结版本意义不大0.31支持webroot、renew、多域名这些核心操作完全没问题。如果你确实需要新特性可以再考虑snap或者直接用acme.sh没必要为了版本去折腾依赖。2.3 验证安装与依赖检查安装完成后建议先跑一次certbot的命令帮助确认没有报缺少模块之类的错误certbot --help再检查一下Python环境。CentOS 7默认是Python 2.7certbot在Python 2.7下运行时偶尔会出现urllib3、pyOpenSSL版本冲突。如果遇到莫名其妙的SSL相关报错优先查升级这些依赖库。还有个容易忽略的地方检查SELinux。CentOS 7默认开启SELinux如果Nginx的webroot目录权限设置不对SELinux会拦截Certbot写入验证文件导致403。临时排查方法是用getenforce看状态如果看到Enforcing而且验证老失败可以去看/var/log/audit/audit.log里的avc拒绝记录或者临时用setenforce 0测试一下。生产环境不建议直接关SELinux正确做法是针对web目录调整文件上下文。3. 从单域名到多域名的证书申请实战3.1 HTTP-01验证与Webroot模式Lets Encrypt主流的验证方式有HTTP-01、DNS-01和TLS-ALPN-01。Certbot默认走HTTP-01CA服务器访问http://域名/.well-known/acme-challenge/token如果返回了正确的响应内容就证明你对这个域名有控制权。Certbot提供几种申请模式。最省事的是--nginx或者--apache插件模式它会自动检测web服务器配置自动放置验证文件、自动改配置、自动重载服务。但这种方式有个副作用就是它会改动你的server配置块。如果你对现有配置有强迫症不想让工具改配置文件那就用certonly加--webroot参数。webroot模式的逻辑很简单你告诉Certbot验证文件应该放到哪个目录它把文件写进去CA来访问时web服务器能直接返回即可。这个模式的好处是完全不碰你的Nginx配置只生成证书文件最适合配置洁癖和生产环境。命令格式是certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com-w指定webroot目录-d指定域名可以重复使用一次申请多个域名。注意如果你的多个域名分别对应不同站点目录可以写多个-wCertbot会按顺序对应到后面的-d上。这个细节很多人会搞混。3.2 单域名申请实战假设你有个站点example.com部署目录是/var/www/htmlNginx已经正常监听80端口。申请命令就很直接certbot certonly --webroot -w /var/www/html -d example.com执行过程中会问你两个问题一个是邮件地址一个是是否同意服务条款然后就开始自动验证。整个过程快的话十几秒就结束顺利的话会看到类似“Congratulations! Your certificate and chain have been saved at:/etc/letsencrypt/live/example.com/fullchain.pem”的提示。这里我要特别强调邮件地址一定要填真实可用的。Lets Encrypt会在证书即将过期时发提醒邮件如果你依赖自动续期且续期出问题这封邮件可能就是最后的保险丝。申请完后在/etc/letsencrypt/live/example.com/目录下会有四个文件cert.pem服务器证书本身chain.pemCA的中间证书链fullchain.pem服务器证书加中间证书链合并的完整链privkey.pem私钥文件部署时优先用fullchain.pem和privkey.pem这两个文件不要单独用cert.pem。因为cert.pem不包含中间证书链很多客户端会因为链不完整而报错。这是我接手别人服务器时最常见的低级错误之一。3.3 多域名证书申请多域名证书在两种场景下需要一种是站点本身就有多个域名指向同一套业务比如example.com和www.example.com另一种是服务器上有多个站点你想用一张证书覆盖所有域名省去多次申请的麻烦。先说第一种很简单一条命令搞定certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com这里要注意-w的对应关系。如果你有多个webroot对应关系是每个-w对应它后面紧跟着的-d直到出现下一个-w为止。比如certbot certonly --webroot -w /var/www/site1 -d site1.com -d www.site1.com -w /var/www/site2 -d site2.com这样site1相关的两个域名都会用/var/www/site1目录来验证site2则用/var/www/site2。这里有一个阿里云等云厂商平台的免费证书不太会告诉你的细节Lets Encrypt单张证书最多可以包含100个域名但如果你一张证书里塞太多域名后续每次续期都要所有域名全部验证通过才行其中任何一个域名失效整张证书续期就会失败。所以我的建议是一个主域名加它的www别名合成一张证书不同业务站点分开申请。域名多了之后就各管各的别为了省事硬塞到一张证书里。3.4 证书文件目录与部署申请完成后把证书配置到Nginx里server { listen 443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; }配置完重载Nginxnginx -t systemctl reload nginxnginx -t一定要养成习惯先检查配置语法再重载否则一旦写错可能导致服务中断。这里补充一个证书链完整性的验证命令。部署完后用openssl命令行检查站点证书链路openssl s_client -connect example.com:443 -servername example.com -showcerts /dev/null如果输出里出现Verify return code: 0 (ok)说明证书链配置正确如果出现unable to get local issuer certificate说明中间链没配全多半是只用了cert.pem而没用fullchain.pem。4. 自动续期这才是真正的零维护4.1 90天有效期与续期原理Lets Encrypt证书有效期为90天这个设计初衷是倒逼自动化运维。90天看起来短但只要配好自动续期你根本感知不到有效期存在。Certbot的续期机制是检查证书剩余天数如果少于30天就执行续期如果还早就什么都不做。需要理解一个关键点续期不是“延长原证书”而是“重新签发一张新证书”。所以续期过程同样会做域名验证验证方式和你申请时一样。如果你申请时用了webroot续期时Certbot也会复用webroot配置所以要确保webroot目录和流程一直可用。Certbot在续期时的任务是重新运行那些原始的签发命令它们存储在/etc/letsencrypt/renewal/目录下的配置文件中但会跳过交互并且带上一堆保护参数。如果你手动改动过配置文件导致续期失败Certbot通常会抛出清晰错误定位这时可以看对应域名的/etc/letsencrypt/renewal/example.com.conf文件内容来排查。4.2 renew钩子与定时任务先看系统里是否已经有自动续期机制。CentOS 7上用yum安装的certbot一般会自带certbot-renew服务systemctl list-timers | grep certbot如果能看到certbot-renew.timer说明证书会自动续期默认每天触发两次。但是注意这个timer只管到期自动执行续期不会自动帮你重载Nginx或Apache。很多人在这里踩坑证书其实已经自动续期成功了但Web服务器还在用旧证书因为服务器进程根本没感知到证书文件变化。解决办法是配置renew hook。编辑/etc/letsencrypt/cli.ini没有就创建deploy-hook systemctl reload nginx如果使用的是不同服务比如Apache就把命令换成systemctl reload httpd。用renew命令时也可以临时指定钩子certbot renew --deploy-hook systemctl reload nginxdeploy-hook只在续期真正成功执行后才会触发所以重载服务是安全、幂等的。如果你不信任系统timer想自己用cron管理也可以禁用timer然后添加crontab0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook systemctl reload nginx每天凌晨3点检查一次到期才续期平时什么都不做几乎零开销。4.3 如何手动验证续期是否成功配置好之后不要干等先用dry-run模式验证一遍整个续期流程certbot renew --dry-run这个命令会模拟续期实际请求Lets Encrypt测试环境来验证域名和webroot配置但不会替换你的真实证书。如果dry-run成功说明自动续期链路是通的如果报错就趁现在修别等到真过期了才知道。mock测试通过后你可以实际触发一次续期来验证deploy-hook是否生效certbot renew --force-renewal这个命令会强制重新签发证书正常情况会生成新证书并触发reload。验证完成后你可以再用openssl命令确认站点证书的到期时间已经更新。顺带提一下“阿里云SSL证书免费续期”这种云厂商场景。云厂商额外发的免费证书通常也是一年或三年到期需要重新申请和下载再手动上传。而Lets Encrypt配合Certbot的优势在于“续期”完全透明证书文件路径不变Web服务无感这才是生产环境需要的体验。4.4 查看证书过期时间的常用命令关于“Linux查看SSL证书过期时间”这是运维的高频需求。最常用的命令openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -dates输出会显示notBefore和notAfter两个时间。如果只看剩余天数可以这样openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -enddate | cut -d -f2再配合date计算剩余天数echo ($(date -d $(openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -enddate | cut -d -f2) %s) - $(date %s)) / 86400 | bc这条命令在脚本监控里非常实用。我一般会在cron里加一个每周检查任务如果剩余天数少于15天就发告警相当于给自动续期上了双保险。判断远程站点的证书状态echo | openssl s_client -connect example.com:443 -servername example.com 2/dev/null | openssl x509 -noout -dates这个命令不占用服务器资源适合批量巡检多台服务器。5. 常见疑难与安全修复5.1 常见SSL连接错误速查我在实际运维中遇到过各种SSL报错这里整理一个速查表都是真实出现过的场景。第一个是no required ssl certificate was sent。这个报错是客户端发起的请求要求服务端返回客户端证书双向TLS但客户端没有提供服务端配置的证书。最常见的场景是Nginx配置了ssl_verify_client on但客户端没有安装对应的客户端证书或者证书链不匹配。解决方法就是确认双向TLS使用的客户端证书是否有效。如果你本意只是做单向HTTPS根本不需要开ssl_verify_client。第二个是exception in invoking authentication handler [ssl: certificate_verify_failed]。这个报错常见于Java客户端连接启用SSL的服务比如SQL Server。Java的信任库和系统信任库是独立的如果你的JVM没有导入CA根证书即使浏览器访问正常Java程序也会验证失败。解决方法是用keytool把根证书导入JRE的cacertskeytool -import -trustcacerts -alias letsencrypt -file /path/to/root-cert.pem -keystore $JAVA_HOME/lib/security/cacerts第三个是an error occurred during ssl communication这个报错信息比较泛常见于客户端和服务端TLS版本不匹配。比如老版本Windows Server默认只启用TLS 1.0而服务端已经禁用了TLS 1.0/1.1双方协商失败。排查时可以用openssl s_client -tls1强制指定TLS版本去测看看服务端支持哪个版本。第四个是“ssl连接错误”这类浏览器提示。这种报错原因很多最常见的是证书和域名不匹配、证书过期、证书链不完整。先看浏览器能不能显示出证书详情如果显示“安全证书不受信任”通常意味着证书链不完整或者根证书不在浏览器信任库中如果显示“域名不匹配”那就是你用了A域名的证书访问B域名。用上一节的openssl s_client命令能快速定位。5.2 SSL/TLS协议信息泄露漏洞CVE-2016-2183很多安全扫描工具会报出一个常见漏洞项“SSL/TLS协议信息泄露漏洞(CVE-2016-2183)【原理扫描】”。第一次见到这个报错的人容易慌其实它针对的是SWEET32攻击一个比较老但依然存在的密码学隐患。原因是服务端仍然允许了3DES这类64位分组密码套件这种算法在会话数据量足够大时可以暴力破解出会话密钥。说白了就是你的服务端密码套件策略太宽松给客户端留下了弱加密选项。修复方式是在Nginx配置里禁用3DES相关套件。推荐用如下配置ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on;把ssl_ciphers设置为不包含DES、3DES、RC4等弱算法的列表即可。配置后重载Nginx再扫描一次这个漏洞就会消失。很多云平台的安全扫描工具只是“原理扫描”它看到服务端默认配置存在弱套件就会报并不代表你的业务已经被攻击但如果漏报扫出来还是老老实实修掉又不难。Apache配置类似修改/etc/httpd/conf.d/ssl.confSSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 SSLHonorCipherOrder on这里顺便强调不要为了图省事直接对全站配置ssl_protocols TLSv1 TLSv1.1 TLSv1.2。TLS 1.0和TLS 1.1早就被主流浏览器弃用了保留它们等于刻意开着旧时代的弱协议安全扫描工具看到一般也会继续报其它协议漏洞。除了极少数必须兼容老客户端的场景我建议直接只开TLS 1.2和TLS 1.3。5.3 MySQL、Vsftpd等非Web服务怎么用证书Certbot自动化申请证书默认是针对Web验证的但证书签发出来是通用文件完全可以用于MySQL、FTP、Kafka等非Web服务。只不过这些服务不算“标准web服务”走HTTP-01验证时你得把验证路径放在一个能被80端口访问的位置所以一般还是先用Nginx或者webroot模式把证书申请下来再进行非Web服务的配置。MySQL 8.0开启SSL的方式在my.cnf里配置。[mysqld] ssl-ca/etc/letsencrypt/live/example.com/chain.pem ssl-cert/etc/letsencrypt/live/example.com/fullchain.pem ssl-key/etc/letsencrypt/live/example.com/privkey.pem要注意MySQL使用的证书文件格式、路径权限都要正确mysqld进程要有权限读取私钥文件。配置完后进入MySQL验证SHOW VARIABLES LIKE %ssl%; SHOW STATUS LIKE Ssl_cipher;如果ssl_cert路径生效说明SSL已经开启。但如果你的业务要求“强制走SSL”还要用CREATE USER ... REQUIRE SSL来限定账号否则只是服务端支持SSL不强制使用。Vsftpd的SSL配置方式则是在/etc/vsftpd/vsftpd.conf里加ssl_enableYES rsa_cert_file/etc/letsencrypt/live/example.com/fullchain.pem rsa_private_key_file/etc/letsencrypt/live/example.com/privkey.pem allow_anon_sslNO force_local_data_sslYES force_local_logins_sslYES ssl_tlsv1YES ssl_sslv2NO ssl_sslv3NO这里有个坑vsftpd要求私钥文件不能有密码保护而Certbot生成的privkey.pem本身就是无密码的所以可以直接用。但如果你之前自己生成过带密码的私钥vsftpd会报cannot load RSA certificate。另外FTP客户端如果版本太老不支持TLS连接会失败这是正常的因为安全性升级必然抛弃老客户端。5.4 容器环境下的证书分发如果服务跑在Docker容器里事情会稍微绕一点。比如Kafka在Docker/KRaft模式下启用SSLCommunity容器官方镜像支持通过环境变量挂载证书文件docker run -d \ -v /etc/letsencrypt/live/example.com/fullchain.pem:/etc/kafka/secrets/server.pem:ro \ -v /etc/letsencrypt/live/example.com/privkey.pem:/etc/kafka/secrets/server.key:ro \ -e KAFKA_SSL_KEYSTORE_LOCATION/etc/kafka/secrets/server.pem \ ...需要注意容器里证书路径通常要求是绝对路径而且Kafka原生里还要求密钥库必须是JKS或P12格式。如果遇到底层库不识别PEM的情况需要先把PEM转成PKCS12再转JKSopenssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out keystore.p12 keytool -importkeystore -srckeystore keystore.p12 -destkeystore kafka.keystore.jks -deststoretype JKS容器场景还有一个通用陷阱证书续期了但容器内挂载的证书文件还是旧的。解决思路一般有两种一种是容器内进程定期读取磁盘上的证书文件重载或重启进程另一种是容器编排系统在证书更新后自动重建容器。无论用哪种都要在续期hook里加上“重启或重载相关容器”的动作而不是只reload Nginx。比如certbot renew --deploy-hook docker exec kafka-broker bash -c kafka-configs.sh --bootstrap-server localhost:9093 --entity-type brokers --entity-default --alter --add-config ssl.keystore.location...这部分和业务绑定较深我不展开具体命令但核心原则是**证书是文件文件更新之后所有使用它的进程都要感知到。**你需要在deploy-hook里把Nginx、Java进程、容器等所有相关方都加上才能在证书到期时真正做到无感知切换。6. 我踩过的坑和最后的建议最后聊几个实操中反复踩过的坑可以说是血泪教训。第一个是权限问题。Certbot生成的文件在/etc/letsencrypt/目录默认权限是rootNginx worker进程通常以nginx用户运行所以读取privkey.pem时可能遇到权限不足。如果你配置完发现HTTPS一直报错先确认/etc/letsencrypt目录的访问权限。正常情况下证书目录下的文件权限是-r--r--r--但live目录的上级archive目录如果被手改成600甚至更严格Nginx就读取不了。我一般不会去改Lets Encrypt目录权限而是调整Nginx的启动用户或者用ACL放行不建议直接chmod 777了事那是把私钥裸奔给别人看。第二个是多个站点多个证书时不要共用一条renew配置文件。Certbot的renew配置是按域名拆分的如果某个站点的验证目录变了最好手动删掉旧的renewal配置重新申请而不是去改配置文件。手改renewal配置容易造成路径、域名对不上续期失败的时候排查起来非常头疼。第三个是强迫重载要适度。我见过有人写cron每分钟执行一次certbot renew systemctl reload nginx这是没有必要的。certbot renew本身自带检查未到期会自动跳过但每分钟执行会让日志文件变得非常大而且systemctl reload nginx太频繁也不是好事。每天一次甚至每周一次都足够系统timer默认每天两次也挺好。第四个是证书监控比证书申请更重要。如果你手里管理的服务器超过三台建议专门做一层证书过期监控。不用多复杂一个脚本加cron就能覆盖把上一节提到的openssl剩余天数计算写成脚本到期前30天、15天、7天各发一次盯梢这个动作能救回很多因为自动续期失败导致的线上事故。我个人在实际操作中的体会是Certbot这套工具本身非常成熟它简化了申请和续期的所有复杂步骤真正决定成败的往往是你对证书文件、服务器配置、自动化钩子这些细节的理解。把原理吃透把续期链路走通再做好到期监控SSL这一块基本可以放手不管了。希望这篇记录对你手上要配的服务器有所帮助后续如果遇到更刁钻的SSL问题欢迎对照这个思路去排查大概率都能找到症结所在。