-今日
-本周
-本月
-累计
项目工作台运维 个人项目与实践记录
我的手记第 9 篇 / 共 9 篇
→ 下一篇

手记 009:从 HTTP 到 HTTPS

域名解析✅ 已指向服务器
80 端口✅ 安全组已放行,公网可达
443 端口❌ 待放行
系统Alibaba Cloud Linux 3(RHEL 8 兼容)

日期: 2026-09-24 用时: 约 90 分钟 难度: ⭐⭐⭐☆☆


1. 背景

ICP 备案通过,域名解析已生效,网站可以通过 http://myzhiwei.xyz 访问。

本次要做的:申请证书、配置 HTTPS、让 HTTP 自动跳转。

前置条件

状态
域名解析✅ 已指向服务器
80 端口✅ 安全组已放行,公网可达
443 端口❌ 待放行
系统Alibaba Cloud Linux 3(RHEL 8 兼容)

2. 证书方案的选择

2.1 为什么用 Let's Encrypt

方案成本有效期自动续期
Let's Encrypt免费90 天✅ 支持
商业 DV 证书几百元/年1 年手动
自签名免费自定义

自签名不能用:浏览器会显示安全警告,需要用户手动信任。对公开站点没有意义。

Let's Encrypt 的取舍:证书只有 90 天 —— 听起来短,但配合自动续期反而更安全(密钥泄露窗口更小)。

2.2 安装

yum install -y certbot python3-certbot-nginx

两个包的分工

作用
certbot核心程序
python3-certbot-nginxnginx 插件,用于自动完成 HTTP-01 验证

版本:certbot 1.22.0(EPEL 源)


3. 申请证书

3.1 先用 --dry-run 验证

certbot certonly --nginx \
  -d myzhiwei.xyz -d www.myzhiwei.xyz \
  --dry-run

--dry-run 做什么

1. 连接 Let's Encrypt 的测试环境
2. 请求一个验证 token
3. 临时改 nginx 配置,添加验证用的 location
4. reload nginx
5. 让 Let's Encrypt 从外部访问验证 URL
6. 验证成功后还原配置
7. 报告结果(不签发真实证书)

结果

Account registered.
Simulating a certificate request for myzhiwei.xyz and www.myzhiwei.xyz
The dry run was successful.

这一句话证明了几件事

验证项说明
域名解析正确Let's Encrypt 能找到你的服务器
80 端口公网可达从外网访问到了验证 URL
nginx 配置正确能处理 ACME 挑战
安全组规则正确没有被防火墙拦截

3.2 正式申请

certbot certonly --nginx \
  -d myzhiwei.xyz -d www.myzhiwei.xyz \
  --agree-tos \
  --email <邮箱> \
  --non-interactive

为什么用 certonly 而不是直接 certbot --nginx

命令行为
certbot --nginx会自动改写 nginx 配置(加 443 server 块、改跳转)
certbot certonly --nginx只签发证书,配置自己写

本机的 nginx 配置是自己维护的(多 server 块 + 限流 + 反向代理), 让 certbot 自动改写可能和现有结构冲突。所以选择只拿证书。

结果

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/myzhiwei.xyz/fullchain.pem
Key is saved at:          /etc/letsencrypt/live/myzhiwei.xyz/privkey.pem

证书文件

文件用途
fullchain.pem证书链(服务器证书 + 中间证书)← nginx 用
privkey.pem私钥 ← nginx 用
cert.pem仅服务器证书(一般不用)
chain.pem仅中间证书

为什么用 fullchain 而不是 cert

浏览器需要验证证书链:
  Let's Encrypt 根证书(浏览器内置)
      ↓ 签发
  中间证书(不在浏览器里)
      ↓ 签发
  你的域名证书

如果只发 cert.pem(域名证书),浏览器找不到中间证书
→ 报 "证书链不完整"

fullchain.pem = 域名证书 + 中间证书,一次发全

4. nginx 配置改造

4.1 改造前的结构问题

原来的配置有两个 server 块(80 和 8080),location 规则重复写了两遍

server {
    listen 80 default_server;
    server_name _ myzhiwei.xyz www.myzhiwei.xyz;   ← 通配 + 域名混在一起
    # ... 20 行 location 规则 ...
}

server {
    listen 8080;
    server_name _;
    # ... 同样的 20 行 location 规则 ...
}

两个问题

问题一:server_name 混了 _ 和具体域名

server_name _ myzhiwei.xyz www.myzhiwei.xyz;
            └┬┘ └────────┬────────────────┘
        通配(兜底)   具体域名

nginx 的 server_name 匹配顺序:精确名称 → 通配开头 → 通配结尾 → 正则 → default_server。

_ 在 nginx 里不是通配符,它只是一个永远不匹配的普通字符串。 真正让它生效的是那个 server 块上的 default_server 标记。

这就导致无法区分"域名访问"和"IP 访问" —— 两种请求都落进同一个 server 块, 没法只对域名做 HTTPS 跳转。

问题二:location 规则重复

80 和 8080 两个块各自写了一遍限流、gzip、缓存、安全头。

