1

SSH 是每一台电脑的标准配置,Linux 就不必说了,连 windows 也从 2018 年开始自带 OpenSSH 了。
它主要的用途是登陆到远程电脑中执行命令,在云开发的时代,它是每个程序每天都要用到的工具。本文将简单介绍一下它的原理,
基本用法以及端口转发和动态转发等高阶用法。

介绍

SSH 叫安全外壳协议(Secure Shell),是一种加密的网络传输协议,可在不安全的网络中网络服务提供安全的传输环境。它通过在网络中创建安全隧道来实现 SSH 客户端和服务器之间的连接。最早的时候,互联网通信都是明文通信,一旦被截获,内容就会被暴露。1995年,芬兰学者 Tatu Ylonen 设计了 SSH 协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为所有操作系统的标准配置。

SSH 是一种协议, 存在多种实现,既有商业实现,也有开源实现(OSSH,OpenSSH)。本文使用的自有软件 OpenSSH, 毕竟它是目前最流行的 SSH 实现,而且是所有操作系统的默认组件。

TIPS: OpenSSH发展史
1995 年 7 月, Tatu Ylonen 以免费软件的形式将一套保护信息传输的程序(也就是 SSH )发布出去。程序很快流行,到年底已经有两万用户,遍布五十国家。所以在年底时,他创立了 SSH 通信安全公司来继续开发和销售 SSH, 所以它变成了专有软件。在 1999 年,瑞典程序员基于 SSH 最后一个开源的版本 1.2.12 开发了 OSSH,之后 OpenBSD 开发者在 OSSH 的基础上进行大量修改,形成了 OpenSSH。它是目前唯一一种最流行的 SSH 实现,成为了所有操作系统的默认组件。

原理介绍

SSH 之所以一经提出,就得到了快速发展,是因为数据的安全性对任何人都非常重要。这里我们对其保护数据安全的原理进行探究。

在聊加密前先介绍一下几个密码学的基本概念:

  • 明文plaintext 指传送方(一般指客户端)想要接受方(一般指服务端)获得的可读信息
  • 密文ciphertext 指明文经过加密后所产生的信息
  • 秘钥key 指用来完成加密、解密、完整性验证等密码学应用的密码信息,是明文转换为密文或密文转换为明文的算法需要的参数
  • 私钥 指私有的秘钥
  • 公钥 指公开的秘钥

对称加密

对称加密就是加密或解密使用的是同一个秘钥。比较常用的对称加密算法有 AES,DES等。其具体的时序图如下:
ssh_symmetric.png

对称加密的优点是加解密效率高,速度快。对于服务端而言,它和每个客户端都要有一个秘钥,庞大的客户端数目导致秘钥数目多,而且一旦机器被登录,所有的秘钥都泄露,所以缺点是秘钥的管理和分发比较困难,不安全。

非对称加密

非对称加密需要一对秘钥来进行加密和解密,公开的秘钥叫公钥,私有的秘钥叫私钥。注意公钥加密的信息只有私钥才能解开(加密过程),私钥加密的信息只有公钥才能解开(验签过程)。比较常用的非对称加密算法有 RSA。其具体的时序图如下:
ssh_asymmetric.png

非对称加密的优点是安全性更高,秘钥管理比较方便,每个服务器只要维护一对公私钥即可。缺点是加解密耗时长,速度慢。不过对于现在的计算机而言,这点成本可以忽略不计。

中间人攻击

中间人攻击的英文全称是 Man-in-the-middle attack,缩写为 MITM。在密码学和计算机安全领域中是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。在许多情况下这是很简单的(例如,在一个未加密的 Wi-Fi 无线接入点的接受范围内的中间人攻击者,可以将自己作为一个中间人插入这个网络)。其具体的时序图如下:
ssh_mitm.png

受到中间人攻击的关键原因是客户端不知道服务端的公钥真假,服务端也不知道客户端的公钥真假。所以破解这个问题的关键是如何相互认证,也就是要像黄宏《开锁》小品里一样证明我就是我,你就是你。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@  WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!   @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:sYNNR1L6T5cSAG4BndqtdDhJEI0eB9LamBTkuIue3+0.
Please contact your system administrator.
Add correct host key in /Users/xx/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/xx/.ssh/known_hosts:40
ECDSA host key for [xx.com] has changed and you have requested strict checking.
Host key verification failed.

基本用法

生成公钥

ssh-keygen 是安全外壳( SSH )协议套件的标准组件,用于生成,管理和转换身份验证密钥。

