Nginx 反向代理 + SSL 实战:Certbot 免费证书配置全指南

Nginx 反向代理 + SSL 实战:Certbot 免费证书配置全指南

目录

  1. 一、为什么需要反向代理
  2. 二、反向代理基础配置
  3. 三、Let’s Encrypt 免费 SSL:Certbot 安装与申请
  4. 四、SSL 安全加固
  5. 五、自动续期:90 天证书不用手动换
  6. 六、常见踩坑与解决
  7. 七、完整验证流程

一、为什么需要反向代理

很多人在部署 Web 服务时,习惯直接让应用监听 80/443 端口。但实际生产环境中,Nginx 作为反向代理几乎是标准做法——它负责处理 SSL termination、静态资源缓存、负载均衡,而背后的应用服务(如 WordPress、Django、Node.js)只需要监听本地端口。

直接让应用监听 443 听起来简单,但实际问题一堆:

场景 直接暴露应用 Nginx 反向代理
SSL 证书管理 每个应用各自配 统一在 Nginx 层管理
静态资源 应用自行处理 Nginx 直接服务,节省应用资源
端口隐藏 内部服务端口暴露在外 只开放 80/443,数据中心网络更安全
负载均衡 应用自带难搞 upstream 轻松扩展
HTTP/2、缓存控制 应用支持参差不齐 Nginx 统一处理

WordPress 还有一个特殊问题:REST API 的认证依赖 Authorization header。如果 Nginx 没有正确转发这个 header,所有基于 Application Passwords 的 API 调用(创建文章、上传媒体)都会返回 401。这是最常见的 WordPress + Nginx 部署坑。


二、反向代理基础配置

完整配置文件

以 WordPress 部署在 127.0.0.1:21080 为例,完整配置如下:

# /etc/nginx/conf.d/wordpress.conf

upstream wordpress_backend {
    server 127.0.0.1:21080;
    keepalive 32;
}

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # HTTP -> HTTPS 跳转(推荐 301 永久重定向)
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    # SSL 证书(Certbot 自动写入)
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # 安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # 反向代理核心配置
    location / {
        proxy_pass http://wordpress_backend;
        proxy_http_version 1.1;

        # —— 以下 header 缺一不可 ——

        # 1. Host:让后端知道请求的原始域名
        proxy_set_header Host $host;

        # 2. X-Real-IP:后端记录真实访客 IP,而不是 Nginx 的 IP
        proxy_set_header X-Real-IP $remote_addr;

        # 3. X-Forwarded-For:传递经过的每一个代理 IP 链
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 4. X-Forwarded-Proto:后端区分真实协议(http 还是 https)
        proxy_set_header X-Forwarded-Proto $scheme;

        # 5. —— 最容易踩坑的:Authorization header —— #
        # 没有这行,所有 WordPress REST API 的认证请求全部 401
        proxy_set_header Authorization $http_authorization;

        # 6. 升级 WebSocket(如果用到)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # 超时控制
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # 静态资源由 Nginx 直接服务,不走代理
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

逐行解释关键 header

很多人知道要配 proxy_set_header,但不清楚每一个的实际作用:

proxy_set_header Host $host

Nginx 默认会把自己的 proxy_pass 地址当作 Host 发送给后端。比如你 proxy_pass http://127.0.0.1:21080,后端收到的 Host 就是 127.0.0.1:21080,WordPress 生成的链接全变成了内网地址。$host 变量保留原始请求的域名。

proxy_set_header X-Real-IP $remote_addr

Nginx 是反向代理,后端看到的连接来源全是 127.0.0.1$remote_addr 是真实访客的 IP,把它放进 header 让后端能够记录真实访客。

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for

如果访客经过了多层代理(比如 A -> B -> Nginx -> 后端),这个 header 会累积整条链路的所有 IP:client_ip, proxy1_ip, proxy2_ip$proxy_add_x_forwarded_for 会自动追加当前层的 IP。

proxy_set_header X-Forwarded-Proto $scheme

后端应用看到的所有请求都是 http(因为它确实跑在 http 上)。$scheme 会保留原始协议(https),这样 WordPress 才能生成正确的链接。

proxy_set_header Authorization $http_authorization

这是 WordPress REST API 认证失败最常见的原因。Nginx 默认不会转发非标准 header(以 X- 开头的和 Authorization)。不写这行,Authorization: Basic xxx 到后端就变成空值,所有 API 新建/编辑/删除操作全部 401。


三、Let’s Encrypt 免费 SSL:Certbot 安装与申请

Let’s Encrypt 提供永久免费的 DV SSL 证书,有效期 90 天,Certbot 自动续期后实际永久有效。

方式一:certonly(不修改 Nginx 配置)

适合手动管理 Nginx 配置的场景:

# 安装 certbot(snap 方式,自动获取最新版本)
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# 申请证书(仅获取证书,不修改 Nginx 配置)
sudo certbot certonly --nginx -d yourdomain.com -d www.yourdomain.com

按提示输入邮箱(用于过期通知),同意条款,稍等片刻证书就到手了。

方式二:–nginx(自动写入 Nginx 配置)

Certbot 直接修改 Nginx 配置文件,插入 SSL 配置和 80→443 重定向,适合首次配置

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

执行后 Certbot 会自动:

  1. 验证域名所有权(通过 HTTP/.well-known/acme-challenge/)
  2. 申请证书
  3. 修改 Nginx 配置,插入 listen 443 ssl http2 和证书路径
  4. 生成 HTTP→HTTPS 跳转配置

验证证书申请成功

sudo certbot certificates

输出类似:

Certificate Name: yourdomain.com
  Serial Number: xxx
  Key Type: RSA
  Domains: yourdomain.com www.yourdomain.com
  Expiry Date: 2026-08-06 12:34:56+00:00 (89 days)
  Certificate Path: /etc/letsencrypt/live/yourdomain.com/fullchain.pem
  Private Key Path: /etc/letsencrypt/live/yourdomain.com/privkey.pem

四、SSL 安全加固

Certbot 自动生成的配置能用,但安全配置偏保守。下面是生产环境推荐加固项:

TLS 版本控制

只启用 TLS 1.2 和 1.3,禁用 TLS 1.0/1.1 和所有 SSL 版本

ssl_protocols TLSv1.2 TLSv1.3;

加密套件选择

使用 Mozilla SSL Configuration Generator 的”Modern”级别:

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 off;

会话复用

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

HSTS(HTTP 严格传输安全)

强制浏览器只通过 HTTPS 访问,从根本上避免 HTTP 降级攻击:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

注意:开启 HSTS 后必须保证所有子域名都支持 HTTPS,否则子域名会无法访问。先用 max-age=300 测试一周,确认无误再放大。

OCSP Stapling

让服务器直接提供 CRL 检查结果,省去浏览器自己去 CA 查询,提升 HTTPS 连接速度:

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

完整 SSL 配置块示例

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # TLS 加固
    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 off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # 安全响应头
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # 反向代理(省略同前文)
    location / {
        proxy_pass http://wordpress_backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Authorization $http_authorization;
    }
}