配置文件的注释里当时就写了一句:

下一步优化时再抽成 include(这也是一个很好的练习点)

这次正好一起做掉。

4.2 改造后的结构

/etc/nginx/conf.d/
├── personal-site.conf
│   ├── server { listen 80; server_name myzhiwei.xyz www.myzhiwei.xyz; → 301 }
│   ├── server { listen 80 default_server; server_name _; → HTTP 服务 }
│   ├── server { listen 443 ssl http2; → HTTPS 主服务 }
│   └── server { listen 8080; server_name _; → HTTP 备用 }
└── snippets/
    └── site-common.conf        ← 共用规则(被后三个 server 块 include)

关键设计

设计理由
域名和 default 分成两个 server 块才能只对域名做跳转
抽成 snippets限流/缓存/gzip/安全头只维护一份
IP 入口保持 HTTP证书不含 IP,走 HTTPS 会报证书名不匹配

关于最后一条

证书签发给 → myzhiwei.xyz
用户访问   → https://<IP>
浏览器检查 → "证书是给 myzhiwei.xyz 的,但你访问的是 <IP>"
          → 报错 "证书名称不匹配"

所以 default_server 那个块保持 HTTP,不跳转。

4.3 跳转用 $host 而不是 $server_name

return 301 https://$host$request_uri;

两个变量的区别

变量问题
$server_name写死的第一个名称访问 www 也会跳到裸域名
$host客户端请求的 Host保留 www

$host 后:

http://myzhiwei.xyz     → https://myzhiwei.xyz
http://www.myzhiwei.xyz → https://www.myzhiwei.xyz     ← www 保留

4.4 TLS 参数

ssl_protocols       TLSv1.2 TLSv1.3;
ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;
ssl_session_tickets off;

逐项说明

配置作用
只启用 TLSv1.2/1.3TLSv1.0/1.1 已被主流浏览器标记为不安全
只用 ECDHE + GCMECDHE 提供前向保密;GCM 是认证加密模式
ssl_prefer_server_ciphers off让客户端选(现代实践 —— 客户端更了解自己的硬件)
ssl_session_tickets off会话票据用静态密钥加密会话,关闭可避免该密钥泄露导致的历史会话解密

前向保密(Forward Secrecy)的意义

没有前向保密:
  攻击者今天录下加密流量 → 将来拿到服务器私钥 → 解密出历史通信内容

有前向保密(ECDHE):
  每次会话用临时密钥协商 → 即使服务器私钥泄露
  → 也无法解密之前录下的流量

4.5 HSTS

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

作用:告诉浏览器"这个域名一年内只用 HTTPS 访问"。

第一次访问 https://myzhiwei.xyz
    ↓
浏览器记住 HSTS 策略
    ↓
以后即使输入 http://myzhiwei.xyz
    ↓
浏览器【在本地】就把地址改写成 https://,根本不发 HTTP 请求

好处:防止 SSL 剥离攻击(中间人把 HTTPS 链接改成 HTTP)。

⚠️ 注意事项

说明
max-age 设长要谨慎一旦下发,浏览器会记住一年,期间 HTTPS 出问题会无法访问
建议先用短 max-age 测试比如 300 秒,确认没问题再调长
本机直接用了 1 年因为证书和续期都已验证通过

如果将来 HTTPS 出问题怎么办:只能在浏览器里手动清除 HSTS 记录, 或者换域名。所以启用 HSTS 前要确保 HTTPS 稳定。


5. 自动续期

5.1 续期链路

certbot-renew.timer(每 12 小时)
    ↓
检查证书剩余有效期
    ↓ 剩余 < 30 天
certbot renew 申请新证书
    ↓ 成功
执行 deploy hook
    ↓
systemctl reload nginx
    ↓
nginx 加载新证书

timer 配置

[Timer]
OnCalendar=*-*-* 00/12:00:00      # 每天 00:00 和 12:00
RandomizedDelaySec=12hours        # 随机延迟(避免全球客户端同时请求 Let's Encrypt)
Persistent=true                   # 关机期间错过的任务,开机后补做

为什么用 RandomizedDelaySec:Let's Encrypt 的服务端要处理全球数百万证书的续期请求。 如果所有客户端都在整点请求,会造成流量尖峰。随机延迟把这个压力摊平。

5.2 deploy hook

为什么必须要有这个 hook

证书文件更新了
    ↓
但 nginx 只在【启动】或【reload】时读取证书文件
    ↓
不 reload → nginx 仍用内存里的旧证书
    ↓
结果:证书明明续期成功,浏览器仍显示"证书已过期"

hook 内容

#!/bin/bash
systemctl reload nginx
logger -t certbot-hook "证书已续期,nginx 已重载"

logger 的作用:把记录写进 syslog,方便事后排查"续期到底有没有执行"。

验证方式

# 直接执行 hook
bash /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

# 检查 syslog
journalctl -t certbot-hook -n 5

关于 --dry-run 是否执行 hook

certbot renew --dry-run 不会执行 deploy hook —— 所以输出里看不到 Running deploy-hook command。这是正常的,验证 hook 要单独直接执行。


