HTTPS 已经不是可选项:浏览器对未加密站点标记"不安全",主流搜索引擎明确把 HTTPS 作为排名的正向因素,而不少浏览器新特性(地理定位、Service Worker、HTTP/2 的广泛支持)也只在安全上下文里可用。但在实际配置中,证书装完不等于配好了——证书链不完整、协议版本老旧、续期没生效、HTTP 跳转写错地方,这些都是高频问题。
这篇文章覆盖证书类型选择、申请与验证方式、Nginx 上的完整配置与自动续期,以及 Windows IIS 上导入 PFX 并绑定的完整流程。命令区分 RHEL、CentOS、Rocky 系(dnf 或 yum)和 Debian、Ubuntu 系(apt)。
先理解证书是怎么被校验的
SSL/TLS 证书本质上是一个把域名和公钥绑定的签名文件。浏览器在建立 HTTPS 连接时做几件事:
第一,服务端发送证书链。链里通常包含服务器证书(叶子证书)和一个或多个中间证书——根证书本来就在浏览器和操作系统的信任库里,不需要发送。
第二,浏览器逐级验证签名:叶子证书被中间证书签名,中间证书被根证书签名,根证书在本地信任库里,这条链要走通。中间任一级缺失,校验就会失败,这就是"证书链不完整"的问题根源。
第三,验证域名是否匹配。证书里的 CN 和 SAN 字段必须包含当前访问的域名。通配符证书 *.example.com 匹配 www.example.com,但不匹配裸域名 example.com,也不匹配二级点 a.b.example.com,这一点经常搞错。
第四,验证有效期和吊销状态。有效期检查依赖本机时间;吊销状态通过 CRL 或 OCSP 查询(OCSP Stapling 能让服务端代为提供该结果,加快握手并减少对 CA 的依赖)。
第五,SNI(服务器名称指示)。握手时客户端在 ClientHello 里带上要访问的域名,服务端据此选择返回哪张证书。正因为有 SNI,一台服务器才能在同一个 IP 上承载多个不同域名的 HTTPS 站点。 老旧的客户端不支持 SNI,在这种机器上访问会拿到默认证书从而告警,但现在这类客户端占比已经很低。
证书类型怎么选
按验证强度分三类:
| 类型 | 全称 | 验证内容 | 签发速度 | 适用场景 |
|---|---|---|---|---|
| DV | 域名验证 | 只验证你对域名的控制权 | 几分钟 | 个人站、博客、绝大多数 API 服务 |
| OV | 组织验证 | 验证域名控制权加组织真实身份 | 数天 | 企业官网,需要展示公司主体 |
| EV | 扩展验证 | 最严格的组织身份核验 | 一到数周 | 金融、支付、政务类对身份展示要求极高的站点 |
一个重要的现实:EV 证书在主流浏览器地址栏已经不再显示特殊的绿色标识,视觉上的差异基本消失。所以除非合规条约里明确要求,否则 DV 证书通常就够用了,安全强度上三者是相同的加密算法,区别只在身份核验的严格程度。
按覆盖范围分三类:
| 类型 | 覆盖范围 | 示例 | 选择建议 |
|---|---|---|---|
| 单域名 | 一个完全限定域名 | www.example.com |
单个站点,最常用 |
| 多域名(SAN/UCC) | 一组明确列出的域名 | example.com、www.example.com、a.net |
域名不多且不连续时 |
| 通配符 | 同一级下的所有子域 | *.example.com |
子域多且随时增加的场景 |
注意通配符证书的边界:*.example.com 覆盖 www.example.com、api.example.com,但不覆盖 example.com 本身。常见做法是申请一张同时包含 example.com 和 *.example.com 两个 SAN 的通配证书。
通配符证书通常只能用 DNS-01 验证方式签发,这一点下面会讲。
文件格式与文件清单
从 CA 拿到或使用 Certbot 生成后,你会看到几种文件:
| 文件 | 内容 | 常见扩展名 | 用到哪 |
|---|---|---|---|
| 私钥 | 必须保密的私钥 | .key、.pem |
所有场景都必需 |
| 服务器证书 | 只包含叶子证书 | .crt、.cer |
一般不单独用,容易漏中间证书 |
| 中间证书 | CA 的中间证书 | .crt、.pem |
需要合并进证书链 |
| 证书链 | 叶子加中间,不含根 | fullchain.pem、chain.crt |
Nginx 推荐用这个 |
| PFX/PKCS#12 | 私钥加证书打包为单个二进制文件,可设密码 | .pfx、.p12 |
Windows、IIS、部分 Java 环境 |
Nginx 上应该用 ssl_certificate 指向 fullchain,而不是单独的叶子证书。只配叶子证书在部分客户端上能正常访问,但在另一些客户端(尤其是某些移动端浏览器和命令行工具)上会因为缺中间证书而报错,这类问题很难复现。
Certbot 的默认路径:
/etc/letsencrypt/live/example.com/
├── cert.pem # 叶子证书
├── chain.pem # 中间证书
├── fullchain.pem # 叶子 + 中间,Nginx 应指向这个
└── privkey.pem # 私钥
这个目录下的文件实际是符号链接,指向 ../../archive/example.com/ 里的真实文件,续期时会更新链接目标,所以配置文件里应该永远写 live 路径。
申请方式:ACME 验证类型
Let's Encrypt 这类免费 CA 使用 ACME 协议自动签发,核心是验证你对域名的控制权。三种验证方式:
| 验证方式 | 原理 | 端口要求 | 能否签发通配符 |
|---|---|---|---|
| HTTP-01 | 在站点指定路径放置验证文件供 CA 访问 | 需要 80 端口对外可达 | 不能 |
| DNS-01 | 添加指定的 TXT 解析记录供 CA 查询 | 不需要任何端口 | 能 |
| TLS-ALPN-01 | 在 443 端口用特殊 ALPN 协议响应 | 需要 443 端口 | 不能 |
选择建议:普通站点用 HTTP-01 最简单,Certbot 的 Nginx 插件会自动完成放置和清理。通配符证书、或 80 端口不方便对外开放的场景,用 DNS-01,配合 DNS 服务商的 API 可以完全自动化。
用 Certbot 申请证书并安装
Debian、Ubuntu:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
Rocky、RHEL、CentOS:
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx
确认版本:
certbot --version
自动模式(推荐)
自动模式会读取现有 Nginx 配置、写入证书路径并按需配置跳转:
sudo certbot --nginx -d example.com -d www.example.com
执行过程中会询问是否把所有 HTTP 请求重定向到 HTTPS,建议选择重定向(选 2)。同时会要求留下邮箱用于到期提醒和安全通知,建议填写真实邮箱。
风险提示:Certbot 会修改你的 Nginx 配置文件。执行前先备份:
sudo cp -r /etc/nginx /etc/nginx.bak.$(date +%F)
手动模式(更可控)
如果不想让工具改动配置,用 --certonly 只签发证书、自己写配置。配合 Nginx 插件:
sudo certbot certonly --nginx -d example.com -d www.example.com
或者用 webroot 模式,不依赖插件:
sudo certbot certonly --webroot -w /var/www/example.com/public \
-d example.com -d www.example.com
webroot 要求站点根目录可写,Certbot 会在其下创建 .well-known/acme-challenge/ 目录。确保 Nginx 没有把这个路径的返回改写掉——比如一个捕获全部请求的 location / 规则可能让验证文件访问不到。常见做法是加一条显式规则:
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com/public;
default_type "text/plain";
}
通配符证书(DNS-01)
手动模式下会提示添加 TXT 记录:
sudo certbot certonly --manual --preferred-challenges dns \
-d '*.example.com' -d example.com
按提示在 DNS 控制台添加一条 _acme-challenge.example.com 的 TXT 记录,用另一个终端验证记录已生效后再回车:
dig -t TXT _acme-challenge.example.com +short
手动 DNS 验证无法自动续期(除非用脚本调 DNS API)。长期方案是使用 DNS 服务商的 API 插件,具体插件名称请参考 Certbot 官方文档和你的 DNS 服务商支持情况。
用测试环境避免触发限流
Let's Encrypt 对同一域名的签发频率有限制,调试配置时应先用暂存环境:
sudo certbot certonly --webroot -w /var/www/example.com/public --staging -d example.com
验证流程跑通后,再用正式环境签发。查看证书信息:
sudo certbot certificates
Nginx 完整 HTTPS 配置
一份可以直接使用的配置模板:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# ACME 验证路径必须放行,否则无法续期
location ^~ /.well-known/acme-challenge/ {
root /var/www/example.com/public;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # Nginx 1.25.1 及以后;更早版本写作 listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
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;
ssl_prefer_server_ciphers off;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000" always;
root /var/www/example.com/public;
index index.php index.html;
access_log /var/log/nginx/example.com.ssl.access.log;
error_log /var/log/nginx/example.com.ssl.error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-php.conf; # RHEL 系改用 fastcgi_params 并补 SCRIPT_FILENAME
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
}
关键指令说明
ssl_protocols TLSv1.2 TLSv1.3。 TLS 1.0 和 1.1 已存在已知弱点并被主流浏览器禁用,不应再启用。只保留 1.2 和 1.3 是当前的安全基线。TLS 1.3 简化握手,比 1.2 少一个往返,对首次访问延迟有实际收益。
ssl_prefer_server_ciphers off。 这个值看似反直觉,但推荐理由是现代客户端自身有合理的套件偏好顺序,让客户端优先通常能得到更好的组合。只有在需要严格服务端控制时才设为 on。
ssl_session_cache 与 ssl_session_timeout。 会话复用让重复访问的握手开销降低。10 MB 的共享缓存大约能存数万条会话,通常足够。
ssl_stapling。 OCSP 装订让服务端定期获取并缓存吊销状态,在握手时一并返回,避免客户端去查询 CA,既加快握手也减少对第三方可用性依赖。需要配 resolver 指定 DNS。
关于加密套件的更新:密码套件列表会随时间变化,建议参考 Mozilla 的 SSL 配置生成器这类持续更新的资源,而不是照抄一篇老文章的固定列表。
HTTP 跳转的正确写法
用 return 301 而不是 rewrite:
# 推荐
return 301 https://$host$request_uri;
# 不推荐:rewrite 在这类场景下可读性差且容易出错
rewrite ^ https://$host$request_uri? permanent;
$request_uri 保留完整路径和查询参数;用 $request_uri 而不是 $uri 是为了保证参数不丢。
注意:ACME 验证路径必须放在跳转之前,且不能跳转。 如果整个 server 块都被 301 跳走,Certbot 续期时会因为拿不到验证文件而失败,导致三个月后证书过期。上面的模板已经包含了这条例外规则。
HSTS 及其风险
HSTS(Strict-Transport-Security 响应头)告诉浏览器"在 max-age 时间内,这个域名只能用 HTTPS 访问",能防御 SSL Stripping 类的降级攻击。
先不带 includeSubDomains 和 preload,用小一点的 max-age 试运行:
add_header Strict-Transport-Security "max-age=300" always;
确认一段时间无问题后再逐步加长(比如 31536000 秒,即一年),再考虑加 includeSubDomains:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
风险必须说清楚:
第一,一旦设置为 max-age=31536000,浏览器会在一年内强制 HTTPS。如果这时证书出问题或你想临时退回 HTTP,用户浏览器不会配合,会直接报错且无法绕过(除了在浏览器里手动清除该域名的 HSTS 状态)。所以要循序渐进地加长。
第二,includeSubDomains 会影响所有子域。如果某个子域还只用 HTTP 且短期内不能改造,加上这个参数会导致它无法访问。加之前务必确认所有相关子域都已支持 HTTPS。
第三,preload 一旦提交并被收录进浏览器的预置列表,撤销需要数月时间,且影响不可逆程度更高。除非明确知道后果并已全面验证,否则不要加。
另外要注意 add_header 的继承规则:在 Nginx 里,如果某个子上下文(比如某个 location)里也用了 add_header,父级的 add_header 会被覆盖。给 HSTS 加上 always 参数可以保证在非 2xx 响应上也发送。
自动续期与验证
Let's Encrypt 证书有效期九十天,所以自动续期是必需配置,不是可选项。
Certbot 安装后通常会生成 systemd timer:
sudo systemctl list-timers | grep -i certbot
systemctl status certbot.timer --no-pager
查看定时任务的具体执行内容:
systemctl cat certbot.timer
systemctl cat certbot.service
关键的验证步骤:
sudo certbot renew --dry-run
这个命令会模拟完整续期流程但不真正签发证书。必须看到成功输出,这是唯一能证明续期链路可用的方法。很多证书过期事故的根源,就是当初装完证书之后从来没跑过这条命令,而续期因各种原因(80 端口被拦、验证路径被跳转、DNS API 凭据失效)一直失败。
如果希望续期成功后自动重载 Nginx(很多时候 Nginx 不会自动感知文件变化):
sudo crontab -e
# 建议每周执行一次,续期成功才 reload;Certbot 默认每天两次检查但只在临到期时真正续期
0 3 * * 1 certbot renew --quiet --deploy-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1
更简洁的做法是 Certbot 自带的钩子机制或 systemd timer 自带的 ExecStartPost。具体用哪种看你的版本默认安装了什么,先查看现有 timer 的内容再决定要不要加 crontab,避免重复执行。
续期后的检查:
# 查看所有证书的域名与剩余有效期
sudo certbot certificates
# 从连接层验证实际生效的证书
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
输出里的 notBefore 和 notAfter 就是实际有效期。这条命令比看文件系统更有说服力,因为它验证的是服务端实际提供的证书。
还要验证证书链是否完整:
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
返回的数量应该大于等于二(叶子加至少一个中间)。只有一个说明中间证书没配好。
最后别忘了防火墙要放行 443:
# Rocky、RHEL、CentOS
sudo firewall-cmd --permanent --add-service=https && sudo firewall-cmd --reload
# Debian、Ubuntu
sudo ufw allow 443/tcp && sudo ufw reload
云平台安全组同样要放行,两层缺一不可。
IIS 上导入 PFX 并绑定
Windows Server 上的 IIS 不支持 PEM 格式,需要 PFX(PKCS#12)文件,里面同时包含私钥和证书。
第一步:准备 PFX 文件
如果手上是 PEM 格式(比如从 Linux 迁移过来),用 openssl 转换。openssl 在 Linux 上自带,Windows 可以从 Git for Windows、WSL 或者安装 OpenSSL 的 Windows 版本:
openssl pkcs12 -export \
-out certificate.pfx \
-inkey privkey.pem \
-in cert.pem \
-certfile chain.pem \
-passout pass:设置一个导出密码
注意:导出密码必须记住,导入时需要输入。如果密码丢失,只能重新转换。
如果是在 Windows 上通过 IIS 生成的 CSR 去 CA 申请的证书,通常直接提供 PFX 下载。
第二步:在 IIS 管理器生成 CSR(如果是新申请)
打开 IIS 管理器,选中服务器节点,双击"服务器证书",右侧操作栏点击"创建证书申请",填写:
- 通用名称(CN):完整域名,如
www.example.com - 组织、组织单位、城市、省、国家:按实际情况填写
- 加密服务提供程序和位长:选 RSA,位长 2048 或更高
生成的 CSR 文本文件提交给 CA(把内容原样复制粘贴到 CA 的申请框)。
CA 签发后下载证书文件,回到"服务器证书"界面,点击"完成证书申请",选择证书文件并填写一个好记的名称。
第三步:导入已有 PFX
方法一,图形界面:在"服务器证书"界面右侧点击"导入",选择 PFX 文件,输入导出密码,勾选"允许导出此证书"(如果还需要备份导出的话,生产环境一般建议不选以增强安全性)。
方法二,PowerShell(适合批量或无人值守):
$pwd = ConvertTo-SecureString -String '导出密码' -Force -AsPlainText
Import-PfxCertificate -FilePath C:\cert\certificate.pfx `
-CertStoreLocation Cert:\LocalMachine\My `
-Password $pwd
导入后确认证书在本地计算机的个人存储区:
Get-ChildItem Cert:\LocalMachine\My | Select-Object Subject, NotAfter, Thumbprint | Format-List
注意 NotAfter 日期,这决定了何时需要续期。
第四步:绑定 HTTPS
在 IIS 管理器左侧展开站点,选中目标站点,右侧点击"绑定",添加:
- 类型:https
- IP 地址:全部未分配(或指定 IP)
- 端口:443
- 主机名:填写域名,如
www.example.com(必须使用 SNI,勾选"需要服务器名称指示") - SSL 证书:选择刚才导入的证书
关于 SNI 勾选:只有勾选了"需要服务器名称指示",才能在一台 IIS 上为多个域名绑定不同的证书。不勾选的话,该 IP 加端口组合只能有一张证书,第二个站点绑定时会冲突或拿到错误的证书。
勾选主机名后,如果希望用户不带 www 也能访问,需要额外加一条绑定。
命令行方式绑定(脚本化场景):
Import-Module WebAdministration
New-WebBinding -Name "站点名称" -Protocol https -Port 443 -IPAddress "*" -HostHeader "www.example.com" -SslFlags 1
# SslFlags 1 表示使用 SNI
然后把证书关联到绑定:
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like '*example.com*' } | Select-Object -First 1
$binding = Get-WebBinding -Name "站点名称" -Protocol https -Port 443 -HostHeader "www.example.com"
$binding.AddSslCertificate($cert.Thumbprint, "My")
第五步:放行防火墙并配置跳转
Windows 防火墙放行 443:
New-NetFirewallRule -DisplayName "HTTPS Inbound" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow -Profile Any
HTTP 跳转 HTTPS 在 IIS 上通常通过 URL Rewrite 模块实现(需要先安装 URL Rewrite 模块)。在站点根目录的 web.config 中加入:
那个 negate="true" 的 .well-known 条件是为了让 ACME 验证路径不被跳转,保证将来能续期。
配置完成后验证:
Invoke-WebRequest -Uri "https://www.example.com" -UseBasicParsing | Select-Object StatusCode
还要在实际浏览器里打开确认没有证书告警,并检查锁形图标里的证书信息。
证书链不完整与常见故障
症状:某些浏览器或工具报证书不受信任,但另一些正常;命令行 curl 报错而浏览器没问题。
原因:只配置了叶子证书,缺少中间证书。浏览器的自动下载中间证书机制(AIA 扩展)掩盖了这个问题,而其他客户端不会。
解决:确保 ssl_certificate 指向包含中间证书的 fullchain 文件。
# 检查服务端返回的证书数量
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
另一个常见问题:主机名不匹配。浏览器的报错信息是 NET::ERR_CERT_COMMON_NAME_INVALID。检查证书包含的 SAN:
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -text | grep -A1 'Subject Alternative Name'
确认你访问的域名在列表里。注意 example.com 和 www.example.com 是两个不同的名字。
还有一个陷阱:IPv6。 如果域名有 AAAA 记录但新服务器没配 IPv6 的 HTTPS 监听,IPv6 优先的用户会连接失败。检查 Nginx 里有没有 listen [::]:443 ssl;。
常见问题
证书签下来了但浏览器提示不安全,第一步查什么?
先看报错的具体类型。是域名不匹配就查 SAN;是不受信任就查证书链是否完整;是过期就查续期任务;是混合内容(Mixed Content)则说明页面里还有 http:// 的资源,需要改程序里的资源链接。
混合内容具体怎么排查?
浏览器开发者工具的控制台会列出所有被阻止的混合资源。常见位置是数据库中已保存的富文本内容、程序里写死的资源域名、以及第三方统计或广告脚本。修复方式是把资源链接改为 HTTPS 或使用 // 协议相对形式。
Let's Encrypt 证书到期会自动续期吗?
只有当 Certbot 的定时器存在且续期命令实际执行成功时才会。必须用 certbot renew --dry-run 验证。容器化或特殊安装方式下定时器可能不存在,需要自己加定时任务。
免费证书和商业证书在加密强度上有区别吗?
没有。只要都是有效的 RSA 2048 位或 ECDSA 证书,TLS 握手的加密强度相同。区别在验证强度、保障条款、支持服务和签发策略,不在算法本身。
多台服务器要用同一张证书怎么办?
一种做法是在一台机器上签发后,把 /etc/letsencrypt 整个目录同步到其他机器(私钥需要安全传输)。另一种做法是让每台机器用自己的 DNS-01 验证独立签发。要注意的是:如果使用 HTTP-01 验证,流量被负载均衡分发到没有验证文件的那台机器时会失败,这种情况下应选 DNS-01。
可以把 HTTP 和 HTTPS 写在同一个 server 块里吗?
可以,但不推荐做法是把 ssl 相关指令放到非 443 的 server 里导致意想不到的行为。更清晰的方式是分成两个 server 块:80 端口做跳转和 ACME 放行,443 端口提供 HTTPS 服务。
总结
- 证书类型按验证强度选 DV 足够绝大多数场景,通配符证书注意不覆盖裸域名;Nginx 的
ssl_certificate必须指向 fullchain,否则会出现"部分客户端不受信任"的证书链不完整问题。 - 协议只保留 TLSv1.2 和 TLSv1.3,
ssl_prefer_server_ciphers设为 off,启用会话复用和 OCSP 装订提升握手性能。 - HTTP 跳转用
return 301 https://$host$request_uri,并且必须为/.well-known/acme-challenge/保留例外,否则三个月后续期会失败。 - HSTS 要循序渐进加长 max-age,加
includeSubDomains前确认所有子域都支持 HTTPS,preload提交后难以撤销,不要贸然使用。 - 证书签发后必须执行
certbot renew --dry-run验证续期链路,并配合 deploy hook 重载 Nginx,让新证书真正生效。 - IIS 侧需要先把 PEM 转成 PFX 再导入到
LocalMachine\My,绑定时勾选"需要服务器名称指示"(对应-SslFlags 1)才能在一台服务器上承载多张证书。
企业QQ咨询