验证 SSL 评分

配置完成后,去 SSL Labs SSL Test 输入域名,检查是否达到 A+A


五、自动续期:90 天证书不用手动换

Let’s Encrypt 证书 90 天有效期,Certbot 带自动续期,但需要验证定时任务已生效:

# 测试续期流程(dry-run 不会真的续)
sudo certbot renew --dry-run

成功输出:

** DRY RUN: simulating 'certbot renew' close enough to determine
Congratulations, all renewals succeeded.

定时任务确认

Certbot 安装时会自动写入 systemd timer 或 cron。检查一下:

# systemd timer 方式(Ubuntu 18.04+)
sudo systemctl list-timers | grep certbot

如果没有自动创建,手动加 cron:

sudo crontab -e
# 每天凌晨 3 点检查,到期自动续
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

--deploy-hook 是关键:续期成功后自动 reload Nginx,让新证书生效。


六、常见踩坑与解决

坑 1:WordPress REST API 返回 401

症状GET /wp-json/wp/v2/posts 正常,但 POST /wp-json/wp/v2/posts 返回 401。

原因:Nginx 没有转发 Authorization header。

解决:在 location / 里加上:

proxy_set_header Authorization $http_authorization;

然后 sudo nginx -t && sudo systemctl reload nginx

坑 2:申请证书时报 Timeout during connect(域名验证超时)

原因:Let’s Encrypt 服务器无法访问 http://yourdomain.com/.well-known/acme-challenge/xxx

排查

# 本地验证 Nginx 是否正常服务 .well-known 路径
curl -v http://yourdomain.com/.well-known/acme-challenge/test

解决:确认防火墙开放 80 端口;确认 Nginx 配置里没有把 .well-known 路径重定向到 HTTPS(Certbot 验证时只能是 HTTP)。

坑 3:多个子域名申请一张证书

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com -d blog.yourdomain.com

所有域名共享同一张证书,不需要分别申请。

坑 4:proxy_pass 用 localhost 还是 127.0.0.1

在容器环境(如 Docker)中,localhost 有时解析到 IPv6 ::1,而应用只监听 IPv4。

建议:始终用 127.0.0.1 或 upstream 名称,避免歧义。

# 避免
proxy_pass http://localhost:21080;   # 可能走 IPv6

# 推荐
proxy_pass http://127.0.0.1:21080;
# 或
proxy_pass http://wordpress_backend;  # upstream 中定义

坑 5:Nginx 配置变更后不生效

# 每次改完必须验证语法
sudo nginx -t

# 语法无误再重载
sudo systemctl reload nginx

nginx -t 能发现大多数配置错误,避免重载失败导致服务中断。

坑 6:续期成功但证书路径变了

用 Certbot 的 --deploy-hook 确保每次续期后 reload Nginx:

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

如果不用 --deploy-hook,即使证书文件更新了,Nginx 还在用旧证书。


七、完整验证流程

配置完成后,按以下步骤逐一验证:

# 1. Nginx 语法检查
sudo nginx -t

# 2. 重载配置
sudo systemctl reload nginx

# 3. HTTP→HTTPS 跳转验证
curl -I http://yourdomain.com
# 期望:HTTP/1.1 301 Moved Permanently,Location: https://...

# 4. HTTPS 证书信息
curl -Ik https://yourdomain.com
# 期望:SSL handshake successful,证书域名匹配

# 5. WordPress REST API 认证
curl -s -o /dev/null -w "%{http_code}" \
  -u "252009134:YOUR_APP_PASSWORD" \
  https://yourdomain.com/wp-json/wp/v2/users/me
# 期望:200(返回用户信息)

# 6. SSL Labs 评分
# 浏览器访问 https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
# 期望:A+ 或 A

# 7. 续期 dry-run
sudo certbot renew --dry-run
# 期望:Congratulations, all renewals succeeded

总结

Nginx 反向代理 + SSL 不是”锦上添花”,而是生产环境的标准配置。它带来的价值:统一入口、隐藏内部结构、简化证书管理、更好的性能和安全性。

最核心的 takeaways:

  1. proxy_set_header Authorization $http_authorization — WordPress API 认证能否工作的分水岭
  2. snap install certbot — 最省心的安装方式,自动更新
  3. --deploy-hook "systemctl reload nginx" — 证书续期后必须自动重载
  4. nginx -t 验证语法 — 每次改配置后必做

搞懂这些,生产环境的 Web 服务部署基本不会再踩坑。

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注