ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

自建CA与OpenSSL实操:轻松签发多域名IP的SSL证书

自建CA与OpenSSL实操:轻松签发多域名IP的SSL证书 最近在做内部网络改造需要给网关、管理面板和几个后端服务配TLS加密顺手翻了下之前搭的内部CA。整套流程走下来发现不少细节值得记录尤其是多域名和IP证书签发的部分网上的资料要么停留在单域名要么含糊带过今天干脆把完整的实操过程整理出来。先说清楚这套东西适用什么场景你在自己公司内网、实验室或者个人服务器集群里需要给多个服务签发统一的SSL证书并且希望浏览器、终端、代码库都能信任这些证书。因为你不可能为内网IP或自定义域名去申请公有CA的证书——成本和流程都不现实更别说纯内网环境压根没有域名。自己搭一个内部CA是最稳妥的方案签发、吊销、续期全部自己掌控。需要的基础条件也很简单一台Linux机器我用的Ubuntu 22.04其他发行版也差别不大安装好OpenSSL 3.0以上版本即可。如果你是Windows环境用WSL或者Git Bash也能跑通大部分命令。1. 为什么需要自建CA以及OpenSSL在其中的位置很多刚接触证书的人会混淆两个概念CA本身是一个权威机构证书是这个机构签发的电子凭证。公有CA和自建CA的区别本质上就是权威的信任半径不一样——浏览器默认信任DigiCert、Lets Encrypt但不会信任你自己生成的根证书而你可以在自己的设备、服务器、代码库里手动安装并信任自己的根证书这样你的内网服务在用户端看起来和正规HTTPS证书没有区别。OpenSSL在这个体系里扮演的角色是工具链它既能生成根证书根CA也能用根CA去签发子证书。核心流程就三条链路生成根CA的私钥和自签名证书用根CA签发的叶子证书这个叶子证书就是写进Nginx、Tomcat、代码客户端的那张证书扩展字段里写入Subject Alternative NameSAN这张叶子证书才能做到一张证书同时覆盖多个域名和IP我在最开始使用的时候犯过一个典型错误直接用openssl req -x509一条命令生成一张自签名证书然后塞给Nginx用。这种做法证书能生效但所有客户端都会提示证书不受信任因为没有人对你的自签名证书做背书。正确做法是先建CA再签叶子证书这样客户端只需要信任根证书一次后续所有由这个CA签发的证书全部自动受信任而且吊销某个证书的能力只有根CA才具备。另一个需要提前规划的事是私钥的安全。根CA的私钥一旦泄露整个信任链崩溃所以我在生产环境里会把根CA私钥放到一台离线机器上权限设成600并且设置密码保护。签发证书的机器可以是另一台相互隔离。2. 搭建根CA目录结构、openssl.cnf配置与初始化OpenSSL工作目录的组织方式有讲究虽然可以用完整路径一把梭但后续证书多起来之后没有目录结构的CA是灾难。推荐用OpenSSL官方的目录风格散列索引、序列号文件、新证书存放路径各司其职。首先建立工作目录mkdir -p ~/myca/{certs,crl,newcerts,private} cd ~/myca touch index.txt echo 1000 serial echo 1000 crlnumber目录说明certs/存放签发的证书crl/证书吊销列表newcerts/OpenSSL每次签发证书后默认放置证书副本private/私钥目录权限必须收紧index.txt数据库文件OpenSSL用它记录所有已签发的证书状态serial下一次签发的证书序列号接下来是决定整个CA行为的主配置文件。网上有些教程用默认的/etc/ssl/openssl.cnf直接签但我不建议这么做——公网CA的模板策略和你内部CA的诉求差别很大尤其policy和x509_extensions这两段直接决定了你能不能签发多域名/IP证书。我自己维护一份独立配置不污染系统文件也方便切换参数。# ~/myca/openssl.cnf [ ca ] default_ca CA_default [ CA_default ] dir /home/user/myca certs $dir/certs crl_dir $dir/crl new_certs_dir $dir/newcerts database $dir/index.txt serial $dir/serial RANDFILE $dir/private/.rand private_key $dir/private/ca.key.pem certificate $dir/certs/ca.cert.pem crlnumber $dir/crlnumber crl $dir/crl/ca.crl.pem crl_extensions crl_ext default_md sha256 name_opt ca_default cert_opt ca_default default_days 3650 preserve no policy policy_loose [ policy_loose ] countryName optional stateOrProvinceName optional organizationName optional organizationalUnitName optional commonName supplied emailAddress optional [ req ] default_bits 2048 distinguished_name req_distinguished_name string_mask utf8only x509_extensions v3_ca [ req_distinguished_name ] countryName Country Name (2 letter code) stateOrProvinceName State or Province Name localityName Locality Name organizationName Organization Name organizationalUnitName Organizational Unit Name commonName Common Name emailAddress Email Address [ v3_ca ] subjectKeyIdentifier hash authorityKeyIdentifier keyid:always,issuer basicConstraints critical, CA:true keyUsage critical, digitalSignature, cRLSign, keyCertSign [ v3_intermediate_ca ] subjectKeyIdentifier hash authorityKeyIdentifier keyid:always,issuer basicConstraints critical, CA:true, pathlen:0 keyUsage critical, digitalSignature, cRLSign, keyCertSign [ usr_cert ] basicConstraints CA:FALSE nsCertType server nsComment OpenSSL Generated Server Certificate subjectKeyIdentifier hash authorityKeyIdentifier keyid,issuer keyUsage critical, digitalSignature, keyEncipherment extendedKeyUsage serverAuth [ crl_ext ] authorityKeyIdentifierkeyid:always [ ocsp ] basicConstraints CA:FALSE subjectKeyIdentifier hash authorityKeyIdentifier keyid,issuer keyUsage critical, digitalSignature extendedKeyUsage OCSPSigning这里几个关键点补充一下policy_loose的每个字段都是optional只有commonName必填。这意味着你签发证书时不需要填公司全称、省市区之类的内容适合轻量内部环境。如果你走严格政策就改成match或supplied要求申请信息必须与根CA一致——对内部用没太大必要。default_days我设置成3650天根CA是长期信任锚点十年不算长。叶子证书下面会单独使用更短的有效期。[ v3_ca ]里的basicConstraints critical, CA:true是根证书的关键标记标记了它就是CA。网上有些自签名证书生成命令没带这个扩展导致证书即使安装信任也无法用来签发下级证书。配置好之后初始化根CA的私钥和证书cd ~/myca openssl genrsa -aes256 -out private/ca.key.pem 4096 chmod 400 private/ca.key.pem openssl req -config openssl.cnf -key private/ca.key.pem \ -new -x509 -days 3650 -sha256 -extensions v3_ca \ -subj /CCN/OMyLab Internal CA/CNMyLab Root CA \ -out certs/ca.cert.pem根私钥加上了-aes256每次使用私钥的时候都会提示输入密码。有人嫌麻烦不带密码保护我的态度是签发根CA的机器本就应该高度受控密码成本可以忽略泄露的代价却承受不起。验证根证书是否正确生成了CA标记openssl x509 -in certs/ca.cert.pem -noout -text输出里应当能看到Basic Constraints: critical CA:TRUE Key Usage: critical Digital Signature, Certificate Sign, CRL Sign看到这两行根CA就绪。3. 签发多域名/IP证书SAN配置与实操命令这是整个流程里最实用的部分。很多网上的教程会教你在req命令里加-subj /CNexample.com然后直接签发。这样签出来的证书现在会被Chrome和Firefox直接拦掉——因为从2020年9月起Chrome 58以上版本不再信任仅依赖commonName的证书必须看SAN字段。SANSubject Alternative Name才是现代浏览器和TLS库真正校验的主机名/IP列表。换句话说SAN字段就是证书的白名单签发页把你的域名和IP全部列进去一张证书就能覆盖多个站点。3.1 生成带SAN的CSR证书签署请求首先生成叶子证书的私钥我用RSA 2048位内部服务不是高敏感场景这个强度足够。cd ~/myca openssl genrsa -out certs/server.key.pem 2048 chmod 400 certs/server.key.pem接着创建CSR的配置文件。这里有两个做法把SAN写进CSR或者直接在CA签发时通过扩展配置文件指定SAN。我更推荐后者——如果你用CSR带SAN那么处理CSR的人可以是不同的人就能决定证书里包含哪些域名这在安全上是个缺口。内部环境没那么多攻防对抗但养成好习惯不亏。所以我会写一个专门的扩展文件在签发时指定# ~/myca/exts/server_ext.cnf subjectAltName DNS:portal.lab.internal, DNS:api.lab.internal, IP:192.168.1.10, IP:192.168.1.11再看具体的签发命令openssl req -new -key certs/server.key.pem \ -subj /CNportal.lab.internal \ -out certs/server.csr.pem openssl ca -config openssl.cnf \ -extfile ext/server_ext.cnf \ -days 825 -notext -md sha256 \ -in certs/server.csr.pem \ -out certs/server.cert.pem这条命令做完之后会输出一张有效期825天的多SAN证书。825天这个数字有讲究从苹果和Google的政策来看2020年9月以后新签发的SSL证书最长有效期不能超过398天差不多13个月。825天看起来超了但对于内部CA没有遵守这个强制约束的硬性要求只是提醒一下——如果你的客户端里有较新的安全策略检查比如某些企业终端管理软件用的证书有效期越长越容易被标黄或拒绝。保持内部证书有效期不超过两年是合理折衷。如果你希望完全合规把-days调成398或者更短即可。3.2 SAN文件里的书写规则SAN字段的书写有一个容易踩坑的细节域名写DNS:前缀且不要带协议前缀。DNS:https://portal.lab.internal是错的IP地址写IP:前缀直接写IPv4或IPv6地址例如IP:192.168.1.10。不要写成DNS:192.168.1.10否则严格校验的客户端不认多个条目用英文逗号分隔中间可以加空格但配置解析时会把空格去掉所以写不写空格无影响我这次签发的证书里同时包含两个域名和两个IP其中portal.lab.internal是Nginx的入口api.lab.internal是后端API域名两个IP分别是网关和数据库管理端。一个非常常见的现实需求是在同一台物理机上有多个服务监听不同端口且都用IP直接访问如果每个端口都摆一张不同证书客户端每次都要重新校验稍有疏漏就告警。一张SAN证书全部覆盖省心很多。3.3 查看签发后证书的SAN信息签发完成之后必须检查证书内容确认SAN真正写进去了openssl x509 -in certs/server.cert.pem -noout -text重点看两段X509v3 Extended Key Usage: TLS Web Server Authentication X509v3 Subject Alternative Name: DNS:portal.lab.internal, DNS:api.lab.internal, IP Address:192.168.1.10, IP Address:192.168.1.11第一段说明这确实是服务器证书可被用于TLS握手第二段就是我们的SAN列表。看到这两行证书签发阶段的活就干完了。4. 叶子证书签发过程中的常见坑与排查链路证书签发不复杂但出问题时定位起来非常烦人。我在搭建过程中踩了几个坑逐一还原排查过程给你的路径做个参考。4.1 name is not recognized 或 unsupported certificate purpose现象执行openssl ca报类似错误有时甚至是The resultant key usage is incompatible。原因CSR里的CN字段和CA策略不匹配最常见的是CN包含了下划线、中文或者过长字符。OpenSSL的string_mask默认值在不同发行版上不一样有的很严格。还有一部分情况是CSR中带了某些扩展字段而openssl ca默认策略里没允许这些扩展导致校验失败。排查链路用openssl req -in certs/server.csr.pem -noout -text检查CSR内容看CN和扩展字段看openssl.cnf中policy_loose对cn的要求commonName supplied表示必须提供如果你CSR里没写CN必然失败确认CSR中不带subjectAltName如果你用的是CA扩展方案因为openssl ca读取CSR时会校验已有扩展是否与配置冲突解法明确-subj /CNportal.lab.internal不要在CN或域名里用下划线然后用-extfile的方式在签发阶段注入SAN不要放在CSR里绕开绝大多数扩展冲突。4.2 每次签发证书大小不一致且index.txt里的记录有问题现象签发两次证书后index.txt里可能多了奇怪的R标记revoked或V标记valid错乱。原因index.txt是CA的核心数据库。如果你手动复制、删除过newcerts目录下的文件或者同时并行跑多个签发命令OpenSSL对数据库的更新会互相覆盖。排查链路查看index.txt的字段格式第一个字符是状态V/R/E后面是过期时间、撤销时间、序列号、路径确认你用同一个serial文件时没有并发执行签发操作如果发现状态错乱备份后用openssl ca -updatedb重建索引该命令会扫描证书目录并更新状态经验CA签发操作必须串行化。我在自动化脚本里加了文件锁flock避免cron或CI任务同时触发签发。这是内网CA最容易被忽略的稳定性问题。4.3 浏览器提示无法将此证书链接到受信任的根颁发机构现象所有服务和域名都配置了证书但浏览器访问仍然大红锁。原因根证书没有安装到客户端或系统的信任库。你签发的叶子证书并不包含根证书内容客户端必须通过预置的信任锚来验证证书链。如果你只安装了叶子证书链就不完整浏览器自然找不到信任锚。另一个常见原因是你没有把中间CA证书一起下发但这个场景下我们没有中间CA这个不适用。排查链路先用openssl verify -CAfile certs/ca.cert.pem certs/server.cert.pem在服务端自检证书链正常应输出OK确认部署的服务端证书是否包含了完整证书链。如果是自签CA单层结构服务端只发叶子证书即可客户端本地必须有根证书如果你在证书文件里拼接了根证书反而会让部分客户端困惑浏览器访问前先把根CA导入系统信任库macOS钥匙串、Windows证书管理、Linux的/usr/local/share/ca-certificates目录经验国内社区里最常见的拆解方式是把根CA和叶子证书拼在一起放在Nginx的ssl_certificate里。这种方式对部分旧客户端有效但对现代浏览器来说不必要甚至不推荐。我现在的做法是Leaf-only证书下发到nginx根CA通过MDM/GPO/Ansible统一推送到所有终端的信任库。两层分离清晰可控。5. 在Nginx、Java/Python等环境里的实际落地配置证书签完最终要用到服务里。我分三个最常面对的环境给出落地方案。5.1 Nginx配置服务端配置的核心是ssl_certificate和ssl_certificate_trusted_certificate分开使用server { listen 443 ssl; server_name portal.lab.internal; ssl_certificate /etc/nginx/certs/server.cert.pem; ssl_certificate_key /etc/nginx/certs/server.key.pem; ssl_trusted_certificate /etc/nginx/certs/ca.cert.pem; }ssl_certificate填叶子证书ssl_trusted_certificate填根CA证书用于OCSP stapling和客户端证书链验证这样配合起来握手信息更完整。如果你有多个站点共用同一张多SAN证书不同server_name块可以重复使用同一份证书路径不需要为每个站点单独签一张。重载配置并验证nginx -t nginx -s reload用openssl命令行模拟客户端连接看证书链是否正常openssl s_client -connect portal.lab.internal:443 -servername portal.lab.internal -showcerts重点看输出里的Verification: OK前提是客户端侧已安装根证书。5.2 Java环境Spring Boot / TomcatJava的信任库和密钥库是分开的。如果应用作为服务端需要把叶子证书私钥导入PKCS12格式密钥库如果应用作为客户端例如调用别的HTTPS接口需要把根CA导入cacerts信任库。服务端证书导入openssl pkcs12 -export \ -out server.p12 \ -inkey certs/server.key.pem \ -in certs/server.cert.pem \ -passout pass:changeit keytool -importkeystore \ -srckeystore server.p12 -srcstoretype PKCS12 -srcstorepass changeit \ -destkeystore server.jks -deststoretype PKCS12 -deststorepass changeit然后Spring Boot配置server.ssl.key-storeclasspath:server.jks server.ssl.key-store-passwordchangeit server.ssl.key-store-typePKCS12 server.ssl.key-alias1客户端信任根证书keytool -import -trustcacerts \ -alias mylabroot \ -file certs/ca.cert.pem \ -keystore $JAVA_HOME/lib/security/cacerts \ -storepass changeit这里有个Java特有坑Java 8和Java 11默认信任库文件路径不同且JDK升级会重置cacerts所以把根CA导入系统级信任库不能一劳永逸要纳入升级检查流程。我在自动化脚本里会额外做一步cacerts里是否存在mylabroot别名的断言不存在就报警。5.3 Python/Node.js等开发环境Python的requests库在内部网络访问HTTPS时默认不会使用你安装到系统信任库的根证书它用的是certifi包自带的证书包。要信任内部CA最省事的方式是给requests显式指定CAimport requests resp requests.get(https://portal.lab.internal, verify/path/to/ca.cert.pem)不要用verifyFalse那是裸奔数据加密形同虚设还会被安全扫描器直接标记。Node.js也有类似问题用环境变量NODE_EXTRA_CA_CERTS指向根证书就无需改代码export NODE_EXTRA_CA_CERTS/path/to/ca.cert.pem node app.js5.4 Docker容器内的证书信任容器内部默认不继承宿主机的信任库。如果你在容器里跑的服务要访问内部HTTPS接口构建镜像时把根CA打进去COPY ca.cert.pem /usr/local/share/ca-certificates/mylab-ca.crt RUN update-ca-certificates然后容器内所有走系统信任库的客户端curl、wget、Python未指定verify时都能直接信任。6. 证书的吊销与续期机制证书终归有生命周期吊销和续期是CA运维里绕不开的能力。内部CA虽然规模小但这两件事需要提前想清楚。6.1 吊销证书当某台机器下线、私钥疑似泄露时要立即吊销对应证书。做法openssl ca -config openssl.cnf -revoke certs/server.cert.pem执行后index.txt里那条记录状态会从V变成R。吊销之后还要生成并分发CRL证书吊销列表客户端才可能知道这张证书不可信openssl ca -config openssl.cnf -gencrl -out crl/ca.crl.pemCRL需要定期刷新并分发到你的Nginx或其他依赖方。如果你觉得CRL维护太麻烦可以用OCSP在线证书状态协议替代但内部环境用CRL足够查询量不大、实现也简单。6.2 续期与签发新证书叶子证书到期前直接重新走一遍签发流程生成新的CSR和证书替换到服务端即可。不需要动根CA。如果你签发的证书在多台机器上使用续期时要注意平滑替换先签发新证书在Nginx里reload验证无告警再替换其它机器避免所有服务同时中断。根CA本身十年到期前也要续期但那时候通常意味着整套信任体系大升级建议预留充足时间因为要把新根证书推送到所有信任方并且在新旧根重叠期保持双CA并行。7. 签名过程中的扩展用途代码签名/Docker镜像签名证书不止能用于TLS服务器。用自建CA签发的代码签名证书可以在内部软件分发时校验完整性。做法是生成一把独立的代码签名子CA或者直接用根CA签发一张extendedKeyUsage codeSigning的证书。对内部场景我更推荐按用途拆分证书而不是共用一个服务器证书。签发代码签名证书时扩展配置改成[ code_sign ] basicConstraints CA:FALSE keyUsage critical, digitalSignature extendedKeyUsage codeSigning然后用openssl smime -sign或者osslsigncodeWindows PE签名对产物签名。这样即便代码签名证书泄露吊销它也不会影响TLS服务两者信任边界清晰。这类证书同样受SAN策略影响吗实际上代码签名证书不太依赖SAN但现代工具链如osslsigncode会读取并展示SAN。无伤大雅填一把也行。8. 实用脚本一分钟签发多域名/IP证书日常使用中最影响效率的就是每次敲一长串命令。我把整个流程封装成一个脚本放进PATH里基本做到“输入域名和IP证书到手”。#!/usr/bin/env bash set -euo pipefail CA_DIR$HOME/myca EXTS$CA_DIR/exts OUT$CA_DIR/certs CN${1:-portal.lab.internal} DAYS${DAYS:-825} if [ -z ${DNS_LIST:-} ] [ -z ${IP_LIST:-} ]; then echo Need DNS_LIST or IP_LIST env, e.g. DNS_LISTa.example.com,b.example.com IP_LIST192.168.1.10 exit 1 fi SAN OLD_IFS$IFS; IFS, if [ -n ${DNS_LIST:-} ]; then for d in $DNS_LIST; do SAN${SAN}DNS:$d,; done fi if [ -n ${IP_LIST:-} ]; then for ip in $IP_LIST; do SAN${SAN}IP:$ip,; done fi IFS$OLD_IFS SAN${SAN%,} echo subjectAltName ${SAN} $EXTS/tmp_san.cnf openssl genrsa -out $OUT/$CN.key.pem 2048 2/dev/null chmod 400 $OUT/$CN.key.pem openssl req -new -key $OUT/$CN.key.pem \ -subj /CN${CN} -out $OUT/$CN.csr.pem openssl ca -config $CA_DIR/openssl.cnf \ -extfile $EXTS/tmp_san.cnf \ -days $DAYS -notext -md sha256 \ -in $OUT/$CN.csr.pem \ -out $OUT/$CN.cert.pem openssl x509 -in $OUT/$CN.cert.pem -noout -text | grep -A1 Subject Alternative Name rm $EXTS/tmp_san.cnf echo Done: $OUT/$CN.cert.pem使用方式DNS_LISTportal.lab.internal,api.lab.internal IP_LIST192.168.1.10,192.168.1.11 ./issue_cert.sh portal.lab.internal脚本有几个细节值得解释需要输入CN但也支持SAN列表通过环境变量传默认没有SAN时它会拒绝执行防止签出一张只有CN的旧式证书每次生成独立私钥不用旧私钥复用签发后自动提取并打印SAN方便确认tmp_san.cnf用完即删避免历史SAN残留被下次使用实际用下来这套流程从生成私钥到拿到证书大约20秒左右手工操作时主要时间花在确认CN和SAN列表上。OpenSSL的命令行工具第一眼看上去参数又多又杂但只要理解整个信任链模型——根CA是信任锚叶子证书是服务凭证SAN是凭证的白名单——它的行为就非常好预测。建好CA之后后续的每次签发都是重复同一套脚本和思维路径复杂度集中在最初的一次搭建上。所以多花点时间把根CA目录结构、配置策略和自动化脚本一次性搭好后续可以省下大量重复劳动证书过期前也不会手忙脚乱。
返回列表