由于不同软件要求不一样,目前常见的自签证书有三种方式。由于不同方法的特殊性要重点强调证书的附加签名部分,例如/C=CN/ST=beijing/L=haidian/O=devA/OU=devB/CN=devC其中C是国家代码、ST是省或州、L是城市或地区、O是组织类信息、OU是部门类信息、CN是具体的一个主体比如人,这些附加消息中,CN 不能瞎写,它的含义是当前证书的持有主体名称,或者通俗的讲,就是服务端出示证书时,客户端会查看证书中附加域名列表 hosts 内是否有服务端的域名或 IP ,如果有则验证通过,此时服务端在客户端的认证中名字就是 CN 配置的值。在双向认证的体系下,CN 的这种归一化不同域名但持有同一证书的服务端为同一名字的作用,也被用来作为构建用户识别的依据 ,如果各位读者会 K8s,就应该明白这一点
第一种:Java 专用 JKS
JKS 是 Java 自身的内置专用格式,使用时通常是成对出现,一个是密钥库,用在服务端对客户端出示证书,一个是受信库,内部是密钥库的公钥部分,用在客户端校验服务端。要特别说明本质上受信库就是密钥库,只不过里面只存放了一条公钥数据而已
凡是使用这种证书机制的应用,特点很明显,都是只对自身的 TLS 加密和身份认证,不涉及复杂的 CA 链路校验,且涉及到集群时,必然内置且通常默认服务通信只验证受信公钥是否正确,这种方式的好处是不用再困扰如何在每个节点生成 JKS 文件的同时还要处理大量的受信公钥,直接一份文件共用即可,而且后期换证书文件的成本也小,比如 Kafka 、ES 就是这样,不过 ES 用的不是 JKS,只不过它也默认使用这种认证校验方式,但这种方式坏处在于不检查 CN/SAN ,可能会应发一些使用上的不便和问题
不过多数架构应用已经直接支持了更标准的通用格式,比如 Java 的 Springboot 在 2.7 版本开始就不再需要从 jks 转 p12 而是可以直接使用通用标准格式下 CA 签发的 pem 文本格式证书
使用时 JKS 生成方式如下
bash
# 在需要证书的服务端节点上运行命令,CN 要写为当前节点的域名,jks下它同时负责 hosts 功能
# -keystore 结果路径 -alias 密钥库中默认有一对密钥对,设置它的别名 -validity 密钥对有效期,默认90天 -genkey 生成新的密钥对
# -keyalg RSA 使用的算法 -keysize 密钥长度 -storepass 密钥库的密码 -keypass 密钥对中私钥的密码
# -dname 部分除了 CN 要写当前节点的 DNS外,其他的自定义
keytool -keystore kafka-keystore.jks -alias localhost -validity 365 -genkey -keyalg RSA -keysize 2048 -dname "CN=localhost, OU=dev,O=dev,L=dev,ST=dev,C=CN" -storepass 123456 -keypass 123456
# 导出密钥对的公钥部分,保存在broker.cer文件中
keytool -export -alias localhost -keystore kafka-keystore.jks -file broker.cer -storepass 123456
# 用一个导入动作,把公钥回写到一个会自动创建的密钥库中,就成了受信库,后期使用中 broker.cer 这个文件就没用了
keytool -import -alias localhost -keystore client.truststore.jks -file broker.cer -storepass 123456 -noprompt
要重点注意,千万不要把 JKS 密钥库中生成的公钥当成 CA 根证书去给别人签发!JKS 密钥库,它从名字上也不难理解,可以放多个密钥对,只不过在给某个服务使用的场景下,密钥库中只有一对密钥,且我们说的 CA 证书,本质就是 CA 密钥对中的公钥,但如果你把 JKS 里的公钥当根证书,会发现 OpenSSL 也能正常解析它的 X.509 格式,可它本质上只是一张"自签名的叶子证书",没有 CA 的签名能力。它的正确用法就只是直接用于 Kafka、Presto 这类服务自身的 TLS 加密和身份认证
对于密钥库来讲,Java 提供了转换为 p12 二进制格式 的方法,但现在很少这样用了,需要 p12 时通常要的都是 X.509 标准CA签发的证书,而不是JKS。p12 本质上是一种二进制打包格式,它里面有完整的证书关系链
bash
# 将 JKS 格式的密钥库转换成 p12 格式
keytool -importkeystore -srckeystore kafka-keystore.jks -destkeystore kafka-keystore.p12 -deststoretype pkcs12
除了上面提到的只校验受信公钥是否正确外,还有些任然使用 JKS 格式的应用,它们会强制要求走标准 CA 签名那一套逻辑,比如 Hadoop 配置 DataNode 加密传输时用的就是这种方式,遇到这种情况如果你可以用自签 CA 的方式,操作方式如下
bash
# 用一个节点做ca,生成一个自签的X.509 CA根证书(cert)和根证书的私钥(key),运行后要求输入ca私钥的密码,按需设置,这里设置123456。
# 根证书本质是个单独存放的公钥,这点和 JKS 受信库类似,相当于这个CA机构的营业执照副本,私钥部分单独存放相当于CA机构的公章
# 单独存在的私钥本质上是一个很长的字符串,用来验证证书的真伪以及作为给别人签名的核心工具
# -keyout 根证书的私钥存放路径 -out 根证书存放路径
# -days 当前ca根证书有效天数,它会影响所签证书请求的有效期超过ca有效期时,实际有效期终止到ca根证书过期的时间。但ca根证书切换成本很大,因此一般都是3-10年。而私钥本身永不会过期
# -newkey 是密钥的算法类型和密钥长度,可以不指定,默认是 rsa 2048位
# -subj 这个是根证书签名中的附加信息,就是这个证书CA机构在那里、那个城市、ca自身的名称等等这些自定义信息
openssl req -new -x509 -keyout /root/hdfs_ca_key -out /root/hdfs_ca_cert -days 3650 -newkey rsa:2048 -subj '/C=CN/ST=beijing/L=haidian/O=devA/OU=devB/CN=devC'
# 随后把根证书和私钥同步给其他节点。
# 注意,由于现在是生成自签证书,发布到各节点为了使用方便,正常的CA机构是你把要签名的文件(密钥库请求文件)发给人家给你签名后,返回属于你的证书和CA根证书
# 相当于你找CA机构签合同,CA会返回给你两个东西,一个是CA根证书,相当于营业执照复印了一份给你,另一个就是来源于你的签名请求但已经盖好公章的合同
scp /root/hdfs_ca_key /root/hdfs_ca_cert node2:/root/
随后每个所需节点用拿到的CA根证书,做两件事,一是生成受信库,二是生成包含完整证书链路的密钥库。注意前面说了图方便下面的操作在各节点本地做,正常是CA机构做使用者等结果就行
bash
# 把根证书导入到一个会自动生成的密钥库中。第一件事情就完成了
# keytool 命令是JavaJDK提供的工具 -keystore 指定结果密钥库路径如果存在则向其中追加导入的内容,不存在则会先生成一个密钥库文件
# -alias 别名导入操作按需自定义即可 -import 是导入用的动作参数 -file 是指定导入的内容,这里指定ca证书 -storepass 生成的密钥库它的密码,导入操作不需要被导入密钥库的任何密码,只要实际发生使用的时候才需要
# -trustcacerts 标记为ca证书 -noprompt 告诉程序使用非交互方式执行,并默认受信导入的内容
keytool -keystore /root/truststore -alias ca -import -file /root/hdfs_ca_cert -storepass 123456 -trustcacerts -noprompt
# 下面做第二件事,每个需要证书的节点生成一个自己的密钥库
# -alias 是生成的密钥它的别名 -keyalg 是采用的算法 -genkey是生成新的密钥对
# -dname这部分是密钥的签名,和上面生成根证书一样是附加信息,但是CN一定要是当前执行命令节点它的DNS或者IP,且在后期签名证书的扩展hosts范围内,通常用DNS,因为浏览器访问时一般很少用IP访问服务,而不是服务器/etc/hostname文件中的那个主机名,这一点非常容易混淆,原因通常是大多公司部署服务喜欢用DNS做主机名,其他的可以自定义
# -storepass 是生成的密钥库密码
# -keypass 是生成的密钥库中密钥的密码,这一步可能会疑惑生成CA根证书时为什么只输入了一次密码,而这里要分开输入,根本原因是工具不一样,openssl核心是用来控制证书的,只需要一个统一的密码就够了,而keytool 是用来管理密钥库的,一个密钥库可以有多个密钥或者证书且来自于不同的地方,所以秘钥的密码会不一样
# 要注意命令中不需要指定过期时间,因为公钥本来就不是加密的,而且默认的公钥,会在后期导入CA根证书的时候被覆盖
keytool -keystore /root/keystore -alias node1 -genkey -keyalg RSA -keysize 2048 -dname "CN=node1, OU=dev,O=dev,L=dev,ST=dev,C=CN" -storepass 123456 -keypass 123456
# 用自己生成的密钥库中密钥对的私钥,生成一个签名请求文件
# -certreq 生成签名请求的动作参数 -alias 别名一定要是密钥库中目标密钥对的别名 -file 是结果存储路径
keytool -certreq -keystore /root/keystore -alias node1 -file /root/cert -storepass 123456 -keypass 123456
# 用ca根证书和ca私钥,给当前节点签名,运行后输入ca私钥的密码
# 如果你用脚本生成,可以带 -passin pass:123456 这个配置非交互的输入根证书密码
# -req 签名的动作参数 -CA ca的根证书文件 -CAkey ca的私钥文件 -in 请求签名文件 -out 签名结果文件存放路径 -days 签名有效天数,一定要在ca根证书的剩余有效期内,如果超出,在不同工具下会出现报错等各种情况
# -CAcreateserial -CAserial /root/hdfs_ca_cert.srl 这两个参数是维护ca签名的序列号,存储在一个文件中,其实就是记录签了多少个,你可以不带,但标准操作方式有要求
openssl x509 -req -CA /root/hdfs_ca_cert -CAkey /root/hdfs_ca_key -in /root/cert -out /root/cert_signed -days 3650 -CAcreateserial -CAserial /root/hdfs_ca_cert.srl
# 将ca根证书 和 签名结果,导入到节点自己生成的密钥库中,第二件事就完成了
keytool -keystore /root/keystore -alias ca -import -file /root/hdfs_ca_cert -storepass 123456 -noprompt
# 导入证书的时候别名要和上面生成的密钥对别名一样,它本质上是覆盖证书
keytool -keystore /root/keystore -alias node1 -import -file /root/cert_signed -storepass 123456 -noprompt
可以关注一下结果文件的格式
bash
# 作为CA机构的主机,用openssl生成的x509根证书是一个PEM格式文件,ca私钥是一个Base64编码的ASCII文本文件
[root@node1 opt]# file hdfs_ca_cert
hdfs_ca_cert: PEM certificate
[root@node1 opt]# file hdfs_ca_key
hdfs_ca_key: ASCII text
#用 keytool 生成的密钥库和受信库文件,是用来给Java应用使用,一种较老的Java特有格式,jks格式文件
[root@node1 opt]# file keystore
keystore: Java KeyStore
[root@node1 opt]# file truststore
truststore: Java KeyStore
#签名请求文件是一个CSR格式过程类的文件
[root@node1 opt]# file cert
cert: RFC1421 Security Certificate Signing Request, ASCII text, with CRLF, LF line terminators
#签名结果文件,和CA根证书一样是PEM格式文件
[root@node1 opt]# file cert_signed
cert_signed: PEM certificate
# ca签名时生成的序列号是一个Base64编码的ASCII文本文件
[root@node1 opt]# file hdfs_ca_cert.srl
hdfs_ca_cert.srl: ASCII text
在其他IT领域,安卓体系用的证书是BKS格式,其他领域大多用的是PKCS12标准通用格式
第二种:Openssl PEM 格式
Openssl 生成 PEM 这种方式属于纯手动,通常用来给某个单一服务来生成证书,比如 Docker 自建仓库用的 Harbor 为例
bash
# 用一个节点做ca,生成一个自签的X.509 CA根证书(cert)和根证书的私钥(key),运行后要求输入根证书的密码,按需设置,这里设置123456。
# 根证书本质是个单独存放的公钥,这点和 JKS 受信库类似,相当于这个CA机构的营业执照副本,私钥部分单独存放相当于CA机构的公章
# 单独存在的私钥本质上是一个很长的字符串,用来验证证书的真伪以及作为给别人签名的核心工具
# -keyout 根证书的私钥存放路径 -out 根证书存放路径
# -days 当前ca根证书有效天数,它会影响所签证书请求的有效期超过ca有效期时,实际有效期终止到ca根证书过期的时间。但ca根证书切换成本很大,因此一般都是3-10年。而私钥本身永不会过期
# -newkey 是密钥的算法类型和密钥长度,可以不指定,默认是 rsa 2048位
# -subj 这个是根证书签名中的附加信息,就是这个证书CA机构在那里、那个城市、ca自身的名称等等这些自定义信息
openssl req -new -x509 -keyout /root/public_ca_key -out /root/public_ca_cert -days 3650 -newkey rsa:2048 -subj '/C=CN/ST=beijing/L=haidian/O=devA/OU=devB/CN=devC'
# 在需要证书的节点上生成一个用来标识当前节点的私钥
openssl genrsa -out harbor.key 2048
# 注意:由于 Harbor 比较特殊它没有输入密码的能力,因此上面的命令没有设置私钥密码保护,在其他场景下需要指定密码
openssl genrsa -traditional -aes256 -out 私钥路径 2048
# 生成签名请求文件
openssl req -new -key harbor.key -out harbor.csr -subj "/C=CN/ST=Beijing/L=Beijing/O=YourOrg/CN=node1"
# 同样的如果私钥有密码需要指定
openssl req -new -key 私钥路径 -passin pass:密码 -out 结果路径 -subj "/C=CN/ST=Beijing/L=Beijing/O=YourOrg/CN=node1"
# 由于 harbor 使用了go语言1.15版本以上的语法库,所以在证书签名时需要在扩展文件中写一些必要的东西。注意:CN必须填你访问Harbor的域名或IP
cat > harbor-san.ext << EOF
[v3_req]
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
# 关键:把你所有的访问方式都列在这里
DNS.1 = node1
DNS.2 = localhost
IP.1 = 127.0.0.1
IP.2 = 192.168.239.56
# 如果你还有域名,也加上
# DNS.3 = harbor.example.com
EOF
# CA用根证书和CA私钥签名,运行后输入根证书密码
openssl x509 -req -in harbor.csr -CA /root/public_ca_cert -CAkey /root/public_ca_key -CAcreateserial -out harbor.crt -days 365 -extfile harbor-san.ext
# 查看证书内容
openssl x509 -in harbor.crt -text -noout
# 验证证书链,输出OK
openssl verify -CAfile /root/public_ca_cert harbor.crt
#查看harbor需要的两个结果物类型
[root@node1 opt]# file harbor.crt
harbor.crt: PEM certificate
[root@node1 opt]# file harbor.key
harbor.key: PEM RSA private key
或者无密码保护时是个普通的文本文件
harbor.key: ASCII text
上面的签名扩展文件是用来签发 barbor 需要的叶子证书,如果你签发证书是为了做中间 CA ,你要用的文件如下
config
[req]
#默认配置,对于签名时指定了私钥文件的情况通常无效
default_bits=2048
distinguished_name=dn
req_extensions=v3_req
prompt=no
[dn]
# 生成私钥时的扩展字段不写也可以
C=CN
ST=Beijing
L=Beijing
O=YourOrg
CN=node1
[v3_req]
# 核心配置,和前面叶子证书区别在于使用 ca 扩展配置
basicConstraints=critical,CA:TRUE
keyUsage=critical,keyCertSign,cRLSign
extendedKeyUsage=serverAuth
subjectAltName=@alt_names
[alt_names]
# 对于一个ca来讲这部分没用,然如果你的中间ca也要用来配置给服务做正常的证书用就要配上
DNS.1=node1
DNS.2=localhost
IP.1=127.0.0.1
IP.2=192.168.239.56
第三种:cfssl PEM 格式
工作中大部分情况证书多数是自签,要构造的证书可能会很多,且需要物化留痕,所以上面 openssl 生成key、生成签名请求、扩展文件、签名这一套下来会很繁琐,因此对于 PEM 这种通用格式的证书公司内部通常使用 cfssl 工具批量构造
cfssl 需要单独下载
bash
# 1. 下载 cfssl 核心工具 (版本 R1.2) 主命令行工具,用于证书签发操作
curl -L -o /usr/local/bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
# 2. 下载 cfssljson 工具 处理JSON格式输出,将证书/密钥/CSR等写入文件
curl -L -o /usr/local/bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
# 3. 下载 cfssl-certinfo 工具 证书信息查询工具(使用频率较低)
curl -L -o /usr/local/bin/cfssl-certinfo https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64
# 4. 赋予执行权限
chmod +x /usr/local/bin/cfssl*
# 验证命令是否正常
检查所在路径:which cfssl
查看版本:cfssl version
生成 CA 根证书和私钥。"CN" 是当前 CA 的一个名字,可以自定义。"key" 是生成证书用的算法和证书长度。"names"是这个 CA 的其他附加信息,C是国家代码、ST是省或州、L是城市或地区、O是组织类信息、OU是部门类信息、CN是具体的一个主体比如人,合理设置即可,后期不可随意更改。"ca" 是定义了ca证书的有效期
bash
vi ca-csr.json
{
"CN": "ClusterCA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "Beijing",
"L": "Beijing",
"O": "cluster",
"OU": "System"
}
],
"ca": {
"expiry": "876000h"
}
}
# 生成命令
cfssl gencert -initca ca-csr.json | cfssljson -bare ca
准备 CA 签名用的证书配置文件,它定义了一些 CA 签名时的策略。"default" 部分是签名时默认采用的时效,设置在 ca 根证书有效期内或相等即可。"profiles"是签名策略预设,里面包含细化的策略类型 通常 "signing" 、"key encipherment" 这两个是固定要写的。"server auth" 意思是https协议通讯时 服务端 需要向客户端证明身份,用的最多。其他地方可能还有一个 "client auth" 就是反过来客户端也要像服务端证明自己的身份,一般在 k8s 这种双向认证的场景下使用
bash
vi ca-config.json
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"server": {
"usages": [
"signing",
"key encipherment",
"server auth"
],
"expiry": "87600h"
}
}
}
}
准备前后端共用的证书。"CN" 在这里是一个域名,指该证书使用者的域名。"hosts"部分是扩展域名,因为 CN 只有一个,你要是不给其他人用就够用,但现在前后端要共用一个整数,就要让证书有多个可被使用的域名 配置时要注意 "*.example.com" 中的 * 只能代表一级域名,如果你需要多级,需要手动向下写,比如 *.api.example.com , 且 * 只能在最左侧 "key" 、"names" 这两个部分和 ca 证书同理。"localhost"、"127.0.0.1" 这两个不要删除,固定的,不然本地访问会提示不安全
bash
vi server.json
{
"CN": "example.com",
"hosts": [
"localhost",
"127.0.0.1",
"example.com",
"*.example.com"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "Beijing",
"L": "Beijing",
"O": "hive-auth-platform",
"OU": "all"
}
]
}
# 生成证书
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server server.json | cfssljson -bare server
最终在你的 CA 节点上应当存在如下的文件
bash
[root@node1 ca]# ll
total 36
-rw-r--r-- 1 root root 288 May 24 15:27 ca-config.json
-rw-r--r-- 1 root root 250 May 24 15:15 ca-csr.json
-rw------- 1 root root 1675 May 24 15:16 ca-key.pem
-rw-r--r-- 1 root root 1005 May 24 15:16 ca.csr
-rw-r--r-- 1 root root 1371 May 24 15:16 ca.pem
-rw------- 1 root root 1679 May 24 15:28 server-key.pem
-rw-r--r-- 1 root root 1094 May 24 15:28 server.csr
-rw-r--r-- 1 root root 278 May 24 15:26 server.json
-rw-r--r-- 1 root root 1468 May 24 15:28 server.pem
关于密码,cfssl 生成的证书中,私钥是没有加密的,这点和 openssl 不同,cfssl 是为了解决自动化部署等场景下批量构造且过程留痕,因此它的产出在使用时需要依靠linux文件系统权限 600 来保证。但如果需要,你可以将 cfssl 产出的私钥使用 openssl加密
bash
[root@corenode ~]# openssl rsa -in ca-key.pem -aes256 -out ca-key-encrypted.pem
writing RSA key
Enter pass phrase:
Verifying - Enter pass phrase:
[root@corenode ~]# openssl rsa -in ca-key.pem -check -noout
RSA key ok
[root@corenode ~]# openssl rsa -in ca-key-encrypted.pem -check -noout
Enter pass phrase for ca-key-encrypted.pem:
RSA key ok
PEM 格式证书转 P12
bash
#-export:表示将 PEM 格式导出为 PKCS#12 格式。
#-in:指定输入的 PEM 证书文件。
#-inkey:指定输入的私钥文件。
#-out:指定输出的 .p12 文件名称。
openssl pkcs12 -export -in cert.pem -inkey key.pem -out cert.p12