# SSH 小窍门

* 原文链接：[SSH Tips and Tricks](https://freebsdfoundation.org/wp-content/uploads/2022/11/reuschling_practical_programmer.pdf)
* 作者：**BENEDICT REUSCHLING**

SSH 是一款多用途工具，几乎所有运行和管理 Unix 机器的人都会使用它，而且用户之间频繁地通过 SSH 登录。作为 SSH 的核心功能，安全加密的登录每天都能派上用场，但这个守护进程还能做更多事情。有很多用例，而我只会介绍我自己使用的一些用例，且非全部。我将从一些基本的加固选项开始，然后再讲一些更高级的用法。

## 登录选项

想要登录系统的 SSH 用户应使用 SSH 密钥而非键盘交互式认证。在理想情况下，应完全禁用后者（显示密码提示），以防止那些密码脚本小子不断尝试利用弱用户密码闯入安全防护不足的系统。在我为那些技术水平较低的用户运行的某些系统上（我曾试图让他们了解 SSH 密钥，但无济于事），我仍然不得不启用这种认证方式。尽管这令人遗憾，但我仍然可以对系统进行加固，限制登录的用户群，并强制某些用户不使用键盘交互式方法。下面我们逐一来看这些选项。

SSH 守护进程的配置文件是 `/etc/ssh/sshd_config`。你可以通过 **AllowUsers** 或 **AllowGroups** 指令来限制允许登录的用户。即使本地用户尝试登录并输入正确的密码，只要他们不在这些指令中列出的名单中，登录依然会被拒绝。在我的使用场景中，所有不懂技术的用户名都以“abc”开头，后面跟着各自的用户 ID。我们可以使用通配符来匹配这样的用户名，如下所示：

```ini
AllowUsers abc*
```

在重新启动守护进程以使这些更改生效之前，请使用 `sshd -t` 或者 `sshd -T`（用于更多测试）来检查 `sshd_config` 文件的有效性。这一行可以防止脚本小子尝试使用诸如 root、admin 等用户名进行登录。虽然这并非绝对安全，但它确实增加了一道额外的障碍。**AllowGroups** 指令对一组用户执行相同的限制。用户组在集中管理方面更为方便。你的新同事肯定会非常感谢能立即获得访问权限，而无需先到你的办公室请求进入服务器。如果他们被添加到该组中，他们就可以立即登录；否则，每次都需要修改 **AllowUsers** 行。当某人离开时也是如此，所以为自己着想，使用用户组来管理 SSH 访问，这样你就不会花费余生在管理整个公司都需要登录的系统的 SSH 访问权限上。

分别使用 **DenyUsers** 或 **DenyGroups** 命令也可以拒绝某些用户或用户组的登录。撇开愚人节笑话不谈，这对于在某些用户归还了唯一的服务器室钥匙（或从假期回来之前）时，限制他们访问系统非常有用。处理这些混合条目时，指令的处理顺序为：**DenyUsers**、**AllowUsers**、**DenyGroups**，然后是 **AllowGroups**。

还可以使用 match 语句限制单个用户的登录方式。这在功能上类似于 if 语句，待匹配发生，它们就会覆盖全局的 `sshd_config` 设置。文件后面进一步的 match 行可以撤销先前 match 块中所做的设置，但现在我们就保持这个例子简单。

上述系统中，同时使用键盘交互和公钥认证的场景由一个名为 monitoring 的特殊用户来监控。该用户会定期使用自己的专用 SSH 密钥登录，运行某些检查（例如磁盘是否已满？），并将结果反馈给中央监控服务器。由于一些检查会以提升的权限运行，若该用户通过键盘登录被攻破，基本上就意味着获得了 root 访问权限。因此，我们告诉 SSH 服务器在该用户登录时只允许使用公钥认证。match 语句如下所示：

```sh
Match User monitoring
 AuthenticationMethods publickey
```

其他用户仍然使用全局设置，但待出现 monitoring 用户，针对该情况的 AuthenticationMethods 设置就会被覆盖。match 语句的其他匹配条件包括 Group、Host、LocalAddress、LocalPort、RDomain（路由域）和 Address。在这里要小心，不要轻易信任用户声称来自的某个网络，因为这些信息可能会被伪造或重新路由。尽量选择匹配条件越少越好，以避免处理时间过长或者匹配过多项，从而失去匹配的目的。请注意，并非 sshd\_config 中的所有关键字都可以在 match 语句中进行更改，但很多是可以的。完整的列表可参见 sshd\_config(5)。祝你匹配愉快！

## 断开挂起的 SSH 会话

你是否曾遇到这样的情况？你远程登录到一台服务器正在工作，突然终端冻结，再也无法发送任何命令；或者，你合上笔记本电脑的盖子后，在另一个网络位置打开，却无法重新连接到原来的会话，导致你被困在终端上？我想你一定会点头同意，所以原因在于：服务器没有检测到可能发生的网络中断，因而无法重新建立连接。如果这让你感到烦恼，不妨看看 net/mosh 包，或许能对你有所帮助。

那么，你如何让冻结的 shell 恢复，或至少正确断开连接？尽管看上去你的 SSH 客户端与服务器之间已经没有任何通信，但服务器仍然能理解一些特殊命令。请在冻结的终端上输入以下命令序列：

```sh
Enter ~ .
```

这会发送一个特殊的中断命令，使服务器立即断开会话，将控制权返回给你的本地 shell 会话。在一个正在运行的会话中，试着按下回车键，然后输入波浪号字符（`~`）紧跟句点（`.`）。要动作快些。待成功，你很快就会把它记作一项良好习惯，哪怕只是为了向同事炫耀你的知识。

其他转义序列记录在 ssh(1) 的“ESCAPE CHARACTERS”部分。输入回车、`~` 和 `?` 的序列会显示出一个转义序列列表。请注意，并非每个 SSH 守护进程都支持所有转义序列，但依我经验，断开连接的功能总是相当可靠。

## 隐藏的登录脚本

你知道吗？每次用户成功通过 SSH 登录系统时，你都可以让一个脚本自动运行。默认情况下，实现这一功能的文件 `/etc/ssh/sshrc` 并不存在。当该文件被创建并设置为可执行后，SSH 守护进程会执行其中列出的命令。这个过程发生在读取完环境文件之后、用户的 shell（或指定命令）启动之前。

你可能会问，这有什么用？它可以为该用户运行自定义的初始化例程。例如，一个 shell 脚本可以利用用户登录时的用户名（可通过 `$USER` 获得），并确保该用户的主目录的访问权限和所有权仍然只限于该用户。如果主目录由于某种原因不存在，则会向标准错误（stderr）输出一条错误信息。实现这一功能的简单脚本可能如下所示：

```sh
#!/bin/sh
HOMEDIR=”/home/${USER}”
# Restore restrictive home directory permissions
if [ -d ${HOMEDIR} ]; then
 chmod 0700 ${HOMEDIR}
 chown ${USER}: ${HOMEDIR}
else
  echo “Home directory ${HOMEDIR} does not exist” >&2
fi
```

确保该文件具有可执行权限，就像任何其他 shell 脚本一样，这样下次有人登录时它就会被激活。其他环境变量也可供使用。请注意，这个脚本会在每次用户登录时运行——即便是通过 scp/sftp 进行文件传输时也会运行。所以不要放入那些执行时间较长的复杂代码，否则用户在完成登录过程前会一直等待。对于那些偷偷在后台进行的操作来说，这是个不错的方法，因为每个成功登录的用户都必须经过这个脚本。

## 廉价而有效的 VPN

虚拟专用网络（VPN）作为收费服务已经层出不穷。它们通过隧道传输并加密互联网上的数据，实现端点之间的安全连接。这帮助人们在疫情期间保持与办公室的联系，甚至在此之前，当支持人员需要在不合时宜的时间修复服务器以防止业务中断时也发挥了作用。对于某个随机的 Josephine 来说，上述收费服务使得她能够伪装成来自其他国家的网络连接，从而以更低的价格购买东西。这可能涉及机票，甚至是你最喜欢的流媒体服务上尚未发布的某个节目集。虽然这种方法并不总能奏效，但它无疑是一个方便的服务。

## SSH 在其中如何（以及何时）发挥作用？

每当我们处于一个不受信任的、未加密的网络中，而我们又不希望别人窥探我们的通信时，这种情况就会发生。这通常出现在火车站、机场、酒店和提供免费公共 WIFI 的图书馆等地。那里的连接通常没有（或加密得不够好）且在多个用户之间共享——这正是基于 SSH 的 VPN 解决方案的理想用例。这对我们来说不需要额外费用，因为我们拥有所有必要的工具。我们只需要一个可以合法登录的、在互联网上公开可达的主机。

具体来说，不是直接从源主机 O 连接到目标主机 D，而是让我们的流量通过主机 P 绕个小弯路。这就可以让网络数据包拥有一个不同的源地址。数据包将由主机 P 获取，并通过 SOCKS 代理转发给你，而不是直接来自你的原始主机 O。目标主机 D 将与主机 P 进行通信，处理所有请求，每个发送给 P 的应答或结果都会转发回 O。SOCKS 代理会允许你的浏览器发送和接收数据包，就像你直接浏览一个正常的网页一样。由于你与主机 P（代理）之间存在转发过程，所以可能会比你平时的速度稍慢，但为了隐藏我们的源地址以及获得额外的加密（而且是免费的），这是非常值得的。

## 下面是具体操作方法

打开一个新的终端，输入以下命令，将其中的 **sshproxyhost.example.com** 替换为你在互联网上的 SSH 主机：

```sh
$ ssh -vD8080 -fCN sshproxyhost.example.com
```

这看起来很复杂，所以我们来解释一下所提供的各个选项：

* **-v**：让 SSH 输出详细信息（verbose），在你熟悉整个过程后可以省略这个选项。刚开始时，它有助于调试流程，并且会输出你通常看不到的任何错误信息。
* **-D 8080**：这定义了用于转发的本地动态端口。在你的本地机器上，指定的端口（在此例中为 8080，你也可以选择其他未被其他守护进程占用的端口）会被打开为本地套接字，而其另一端则为与代理服务器建立的安全连接（在这个过程中创建该连接）。
* **-f**：将 SSH 进程放到后台运行，以便 shell 仍然可以接收其他命令。请注意，如果你需要输入密码来登录，这个选项可能不太适用。为此，你可以为这次连接生成一个 SSH 密钥（使用 ssh-keygen(1)），并通过 ssh-copy-id(1) 与代理主机交换密钥，以实现无密码登录。这个选项并非绝对必要，但在整个过程正常运行后非常有用。
* **-C**：对加密的 VPN 数据进行压缩。根据你的网络状况和处理器速度，这可能会降低或提高连接速度。你可以试验这个选项，如果在你只住一晚的那家条件较差的酒店服务下速度太慢，就将其移除。
* **-N**：SSH 默认期望在远程主机上运行一个交互式 shell，但对于我们的 VPN 来说并不需要。此选项会使 SSH 不打开终端，而仅转发端口——这正是我们所需要的。

这些选项的详细说明可以在 ssh(1) 手册页中找到。当使用 **-v** 选项时，一个典型会话会输出类似如下的信息：

```sh
OpenSSH_8.6p1, LibreSSL 3.3.6
debug1: Reading configuration data /Users/bcr/.ssh/config
debug1: /Users/bcr/.ssh/config line 1: Applying options for *
debug1: /Users/bcr/.ssh/config line 16: Applying options for sshproxyhost.example.com
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 21: include /etc/ssh/ssh_config.d/* matched no files
debug1: /etc/ssh/ssh_config line 54: Applying options for *
debug1: Authenticator provider $SSH_SK_PROVIDER did not resolve; disabling
debug1: auto-mux: Trying existing master
debug1: Requesting forwarding of dynamic forward LOCALHOST:8080 -> *
debug1: mux_client_request_session: master session id: 7
...
debug1: Connection to port 8080 forwarding to socks port 0 requested.
debug1: channel 3: new [dynamic-tcpip]
...
debug1: channel 8: new [dynamic-tcpip]
debug1: channel 7: free: direct-tcpip: listening port 8080 for sshproxyhost.example.
com port 443, connect from 127.0.0.1 port 58994 to 127.0.0.1 port 8080, nchannels 9
```

这确认了 VPN 已经建立。下面这行

```sh
debug1: Requesting forwarding of dynamic forward LOCALHOST:8080 -> *
```

这证明了 VPN 已经建立。该行展示了其工作原理：你连接到本地 8080 端口，然后（沿着箭头）建立了通往互联网那广阔而狂野世界（www）的连接，回复数据则沿着相反的方向发送回来。

你的浏览器（或任何需要使用此 VPN 隧道的应用程序）只需在连接设置中配置使用这个 SOCKS 代理。寻找类似“手动代理配置”的选项，将 socks 主机设置为“127.0.0.1”（即上文所述的 localhost），端口设置为 8080（或者你指定的其他端口），并选择 SOCKS5 代理（如果有此选项）。

就是这样。接下来，我们可以通过访问诸如 `https://www.whatismyip.com`（或类似网站，这些网站会显示你的公网 IP 地址）的服务来检查它是否有效。如果显示的是你在 SSH 命令中使用的主机 IP（在我的例子中是 `sshproxyhost.example.com`），则说明 VPN 正常工作。无论你接下来在浏览器中访问哪个网站，这些网站都会与这个特定主机建立连接、交换数据，并忠实地将流量转发给你。很不错，不是吗？

## 注意事项

只要 SSH 连接保持与目标系统的开放，VPN 隧道就处于建立状态。请确保在笔记本电脑休眠后重新建立隧道，因为长时间不活动可能会导致你被断开连接。如果你租用了一台服务器作为代理，或者由他人承担该系统的流量费用，请不要过度使用，因为这可能会增加成本。在这种情况下，这并非免费的解决方案，如果你经常使用，不如直接选择一个专业的 VPN 服务，该服务会为你提供全球多个服务器供选择。

另外，请注意，你并非完全隐形。作为代理服务器使用的 SSH 日志会记录你的登录信息。不要利用此方式进行任何恶意或有害的活动，否则，当你被抓住时，我们可不会把你的下一期《FreeBSD 期刊》寄往那个把你送进 Jail 的星球。

希望这些提示和技巧对你有所帮助，并能在你日常使用 SSH 时提供便利。务必查看客户端（ssh(1)）和服务器端（sshd(8)）的手册页，以获得更多信息。对于有关 SSH 的所有内容，我强烈推荐 Michael W. Lucas 的《SSH Mastery》一书，它将带来更加有趣和全面的阅读体验。

***

**BENEDICT REUSCHLING** 是 FreeBSD 项目的文档提交者，也是文档工程团队的成员。过去，他曾担任 FreeBSD 核心团队成员两个任期。他在德国达姆施塔特应用科学大学管理着一个大数据集群，同时还为本科生教授“Unix for Developers”课程。Benedict 也是每周 bsdnow\.tv 播客的主持人之一。
