一次SSH公钥免密码登录实现的实践

笔者前面有一篇博文。标题应该是《了不起的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命令,生成密钥对
powershell 复制代码
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)
powershell 复制代码
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连接过程的调试信息(稍微繁琐,但能看出其处理方式):

js 复制代码
 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: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com 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,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256>
debug1: kex_input_ext_info: publickey-hostbound@openssh.com (unrecognised)
debug1: kex_input_ext_info: ping@openssh.com (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 no-more-sessions@openssh.com
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 hostkeys-00@openssh.com 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命令),有些地方可以根据情况手动的进行处理,也可以达到相同的效果。

相关推荐
ServBay9 小时前
打通 AI 编程本地运维边界,利用 MCP 协议简化环境与服务管理
后端·ai编程·mcp
程序员cxuan9 小时前
DeepSeek 杀入多模态,识图功能正式上线!
人工智能·后端·程序员
IT_陈寒12 小时前
SpringBoot这个自动配置坑我跳了三次
前端·人工智能·后端
用户3952409988013 小时前
排坑日记:ASP.NET Core 中 "Required field is not provided" 验证错误全记录
后端
用户83562907805114 小时前
使用 Python 自动化 PowerPoint 形状布局与格式设置
后端·python
AlfredZhao14 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
Oneslide14 小时前
sudo免密权限配置不生效
后端
站大爷IP14 小时前
为什么Python不用var或let声明变量?
后端
赴星半途14 小时前
NestJS实战-创建AuthService
后端