一台轻量服务器从零到 HTTPS:Nginx 站点配置实操记录
站点文件早早传上去了,域名解析也做了,浏览器里却一直是个默认页。
这篇最初是当天的操作记录,从「Nginx 不认识这个站点」到「443 能通、证书能握手」。踩到两个坑:一个在自己手上(tee -a 追加了重复的 server 块),一个在云控制台(安全组没放 443)。都写下来了。
后来站点真的跑起来,又陆续补了自定义 404、重复 URL 收敛、gzip 和缓存优先级(第六节),一并记在这里——这几处都是被实际访问问题逼出来的,比配置本身更值得记。
文中域名与 IP 已脱敏:
zhi***.online、1.*.*.*。路径里的域名同步做了替换,照抄时全局替换成你自己的即可。配置块里的
ssl_ciphers、证书路径、目录结构没有打码——这些不是秘密。密码套件是 TLS 握手里明文协商的参数,任何人openssl s_client连一下就看得见;证书本身也是公开信息。真正不能出现在博客里的是私钥内容和密码,本文只写到私钥的路径和权限。域名打码的目的不是藏站点(证书透明度日志里本来就公开),而是防止文章被转载后把「博客 → 服务器 → IP」串成一条可直接扫描的线索。
零、起点:文件在,站点不在
服务器是腾讯云轻量,Ubuntu 24.04,Nginx 1.24。静态站的产物已经放在 /var/www/zhi***.online 下,index.html 就在根目录。
Nginx 的默认行为是:/etc/nginx/sites-enabled/ 下的文件会被 nginx.conf 里的 include 加载,而目录里只有一个 default。所以不管域名解析到哪,请求都落到 /var/www/html。
先确认三件事,省得后面白折腾:
# 1. 文件真的在(-R 看权限,Nginx 需要对目录有 x 权限)
ls -la /var/www/zhi***.online
# 2. Nginx 的加载结构
ls -la /etc/nginx/sites-available/ /etc/nginx/sites-enabled/
grep -n "include" /etc/nginx/nginx.conf
# 3. Nginx 在跑、配置是哪一份
systemctl status nginx --no-pager | head -5
nginx -v
第 2 步的输出基本是这两个目录:
| 目录 | 作用 |
|---|---|
sites-available/ | 所有站点配置的「仓库」,写在这里不生效 |
sites-enabled/ | 真正被加载的目录,放的是指向 available 的软链接 |
这个设计的意义是启停站点 = 建/删一个软链接,不用在配置文件里注释来注释去。Debian/Ubuntu 的 Nginx 包默认就是这套结构,CentOS 的 /etc/nginx/conf.d/ 只有一层,习惯不同但道理一样。
一、写站点配置
在 sites-available/ 下新建一份:
sudo vim /etc/nginx/sites-available/zhi***.online
内容:
server {
listen 80;
listen [::]:80;
server_name zhi***.online www.zhi***.online;
root /var/www/zhi***.online;
index index.html index.htm;
access_log /var/log/nginx/zhi***.online.access.log;
error_log /var/log/nginx/zhi***.online.error.log;
location / {
try_files $uri $uri/ =404;
}
# 静态资源长缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
}
逐行说几个容易含糊的点:
listen [::]:80;单独一行,不是写listen [::]:80 default。少了这行,IPv6 客户端(现在很多移动网络优先走 v6)会连不上,而 IPv4 一切正常——这种「部分用户打不开」最难查。server_name写两个。www和裸域在 DNS 上是两条记录,Nginx 上也必须显式列出,否则访问www.会落到默认 server。注意这里没有配跳转,两者各回各的,后续应该统一到一个(见第五节)。try_files $uri $uri/ =404:先找文件,再找同名目录(触发目录下的 index),都没有就返回 404。不要写成try_files $uri /index.html——那是 SPA 的写法,静态站这么做会让所有不存在的 URL 都返回 200,搜索引擎会收录一堆垃圾页。- 静态资源那条
location ~*用了不区分大小写的正则匹配,优先级在普通前缀匹配location /之上。immutable是告诉浏览器「这个 URL 的内容永不变化」,配合文件名带 hash 的构建产物才安全;如果你的文件名不带 hash,把immutable去掉,只留expires 30d。 - 日志按站点分开写。默认的
access.log混着所有站点,出了问题先得用grep筛域名,很别扭。
二、启用、测试、重载
# 启用(软链接,用绝对路径,别用相对路径)
sudo ln -s /etc/nginx/sites-available/zhi***.online /etc/nginx/sites-enabled/
# 语法测试
sudo nginx -t
# 重载
sudo systemctl reload nginx
三个动作的顺序不能换,尤其是「先 -t 再 reload」——nginx -t 失败时 reload 也会失败,但更糟的情况是配置改坏了而你没测,reload 直接拒绝生效,你还以为上线了。
nginx -t 的正常输出:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
reload 和 restart 的区别值得记一下:
| 动作 | 实际行为 | 什么时候用 |
|---|---|---|
reload | 发 SIGHUP,master 校验配置后新起 worker、老 worker 处理完存量连接再退出 | 改配置的默认选择,连接不断 |
restart | 停掉整个进程再启动,瞬间拒绝所有连接 | 换二进制、改 worker_processes 等无法热加载的项 |
reload 之后老 worker 会优雅退出,所以那一刻 ps 里能同时看到两批 worker,属于正常现象。真想确认配置生效,看进程启动时间:
systemctl show nginx -p ActiveEnterTimestamp
三、证书文件放哪、权限怎么设
证书是从腾讯云控制台申请签发的,下载下来是 Nginx 格式的压缩包,解压后有三个文件:
zhi***.online_bundle.crt # 证书链(站点证书 + 中间 CA)
zhi***.online.key # 私钥
zhi***.online.csr # 申请文件,服务器上不需要
统一放到 /etc/nginx/ssl/ 下,按域名建目录——一台机器挂多个站点时,这个习惯能省掉「哪张证书对应哪个站」的混乱:
sudo mkdir -p /etc/nginx/ssl/zhi***.online_nginx
sudo cp zhi***.online_bundle.crt zhi***.online.key /etc/nginx/ssl/zhi***.online_nginx/
# 私钥只给 root 读
sudo chmod 600 /etc/nginx/ssl/zhi***.online_nginx/zhi***.online.key
sudo chmod 644 /etc/nginx/ssl/zhi***.online_nginx/zhi***.online_bundle.crt
sudo chown -R root:root /etc/nginx/ssl
两个细节:
bundle.crt不是单张证书,它把站点证书和中间 CA 按顺序拼在了一起。只传站点证书、漏掉中间证书,浏览器会报「证书链不完整」,PC 上 Chrome 可能靠缓存补全而看起来没事,手机上直接红警告。用grep -c "BEGIN CERTIFICATE" bundle.crt数一下,正常是 2 或 3。- 私钥权限 600 就够。Nginx 的 master 进程以 root 启动,能读;worker 降权到
www-data也不影响,因为证书是 master 在启动时读取并放进内存的。设成 644 也能跑,但等于把私钥给全机器所有用户看了一遍。
顺带一提:/etc/nginx 整个目录值得用 Git 管起来。
cd /etc/nginx && sudo git init && sudo git add -A && sudo git commit -m "baseline"
以后每次改配置前 git diff 一下,出了事 git checkout 就回来了——比 cp xxx.bak 这种满目录 .bak 文件清爽得多。
四、补上 443:追加 server 块
这次踩的坑:tee -a 会重复追加
当时用的命令是:
sudo tee -a /etc/nginx/sites-available/zhi***.online > /dev/null << 'EOF'
server {
listen 443 ssl;
...
}
EOF
tee -a 是追加,不是覆盖。第一次执行没问题;如果后来重跑了一次(改配置时很常见),文件里就有两个 server_name 相同的 server 块,nginx -t 会给出这样的警告:
nginx: [warn] conflicting server name "zhi***.online" on 0.0.0.0:443, ignored
「ignored」意味着后写入的那一份会被丢弃——你改的东西完全没生效,但配置测试是「成功」的。这个坑安静得可怕。
正确姿势是用编辑器改文件,或者先确认文件里的块数:
grep -c "listen 443" /etc/nginx/sites-available/zhi***.online # 期望 1
如果已经重复了,把文件拆回两个干净的 server 块。顺手记一条:tee -a 只适合「往日志尾部塞一行」,不适合往配置文件里塞结构。
干净的配置
一个文件里两个 server 块:80 负责跳转,443 负责服务。
# ---------- HTTP:只做跳转 ----------
server {
listen 80;
listen [::]:80;
server_name zhi***.online www.zhi***.online;
# 保留 ACME 挑战路径,方便以后换成自动续期
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://$host$request_uri;
}
}
# ---------- HTTPS:真正干活 ----------
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name zhi***.online www.zhi***.online;
root /var/www/zhi***.online;
index index.html index.htm;
ssl_certificate /etc/nginx/ssl/zhi***.online_nginx/zhi***.online_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/zhi***.online_nginx/zhi***.online.key;
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_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# 别在响应头里报名自己的版本号
server_tokens off;
access_log /var/log/nginx/zhi***.online.access.log;
error_log /var/log/nginx/zhi***.online.error.log;
location / {
try_files $uri $uri/ =404;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
}
值得解释的几处:
http2写在listen后面(listen 443 ssl http2;)是 Nginx 1.25.1 之前的语法;从 1.25.1 起官方推荐独立的http2 on;指令,旧写法在这个版本之后会被标记为 deprecated(1.24 及更早照常用旧写法,不会报错)。所以先nginx -v看版本再抄:1.24 及以下用listen 443 ssl http2,1.25.1 及以上改成listen 443 ssl;+http2 on;。ssl_protocols只留 TLSv1.2 / 1.3。默认值里可能还带 TLSv1.1,那是 2021 年就被主流浏览器判死刑的协议。ssl_prefer_server_ciphers off是反直觉的:让客户端选密码套件,因为现代客户端的偏好排序通常比服务端配置更准,且能享受客户端的硬件优化。只有在必须强制某套件(比如合规要求国密)时才开。ssl_session_cache shared:SSL:10m让多个 worker 共享会话票据,10MB 大约能存 4 万个会话。不加这行,每个 worker 各自缓存,回访用户经常要重新完整握手,移动端感知很明显。always后缀让安全响应头在 4xx/5xx 上也照发。不带always时,Nginx 在错误页里会丢掉这些头——攻击者最喜欢用错误页做探测。server_tokens off把Server:响应头从nginx/1.24.0 (Ubuntu)改成nginx。这条比藏ssl_ciphers有意义得多:密码套件摊在明面上无所谓(见文首说明),但版本号会直接被扫描器拿去匹配 CVE 列表。顺便说一句,server_tokens off不影响nginx -v和错误页——错误页里的版本号是另一处,真要去干净得靠error_page重定向,静态站一般不值得。
五、80 和 www 的收敛(当时没做,现在补上)
严格说这一节是事后补的,但它是同一件事的收尾:一个站点只应该有一个规范地址。
上面的配置已经做了 80 → 443 的 301,但 www 和裸域还是各回各的、都返回 200。结果是同一个页面有四个可访问地址:
http://zhi***.online → 301 → https://zhi***.online
http://www.zhi***.online → 301 → https://www.zhi***.online
https://zhi***.online → 200
https://www.zhi***.online → 200
对搜索引擎来说是两份重复内容,对你自己来说是两套缓存、两份统计。选一个当主域名(静态站在裸域上省一次跳转,我选裸域),另一个收敛成跳转。
这里有个前提必须先确认:上面那个 443 主块的 server_name 同时写了裸域和 www。收敛有两种写法,选哪种取决于你要不要动这一行——选错了会直接撞上第四节那个 conflicting server name。
写法 A:独立 server 块(更干净)
前提:把主 443 块的 server_name 改成只剩裸域,www 从主块里移除。
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.zhi***.online;
ssl_certificate /etc/nginx/ssl/zhi***.online_nginx/zhi***.online_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/zhi***.online_nginx/zhi***.online.key;
return 301 https://zhi***.online$request_uri;
}
写法 B:主块内 server 级 if(我线上用的)
不想多一个 server 块,就保留 server_name 里的 www,在主 443 块里加三行:
if ($host = www.zhi***.online) {
return 301 https://zhi***.online$request_uri;
}
if 放在 server 级、里面只有 return,属于 rewrite 阶段,每个请求只执行一次,行为是确定的。(Nginx 社区那句「if is evil」针对的是在 if 里放 proxy_pass、try_files 这类内容处理指令,不是否定所有 if。)
两种写法不能同时用
主块 server_name 已经含 www,再新建一个 server_name www.zhi***.online 的 443 块,nginx -t 只会给一条警告:
nginx: [warn] conflicting server name "www.zhi***.online" on 0.0.0.0:443, ignored
后一个块被静默丢弃。跳转看起来配好了,实际完全没生效——和第四节 tee -a 那个坑是同一个机制。所以要么从主块删 www 用写法 A,要么保留 www 用写法 B,二者选一。
注意 $request_uri 而不是 $uri:$request_uri 保留原始路径和查询串(/a?b=1),$uri 是解码并规范化之后的路径,查询串会丢。这个细节在带参数的分享链接上会直接暴露。
301 会被浏览器长期缓存(Chrome 甚至缓到磁盘),配错了很难回退——调试阶段先用 302,确认无误再改 301。
六、后来又补的四件事
站点能打开只是及格线。下面这四处都是「实际访问出了问题」之后补的,每一处都对应一个不明显的机制。
1. 自定义 404:状态码和页面是两件事
try_files $uri $uri/ =404 只是返回 404 状态码,页面还是 Nginx 自带的那个白底黑字版本。要换成站点自己的:
error_page 404 /404.html;
location = /404.html {
internal;
}
internal 不能省。少了它,用户可以直接访问 /404.html,拿到一个状态码 200 的「页面不存在」页——搜索引擎会把它当正常内容收录,等于凭空多一个重复页。
另一个反直觉的细节:error_page 404 /404.html 是内部跳转,浏览器地址栏不变,返回的仍是 404 状态码。用 curl -o /dev/null -w '%{http_code}' 验证时看到 404 才对,看到 200 说明写成了 rewrite 或 return。
2. 同一份内容只留一个 URL
第五节处理了 www,还剩 /index.html:/ 和 /index.html 都能打开首页,是两个地址、两份索引。最直觉的写法是加一条精确匹配:
location = /index.html {
return 301 /;
}
这个写法会死循环。 因为 try_files $uri/ 命中目录后会内部跳转到 /index.html,又触发这条 301 跳回 /,首页 301 到自己,浏览器报 ERR_TOO_MANY_REDIRECTS。我在这里卡了一次,症状是首页打不开、日志里一串 301。
正确姿势是用 server 级判断——它只在 server-rewrite 阶段执行一次,try_files 的内部跳转不会再进入这个阶段:
if ($request_uri ~ "^/index\.html$") {
return 301 /;
}
3. gzip:别看响应头,看字节数
静态站 90% 的字节是 HTML / CSS / JS,开 gzip 通常能省 60% 以上。实测首页 11022 → 4188 字节:
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json
application/xml image/svg+xml;
三个点:
text/html不用写进gzip_types。Nginx 默认就对它压缩,重复写会触发duplicate MIME type警告gzip_min_length 1024是止损线。几百字节的响应压完可能更大,gzip 头本身就有开销gzip_vary on输出Vary: Accept-Encoding,否则 CDN 或中间代理可能把压缩版发给不支持的客户端
验证要看实际字节数,不是看响应头里有没有 content-encoding:
curl -s -o /dev/null -w 'gzip=%{size_download}\n' -H 'Accept-Encoding: gzip' https://<domain>/
curl -s -o /dev/null -w 'plain=%{size_download}\n' https://<domain>/
4. 缓存:/_astro/ 那条规则其实没生效
构建产物 /_astro/about.9CU-ITdI.css 文件名带 hash,内容一变文件名就变,可以永久缓存。于是我加了:
location /_astro/ {
expires 1y;
}
结果实测只拿到 30 天:
cache-control: max-age=2592000
原因是正则 location 优先于普通前缀 location。文件下面还有一条 location ~* \.(css|js|woff2?|ttf|eot)$,.css 先被正则抢走,/_astro/ 从头到尾没命中过。
^~ 就是为这种情况准备的:前缀匹配命中后直接采用,跳过后续所有正则。
location ^~ /_astro/ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
access_log off;
}
注意这里没有写 expires。expires 1y 本身就会输出一个 Cache-Control: max-age=31536000,再 add_header Cache-Control 会追加出第二个同名头,两个 Cache-Control 并存时浏览器取哪个是不确定的。要用 immutable 就只用 add_header 把完整值写全。
顺带记一条 /api/ 的:走反代时必须把真实 IP 传过去。
location /api/ {
proxy_pass http://127.0.0.1:8787;
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;
}
漏了 X-Real-IP,服务端看到的所有访客都是 127.0.0.1。如果它按 IP 做去重,所有人共用一个 key,计数永远只加一次——这种 bug 不报错、不打日志,只表现为「数字不动」。
七、验证:四层,从里往外
# 第 1 层:端口在听吗
sudo ss -tlnp | grep -E ':(80|443)'
# 第 2 层:本机 HTTP 通吗
curl -sI http://127.0.0.1 -H 'Host: zhi***.online' | head -5
# 第 3 层:TLS 握手和证书对不对(-k 跳过校验只为看握手)
curl -sI -k https://127.0.0.1 -H 'Host: zhi***.online' | head -5
# 第 4 层:证书链和域名是否匹配(不看返回码,只看证书)
openssl s_client -connect 127.0.0.1:443 -servername zhi***.online </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
那天的实际结果是:443 正常监听,本地请求返回 200 OK,ss 里能看到 nginx: worker process 占着 443。本机层面全部通过。
但用手机 4G 访问公网 IP,转圈到超时。这就是下一节。
如果第 4 层用 openssl 看,重点核对三样:subject 里的 CN/SAN 是否含你的域名、issuer 是不是中间 CA 而不是根 CA、notAfter 的到期时间。
八、本机能通、外网不通:按顺序排查
这个现象只有三种可能,按「离你最近的开始」排:
| 层 | 检查命令 | 通过的表现 |
|---|---|---|
| 1. 进程是否监听 | ss -tlnp | grep :443 | 有 LISTEN,进程是 nginx |
| 2. 主机防火墙 | sudo ufw status / sudo firewall-cmd --list-ports | 443 已放行,或防火墙未启用 |
| 3. 云平台安全组 | 控制台 → 轻量应用服务器 → 防火墙 | 有 TCP:443 允许规则 |
| 4. 域名解析 | dig +short zhi***.online | 返回的 IP 与服务器一致 |
当时的 ufw status 是 inactive,第 1、2 层都没问题——问题必然在第 3 层。这是排查这类问题的好用思路:先用排除法把范围压到一个,再动手,比在服务器上反复重启 Nginx 高效得多。
腾讯云轻量的操作路径:控制台 → 轻量应用服务器 → 选中实例 → 防火墙 → 添加规则:
| 字段 | 值 |
|---|---|
| 应用类型 | 自定义 |
| 协议 | TCP |
| 端口 | 443 |
| 来源 | 0.0.0.0/0(全部) |
| 策略 | 允许 |
保存即生效,不用重启服务器。之后 https://1.*.*.* 就通了。
这里有个容易混淆的点:轻量应用服务器的「防火墙」和 CVM 的「安全组」是两个东西。轻量用的是简化版的防火墙页面,配置逻辑跟安全组一样但入口不同,照着 CVM 的教程找会找不到。而且轻量的防火墙规则默认不含 443,只开了 22/80/3389 这类常用端口——所以「装了证书、配了 Nginx、本机 200」,外网照样连不上。
九、用 IP 访问报证书错误,是正常的
配置完第一次用 https://1.*.*.* 打开,浏览器一定会红:
NET::ERR_CERT_COMMON_NAME_INVALID
原因很简单:证书是签发给 zhi***.online 的,subject 和 SAN 里根本没有 IP。TLS 握手时客户端拿「我访问的地址」去比对证书里的域名,比不中就报错——这不是配置错了,是证书本来就不该这么用。
真正的解决办法是用域名访问,而域名访问在国内服务器上又卡在备案:未备案的域名解析到境内服务器,80/443 会被运营商侧拦截。所以顺序是:
- 备案通过前:只能用 IP + 忽略证书错误访问(自己看内容够用)
- 备案通过后:
https://zhi***.online直接打开,无警告
补一条实测(2026-11 更新)。未备案被拦的表现比「连不上」刁钻得多:
http://zhi***.online/... → 302 → dnspod.qcloud.com/static/webblock.html
https://zhi***.online → 连接被直接切断
https://1.*.*.*(不带 SNI) → 200 正常
也就是说 80/443 端口本身是通的,被拦的是 Host / SNI。这个区别直接决定证书续期方案:Let’s Encrypt 的 HTTP-01(往 80 端口放验证文件)和 TLS-ALPN-01(在 443 握手阶段校验)都会被这道拦截挡掉,验证必然失败,还会白白消耗失败配额。唯一可行的是 DNS-01——在 DNSPod 加一条 TXT 记录完成验证,请求根本不经过 80/443,具体命令在下一节。
想彻底消掉 IP 访问的警告,只能给 IP 单独签一张证书(部分 CA 支持 IP SAN,通常要付费),对一个静态博客完全不值得。加个 -k 或者点「继续前往」就行。
十、收尾:四件没做但该做的事
站点能用不等于可以不管了。以下四条按紧急程度排:
1. 证书续期(最紧急)
免费证书有效期通常 90 天(腾讯云免费 DV 就是 3 个月),过期那天全站直接不可用,而且是浏览器级的红屏,比 502 更难解释。腾讯云控制台有到期提醒,但别只依赖它——服务通知邮件很容易漏。
告警只是止损,真正省事的是自动续期。备案没下来之前只能走 DNS-01(原因见上一节):
# 1. 装 acme.sh(GitHub 拉不动时用 gitee 镜像)
git clone https://gitee.com/neilpang/acme.sh.git
cd acme.sh && ./acme.sh --install -m you@example.com
# 2. 密钥单独放文件,别写在命令行里(否则进 shell history)
cat > /etc/acme.sh/dnspod.env <<'EOF'
DP_Id="你的ID"
DP_Key="你的Key"
EOF
chmod 600 /etc/acme.sh/dnspod.env
# 3. 签发,证书直接写回 Nginx 现有路径,配置一行不用改
/etc/acme.sh/acme.sh --issue --dns dns_dnspod \
-d zhi***.online -d www.zhi***.online \
--key-file /etc/nginx/ssl/zhi***.online_nginx/zhi***.online.key \
--fullchain-file /etc/nginx/ssl/zhi***.online_nginx/zhi***.online_bundle.crt \
--reloadcmd "systemctl reload nginx"
三个必须注意的点:
--reloadcmd不能省。续期成功后证书文件换了,但 Nginx 只在启动时把证书读进内存,不 reload 就还在用旧证书——文件是新的、握手是旧的,这种状态最难判断-d要把 www 一起带上(前提是证书需要覆盖 www)。漏了它,续期后 www 直接掉证书- 用
--fullchain-file而不是--cert-file写bundle.crt。只写站点证书会丢中间证书,就是第三节那个「证书链不完整」
如果暂时不想动 DNS API,也可以只做告警,cron 仍然值得留着:
# /etc/cron.daily/check-cert (记得 chmod +x)
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="zhi***.online"
END=$(openssl s_client -connect 127.0.0.1:443 -servername "$DOMAIN" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
LEFT=$(( ( $(date -d "$END" +%s) - $(date +%s) ) / 86400 ))
if [ "$LEFT" -lt 30 ]; then
logger -t check-cert "WARN: ${DOMAIN} 证书 ${LEFT} 天后到期"
echo "WARN: ${DOMAIN} 证书 ${LEFT} 天后到期"
fi
两个细节:/etc/cron.daily/ 下的脚本必须有可执行位,否则 cron 会静默跳过;结尾用 if 而不是 [ ... ] && echo——在 set -e 下,当条件不成立时 && 整句返回非 0,脚本会以失败状态退出,看起来像「检查脚本挂了」。
顺手接入前面那套拨测的「证书剩余天数」监控,两个渠道互为备份。
2. 配置备份
/etc/nginx 没进版本控制,改错一次就得凭记忆回滚。第三节提的 git init 现在做,只要一分钟。
3. HSTS
让浏览器在指定时间内强制走 HTTPS,防住 SSL Strip 这类降级攻击:
add_header Strict-Transport-Security "max-age=31536000" always;
但先别急着开 includeSubDomains 和 preload。HSTS 会被浏览器记住,一旦某个子域只有 HTTP,开了 HSTS 就等于把它废了,而且回退要等到期。先用 max-age=300 观察一周。
4. 日志轮转
Nginx 包默认装了 logrotate 配置(/etc/logrotate.d/nginx),但自定义的日志路径不在默认规则里。上面我把日志写到了 /var/log/nginx/zhi***.online.access.log,如果哪天访问量上来,这个文件会一直长。检查并补一条:
ls /etc/logrotate.d/nginx && grep -n "<domain>" /etc/logrotate.d/nginx || echo "需要补规则"
不补的后果是磁盘满,而磁盘满的表现是「Nginx 突然 500、SSH 登不上、数据库写不进去」——查起来又是另一轮折腾。
十一、这次操作的清单
留一份可复用的核对表,下次换机器照着走:
# 1. 文件到位
ls /var/www/<domain>/index.html
# 2. 写配置(80 + 443 两个 server 块,一个文件)
vim /etc/nginx/sites-available/<domain>
# 3. 放证书
mkdir -p /etc/nginx/ssl/<domain>_nginx && chmod 600 /etc/nginx/ssl/<domain>_nginx/*.key
# 4. 启用
ln -s /etc/nginx/sites-available/<domain> /etc/nginx/sites-enabled/
# 5. 测试 + 重载
nginx -t && systemctl reload nginx
# 6. 本机验证
ss -tlnp | grep :443
curl -sI -k https://127.0.0.1 -H 'Host: <domain>' | head -3
openssl s_client -connect 127.0.0.1:443 -servername <domain> </dev/null 2>/dev/null | openssl x509 -noout -dates
# 7. 云控制台放行 443(最容易被忘的一步)
# 8. 外网验证
curl -sI https://<domain> | head -3
- 站点根目录就位
- 80 端口站点配置生效
- 443 server 块追加,证书路径正确
-
nginx -t通过,reload完成 - 本机 443 监听、本地返回 200
- 安全组放行 TCP:443
- 80 → 443 全量 301,www 收敛到裸域
- 自定义 404 页(
error_page+internal) -
/index.html收敛到/ - gzip 与静态资源长缓存(
^~修正优先级) - 证书续期脚本(acme.sh DNS-01,待 DNSPod 密钥启用)
-
/etc/nginx入 Git - 日志轮转补规则
- HSTS(观察期结束后再开)
小结
整个过程没什么技术含量,值钱的是顺序:
- 先确认文件在,再谈 Nginx 认不认
sites-available写、sites-enabled链接,启停就是建删软链接- 改完必
nginx -t,再reload而不是restart - 本机自测只看应用层,
ss+curl -H Host能证明 Nginx 没问题 - 外网不通先排防火墙三层:进程 → 主机 → 云安全组,用排除法压范围
- IP 访问证书报错不是 bug,是证书本来就没有 IP SAN
- 能用之后立刻处理到期,证书过期是唯一「一定会发生且一定全站挂」的故障
- 缓存规则要按响应头验收,
location的优先级能让一条规则从头到尾没生效过
那天最花时间的不是配 Nginx,是「本机能通、手机不通」的那二十分钟。而最后发现的原因,是控制台里少点了一个「添加规则」。
运维里这类事情的比例比想象中高:命令都对,环境差一层。