6. 验证

6.1 功能验证

# HTTP 跳转
curl -I http://myzhiwei.xyz/
HTTP/1.1 301 Moved Permanently
Location: https://myzhiwei.xyz/
# HTTPS 访问与证书验证
curl -s -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://myzhiwei.xyz/
200 0

ssl_verify_result=0 表示证书验证通过(0 是 OpenSSL 的成功码, 非 0 是各种验证失败的原因码)。

6.2 证书详情

echo | openssl s_client -connect myzhiwei.xyz:443 -servername myzhiwei.xyz 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates
subject=CN = myzhiwei.xyz
issuer=C = US, O = Let's Encrypt, CN = YR2
notBefore=Sep 24 07:42:51 2026 GMT
notAfter=Dec 23 07:42:50 2026 GMT

注意 -servername 参数:这叫 SNI(Server Name Indication)。 一台服务器可能承载多个域名,SNI 告诉服务器"我要访问哪个域名", 服务器才能返回对应的证书。

6.3 TLS 版本

for p in tls1_2 tls1_3; do
  echo | openssl s_client -$p -connect myzhiwei.xyz:443 2>/dev/null | grep Protocol
done
TLSv1.2     Cipher: ECDHE-RSA-AES256-GCM-SHA384
TLSv1.3     Cipher: TLS_AES_256_GCM_SHA384

两个版本都支持,加密套件都是强套件。

6.4 验证结果汇总

检查项结果
HTTP → HTTPS301 ✅
HTTPS 访问200 ✅
证书验证0(通过)✅
TLS 版本1.2 + 1.3 ✅
HSTSmax-age=31536000 ✅
IP 直连保持 HTTP 200(不跳转)✅
Tomcat 代理链路未受影响 ✅

7. 遇到的问题

问题一:OCSP Stapling 警告

nginx -t
nginx: [warn] "ssl_stapling" ignored, no OCSP responder URL in the certificate

OCSP Stapling 是什么:一种优化机制 —— 服务器提前向 CA 查询证书吊销状态, 把结果(签名的响应)随 TLS 握手一起发给客户端, 省去客户端自己去查 OCSP 服务器的往返。

为什么警告

ssl_stapling on;  → nginx 需要从证书里读 OCSP Responder URL
     ↓
但证书里没有这个字段
     ↓
无法查询 → 配置被忽略 → 警告

根因Let's Encrypt 自 2025 年起不再在证书中提供 OCSP URL, 改用 CRL(证书吊销列表)机制。

这是 CA 侧的策略变化,不是配置错误。

处理:移除那三行配置。

# 修改前
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/myzhiwei.xyz/chain.pem;
resolver 223.5.5.5 119.29.29.29 valid=300s;
resolver_timeout 5s;

# 修改后(只留说明注释)
# ---- OCSP Stapling:未启用 ----
# Let's Encrypt 自 2025 年起不再在证书中提供 OCSP Responder URL
# (改用 CRL 机制),因此 ssl_stapling 不生效且会产生 nginx -t 警告。
# 移除相关配置,使「配置内容」与「实际行为」一致。

判断依据

配置应该反映实际生效的行为。 保留一条不生效的指令,会让人误以为 OCSP 在工作。

问题二:编辑长配置时终端断连

现象:粘贴 60+ 行的 cat > file <<'EOF' 时,SSH 会话中断, 命令只执行了一部分。

后果:不确定配置文件是否被写坏。

排查

nginx -t                                    # 先确认配置没坏
head -20 /etc/nginx/conf.d/personal-site.conf  # 看文件内容

结果nginx -t 通过,文件是旧内容(说明 cat 没执行)。

改进方式把长配置写成脚本文件,执行脚本而不是粘贴

这个教训的一般形式

场景风险
粘贴长命令断连导致部分执行
分批执行 + 每步验证✅ 安全

后来又遇到一次类似情况:一个脚本因为 grep -c 返回值处理不当(空结果导致 [ "$N" -eq 0 ] 报错)而中断。同一类问题(输出解析不严谨)在项目里出现了多次。


8. 遗留


9. 后续计划

  1. 等公安联网备案审核结果
  2. 检查日志保留期是否满足《网络安全责任告知书》的要求
  3. 整理端口规划(443 已加入)
  4. 继续系统练习 Linux 命令

10. 协作说明

本次的命令与配置内容由 AI 提供,执行和判断由我做。

两处需要自己判断的地方:

一是 certonly 与默认模式的选择。certbot 的 nginx 插件默认会改写配置, 但本机配置结构是自己维护的(多 server 块 + 限流 + 反向代理), 让工具自动改写的风险大于收益 —— 所以选择只拿证书、自己配 nginx。

二是那个 OCSP 警告的处理。看到警告时的第一反应是"配置写错了", 但查证后发现是 CA 侧策略变化(Let's Encrypt 移除 OCSP URL)。 判断"这是配置问题还是外部变化"需要先理解 OCSP 的机制。

我的手记第 9 篇 / 共 9 篇
→ 下一篇

可用键盘 ← → 切换上下篇