Debian 11 Apache 配置 Let‘s Encrypt HTTPS 完整实践指南

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 。提交前必须满足:

  1. Strict-Transport-Security 响应头存在且 max-age>=31536000
  2. includeSubDomains 已启用;
  3. 所有子域(包括 www )均能通过 HTTPS 访问且返回有效证书;
  4. 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)缓存了旧证书

排查步骤:

  1. openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name" 获取证书 SAN;
  2. 对比用户访问的 URL(如 https://www.example.com ),确认 www.example.com 是否在 SAN 列表中;
  3. 检查 Apache 配置中 ServerName 是否为 www.example.com ,且该 VirtualHost 是否启用了正确的 SSLCertificateFile
  4. 若使用 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 或 `
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值