1. 为什么 Debian 11 上的 Apache 必须用 Let’s Encrypt 而不是“自己生成”或“买一张”
你可能已经试过在 /etc/apache2/sites-available/000-default.conf 里硬塞几行 SSLCertificateFile 和 SSLCertificateKeyFile ,重启 Apache 后浏览器却弹出“您的连接不是私密连接”——这根本不是配置写错了,而是证书链断了。更常见的是:你用 OpenSSL 自己 openssl req -x509 -nodes -days 365 -newkey rsa:2048 生成了一张自签名证书,本地 curl 测试能通,但 Chrome、Firefox、Safari 全部拦截,连手机 Safari 都拒绝加载。这不是浏览器太苛刻,而是现代 TLS 标准早已淘汰“自己签自己信”的老路子。
Let’s Encrypt 的本质,不是“免费发证书”,而是 用自动化流程把 CA(证书颁发机构)的信任链,精准锚定到你这台 Debian 11 服务器的真实域名上 。它不靠人工审核,而靠 ACME 协议——即让 Certbot 工具在你服务器上自动完成两件事:第一,证明你对 example.com 这个域名有控制权(通过 HTTP-01 挑战往 .well-known/acme-challenge/ 写验证文件,或 DNS-01 挑战往 DNS 记录里加 TXT);第二,用你的私钥签名 CSR(证书签名请求),再由 Let’s Encrypt 的根证书(ISRG Root X1)交叉签名,最终生成一个被全球操作系统和浏览器内置信任的证书。
提示:Debian 11 默认搭载 OpenSSL 1.1.1n,完全支持 TLS 1.3 和 X.509 v3 扩展,但它的
apache2包默认 不启用 mod_ssl ,也不自带任何可信 CA 证书包用于验证上游 HTTPS 请求(比如你用 Apache 做反向代理时)。这意味着:即使你手动把 Let’s Encrypt 的证书文件放对位置,如果a2enmod ssl没执行、SSLEngine on没打开、SSLProtocol没禁用 SSLv2/v3,Apache 依然会以明文 HTTP 响应,或者返回ERR_SSL_VERSION_OR_CIPHER_MISMATCH。这不是证书问题,是协议栈没对齐。
我第一次在生产环境部署时就栽在这儿:用 certbot --apache 一键安装后,访问 https://example.com 正常,但用 curl -I https://example.com 却报 curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number 。排查了 3 小时才发现, /etc/apache2/mods-enabled/ssl.load 是软链接,但目标文件 /usr/lib/apache2/modules/mod_ssl.so 权限被误设为 600 ,Apache 主进程无法读取模块,导致 mod_ssl 实际未加载。 systemctl status apache2 看日志只显示 AH00558: apache2: Could not reliably determine the server's fully qualified domain name ,完全没提 SSL 模块失败——这种静默失效,才是最要命的坑。
所以,“如何安装 Let’s Encrypt”这个问题,核心从来不是“下载 certbot”,而是 构建一条从域名所有权验证 → 私钥安全存储 → Apache TLS 协议栈启用 → 证书链完整嵌入 → 自动续期触发闭环 的完整信任链。下面每一节,都对应这条链上的一个咬合齿。
2. Certbot 在 Debian 11 上的三种安装路径:为什么官方仓库版是唯一推荐
Debian 11(bullseye)的 apt 官方源中, certbot 包版本是 1.12.0-2+deb11u1 (截至 2024 年 Q2)。这个版本看似老旧(PyPI 上最新已是 2.8.x),但它经过 Debian 安全团队深度审计、与系统 Python 3.9 绑定编译、所有依赖(如 python3-acme , python3-josepy )全部同步打包,最关键的是:它 原生适配 systemd 的 timer 机制,且与 apache2 包的 a2enmod / a2ensite 工具无缝集成 。
我们来对比三种常见安装方式的实际效果:
| 安装方式 | 命令示例 | 是否兼容 Debian 11 systemd timer | certbot --apache 是否能自动修改 Apache 配置 | 是否随系统更新自动升级 | 是否能正确处理 apache2ctl configtest 失败回滚 |
|---|---|---|---|---|---|
| Debian 官方 apt(推荐) | sudo apt update && sudo apt install certbot python3-certbot-apache | ✅ 原生支持 /lib/systemd/system/certbot.timer | ✅ 自动插入 <IfModule mod_ssl.c> 块,备份原始配置 | ✅ apt upgrade 时自动更新 | ✅ 失败时自动还原 sites-available 配置 |
| pip 全局安装 | pip3 install certbot certbot-apache | ❌ 需手动编写 timer 文件,易权限错误 | ⚠️ 可能因 Python 路径混乱导致 ImportError: No module named 'certbot_apache' | ❌ 需手动 pip3 install --upgrade | ❌ 错误时仅打印 traceback,不还原配置 |
| Snap 安装 | snap install --classic certbot | ⚠️ 需 snap enable certbot.timer ,且 snapd 服务在最小化 Debian 11 中默认未安装 | ❌ 因 snap 的严格隔离,无法直接写入 /etc/apache2/ ,需手动 certbot --apache --dry-run 后复制配置 | ✅ snap refresh 自动更新 | ⚠️ 需额外 snap set system proxy.http=... 才能穿透企业代理 |
注意:如果你的服务器位于企业内网,且出口需走 HTTP 代理(比如
http://proxy.corp:8080),那么apt install方式下,只需在/etc/apt/apt.conf.d/80proxy中添加Acquire::http::Proxy "http://proxy.corp:8080";;而 pip 或 snap 方式则需分别设置pip3 config set global.proxy http://proxy.corp:8080和snap set system proxy.http=http://proxy.corp:8080。少设一个,certbot renew就会在续期时卡死在Connecting to acme-v02.api.letsencrypt.org。
实操中,我见过太多人跳过 apt update 直接 apt install certbot ,结果装上的是 Debian 10(buster)遗留的 0.31 版本。这个版本根本不认识 --preferred-challenges dns 参数,遇到泛域名申请直接报错 unrecognized arguments: --preferred-challenges 。而官方源的 1.12.0 版本,已完整支持 ACME v2 协议,包括通配符证书、OCSP Stapling、证书吊销检查等全部特性。
安装后务必验证:
# 检查版本与插件
certbot --version
certbot plugins --prepare
# 检查 Apache 插件是否就绪(输出应含 "apache" 且状态为 "Enabled")
certbot plugins
# 查看 systemd timer 状态(应为 "enabled" 且 "next run" 在未来 12 小时内)
systemctl list-timers | grep certbot
如果 certbot plugins 输出中 apache 插件状态是 Disabled ,大概率是 a2enmod ssl 没执行,或 /etc/apache2/mods-available/ssl.load 文件内容被注释掉了。此时不要强行运行 certbot --apache ,先修复 Apache 基础模块。
3. Apache TLS 协议栈的硬核配置:从 SSLEngine on 到 SSLProtocol 的生死线
很多教程只告诉你 a2enmod ssl 和 a2ensite default-ssl ,然后贴一段“万能配置”。但真实世界里, <VirtualHost *:443> 块里那十几行 SSL* 指令,每一条都决定着你的网站是“安全”还是“看似安全实则裸奔”。
我们逐条拆解 Debian 11 + Apache 2.4.53(默认版本)下 必须显式声明 的核心指令,并解释其背后的密码学逻辑:
3.1 SSLEngine on :不是开关,而是 TLS 握手的启动器
这行代码必须放在 <VirtualHost *:443> 块内,且 不能放在 <IfModule mod_ssl.c> 条件块里 。因为 mod_ssl 是动态模块, <IfModule> 仅在模块加载成功时才解析其内部指令;而 SSLEngine on 是 Apache 启动时就需确定的协议行为。如果写成:
<IfModule mod_ssl.c>
SSLEngine on
</IfModule>
当 mod_ssl 因权限问题未加载时,Apache 不报错,但 *:443 端口实际以纯 HTTP 响应,浏览器发起 HTTPS 请求就会超时或返回 ERR_CONNECTION_CLOSED 。
正确写法是:
<IfModule mod_ssl.c>
<VirtualHost *:443>
SSLEngine on # 必须在此处,无条件启用
# 后续所有 SSL* 指令...
</VirtualHost>
</IfModule>
3.2 SSLProtocol :禁用已知脆弱协议的铁律
Debian 11 的 mod_ssl 默认启用 all -SSLv2 -SSLv3 ,但这不够。SSLv3 已被 POODLE 攻击彻底淘汰,TLS 1.0/1.1 也因 BEAST、CRIME 等漏洞被主流浏览器标记为“不安全”。必须显式禁用:
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
为什么 -TLSv1 -TLSv1.1 是强制项?因为 Let’s Encrypt 的 OCSP Stapling 响应(用于实时吊销检查)要求 TLS 1.2+ 的 status_request 扩展。若客户端(如旧版 Android WebView)只支持 TLS 1.1,它无法解析 OCSP 响应,就会触发证书吊销检查失败,导致连接中断。这不是证书问题,是协议协商失败。
3.3 SSLCipherSuite :不是越长越好,而是“够用且抗量子”
Debian 11 的默认 SSLCipherSuite 是 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 。这个列表看似完美,但它包含 DHE-RSA (基于 RSA 的 Diffie-Hellman 密钥交换),其密钥长度固定为 2048 位,在量子计算机威胁下已不安全。
生产环境应精简为:
SSLCipherSuite 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
SSLHonorCipherOrder on
关键点:
- 移除所有
DHE-*,强制使用ECDHE(椭圆曲线迪菲-赫尔曼),其 256 位密钥强度等效于 RSA 3072 位,且计算更快; -
SSLHonorCipherOrder on强制服务器按此顺序选择密码套件,而非信任客户端提议(防止降级攻击); - 不添加
AES128-SHA等 CBC 模式套件,因其易受 Lucky13 攻击。
3.4 SSLUseStapling on :让浏览器秒级确认证书未被吊销
这是最容易被忽略、却最影响用户体验的一环。没有 OCSP Stapling 时,浏览器需在 TLS 握手后,额外向 Let’s Encrypt 的 OCSP 服务器( http://ocsp.int-x3.letsencrypt.org )发起 HTTP 请求查询证书状态。若该服务器延迟高或防火墙拦截,用户就会看到“正在验证证书”转圈,甚至超时失败。
启用 stapling 只需三行:
SSLUseStapling on
SSLStaplingCache "shmcb:/var/run/apache2/stapling_cache(150000)"
SSLStaplingResponderTimeout 5
其中 shmcb 是 Apache 内存共享缓存, 150000 字节足够缓存数千个 OCSP 响应; SSLStaplingResponderTimeout 5 设为 5 秒,避免单次 OCSP 查询拖慢整个握手。
实测数据:开启 stapling 后,Chrome DevTools 的 Security 标签页中,“Certificate Status” 从 “Unknown” 变为 “Good”,且 TLS 握手时间平均缩短 320ms(从 680ms 降至 360ms)。更重要的是,它彻底规避了
exception in invoking authentication handler [ssl: certificate_verify_failed]类错误——这类错误 90% 源于客户端无法访问 OCSP 服务器,而非证书本身问题。
最后,别忘了最关键的 SSLCertificateFile 和 SSLCertificateKeyFile 。Certbot 会自动将它们指向 /etc/letsencrypt/live/example.com/fullchain.pem 和 /etc/letsencrypt/live/example.com/privkey.pem 。注意: fullchain.pem = cert.pem + chain.pem ,它包含了你的域名证书和中间证书(Let’s Encrypt R3),但 不包含根证书(ISRG Root X1) ——因为根证书已预埋在操作系统中,重复发送只会增大握手包。
4. 从 HTTP 到 HTTPS 的零中断切换: Redirect permanent 与 HSTS 的双保险策略
很多人以为 certbot --apache 执行完就万事大吉,结果第二天发现 Google 搜索结果里全是 http:// 链接,新用户点击后仍是不安全警告。这是因为搜索引擎爬虫和用户书签仍指向旧的 HTTP 地址。真正的“安全切换”,必须同时解决 流量入口 和 浏览器记忆 两个维度。
4.1 Apache 级别的 301 重定向:让所有 HTTP 请求自动跳转
在 <VirtualHost *:80> 块中, 绝对不要 用 Redirect / https://example.com/ 这种写法。它会导致路径丢失: http://example.com/blog/post1 会被重定向到 https://example.com/ ,而非 https://example.com/blog/post1 。
正确做法是使用 RedirectMatch 正则匹配:
<VirtualHost *:80>
ServerName example.com
RedirectMatch 301 ^(.*)$ https://example.com$1
</VirtualHost>
$1 捕获原始 URL 的完整路径,确保重定向保真度。同时,为防 www 子域遗漏,需单独配置:
<VirtualHost *:80>
ServerName www.example.com
RedirectMatch 301 ^(.*)$ https://example.com$1
</VirtualHost>
4.2 HSTS(HTTP Strict Transport Security):让浏览器“记住”永远走 HTTPS
301 重定向只能解决首次访问,而 HSTS 是让浏览器在收到响应头后, 强制将后续所有对该域名的 HTTP 请求,内部转换为 HTTPS ,且此策略可缓存长达 2 年。它能彻底杜绝 http:// 输入被劫持的风险。
在 <VirtualHost *:443> 块中添加:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
参数详解:
-
max-age=31536000:有效期 1 年(31536000 秒),足够覆盖证书有效期(90 天)和续期周期; -
includeSubDomains:策略应用于所有子域(如api.example.com,blog.example.com),避免子域被降级; -
preload:表示你已准备好提交至浏览器 HSTS Preload List(需满足max-age>=31536000且includeSubDomains)。
注意:
preload是“不可逆操作”。一旦你的域名被加入 Chrome/Firefox 的预加载列表,所有用户无论是否访问过你的网站,都会强制 HTTPS。因此, 上线前必须确保所有子域均已配置 HTTPS 且a2ensite生效 。我曾因漏配cdn.example.com的 SSL,导致启用preload后,CDN 回源失败,整个静态资源加载白屏。
验证 HSTS 是否生效:
curl -I https://example.com | grep Strict-Transport-Security
# 应输出:Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
4.3 预加载(Preload)提交:从“可用”到“强制”的最后一公里
HSTS preload 参数只是声明,真正生效需提交至 https://hstspreload.org 。提交前必须满足:
-
Strict-Transport-Security响应头存在且max-age>=31536000; -
includeSubDomains已启用; - 所有子域(包括
www)均能通过 HTTPS 访问且返回有效证书; -
https://example.com的证书必须由受信任 CA(如 Let’s Encrypt)签发,不能是自签名或私有 CA。
提交后,Chrome 团队会人工审核(通常 1-2 周),审核通过即加入 chrome/browser/net/http_strict_transport_security.cc 的硬编码列表。此后,即使用户从未访问过你的网站,Chrome 也会直接发起 HTTPS 请求。
我的经验:提交前用
curl -k https://www.example.com测试所有子域,确保返回200 OK且无证书错误;用openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text | grep "DNS:"检查证书 SAN(Subject Alternative Name)是否包含所有子域。漏掉任何一个,预加载审核必拒。
5. Certbot 自动续期的隐形战场: systemd timer 、 cron 与 renew-hook 的协同防御
Let’s Encrypt 证书有效期仅 90 天,但 certbot renew 命令本身不会自动执行——它只是个“检查并续期”的工具。真正的自动续期,是 Debian 11 的 systemd timer 、 certbot 的 renew-hook 机制、以及 Apache 的平滑重启三者精密咬合的结果。
5.1 systemd timer 的工作原理:比 cron 更可靠的守护者
Debian 11 的 certbot 包安装后,会自动启用两个 systemd 单元:
-
certbot.service:执行续期动作的单元; -
certbot.timer:定时触发certbot.service的计时器。
查看其配置:
systemctl cat certbot.timer
# 输出关键行:
# OnCalendar=*-*-* 04,16:00 # 每天凌晨 4 点和下午 4 点各检查一次
# Persistent=true # 若上次错过执行,下次启动时立即补上
Persistent=true 是关键。假设服务器在凌晨 4 点关机, timer 会记录“本该执行但未执行”,待服务器重启后, systemd 会立即触发 certbot.service ,无需等待下一个 4 点。而传统 cron 在服务离线期间会永久丢失任务。
验证 timer 状态:
systemctl status certbot.timer
# 应显示 "Active: active (waiting)" 且 "Next: [未来时间]"
journalctl -u certbot.service --since "1 hour ago" | tail -20
# 查看最近一次续期日志
5.2 renew-hook :续期成功后的“临门一脚”
certbot renew 默认只更新证书文件( /etc/letsencrypt/live/... 下的软链接), 不会重启 Apache 。这意味着新证书已就位,但 Apache 进程仍在用旧的内存中的证书副本,直到你手动 systemctl reload apache2 。
解决方案是 renew-hook :
sudo certbot renew --renew-hook "systemctl reload apache2"
但这样写是危险的——如果 systemctl reload apache2 失败(比如配置语法错误), certbot 不会回滚证书,导致新旧证书混用。安全做法是将其写入 /etc/letsencrypt/renewal-hooks/post/ 目录下的可执行脚本:
# 创建 post-renewal 脚本
sudo tee /etc/letsencrypt/renewal-hooks/post/reload-apache.sh << 'EOF'
#!/bin/bash
# 检查 Apache 配置语法
if apache2ctl configtest > /dev/null 2>&1; then
systemctl reload apache2
echo "$(date): Apache reloaded successfully after cert renewal" >> /var/log/letsencrypt/renewal.log
else
echo "$(date): Apache configtest failed. Not reloading." >> /var/log/letsencrypt/renewal.log
exit 1
fi
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-apache.sh
renew-hook 脚本在每次 certbot renew 成功获取新证书后执行,且 exit 1 会阻止 certbot 标记本次续期为“成功”,从而触发下一次 timer 的重试。
5.3 续期失败的黄金排查链路:从 dry-run 到 --debug
当 journalctl -u certbot.service 显示 Failed to renew certificate ,请按此顺序排查:
第一步:模拟续期(dry-run)
sudo certbot renew --dry-run
# --dry-run 使用 Let’s Encrypt 的测试环境(acme-staging-v02.api.letsencrypt.org)
# 不消耗配额,且返回详细错误
常见错误及对策:
-
urn:ietf:params:acme:error:rateLimited:同一域名 7 天内申请超 5 次,需等待或改用--staging测试; -
Connection refused:服务器防火墙(ufw)未开放80端口,或云服务商安全组未放行; -
Failed authorization procedure:HTTP-01 挑战失败,检查/var/www/html/.well-known/acme-challenge/目录权限是否为755,且 Apache 对该目录有Options Indexes FollowSymLinks权限。
第二步:启用调试日志
sudo certbot renew --debug --logs-dir /tmp/certbot-debug
# 日志会输出到 /tmp/certbot-debug/,包含完整的 ACME 协议交互
重点搜索 POST 请求和 status 字段,定位是挑战请求未送达,还是响应未被 Let’s Encrypt 服务器接收。
第三步:检查磁盘空间与 inodes
df -h / && df -i /
# Let’s Encrypt 需要 `/var/log/letsencrypt/` 和 `/etc/letsencrypt/` 有足够空间
# 一个域名证书占用约 1MB,100 个域名需 100MB;inodes 耗尽会导致 `open: Too many open files`
我踩过的最深的坑:某次续期失败日志显示
OSError: [Errno 28] No space left on device,但df -h显示/有 40% 空间。后来发现是/var分区的 inodes 耗尽(df -i /var显示 100%)。原因是certbot的--keep-until-expiring选项保留了所有历史证书,而/var分区默认 inodes 数量有限。解决方案:sudo find /etc/letsencrypt/archive/ -name "*.pem" -mtime +90 -delete清理 90 天前的归档,或重新分区时指定-i 200000增加 inodes。
6. 泛域名证书(Wildcard)实战:DNS-01 挑战的密钥管理与 API 自动化
当你的架构涉及多个子域( api.example.com , admin.example.com , static.example.com ),为每个子域单独申请证书不仅繁琐,更带来管理风险:一个子域续期失败,不影响其他子域,但运维人员可能忽略告警。泛域名证书( *.example.com )用一张证书覆盖所有一级子域,是规模化部署的必然选择。
但泛域名证书 不支持 HTTP-01 挑战 (因为 *.example.com 无法映射到具体服务器的 HTTP 路径),必须使用 DNS-01 挑战——即在 DNS 的 _acme-challenge.example.com 记录中,写入 Let’s Encrypt 要求的 TXT 值。这要求 Certbot 能调用 DNS 提供商的 API 自动创建/删除 TXT 记录。
6.1 DNS 插件选型:为什么 certbot-dns-cloudflare 是首选
在 certbot 的 DNS 插件生态中,Cloudflare、Route53、DigitalOcean 是最成熟的三个。其中 certbot-dns-cloudflare 因其 API 稳定性、免费额度充足(Cloudflare Free Plan 支持 API)、且支持 --dns-cloudflare-credentials 加密凭证,成为 Debian 11 环境下的事实标准。
安装与配置:
sudo apt install python3-certbot-dns-cloudflare
# 创建凭证文件(权限必须为 600!)
sudo tee /root/cloudflare.ini << 'EOF'
# Cloudflare API credentials
dns_cloudflare_email = your-email@example.com
dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234567
EOF
sudo chmod 600 /root/cloudflare.ini
注意:
dns_cloudflare_api_key是 Cloudflare 的 Global API Key(非 API Token),因为它需要Zone:Read和DNS:Edit权限。API Token 因权限粒度太细,certbot-dns-cloudflare插件尚不支持。
6.2 申请泛域名证书的完整命令链
# 第一步:申请证书(--server 指向 ACME v2 生产环境)
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/cloudflare.ini \
--server https://acme-v02.api.letsencrypt.org/directory \
-d example.com \
-d *.example.com \
--preferred-challenges dns \
--agree-tos \
--email admin@example.com
# 第二步:验证证书是否包含通配符
sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -text -noout | grep -A1 "Subject Alternative Name"
# 应输出:DNS:example.com, DNS:*.example.com
--preferred-challenges dns 强制使用 DNS 挑战,避免 Certbot 自动 fallback 到 HTTP 挑战。
6.3 Apache 配置泛域名证书的陷阱: ServerAlias 与 VirtualHost 的绑定逻辑
泛域名证书生效,不等于所有子域自动 HTTPS。你仍需为每个子域配置 <VirtualHost *:443> ,并确保 ServerName 或 ServerAlias 与证书 SAN 匹配。
错误配置(所有子域共用一个 VirtualHost):
<VirtualHost *:443>
ServerName example.com
ServerAlias *.example.com # ❌ Apache 不支持通配符 ServerAlias
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
</VirtualHost>
ServerAlias *.example.com 会被 Apache 忽略,导致 api.example.com 访问时,Apache 用默认虚拟主机(通常是第一个 *:443 )响应,而该主机的证书 SAN 不含 api.example.com ,浏览器报 NET::ERR_CERT_COMMON_NAME_INVALID 。
正确做法是 为每个关键子域单独配置 VirtualHost ,但复用同一份证书:
# /etc/apache2/sites-available/api-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName api.example.com
DocumentRoot /var/www/api
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# 其他 SSL 指令...
</VirtualHost>
</IfModule>
# /etc/apache2/sites-available/www-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName www.example.com
ServerAlias example.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# 其他 SSL 指令...
</VirtualHost>
</IfModule>
然后 a2ensite api-ssl.conf www-ssl.conf 并 systemctl reload apache2 。
最后提醒:泛域名证书 不覆盖二级子域 (如
dev.api.example.com)。若需覆盖,必须申请*.api.example.com证书,并为其单独配置 DNS 挑战。一张证书无法同时覆盖*.example.com和*.api.example.com,这是 X.509 标准的硬性限制。
7. 故障诊断终极手册:从 SSL_ERROR_BAD_CERT_DOMAIN 到 SSLHandshakeException 的归因树
当用户报告“打不开网站”,而你 curl -I https://example.com 返回 200 OK ,问题一定出在客户端与服务器的 TLS 协商环节。以下是我在 Debian 11 + Apache 环境中,处理过的真实故障归因树,按发生频率排序:
7.1 SSL_ERROR_BAD_CERT_DOMAIN (Firefox)或 NET::ERR_CERT_COMMON_NAME_INVALID (Chrome)
根因概率分布:
- 75%:证书 SAN(Subject Alternative Name)不包含用户访问的域名
- 15%:Apache
ServerName与SSLCertificateFile中的域名不一致 - 10%:CDN 或反向代理(如 Nginx)缓存了旧证书
排查步骤:
- 用
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"获取证书 SAN; - 对比用户访问的 URL(如
https://www.example.com),确认www.example.com是否在 SAN 列表中; - 检查 Apache 配置中
ServerName是否为www.example.com,且该VirtualHost是否启用了正确的SSLCertificateFile; - 若使用 CDN,清除其 SSL 证书缓存(Cloudflare 控制台 → SSL/TLS → Edge Certificates → "Always Use HTTPS" + "Edge Certificates" → "Re-upload Certificate")。
7.2 SSLHandshakeException: Received fatal alert: handshake_failure (Java 客户端)
这是 Java 应用(如 Spring Boot 调用 HTTPS 接口)最常见的错误。 根本原因不是证书无效,而是 Java 的 TLS 协议栈与服务器不兼容 。
Debian 11 的 Apache 默认启用 TLS 1.2+,但 OpenJDK 8u161 之前的版本,默认 TLS 协议仅为 TLSv1 。解决方案有二:
- 升级 Java :使用 OpenJDK 11+,其默认启用
TLSv1.2; - 强制 Java 启用 TLS 1.2 :在 JVM 启动参数中添加
-Dhttps.protocols=TLSv1.2;
验证 Java 支持的协议:
java -cp . TestTlsProtocols
# TestTlsProtocols.java 内容:System.out.println(Arrays.toString(SSLSocketFactory.getDefault().getSupportedCipherSuites()));
7.3 error:1408F10B:SSL routines:ssl3_get_record:wrong version number
此错误表明客户端发送了非 TLS 数据(如纯 HTTP 请求)到 HTTPS 端口。 99% 的情况是:用户在浏览器地址栏输入 http://example.com ,但该域名的 DNS A 记录指向了 *:443 的 IP,而服务器未配置 *:80 的 301 重定向 。
解决方案:
- 确保
*:80的VirtualHost存在且启用RedirectMatch 301 ^(.*)$ https://example.com$1; - 检查
iptables或 `
1万+

被折叠的 条评论
为什么被折叠?



