掘金 后端 ( ) • 2024-04-09 18:59

笔者前面有一篇博文。标题应该是《了不起的SSH》,比较深入的探讨了SSH这个伟大的远程连接技术。但由于SSH涉及的内容实在是太过广泛,还有水平和篇幅的限制,在某些细节方面,在当时并没有深入细致的探讨。

最近笔者正好有机会使用一个远程的VPS系统,平台默认情况下,只提供了密钥登录的方式,同时虚拟机的默认配置,也支持公钥登录。所以在使用过程中,笔者有机会实践了自定义的公钥登录的过程,觉得又有新的理解和体验,有一些新鲜的东西,这里就著文记录和分享给读者。

基本过程

那个VPS系统的基本情况是这样的。在创建VPS的时候,不再需要设置用户名和密码,而是提供一个默认的非root账号,和生成密钥对的选项。生成密钥对后,平台系统会将这个账号的公钥,写入VPS系统,并且同时用户可以将对应的私钥PEM文件,下载到当前操作的管理计算机本地。然后,用户就可以使用这个私钥PEM文件,使用任意兼容的SSH客户端软件,建立SSH连接,但使用私钥登录的方式。

这个流程,和我们一般使用私钥登录的过程就不一样了。在那个过程中,我们一般会在本地,使用ssh-keygen命令,在本地生成一个密钥对,然后使用ssh-copyid命令,将其中的公钥,复制到远程系统,并进行相关的设置,后续的操作,就可以使用私钥直接进行登录了。

这两种方式,其实本质上是没有区别的,也各有各的优缺点。我们假设本地生成密钥对的场景为L,远程为R,它们的优缺点如下,其实就是安全和方便的差异:

  • L在理论上更安全一点,因为密钥生成过程在本地的用户环境中,在任何情况下都没有在其他系统或者网络中存储或者传输
  • L的操作更复杂,需要生成密钥对,并传输公钥到远程系统
  • L虽然在密钥角度更安全,但初始传输公钥,仍然需要使用密码进行登录,这是一个安全隐患或者不便的地方
  • R在使用的角度更方便,因为直接在远程系统已经自动配置好了用户的公钥,用户只需要下载和配置SSH连接的私钥就可以了
  • R可以方便在平台集的集中管理
  • 无论L或者R,它们的最终状态都是一样的

需求和问题

在了解了SSH私钥免密登录的一般过程之后,再来谈谈笔者遇到的问题。

除了在注册和管理VPS的系统(管理机和管理账号)之外,笔者另有一台开发用的计算机系统(开发机),需要连接到这个VPS(服务器)上进行工作和相关的操作。另外基于安全和业务方便的考虑,笔者还想要使用另外一个系统账号(开发账号)。这个时候就遇到了一些不便的地方。

当然,笔者可以使用管理机登录服务器,来创建这个开发账号。然后问题就来了,常规操作就是使用开发账号,在开发机上,生成密钥对,然后使用ssh-copyid和SSH登录服务器,将公钥复制到服务器上开发账号的用户环境中。但问题是服务器没有开放SSH密码登录,所以必须要设置SSH服务可以使用密码登录,然后才能进行登录操作,操作完成后,还需要关闭密码登录,这个操作还可能需要重启SSH服务,非常繁琐。

有没有更简单一点的方式? 答案是肯定的,笔者会在下面提供操作流程,但重点其实还是基于对SSH登录过程的原理性了解。我们先讨论操作,然后分析原理。

操作过程

基于对SSH登录过程的原理的理解,笔者针对这一需求,具体的解决方式和操作过程如下:

  • 在开发机上,在当前账号环境中,使用ssh-keygen命令,生成密钥对
