我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Linux Nginx 怎么在配置文件中处理多域名共用 SSL 证书的结构

Linux Nginx 怎么在配置文件中处理多域名共用 SSL 证书的结构 前言多域名共用一张证书这句话本身就有歧义它可能指两件完全不同的事一张证书的内容覆盖了多个域名即证书里带了多个 SAN 条目或者是一张通配符证书也可能指多个域名共用同一个server配置块和同一份证书文件。这两件事经常被混在一起讨论结果配出来的东西一半对一半错证书里没有这个域名握手就失败证书里有但server_name没写请求就被路由到默认站点。还有一个更底层、也更容易被忽略的点TLS 握手发生在 HTTP 请求之前。Nginx 决定用哪张证书靠的是客户端在 ClientHello 里带的 SNIServer Name Indication服务器名称指示而不是后面 HTTP 请求里的 Host 头。所以用server_name决定用哪张证书这个说法本身就是错的——server_name决定的是握手完成之后由哪个server块处理请求。本文把三种常见结构讲清楚一张多域名证书配一个server块、多张证书配多个server块、以及通配符证书配正则server_name。文中配置基于 RHEL 9 / nginx 1.24、OpenSSL 3.x证书文件路径以/etc/nginx/ssl/为例。一、三个概念的边界SAN、SNI、server_name理解配置结构之前先把这三个东西分清楚它们分别在不同的时间点起作用。概念作用层生效时机决定什么SANSubject Alternative Name证书内容签发时确定这张证书能证明哪些域名SNITLS 协议扩展ClientHello 阶段Nginx选哪张证书发给客户端server_nameHTTP 层握手完成之后请求由哪个server块处理由此可以推出两条硬性约束第一证书里没写的域名无论server_name怎么写都会握手失败或证书告警。用下面这条命令确认证书到底覆盖了哪些名字openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -text \ | grep -A1 Subject Alternative Name第二server_name写得再全如果没有匹配到 SNI比如客户端不支持 SNI或者直接用 IP 访问Nginx 会退回该监听端口上的default_server并发出它的证书。默认情况下第一个出现在配置里的server块就是隐式的default_server——这就是访问 IP 拿到了某个莫名其妙的证书的由来。二、结构一一个 server 块 多域名 server_name 一张多域名证书适用于多个域名的内容完全一样、共用一套应用和一份证书的场景。# /etc/nginx/conf.d/multi-domain.conf server { listen 443 ssl; listen [::]:443 ssl; server_name www.example.com example.com m.example.com; # 一张包含上述全部 SAN 的证书或 *.example.com 通配符证书 ssl_certificate /etc/nginx/ssl/example.com.fullchain.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ 404; } }这个结构的关键点server_name后面可以跟任意多个名字用空格分隔也可以用通配符*.example.com或正则以~开头。整个块只有一份ssl_certificate证书必须覆盖server_name里列出的全部名字。少一个那个域名访问时就会报证书不匹配。如果证书是 RSA 和 ECDSA 双份的nginx 1.11.0 起支持可以写多个ssl_certificate/ssl_certificate_key对Nginx 会按客户端的能力挑选。HTTP 到 HTTPS 的跳转可以单独用一个server块统一处理多个域名server { listen 80 default_server; listen [::]:80 default_server; server_name _; # $host 取的是请求里的域名多个域名共用一套跳转 return 301 https://$host$request_uri; }server_name _;是一个约定俗成的写法它只是一个永远匹配不上的普通名字用来表示这里的server_name不重要我就是要兜住所有请求。真正让它成为兜底的是default_server参数。注意$host在 HTTP/1.1 且带 Host 头时可靠HTTP/1.0 不带 Host 的请求会让$host退化成server_name的值跳转目标可能不符合预期遇到这种情况可以再加一个server_name ;的块单独处理。三、结构二多个 server 块各自持有自己的证书适用于每个域名有独立证书、或者内容完全不同的场景。这是最灵活的写法也是多域名场景下最接近各管各的的结构。# a.example.com server { listen 443 ssl; server_name a.example.com; ssl_certificate /etc/nginx/ssl/a.example.com.fullchain.pem; ssl_certificate_key /etc/nginx/ssl/a.example.com.key; root /srv/www/a; } # b.example.com另一张证书 server { listen 443 ssl; server_name b.example.com; ssl_certificate /etc/nginx/ssl/b.example.com.fullchain.pem; ssl_certificate_key /etc/nginx/ssl/b.example.com.key; root /srv/www/b; } # 兜底块SNI 没匹配上未知域名扫描、直接用 IP 访问时走这里 server { listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name _; # nginx 1.19.4 可直接拒绝握手不向未知域名暴露任何证书 ssl_reject_handshake on; }这里有两个必须做的动作一是显式指定default_server。不指定的话配置里排在最前面的那个server会被隐式当成默认它的证书就会被发给所有没匹配上 SNI 的客户端。这既不安全暴露了你有哪些站点也容易误导排查用户明明访问的是 IP却看到了 a 站的证书。二是兜底块不要发证书。ssl_reject_handshake on;nginx 1.19.4 及以上会让这个块在 TLS 握手阶段直接终止连接比发一张证书再返回 444更干净。如果你的 Nginx 版本低于 1.19.4退而求其次是给它配一张自签名的占位证书或者干脆让这个块成为唯一的default_server并只做return 444;。ssl_certificate放在server块里而不是http块里——这一点很重要如果一个域名单独写了ssl_certificate它就只对那个server块生效写在http里的那份不会覆盖它。四、结构三通配符证书与正则 server_name通配符证书wildcard certificate是一张覆盖*.example.com的证书配合server_name的通配符使用可以避免为每个子域名单独签发。先记住两条容易踩的规则server_name *.example.com;不匹配example.com本身。裸域必须单独写server_name example.com *.example.com;。写成.example.com则同时匹配裸域和所有一级子域这是 Nginx 特有的简写。通配符证书*.example.com只覆盖一级子域a.b.example.com不在覆盖范围内。如果要给每个子域名单独指定root用正则server_name捕获子域名server { listen 443 ssl; server_name ~^(?sub[a-z0-9-])\.example\.com$; # 一张 *.example.com 的通配符证书 ssl_certificate /etc/nginx/ssl/wildcard.example.com.fullchain.pem; ssl_certificate_key /etc/nginx/ssl/wildcard.example.com.key; # 用捕获到的子域名拼出站点目录 root /srv/www/$sub; }Nginx 中server_name的匹配顺序是固定的精确名字 → 以*开头的最长通配符 → 以*结尾的最长通配符 → 按配置顺序第一个匹配的正则 →default_server。正则是在最后才参与匹配的所以哪怕你把正则写在配置文件最前面精确名字也永远优先。用捕获的变量拼路径时要留意安全性正则里限制字符集上例中的[a-z0-9-]是必要的否则攻击者可以构造包含..的域名做目录穿越尝试。Nginx 会拒绝大部分非法域名但不要依赖这个保护。五、把公共 SSL 参数抽成一份 include无论用哪种结构ssl_protocols、会话缓存、OCSP stapling 这些参数是共用的重复写容易漏。把它们抽到一个片段文件里# /etc/nginx/snippets/ssl-common.conf ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; 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; # OCSP stapling需要能解析并访问 CA 的 OCSP 响应器 ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/example.com.chain.pem; resolver 223.5.5.5 119.29.29.29 valid300s; resolver_timeout 5s; ssl_prefer_server_ciphers off;然后在每个server块里引入server { listen 443 ssl; server_name a.example.com; ssl_certificate /etc/nginx/ssl/a.example.com.fullchain.pem; ssl_certificate_key /etc/nginx/ssl/a.example.com.key; include /etc/nginx/snippets/ssl-common.conf; root /srv/www/a; }几个必须说清楚的点ssl_ciphers只影响 TLS 1.2 及更早版本。TLS 1.3 的密码套件由 OpenSSL 单独管理要用ssl_conf_command Ciphersuites ...;nginx 1.19.4 及以上配置且不能配错名字。上面这串密码套件是 OpenSSL 里真实存在的名字但你应该用openssl ciphers -v 你的字符串在本机验证一遍不同发行版的 OpenSSL 版本支持情况不一样。ssl_trusted_certificate指的是用来验证 OCSP 响应签名的 CA 证书链和ssl_certificate指向的站点证书链是两个文件别指同一个。ssl_stapling on;要求resolver可用否则 Nginx 无法访问 OCSP 响应器会静默不 staple。ssl_prefer_server_ciphers off;是现代推荐值让客户端选设成on只在支持老旧客户端时有意义。六、证书文件本身的检查清单配置结构对了证书文件本身不对照样白搭。下面这几条命令建议在每次换证后跑一遍。# 1) 证书覆盖了哪些域名 openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -text \ | grep -A1 Subject Alternative Name # 2) 链是否正确fullchain 里应当是站点证书在前中间证书在后 grep -c BEGIN CERTIFICATE /etc/nginx/ssl/example.com.fullchain.pem # 3) 证书与私钥是否配对RSA / ECDSA 通用做法 openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -pubkey /tmp/cert.pub openssl pkey -in /etc/nginx/ssl/example.com.key -pubout /tmp/key.pub diff /tmp/cert.pub /tmp/key.pub echo 证书与私钥匹配 # 4) 有效期 openssl x509 -in /etc/nginx/ssl/example.com.fullchain.pem -noout -dates # 5) 握手时实际拿到的是哪张证书-servername 就是 SNI openssl s_client -connect 127.0.0.1:443 -servername a.example.com -showcerts /dev/null 2/dev/null \ | openssl x509 -noout -subject -issuer -dates第 5 条是最有价值的验证它直接模拟了带 SNI 的客户端能一次性验证 SNI 选证书、server_name路由、链完整性三件事。常见坑点❌ 在http块里写ssl_certificate然后在某个server块里又写了一份不同的。 ✅server块里的配置只作用于该块不会和http层的合并。多域名场景建议统一在server块内指定公共参数才用include抽取。❌ 认为server_name决定了用哪张证书。 ✅ 证书是由 SNI 在握手阶段选择的server_name只影响握手之后的 HTTP 路由。客户端不支持 SNI 时会落到default_server并拿到它的证书。❌ 多个server块都监听 443 却没人加default_server。 ✅ 配置里第一个出现的块会隐式成为默认未知域名的扫描请求会拿到它的证书。显式加一个兜底块并用ssl_reject_handshake on;1.19.4拒绝握手。❌ 通配符证书*.example.com配了server_name *.example.com;结果裸域访问报证书错误。 ✅ 通配符既不匹配裸域也不匹配多级子域。写成server_name example.com *.example.com;或者用.example.com简写。❌ssl_certificate指向的文件里只有站点证书没带中间证书。 ✅ 桌面浏览器可能靠缓存补上中间证书而正常显示手机和命令行客户端却会报链不完整。用grep -c BEGIN CERTIFICATE确认文件里有多个证书块且站点证书在最前面。❌ 换了证书文件后直接systemctl reload nginx不验证。 ✅ 先openssl s_client -connect 127.0.0.1:443 -servername 你的域名 /dev/null看实际发出的证书再 reloadnginx -t只检查文件能否打开不检查内容是否匹配。❌ 配了ssl_stapling on;却没有resolver或者ssl_trusted_certificate指成了站点证书链。 ✅ OCSP stapling 需要解析 OCSP 响应器域名且需要一份独立的CA 链文件来验证响应签名缺一个都会静默失效用openssl s_client -status检查有没有OCSP Response Status: successful。❌ 以为ssl_ciphers管住了全部密码套件。 ✅ 它只作用于 TLS 1.2 及更低版本TLS 1.3 的套件需要ssl_conf_command Ciphersuites ...;且用openssl ciphers -v校验字符串是否被本机 OpenSSL 接受。总结结构证书形态server_name写法适用场景关键约束单块多域名一张 SAN 或通配符证书a.com b.com m.a.com多域名同一套内容证书必须覆盖全部名字多块各自持证每域名一张每块一个精确名字各站点独立、证书独立必须显式指定default_server通配符 正则*.example.com一张~^(?sub.)\.example\.com$子域名动态生成只覆盖一级子域正则字符集要收紧多域名 SSL 的难点不在配置语法而在把证书内容SNI 选证HTTP 路由这三层分开看待。配置写完用openssl s_client -servername 你的域名对着每个域名各跑一次确认拿到的证书和预期一致再用一个不存在的域名或直接连 IP 试一次确认兜底块的行为是你想要的。这两步做完结构才算真正立住。
返回列表