ElIauk

Nginx 配置 HTTPS 与证书续期

证书从哪来 给网站配 HTTPS,第一步是拿到证书。常见来源有三类: Let's Encrypt 免费证书:有效期 90 天,支持自动续期,个人项目和小团队用得最多; 云厂商的免费或付费证书:在控制台申请,下载后上传到服务器,有效期一年,到期要手动换; 企业 CA 签发的证书:需要提交资料审核,适合

评论
Nginx 配置 HTTPS 与证书续期

证书从哪来

给网站配 HTTPS,第一步是拿到证书。常见来源有三类:

  1. Let's Encrypt 免费证书:有效期 90 天,支持自动续期,个人项目和小团队用得最多;
  2. 云厂商的免费或付费证书:在控制台申请,下载后上传到服务器,有效期一年,到期要手动换;
  3. 企业 CA 签发的证书:需要提交资料审核,适合对外商业站点。

Let's Encrypt 的优势是自动化。用 certbot 申请,配合 cron 或 systemd timer 续期,基本可以做到"配一次,长期不用管"。缺点是有速率限制,反复申请同一个域名会被临时封禁。

申请方式推荐 webroot,不打断现有 Nginx:

sudo certbot certonly --webroot \
  -w /var/www/certbot \
  -d example.com -d www.example.com

前提是 Nginx 里已经把 /.well-known/acme-challenge/ 映射到 /var/www/certbot。证书会生成在 /etc/letsencrypt/live/example.com/ 下,其中 fullchain.pem 是证书加中间证书,privkey.pem 是私钥。

443 配置

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    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_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    add_header Strict-Transport-Security "max-age=31536000" always;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

几个选择说明:

  • ssl_certificate 用 fullchain.pem,不要用 cert.pem。中间证书缺失时,浏览器可能正常,但 Android、Java 客户端会报证书链不完整;
  • 只开 TLSv1.2 和 TLSv1.3,老版本协议有已知漏洞;
  • ssl_session_cache 减少重复握手,对高并发有帮助;
  • HSTS 告诉浏览器以后强制走 HTTPS,配置前确认所有子域名都支持 HTTPS,否则会把用户挡在门外。

证书路径要写 live/ 下的符号链接,不要直接指向 archive/ 里的实际文件。certbot 每次续期都会生成带序号的新文件,live/ 下的链接会自动指向最新版本,Nginx reload 后就能加载到新证书;写死 archive 路径,续期后链接的还是旧文件。

后端如果是 Spring Boot,记得让它知道原始协议:

server:
  forward-headers-strategy: framework

否则应用生成的绝对链接、重定向地址可能还是 http://,前端会遇到混合内容警告。

自动续期

先手动跑一次演练,确认续期流程没问题:

sudo certbot renew --dry-run

正式续期用 cron:

0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

--deploy-hook 只在证书真正更新时执行,避免每天无意义地 reload。也可以直接用 systemctl list-timers | grep certbot 查看系统自带的定时器,很多发行版安装 certbot 时已经配好了。

续期之后一定要 reload,否则 Nginx 还在用内存里的旧证书,浏览器看到的还是过期证书。

常见坑

1. 证书链不完整

只配了站点证书,没配中间证书。桌面浏览器可能自动补全,移动端和 Java 客户端直接握手失败。统一用 fullchain.pem。

2. 域名不匹配

证书只签了 example.com,用户访问 www.example.com 就报 NET::ERR_CERT_COMMON_NAME_INVALID。申请时把裸域和 www 都带上,或者用通配符证书。

3. 续期成功但没 reload

certbot certificates 显示证书有效期已经更新,openssl 看到的还是旧的。检查 --deploy-hook 是否配置、reload 命令是否有权限执行。

4. 80 端口被关闭导致验证失败

Let's Encrypt 的 HTTP-01 验证需要访问 80 端口。如果防火墙只开了 443,续期会失败。保留 80 端口做跳转和验证,或者改用 DNS-01 验证。

5. 服务器时间不准

证书有效期校验依赖系统时间。时间偏差太大,会出现"证书未生效"或"证书已过期"的误报。开启 NTP 同步,timedatectl 确认时间状态。

6. 混合内容

页面是 HTTPS,里面的图片、脚本、接口还是 HTTP,浏览器会拦截。检查后端返回的链接、前端写死的资源地址,统一改成相对路径或 HTTPS。

7. HSTS 配得太激进

max-age 设成一年且包含子域名,一旦某个子域名没有 HTTPS,用户会直接无法访问,而且很难回退。先在测试域名上验证,再逐步加长。

排查步骤

证书问题先用命令行确认,不要只看浏览器:

# 查看证书链、有效期、域名
openssl s_client -connect example.com:443 -servername example.com </dev/null \
  | openssl x509 -noout -subject -issuer -dates

# 看完整的证书链
openssl s_client -showcerts -connect example.com:443 -servername example.com </dev/null

# 确认 HTTP 跳转
curl -I http://example.com

# 确认 HTTPS 响应
curl -vI https://example.com

如果 openssl 显示证书正常、浏览器报错,多半是证书链或本地时间问题;如果 curl 报 SSL certificate problem,看错误码里的 unable to get local issuer certificate(缺中间证书)还是 certificate has expired(过期)。再看 nginx -t 和 error.log,确认 Nginx 加载的是哪份证书文件。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者