参数说明

  • -b bits 指定要创建的秘钥中的位数,默认 2048 位。值越大,密码越复杂
  • -C comment 注释,在 id_rsa.pub 中末尾
  • -t rsa/dsa等 指定要创建的秘钥类型,默认为 RSA
  • -f filename 指定公私钥的名称,会在 $HOME/.ssh 目录下生产私钥 filename 和公钥 filename.pub
  • -N password 指定使用秘钥的密码,使得多人使用同一台机器时更安全

常用命令

# 生成公私钥,默认文件为 ~/.ssh/id_rsa
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

管理秘钥

ssh-agent 和 ssh-add 是安全外壳(SSH)协议套件的标准组件,用于管理私钥。一般情况下我们使用不带密码的 id_rsa 作为我们的默认私钥,此时是没必要启动 ssh-agent 的。当我们碰到以下两种情况则需要它:

  1. 使用不同的秘钥连接到不同的主机时,需要手动指定对应的秘钥。(ssh-agent 帮我们选择对应的秘钥进行认证)
  2. 当私钥设置了密码,而我们又需要频繁的使用私钥进行认证。(ssh-agent 帮我们免去重复输入密码)

代理常用命令

# 启动代理
eval `ssh-agent`
# 关闭代理
ssh-agent -k
# 在 ~/.bashrc 中加入以下来实现登陆自动启动 ssh-agent,退出自动 kill 掉程序
eval $(ssh-agent -s) > /dev/null
trap 'test -n "$SSH_AGENT_PID" && eval `/usr/bin/ssh-agent -k` > /dev/null' 0

# 查看代理中的私钥
ssh-add -l
# 查看代理中私钥对应的公钥
ssh-add -L
# 移除指定的私钥
ssh-add -d /path/of/key/key_name
# 移除所有的私钥
ssh-add -D

发送公钥

ssh-copy-id 是一个用来将公钥放到服务器上的脚本。它通过 SSH 密码登陆远程服务器,并将指定的公钥放到远程服务器 $HOME/.ssh/authorized_keys 中。这个操作也可以先登陆到服务器中,然后通过 vi 等文本编辑命令向 $HOME/.ssh/authorized_keys 中加入允许登陆的公钥。不过对于云服务器可以在启动服务器时在页面上操作绑定公钥,这样更安全些(阿里云和腾讯云默认关闭秘钥登陆 PasswordAuthentication no )。特别注意的是,千万别在公共的网络中通过密码登陆远程服务器,而秘钥登陆没有问题。

# 发送公钥的两种方式(等价)
ssh-copy-id -i ~/.ssh/id_rsa.pub user@host
ssh user@host 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub

登录

登录配置

SSH 登陆服务器需要知道服务器的主机地址(主机名或主机 IP 地址),用户名和密码,有时还要指定端口号(默认 22 )。主机名还好,但是主机IP 地址就比较难记的,特别是当你可能要登录十几台服务器时。一般我们使用的登陆命令如下:

# 登陆目标服务器( 172.17.132.120 )
ssh -p 58422 user@172.17.132.120
# 通过跳板机登陆目标服务器( 172.17.132.120 )
ssh -p 58422 user@jumper.example.com ssh user@172.17.132.120
# 端口映射
ssh -p 58422 user@jumper.example.com -fNL 5433:172.17.132.120:5432 -N

通过配置 $HOME/.ssh/config 可以使用以下命令来登录。

# 登陆目标服务器( 172.17.132.120 )
ssh target
# 通过跳板机登陆目标服务器( 172.17.132.120 )
ssh jump_target
# 端口映射
## 登陆时通过 LocalForward 配置
ssh jump_target
## 使用-L来实现本地端口映射
ssh -C -N -g -L 5433:127.0.0.1:5432 jump_target
# 通用配置,所有配置都使用
Host *
    AddKeysToAgent yes      # 将私钥添加到ssh-agent中
    UseKeychain yes         # 保存密码到agent中
    ServerAliveInterval 10  # 连接心跳间隔10s
    ServerAliveCountMax 3   # 重连次数为3
# target配置
Host target
    HostName 172.17.132.120
    User user
    Port 58422
    IdentityFile ~/.ssh/id_rsa
# 跳板机配置
Host jumper
    HostName jumper.example.com
    User user
    Port 58422
    IdentityFile ~/.ssh/id_rsa
