搞WEB开发这些年,我一直把自建CA证书体系当作内部基础设施的标配。不管是内网的管理系统、测试环境的前后端联调、还是给抓包工具导入证书来解HTTPS流量,只要涉及到WEB通信,一套自己能掌控的CA证书体系能省掉大量“证书不可信”“连接被拦截”的破事。这几个月我在做基于Flask的个人日常记账WEB系统,从开发联调到部署访问,全程被HTTPS证书的问题教做人了:自签证书浏览器不认识,单域名临时证书换个端口就失效,抓包工具不加载证书根本看不到真实请求。后来痛定思痛,把整套WEB CA证书搭建与管理从头到尾捋了一遍,现在把完整方案和踩坑记录整理出来,相信对还停留在“临时自签证书”阶段的开发者,以及负责内网服务部署的同学都会有帮助。
这套方案的核心思路其实很简单:自己动手搭一个内部CA体系,用这个CA来签发和管理WEB站点证书。它解决的不只是“证书能不能用”的问题,更解决了“证书从哪来、如何统一签发、如何保证浏览器信任、如何吊销”这几个内网场景最头疼的环节。适合范围内网运维、WEB安全测试人员、前后端开发、以及所有需要在业务中启用HTTPS但暂时不打算对接公网证书机构的团队。全文以OpenSSL为主要工具展开,命令和配置都是可直接复现的。
1. 内容整体设计与思路拆解
1.1 为什么需要自建CA,而不是继续用临时自签证书
很多开发者最开始接触的都是“自签证书”,也就是用一句话生成一个证书然后直接丢给Nginx或Tomcat用。这种做法的体验大家都懂:浏览器弹出红色警告、抓包工具不认、移动端调试更难受。但更核心的问题是,自签证书只能解决“这有一个证书”的问题,解决不了“证书体系”的问题。
打个比方,自签证书就像一个没有备案的名片,你自己觉得是真的,但合作方完全不认;而自建CA相当于你成立了一家“小型认证机构”,这家机构的信任凭证你提前装进了所有需要校验的设备里。之后你用这个CA签发的每一张证书,都会被相关设备和工具自动信任,浏览器、抓包工具、手机、后端服务,一次信任全局生效。
从结构上看,一个标准CA体系包含根证书、中间证书、服务端证书三层。根证书是整个信任链的锚点,它的私钥需要最高保护级别;中间证书用来替根证书实际执行签发,减少根私钥的暴露面;服务端证书才是真正挂到WEB服务上给访客用的那张。理解这个链路,是后续所有配置的基础。
1.2 这套方案能覆盖哪些典型场景
结合我实际使用中接触到的需求,自建CA能覆盖的场景比想象中广得多:
- 开发环境启HTTPS。前端的Webpack DevServer、后端的Flask/Django/Node服务,要测试登录态、Cookie安全属性,没有HTTPS根本无法模拟真实环境。
- 内网业务系统。资产管理系统、研发项目管理系统、测试平台的WEB页面,通过内部CA签发的证书,员工访问时不会再有告警。
- 抓包和调试。抓包工具要解密HTTPS流量,必须安装并信任一个CA证书,把自建CA的根证书装进抓包工具和系统信任区,所有域名流量都能看明文。
- 客户端证书认证。某些高安全性的WEB系统,可以要求客户端必须携带由我们CA签发的证书才能访问,这就是mTLS双向认证。
由于涉及WEB项目部署,我还会以自己做的Flask记账系统为例,把证书签发和部署的落地过程完整带一遍。
1.3 整体架构和职责边界
整个体系分为三步走:第一步,搭建CA基础设施,包括根CA和中间CA的密钥与证书生成;第二步,利用中间CA为WEB站点签发服务端证书,并在服务器上完成部署;第三步,把根证书安装到浏览器、系统或抓包工具的信任区,同时把中间证书和服务端证书正确拼接,确保证书链完整不被浏览器报错。
这里特别要强调一个容易被忽视的职责边界:CA证书体系负责的是“信任关系的建立”,而WEB服务器(Nginx/Tomcat/IIS/Flask内置服务器)负责的是“证书的实际启用”,两者缺一不可。很多时候证书配置不上,不一定是证书本身有问题,而是服务器端的SSL配置参数与证书类型不匹配,比如Nginx的ssl_certificate必须要填完整的证书链而不是只填站点证书,这一点在后文会专门展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 工具选型:为什么选OpenSSL
搭建和管理CA证书体系,工具链我在实际项目中用了好几套,包括OpenSSL、CFSSL和基于Web面板的PKI管理工具。最终长期稳定使用的还是OpenSSL,原因有三:
一是跨平台覆盖完整。OpenSSL在Linux、macOS、Windows(通过Git Bash或WSL)都有成熟的发行版本,命令语法完全一致,脚本化程度高。
二是没有额外的架构依赖。CFSSL是Go语言写的,功能非常强大,尤其适合自动化批量签发场景,但对于大多数中小团队,反而因为需要维护一个额外的服务和API而增加复杂度。OpenSSL是纯命令行工具,不需要启动常驻服务,也没有对外端口,安全性上天然更具优势。
三是最低层级可控性最强。OpenSSL的配置文件是明文形式,密钥长度、有效期、签名算法、扩展属性(比如SAN、Key Usage)全部可以直接修改核对,出了问题排查起来路径清晰。
注意:搭建CA体系的机器建议是独立的内部服务器,或者至少是一台权限管理到位的个人工作机。因为根CA的私钥一旦泄露,整个体系就失去了信任基础,所有由它签发的证书都需要作废重签。
2.2 CA体系的密钥与参数设计逻辑
密钥长度方面,我统一采用RSA 4096位。虽然RSA 2048在安全性上目前也够用,但作为CA体系的根密钥,多出的位数换来的是更长的抗量子计算缓冲时间,而且CA签发的频率不高,性能损耗可以忽略。
有效期策略是整个体系里最容易出错的地方。我踩过的坑是把根证书有效期设成10年,然后中间证书也设成10年,导致现在想调整策略时非常被动。合理的搭配是:根证书10年,中间证书5年,服务端证书最长不超过398天(浏览器对超过这个期限的证书会报错,这是行业共识),日常开发环境我一般直接签90天。这样即使服务端证书出问题,也不会牵连整个CA体系。
摘要算法统一使用SHA-256。SHA-1在现如今的浏览器和抓包工具里基本已经被视为不安全了,凡是遇到证书告警提示“签名算法太弱”,十有八九是用了SHA-1。
2.3 目录结构与配置文件规划
搭建CA体系前,先把目录结构规划清楚,后面所有操作都按这个落盘,方便管理和备份:
code复制pki/
├── root/
│ ├── private/
│ ├── certs/
│ ├── newcerts/
│ └── crl/
├── intermediate/
│ ├── private/
│ ├── certs/
│ ├── newcerts/
│ └── crl/
根CA和中间CA分开目录,是为了从根本上隔离权限。日常签发服务端证书时只要进入中间CA目录操作,根CA私钥可以离线存放在U盘或保险柜里,不需要天天暴露在系统中。
OpenSSL的配置文件是整个操作的核心,我分为root.cnf和intermediate.cnf两份。核心配置段如下:
code复制[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /data/pki/intermediate
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/certs/intermediate.crt
private_key = $dir/private/intermediate.key
serial = $dir/serial
crlnumber = $dir/crlnumber
crl = $dir/crl/intermediate.crl
default_md = sha256
default_days = 365
policy = policy_loose
unique_subject = no
copy_extensions = copy
[ policy_loose ]
commonName = supplied
emailAddress = optional
organizationName = optional
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name
stateOrProvinceName_default = Beijing
localityName = Locality Name
localityName_default = Beijing
organizationName = Organization Name
organizationName_default = Internal Tech
commonName = Common Name
commonName_max = 64
[ server_cert ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
DNS.2 = example.internal
IP.1 = 127.0.0.1
这里有两个关键点需要解释一下。copy_extensions = copy 的作用是让CA在签发证书时,把CSR文件里的扩展属性(比如SAN)一起复制到最终签发的证书里。我早期没配这一行,导致CSR里写好的域名IP全部丢失,证书打开只有Common Name,浏览器依旧报不安全。另一个是unique_subject = no,允许相同Common Name重复签发证书,否则同一个域名第二次签发时会报“subject already exists”,非常耽误事。
3. 实操过程与核心环节实现
3.1 根CA的创建与保护策略
搭建完整流程时,我习惯写成一个脚本,每次从零初始化时有据可查。根CA的初始化分四个命令完成:
bash复制mkdir -p /data/pki/root/{private,certs,newcerts,crl}
echo 1000 > /data/pki/root/serial
touch /data/pki/root/index.txt
openssl genrsa -aes256 -out /data/pki/root/private/root.key 4096
openssl req -new -x509 -sha256 -days 3650 \
-config /data/pki/root.cnf \
-key /data/pki/root/private/root.key \
-out /data/pki/root/certs/root.crt
第一条genrsa生成根私钥时,建议加上-aes256参数设置密码保护,这样每次使用根私钥签中间CA时都需要输入密码,相当于多了一把锁。如果不需要交互式输入,可以后续用openssl rsa -in root.key -out root.key去掉密码,但我建议保留,根密钥永不离线且受密码保护是底线。
生成的root.crt就是整个体系的信任锚点,它需要被导入到浏览器、操作系统和抓包工具的信任区,后面会讲到具体哪里导入。生成之后立即用openssl x509 -in root.crt -text -noout检查一下有效期和签名算法,确认无误后再进入下一步。
3.2 中间CA的生成与授权签署
中间CA的私钥和证书请求流程,与根CA的生成相似,但多了“让根CA来签中间CA”这一层授权动作:
bash复制mkdir -p /data/pki/intermediate/{private,certs,newcerts,crl}
echo 1000 > /data/pki/intermediate/serial
touch /data/pki/intermediate/index.txt
echo 1000 > /data/pki/intermediate/crlnumber
openssl genrsa -aes256 -out /data/pki/intermediate/private/intermediate.key 4096
openssl req -new -sha256 \
-config /data/pki/intermediate.cnf \
-key /data/pki/intermediate/private/intermediate.key \
-out /data/pki/intermediate/certs/intermediate.csr
然后把这步生成的intermediate.csr拿给根CA签署。签署时要带上-extensions v3_ca参数表示这个证书是CA证书,而不是服务端证书:
bash复制openssl ca -batch \
-config /data/pki/root.cnf \
-extensions v3_ca \
-days 1825 \
-notext \
-in /data/pki/intermediate/certs/intermediate.csr \
-out /data/pki/intermediate/certs/intermediate.crt
这里v3_ca扩展定义了证书的Basic Constraints = CA:TRUE,表明它具备签发下级证书的权限。注意,中间CA证书的keyUsage里应当包含keyCertSign和cRLSign,否则后面签发服务端证书时会报“invalid key usage”。
签发完成后,把根证书和中间证书串联成一个证书链文件,后续部署WEB服务会用到:
bash复制cat /data/pki/intermediate/certs/intermediate.crt \
/data/pki/root/certs/root.crt \
> /data/pki/intermediate/certs/ca-chain.crt
这个ca-chain.crt文件非常重要。Nginx配置里ssl_certificate指的就是它,而不是单独的站点证书。很多人把站点证书填进去后浏览器报“证书链不完整”,就是因为漏了这一步。
3.3 为WEB站点签发服务端证书
签发服务端证书我封装了一个Shell脚本,把CSR生成和实际签发合并起来,避免每次手动输入一长串命令时出错。核心逻辑如下:
bash复制#!/bin/bash
DOMAIN=$1
DAYS=${2:-397}
KEY_DIR=/data/pki/intermediate/private
CSR_DIR=/data/pki/intermediate/certs
CRT_DIR=/data/pki/intermediate/certs
openssl genrsa -out $KEY_DIR/${DOMAIN}.key 2048
openssl req -new -sha256 \
-key $KEY_DIR/${DOMAIN}.key \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Internal Tech/CN=${DOMAIN}" \
-reqexts SAN \
-config <(cat /data/pki/intermediate.cnf \
<(printf "\n[SAN]\nsubjectAltName=DNS:${DOMAIN},DNS:*.${DOMAIN},IP:127.0.0.1,IP:192.168.1.100")) \
-out $CSR_DIR/${DOMAIN}.csr
openssl ca -batch \
-config /data/pki/intermediate.cnf \
-extensions server_cert \
-days $DAYS \
-notext \
-in $CSR_DIR/${DOMAIN}.csr \
-out $CRT_DIR/${DOMAIN}.crt
脚本里最值得关注的是SAN扩展的写法。现在浏览器检验证书是否匹配域名时,只看subjectAltName,而不看Common Name。如果证书里没有SAN,即便Common Name是对的,Chrome也会提示“NET::ERR_CERT_COMMON_NAME_INVALID”。利用-reqexts SAN配合进程替换传配置文件,可以把域名、泛域名和IP一次性写入CSR,后续CA签名时由于copy_extensions = copy的配置会自动带入。
我用实际项目验证过,签发出来的证书用OpenSSL检查时,SAN字段会显示完整内容:
bash复制openssl x509 -in /data/pki/intermediate/certs/ledger.internal.crt -text -noout | grep -A 4 "Subject Alternative Name"
输出类似:
code复制X509v3 Subject Alternative Name:
DNS:ledger.internal, DNS:*.ledger.internal, IP Address:127.0.0.1, IP Address:192.168.1.100
3.4 在WEB服务器上落地部署HTTPS
证书签发完成后,就进入部署环节。我分别用过Nginx、Tomcat和Flask内置服务器,下面分享各自的配置要点。
Nginx是最常见的部署目标,配置非常简练:
nginx复制server {
listen 443 ssl;
server_name ledger.internal;
ssl_certificate /data/pki/intermediate/certs/ledger.internal.crt;
ssl_certificate_key /data/pki/intermediate/private/ledger.internal.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
}
注意,Nginx的ssl_certificate这里我填的是站点证书文件,并不包含证书链。如果你前面生成了ca-chain.crt,推荐直接填ca-chain.crt,这样即使站点证书没有和链合并,也可以保证浏览器能正确识别。两种方式都可行,但合并链文件管理起来更省心。
Tomcat的场景,先在server.xml里打开HTTPS连接器:
xml复制<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="/data/pki/tomcat/ledger.p12"
certificateKeystorePassword="changeit"
certificateKeystoreType="PKCS12" />
</SSLHostConfig>
</Connector>
Tomcat不支持直接加载PEM格式的私钥,需要先把key和证书转换成PKCS12格式:
bash复制openssl pkcs12 -export \
-inkey /data/pki/intermediate/private/ledger.internal.key \
-in /data/pki/intermediate/certs/ledger.internal.crt \
-certfile /data/pki/intermediate/certs/ca-chain.crt \
-out /data/pki/tomcat/ledger.p12
转换的时候会要求设置PKCS12的导出密码,这个密码就是certificateKeystorePassword要填的。PKCS12的好处是私钥和证书链封装在一个文件里,Tomcat读取时不容易出现链断裂的问题。
Flask这类开发框架自带的服务器是单进程的,通常不直接在生产环境用HTTPS,但在开发联调阶段,可以通过ssl_context参数快速启用证书:
python复制from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello HTTPS"
if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=8443,
ssl_context=(
"/data/pki/intermediate/certs/ledger.internal.crt",
"/data/pki/intermediate/private/ledger.internal.key",
),
debug=True
)
3.5 信任配置:把根证书装进需要信任它的地方
服务端全部启用了HTTPS以后,剩下的关键步骤是让客户端信任我们的根证书。
Linux服务器端,通过更新系统信任库就可以实现。以CentOS/RHEL系为例:
bash复制cp /data/pki/root/certs/root.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust extract
Ubuntu/Debian系是:
bash复制cp /data/pki/root/certs/root.crt /usr/local/share/ca-certificates/
update-ca-certificates
Windows和macOS的信任库导入,图形界面操作即可。但有一个细节值得讲:如果只是给某个用户装浏览器,可以只导入到“当前用户”的证书存储区;但如果是给系统级服务(比如Java后端调用外部HTTPS接口)信任,就必须导入到“本地计算机”的“受信任的根证书颁发机构”存储区。
以我的Flask记账系统为例,实际部署时我同时在笔记本的Chrome里导入了根证书,以及在手机(遇到了小米CA证书安装不上问题)上做了特殊处理。这个问题我在后面的常见问题部分会单独讲。
Java环境下的信任配置同样常见。很多WEB系统本身是Java技术栈,连接外部接口时如果对方是自建CA签的证书,不导入信任库就会报PKIX path building failed。导入方法:
bash复制keytool -import -trustcacerts -alias internal-root \
-file /data/pki/root/certs/root.crt \
-keystore cacerts -storepass changeit
3.6 实战案例:基于Flask的个人日常记账WEB系统
结合我手上的实际项目,说说这套证书是怎么落到那个记账系统上的。“基于Flask的个人日常记账WEB系统的设计与实现”是我最近在推进的一个内部小项目,功能包括日常收支录入、分类统计、月度报表导出、WEB页面PDF打印。核心诉求是:既能自己随时随地记账,也能在多人小团队内共享账单数据。由于涉及个人隐私和财务数据,我在设计之初就决定通信链路必须全程加密,但公网证书对这类纯内网系统来说成本高且续期麻烦,于是自建CA成了刚需。
系统架构分两层:Nginx负责对外HTTPS接收请求、转发静态资源;后端的Flask进程监听127.0.0.1的8000端口处理业务逻辑。基于前面的步骤,我签发了一张ledger.internal的证书,SAN里配了ledger.internal、*.ledger.internal以及多个内网IP。Nginx配置启动后,通过HTTP访问时明确403,只有HTTPS才能正常进入系统,Cookie设置了Secure和HttpOnly,登录态传输规规矩矩地加密。
这个项目带给我的实际收益非常直观。之前用IP加端口硬访问Shared服务,每次都有“不安全连接”的提示;部署HTTPS后,从浏览器地址栏输入https://ledger.internal到打开首页,全程没有任何告警,更重要的是,用抓包工具查看请求时能按照证书解密到完整流量,开发调试效率直接上一个台阶。
4. 常见问题与排查技巧实录
4.1 浏览器提示证书不受信任
这是部署完成后遇到最多的一个问题。现象是Chrome或Edge打开站点时直接红色警告“NET::ERR_CERT_AUTHORITY_INVALID”。排查顺序我建议这样走:
先在浏览器里看看证书详情,检查证书链中“根证书”那一层是不是我们自己的Internal Tech Root CA。如果不是,说明根证书没有成功安装,或安装的是中间证书而不是根证书。再检查服务端返回的证书链是否完整,Nginx可以这样验证:
bash复制echo | openssl s_client -connect ledger.internal:443 -servername ledger.internal 2>/dev/null | grep -E "CONNECTION ESTABLISHED|BEGIN CERTIFICATE"
或者直接在线看链。还有一个非常冤枉的坑:浏览器缓存了旧证书。我重签证书后忘记了清理浏览器缓存,折腾了大半天,以为是配置问题,结果清一下缓存全好了。
4.2 证书被提示“不完整证书链”
浏览器报“Certificate chain incomplete”时,大概率是服务器只发了站点证书而没发中间证书。这个问题的根源在于服务器配置。Nginx解决方法是把站点证书和中间证书合并后填入ssl_certificate:
bash复制cat ledger.internal.crt intermediate.crt root.crt > ledger_fullchain.crt
把ledger_fullchain.crt填进ssl_certificate,ledger.internal.key填进ssl_certificate_key,然后重载配置。
如果用了Tomcat且转换PKCS12文件时没带-certfile,也会导致链不完整,重新执行一遍带-certfile参数的pkcs12转换即可。
4.3 主机名和IP不匹配的问题
浏览器报“NET::ERR_CERT_COMMON_NAME_INVALID”或“ERR_SSL_VERSION_OR_CIPHER_MISMATCH”,很多情况下是因为站点证书里面的SAN没有覆盖访问时的域名或IP。比如我早期给Flask记账系统签发证书时没有写入内网IP地址,用域名访问正常,但同事用IP访问就报错。
解决办法是重新签发一张SAN里同时包含域名和IP的证书。如果你的访问地址是https://192.168.1.100:8443,那么SAN里必须有IP:192.168.1.100;如果是https://ledger.internal,SAN里必须有DNS:ledger.internal。多个域名和IP用逗号分隔写在一行,签完再仔细检查一遍SAN输出。
4.4 安卓手机安装CA证书失败
小米手机和一些国产安卓系统在“认证机构”页面导入CA证书时会提示安装失败。这个问题我在手机调试Flask记账系统时遇到过,起初以为是PEM格式有误,后来确认是系统限制用户必须在锁屏密码之外再设置一个“凭据存储密码”才能安装CA证书。
解决路径:设置 – 安全 – 加密与凭据 – 设置屏幕锁定方式为“密码”(必须是非生物识别的密码),然后在“安装CA证书”里选择根证书文件,输入凭据存储密码完成安装。另外,Android 7开始,App默认不信任用户添加的CA证书,需要修改App的网络安全配置networkSecurityConfig,或者在开发阶段使用debug-overrides信任用户证书。抓包工具最常见的就是卡在这一环,所以一定要连同客户端一起设置好。
4.5 证书已过期或吊销状态未知
证书过期是最直白的错误,浏览器直接阻止访问。由于自建CA签发的证书有效期完全由我们自己控制,建议在日历或脚本里建立续期提醒。更专业的做法是启用CRL或OCSP,让客户端在访问时可以校验证书吊销状态。给中间CA生成CRL的命令:
bash复制openssl ca -config /data/pki/intermediate.cnf \
-gencrl -out /data/pki/intermediate/crl/intermediate.crl
然后在Nginx可以配置ssl_crl:
nginx复制ssl_crl /data/pki/intermediate/crl/intermediate.crl;
虽然内网环境用的不多,但如果是安全审计或等保评估,看到CRL配置会很加分。
4.6 抓包工具看不到HTTPS明文
这是WEB安全测试和前后端调试最常遇到的问题。抓包工具需要先安装根证书并开启SSL解密,才会把HTTPS流量解密成明文。具体到每个工具,操作路径不同,但核心都是“导入根证书+启用SSL代理”。如果你用的是通用抓包工具,需要确保导入的是root.crt,而不是中间证书。此外,抓包时如果系统代理没有正确开启,同样看不到流量,这两个点要一起排查。
4.7 常见问题排查速查表
| 现象 | 常见原因 | 解决方向 |
|---|---|---|
| 浏览器ERR_CERT_AUTHORITY_INVALID | 根证书未导入客户端信任区 | 下载root.crt导入系统或浏览器信任库 |
| 浏览器ERR_CERT_COMMON_NAME_INVALID | 证书SAN未覆盖访问域名/IP | 重新签发包含正确SAN的证书 |
| 浏览器ERR_CERT_DATE_INVALID | 证书过期或客户端系统时间异常 | 检查日期、重签证书 |
| SSL握手失败TLS alert | 证书链不完整或私钥类型不匹配 | 合并chain文件填到ssl_certificate |
| Android导入CA证书失败 | 未设置凭据存储密码 | 设置锁屏密码,重新安装CA证书 |
| Java报PKIX path building failed | 根证书未导入Java信任库 | keytool导入root.crt到cacerts |
5. 证书生命周期管理与自动化续期经验
5.1 证书签发台账与命名规范
证书数量一多,靠记忆管理必然出错。我在/data/pki下维护了一个certs-index.md文件,每条证书记录四列:域名、签发日期、到期日期、用途。每次签发后手动追加一行,定期扫描。不想手动记的,可以通过脚本把OpenSSL的index.txt解析出来,转成TSV表格。
命名规范也建议提前定好。我的习惯是:私钥、CSR、证书文件名全部用域名全称命名,比如ledger.internal.key、ledger.internal.csr、ledger.internal.crt。不要用web1-crt.pem这种不明所以的名字,不然半年后自己都不知道这个证书对应哪个域名。
5.2 私钥泄露的应急响应
私钥泄露意味着攻击者可以用它劫持HTTPS通信。一旦发现私钥泄露,需要立即吊销对应证书,并生成新密钥。吊销命令:
bash复制openssl ca -config /data/pki/intermediate.cnf \
-revoke /data/pki/intermediate/certs/ledger.internal.crt
然后重新生成CRL并颁发新证书。如果泄露的是根私钥,情况就更严重,整个CA体系都要推倒重建,所有使用该根证书信任的设备需要更新。这也是为什么我坚持把根私钥离线保存,不参与日常签发。
5.3 基于脚本的证书批量签发
对于WEB服务多、域名多的团队,手动跑openssl ca确实低效。我封装了上面的issue_cert.sh之后,还挂了cron定时任务检查哪些证书快过期了:
bash复制0 0 * * * find /data/pki/intermediate/certs -name "*.crt" -mtime +300 -exec echo "expiring soon: {}" \;
再配合简单的邮件通知或企业微信群通知,基本不会出现证书到期才发现的情况。
5.4 证书自动续期与负载均衡场景的注意点
如果你日常用的是Nginx或Apache,配合acme.sh或openssl的脚本完全可以做到自动续期,然后nginx -s reload热更新证书,整个过程可以做到不中断服务。但如果你的业务跑在负载均衡器后面,证书可能同时挂载在多个节点上,修改证书后必须一个节点一个节点地检查更新时间,否则可能出现同一域名不同节点返回不同证书的诡异问题。
我实际过程中遇到的坑是:某次续期后只更新了后端WEB服务器,负载均衡的证书还是老的那张,导致用户访问时概率性出现证书告警,排查了两个小时才发现是负载均衡的证书缓存没有刷新。这个经验写出来,希望后来者少走弯路。
6. 我的体会与后续扩展建议
搞完这套WEB CA证书体系,最大的体会是:证书这个东西,单独看是运维的事,但在WEB开发链路里,从前端调试到后端部署,再到安全测试,每一个环节都会被它卡住。与其每次卡住临时生成个证书凑合,不如一次性把CA基础设施搭起来,这才是一劳永逸的正解。
另外分享一个小技巧:现在的OpenSSL版本已经普遍支持openssl req -addext参数,可以直接在生成CSR时附加SAN,不用再写临时配置文件了:
bash复制openssl req -new -sha256 \
-key ledger.internal.key \
-subj "/C=CN/ST=Beijing/O=Internal Tech/CN=ledger.internal" \
-addext "subjectAltName=DNS:ledger.internal,DNS:*.ledger.internal,IP:192.168.1.100" \
-out ledger.internal.csr
这样格式更简洁,也非常适合写进自动化脚本里。我后来把所有签发脚本都改成了这种写法,明显少碰了很多配置文件的坑。
如果后续有更大的团队规模或更复杂的证书需求,可以在这个基础上扩展ACME协议,配合小型CA软件搭建全自动化的内部证书签发平台;也可以为移动端专属业务开通mTLS双向认证。总之,这套自建CA的基础打牢了,往上扩展任何WEB安全能力都是很顺理成章的。
