承诺每位订阅者一台设备的隧道服务,很少能从 MaxSessions 那里得到这个承诺,第一次测试通常就能证明这一点。订阅者再开一个 ssh 进程,第一个会话照常运行,两条隧道都在承载流量。每个配置项统计的东西各不相同,而且没有一个统计设备。先把承诺说清楚,才能看出该在它们之间怎么选。

每个选项实际统计的是什么

MaxSessions 限制的是一条已认证网络连接所承载的打开会话数。这里的会话指 shell、exec 请求、SFTP 之类的子系统,或是在一条 SSH 传输内复用的类似通道。第二次 ssh 调用会打开第二条传输,重新认证并获得自己的配额。第一条连接的会话数对它没有意义。

MaxStartups 限制的是尚未认证的并发连接,以起始阈值、速率和满额阈值表示。它存在的目的,是让半开握手的突发无法耗尽守护进程在认证前的资源。客户端一旦完成认证,那条连接就不再计入 MaxStartups,而变成某人的会话。它从来不是按用户的配额,调高它也不会让订阅者多做一点事。

设备登记规则根本不是 sshd 的选项。它属于任何负责签发凭据、追踪哪台设备持有凭据的系统,也就是说它需要登记、清点和吊销,而不是一个计数器。把这项要求混进 sshd 配置,正是大多数这类设计卡住的地方。

MaxSessions 从哪里开始不再是配额

Match User 块会改变哪些配置值适用于该用户,所以它是为服务账户设定更严格 MaxSessions 的自然位置。它做不到的是跨连接汇总,因为这个选项是按每条传输分别评估的。两条传输各自获得自己的限额,用户最终会拿到这个块允许值的两倍。

转发是另一个意外。TCP 转发不需要会话通道,所以 MaxSessions 0 可以禁止 shell 和子系统,却仍然允许 -L 和 -R 隧道。只做转发、并把会话数当作隧道配额的服务,什么也衡量不了。这一行为记录在OpenSSH 服务器配置参考中,在围绕某个选项名做设计之前值得一读。

输出实际生效的配置,而不是你以为已经加载的文件。include 和 drop-in 目录经常意味着生效值并不是你改过的那个值。

MaxStartups 守护的是守护进程,不是订阅者

MaxStartups 的三段式描述的是:在守护进程开始丢弃一定比例的新连接之前,可以存在多少条未认证连接。它的默认形态之所以存在,是因为未认证的 SSH 连接开起来便宜、留着贵。这是面向服务器的资源保护控制,而且同时适用于所有连接。

在登录风暴期间调高它,是用守护进程保护换准入,有时是正确取舍,而且永远是刻意的取舍。对想要第二条隧道的订阅者毫无帮助,因为第一条隧道已经认证过了。把两者混为一谈,会导致一次既治错症状、又同时削弱服务器的改动。

当登录被成批拒绝时,先看守护进程日志里认证前的消息,再动任何一个选项。这条消息会指出是哪一道防护被触发,而这些防护不能互换。

按用户的槽位需要共享状态

真正的单活跃传输策略需要一份存放在 sshd 之外的预留。准入这一步必须以原子方式占用账户槽位,在连接关闭时释放,在客户端不告而别时让它过期,并决定新的尝试是被拒绝还是替换旧的。最后一个决定是产品决定,而不是技术细节,因为替换一条活跃隧道会切断某个人的连接。

这份预留还必须由每一个接受该账户的服务器共享。两台各自维护本地计数器的服务器会执行两套独立的限额,而从另一台网关重连的订阅者会免费拿到第二个槽位。无论用什么存储状态,它都需要自己的故障行为,因为存储不可达时不能把所有订阅者都锁在外面。

要兑现一台设备的承诺,就为每台已登记设备签发独立凭据,并在那一层控制登记与吊销。被复制的私钥照样能从任何地方通过认证,所以给每位用户只发一把钥匙,并不能证明只有几台机器持有它。

用两条真实连接检验策略

先以命令的形式写出验收测试,再写策略。五个用例是:第一条传输活跃时再开第二条、一条传输内多个通道、只做端口转发的客户端、链路断开后重连,以及同一个账户出现在另一台网关上。

先读实际生效的限额,再打开策略本该拒绝的连接。这个测试的两半都重要,任何一半都不能替代另一半。

sshd -T | grep -iE "maxsessions|maxstartups|allowtcpforwarding|permitopen"
ss -tn state established '( sport = :22 )' | head
ssh -N -L 8080:127.0.0.1:80 [email protected]

在两个终端里各运行一次隧道命令,数一数 22 端口上建立的连接。如果策略声称只有一条活跃传输,而两条都在承载流量,那这份策略只是文档,不是执行。要在准入层补上这个缺口,而不是调低 MaxSessions,指望第二条传输会自己注意到。

相关文章