Host jump_target
    HostName 172.17.132.120
    User user
    Port 22
    IdentityFile ~/.ssh/id_rsa
    ProxyCommand ssh user@jumper -W %h:%p 2>/dev/null
    LocalForward 5433 localhost:5432    # 本地5433映射到jump_target的5432
TIPS:
VS Code 的 Remote 插件会读取本地的配置文件 $HOME/.ssh/config,以便像本地一样进行远程开发。

首次登陆

一般在 $HOME/.ssh 目录下除了公私钥文件,config 配置文件,authorized_keys 认证文件外,还有一个 known_hosts 文件。
这个文件记录了远程主机 ip 和远程主机对应的公钥指纹。我们在第一次登陆(密码或秘钥登陆)服务器时,会有如下的提示界面:

### SSH 首次登陆的提示
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:HosOqhcUmbB7QG81yCuDPkvxTgot+vpple+czXPrEug.
ECDSA key fingerprint is MD5:fd:d7:e1:2c:42:4e:b4:2d:a3:21:4d:d1:c4:74:64:2d.
Are you sure you want to continue connecting (yes/no)?

此时 known_hosts 并没有 127.0.0.1 这台机器的指纹信息,所以显示这个提示来让我们确认这个指纹是否是目标机器的 ECDSA 算法的指纹。
当我们输入 yes 确认后,在下次登录的时候,远程主机发送过来的公钥指纹,直接和 known_hosts 文件中对应 ip 的公钥指纹比较即可。

# 本机查看服务器 172.17.132.120 的所有公钥(要与服务器上 /etc/ssh 下面的公钥 *.pub 一致)
ssh-keyscan -p 22 172.17.132.120

# 查看服务器公钥 ecdsa 的指纹 -E md5/sha256 指纹 hash 算法
ssh-keygen -E md5 -lf /etc/ssh/ssh_host_ecdsa_key.pub
## 256 MD5:84:3d:9c:6e:75:f2:6b:b2:0b:40:aa:d6:29:2f:b4:40 no comment (ECDSA)
## 256 SHA256:ZoGnph63gnKLC9wQYrHYVU8ROTf6+K9LKAjn+jrXB2o no comment (ECDSA)

# 从客户端查看服务器公钥 ecdsa 的指纹(初次登陆时要验证的指纹)
ssh-keyscan -t ecdsa -p 22 172.17.132.120 |ssh-keygen -lf -

# 公钥转换成特定指纹 hash 算法的指纹
awk '{print $2}' /etc/ssh/ssh_host_ecdsa_key.pub | base64 -d|openssl sha256 -binary |base64
TIPS: known_hosts的重要性
known_hosts 这个文件是客户端验证服务端身份的重要依据。每次客户端向服务端发起连接请求时,不仅服务端要验证客户端的合法性,客户端也需要验证服务端的身份。客户端就是通过 known_hosts 中的公钥指纹来验证服务器是否发生了变化。它在一定程度上能避免中间人攻击,除了第一次登陆,因为那时 known_hosts 中还没有服务器的身份信息,所以对于首次提示的登陆指纹信息还是需要和服务器比对的。最安全保险的做法是第一次登陆就使用秘钥登陆。

登陆流程

  1. 版本号协商阶段
  2. 密钥和算法协商阶段
    服务端和客户端分别发送算法协商报文给对方,报文中包含自己支持的公钥算法列表、加密算法列表、消息验证码算法列表、压缩算法列表等。服务端和客户端根据对方和自己支持的算法得出最终使用的算法。服务端和客户端利用 DH 交换算法、主机密钥对等参数,生成会话密钥和会话 ID。
  3. 认证阶段( publickey > gssapi-keyex > gssapi-with-mic > password )
  4. 会话请求阶段
  5. 会话交互阶段

密码登陆

密码登陆的认证流程如下:

  1. 客户端使用密钥和算法协商阶段生成的会话密钥加密账号、认证方法、口令,将结果发送给服务器。
  2. 服务端使用获得的会话密钥解密报文,得到账号和口令。
  3. 服务端对这个账号和口令进行判断,如果失败,向客户端发送认证失败报文,其中包含了可以再次认证的方法列表。
  4. 客户端从认证方法列表中选择一种方法进行再次认证。
  5. 这个过程反复进行,直到认证成功或者认证次数达到上限,服务端关闭本次TCP连接。

ssh_password

秘钥登陆

