大家都知道 ssh 命令,是用于远程登陆的,使用用户名密码或公钥进行认证。
如命令

ssh user@host

输入用户名密码,即可在客户端使用 user 登陆远程 host 主机。
当然也可以实现免密码登陆:
在客户端用执行

ssh-keygen

然后一直确认,将会生成公钥,存放在 /home/xxx/.ssh/id_rsa.pub 中,将
id_rsa.pub 中的公钥字符串复制到 host 主机下 user 的家目录的 authorized_keys 文件中,即 /home/user/.ssh/authorized_keys 中即可实现免密码登陆。

百度百科介绍:SSH 为建立在应用层基础上的安全协议。SSH 是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。那么以上两种认证方式它是如何确保是安全通信的呢?

安全的通信离不开加密解密,SSH 也不例外,它使用的是对称加密算法和非对称加密算法的结合。那么,具体是如何使用加密算法的呢?

当客户端第一次登陆服务器时会有以下提示

(pyenv) aaron@ubuntu:~$ ssh aaron@192.168.0.111
The authenticity of host '192.168.0.111 (192.168.0.111)' can't be established.
ECDSA key fingerprint is SHA256:ckDktQw16lOAnl+bKIWVo+J3kE/HaeNoiutDv2cFy7c.
Are you sure you want to continue connecting (yes/no)?

这一步到底发生了什么?经过学习和调查,发现有以下几个阶段:

第一阶段:版本协商阶段。服务器等待客户端连接,客户端发起 TCP 连接请求后,服务器返回客户端一个版本号;客户端收到报文后,解析该数据包,如果能使用服务器端的协议版本号,则使用,否则使用自己的协议版本号,然后将协议版本号发送至服务器;服务器比较客户端发来的版本号,决定是否能同客户端一起工作。如果能一起工作,进入下一阶段,如不能服务器断开 TCP 连接,会话结束。
第一阶段均为明文传输。

第二阶段:算法协商阶段。第一阶段完成后,服务器和客户端向对方发送一个自己支持的公钥算法列表、加密算法列表、消息验证码算法列表、压缩算法列表等。服务器和客户端确定最终使用的算法,然后使用 DH 交换算法,利用主机密钥对等参数,生成会话密钥和会话 ID,DH 交换算法仅用于客户端和服务器会话密钥和会话 ID 的交换,并不用于消息的加密,DH 交换算法的特性使得第三方很难获取到会话密钥和会话 ID,有关 DH 交换算法如何使第三方难以获取会话密钥,与计算离散对数的难度有关,请访问【https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange】了解具体情况。
后续所有的会话,客户端和服务端都会使用同一会话密钥加密和解密,这里用到了对称加密算法。


第三阶段:认证阶段。本文开始已经提到,有两种认证方式:

1. 用户名密码。回到这里

(pyenv) aaron@ubuntu:~$ ssh aaron@192.168.0.111
The authenticity of host '192.168.0.111 (192.168.0.111)' can't be established.
ECDSA key fingerprint is SHA256:ckDktQw16lOAnl+bKIWVo+J3kE/HaeNoiutDv2cFy7c.
Are you sure you want to continue connecting (yes/no)?

这里表达的意思是只知道此 host 的公钥指纹,还继续连接吗?
我们输入yes之后,服务器将其公钥发送给客户端,客户端将服务器公钥保存在 /home/aaron/.ssh/known_hosts 里,后续再也不会看到输入 yes/no 的这一步。

(pyenv) aaron@ubuntu:~$ ssh aaron@192.168.0.111
The authenticity of host '192.168.0.111 (192.168.0.111)' can't be established.
ECDSA key fingerprint is SHA256:ckDktQw16lOAnl+bKIWVo+J3kE/HaeNoiutDv2cFy7c.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.0.111' (ECDSA) to the list of known hosts.
aaron@192.168.0.111's password:

到这里,服务器请求客户端输入密码,客户端获取用户输入的密码后使用服务器的公钥进行加密并发送给服务器,服务器使用自己的私钥进行解密,对比密码是否是该用户的密码,如是则允许登陆,接收客户端用户发送的指令,如否则返回验证失败,拒绝登陆。当然指令也是经过公钥加密的,所有的会话都会再经过第二阶段产生的会话密钥进行加密,所以传输是相当安全的。


2. 使用公钥认证(免密码登陆)。这一步在我们手动将客户端的公钥复制到服务器的 authorized_keys 文件中后才能实现。相当于服务器获取了客户端的公钥。那么不需要密码便可登陆,这一步是如何实现的呢?
服务器用客户端的公钥加密一个 256 位的随机字符串,客户端接收后使用自己的私钥解密,然后将这个字符串和会话 id 合并在一起,对结果应用 MD5 散列函数并把散列值返回给服务器,服务器进行相同的 MD5 散列函数处理,如果客户端和该值可以匹配,那么认证成功,允许登陆,达到免密登陆的效果。至此认证阶段结束。


中间人攻击:如果攻击者插在用户与远程主机之间(比如在公共的 wifi 区域),用伪造的公钥,获取用户的登录密码。再用这个密码登录远程主机,那么 SSH 的安全机制就荡然无存了。这种风险就是著名的中间人攻击(http://en.wikipedia.org/wiki/Man-in-the-middle_attack)

那么,ssh 是如何应对中间人攻击的?

一开始我以为是 DH 密钥交换算法可有效防止第三人获取会话密钥,但这并不能阻止中间人伪装成服务器。虽然 SSH 从原理上不能抵御中间人攻击,但 SSH 首次连接会下载服务端的公钥,并提示服务器的公钥指纹,用户可以核对此指纹与服务器公钥生成的指纹是否一致,一致则保存并信任,下次访问时客户端将会核对服务端发来的公钥和本地保存的是否相同,不同就发出中间人攻击的警告拒绝连接,除非用户手动清除已保存的公钥。结论:如果首次连接没有中间人,之后的连接就无需担心中间人,因为中间人给出的公钥和服务端给出的公钥相同的可能性可以忽略。

附几个常用的 ssh 命令:

1. 将客户端公钥复制到服务器的authorized_keys文件中

  $ ssh user@host 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub

2. 将$HOME/src/目录下面的所有文件,复制到远程主机的$HOME/src/目录。

$ ssh user@host 'tar cz src' | tar xzv

3. 查看远程主机是否运行进程httpd。

  $ ssh user@host 'ps ax | grep [h]ttpd'

4. 绑定本地端口。

  $ ssh -D 8080 user@host

说明:既然SSH可以传送数据,那么我们可以让那些不加密的网络连接,全部改走SSH连接,从而提高安全性。这里,SSH会建立一个socket,去监听本地的 8080 端口。一旦有数据传向那个端口,就自动把它转移到SSH连接上面,发往远程主机。可以想象,如果8080端口原来是一个不加密端口,现在将变成一个加密端口。