SSH使用指南

Last Updated: 2022-08-14 14:42:39 Sunday

-- TOC --

使用Linux环境的同学,一定离不开SSH(Secure Shell)这个重要工具!

openssh

SSH的历史和作用

历史上,网络主机之间的通信是明文通信,不加密。这使得通信很不安全,一个典型的例子就是远程服务器的登录。登录远程服务器,需要用户输入的账户密码,并传给服务器,如果这个过程是明文通信,就意味着传递过程中,线路经过的所有中间设备都能看到账号密码,这是很可怕的。

SSH 就是为了解决这个问题而诞生的,它能够加密计算机之间的通信,保证不被窃听或篡改。它还能对操作者进行认证(authentication)和授权(authorization)。明文网络协议可以套用在它的里面,从而实现加密通信。

1995年,芬兰赫尔辛基工业大学的研究员 Tatu Ylönen 设计了SSH 协议的第一个版本,同时给出了第一个实现(称为 SSH1)。当时,他所在的大学网络一直发生密码嗅探攻击(sniffing),他不得不为服务器设计一个更加安全的登录方式。写完SSH1以后,他就把这个工具公开了,并允许其他人免费使用。

嗅探(Sniffing)是指不间断地监测网络通信设备,截获用户传给服务器的账户密码,记录下来后另外使用。

SSH1可以替换 rlogin、TELNET、FTP 和 rsh 这些不安全的协议,所以大受欢迎,用户快速增长,1995年底已经发展到五十个国家的20,000多个用户。SSH1协议也变成 IETF 的标准文档。

1995年12月,由于客服需求越来越大,Tatu Ylönen 就成立了一家公司叫 SCS,专门销售和开发 SSH软件。这个软件的后续版本,逐渐从免费软件变成了专有的商业软件。由于SSH1 协议存在一些安全漏洞,所以1996年SCS公司又提出了 SSH2 协议(或者称为 SSH 2.0)。这个协议与SSH1.0版不兼容。1997年SSH2进行了标准化,1998年推出了软件实现 的SSH2。但是,官方的 SSH2 软件是一个专有软件,不能免费使用,而且 SSH1 的有些功能也没有提供。

1999年,OpenBSD的开发人员决定写一个SSH2协议的开源实现,这就是著名的OpenSSH 项目。该项目最初是基于SSH 1.2.12版本,那是当时SSH1 最后一个开源版本。但是,OpenSSH 很快就完全摆脱了原始的官方代码,在许多开发者的参与下,按照自己的路线发展。OpenSSH随OpenBSD 2.6版本一起提供,以后又移植到其他操作系统,成为最流行的SSH 实现。目前,Linux的所有发行版几乎都自带OpenSSH。现在,SSH2 有多种实现,既有免费的,也有收费的。OpenSSH还提供一些辅助工具软件(比如 ssh-keygen 、ssh-agent)和专用的客户端工具(比如 scp 和 sftp)。

现在还有谁会去使用收费的SSH2呢?Tatu Ylönen如果不成立公司,一直保持SSH的开源免费状态,现在会是什么样呢?不过,1995年发生的这些,的确太早了......我一直深深感到自己的落后......

除了安全登录服务器,SSH还可以用来创建Tunnel(隧道)和端口转发,为其它自身不带安全加密的业务提供一条安全的通道。

SSH服务器的安装

$ sudo apt install openssh-server

安装成功后,sshd进程(SSH服务器后台进程的名称)就运行起来了,非root账户可以直接登录(有的云服务器在初始化后,只有root账户,此时只能用root登录)。同时,SFTP也自动运行起来,SFTP与SSH使用相同端口。(注意:SFTP与FTPS是不一样的)

树莓派官方提供的系统自带SSH Server,不过要通过配置启动才可以使用。Lite系统启动SSH Server的方法如下:

$ sudo raspi-config

然后选择 3 Interface Options --> P2 SSH,按提示打开启动即可。

SSH服务器的配置

man 5 sshd_config

SSH服务器的配置文件如下:

$ sudo vim /etc/ssh/sshd_config

修改配置后,需要重启sshd进程:

$ sudo systemctl restart sshd

Ubuntu安装的sshd的配置文件中,没有指定具体的版本,anyway,增加一个版本信息不复杂,还可以提高安全性:

Protocol 2

SSH服务器默认的监听IP为运行主机上所有IP地址,默认端口号为22。

Port 22
Port 12345
ListenAddress 0.0.0.0  # listen on all ip, but no port specified
ListenAddress 0.0.0.0:12345  # listen on all ip and with port 12345
ListenAddress 192.168.1.8:12345  # listen only on 192.168.1.8

SSH服务器可以同时在多个端口上监听,这时就配置多条Port。公网上的服务器为了安全,很多时候不会现在在22号默认端口上监听,而会选择一个自定义的端口号。

在切换SSH监听端口的时候,可以先让其同时在两个端口上监听,然后再去掉其中一个,这样可以保证SSH的连接在修改配置时,不会被中断。如果直接将端口号从一个端口号修改到另一个,然后重启,可能当前连接就会中断,然后你再去连新端口号,可能会由于一些其它配置方面的问题,导致无法连接,这会很麻烦。

有的时候修改SSH监听端口后,无法成功连接,可能的原因是你的服务器外有一层防火墙,比如云服务器供应商可能会在外围提供一些安全机制,让你可以配置防火墙规则。

禁止root用户登录,是为安全考虑,阿里云的云盾就有一个检查项,叫做SSH登录基线检查,就是检查这个。

PermitRootLogin no  # yes
AllowUsers xinlin abc xyz

登录需要认证,禁止密码认证,实际上就是禁止用户用密码登录。比如:如果设置了使用SSH秘钥登录,这个时候取消密码认证,就没有人可以再使用密码登录系统,只能用你的私钥登录。

PasswordAuthentication no

默认是不允许的,但是如果你的环境非常安全,或者因为其他原因无法输入密码,或者可以把输入密码省掉:

PermitEmptyPasswords yes  # no

如果不配置服务器心跳,常常遇到的情况就是,ssh session无操作一段时间,连接就终止了。要不断地重连,挺烦人的。

ClientAliveInterval 30
ClientAliveCountMax 5

30表示:服务器每隔30秒向客户端发出一次心跳;5表示:如果服务器连续5次都收不到客户端的心跳回复,就断开连接。30和5这两个数字可以自己按需配置。

SSH一段时间不操作后断开连接,常常是因为长时间没有数据传输,这条连接被中间的路由器掐断了。有了心跳就可以解决这个问题。SSH Tunnel的Keep alive,也依赖这个机制。这个机制可以在ssh client上配置,效果一样。

关于保活,还有个TCPKeepAlive配置项,打开也无妨,只是它们工作在不同层面,TCPKeepAlive保活的效果可能并不好,据说防火墙可能丢弃保活报文,似乎很少见到真正依赖TCP层保活机制的实现?!

有了X11Forwarding的加持,我们就可以通过SSH客户端直接运行对端主机上的GUI程序。这个功能在调试GUI程序的时候,异常的方便。

X11Forwarding yes
AllowTcpForwarding yes

后面又对此功能的详细介绍。

sftp是ssh服务器自带的一个功能,默认的配置和建议配置如下:

Subsystem sftp /usr/lib/openssh/sftp-server  # default
Subsystem sftp internal-sftp  # recommend