秘钥登陆的认证流程如下:

  1. 客户端使用密钥和算法协商阶段生成的会话密钥加密账号、认证方法、id_rsa.pub,将结果发送给服务端。
  2. 服务端使用会话密钥解密报文,得到账号、id_rsa.pub。服务端在 $HOME/.ssh/authorized_keys 中找对应的公钥,如果没有找到,发送失败消息给客户端,如果找到,比较客户发送过来的这个公钥和找到的公钥,如果内容相同,服务端生成一个随机的字符串,简称“质询”,然后使用找到的公钥加密这个质询,然后使用会话密钥再次加密。
  3. 服务端把这个双重加密的数据发送给客户端
  4. 客户端使用会话密钥解密报文,然后使用 id_rsa 再次解密数据,得到质询。
  5. 客户端使用会话密钥加密质询,发送给服务端。
  6. 服务端使用会话密钥解密报文,得到质询,判断是不是自己生成的那个质询,如果不相同,发送失败消息给客户端,如果相同,认证通过。

ssh_key

二者区别

我们常说使用秘钥登陆比密码登陆更方便更安全,为什么这么说呢?方便是因为不用记密码,安全是一方面敏感关键的密码没有在传输,另一方面是因为质询的存在使得在一次对话中同时验证了客户端和服务端。

高阶用法

免密安全传输

scp/rsync/sftp 都可以基于 SSH 来进行免密安全传输,常见命令如下:

# 从本地同步 src.tar.gz 文件到远程服务器 jump_target 的目录 /path/to/des/
scp src.tar.gz jump_target:/path/to/des/
rsync -avz src.tar.gz jump_target:/path/to/des/

# 从远程服务器 jump_target 的文件 /path/to/src.tar.gz 到本地
scp jump_target:/path/to/src.tar.gz .
rsync -avz jump_target:/path/to/src.tar.gz .

端口转发

SSH 不仅仅能够自动加密和解密 SSH 客户端与服务端之间的网络数据,同时,SSH 还能够提供了一个非常有用的功能,那就是端口转发,即将 TCP 端口的网络数据,转发到指定的主机某个端口上,在转发的同时会对数据进行相应的加密及解密。如果工作环境中的防火墙限制了一些网络端口的使用,但是允许 SSH 的连接,那么也是能够通过使用 SSH 转发后的端口进行通信。转发主要分为本地转发与远程转发两种类型。

转发常用参数

  • -C: 压缩传输,提高传输速度。
  • -f: 将 SSH 传输转入后台执行,不占用当前 SHELL, 常与 -N 一起使用
  • -N: 建立静默连接(建立了连接但看不到具体会话)
  • -g: 在 -L/-R/-D 参数中,允许远程主机连接到建立的转发的端口,如果不加这个参数,只允许本地主机建立连接。
  • -L: 本地端口转发
  • -R: 远程端口转发
  • -D:动态转发( SOCKS 代理)
  • -P: 指定 SSH 端口

本地端口转发

由本地网络服务器的端口 A,转发到远程服务器端口 B。说白了就是,将发送到本地端口 A 的请求,转发到目标端口 B。格式如下

ssh -L 本地网卡地址:本地端口:目标地址:目标端口 用户@目标地址
常见的应用场景见下图:
ssh_local

对应的命令如下:

# jump_target 服务器上的 3306 端口服务映射到本地 33306 `mysql -u root -p root -H localhost -P 33306`
## 1 是 2,3,5 路线中的加密通道,将本地 33306 的网络数据转发到 jump_target 的 3306 端口
ssh -C -N -g -L 33306:localhost:3306 jump_target
## 在 2,3 中搞了个加密通道,然后在跳板机上将本地 33306 的网络数据转发到 172.17.132.120 的 3306 端口
ssh -C -N -g -L 33306:172.17.132.120:3306 jumper

远程端口转发

由远程服务器的某个端口,转发到本地网络的服务器某个端口。说白了,就是将发送到远程端口的请求,转发到目标端口。格式如下:

ssh -R 远程网卡地址:远程端口:目标地址:目标端口 用户@目标地址
常见的应用场景有个专用术语叫内网穿透,结构如下图:
ssh_remote

# 将公网上的服务器 jump_target 的端口 33333 映射到本地的 22,这样就可以通过在 jump_target 上通过 SSH 来访问本地机器
ssh -f -N -g -R 33333:127.0.0.1:22 jump_target
TIPS:
公网上的服务器 jump_target 要设置 GatewayPorts yes,默认为 no。此外要映射的端口 33333 要可以访问。

