证书认证(Certificate Authentication)和密钥认证(Public Key Authentication)不是一回事。
密码登录和密钥登录,都有各自的缺点。
密码登录需要输入服务器密码,这非常麻烦,也不安全,存在被暴力破解的风险。
密钥登录需要服务器保存用户的公钥,也需要用户保存服务器公钥的指纹。这对于多用户、多服务器的大型机构很不方便,如果有员工离职,需要将他的公钥从每台服务器删除。
证书登录就是为了解决上面的缺点而设计的。它引入了一个证书签发机构(Certificate Authority,简称 CA),对信任的服务器签发服务器证书,对信任的用户签发用户证书。
登录时,用户和服务器不需要提前知道彼此的公钥,只需要交换各自的证书,验证是否可信即可。
证书登录的主要优点有两个:(1)用户和服务器不用交换公钥,这更容易管理,也具有更好的可扩展性。(2)证书可以设置到期时间,而公钥没有到期时间。针对不同的情况,可以设置有效期很短的证书,进一步提高安全性。
摘自阮一峰的网络日志
证书认证主要包含主机证书和用户证书两部分。主机证书用于认证主机,用户在登陆使用受信 CA 签发证书的新主机时不需要手动确认新指纹。用户证书用于认证用户,主机可以直接信任使用受信 CA 签发证书的用户密钥登陆,而不需要在主机端手动添加用户公钥(authorized_keys
)。这两个功能可以同时使用,也可以仅选用其中一种。
Plain Certificate Authentication
基础版的使用方法非常简单,主要分为生成CA&私钥、签发公钥证书、分发CA公钥三个步骤。
生成密钥
CA的本质是一对普通的密钥。
证书的本质是包含CA对用户的公钥的签名的公钥,所以在证书认证中,用户也是要创建密钥的。
虽然 CA 可以用同一对密码签发用户证书和服务器证书,但是出于安全性和灵活性,最好用不同的密钥分别签发。所以,CA 需要至少两对密钥,一对用于签发用户证书的密钥,即User CA
,另一对用来签发服务器证书,即Host CA
。
创建密钥很简单,使用ssh-keygen
程序即可。比如:
1 | # 生成ed25519的Host CA |
-t
指定密钥算法
-f
生成私钥的位置
-C
指定密钥的识别字符串,即公钥末尾的明文字符串,相当于注释,可以随便设置
因为ed25519
算法不需要指定密钥位数,所以没有-b
参数。
上面的命令会在当前目录生成一对密钥:host_ca
(私钥)和host_ca.pub
(公钥)。
SSH对密钥的类型并没有限制,CA和用户的密钥类型不需要一致,喜欢什么用什么即可,可以使用传统的RSA
,也可以使用更先进(?)的ed25519
。
1 | ssh-keygen -t ed25519 -f host_ca -C host_ca # 使用ed25519的Host CA |
一般情况下,SSH 服务端会自动生成主机密钥,一般为/etc/ssh/ssh_host_xxx_key
,可以先下载到本地后进行签名操作,也可以使用自己生成的密钥。在一些自动部署场景,比如 CI ,可以直接信任一个 Host CA 而不需要单独添加对应主机的指纹,减少不必要的操作。
签发公钥证书
需要分别签发主机和用户的证书。
主机证书
签发主机证书,需要使用Host CA。
1 | ssh-keygen -s host_ca -I host.example.com -h -n host.example.com -V +52w ssh_host_rsa_key.pub |
-s
指定 CA 的私钥。
-I
身份字符串,可以随便设置,相当于注释,方便区分证书,将来可以使用这个字符串撤销证书。非明文,包含在签名中
-h
指定该证书是服务器证书,而非用户证书。
-n
指定服务器的域名,表示证书仅对该域名有效。如果有多个域名,则使用逗号分隔。用户登录该域名服务器时,SSH 通过证书的这个值,分辨应该使用哪张证书发给用户,用来证明服务器的可信性。
-V +52w
指定证书的有效期,这里为52周(一年)。默认情况下,证书是永远有效的。建议使用该参数指定有效期,并且有效期最好短一点,最长不超过52周。
ssh_host_rsa_key.pub
服务器公钥。
上面的命令会生成主机证书ssh_host_rsa_key-cert.pub
(公钥名字加后缀-cert
)。
最后,为证书设置权限。
1 | chmod 600 ssh_host_rsa_key-cert.pub |
生成证书以后,可以使用下面的命令,查看证书的细节:
1 | ssh-keygen -L -f ssh_host_rsa_key-cert.pub |
用户证书
同理:
1 | ssh-keygen -s user_ca -I [email protected] -n user -V +1d user_key.pub |
-s
指定 CA 签发证书的密钥
-I
身份字符串,可以随便设置,相当于注释,方便区分证书,将来可以使用这个字符串撤销证书。
-n user
指定用户名,表示证书仅对该用户名有效。如果有多个用户名,使用逗号分隔。用户以该用户名登录服务器时,SSH 通过这个值,分辨应该使用哪张证书,证明自己的身份,发给服务器
-V +1d
指定证书的有效期,这里为1天,强制用户每天都申请一次证书,提高安全性。默认情况下,证书永久有效
user_key.pub
用户公钥
默认生成用户证书,所以这里不需要指定其他参数。
最后,为证书设置权限:
1 | chmod 600 user_key-cert.pub |
分发CA公钥
主机安装证书 & CA 公钥
将ssh_host_rsa_key-cert.pub
证书传回主机(默认为/etc/ssh
下),可以使用rsync
、scp
、sftp
等方法上传,喜欢哪种用哪种就行。
添加 SSH 配置。在/etc/ssh/sshd_config
中添加:
1 | HostCertificate /etc/ssh/ssh_host_xxx_key-cert.pub |
指定主机证书的位置。
为了让主机信任用户证书,需要将 User CA 的公钥user_ca.pub
也上传到主机中。
有两种方法添加主机对用户证书的信任,一种是全局方法,对主机中所有用户都有效,
添加一行至/etc/ssh/sshd_config
:
1 | TrustedUserCAKeys /etc/ssh/user_ca.pub |
还有一种是将证书添加到某个用户的authorized_key
中,只让该用户信任这个CA。
在该用户的.ssh/authorized_keys
中追加一行:
1 | @cert-authority principals="用户名" ... |
行尾添加user_ca.pub
的内容,大概是这个样子:
1 | @cert-authority principals="user" ssh-ed25519 AAAAC3Nza......HeJ |
最后重启sshd
服务。
1 | sudo systemctl restart sshd |
客户端安装证书 & CA 公钥
客户端安装用户证书很简单。将用户证书user_key-cert.pub
与用户的密钥user_key
保存在同一个位置即可。
为了让客户端信任服务器证书,必须将 Host CA 的公钥host_ca.pub
,添加到用户/etc/ssh/ssh_known_hosts
(全局)或者~/.ssh/known_hosts
文件(仅用户)。
具体做法是打开文件,追加一行,开头为@cert-authority *.example.com
,然后将host_ca.pub
的内容(即公钥)粘贴在后面,大概是这个样子:
1 | @cert-authority *.example.com ssh-rsa AAAAB3Nz...XNRM1EX2gQ== |
*.example.com
是域名的模式匹配,表示只要服务器符合该模式的域名,且证书的 CA 匹配该公钥就可以信任。如果没有域名限制,这里可以写成*
。如果有多个域名模式,可以使用逗号分隔;如果服务器没有域名,可以用主机名(比如host1,host2,host3
)或者 IP 地址(比如11.12.13.14,21.22.23.24
)。
然后就可以使用证书登录远程服务器了。
撤销证书
分为用户证书的撤销和服务器证书的撤销。
主机证书的撤销只需要在known_hosts
文件里,修改或删除对应的@cert-authority
。
用户证书的撤销,需要在主机中新建/etc/ssh/revoked_keys
文件,然后在sshd_config
添加:
1 | RevokedKeys /etc/ssh/revoked_keys |
revoked_keys
文件保存不再信任的用户公钥,由下面的命令生成。
1 | ssh-keygen -kf /etc/ssh/revoked_keys -z 1 ~/.ssh/user1_key.pub |
上面命令中,-z
参数用来指定用户公钥保存在revoked_keys
文件的哪一行,这里保存在第1行。
如果以后需要撤销其他的用户公钥,可以用下面的命令保存在第2行。
1 | ssh-keygen -ukf /etc/ssh/revoked_keys -z 2 ~/.ssh/user2_key.pub |
如果只写这些内容,那就和引用的博客重复,这篇博客就没有存在的意义了。
With PIV
在查阅ssh-keygen
的 manual 时,发现签名语句的用法中还有一个可选参数-D
ssh-keygen
-I
certificate_identity-s
ca_key [-hU
] [-D
pkcs11_provider] [-n
principals] [-O
option] [-V
validity_interval] [-z
serial_number] file …
pkcs11
即 PIV 智能卡使用的接口。是否可以用 PIV 卡代替 CA 的文件密钥呢,答案是可以的,不过在此之前,先学习一下如何使用 PKCS#11 密钥登陆SSH。
生成自签名证书
先生成 OpenSSL 格式的私钥。如果直接在 Yubikey 中生成可以使用yubico-piv-tool
工具:
1 | yubico-piv-tool -a generate -s 9a -A RSA2048 -o public.pem |
-a
指定操作指令
-s
指定 PIV 插槽。要进行 SSH 认证,只能选择9a
(Authentication)和9e
(Card Authentication)插槽。如果选择其他插槽,虽然可以读取到插槽的 SSH 公钥,但实际上并不能使用
-A
指定密钥算法,Yubikey 5 支持RSA1024
、RSA2048
、ECCP256
和ECCP384
-o
指定公钥导出路径。卡上生成不支持导出私钥,如果需要备份私钥,必须本机生成后导入
同时可以设置插槽的安全策略,可以添加--pin-policy
、--touch-policy
参数设置使用时是否需要触摸或是否需要输入 PIN 验证。
然后生成自签名证书。这一步也可以在卡上进行。
1 | yubico-piv-tool -a selfsign-certificate -s 9a -S "/CN=SSH key/" -i public.pem -o cert.pem |
-a
指定操作指令
-S
指定证书生成的信息
-s
指定使用的 PIV 插槽,即上一步使用的插槽
-i
指定公钥位置
-o
指定生成证书的位置
生成证书之后还需要导入:
1 | yubico-piv-tool -a import-certificate -s 9a -i cert.pem |
完成了。
本地生成需要使用 OpenSSL 的工具,执行:
1 | openssl genrsa -out ca-key.pem 2048 |
这条指令会生成一个RSA2048
格式的 OpenSSL 密钥。末尾的数字指定算法的位数,如果需要导入 Yubikey,只能使用1024
和2048
。
如果需要生成 ECC 证书,执行:
1 | openssl ecparam -name secp384r1 -genkey -noout -out ca-key.pem |
-name
指定使用的椭圆曲线算法,可以使用openssl ecparam -list_curves
查看可用曲线。Yubikey 5 仅支持secp384r1
和secp256k1
。
-genkey
执行生成密钥操作
-noout
关闭输出
-out
指定生成私钥路径
生成自签证书:
1 | openssl req -new -sha256 -x509 -set_serial 1 -days 1000000 -key ca-key.pem -out ca-crt.pem |
-sha256
指定证书的散列算法
-days
指定证书的到期时间,必须设置,默认为一个月。可以设置一个足够长的天数,比如36500
(100年)
-key
指定密钥私钥位置
-out
指定导出证书的位置
生成后可以查看证书的详细信息:
1 | openssl x509 -text < ca-crt.pem |
生成证书之后需要导入 Yubikey :
1 | yubico-piv-tool -a import-key -s 9a -i ca-key.pem # 导入私钥 |
完成了。
使用 PIV 登陆 SSH
先导出 PIV 对应的公钥:
1 | ssh-keygen -D /usr/lib/libykcs11.so -e |
-D
指定提供 PKCS#11 支持的库,这里使用的是 Yubico 公司的 Yubikey 专用库,如果是 Canokey 等其他智能卡,可以使用opensc
的开源库:
1 | ssh-keygen -D /usr/lib/opensc-pkcs11.so -e |
-e
执行导出公钥操作
应该能得到类似如下的输出:
1 | # libykcs11.so |
两个公钥来自两个插槽9a
和9e
,如果同时导入了两个插槽,请注意区分公钥的用途。
libykcs11.so
会多输出一个其他用途的RSA
公钥,该密钥并不能用于 SSH 认证。
这里的公钥可用于密钥登陆,比如 Github 登陆。
也可以使用ssh-add -L
查看公钥。
测试一下 Github 登陆:
1 | ssh [email protected] |
With X.509 PIV
现在你已经学会 PIV 登陆了,接下来让我们结合简单的 SSL 知识构建一条简易的证书链,来签发我们的 SSH 证书。
创建 Root CA
创建私钥:
1 | openssl ecparam -name secp384r1 -genkey -noout -out root.ca-key.pem |
一般来说,为了保证安全性,Root CA 的加密算法需要尽可能复杂,并且私钥需要离线存储,所以这里可以选择一些超出 Yubikey 可存储要求的加密算法,比如RSA3744
,不过其他的算法强度作学习用也足够了。
为 Root CA 生成自签证书:
1 | openssl req -new -sha256 -x509 -set_serial 1 -days 365000 -key root.ca-key.pem -out root.ca-crt.pem |
限制证书链长度:
1 | echo 01 > root.ca-crt.srl |
创建 Sub CA
生成私钥:
1 | openssl ecparam -name secp384r1 -genkey -noout -out users.ca-key.pem # User CA |
生成 Certificate Request(CSR):
1 | openssl req -sha256 -new -key users.ca-key.pem -nodes -out users.ca-csr.pem |
生成 CA 证书:
1 | openssl x509 -sha256 -CA root.ca-crt.pem -CAkey root.ca-key.pem -req -in users.ca-csr.pem -out users.ca-crt.pem |
你可能想看一下 Sub CA 证书的信息:
1 | openssl x509 -text < users.ca-crt.pem |
将 Sub CA 导入 Yubikey:
1 | yubico-piv-tool -a import-key -s 9a -i users.ca-key.pem |
这里只把 User CA 导入到9a
插槽,也可以将 Host CA 导入到另一个插槽中:
1 | yubico-piv-tool -a import-key -s 9e -i hosts.ca-key.pem |
签发主机 & 用户证书
首先需要导出卡中 User CA 和 Host CA 的公钥:
1 | ssh-keygen -D /usr/lib/opensc-pkcs11.so -e |
将输出的公钥分别保存为 User CA 公钥user.CA.pub
和 Host CA 公钥host.CA.pub
。
生成用户和主机密钥。
1 | ssh-keygen -t ed25519 -f id_ed25519 |
签发主机&用户证书:
1 | ssh-keygen -s user.CA.pub -D /usr/lib/opensc-pkcs11.so -I User_test id_ed25519.pub |
即可得到用户证书id_ed25519-cert.pub
和主机证书ssh_host_ed25519_key-cert.pub
。
分发过程可参考第二节。
With GPG-Agent
既然只需要签名公钥就可以使用证书登陆,那如果给 GPG 导出的公钥签名是否可以让 Yubikey GPG 支持证书登陆。经过一番探索,答案是:
理论上可行。
相关讨论在 GPG 的 issues 中可以看到。这个 Issue中可以看到 GPG-Agent 是可以支持 SSH-Agent 功能并提供证书支持的。
但是我的 Yubikey 5 使用ed25519
密钥在搭配证书使用时失败并提示如下错误:
1 | sign_and_send_pubkey: signing failed for ED25519 "/home/nomad/.ssh/id_ed25519": agent refused operation |
深入搜索资料后我发现了这篇 Issue,表明这个错误与卡片性能有关,更换其他密钥类型就可以解决问题。经测试,NIST256
、NIST384
算法工作正常。NIST512
算法未测试。brainpool
系列算法不支持 SSH 认证。
参考链接
SSH user certificates - Yubico
Certificate authority - Yubico
ssh-keygen(1) — Arch manual pages