这篇解决什么问题
你已经有一台 Linux 服务器、一个域名,http://你的域名/ 能打开,但浏览器地址栏写着“不安全”。
你想上 HTTPS,又不想每年花钱买证书、每年手动换一次。
读完你会得到:一个自动续期的 HTTPS 站点——装一次,之后不用管。
适合谁:会用 SSH 登录服务器、能编辑配置文件、知道 Nginx 大概是什么的人。 不需要懂密码学。
全文命令以 root 或 sudo 执行。发行版以 CentOS 系(含 Rocky / Alma / 阿里云 Linux)为主,
Ubuntu / Debian 的差异会单独标出。
第 0 步:先搞懂三件事
不懂这三件,后面就是盲操作——出了错也不知道错在哪。
① 证书是什么
证书是一对密钥 + 一份“身份证”。它的作用只有一个:让浏览器和你的服务器之间的通信加密, 并且证明“这个域名确实是你的”。
没有它,浏览器会提示“不安全”;有了它,地址栏出现一把锁。
② ACME 是什么,为什么要“证明域名是你的”
证书颁发机构(CA)不能谁来申请就给谁签——否则任何人都能申请一张 你的域名 的证书,
然后冒充你的网站,浏览器还会显示“安全”。
所以 CA 必须先确认:申请的人确实控制着这个域名。
这套“证明”的流程叫 ACME。最常见的方式是 HTTP-01:
- CA 给你一个随机字符串
- 你把它放在
http://你的域名/.well-known/acme-challenge/<随机串> - CA 自己从公网来访问这个网址
- 取到了、内容对得上 → 证明你控制着这个域名 → 签发证书
这一步决定了后面所有的配置细节:你必须让 CA 能通过 80 端口取到那个文件。 后面第 6 步里所有的“坑”,本质上都是这一步没走通。
③ 备案和证书的关系(大陆服务器必看)
这两件事完全无关,管的人都不一样:
| 谁管 | 看什么 | |
|---|---|---|
| ICP 备案 | 工信部(通过云厂商提交) | 你的服务器在哪、站内是什么内容 |
| SSL 证书 | CA 机构 | 只验证“你能不能控制这个域名”,不问备案 |
所以:没备案也能签下证书。
但要注意另一件事:如果你的服务器在中国大陆,域名又没有备案, 云厂商的监测系统可能拦截对 80/443 的访问(阿里云官方文档明确写了会阻断)。 那样证书签得下来,网站却打不开。
结论:证书可以照本文做;但服务器在大陆的话,备案该办还是要办。
第 1 步:前置检查(不满足就别往下走)
做任何操作之前,先把这三条确认掉——否则后面每一步都会失败,而你不知道是哪一步的问题。
1.1 域名解析指向你的服务器了吗
nslookup www.example.com
- 看到什么算成功:返回的
Address是你的服务器公网 IP - 不对怎么办:
- 报“找不到”/“Non-existent domain” → 解析没配或没生效。去域名控制台加一条 A 记录
- 返回了别的 IP → 解析写错了,改过来
⚠️ 注意
www和不带www是两条独立记录。很多人只加了www, 然后发现裸域名打不开——那是正常的,需要分别添加。 本文统一以www.example.com为例;你想用裸域名,把命令里的域名换掉即可(两者不能混用)。
1.2 HTTP 能访问吗
curl -I http://www.example.com/
- 看到什么算成功:
HTTP/1.1 200 OK - 不对怎么办:
- 连不上 → 先确认 Nginx 在跑:
systemctl status nginx - 403 / 404 → Nginx 在跑,但配置的站点根目录不对,先解决这个
- 连不上 → 先确认 Nginx 在跑:
1.3 防火墙和云安全组放行了吗
两层都要放行,只放一层是最常见的坑:
| 层 | 怎么放行 |
|---|---|
| 云厂商安全组 | 控制台里加入方向规则:TCP 80 和 443,源 0.0.0.0/0 |
| 系统防火墙 | firewall-cmd --add-port=80/tcp --add-port=443/tcp --permanent && firewall-cmd --reload<br>(Ubuntu:ufw allow 80,443/tcp) |
为什么必须先做:证书验证走 80,HTTPS 访问走 443。 这两个端口不通,后面所有步骤都会以“超时”告终——而超时的报错信息不会告诉你是防火墙的问题。
第 2 步:装 certbot
certbot 是 Let’s Encrypt 官方的客户端,负责申请、安装、自动续期证书。
CentOS / Rocky / Alma / 阿里云 Linux
dnf install -y certbot python3-certbot-nginx
certbot --version
- 看到什么算成功:打印出
certbot 2.x.x之类的版本号 - 出问题怎么办:
- 报依赖冲突(
conflicts with)→ 系统的软件源里有定制版的 EPEL, 可以试dnf install -y certbot python3-certbot-nginx --allowerasing。 但--allowerasing会移除冲突的包——执行前先看清楚它要移除什么, 不认识就别硬来 No package certbot available→ 不要自己去加通用 EPEL 源, 在云厂商定制系统上那很容易把依赖搞乱。先查厂商文档
- 报依赖冲突(
Ubuntu / Debian
apt update && apt install -y certbot python3-certbot-nginx
为什么用
python3-certbot-nginx:它让 certbot 能自动读写 Nginx 配置。 本文不使用这个自动能力(见第 5 步的说明),但装上它无害。
第 3 步:先彩排,别直接签
这一步很多人跳过,然后付出代价。
Let’s Encrypt 有限流机制:同一域名验证失败次数太多,会被临时拒绝服务。 排错过程中很容易把额度用光,然后只能干等。
所以先跑彩排模式——它完整走一遍流程(真的写验证文件、CA 真的来取), 只是最后不签发证书:
certbot certonly --webroot -w /var/www/example/acme \
-d www.example.com \
--dry-run \
--non-interactive --agree-tos --register-unsafely-without-email
逐段解释这条命令:
| 参数 | 作用 |
|---|---|
certonly |
只申请证书,不改我的 Nginx 配置(改配置我自己来,见第 5 步) |
--webroot -w /var/www/example/acme |
把验证文件写到这个目录。它会自己往这个目录下建 .well-known/acme-challenge/ |
-d www.example.com |
给哪个域名签(可写多个 -d) |
--dry-run |
用测试环境跑一遍,不签真证书 |
--non-interactive --agree-tos |
不交互、同意条款 |
--register-unsafely-without-email |
不填邮箱注册。彩排阶段用它就行,正式签的时候建议填邮箱(见第 4 步) |
- 看到什么算成功:末尾出现
The dry run was successful. - 它同时证明了三件事(这就是彩排的价值):
- 80 端口从公网可达
- 域名解析正确
-w指定的目录,Nginx 能对外提供访问
出问题怎么办:
| 报错 | 含义 | 怎么办 |
|---|---|---|
Timeout during connect |
CA 访问不到你的 80 端口 | 回到第 1 步查防火墙 / 安全组 |
Invalid response ... 404 |
能访问,但取不到验证文件 | 说明 -w 目录没被 Nginx 提供出去 —— 见第 6 步的 location 配置 |
unauthorized |
被拦了 | 大陆未备案的域名可能是这个原因(见第 0 步③) |
第 4 步:正式签证书
这次填你自己的邮箱(证书快过期时 CA 会发提醒):
certbot certonly --webroot -w /var/www/example/acme \
-d www.example.com \
--non-interactive --agree-tos -m you@example.com
- 看到什么算成功:
Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/www.example.com/fullchain.pem
记下这个路径,第 5 步要用。四份文件的分工:
| 文件 | 用途 |
|---|---|
fullchain.pem |
证书链(给 Nginx 的 ssl_certificate) |
privkey.pem |
私钥(给 ssl_certificate_key)—— 绝不能泄露 |
cert.pem / chain.pem |
单独看证书 / 中间证书时用,一般配置里不直接用 |
它们都是软链,指向
../../archive/<域名>/<名字><序号>.pem。 续期时序号会加一,软链自动指过去——所以配置里永远写软链路径,别写archive/里的真实文件名。
第 5 步:改 Nginx 配置
为什么不直接用 certbot --nginx
certbot --nginx 能自动改你的 Nginx 配置,看起来省事。但它会直接改你手上跑着的配置——
而那份配置可能已经稳定运行很久,被工具改坏之后不好回退。
本文的做法:certonly 只签证书,配置自己写。多花五分钟,换来“每处改动都看得见”。
完整配置
改 /etc/nginx/conf.d/example.conf(改之前先备份):
# ---------- ① 域名 + 80:除 ACME 验证外,全部跳 HTTPS ----------
server {
listen 80;
server_name www.example.com;
root /var/www/example/html;
index index.html;
charset utf-8;
server_tokens off;
# 默认不缓存
add_header Cache-Control "no-cache";
# ★ ACME 验证目录:必须真的能把文件读出来
location ^~ /.well-known/acme-challenge/ {
alias /var/www/example/acme/.well-known/acme-challenge/;
try_files $uri =404;
log_not_found off;
}
# 其余全部跳 HTTPS
location / {
return 301 https://$host$request_uri;
}
}
# ---------- ② 域名 + 443:正式服务 ----------
server {
listen 443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 刻意不写 ssl_ciphers:用 Nginx 默认值即可,
# 手写一串很容易把已被淘汰的算法带进来,反而更不安全。
root /var/www/example/html;
index index.html;
charset utf-8;
server_tokens off;
add_header Cache-Control "no-cache";
location / {
try_files $uri $uri/ =404;
}
}
逐段解释
| 段落 | 为什么这么写 |
|---|---|
location ^~ /.well-known/acme-challenge/ |
^~ 的优先级高于普通 location /,这样验证请求不会被下面的 301 跳走 |
alias 而不是 root |
验证文件的真实路径是 -w 目录下的 .well-known/acme-challenge/。用 alias 把 URL 前缀直接映射到那个目录,最不容易搞错 |
try_files $uri =404; |
真的去读文件,读不到才 404 |
location / { return 301 ...; } |
除验证路径外,所有 HTTP 请求跳到 HTTPS |
443 块不写 ssl_ciphers |
Nginx 默认套件已经够用;手写容易带进弱算法 |
两个块都写 root |
443 块是真正对外服务的那个,必须有根目录 |
生效(语法不过就别 reload)
nginx -t && nginx -s reload && echo "=== RELOAD OK ==="
- 看到什么算成功:
syntax is ok+test is successful+=== RELOAD OK === nginx -t报错的常见原因:
| 报错 | 原因 |
|---|---|
unknown directive "http2" |
你写了 http2 on;,那是 Nginx 1.25.1+ 的写法。低版本要写成 listen 443 ssl http2; |
duplicate default server |
有两个 listen 80 default_server |
conflicting server name(这是警告不是错误) |
有别的 server 块用了同名或 _。不影响启动,但说明有冗余配置 |
第 6 步:验证(五个检查,一个都不能省)
① HTTPS 能打开吗
浏览器打开 https://www.example.com/:地址栏应该有锁形图标,没有“不安全”警告。
命令行版本:
curl -I https://www.example.com/
② HTTP 会跳到 HTTPS 吗
curl -I http://www.example.com/
- 期望:
HTTP/1.1 301+Location: https://www.example.com/
③ 证书内容对不对
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
- 期望:
subject里有你的域名、issuer是 Let’s Encrypt、notAfter是 90 天后
④ ★ 验证文件真的能被公网读到(最重要的一条)
前面几条都过了,也不代表续期通道是好的。 必须单独验这一条:
# 在服务器上:放一个真文件
mkdir -p /var/www/example/acme/.well-known/acme-challenge
echo "PROBE-OK" > /var/www/example/acme/.well-known/acme-challenge/probe.txt
# 在你自己的电脑上(不是服务器):把内容读回来
curl http://www.example.com/.well-known/acme-challenge/probe.txt
- 看到什么算成功:打印出
PROBE-OK - 只看到 404 或 301 → 这段配置有问题,续期将来一定会失败
- 验完删掉:
rm -f /var/www/example/acme/.well-known/acme-challenge/probe.txt
⚠️ 为什么必须这样做:光看“没有跳转”是不够的。 必须确认“文件内容真能取回来”。 把 404 误当成“通道正常”, 是后面“证书到期才发现续期一直失败”的最常见原因。
⑤ 续期干跑能过吗
certbot renew --dry-run
- 看到什么算成功:
Congratulations, all simulated renewals succeeded - 它的意思:完整走了一遍续期流程,但不真续期
- 失败了怎么办:多半是第 ④ 条没通过,回头查那段 location
第 7 步:自动续期(装完就不用管了)
Let’s Encrypt 证书只有 90 天,靠自动续期。certbot 通常会自己装好定时任务,你只需确认它真的在。
确认定时任务存在
systemctl list-timers --all | grep certbot
# 或者(老系统)
ls -l /etc/cron.d/ | grep certbot
- 看到什么算成功:能看到
certbot-renew.timer(状态 enabled),或/etc/cron.d/certbot
如果什么都没有
systemctl enable --now certbot-renew.timer
systemctl list-timers --all | grep certbot
或者用 cron 兜底(crontab -e 加一行,每天两次检查):
0 3,15 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"
为什么是“每天两次”:certbot 只在“剩余有效期不足 30 天”时才真的续, 其余时候什么都不做。所以跑得频繁是无害的,反而能保证不错过窗口。
为什么带
--deploy-hook "systemctl reload nginx": 换了新证书必须让 Nginx 重新加载,否则它还在用旧证书。
随时可以自查
certbot renew --dry-run
这是“证书到期前唯一需要你主动做的事”——跑一次,看到成功就放心。
第 8 步:两个真实踩过的坑
这一节是本文最有价值的部分。 两个坑都花了实际时间才定位。
坑一:http2 on; 不是所有版本都认
症状:
nginx: [emerg] unknown directive "http2" in /etc/nginx/conf.d/example.conf:20
原因:http2 on; 是 Nginx 1.25.1 才引入的独立指令。
低版本要用旧写法——把 http2 加到 listen 后面:
# 1.25.1 及以上
listen 443 ssl;
http2 on;
# 1.25.1 以下(1.24 等)
listen 443 ssl http2;
先查版本:nginx -v。
别为了这个去升级 Nginx——生产服务器上升级影响面太大, 改一行写法就解决的事,不值得。
坑二:ACME 路径“不跳转” ≠ “能读到文件”
这个坑更隐蔽,而且我“修”它的过程本身又造出一个新问题。
背景:配好 HTTP→HTTPS 跳转后,验证路径也会被跳走,续期就废了。所以需要在跳转之前“截住”它。
我试过的三种写法:
# ❌ 写法一:只写 root —— 验证请求仍被 location / 跳走(301)
location ^~ /.well-known/acme-challenge/ {
root /var/www/example/acme;
}
# ❌ 写法二:直接返回 404 —— 本意是"别跳转",结果真文件也读不到
if ($request_uri ~ ^/\.well-known/) { return 404; }
# ✅ 写法三:alias + try_files —— 真的把文件读出来
location ^~ /.well-known/acme-challenge/ {
alias /var/www/example/acme/.well-known/acme-challenge/;
try_files $uri =404;
log_not_found off;
}
写法二的错在哪:
我把目标定成了”不要跳转“,而真正的目标是”能被读到”。
改完我用一个不存在的假路径测了一下,得到 404——我以为“不跳转 = 通过”,
实际上那个 404 恰恰说明“文件取不到”。
真正跑一遍续期时,报错就来了:
Detail: <服务器IP>: Invalid response from
http://www.example.com/.well-known/acme-challenge/<一串随机字符>: 404
教训(两条,都很通用):
- “没有明确错误”不等于“正确”。用假路径测出 404,看着不像错误, 但它和“文件读不到”是同一个现象——必须用真文件去测。
- 验证的目标要写对。这条通道的目标不是“不跳转”,而是“CA 能取到那个文件”。 目标写错,验证方法就跟着错。
所以第 6 步的 ④ 才是“放一个真文件、从公网把内容读回来”。 不能只看状态码。
第 9 步:出问题先看哪里
| 现象 | 先查 |
|---|---|
certbot 报 Timeout during connect |
80 端口:云安全组 + 系统防火墙都要放行 |
certbot 报 Invalid response ... 404 |
ACME 的 location 配置(第 5 步),并按第 6 步 ④ 用真文件验 |
certbot 报 unauthorized |
大陆服务器 + 未备案域名可能被拦截 |
| 浏览器报“证书无效/不安全” | certbot certificates 看有效期;过期就 certbot renew && systemctl reload nginx |
| 改了配置但网站没变 | nginx -t && nginx -s reload 了吗?配置改完必须 reload |
| 换域名后站内链接全断 | 站内链接要用站内绝对路径(/posts/…),不要写死域名或 IP |
nginx -t 报 unknown directive |
指令的版本不兼容,查该指令是哪个版本引入的 |
附:命令清单(可整段复制,按顺序)
把 www.example.com、/var/www/example、you@example.com 换成你自己的。
# ---- 前置检查 ----
nslookup www.example.com
curl -I http://www.example.com/
# ---- 装 certbot(CentOS 系)----
dnf install -y certbot python3-certbot-nginx
certbot --version
# ---- 彩排 ----
certbot certonly --webroot -w /var/www/example/acme \
-d www.example.com --dry-run \
--non-interactive --agree-tos --register-unsafely-without-email
# ---- 正式签 ----
certbot certonly --webroot -w /var/www/example/acme \
-d www.example.com \
--non-interactive --agree-tos -m you@example.com
# ---- 改 Nginx 配置(用第 5 步的全文)----
cp -a /etc/nginx/conf.d/example.conf /etc/nginx/conf.d/example.conf.bak
vi /etc/nginx/conf.d/example.conf
nginx -t && nginx -s reload && echo "=== RELOAD OK ==="
# ---- 验证:放真文件 ----
mkdir -p /var/www/example/acme/.well-known/acme-challenge
echo "PROBE-OK" > /var/www/example/acme/.well-known/acme-challenge/probe.txt
# ---- 验证:续期干跑 ----
certbot renew --dry-run
# ---- 清理测试文件 ----
rm -f /var/www/example/acme/.well-known/acme-challenge/probe.txt
# ---- 出问题时回退配置 ----
cp -a /etc/nginx/conf.d/example.conf.bak /etc/nginx/conf.d/example.conf
nginx -t && nginx -s reload
在你自己的电脑上(验证第 ④ 条):
curl http://www.example.com/.well-known/acme-challenge/probe.txt
# 期望输出:PROBE-OK
一句话总结
HTTPS 不难配,难的是“验证自己配对了”。
三条最值钱的:
- 先彩排(
--dry-run),别把限流额度浪费在排错上 - ACME 通道要用真文件验,不能只看状态码
nginx -t通过才 reload——它已经救过很多次“配置写错但还没生效”的场面