1. 为什么在 Ubuntu 上为 Nginx 创建自签名证书不是“走个过场”而是必须亲手操刀的硬技能在 Ubuntu 系统上给 Nginx 配置 HTTPS很多人第一反应是“找个现成的脚本跑一下生成 crt 和 key 就完事了”。我最初也是这么想的——直到某天凌晨三点线上测试环境的前端页面突然报错NET::ERR_CERT_AUTHORITY_INVALID而浏览器地址栏那个刺眼的“不安全”红字像一记耳光打在我刚提交的 CI/CD 流水线配置上。排查了两小时才发现问题根本不在 Nginx 配置本身而在于 OpenSSL 生成证书时漏掉了-addext subjectAltName DNS:localhost,IP:127.0.0.1这一行。没有 SANSubject Alternative Name扩展现代浏览器Chrome 58、Firefox 48、Safari 10会直接拒绝信任哪怕证书日期、密钥长度、签名算法全都没问题。这背后不是玄学而是 HTTP/HTTPS 演进中一个被广泛忽略的硬性分水岭从“能用”到“可用”的临界点就卡在证书的 SAN 字段是否完备。你可能在本地开发时用openssl req -x509 -nodes -days 365一键生成过证书但那张证书只写了CNlocalhost它在 curl 里能通在旧版 IE 里能开但在今天任何一台新装的 Ubuntu 22.04 或 24.04 桌面系统上用 Firefox 访问https://localhost大概率会触发SSL Certificate OpenSSL verify result: unable to get local issuer certificate的错误提示——注意这不是 Nginx 报错而是 OpenSSL 库在验证链时自己失败了。因为现代系统默认启用更严格的证书路径验证策略要求根证书必须可追溯且域名匹配必须通过 SAN 字段完成而非过时的 CN 字段。所以这篇内容不是教你怎么“复制粘贴命令”而是带你重新理解在 Ubuntu Nginx 这个组合里自签名证书的本质是一份由你亲手签署、并强制系统信任的“数字身份契约”。它包含三个不可割裂的环节① 用 OpenSSL 构建符合 RFC 5280 规范的证书结构② 让 Ubuntu 系统级信任库ca-certificates识别并接纳你的私有 CA③ 在 Nginx 中精确绑定证书链与私钥同时规避 TLS 握手阶段常见的 SNI、ALPN、OCSP Stapling 等隐性陷阱。这三个环节中任意一个出偏差都会导致no required ssl certificate was sent这类看似 Nginx 配置错误、实则底层握手失败的诡异现象。而这类问题绝不会出现在官方文档的“Quick Start”章节里——它们只藏在你第一次手动敲下openssl x509 -in cert.crt -text -noout查看证书细节时那一行被你忽略的X509v3 Subject Alternative Name字段里。2. 从零构建可信证书链OpenSSL 命令背后的 RFC 逻辑与 Ubuntu 系统信任机制很多教程教你用一条命令生成自签名证书openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem这条命令确实能跑通但它生成的证书存在两个致命缺陷缺少 SAN 扩展且未声明为 CA 证书。前者导致现代浏览器拒绝加载后者导致 Ubuntu 系统无法将其纳入信任链。要真正解决问题我们必须拆解 OpenSSL 的证书生命周期并理解 Ubuntu 如何管理信任锚点。2.1 第一步创建私有 CA 根证书非临时签发而是建立信任源头在生产或严肃的开发环境中“自签名”不等于“随便签”。你需要先创建一个受控的私有证书颁发机构CA再用它签发服务器证书。这样做的好处是你可以将根证书一次性导入 Ubuntu 系统信任库后续所有由该 CA 签发的证书都会自动被信任无需逐个导入。命令如下# 1. 生成 CA 私钥使用 4096 位 RSA比默认 2048 更稳妥 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key # 2. 生成 CA 自签名证书关键明确声明为 CA并设置有效期 10 年 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj /CCN/STShanghai/LShanghai/OMyLocalCA/CNMyLocalCA这里-x509表示生成自签名证书-nodes跳过私钥加密开发环境可接受生产务必加密而-days 3650是重点Ubuntu 的ca-certificates包在更新时会过滤掉有效期短于 30 天的证书设为 10 年可避免频繁重签。更重要的是-subj参数中的/CNMyLocalCA这个 CN 名称将成为你后续所有服务器证书的信任链终点。提示/CCN/STShanghai这些字段并非随意填写。Ubuntu 的update-ca-certificates工具在解析证书时会严格校验证书的basicConstraints扩展是否包含CA:TRUE。如果省略-x509或未正确设置生成的证书将不具备 CA 属性后续导入系统信任库后Nginx 仍会报unable to get local issuer certificate——因为系统认出了你的根证书但发现它不能签发其他证书信任链断裂。2.2 第二步为 Nginx 生成带完整 SAN 的服务器证书请求CSR服务器证书不能直接自签必须通过 CSRCertificate Signing Request流程由你的私有 CA 签发。这一步的核心是必须显式注入 SAN 扩展且 SAN 必须覆盖所有你计划访问的域名和 IP。例如你既想用https://localhost又想用https://127.0.0.1甚至未来可能加https://dev.myapp.local这些都得写进 SAN。首先创建一个配置文件server.conf[req] default_bits 2048 prompt no default_md sha256 distinguished_name dn req_extensions req_ext [dn] C CN ST Shanghai L Shanghai O MyLocalCA OU DevOps CN localhost [req_ext] subjectAltName alt_names [alt_names] DNS.1 localhost DNS.2 dev.myapp.local IP.1 127.0.0.1 IP.2 ::1然后生成 CSR 和私钥# 生成服务器私钥2048 位足够4096 会显著拖慢 TLS 握手 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key # 生成 CSR引用上面的配置文件 openssl req -new -key server.key -out server.csr -config server.conf注意openssl req -newkey命令虽能一步到位但无法指定req_extensions因此必须拆分为genpkeyreq两步。这是很多教程踩坑的根源——他们用-newkey生成的 CSR 默认不含 SAN导致后续签发的证书依然无效。2.3 第三步用私有 CA 签发服务器证书关键继承 CA 属性并嵌入完整链现在用你的 CA 根证书和私钥对服务器 CSR 进行签名# 签发服务器证书有效期 365 天强制继承 CA 的签名算法和扩展 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 365 -sha256 -extfile server.conf -extensions req_ext这个命令中-extfile server.conf -extensions req_ext是灵魂所在。它告诉 OpenSSL把server.conf文件中[req_ext]段定义的subjectAltName扩展原封不动地写入最终的server.crt。执行后用openssl x509 -in server.crt -text -noout | grep -A1 Subject Alternative Name查看你会看到清晰列出的DNS:localhost, DNS:dev.myapp.local, IP Address:127.0.0.1, IP Address:::1。此时证书链已形成server.crt← signed by →ca.crt。但 Nginx 需要的是完整的证书链chain即server.crt后面紧跟ca.crt。我们合并为fullchain.crtcat server.crt ca.crt fullchain.crt提示不要用cat ca.crt server.crt fullchain.crt顺序错了。Nginx 要求第一个证书是服务器证书后续才是中间或根证书。顺序颠倒会导致SSL_CTX_use_certificate_chain_file failed错误。3. 让 Ubuntu 系统级信任你的私有 CAca-certificates 机制的深度解析与实操陷阱生成了ca.crt只是完成了“造币”要让 Ubuntu “认账”必须把它放进系统的信任钱包——即/usr/local/share/ca-certificates/目录并触发update-ca-certificates更新。但这远非sudo cp ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates两行命令那么简单。Ubuntu 的证书信任体系有三层校验逻辑任何一层失败都会导致unable to get local issuer certificate。3.1 Ubuntu 证书信任的三层校验模型层级位置校验内容失败表现L1文件格式与命名规范/usr/local/share/ca-certificates/文件必须是.crt后缀内容必须是 PEM 格式以-----BEGIN CERTIFICATE-----开头且不能包含空行或多余字符update-ca-certificates输出WARNING: Skipping duplicate certificate ca.pem或静默忽略L2证书自身有效性/etc/ssl/certs/软链接目标证书必须未过期、签名有效、basicConstraints包含CA:TRUE、Key Usage包含Certificate Signupdate-ca-certificates报ERROR: cannot create symlink ...或生成的/etc/ssl/certs/ca.pem文件为空L3系统级信任链加载/etc/ca-certificates.confupdate-ca-certificates会读取此文件确认该证书是否被“启用”。若文件中无对应条目即使物理文件存在也不会被加载curl https://localhost仍报证书错误但openssl s_client -connect localhost:443 -CAfile /etc/ssl/certs/ca-certificates.crt却能成功3.2 安全可靠的导入流程绕过所有常见陷阱第一步确保ca.crt符合 L1 要求用file ca.crt检查是否为PEM certificate用head -n1 ca.crt确认首行为-----BEGIN CERTIFICATE-----用tail -n1 ca.crt确认末行为-----END CERTIFICATE-----。若有空行用sed /^$/d ca.crt ca_fixed.crt清理。第二步复制到标准目录并重命名L1 关键Ubuntu 要求文件名必须以.crt结尾且不能有特殊字符。推荐命名mylocalca.crtsudo cp ca.crt /usr/local/share/ca-certificates/mylocalca.crt第三步手动编辑/etc/ca-certificates.confL3 关键直接运行sudo update-ca-certificates可能不会自动启用新证书。更可靠的方式是echo mylocalca.crt | sudo tee -a /etc/ca-certificates.conf sudo update-ca-certificates执行后你会看到类似输出Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done. Running hooks in /etc/ca-certificates/update.d... done.第四步验证是否生效L2 L3 综合验证检查/etc/ssl/certs/下是否生成了对应哈希链接ls -l /etc/ssl/certs/ | grep mylocalca # 应看到类似lrwxrwxrwx 1 root root 42 May 20 10:30 12345678.0 - /usr/local/share/ca-certificates/mylocalca.crt再用 OpenSSL 直接验证信任链openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.crt # 正确输出server.crt: OK # 错误输出server.crt: C CN, ST Shanghai, L Shanghai, O MyLocalCA, CN localhost # error 20 at 0 depth lookup: unable to get local issuer certificate注意openssl verify命令中的-CAfile必须指向/etc/ssl/certs/ca-certificates.crt这是系统合并后的信任库而不是你原始的ca.crt。这是区分“证书是否被系统信任”的黄金标准。3.3 一个被严重低估的陷阱Docker 容器内的证书信任如果你在 Ubuntu 主机上配置好了一切但curl在 Docker 容器内访问https://host.docker.internal依然报错问题很可能出在这里。Docker 容器默认使用自己的ca-certificates包它完全独立于宿主机。你必须在容器内重复上述步骤或在构建镜像时注入COPY mylocalca.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates或者更轻量的做法是在运行容器时挂载宿主机证书docker run -v /etc/ssl/certs:/etc/ssl/certs:ro your-image但要注意/etc/ssl/certs是符号链接目录直接挂载可能失效。更稳妥的是挂载/usr/share/ca-certificates并在容器内执行update-ca-certificates。4. Nginx 配置的终极校验清单从 listen 指令到 TLS 1.3 握手的全链路避坑证书和系统信任都搞定了最后一步是 Nginx 配置。网上大量教程只告诉你加几行ssl_certificate却忽略了 Nginx 的 SSL 配置是一个精密的“握手协议协商器”任何一个参数不匹配都会导致no required ssl certificate was sent这类底层错误。这不是 Nginx 拒绝服务而是 TLS 握手在 ClientHello 阶段就失败了——客户端甚至没机会发送证书请求。4.1 最小可行配置去掉所有干扰项直击核心先写一个绝对精简、100% 可用的配置放在/etc/nginx/sites-available/selfsignedserver { listen 443 ssl http2; server_name localhost; # 证书与私钥必须用 fullchain.crt不是 server.crt ssl_certificate /path/to/fullchain.crt; ssl_certificate_key /path/to/server.key; # 强制使用现代 TLS 协议禁用 TLS 1.0/1.1 ssl_protocols TLSv1.2 TLSv1.3; # 指定强密码套件优先 ECDHE禁用弱算法 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; # 启用 OCSP Stapling提升性能减少握手延迟 ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 1.1.1.1 valid300s; resolver_timeout 5s; # 其他基础配置 root /var/www/html; index index.html; location / { try_files $uri $uri/ 404; } }关键点解析listen 443 ssl http2;ssl关键字是强制的没有它 Nginx 不会初始化 SSL 上下文http2启用 HTTP/2需 OpenSSL 1.0.2 和 Nginx 1.9.5。ssl_certificate必须指向fullchain.crt即server.crtca.crt的合并文件。若只指server.crtNginx 无法向客户端提供完整的信任链客户端尤其是 Chrome会因“无法验证颁发者”而中断连接。ssl_protocols TLSv1.2 TLSv1.3;Ubuntu 22.04 默认 OpenSSL 3.0完全支持 TLS 1.3。但若你用的是老旧的 Ubuntu 18.04OpenSSL 1.1.1请保留TLSv1.2移除TLSv1.3否则 Nginx 启动失败。ssl_ciphers列表中ECDHE开头的套件表示前向保密PFS这是现代 HTTPS 的基石AES128-GCM是当前最平衡的加密算法比AES256性能更好安全性不输。4.2 启用 HTTP/HTTPS 重定向安全加固的必选项仅配置 443 端口是危险的。用户可能手动输入http://localhost而你的应用若未强制跳转就会以明文传输。添加一个纯 HTTP server 块server { listen 80; server_name localhost; return 301 https://$server_name$request_uri; }注意return 301比rewrite更高效且$server_name保证了重定向 URL 与server_name一致避免https://localhost被重写成https://127.0.0.1导致证书域名不匹配。4.3 终极调试用 OpenSSL 模拟真实 TLS 握手当浏览器报错时别急着改 Nginx 配置。先用 OpenSSL 客户端模拟一次完整的握手定位是哪一层失败# 测试基础握手不验证证书 openssl s_client -connect localhost:443 -servername localhost # 测试带证书验证的握手使用系统信任库 openssl s_client -connect localhost:443 -servername localhost -CAfile /etc/ssl/certs/ca-certificates.crt # 测试 TLS 1.3 握手排除协议兼容问题 openssl s_client -connect localhost:443 -servername localhost -tls1_3观察输出中的关键行SSL handshake has read 1234 bytes and written 567 bytes握手成功。Verify return code: 0 (ok)证书验证通过。New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384协议和密码套件协商成功。若出现read:errno0或SSL routines:ssl3_read_bytes:sslv3 alert handshake failure说明 Nginx 根本没返回证书问题在ssl_certificate路径或listen ssl缺失。若出现Verify return code: 21 (unable to verify the first certificate)说明fullchain.crt缺少根证书或系统信任库未更新。实操心得我在调试一个unexpected status 404 not found的接口错误时发现它根本不是后端问题而是前端调用fetch(https://localhost/api)时TLS 握手失败导致请求被浏览器拦截最终返回了 404。用openssl s_client一测Verify return code: 20unable to get local issuer certificate立刻锁定是 CA 未导入系统。这种底层握手失败日志里往往只显示connection reset必须用 OpenSSL 才能精准捕获。5. 从开发到部署的平滑演进如何将自签名方案无缝升级为生产级证书自签名证书的价值从来不是替代 Lets Encrypt而是构建一套可平滑演进的信任基础设施。你在 Ubuntu 上为 Nginx 配置的这套流程其目录结构、证书管理逻辑、Nginx 配置模板完全可以复用到生产环境。下面是我团队实践出的三级演进路径5.1 阶段一本地开发当前方案证书存储/etc/nginx/ssl/dev/ca.crt,server.crt,server.key,fullchain.crtNginx 配置/etc/nginx/sites-available/dev-localhost特点CA 有效期 10 年私钥不加密SAN 包含localhost,127.0.0.1,::15.2 阶段二测试/预发布环境引入自动化签发当项目进入 QA 阶段需要为test.myapp.com等真实域名配置 HTTPS。此时不再手动生成 CSR而是用certbot申请免费的 Lets Encrypt 证书但复用你已有的 Nginx 配置结构# 使用 --webroot 模式复用现有 Nginx 的 web root sudo certbot certonly --webroot -w /var/www/html -d test.myapp.com # 生成的证书在 /etc/letsencrypt/live/test.myapp.com/ # 直接软链接到你的标准路径 sudo ln -sf /etc/letsencrypt/live/test.myapp.com/fullchain.pem /etc/nginx/ssl/test/fullchain.crt sudo ln -sf /etc/letsencrypt/live/test.myapp.com/privkey.pem /etc/nginx/ssl/test/server.keyNginx 配置文件几乎不用改只需更新server_name和证书路径。certbot renew命令会自动更新证书你只需在renew-hook中加一行systemctl reload nginx。5.3 阶段三生产环境企业级 PKI 集成对于金融、医疗等合规要求高的场景Lets Encrypt 不够用。此时你的私有 CA 就派上大用场了。将ca.key和ca.crt迁移到企业内部的 HashiCorp Vault 或 Keycloak 中所有服务器证书的签发都通过 Vault 的 PKI 引擎 API 完成# Vault CLI 签发示例 vault write -fieldcertificate pki/issue/myapp-server common_nameprod.myapp.com alt_namesprod-api.myapp.com,10.0.1.100生成的证书依然保存在/etc/nginx/ssl/prod/Nginx 配置保持不变。唯一的区别是ca.crt不再是本地文件而是由 Vault 的pki/cert/ca接口动态获取并通过 Ansible 注入到所有节点。这就是我坚持手写 OpenSSL 命令的根本原因它不是为了“炫技”而是为了掌握证书信任的原子操作。当你理解了basicConstraints、subjectAltName、CAfile、fullchain这些概念的物理含义你就拥有了在任何环境Docker、K8s、VMware、裸金属中快速构建、诊断、迁移 HTTPS 信任链的能力。那些“一键生成”的脚本永远无法教会你当curl报unable to get local issuer certificate时该去/etc/ssl/certs/下找哪个哈希文件或者该用openssl x509 -in去检查哪一行扩展字段。最后分享一个小技巧在 Ubuntu 终端里把常用的 OpenSSL 命令做成 alias比如alias nginx-cert-checkopenssl x509 -in /etc/nginx/ssl/dev/server.crt -text -noout | grep -A1 Subject Alternative Name。每次怀疑证书有问题敲一行就出结果。真正的效率从来不是省掉几秒钟而是把模糊的“可能哪里错了”变成确定的“就在这行字段里”。