ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenSSL自签名证书生成指南:从私钥到Nginx落地配置

OpenSSL自签名证书生成指南:从私钥到Nginx落地配置 如果你做Web开发、跑内网服务或者维护测试环境迟早会撞上这么一件事需要给某个服务加HTTPS但域名是临时的、IP是内网的申请公共证书要么麻烦要么直接不被允许。这时候OpenSSL生成自签名证书就是最直接的办法——不用花钱、不受域名限制、三分钟就能签发一张能用的证书。不过很多人在这一步就停住了不是命令敲不出来而是搞不明白生成之后怎么让服务认它、让浏览器和客户端不再报错以及Windows环境下OpenSSL本身那些恼人的版本问题。这篇内容我打算一次讲透从自签名证书到底解决什么问题、Windows下OpenSSL怎么装怎么避开DLL版本冲突到私钥、CSR、签发证书的完整命令拆解再到SAN扩展、证书链、格式转换和各环境落地配置。适合正在做本地开发、内网部署、API加密调试的开发者也适合刚接手一台服务器、需要快速搞定TLS配置的运维。读完之后你可以把这篇文章当一份安全手册存着需要时照着敲就行。1. 自签名证书不是不安全而是信任关系需要自己维护1.1 自签名证书和CA签发证书的本质区别很多人一听到自签名证书就下意识觉得不安全其实这是个误解。证书本身不外乎是一段用非对称加密技术签名的数据它的作用不是让通信变得更安全而是让通信双方能够验证对方确实是它声称的那个实体。CA签发和自签名的区别在于信任的起点不同。CA签发的证书浏览器和操作系统因为内置了这些CA的根证书所以天然信任链条是通的用户访问你的网站浏览器发现证书链可以追溯到它信任的根CA于是显示绿色锁。自签名证书则是签名者就是证书本身浏览器没有内置信任它于是弹出您的连接不是私密连接。换句话说自签名证书在加密强度上一点不弱它缺的只是被广泛信任这个社会属性。所以自签名证书完全可以在特定场景下放心用关键是控制好使用边界。我在本地开发、内网工具、自动化测试里用自签名证书从来没有出过安全问题。因为真正加密传输的部分和CA签发的证书用的是同一套RSA或ECC算法没有本质差别。1.2 这些场景选自签名证书是正确姿势我归纳了一下自签名证书主要有四个合适的用武之地本地开发环境本地起一个HTTPS服务需要模拟线上环境调试尤其是调试Cookie、Service Worker、OAuth跳转这些对协议敏感的功能。本地环境域名往往是localhost或127.0.0.1没法申请公共证书现在公共CA一般不签纯IP证书签localhost的也很少。内网服务加密公司内网部署的Jenkins、GitLab、监控面板、内部Wiki访问域名是内网IP或者内网私有域名外网CA不会给你签这种证书。设备与客户端身份认证IoT设备、内部API之间做双向TLSmTLS服务端和客户端各持一张证书互相验证。这种场景是自己维护信任模型自签名反而更合适。临时测试环境压测平台、临时演示环境用自签名快速顶上过期了再签一张就是几秒钟的事。不应该用自签名证书的场景是公网用户直接访问的站点。用户浏览器会弹出警告可能直接劝退访客到时候你省下的证书钱还不够客服解释的。公网服务老老实实用免费的Lets Encrypt或云厂商证书没必要省。1.3 一台服务器直接自签多台服务器一定要建私有CA这里有一个我踩过坑之后才总结出来的规律如果只有一台服务器需要TLS直接给这台服务器生成自签名证书就够了。如果会有多台服务器、多个域名甚至以后还要增删一定上来就先做一个私有根CA。原因很简单私有CA方案下你只需要在每台客户端电脑上安装一次根CA证书之后你用这个根CA签发的所有服务器证书客户端都会自动信任。如果你贪省事每台服务器各签一个自签名证书那么客户端每访问一台新服务器就要手动信任一张新证书人被逼疯是迟早的事。我见过一个团队前后装了二十多套内网系统全部是各自的自签名证书。员工每打开一个新系统就要点一次高级-仍然继续后来换了私有CA方案一次性把根证书通过域策略推给全员世界清净了。这个决策要趁早做越早越省事。2. 环境准备Windows下OpenSSL的安装与版本混乱问题2.1 选哪个Windows发行版Light版、MSYS2还是包管理器先明确一点OpenSSL官方不提供Windows安装包Windows上常见的获取渠道有Shining Light Productions也就是很多教程里那个slproweb链接、MSYS2、Cygwin以及Chocolatey/Winget等包管理器。Shining Light的Win64/Win32安装包最省事装完就有openssl.exe可用。分成Light版和Full版Light版只有可执行程序和DLLFull版还带开发用的头文件、库文件。只使用命令行工具Light版足够要编译C/C程序链接OpenSSL才需要Full版。MSYS2或Cygwin适合已经在用这些工具链的开发者包内版本很新还带依赖管理。Chocolatey一条命令choco install openssl搞定适合习惯包管理的同学。版本上我个人建议直接装3.x系列原因等下说。如果因为老程序兼容性问题必须用1.1.1那也要固定好版本避免多个版本混装。2.2 安装后必须配置的环境变量与验证命令Windows安装包在安装时会提示是否把OpenSSL的bin目录加入PATH这一步别跳过。装完后手动检查一下环境变量确认openssl.exe所在目录常见是C:\Program Files\OpenSSL-Win64\bin确实在PATH里。在CMD或PowerShell里执行openssl version看到版本号就说明基本OK。执行openssl version -a可以看更多信息包括OpenSSL的配置目录路径。另外需要注意有些工具会自带OpenSSL并且它们把DLL放在自己的目录里。比如Git for Windows自带一套OpenSSLPython在Windows上也会带上libcrypto和libssl的DLL。这些本身没问题问题在于它们会互相抢DLL版本这就是下面要说的经典坑。2.3 经典报错 openssl version mismatch built against 30000070, you have 38500000 的来龙去脉这个报错Google一搜一大把中文网上讲透的很少。它本质上是编译期版本与运行期版本不一致。这里先解释一下OpenSSL版本数字的规则。OpenSSL内部把版本号编码成一个十六进制整数30000070对应的是OpenSSL 3.0.738500000这种大数字意味着运行时加载到了一个比预期更高版本号的DLL。当你运行某个程序比如git、nginx、一些编译好的工具程序内部声明我是基于3.0.7编译的但Windows在加载DLL时按照搜索顺序找到了另一个版本的libcrypto-3-x64.dll或libssl-3-x64.dll于是程序直接罢工。这个报错在Windows上高发的具体原因有两个PATH环境变量里同时排了几个OpenSSL的bin目录。Windows加载DLL的搜索顺序是程序所在目录、系统目录、PATH目录。如果你装过多个版本PATH靠前的那位就把版本定死了。应用自带旧版本DLL系统里又装了一个新版本。比如老工具编译时用的是1.1.1w0x1010117f运行期系统PATH里却放了一个3.x的DLL两边版本协议不兼容就会报类似version mismatch的错误。热搜词里那批人说win64 openssl v1.1.1w能解决问题正是这个原理——把编译期版本和运行期版本重新对齐。我给的排查和解决办法依次是先定位加载了哪个DLL。在CMD里运行where openssl确认命令行工具指向哪个目录再用where libcrypto-3-x64.dll之类的命令查看要加载的动态库实际在哪里。这一步能确认PATH里到底有几个OpenSSL在打架。清理PATH只保留一个OpenSSL的bin目录把其他版本的目录删掉。如果应用自带DLL在它自己的程序目录那就保证这个目录排在PATH前面或者干脆把系统PATH里的OpenSSL目录先拿掉让程序用自带DLL。如果应用编译期版本明确比如报错里写了built against 30000070那就装回OpenSSL 3.0.7或同系列版本让运行期版本和编译期版本一致。这也是为什么很多老软件必须搭配1.1.1w才能跑不是OpenSSL 3.x不好而是软件没跟上。终极解法用Docker或者WSL跑OpenSSL和Windows环境完全隔离。后面我会给一个Docker命令示例。3. 三步生成自签名证书私钥、CSR、签名到底在做什么3.1 第一步用genrsa还是genpkey生成RSA私钥生成私钥有很多种命令最常见的是openssl genrsa不过我看官方文档的推荐倾向已经变成了更通用的openssl genpkey。两者都能用差别在于genpkey统一了RSA、EC、Ed25519等多种算法的生成入口而且允许直接指定算法参数。生成RSA 2048位私钥openssl genrsa -out server.key 2048或者用genpkeyopenssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key如果你想用ECC椭圆曲线密钥更小更快适合移动端场景生成基于secp256r1也就是P-256曲线的私钥openssl ecparam -genkey -name secp256r1 -out server_ec.key我实际使用中的建议是追求最大兼容性选RSA 2048追求握手性能选ECC P-256。以前我总喜欢RSA 4096觉得更安全后来发现这玩意儿握手时CPU开销明显高移动端弱网环境下差异更明显而普通业务场景2048位已经完全够用。如果你的证书是用来做设备通信、API加密ECC明显更合适——密钥短、握手快、功耗低。私钥生成后Linux下记得改权限chmod 600 server.keyWindows下则要在文件属性里做ACL设置只保留当前用户和SYSTEM的读取权限。私钥泄露等于证书作废这个习惯最好一开始就养成。3.2 第二步CSR里的每个字段都代表什么CSRCertificate Signing Request是证书签名请求。你可以把它理解为申请人填的一张申请表里面写了你要申请证书的域名、组织信息然后用私钥做了签名CA拿到之后确认信息无误就用CA的私钥帮你签发。生成CSR的命令openssl req -new -key server.key -out server.csr \ -subj /CCN/STBeijing/LBeijing/OMy Company/CNapi.example.com其中每个字段的含义分别是C国家代码比如CN、US填两位字母。ST省份或州。L城市。O组织名称一般是公司名。CNCommon Name通用名称。这是最关键的一个字段对服务器证书来说它应当是你的域名或IP地址比如api.example.com。浏览器历史上校验的就是这个字段和地址栏域名是否一致。OU部门可选。如果不带-subj命令会交互式地一个字段一个字段问你。写脚本自动化签发时强烈建议用-subj一步到位避免交互过程卡住CI/CD流程。3.3 第三步直接自签还是借道CSR用CA签生成CSR之后有两个方向。如果只是单张自签名证书完全不需要CSRopenssl req可以直接一步生成openssl req -x509 -newkey rsa:2048 \ -keyout server.key -out server.crt \ -days 365 -nodes \ -subj /CCN/STBeijing/LBeijing/OMy Company/CNapi.example.com这里-x509表示直接输出自签名证书而不是CSR-newkey表示同时生成新私钥-days 365是有效期一年-nodes表示私钥不加密这样nginx等服务启动时不需要输入密码。如果你走的是私有CA方案用根CA的私钥给申请方签发证书openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 825-CAcreateserial会自动生成并维护一个序列号文件ca.srl避免每次签发的证书序列号重复。序列号的作用我们下一节详细说。到这一步你其实已经有三张文件私钥server.key、证书server.crt、也许还有CSR。CSR签完就没用了但建议留一份存档方便以后看证书申请历史。4. 让证书在浏览器里真正可用SAN、序列号与证书链4.1 Chrome 58之后只看SANCN字段不再是免死金牌我见过不少人信心满满地把带CN的自签名证书安在Nginx上结果浏览器照样报NET::ERR_CERT_COMMON_NAME_INVALID怎么查都查不出来。原因是Chrome 582017年发布之后浏览器不再检查CN字段只检查SANSubject Alternative Name扩展。这是个非常容易踩的坑。过去证书里写CN域名就够了现在必须在证书扩展里写清楚这个证书支持哪些域名和IP。自签名证书默认不带SAN需要手动加。生成带SAN的自签名证书一条命令openssl req -x509 -newkey rsa:2048 \ -keyout server.key -out server.crt \ -days 365 -nodes \ -subj /CCN/STBeijing/LBeijing/OMy Company/CNlocalhost \ -addext subjectAltNameDNS:localhost,DNS:api.example.com,IP:127.0.0.1,IP:10.0.0.8-addext是OpenSSL 1.1.1以后才支持直接命令行加扩展早期版本只能靠配置文件。如果你用的是我在第2节建议的OpenSSL 3.x那直接用-addext就行。如果签发的是私有CA签名的服务器证书扩展信息放在一个ext文件里更清晰# san.cnf subjectAltNameDNS:api.example.com,DNS:*.example.com,IP:10.0.0.8然后在用CA签名时指定openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 825 -extfile san.cnfSAN字段里同时支持DNS:和IP:两种形式。开发常用DNS:localhost配合IP:127.0.0.1内网服务用IP:10.x.x.x生产用域名。为了测试方便把可能用到的都写进去一张证书通吃。4.2 用openssl防坑标配SAN加随机序列号我们上面已经解决了SAN问题。序列号Serial Number也要顺手提一下。自签名证书如果不指定序列号OpenSSL有时候会默认生成一个固定值或者容易在证书轮换时出现相同序列号。在客户端做证书校验时序列号相同而内容不同的证书会导致缓存和验证混乱。稳妥的做法是用随机数生成序列号openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -set_serial 0x$(openssl rand -hex 16) \ -out server.crt -days 825 -extfile san.cnf因为用了-CAcreateserial根CA自己会维护序列号自增但每张证书单独生成时如果想更保险也可以配合-set_serial基因随机数。这里给大家一个经验CA签发大流量证书时序列号冲突是真实存在的事故原因别在这种细节上偷懒。4.3 多服务器场景私有CA签发证书与证书链拼接回到多服务器场景。搭建私有根CA的完整三条命令如下先生成根CA私钥和自签名根证书有效期放长到十年openssl req -x509 -newkey rsa:4096 \ -keyout ca.key -out ca.crt \ -days 3650 -nodes \ -subj /CCN/STBeijing/LBeijing/OMy Private CA/CNMy Private Root CA然后用根CA给具体服务器签发证书openssl req -new -key server.key -out server.csr \ -subj /CCN/STBeijing/LBeijing/OMy Company/CNapi.example.com openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 825 -extfile san.cnfNginx和Apache配置证书链时要把服务器证书和根CA证书合并成一个文件顺序是服务器证书在前、根CA在后拼接成一个fullchain.crtcat server.crt ca.crt fullchain.crt这个文件在Nginx里配到ssl_certificate客户端连接时服务端会同时下发证书链浏览器就能顺着链条找到它信任的根CA。漏掉证书链是内网TLS配置最常见的失误之一往往服务端看起来配好了但不少客户端依然报certificate chain incomplete。5. 从生成到落地Nginx、IIS、Java和curl的配置实战5.1 PEM、DER、PFX三种格式与转换命令OpenSSL默认生成的证书是PEM格式就是那种以-----BEGIN CERTIFICATE-----开头的Base64文本。但不同平台要求的格式不一样PEMNginx、Apache、curl、大多数Linux服务。DERWindows系统比较常见是二进制的证书存储格式。PFX/P12包含私钥和证书的组合文件Windows IIS、macOS钥匙串、Java导入都爱用这种。转换命令我整理成一组直接抄# PEM转DER openssl x509 -in server.crt -outform DER -out server.der # DER转PEM openssl x509 -inform DER -in server.der -outform PEM -out server.pem # PEM含私钥转PFX openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt -passout pass:yourpassword # 查看PFX内部内容 openssl pkcs12 -info -in server.pfx有两点经验提醒大家转换PFX时-passout pass:...如果不写命令会交互式让你输入密码脚本自动化时要显式指定另外PFX内同时含私钥和证书所以文件权限要按私钥的标准来管别当成普通证书随便发。5.2 Nginx、IIS和Java环境分别要什么格式Nginx配置server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/certs/fullchain.crt; ssl_certificate_key /etc/nginx/certs/server.key; }注意Nginx的ssl_certificate指向的是合并后的证书链文件ssl_certificate_key是私钥。改完配置必须执行nginx -t测试语法然后nginx -s reload让配置生效。IIS环境IIS无法直接使用PEM格式需要先转成PFX然后在IIS管理器的服务器证书里导入再给站点绑定443端口选择这张证书。netsh http add sslcert命令行方式也可以但操作起来不如图形界面直观这里就不展开。Java环境如果你的服务跑在Tomcat、Spring Boot上Java用keytool管理证书。先导入PFXkeytool -importkeystore \ -srckeystore server.pfx -srcstoretype PKCS12 -srcstorepass yourpassword \ -destkeystore server.jks -deststoretype JKS -deststorepass changeit把server.jks放到应用目录然后在Spring Boot的application.yml里配置server: ssl: key-store: classpath:server.jks key-store-password: changeit key-store-type: JKSJava环境里一个高频问题是SSLHandshakeException多半是证书链不完整解决方法和Nginx一样把服务器证书和根CA按正确顺序合并后再导入。5.3 双向TLS场景下的客户端证书自签名证书的另一个杀手级用法是双向TLSmTLS。服务端和客户端各持一张证书握手时互相验证。内网API认证、K8s集群组件通信、设备接入认证都用它。生成客户端证书的套路和服务端一模一样# 客户端生成私钥和CSR openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out client.key openssl req -new -key client.key -out client.csr -subj /CNclient-001 # 用根CA给客户端签发 openssl x509 -req -in client.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out client.crt -days 825curl测试双向TLScurl --cacert ca.crt --cert client.crt --key client.key https://api.example.com注意这里--cacert ca.crt是让curl信任我们的根CA--cert和--key带上客户端身份。服务端Nginx如果需要开启客户端证书验证配置里加两行ssl_verify_client on; ssl_client_certificate /etc/nginx/certs/ca.crt;我就靠这套方案给内部微服务之间加了加密和身份认证比在代码里写token、配密钥来得干净利落。你只需信任根CA剩下所有组件身份都由证书自证权限模型清晰很多。6. 生成证书后的验证与常见报错排查6.1 openssl verify与openssl x509 -text的验证套路证书生成完别急着配到服务上去先用OpenSSL自带命令验一遍能省下后面一大截排查时间。查看证书内容openssl x509 -in server.crt -noout -text重点看输出里的三块Subject持有人、Validity有效期、X509v3 extensions扩展重点看有没有SAN。如果你刚加了SAN但输出的扩展里死活看不到说明-addext或-extfile没生效赶紧回第4节检查。验证签名关系用openssl verify# 验证自签名证书证书自己作为信任锚 openssl verify -CAfile server.crt server.crt # 验证私有CA签发的证书 openssl verify -CAfile ca.crt server.crt如果输出server.crt: OK就说明签名链和期限都没问题。如果输出带错误码最常见的两个是unable to get local issuer certificate找不到签发者。说明CA证书没给对或者证书链不完整。certificate has expired证书已过期。6.2 Windows下升级OpenSSL的正确姿势前面第2节挖的坑这里填上。Windows下升级OpenSSL最忌讳的就是装新版不卸旧版。这样PATH里可能同时出现两个openssl.exe一堆动态库互相干扰你永远不知道自己用的到底是哪个版本。我建议的升级流程先卸载旧版。控制面板里找到旧版OpenSSL卸载干净。清理PATH。打开系统环境变量把旧的C:\Program Files\OpenSSL-Win64\bin之类的目录删除。安装新版。如果允许用Full版只跑命令行就Light版。验证openssl version where openssl确认where openssl指向的路径和安装路径一致且全系统只有一个。但再强调一遍Windows上多个应用依赖不同OpenSSL版本几乎是无法避免的Git自带一套、Python自带一套、某些商业软件又捆绑一套。所以我的终极建议是没必要把系统级OpenSSL和项目级OpenSSL混为一谈。系统级工具链nginx、apache要什么版本就装什么版本临时做一些证书生成、签名验证的操作直接用Dockerdocker run --rm -it -v $PWD:/work -w /work alpine/openssl version用容器跑OpenSSL就不存在DLL冲突问题了生成的证书文件还能挂载到宿主机上。我自己现在80%的证书操作都在Docker或者WSL里完成Windows原生那套就服务于必须依赖系统PATH的工具链。6.3 证书不生效时的排查顺序如果你配好了HTTPS但用浏览器或客户端访问就是报错我建议按下面的顺序排查第一步看有效期和系统时间。openssl x509 -in server.crt -noout -dates看证书起止时间再用date命令看服务器当前时间。服务器时间漂移导致HTTPS证书失效是我遇到过的最多次的乌龙尤其是某些虚拟机模板时间常年不准。第二步看SAN和域名匹配。用开发工具直接看证书详情确认证书里的SAN包含你URL里用到的域名或IP。第三步看证书链。Nginx等服务端是否发送了完整链用openssl s_client直接模拟客户端握手openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts输出的Verification: OK或Verification error: ...能直接告诉你是链断了还是不信任根。如果显示self-signed certificate in certificate chain说明客户端没有导入你的根CA或者服务端下发顺序不对。第四步看客户端是否加载了根证书。Windows下用命令行certutil -user -addstore Root ca.crt可以把根CA导入当前用户的受信任根证书颁发机构存储。macOS用钥匙串访问导入。curl加--cacert ca.crt测试。这一步搞定其余问题多半迎刃而解。6.4 一个省心的收尾习惯最后分享一个我个人的操作习惯。我在本地新建了一个certs工作目录里面放了一个生成根CA的脚本和一个按域名签发的脚本。新来个内部系统要HTTPS我只要改一下域名参数跑一下脚本然后把证书文件发给对应负责人就行。签发一张证书的全程不超过十秒。#!/usr/bin/env bash # 用根CA给新域名签发证书的脚本片段 DOMAIN$1 openssl req -new -newkey rsa:2048 -nodes \ -keyout $DOMAIN.key -out $DOMAIN.csr \ -subj /CCN/STBeijing/LBeijing/OMy Company/CN$DOMAIN openssl x509 -req -in $DOMAIN.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out $DOMAIN.crt -days 825 \ -extfile (echo subjectAltNameDNS:$DOMAIN,IP:127.0.0.1)把根CA的私钥ca.key保存在一个只有你自己能访问的目录里权限设到最小它就是整个信任体系的命根子。丢了、泄露了你维护的整个内网信任体系就得全部重建。自签名证书这事说穿了就三步生成私钥、填写身份信息、完成签名。难的是理解信任模型、处理各种环境的格式和信任链问题。把上面这些环节都打通之后你再遇到TLS相关需求就不是到处搜索命令拼凑而是顺手就能搭出一套安全、可控、能解释原理的证书体系了。
返回列表