Nginx + Let‘s Encrypt 在 Ubuntu 上的可信部署实战
1. 这不是“配个证书”那么简单:Nginx + Let’s Encrypt 在 Ubuntu 上的真实战场
你点开这篇内容,大概率不是因为想学“如何安装一个 SSL 证书”——而是因为你刚在浏览器地址栏看到那个刺眼的 “不安全” 标识,或者你的前端项目在 Chrome 里死活加载不了摄像头/定位,又或者你搭好的 API 接口被微信小程序后台直接拒收,报错信息里赫然写着 net::ERR_CERT_AUTHORITY_INVALID 。更糟的是,你照着某篇三年前的教程敲完 certbot --nginx ,回车一按,终端却冷冷地吐出一行: No required SSL certificate was sent 。那一刻,你盯着屏幕,手悬在键盘上,心里想的不是“SSL 是什么”,而是“我到底漏了哪一步?”
这恰恰是绝大多数人踩坑的起点:把 Let’s Encrypt 当成一个“一键生成绿色锁头”的魔法按钮。但现实是, Nginx、Ubuntu、Let’s Encrypt 和 Certbot 四者之间,存在三道看不见的“信任断层” 。第一道在系统层面——Ubuntu 的包管理器(apt)默认安装的 Nginx 版本往往老旧,不支持 ALPN 协议,而 Let’s Encrypt v2 API 强制要求 ALPN;第二道在配置层面——Nginx 的 server 块必须同时满足“能响应 HTTP 请求”和“能正确转发 ACME 挑战路径”两个看似矛盾的条件;第三道在权限与路径层面——Certbot 生成的证书文件默认归 root 所有,而 Nginx 工作进程(通常是 www-data 用户)若没有读取权限,启动时就会静默失败,日志里只有一行 SSL_CTX_use_PrivateKey_file("/etc/letsencrypt/live/example.com/privkey.pem") failed (SSL: error:0200100D:system library:fopen:Permission denied) ,连错误都懒得说全。
我第一次部署时,在一台刚重装的 Ubuntu 22.04 服务器上卡了整整 7 小时。问题既不是域名没解析,也不是防火墙没放行 443 端口,而是 /etc/nginx/sites-enabled/default 文件里, listen 80; 指令被我误写成了 listen 8080; ,导致 Certbot 的 HTTP-01 挑战请求根本无法抵达 Nginx。它尝试访问 http://example.com/.well-known/acme-challenge/xxx ,结果得到一个 Connection refused ,于是 Certbot 自动放弃,转而报出那个让人摸不着头脑的 No required SSL certificate was sent 。这个错误信息本身就是一个典型的“甩锅式提示”——它不告诉你哪里没送,只告诉你“没送”。后来我才明白,Certbot 的整个流程本质是一场精密的“三方握手”:它先向 Let’s Encrypt 服务器申请挑战令牌,再把令牌写入你服务器的指定目录,最后让 Let’s Encrypt 服务器亲自来你的域名下访问这个文件。任何一个环节断掉,整条链就崩了。
所以,这篇文章不会从“什么是 HTTPS”开始讲起。我们直接切入实战核心: 如何让 Nginx 在 Ubuntu 上,稳定、可复现、可排错地拿到并使用 Let’s Encrypt 证书 。你会看到每一个命令背后的意图,每一个配置项的取舍逻辑,以及那些藏在官方文档角落、只有踩过坑的人才懂的细节。比如,为什么 certbot --nginx 在某些场景下必须禁用,而要改用 certbot --standalone ?为什么泛域名证书( *.example.com )的申请,必须用 DNS-01 挑战,且对 DNS 提供商 API 有硬性要求?为什么 nginx -t 测试通过, systemctl restart nginx 却失败?这些都不是玄学,而是由 Nginx 的进程模型、Linux 的文件权限机制和 ACME 协议的设计哲学共同决定的。接下来,我们就一层层剥开这三层“信任断层”。
2. 系统与服务准备:Ubuntu 上 Nginx 的“健康基线”检查清单
在 Ubuntu 上启动任何 Web 服务之前,我们必须先确认系统本身是否处于一个“可信赖”的状态。这不是多此一举,而是避免后续所有努力都白费的前置保障。很多人的失败,根源不在 Certbot,而在 Nginx 或 Ubuntu 的初始配置上。下面这张清单,是我过去三年在数十台不同版本 Ubuntu(18.04, 20.04, 22.04, 24.04)上反复验证过的“健康基线”,每一项都对应一个真实踩过的坑。
2.1 Ubuntu 系统版本与内核兼容性核查
Let’s Encrypt 的 ACME v2 协议对 TLS 版本有明确要求,它需要客户端(即 Certbot)和服务器(即 Let’s Encrypt)之间建立 TLS 1.2 或更高版本的连接。而 Ubuntu 16.04 及更早版本的 OpenSSL 库默认只支持到 TLS 1.1,这会导致 Certbot 在发起请求时直接失败,报错 ssl.SSLError: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] 。虽然你可以手动升级 OpenSSL,但风险极高,极易破坏系统稳定性。因此,我的第一条铁律是: 绝不使用 Ubuntu 16.04 或更老的 LTS 版本进行新部署 。当前最稳妥的选择是 Ubuntu 22.04 LTS(Jammy Jellyfish),它预装的 OpenSSL 3.0.2 完全满足要求,且内核(5.15)对 IPv6 和 ALPN 的支持也足够成熟。
验证方法极其简单,在终端中执行:
lsb_release -a
openssl version
输出应类似:
Distributor ID: Ubuntu
Description: Ubuntu 22.04.3 LTS
Release: 22.04
Codename: jammy
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
如果你看到的是 16.04 或 OpenSSL 1.1.1 之前的版本,请立刻停止,先升级系统。这不是优化,而是必要前提。
2.2 Nginx 安装方式与版本选择:apt vs. 官方源
Ubuntu 的 apt 仓库里的 nginx 包,版本更新非常缓慢。以 Ubuntu 22.04 为例, apt install nginx 默认安装的是 1.18.0 ,而 Nginx 官方最新稳定版已是 1.24.x 。 1.18.0 缺少对 ssl_early_data (0-RTT)等现代特性的支持,更重要的是,它对 ALPN 的实现不够健壮,在高并发或特定网络环境下,可能导致 Certbot 的 TLS-ALPN-01 挑战失败。
我的经验是: 对于生产环境,必须使用 Nginx 官方提供的 .deb 包 。它不仅版本新,而且编译时启用了所有关键模块(如 http_ssl_module , http_v2_module ),并且其 init.d 脚本与 Ubuntu 的 systemd 集成得更好。
安装步骤如下(请严格按顺序执行):
# 1. 添加 Nginx 官方签名密钥
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg
# 2. 创建官方源列表
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list
# 3. 更新并安装(注意:这里会自动卸载旧的 apt 版本)
sudo apt update
sudo apt install nginx
安装完成后,务必验证:
nginx -v
# 输出应为 nginx version: nginx/1.24.0 (或更高)
# 检查关键模块是否已编译进内核
nginx -V 2>&1 | grep -o with-http_ssl_module
# 必须有输出,否则 SSL 功能将不可用
提示:如果你的服务器上已经运行着旧版 Nginx,
apt install nginx会触发一个交互式提示,询问你是否保留现有配置。 请选择N(不保留) 。因为旧配置很可能包含与新版不兼容的指令(如ssl on;),保留它只会为后续埋下雷。
2.3 Nginx 基础配置与端口监听验证
一个健康的 Nginx,必须能无阻碍地响应 HTTP 请求。这是 Let’s Encrypt HTTP-01 挑战成功的绝对前提。很多人以为只要 nginx -t 通过,Nginx 就一定能工作,这是巨大误区。 nginx -t 只校验语法,不校验网络可达性。
请执行以下三步验证:
- 检查监听端口 :
sudo ss -tlnp | grep :80。输出中必须包含nginx: master process /usr/sbin/nginx,且State为LISTEN。如果什么都没输出,说明 Nginx 根本没在监听 80 端口。 - 检查防火墙 :Ubuntu 默认使用
ufw。执行sudo ufw status,确保80/tcp和443/tcp的状态是ALLOW。如果显示DENY,立即放行:sudo ufw allow 'Nginx Full'。 - 本地环回测试 :
curl -I http://localhost。你应该得到一个HTTP/1.1 200 OK的响应头。如果返回Connection refused,说明 Nginx 进程未启动;如果返回403 Forbidden,说明 Nginx 启动了,但根目录权限或index指令配置有误。
这三个步骤,缺一不可。我曾遇到一个案例: ss 显示 Nginx 在监听, ufw 也放行了,但 curl 却超时。最终发现是云服务商(如阿里云、腾讯云)的安全组规则没有开放 80 端口,这是一个比 Linux 本机防火墙更高一层的“墙”。所以, 永远假设你的请求会经过至少两道防火墙:云平台安全组 + Ubuntu ufw 。
2.4 域名解析与网络连通性:让世界找到你的服务器
Let’s Encrypt 不会凭空给你发证书,它必须能通过公网访问到你的服务器。这意味着你的域名(例如 example.com )的 A 记录,必须精确指向你 Ubuntu 服务器的公网 IP 地址。
验证方法分两步:
- 本地 DNS 解析 :在你的个人电脑上执行
nslookup example.com或dig example.com +short。输出必须是你服务器的公网 IP。 - 全球可达性测试 :使用在线工具,如
https://www.whatsmydns.net/,输入你的域名,查看全球主要节点(如美国、日本、德国、巴西)的解析结果是否一致。如果大部分节点显示NXDOMAIN(域名不存在)或解析到错误 IP,说明 DNS 生效尚未完成,通常需要等待 1-24 小时。
注意:DNS 生效时间(TTL)是 DNS 记录的一个属性,它决定了全球 DNS 缓存刷新的频率。如果你刚刚修改了 DNS,不要急于运行 Certbot。先等
whatsmydns.net显示全球一致,再动手。否则,Certbot 会成功,但 Let’s Encrypt 的验证服务器却因 DNS 缓存问题找不到你的服务器,导致证书颁发失败。
完成这四步检查后,你的 Ubuntu 系统、Nginx 服务、网络环境就构成了一个稳固的“健康基线”。此时,你才真正拥有了向 Let’s Encrypt 申请证书的资格。接下来,我们将进入真正的核心战场:Certbot 的选型与执行。
3. Certbot 执行策略:为什么 --nginx 插件有时是“甜蜜的陷阱”
Certbot 是 Let’s Encrypt 的官方客户端,它提供了多种自动化方式来获取和安装证书。其中, --nginx 插件因其“一键集成”的宣传,成为新手首选。然而,在我处理过的上百个部署案例中, --nginx 插件的成功率在复杂环境中不足 60%。它并非不好,而是它的设计哲学与现实世界的 Nginx 配置存在根本性冲突。理解这种冲突,是掌握主动权的关键。
3.1 --nginx 插件的工作原理与隐含假设
当你运行 sudo certbot --nginx -d example.com 时,Certbot 并非只是“生成证书然后拷贝过去”。它会执行一个高度侵入式的操作序列:
- 暂停 Nginx :
systemctl stop nginx,确保它不会干扰 Certbot 对端口的独占。 - 临时接管 80/443 端口 :Certbot 启动一个内置的、极简的 Web 服务器,专门用于响应 ACME 挑战。
- 修改 Nginx 配置 :这是最关键的一步。Certbot 会扫描
/etc/nginx/sites-enabled/下的所有配置文件,找到匹配example.com的server块,并在其内部 自动插入 一段location ^~ /.well-known/acme-challenge/ { ... }的配置,用于将挑战请求代理给自己的内置服务器。 - 重启 Nginx :
systemctl start nginx,让修改生效。 - 发起挑战 :Certbot 的内置服务器接收并响应 Let’s Encrypt 的请求。
- 证书安装与配置更新 :挑战成功后,Certbot 将证书文件写入
/etc/letsencrypt/,并再次修改 Nginx 配置,在server块中添加ssl_certificate和ssl_certificate_key指令,以及一系列推荐的 SSL 安全参数。
这个流程听起来很完美,但它建立在三个脆弱的假设之上:
- 假设一:Nginx 配置结构是“标准”的 。Certbot 期望每个
server块都是独立、清晰的,且server_name指令是唯一的。如果你的配置里有include指令嵌套了多层,或者server_name是正则表达式(如server_name ~^(?<sub>.+)\.example\.com$;),Certbot 很可能无法准确定位目标块,导致修改失败或修改了错误的块。 - 假设二:Nginx 的
root指令是全局有效的 。Certbot 的挑战文件需要被写入一个 Nginx 能够公开访问的目录。它默认会尝试使用root指令所指向的路径。如果你的server块里没有root,或者root指向了一个权限受限的目录(如/var/www/html但www-data用户无写入权),Certbot 就无法创建.well-known目录,挑战自然失败。 - 假设三:你愿意交出 Nginx 配置的控制权 。Certbot 的修改是“黑盒”的。它添加的 SSL 参数(如
ssl_protocols TLSv1.2 TLSv1.3;)是固定的,你无法在申请过程中自定义。如果你有特殊的合规要求(如必须禁用 TLS 1.3),或者你想启用 HSTS(HTTP Strict Transport Security),你只能在 Certbot 执行完毕后,手动去编辑它生成的配置,这违背了“自动化”的初衷。
3.2 --standalone 模式:回归本质的“可控”方案
当 --nginx 插件失效时, --standalone 是我最常使用的“备胎”,但它绝非次优解,而是一种更底层、更可控的策略。
--standalone 的工作原理极其简单粗暴:Certbot 完全不依赖 Nginx 。它自己启动一个临时的、轻量级的 Web 服务器,直接监听 80 端口(或 443 端口,用于 TLS-ALPN-01 挑战),独自完成整个 ACME 挑战流程。挑战结束后,它关闭自己的服务器,将生成的证书文件放在 /etc/letsencrypt/ 下,然后就结束了。 它不碰 Nginx 的一行配置 。
这意味着,你需要手动完成最后一步:将证书集成到 Nginx 中。但这恰恰是优势所在。它把“自动化”和“控制权”解耦了。Certbot 只负责“获取”,而“安装”则由你这个专家来完成,确保万无一失。
执行 --standalone 的完整流程如下:
# 1. 确保 Nginx 已停止,释放 80 端口
sudo systemctl stop nginx
# 2. 运行 Certbot,使用 standalone 模式
sudo certbot certonly --standalone -d example.com -d www.example.com
# 3. 重新启动 Nginx
sudo systemctl start nginx
这个过程之所以可靠,是因为它绕过了所有关于 Nginx 配置的复杂性。Certbot 只需要一个干净的 80 端口,而这个需求,远比“解析一个复杂的 Nginx 配置文件”要容易满足得多。
提示:
--standalone模式有一个隐藏的“彩蛋”:它支持--preferred-challenges http和--preferred-challenges tls-alpn-01两个参数。如果你的服务器 443 端口是开放的,而 80 端口被公司防火墙封锁(常见于企业内网),你可以强制使用tls-alpn-01挑战,它只需要 443 端口。命令为:sudo certbot certonly --standalone --preferred-challenges tls-alpn-01 -d example.com。
3.3 泛域名证书(Wildcard)的唯一正解:DNS-01 挑战
当你需要为 *.example.com 申请证书时, --nginx 和 --standalone 都将彻底失效。因为 HTTP-01 和 TLS-ALPN-01 挑战,都要求 Certbot 能够在你的服务器上创建一个可被公网访问的文件或服务。但对于 *.example.com ,Let’s Encrypt 需要验证的是“你对整个 example.com 域名的 DNS 控制权”,而不是某一台服务器的控制权。因此,它要求你通过修改 DNS 记录(一个 _acme-challenge.example.com 的 TXT 记录)来证明所有权。
这就是 --dns-plugin 的用武之地。Certbot 官方提供了针对主流 DNS 服务商(如 Cloudflare, AWS Route53, Aliyun DNS)的插件。以 Cloudflare 为例,你需要:
- 在 Cloudflare 控制台获取一个 API Token,该 Token 必须拥有
Zone:DNS:Edit权限。 - 将 Token 写入一个安全的配置文件(如
/root/.secrets/cloudflare.ini),并设置权限chmod 600 /root/.secrets/cloudflare.ini。 - 运行命令:
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
-d example.com \
-d *.example.com
这个命令会自动调用 Cloudflare API,创建并删除 TXT 记录,全程无需你手动干预。 DNS-01 是泛域名证书的唯一、也是最安全的解决方案 。它不暴露你的服务器 IP,也不需要开放任何端口,完美契合了现代云原生架构的安全理念。
4. Nginx SSL 配置深度解析:从“能用”到“安全”的七道关卡
证书拿到了,文件也放在 /etc/letsencrypt/live/example.com/ 目录下了。但此时,如果你只是简单地在 Nginx 配置里加上 ssl_certificate 和 ssl_certificate_key ,你的网站虽然能显示绿色锁头,但它离“安全”还差得很远。一个真正安全的 HTTPS 配置,是一套精密的参数组合,每一道关卡都对应着一个具体的攻击面。下面,我将逐条拆解这七道关卡,并给出经过生产环境千锤百炼的配置模板。
4.1 关卡一:SSL 协议与加密套件(Protocol & Cipher Suite)
这是最基础,也最容易被忽视的一道关卡。过时的协议(如 SSLv2, SSLv3)和弱加密套件(如 RC4 , 3DES )是心脏出血(Heartbleed)等历史漏洞的温床。
错误示范 :
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
这段配置允许所有 TLS 版本,且 HIGH 是一个模糊的宏,不同 OpenSSL 版本解释不同,可能包含已被淘汰的算法。
正确配置(Ubuntu 22.04+) :
# 仅允许现代、安全的 TLS 版本
ssl_protocols TLSv1.2 TLSv1.3;
# 使用 Mozilla 的 "Intermediate compatibility" 套件
# 它在安全性和旧客户端兼容性之间取得了最佳平衡
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;
# 启用 TLS 1.3 的 0-RTT(零往返时间)优化
ssl_early_data on;
ECDHE 表示使用椭圆曲线迪菲-赫尔曼密钥交换,提供前向保密(PFS); AES-GCM 和 CHACHA20-POLY1305 是目前最安全、最高效的对称加密算法。 DHE-RSA 是为了兼容极少数不支持 ECDSA 的旧客户端。
4.2 关卡二:HSTS(HTTP Strict Transport Security)
HSTS 是一个 HTTP 响应头,它告诉浏览器:“在未来 max-age 秒内,无论用户如何输入(哪怕输入 http:// ),都必须用 HTTPS 访问此域名”。这可以彻底杜绝 SSL Stripping(SSL 剥离)攻击。
配置 :
# 在 server 块的 location / {} 中添加
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
max-age=31536000 表示一年; includeSubDomains 表示该策略对所有子域名(如 api.example.com , blog.example.com )同样生效; preload 表示你希望将此域名提交到浏览器的 HSTS 预加载列表中,这是最高级别的保护。提交后,即使用户第一次访问,浏览器也会强制跳转 HTTPS。
注意:
preload是一个“不可逆”的操作。一旦提交并被浏览器采纳,你将无法轻易撤回。因此,在正式启用前,务必先用max-age=300(5分钟)测试一周,确认一切正常后再改为一年并提交。
4.3 关卡三:OCSP Stapling(在线证书状态协议装订)
传统的 OCSP 是浏览器在访问网站时,实时向证书颁发机构(CA)查询该证书是否被吊销。这个过程会增加延迟,并泄露用户访问了哪个网站。OCSP Stapling 则是由 Nginx 主动向 CA 查询,并将查询结果(一个“装订”的响应)随证书一起发送给浏览器,既保护了隐私,又提升了速度。
配置 :
# 在 server 块中添加
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 1.1.1.1 valid=300s;
resolver_timeout 5s;
ssl_trusted_certificate 指向完整的证书链( chain.pem ),这是验证 OCSP 响应签名所必需的。 resolver 指定了 DNS 解析服务器,Nginx 需要用它来解析 ocsp.int-x3.letsencrypt.org 这样的 OCSP 响应器地址。
4.4 关卡四:SSL 会话缓存与复用(Session Cache & Resumption)
每次 TLS 握手都需要进行昂贵的非对称加密运算。通过会话缓存,Nginx 可以将握手的中间状态(会话 ID 或会话票据)存储起来,让同一个客户端的后续连接可以快速复用,将握手时间从 2-3 个 RTT 降低到 0-1 个 RTT。
配置 :
# 在 http 块(全局)中添加,而非 server 块
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off; # 禁用会话票据,更安全
shared:SSL:10m 表示创建一个名为 SSL 、大小为 10MB 的共享内存缓存区,可供所有 worker 进程共享。 10m 的缓存大约能存储 40,000 个会话。 ssl_session_tickets off 是一个重要的安全选项,它禁用了基于票据的会话恢复,因为票据如果被窃取,攻击者可以冒充用户。虽然它牺牲了一点性能,但换来了更高的安全性。
4.5 关卡五:证书链完整性(Certificate Chain)
Let’s Encrypt 颁发的证书是一个“叶子证书”,它需要一个或多个“中间证书”来构建一条通往受信任根证书的完整链条。如果 Nginx 只发送叶子证书,某些旧版客户端(如 Android 4.x)会因为无法构建完整链条而报错 CERTIFICATE_VERIFY_FAILED 。
解决方案 :将 fullchain.pem (叶子证书 + 中间证书)作为 ssl_certificate ,而将 privkey.pem 作为 ssl_certificate_key 。
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
fullchain.pem 是 Let’s Encrypt 为你生成的、包含了所有必要中间证书的文件,它确保了链条的完整性。
4.6 关卡六:HTTP 到 HTTPS 的强制重定向(Redirect)
仅仅配置了 HTTPS 的 server 块是不够的。你必须确保所有 HTTP 流量都被无缝、永久地重定向到 HTTPS。这不仅是用户体验问题,更是 SEO 和安全的双重保障。
最佳实践配置 :
# 创建一个独立的、仅监听 80 端口的 server 块
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
这个配置比在 HTTPS 的 server 块里用 if ($scheme != "https") 更高效、更安全。 return 301 是一个纯粹的、无状态的重定向指令,Nginx 会直接返回一个 301 Moved Permanently 响应,不经过任何其他处理,性能开销几乎为零。
4.7 关卡七:安全头(Security Headers)
除了 HSTS,还有几个关键的 HTTP 安全头,它们共同构成了现代 Web 应用的“盔甲”。
完整配置 :
# 在 server 块的 location / {} 中添加
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https:;" always;
X-Frame-Options: DENY防止你的网站被嵌入到别人的<iframe>中,抵御点击劫持(Clickjacking)。X-Content-Type-Options: nosniff阻止浏览器根据文件内容“猜测”MIME 类型,防止 MIME 类型混淆攻击。X-XSS-Protection: 1; mode=block是一个较老的 XSS 防护头,虽然现代浏览器已逐渐弃用,但保留它对旧客户端仍有价值。Referrer-Policy控制 Referer 头的发送行为,保护用户隐私。Content-Security-Policy (CSP)是最强大的防护头,它定义了哪些外部资源(脚本、样式、图片等)可以被加载。上面的配置是一个相对宽松的模板,你需要根据你网站的实际资源引用情况进行调整。
将这七道关卡全部配置到位后,你的 Nginx SSL 配置就不再是“能用”,而是真正达到了生产环境的安全标准。你可以用在线工具 https://www.ssllabs.com/ssltest/ 对你的域名进行扫描,它会给出一个详细的评分报告。一个配置完美的站点,应该能拿到 A+ 评级。
5. 自动化续期与故障排查:让证书永不“过期”的运维闭环
Let’s Encrypt 的证书有效期只有 90 天,这是其安全哲学的核心——短生命周期意味着即使证书被泄露,其危害窗口期也极短。但这给运维带来了挑战:你不能指望人工每三个月去手动续期一次。Certbot 提供了自动化续期机制,但它的默认设置在生产环境中往往是“半残废”的。我们必须亲手打造一个坚不可摧的运维闭环。
5.1 理解 Certbot 的续期机制与默认陷阱
Certbot 的续期命令是 sudo certbot renew 。它的工作原理是扫描 /etc/letsencrypt/renewal/ 目录下的所有 .conf 文件,检查每个证书的剩余有效期。 只有当剩余有效期少于 30 天时,它才会真正执行续期操作 。这是一个非常保守的策略,目的是避免过于频繁的请求给 Let’s Encrypt 服务器造成压力。
然而,这个“30 天”的阈值,正是第一个陷阱。想象一下,你的服务器因为某种原因(如磁盘空间耗尽、网络短暂中断)在证书还剩 35 天时,未能成功执行 renew 命令。那么,它会在 5 天后(即剩余 30 天时)再次尝试。如果这次又失败了呢?证书就会在 30 天后过期,而你的网站将在毫无预警的情况下变成“不安全”。
第二个陷阱是 renew 命令的执行环境。 renew 命令会尝试复用当初申请证书时所用的“验证方式”。如果你当初是用 --standalone 申请的,那么 renew 也会尝试用 --standalone 。这意味着,它会在续期时自动停止 Nginx,这在生产环境中是绝对不可接受的——它会造成数秒的服务中断。
5.2 构建高可用续期方案: --webroot + systemd timer
为了解决上述问题,我采用了一套被验证为“零中断”的续期方案: --webroot 验证方式 + 自定义 systemd timer。
--webroot 的核心思想是:Certbot 不需要接管端口,它只需要一个 Nginx 已经在服务的、可公开访问的 Web 根目录。它会在这个目录下创建 .well-known/acme-challenge/ 子目录,并将挑战文件放进去。Nginx 的配置只需保证这个路径能被正确路由即可。
第一步:配置 Nginx 的挑战路径 在你现有的、监听 80 端口的 server 块中,添加以下 location :
location ^~ /.well-known/acme-challenge/ {
# 指向一个专用的、权限宽松的目录
root /var/www/letsencrypt;
# 禁用所有可能干扰的模块
try_files $uri =404;
}
然后创建该目录并赋予权限:
sudo mkdir -p /var/www/letsencrypt
sudo chown -R www-data:www-data /var/www/letsencrypt
第二步:首次申请时使用 --webroot
sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.com -d www.example.com
这样,Certbot 就会将挑战文件写入 /var/www/letsencrypt/.well-known/acme-challenge/ ,而 Nginx 会将其原样返回给 Let’s Encrypt 服务器。
第三步:创建自定义 systemd timer 创建一个定时任务,每天凌晨 2:15 执行续期,并在失败时发送邮件告警。
- 创建续期脚本
/usr/local/bin/renew-letsencrypt.sh:
#!/bin/bash
# 日志记录
LOGFILE="/var/log/letsencrypt/renew.log"
echo "=== $(date) ===" >> $LOGFILE
# 执行续期,使用 webroot 方式,不重启 Nginx
sudo certbot renew --webroot -w /var/www/letsencrypt --quiet --no-self-upgrade >> $LOGFILE 2>&1
# 检查续期是否成功(检查日志中是否有 "renewed" 字样)
if grep -q "renewed" "$LOGFILE"; then
echo "Renewal successful. Reloading Nginx..." >> $LOGFILE
# 仅重载配置,不重启服务,零中断
sudo nginx -s reload >> $LOGFILE 2>&1
else
echo "Renewal failed. Sending alert..." >> $LOGFILE
# 发送邮件告警(需提前配置好 mailutils)
echo "Let's Encrypt renewal failed on $(hostname) at $(date)" | mail -s "ALERT: Certbot Renewal Failed" admin@example更多推荐


所有评论(0)