基础入门:Nginx HTTPS 核心指令与最小配置

简单来说,HTTPS 现在就是互联网的门面,没它浏览器直接给你挂个大红叉,用户看着就心里发毛。作为全栈工程师,咱们在部署服务的时候,Nginx 肯定是绕不开的。现在 Nginx 的主线版本都到 1.27.3(2024年10月发布)了,这货对 TLS 的支持已经非常完善了。

很多新手一看到 SSL 配置就头大,觉得参数巨多。其实咱们搞个最基础的 HTTPS 服务,真没那么复杂。面试的时候也经常问:Nginx 实现 HTTPS 的最小配置包含哪几个指令? 答案就三个:ssl_certificate(指定证书)、ssl_certificate_key(指定私钥)、还有 listen 443 ssl(监听端口并开启 SSL)。

咱们直接上代码,看一个最精简、能跑起来的配置。假设你已经有了证书文件 example.com.crt 和私钥 example.com.key,放在了 /etc/nginx/ssl/ 目录下。

server { # 监听 443 端口,并且必须带上 ssl 参数 listen 443 ssl; # 如果是 IPv6 环境,加上这行 listen [::]:443 ssl; # 你的域名 server_name example.com www.example.com; # 证书路径(公钥) ssl_certificate /etc/nginx/ssl/example.com.crt; # 私钥路径 ssl_certificate_key /etc/nginx/ssl/example.com.key; # 日志配置,方便排查问题 access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log; # 网站根目录 location / { root /usr/share/nginx/html; index index.html index.htm; } }

值得留意的是,光有上面的配置,用户如果输入 http://example.com 是访问不到的,因为 HTTP 走的是 80 端口。咱们还得加一个 80 端口的 server 块,专门用来做跳转。这也是面试常客:Nginx 如何配置 HTTP 到 HTTPS 的强制跳转?

server { listen 80; listen [::]:80; server_name example.com www.example.com; # 直接 301 重定向到 HTTPS,简单粗暴 return 301 https://$host$request_uri; }

这里有个🔧 实战技巧:千万别在 return 301 后面写死域名,比如 return 301 https://example.com$request_uri;,万一你以后改域名或者加个二级域名,还得回来改配置。用 $host 变量是最稳妥的,它会自动匹配请求头里的 Host 字段。

还有个坑得提醒一下,很多老教程里会在 listen 443 后面加 http2,像这样 listen 443 ssl http2;。在 Nginx 1.25.1 之后的版本,其实不需要显式指定了,只要 Nginx 编译时带了 HTTP/2 模块,它会自动在 SSL 连接上协商 ALPN 来启用 HTTP/2。不过为了兼容性,写上也没啥坏处。

证书格式与私钥权限

打个比方,证书就是个文本文件,但格式得对。通常是 PEM 格式。如果你拿到的证书是 .pem 或者 .crt 都行,只要内容是 -----BEGIN CERTIFICATE----- 开头的就没问题。私钥同理,得是 -----BEGIN PRIVATE KEY----- 或者 -----BEGIN RSA PRIVATE KEY-----

另外,私钥文件的权限一定要控好!Nginx 启动用户(通常是 www-datanginx)需要能读取这个文件,但其他用户不能乱看。建议把私钥权限设为 600

chmod 600 /etc/nginx/ssl/example.com.key chown nginx:nginx /etc/nginx/ssl/example.com.key

不然重启 Nginx 的时候,日志里可能会报权限拒绝的错误,这种低级错误咱们得避免。

实战演练:使用 Certbot 配置免费 SSL 证书并自动续期

搞 HTTPS 最烦的是啥?买证书!以前一张通配符证书动不动几千块,对小项目来说肉疼。现在咱们都用 Let's Encrypt 的免费证书,配合 Certbot 这个神器,简直是开发者的福音。打个比方,Certbot 就是个自动化脚本,帮你申请、配置、续期一条龙服务。

现在的趋势就是 ACME v2 深度集成,虽然 Nginx 还没做到完全原生内置(还得靠 Certbot 这种客户端),但自动化程度已经很高了。咱们来实操一下,假设你的服务器是 Ubuntu/Debian 系统,跑的是最新的 Nginx。

第一步,先装 Certbot 和 Nginx 插件。别用 apt-get install certbot 这种太老的源,有时候版本跟不上。

sudo apt update sudo apt install certbot python3-certbot-nginx

装完之后,直接一条命令搞定:

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

运行后它会问你几个问题:

执行完之后,你再去看看你的 Nginx 配置文件,会发现 Certbot 已经很“贴心”地把证书路径和密钥路径都给你填进去了,甚至帮你加了一些基础的 SSL 参数。

证书续期与自动化

Let's Encrypt 的证书有效期只有 90 天,这是为了防止私钥泄露后长期有效。别担心,Certbot 装完后会自动在系统的定时任务(crontab)里加一个脚本,每天检查两次,快过期了就自动续。

不过,咱们做开发的,不能把宝全压在自动化上,得自己验证一下续期能不能跑通。运行这个命令(加 --dry-run 是模拟运行,不会真的申请证书,很安全):

sudo certbot renew --dry-run

如果看到 "Congratulations, all renewals succeeded" 这种字样,说明你的续期机制是通的。

💡 经验总结:虽然 Certbot 很智能,但它有时候会“手贱”改乱你的 Nginx 配置。如果你对配置文件的格式有严格要求,建议使用 certonly 模式,只让 Certbot 下载证书,不修改 Nginx 配置。

# 只获取证书,不修改 Nginx 配置 sudo certbot certonly --nginx -d example.com -d www.example.com

然后你自己手动去 Nginx 配置里指定路径。Let's Encrypt 的证书默认会存在 /etc/letsencrypt/live/example.com/ 目录下。你的配置得这么写:

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

实际案例记录:权限与端口

社区里讨论最多的坑就是权限问题。Certbot 运行时需要读取 /etc/letsencrypt 目录,而且 Nginx 也需要读取里面的文件。如果你是用 root 跑的 Certbot,但 Nginx 用的是普通用户,可能会遇到权限问题。

还有一个坑,Certbot 在做验证的时候,会临时启动一个服务占用 80 端口,或者让 Nginx 临时响应一个请求。如果你的 80 端口被其他程序(比如 Docker 容器、或者别的 Web 服务)占用了,就会失败。所以运行 Certbot 前,确保 80 端口是空闲的,或者 Nginx 是正常运行的。

另外,如果你在 Kubernetes 里用 Nginx Ingress,那就别用 Certbot 了,直接用 cert-manager 这个组件,它是 K8s 生态里专门干这个的,配合 Nginx Ingress 控制器,那是相当丝滑。

进阶优化:开启 TLS 1.3、OCSP Stapling 与 HSTS 安全头

基础配置跑通了,证书也自动续期了,这就结束了吗?作为一个有追求的全栈工程师,咱们得追求极致。现在的版本(比如 Nginx 1.27.3)默认都支持 TLS 1.3 了,这玩意儿比 TLS 1.2 握手快多了,而且更安全。咱们得把那些老旧的、不安全的协议(比如 SSLv3, TLSv1.0, TLSv1.1)都给禁了。

还有,面试常问的:什么是 TLS 握手?如何优化 HTTPS 的访问速度? 答案里肯定包含开启 TLS 1.3、会话复用、OCSP Stapling 这些。咱们一个个来配置。

先上一份比较“硬核”的配置,我直接把优化项都加进去了:

server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.example.com; # 证书路径 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # 1. 协议与加密套件优化 # 只开启 TLS 1.2 和 TLS 1.3,干掉那些老古董 ssl_protocols TLSv1.2 TLSv1.3; # 优先使用服务端定义的加密套件(现代浏览器通常不需要这个,但加上更稳) ssl_prefer_server_ciphers off; # TLS 1.3 的加密套件 ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; # 2. 会话复用优化 (Session Resumption) # 设置会话缓存,减少重复握手 ssl_session_cache shared:SSL:10m; # 会话超时时间,设为 1 天 ssl_session_timeout 1d; # 开启 TLS 1.3 的 0-RTT,也就是“早期数据”,能进一步加速,但有重放攻击风险,看情况开 ssl_early_data on; # 3. OCSP Stapling 配置 # 让服务器主动去查询证书状态,并缓存起来,客户端就不用自己跑去 CA 查了 ssl_stapling on; ssl_stapling_verify on; # 指定一个用于验证 OCSP 响应的信任证书链(通常是根证书或中间证书) ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; # 解析器,用于查询 OCSP 服务器,这里用 Google 的 DNS,或者你用 223.5.5.5 也行 resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # 4. HSTS 安全头 (HTTP Strict Transport Security) # 告诉浏览器:以后半年内,这个域名必须强制用 HTTPS 访问,哪怕你输的是 HTTP # max-age=31536000 是一年,includeSubDomains 包含子域名 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # 5. 其他安全头(顺手加一下) add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; # 网站根目录 location / { root /usr/share/nginx/html; index index.html index.htm; } }

深入聊聊 OCSP Stapling

简单来说,OCSP Stapling 就是为了解决“证书吊销检查”这个痛点。以前客户端拿到证书,还得去 CA 问问“这证是不是废了?”,这一问一答就增加了延迟,而且还泄露了用户隐私(CA 知道你访问了啥)。

开启 ssl_stapling on; 后,Nginx 会定期去 CA 那里把证书状态取回来,缓存到服务器上。当客户端握手时,Nginx 直接把这个状态(Stapling)发给客户端。客户端一看,哦,这证没问题,就省去了那次额外的查询。配置里那个 ssl_trusted_certificate 很关键,很多新手就是漏了这个导致 Stapling 没生效。

HTTP/3 (QUIC) 的前瞻

现在社区里聊得最火的就是 QUIC/HTTP3。Nginx 从 1.25 版本开始,通过 ngx_http_v3_module 实验性地支持了 HTTP/3。虽然现在(2024年)生产环境大规模部署的还不多,但趋势在那。如果你用的是最新的 1.27.3,想尝鲜的话,可以编译时加上 --with-http_v3_module

不过,HTTP/3 走的是 UDP 协议,监听方式跟 TCP 不一样,配置起来稍微麻烦点,还得处理 Alt-Svc 头。对于大多数业务,现在的 HTTP/2 配合 TLS 1.3 已经足够快了。

⚡ 效率提示:配置完这些优化后,千万别凭感觉觉得“应该没问题了”。去用工具测一下。推荐直接用 SSL Labs 的在线测试(搜索 SSL Test),输入你的域名,它会给你打个分。如果你按照上面的配置搞,基本能拿 A+。如果没拿到,看看是不是证书链没配全(要用 fullchain.pem 而不是 cert.pem),或者 HSTS 没开对。

另外,关于 ECC vs RSA 的选择,现在 Let's Encrypt 默认给的就是 ECC 证书(椭圆曲线),它比 RSA 密钥短但安全性更高,性能也更好,特别是在移动端。除非你的用户还在用那种老掉牙的 Android 4.x 系统,否则放心用 ECC。

4. 性能加速:HTTP/3 (QUIC) 部署与 SSL 会话复用调优

可以这么理解,现在给 Nginx 配上 HTTPS 只是个起步价,真正考验你功力的地方在于:用户访问你的网站快不快?尤其是在移动端网络不稳定的情况下。咱们现在用的是 Nginx 1.27.3(2024年10月刚发布的 Mainline 版本),这个版本对 QUIC 的支持已经相当成熟了,别再守着 HTTP/1.1 不放了。

HTTP/3 与 QUIC:真不是赶时髦

很多同学一听 HTTP/3 就觉得是噱头,其实不是。传统的 HTTPS 跑在 TCP 上,一旦丢包,整个连接都得等着(队头阻塞)。而 HTTP/3 是基于 UDP 的 QUIC 协议,抗丢包能力极强。在弱网环境下,那种秒开的感觉是 HTTP/2 给不了的。

要在 Nginx 1.27.3 上开启它,你得确认编译的时候带了 --with-http_v3_module。如果没带,那就得重新编译一下。配置起来其实没那么玄乎,核心就是监听 UDP 的 443 端口,并声明你是支持 h3 的。

看看这个配置,直接拿去用:

server { # 监听 443 端口,同时支持 TLS 和 HTTP/3 # 注意这里多了 quic 和 reuseport 参数 listen 443 ssl http2; listen 443 quic reuseport; listen [::]:443 ssl http2; listen [::]:443 quic reuseport; server_name example.com; # 证书配置(这里假设你已经申请好了) ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 核心要点:必须开启 TLS 1.3,HTTP/3 强依赖这个 ssl_protocols TLSv1.2 TLSv1.3; # 告诉浏览器:我支持 HTTP/3,你可以用 UDP 连我 # 这个 Header 非常关键,叫 Alt-Svc add_header Alt-Svc 'h3=":443"; ma=86400'; # 为了兼容性,也加上 h2 的 Alt-Svc(虽然通常不需要) add_header Alt-Svc 'h2=":443"; ma=86400'; # 日志格式里加上协议版本,方便你调试看是不是真的走了 QUIC access_log /var/log/nginx/access.log combined_quic; log_format combined_quic '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http3"'; location / { root /var/www/html; index index.html; } }

💡 经验总结:开启 QUIC 后,别急着上线。先用 Chrome 访问,打开 chrome://net-internals/#http3,看看你的域名是不是出现在了列表里。或者直接在 DevTools 的 Network 面板里看 Protocol 列,是不是显示了 h3。如果没显示,多半是防火墙把 UDP 443 给拦了,云服务器的话记得去安全组放行 UDP。

SSL 会话复用:别让用户每次都“握手”

除了换协议,还有一个老生常谈但极其有效的优化点:SSL 会话复用

换个角度看,TLS 握手是个很耗 CPU 的过程。如果每次用户刷新一下页面或者开个新标签都要重新握一次手,服务器压力山大。会话复用就是让客户端和服务器“记仇”(记住之前的密钥),下次连接直接复用,省去复杂的密钥交换过程。

Nginx 里主要用两种机制:ssl_session_cachessl_session_ticket

# 在 http 块或者 server 块里配置 ssl_session_cache shared:SSL:50m; # 搞个 50M 的共享内存,存会话信息 ssl_session_timeout 1d; # 会话有效期 1 天 ssl_session_tickets on; # 开启 Session Ticket # 如果你想更极致一点,调整下 buffer 大小 # 对于大文件下载或者视频流,适当调大 ssl_buffer_size 能提升吞吐量 ssl_buffer_size 16k; # 默认是 16k,如果是小接口 API 可以改成 4k 减少延迟

这里有个坑,很多新手以为配了 ssl_session_cache 就万事大吉,结果发现在负载均衡环境下不生效。那是因为 shared 类型的缓存只在同一台机器同一个 Nginx 进程间共享。如果你有多台服务器,得用 ssl_session_ticket_key 来同步密钥,不然用户跳转到另一台机器还是得重新握手。

🔧 实战技巧:别盲目追求 TLS 1.3 的 0-RTT。虽然 0-RTT 很快,但它有重放攻击的风险。对于金融或者涉及写操作的 API,建议把 ssl_early_data off;(默认就是关的),别为了那几十毫秒的延迟把安全给丢了。

---

5. 疑难排查:混合内容、证书链配置错误与常见坑点解析

搞 HTTPS 最让人头秃的不是配置,而是配完了浏览器上挂个“不安全”的小锁,或者明明证书没过期,用户访问却报错。这一章咱们就来聊聊那些年我踩过的坑,以及怎么填。

混合内容:那个烦人的小锁头

这是最常见的问题。你兴冲冲地把 Nginx 配好了,访问 https://example.com 也没问题,结果浏览器地址栏的小锁是灰色的,或者干脆有个黄色警告。F12 打开控制台,一堆红色的错误:Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource...

换个角度看,就是你网页里的图片、CSS 或者 JS 还在用 http:// 开头的地址。浏览器现在很严格,HTTPS 页面里不允许加载 HTTP 资源。

解决办法

server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 强制浏览器将后续的 HTTP 请求转为 HTTPS(除了你明确排除的) # upgrade-insecure-requests 这个指令很管用 add_header Content-Security-Policy "upgrade-insecure-requests"; # 如果是老项目,还可以用这个简单的 Meta 标签,但 Header 优先级更高 # 不过加在 Nginx 里最省心 location / { # 如果你用的是反向代理,记得把后端的 HTTP 链接也替换了 # sub_filter 'http://example.com' 'https://example.com'; # sub_filter_once off; proxy_pass http://localhost:8080; } }

证书链不完整:安卓手机和老浏览器的大坑

这是另一个重灾区。很多同学用 Certbot 或者 acme.sh 申请证书,往往只把 cert.pem 或者 fullchain.pem 里的一部分内容丢给 ssl_certificate。结果就是,Chrome 浏览器访问正常(因为它会自动补全证书链),但是安卓手机、老旧的 Java 客户端或者 Python 脚本访问就直接报错,提示证书不可信。

关键点:Nginx 的 ssl_certificate 指令,一定要指向 fullchain.pem(完整链),而不是单纯的 cert.pem。完整链包含了你的域名证书 + 中间证书。

看看这个标准的证书配置结构:

server { listen 443 ssl; server_name example.com; # 这里必须是 fullchain.pem,也就是包含了中间证书的文件 # 很多人的坑就在于这里只配了 domain.pem ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # 私钥,这个没啥好说的,权限一定要设成 600,别让其他人读 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # OCSP Stapling 配置,这能解决证书状态查询的延迟问题 # 服务器主动去 CA 那边拿证书状态,而不是让客户端去查 ssl_stapling on; ssl_stapling_verify on; # 这里需要指定一个信任的证书链,通常和 ssl_certificate 一样,或者指向系统的 CA 包 # Ubuntu/Debian 通常在 /etc/ssl/certs/ca-certificates.crt ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # DNS 解析器,用于 OCSP 查询,别用 8.8.8.8 在国内的话,用 223.5.5.5 或者 114.114.114.114 resolver 223.5.5.5 114.114.114.114 valid=300s; resolver_timeout 5s; location / { return 200 "SSL Config OK"; add_header Content-Type text/plain; } }

📌 要点提醒:配完之后,别光用浏览器看。去用 openssl 命令或者在线工具(比如 SSLLabs 的测试)跑一下。如果你看到提示 "Chain issues: Incomplete" 或者 "Extra download",那就说明你的证书链配错了。一定要确保服务器直接发送了完整的证书链,而不是让客户端去猜。

那些年 Certbot 挖的坑

既然提到了 Certbot,就不得不说自动化续期的问题。很多新手配了 Certbot 的 cron 任务,结果证书过期了还没续上。

通常是因为 Nginx 正在运行,Certbot 的 standalone 模式抢不到 80 端口,或者 webroot 模式路径不对。现在推荐用 --nginx 插件,它能自动修改配置并重载。

# 如果你是用 Certbot 申请的,续期脚本通常是这样 # 先测试一下 dry-run certbot renew --dry-run # 如果没问题,在 crontab 里加上(通常 Certbot 安装时会自动加) # 0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

关键点:--deploy-hook 非常重要!证书续期后,Nginx 必须 reload 才能加载新证书。如果没有这个 hook,哪怕证书续期成功了,Nginx 还在用旧的(快过期的)证书,到时候照样报警。

还有一个坑,就是权限问题。Certbot 默认用 root 跑,生成的证书文件权限很严格。如果你的 Nginx 是用非 root 用户跑的(虽然很少见),或者你手动把证书拷到了别的地方,一定要检查读权限,不然 Nginx 启动会报 SSL_CTX_use_PrivateKey_file failed 的错误。