PS C:\Users\yanjh> ssh-keygen.exe -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (C:\Users\yanjh/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in C:\Users\yanjh/.ssh/id_ed25519.
Your public key has been saved in C:\Users\yanjh/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:B/grjkIz13PamFHy2cQJ14ALavib35LYYV69rQx0OFI yanjh@WK-YANJH-AMD
The key's randomart image is:
+--[ED25519 256]--+
|         ..o     |
|      ..o . .    |
|   . ...E= .     |
|  . o .ooo+      |
|   o ..+S=o      |
|  + o *o==o      |
| . + B.%o  o     |
|  . +oOo.o. .    |
|   ...o.. o.     |
+----[SHA256]-----+
PS C:\Users\yanjh> dir ~/.ssh
    Directory: C:\Users\yanjh\.ssh
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          4/9/2024   4:49 PM            411 id_ed25519
-a----          4/9/2024   4:49 PM            101 id_ed25519.pub
-a----         2/18/2024   7:03 PM          10187 known_hosts
-a----         8/23/2022   1:36 PM           4726 known_hosts.old

这里这个密钥对,其实就是以id作为开头的两个文件,.pub就是公钥。生成密钥对是可以选择密钥类型的,默认是RSA,笔者这里使用ed25519椭圆曲线算法也是可以的。

  • 记录并复制公钥信息,简单而言,它是一段文本信息(base64)

PS C:\Users\yanjh> cat C:\Users\yanjh\.ssh\id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDi/TpIw84Hi87E34tL9ePqgBm9tXOuKnMhey7wU0Pye yanjh@WK-YANJH-AMD

我们看到的这个格式就是三段,第一段是公钥类型,第二段是base64编码的内容,第三段是描述信息或者标识

  • 使用管理账号,在管理机上,使用私钥PEM文件和SSH远程登录服务器

  • 登录服务器后,创建开发账号,包括设置密码和主文件夹

  • 使用 su - 命令,切换到开发账号

  • 将开发账号公钥内容,注入到 ~/.ssh/authorized_keys

可以直接使用nano编辑文件,将公钥内容复制到最后。

  • 在开发机上,使用开发账号连接ssh

开发机上操作的本地账号名词和开发账号可以没有关系,因为在连接时,会指定连接用的账号,如:

ssh -v devuser@devserver

这里 -v 可以用于显示连接过程的调试信息,发现连接错误的位置和原因。如果一切正常的话,开发机就可以使用开发账号,直接登录服务器了。

基于这样一个操作过程,我们就可以简单的分析和理解SSH私钥免密登录的原理了。

免密连接过程和原理

为了更直观起见,这里的分析,主要来源于SSH连接过程的调试信息(稍微繁琐,但能看出其处理方式):


 ssh 192.168.10.22 -v
OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2
debug1: Connecting to 192.168.10.22 [192.168.10.22] port 22.
debug1: Connection established.
debug1: identity file C:\\Users\\yanjh/.ssh/id_rsa type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_rsa-cert type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_dsa type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_dsa-cert type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_ecdsa type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_ecdsa-cert type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_ed25519 type 3
debug1: identity file C:\\Users\\yanjh/.ssh/id_ed25519-cert type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_xmss type -1
debug1: identity file C:\\Users\\yanjh/.ssh/id_xmss-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_for_Windows_8.1
debug1: Remote protocol version 2.0, remote software version OpenSSH_9.7
debug1: match: OpenSSH_9.7 pat OpenSSH* compat 0x04000000
debug1: Authenticating to 192.168.10.22:22 as 'yanjh'
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: curve25519-sha256
debug1: kex: host key algorithm: ecdsa-sha2-nistp256
debug1: kex: server->client cipher: [email protected] MAC: <implicit> compression: none
debug1: kex: client->server cipher: [email protected] MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ecdsa-sha2-nistp256 SHA256:thK7chc14GZeuCir7Auin4+mCR6P5mEWAz2DNrKvXT4
debug1: Host '192.168.10.22' is known and matches the ECDSA host key.
debug1: Found key in C:\\Users\\yanjh/.ssh/known_hosts:50
debug1: rekey out after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: rekey in after 134217728 blocks
debug1: pubkey_prepare: ssh_get_authentication_socket: No such file or directory
debug1: Will attempt key: C:\\Users\\yanjh/.ssh/id_rsa
debug1: Will attempt key: C:\\Users\\yanjh/.ssh/id_dsa
debug1: Will attempt key: C:\\Users\\yanjh/.ssh/id_ecdsa
debug1: Will attempt key: C:\\Users\\yanjh/.ssh/id_ed25519 ED25519 SHA256:B/grjkIz13PamFHy2cQJ14ALavib35LYYV69rQx0OFI
debug1: Will attempt key: C:\\Users\\yanjh/.ssh/id_xmss
debug1: SSH2_MSG_EXT_INFO received
debug1: kex_input_ext_info: server-sig-algs=<ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,[email protected],[email protected],rsa-sha2-512,rsa-sha2-256>
debug1: kex_input_ext_info: [email protected] (unrecognised)
debug1: kex_input_ext_info: [email protected] (unrecognised)
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Trying private key: C:\\Users\\yanjh/.ssh/id_rsa
debug1: Authentications that can continue: publickey,password
debug1: Trying private key: C:\\Users\\yanjh/.ssh/id_dsa
debug1: Trying private key: C:\\Users\\yanjh/.ssh/id_ecdsa
debug1: Offering public key: C:\\Users\\yanjh/.ssh/id_ed25519 ED25519 SHA256:B/grjkIz13PamFHy2cQJ14ALavib35LYYV69rQx0OFI
debug1: Server accepts key: C:\\Users\\yanjh/.ssh/id_ed25519 ED25519 SHA256:B/grjkIz13PamFHy2cQJ14ALavib35LYYV69rQx0OFI
debug1: Authentication succeeded (publickey).
Authenticated to 192.168.10.22 ([192.168.10.22]:22).
debug1: channel 0: new [client-session]
debug1: Requesting [email protected]
debug1: Entering interactive session.
debug1: pledge: network
debug1: ENABLE_VIRTUAL_TERMINAL_INPUT is supported. Reading the VTSequence from console
debug1: ENABLE_VIRTUAL_TERMINAL_PROCESSING is supported. Console supports the ansi parsing
debug1: client_input_global_request: rtype [email protected] want_reply 0
debug1: Remote: /home/yanjh/.ssh/authorized_keys:1: key options: agent-forwarding port-forwarding pty user-rc x11-forwarding
debug1: Remote: /home/yanjh/.ssh/authorized_keys:1: key options: agent-forwarding port-forwarding pty user-rc x11-forwarding
Last login: Tue Apr  9 17:32:55 2024 from 192.168.10.209

这个过程大体可以分为下面几个阶段:

  • 连接准备和初始化

连接请求的命令中,包括了连接使用的账号和服务器地址。ssh客户端会连接服务器地址的ssh端口(默认为22),并建立TCP连接。

  • 建立安全通道

连接成功后,SSH服务器会向客户端提供服务器的公钥信息。如果客户端以前连接过服务器,它会和保存的服务器条目进行比较,如ip地址、主机名和公钥等信息;如果是全新的连接,客户端会在know_hosts中建立这个服务器的相关条目。所以如果在连接过程中,发生如“公钥变更”等错误信息,可以确定这个问题的话(如主机IP确实修改了),就可以从know_hosts文件中删除对应的条目,然后重新尝试连接。

公钥检查如果没有问题的话,客户端和服务器会使用非对称加密的方式,协商一个加密通道,后续的TCP数据,都可以进行加密传输了。

  • 密钥匹配

安全通道建立之后,服务器端会告知客户端可以使用的认证方式,如公钥,或者密码。如果确定使用公钥,服务端会检索登录用户主文件夹中的 .ssh/authorized_keys,找到用户公钥,并使用这个公钥加密一个随机信息,作为认证挑战发送给客户端。

客户端收到认证方式和挑战信息,看到需要使用公钥认证,则会在遍历当前用户的私钥文件,尝试使用它们对挑战信息进行解密,如果成功,则将解密结果返回给服务器。服务器检查解密结果,确认当前会话的用户确实持有用户公钥所对应的私钥,认证完成,SSH连接正式成功建立。

小结

理解了上述原理和过程,我们就可以很容易的理解前面的实践和操作过程,其实质就是利用了非对称加密的特性,在服务端保存公钥,在客户端保存对应的私钥。认证时,能够找到和使用对应的密钥对,来完成一个信息交互和验证的过程。当然,密钥保存的位置和方式,是事先约定好的,在客户端和服务端都有相应的设置和处理。了解了这些原理和细节,我们就可能可以不使用标准的流程(如不使用ssh-copyid命令),有些地方可以根据情况手动的进行处理,也可以达到相同的效果。