动态转发

动态转发就是建立一个SSH加密的SOCKS 4/5代理通道。任何支持 SOCKS 4/5 协议的程序都可以使用这个加密的通道进行访问。格式如下:
ssh -D [本地地址:]本地端口号 远程用户@远程地址

# 将访问本地 55558 端口的请求都转发给 jump_target ,并让它去执行
ssh -C -N -g -T -D 127.0.0.1:55558 jump_target

日常使用问题

跳板机的配置

# 跳板机的配置
Host jump
  HostName  jumper.example.com
  Port      58422
  User      haojunyu
  IdentityFile ~/.ssh/dg_rsa
  AddKeysToAgent yes    # 将私钥添加到 agent 中
  UseKeychain yes       # 保存密码到 agent 中
# 目标机的配置
Host ws
  HostName  172.17.132.120
  Port      22
  User      haojunyu
  IdentityFile ~/.ssh/dg_rsa
  ProxyCommand ssh haojunyu@jump -W %h:%p 2>/dev/null
  ServerAliveInterval 10
  ServerAliveCountMax 3

内网任意服务访问

日常工作中经常会启很多服务在内网机器上,然后通过打洞(本地端口转发)来将本地的端口映射到内网机器上服务端口。
这样有个问题就是一个服务就得维持一个打洞命令 ssh -C -N -g -L 33306:172.17.132.120:3306 jumper
对应这样的问题,最好的解决方案是使用动态转发 ssh -C -N -g -T -D 127.0.0.1:55557 hb_jumper
本地通过 SwitchyOmega 或 proxifier 工具来将内网 IP 段 172.17.* 的请求转发到本地的 55557 端口。

git push 报权限不允许(公钥)

通常在服务器上执行 git push 时会报如下错误

具体报错信息:
Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights and the repository exists.

报错的原因是当前机器上没有服务告诉 git 要使用哪个私钥来进行 git 的操作。
对应的解决方法也比较多,推荐解法一和二:

  • 解法一:通过 ~/.ssh/config 指定(适用个人机器)

    Host github.com
        HostName  github.com
        User      haojunyu
        IdentityFile ~/.ssh/id_rsa
  • 解法二:配置仓库或全局的 core.sshCommand(指定仓库适用共享机器,全局适用个人机器.git版本高于2.3.0

    git config core.sshCommand "ssh -i ~/.ssh/id_rsa -F /dev/null"
  • 解法三:ssh-agent 临时授权(适用共享机器)

    eval `ssh-agent`
    ssh-add ~/.ssh/id_rsa

端口转发命令服务化

这个情况是希望开机时就把端口转发开通,并且一直保持着。这就得介绍 linux 中常用的两种服务化的工具:Supervisor 和 Systemd。
前者是需要安装 Supervisor, 但工具比较轻量,使用也比较简单,后者虽然比较重,但是基本所有系统都自带。下面提供两者的配置方法:

  • Supervisor 的配置
[program:ssh-wifi_ol]
command=ssh -C -N -g -L 9789:127.0.0.1:9789 jump
stdout_logfile=/Users/haojunyu/.supervisord_log/ssh-wifi_ol.log
autostart=true
autorestart=true
startsecs=5
priority=1
stopasgroup=true
killasgroup=true
  • Systemd 的配置
# gfw service
[Unit]
Description=gfw
After=network.target
[Service]
Type=simple
User=hjy
ExecStart=ssh -C -N -g -T -D 127.0.0.1:55558 gfw
Restart=on-failure
[Install]
WantedBy=multi-user.target
TIPS:
把一些经常用的服务通过端口转发服务化,而一些临时性的服务通过命令来进行端口转发,也可以使用同事编写的端口转发的 Python 程序来进行。

参考文献

  1. 什么是SSH?你应该用过吧
  2. 维基百科-SSH
  3. windows支持openssh
  4. 图解SSH原理
  5. SSH官方文档
  6. 所有配图
  7. 中间人攻击
  8. 了解ssh代理
  9. ssh远程登陆中的钥匙指纹是什么以及如何比对
  10. ssh登陆认证过程详解

如果该文章对您产生了帮助,或者您对技术文章感兴趣,可以关注微信公众号: 技术茶话会, 能够第一时间收到相关的技术文章,谢谢!
技术茶话会
本篇文章由一文多发平台ArtiPub自动发布


禹过留声
3 声望0 粉丝

从事推荐系统和知识图谱相关方面的工作。