以上两行配置,任何时候只能保留一行。(用#注释掉其中一行即可)

使用internal-sftp有两个好处:

(1) 性能更好,在连接sftp的时候,系统不会fork出一个新的进程;

(2) 可以在sshd_config配置文件中,使用ChrootDirectory,Match,ForceCommand等指令来定义登录sftp用户的功能。

远程登录前可以显示给用户的信息,通过在Banner字段后指定一个文件来实现,文件内容不支持转义:

# Banner none  # this is default config
Banner /etc/issue.net

传统都是使用/etc/issue.net文件,具体参考:issue,motd文件

登录后是否显示Motd,由下面这条配置控制:

PrintMotd no

看到一个别人的故障说明:motd信息在登录的时候,显示了两次!因为sshd配置了PrintMotd yes,pam也配置了显示motd,去掉其中一个就OK。

SSH客户端的配置

OS系统一般都自带SSH客户端,Windows10系统除了可以在cmd窗口里直接使用ssh之外,还有很多独立的可以用来做SSH客户端的软件,比如我比较喜欢的MobaXterm。Putty也是一个非常著名的SSH客户端软件。

man 5 ssh_config

Windows7系统没有自带的ssh客户端,需要自己安装。Windows10系统有自带的,但你可以对版本不满意。搜索openssh for windows,或者访问https://www.mls-software.com/opensshd.html,装好后再cmd窗口就可以使用ssh客户端了。

# on my raspberry pi
$ ssh -V
OpenSSH_7.9p1 Raspbian-10+deb10u2+rpt1, OpenSSL 1.1.1d  10 Sep 2019

在使用ssh连接某台主机的时候,如果加上-v参数,会打印出非常多的debug信息,用以辅助调试链接方面的问题。可以使用多个v,显示出来的信息更多,最多3个。

$ ssh -l pi 192.168.2.107 -p 22
$ ssh pi@192.168.2.107:22

-l(小写L)跟户名,-p跟端口号;上面两种写法等效!如果不指定端口号,端口号默认使用22。如果不指定用户名,默认使用登录当前系统的用户名(Windows系统下基本上就是Administrator)。

如果你无法配置服务器,而正好这台服务器没有配置心跳,你可以配置客户端发起心跳。客户端配置文件如下:

$ sudo vim /etc/ssh/ssh_config  # different with sshd_config
# configurate items
TCPKeepAlive yes
ServerAliveInterval 30
ServerAliveCountMax 5

保持TCP连接,客户端每30秒发一次心跳给服务器,如果连续5次都收不到服务器的心跳回复,就断开连接。30和5可按需配置。

当sshd配置了Client保活心跳,同时又在Client配置了Server保活心跳,测试发现,这种情况下,只有配置间隔较小的心跳起作用

在Windows系统中修改ssh_config文件可能存在困难,可以使用将配置直接在命令行中输入的方式:

$ ssh -o ServerAliveInterval=15 \
-o ServerAliveCountMax=5 username@serverip -p port_number

如果sshd端没有配置ClientAlive,ssh客户端就可以在命令行使用ServerAlive配置项,他们的功能和效果是一样的。不推荐使用TcpKeepAlive,它在TCP层发挥作用,发送空的TCP ACK报文,这样的报文有可能会被防火墙丢弃。(TCP的KeepAlive机制似乎很少人使用)

ssh tunnel的保活,也是使用ClientAlive或ServerAlive配置项。

使用-o可以配置任意本需要在配置文件中配置的参数。

ssh通信是加密的,而且加密算法按什么优先级选择,可以自己设置,使用-c参数。

-c cipher_spec

Selects the cipher specification for encrypting the session.  

cipher_spec is a comma-separated list of ciphers listed in order of preference.  

See the Ciphers keyword in ssh_config(5) for more information.

我理解,如果-c参数后面只有一个加密算法,相当于指定了用此算法。用-o参数指定Ciphers配置的效果是一样的。

$ ssh -Q cipher

SSH Port Forwarding

SSH Port Forwarding,SSH端口转发,也称为Tcp Forwarding,其又分为本地端口转发(Local Port Forwarding),远端端口转发(Remote Port Forwarding)和动态转发(Dynamic Port Forwarding)。

SSH有Tunnel技术,这是个3层概念,可以用来实现SSH-Based V@P$N,而Port Forwarding是4层概念。

SSH提供了一个非常有用的端口转发功能。它能够将其他TCP端口的网络数据通过SSH链接来转发,并且自动提供了加密及解密服务。

打开SSH端口转发功能:

AllowTcpForwarding yes

本地端口转发

顾名思义,本地端口转发,通过访问本地端口,实现访问另一个通过SSH连接的远程端口。这个功能常用来正面穿透防火墙。

比如A和X在防火墙外,B和C在防火墙内,A和X不能直接访问B和C上的资源,幸运的是,A可以通过SSH连接B,此时,我们就可以通过SSH的本地端口转发功能,实现A任意访问B和C上的资源,也可以让X通过A实现同样的访问。如下图:

ssh_local_port_forwarding

A$ ssh -g -N -f -L 12345:remotehost:23456 username@B -p port

-L 参数是必须的,-g-f-N可选。

远端端口转发

如果说本地端口转发是正面穿透防火墙,那么远端端口转发就是反向穿透,从内向外打通一条TCP链路。这几乎是最简单的内网穿透的方案

A和X在防火墙外,B和C在防火墙内部,A和X不能访问防火墙内部,不幸的是,A和X也不能通过SSH连接B和C。幸运的是,B可以在防火墙内部通过SSH连接A,此时可以在B上发起远端端口转发,实现A可以任意访问B和C的资源。

ssh_remote_port_forwarding

B$ ssh -N -f -R 12345:host:23456 username@A -p port

这样,A就可以访问B或C了!但是X还是不能访问,默认A只在loopback上监听12345端口。要实现X也能够像A一样访问B或C,可以

A$ ssh -g -N -L 12346:localhost:12345 localhost

X访问A的12346,相同于访问A的12345,也就相当于访问B或C。

这个方法最好,默认A上的端口只在localhost上监听,如果A在公网,这是必须的。X建立加密链接到A,去访问A的localhost,整个链路都有加密。

GatewayPorts yes

这样默认remote port forwarding的端口,会在remote host的所有接口上监听。如果host在公网,相当于端口就直接暴露在公网了,慎用!

最后,推荐自研开源小工具report:轻量级的remote port forwarding工具

动态端口转发

不同于前两种端口转发,动态端口转发不限制访问的端口号,因此被称为动态。

$ ssh -D localhost:12345 username@remotehost -p port

在本地建立一个动态端口转发,访问本地的12345端口时,所有访问流量全部通过加密TCP链接,转发到remotehost,然后再转发出去。

内网穿透

多说一点内网穿透(Intranet Penetration)。

我给公司写过一个简单的定制化的内网穿透方案,在测试的时候,遇到了问题。当一个内网业务端口被映射到公网后,有可能对这个端口的访问,有多个TCP连接,比如典型的HTTP 80端口。因此,推到重来了一次:

  1. 映射出去的端口,要支持多连接;
  2. 公网先连接,然后再在内网发起连接;

这一版的难点,是要在一路TCP连接中,区分多路TCP的数据。这需要设计一个私有协议,在IO multiplexing的时候,要非常小心。(私有协议参考:用一路TCP传输多路TCP数据

很快代码就运行成功了,但做完之后,我发现这个功能,跟SSH的Remote Port Forwarding是何其相似呀!因此我说,一般情况下,SSH的Remote Port Forwarding,就是最简单最好用的内网穿透方案!只要是本地在内网可以访问的业务端口,都可以一键穿透。

动态端口的内网穿透

按前述方法,内网穿透只能是单端口的,即只能将内网中的某一个端口映射出去,能干什么取决于这个端口的能力。是否能够实现,内网穿透之后的动态端口转发的能力呢?可以的。

方法01

在内网,将一台Linux主机的22号端口先Remote出去:

$ ssh -fNCR 10055:localhost:22 username@yourdomain -p port

在yourdomain上,设置动态端口转发,目标端口是localhost:10055:

$ ssh -fNCD 0.0.0.0:10056 name2@localhost -p 10055

name2是内网Linux主机的username。在外网,将10056的访问映射到10055,10055对应内网的22,因此10056就相当于一个穿透到内网的动态转发端口。测试OK。

方法1的优点:动态端口的建立在外网主机上,在外网可以方便灵活控制。

方法02

可以将方法1反过来,在内网设置一个动态点,然后将内网的动态点remote出去。

方法2的优点:

  1. 所有的ssh命令输入,都在内网的某一个主机上完成;
  2. Windows主机也可以,现在默认都支持ssh client;

缺点就是方法1的优点。有的时候,这种灵活控制非常重要,涉及安全问题。

SSH Port Forwarding使用技巧

以上对SSH Port Forwarding的总结,肯定有一些不全面的地方,以后在使用过程中,再慢慢积累。

技巧01:多条SSH端口转发路径可以相互之间连接,组合形成一条更长的转发链路。

技巧02:在网速慢而且不主要传视频的情况下,可以使用-C参数,开启压缩传输,来提高速度!比如git clone的时候,能够从十几K一下子提高到几百K!

技巧03:SSH不管是Server还是Client,都可以配置链路保活心跳(间前面配置部分的说明),这个心跳配置对于端口转发也适用(抓包验证)。因此,不用太担心长时间没有数据的TCP链接的保活的问题。不过,还是有可能因为各种原因导致中间的TCP连接被中断,此时,可以试试autopass

SSH秘钥登录

使用秘钥登录的好处是免密,因此可以很顺畅的运行一些重要的脚本,还很安全!但需要知道,用SSH秘钥来实现免密,不是惟一的方法,也并不一定是最安全的方式!

被我长期忽视的SSH Client的配置文件:~/.ssh/config

ssh-keygen

在命令行直接运行ssh-keygen,回车几次,就可以在~/.ssh目录中生成两个以id_开头文件,一个私钥,一个公钥!

-t,选择加密方式,默认rsa

-b,秘钥长度,默认1024bit

-f,指定生成的文件名

-C,Comments,默认为hostname@username

ssh-keygen生产的public key文件格式,与openssl略有不同,前面有ssh-rsa前缀,后面有用-C生产的comments:

$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCtTajTMbZPSinFDPIvAMRkBdy03j52zXyLNUk9tsJaurS23eAW2Dr6qEiFRLEVfZ8Kh/KJwxdB8OnJlhWYSJWUQF0+8mxEZ6/tw7XHH3o5+WLcD5/W25tY8hEuAVqgUn4X0P3W4yuFhkEUawnKHQwFFvkOz0E7kJVsMCEeui0NKEKqEKftOUjdB1C/SVren9jmAr9ksqS3snzB5NKDGkk2OytrMQw3yqj054xyFQ81G+a1U7NHJiQf5RozvYxKWFwqIMoNZBOhf8EGZqKekoFvKtBJYFkMC8J8+G9SSx1su2+vsdZkbLAu5VnMgsDZxTaIpGO0PKkY+kUY8sZIRRu6MyGtk0EwTzZDH+E7pDgT/B/cOCKstVzUfg1gfWFKjssqNFm0U2qYq3Qnul5f5Qydgn1hBGP2qnFSqoPG5fH23JJmBXNlqchQzlbsgzLCCb/7JzXQM09mciF2jEgavZUh32ajw4n2qRZPs7s5ShwIUStX2gTpFXM66F+XM33R0I0= xinlin@K

指定具体哪个网站使用哪个private key,要修改~/.ssh/config

$ cat ~/.ssh/config
# jimin gitlab
Host 192.222.1.150
#HostName github.com
#User xinlin
IdentityFile ~/.ssh/id_rsa

identityfile,不是identifyfile....:)

ssh-copy-id

要把你的密钥复制到服务器上,使用 ssh-copy-id 命令。例如,如果我拥有一台名为 example.com 的服务器,那么我可以用这个命令把我的公钥复制到它上面:ssh-copy-id yourname@example.com,最后修改sshd配置文件,PasswordAuthentication no

SSH远程命令执行

在登录ssh的命令后面,跟上一组命令字符串,就实现了远程命令的执行。

$ ssh -l <name> <domain|ip> [-p <port>] <command>
$ ssh name@domain|ip [-p <port>] <command>
$ ssh -l <name> <domain|ip> [-p <port>] 'pwd; cd ~/repos;ls'

当需要远程执行需要交互命令的时候,比如top命令,需要使用-t参数,来显式地告诉ssh提供一个TTY:

$ ssh name@domain|ip [-p <port>] -t <command>

An often overlooked feature of ssh is the ability to run commands directly. ssh foobar@server ls will execute ls in the home folder of foobar. It works with pipes, so ssh foobar@server ls | grep PATTERN will grep locally the remote output of ls and ls | ssh foobar@server grep PATTERN will grep remotely the local output of ls.

本地有一个shell脚本,在远端的主机上执行:

$ ssh name@domain|ip [-p <port>] 'bash -s' < test.sh

用autopass自动输入ssh密码

这是我个人的开源项目,纯Python开发,专注于实现自动输入ssh的密码,以及sudo时的密码自动输入。具体请参考这里:autopass:自动输入ssh,scp,sudo的密码

SSH Tunnel

-- EOF --

本文链接:https://cs.pynote.net/net/202109201/

-- More --