NAT 后的机器怎么统一纳管:受限密钥 + 正反向 SSH 隧道的舰队接入实践

举报
云上复盘笔记 发表于 2026/10/31 06:59:09 2026/10/31
【摘要】 手里有多台分布在各地的云主机时,最容易被忽视的不是"能不能连上",而是连上之后你到底把多大的权限交了出去。一台管理中枢要采集所有节点的状态、偶尔执行受控动作,常见做法是在每台机器上放同一把私钥——这把钥匙一旦泄露,等于把整个舰队一次性交出去。另一种更常见的问题是拓扑:并非每台机器都有公网 SSH。位于家庭宽带、企业内网或 NAT 网关后的节点,中枢根本连不进去。本文给一套落地方案:任何一把密...

手里有多台分布在各地的云主机时,最容易被忽视的不是"能不能连上",而是连上之后你到底把多大的权限交了出去。一台管理中枢要采集所有节点的状态、偶尔执行受控动作,常见做法是在每台机器上放同一把私钥——这把钥匙一旦泄露,等于把整个舰队一次性交出去。

另一种更常见的问题是拓扑:并非每台机器都有公网 SSH。位于家庭宽带、企业内网或 NAT 网关后的节点,中枢根本连不进去。

本文给一套落地方案:任何一把密钥只做一件事,有公网的机器走正向隧道,NAT 后的机器走反向隧道,配合一条 iptables 链统一收口监控端口。文中的端口均为示例规划,可整体平移。

一、总体拓扑:两类通道

                    ┌──────────── 管理中枢(hub 所在) ────────────┐
 公网节点 A ──正向隧道─▶│ -L 本地端口转发   ──▶ hub 容器            │
 公网节点 B ──正向隧道─▶│                                          │
 公网节点 C ──正向隧道─▶│  socat 桥接(docker 网关侧)◀── 反向隧道 ◀── NAT 节点 1 │
                    │                                          │
                    │  fleet worker ──▶ 按机钉钥 SSH ─▶ 各机 agent │
                    └──────────────────────────────────────────┘

差异只有一句话:谁主动连谁。

机器类型 通道方向 谁主动 中枢侧暴露面
有公网 SSH 的节点 正向隧道 中枢 → 节点 -L 本地转发,绑定 docker 网关地址
NAT 后的节点 反向隧道 节点 → 中枢 authorized_keys 里 permitlisten 限定端口 + socat 桥接

二、端口规划:回环端口和网关端口是两套编号

这是最容易埋雷的一节。规划时把下面两件事分开记:

  • 回环端口=这台机器真实的 sshd / agent 监听口,由 RemoteForward 在中枢本机回环上开出来;
  • 网关端口=hub 容器实际能访问到的映射口,由 socat 或 -L 转发提供。

二者编号独立,只有桥接那一刻才对上。示例规划如下(端口可整体替换):

用途 中枢侧地址 说明
hub 自身 agent 网关:19001 hub 容器经 docker 网关访问宿主机 agent
公网节点 A/B/C 探针 网关:19002–19004 ssh -L 本地转发
NAT 节点 1 探针 / SSH 127.0.0.1:19005 / 19006 反向隧道 RemoteForward 回环监听
NAT 节点 1 探针桥接 网关:19007 socat → 127.0.0.1:19005

三、受限密钥:一把钥匙只做一件事

每机一对密钥,只存放在中枢 /etc/fleet/tunnels/<node>_ed25519,并配套 <node>_known_hosts 钉死主机指纹。节点侧的 authorized_keys 是整套方案的权限边界:

from="127.0.0.1",restrict,command="sudo -n /usr/bin/python3 /usr/local/libexec/fleet-agent.py" ssh-ed25519 AAAA... fleet-<node>

四个限制各有分工:

  • from="127.0.0.1":只允许经隧道进来的连接。正向隧道的落点是节点本机回环,反向隧道同理,因此公网扫描根本碰不到这个入口。
  • restrict:一次性禁掉 pty、端口转发、X11、agent 转发。
  • command=:无论客户端传什么命令,服务端只执行那一个 agent 脚本。这是"给密钥不等于给 shell"的关键。
  • sudoers 只放行 NOPASSWD 执行这一个脚本路径,并用 env_keep 透传 SSH_ORIGINAL_COMMAND。

上层协议:白名单分发

-agent 解出 SSH_ORIGINAL_COMMAND 后按动作白名单分发,例如 status / logs / restart / network_probe / speedtest。三个必做的自检点:

  1. 动作白名单在 agent 内二次校验,不是靠调用方自觉;
  2. 参数再校验,比如测速的 mb 上限锁在 50,超过即拒;
  3. 所有输出过脱敏函数(密钥、token、邮箱等模式擦除)并限长——否则一次 logs 就可能把凭据打进你的日志系统。

此外,agent 每次调用自记账:追加一行 JSON(时间/agent/动作/结果/摘要)到本机日志,控制台可回看。"任何一次远程执行都可追溯"这句话,只有在设计时就有日志才算数。

四、反向隧道:让 NAT 后的机器自己走出来

NAT 节点上运行一个常驻服务,开机自连中枢:

ExecStart=/usr/bin/ssh -N -T -F reverse-ssh.conf hub-entry

# reverse-ssh.conf 关键行
RemoteForward 127.0.0.1:19005 127.0.0.1:19001   # 中枢 → NAT 节点探针
RemoteForward 127.0.0.1:19006 127.0.0.1:22      # 中枢 → NAT 节点 sshd
ExitOnForwardFailure yes
ServerAliveInterval 20

中枢侧对这把"反向密钥"的授权行,要用 permitlisten 把转发端口钉死:

restrict,port-forwarding,permitopen="127.0.0.1:1",
permitlisten="127.0.0.1:19005",permitlisten="127.0.0.1:19006",
command="/usr/bin/false" ssh-ed25519 AAAA... reverse-<node>

效果很直接:这把密钥除了开这两个转发端口,什么都做不了,连 shell 都没有。 即使这把密钥被人拿到,攻击者也只能"替你转发流量",拿不到中枢权限。

端口撞号:一个真实踩过的坑

给第二台 NAT 节点规划回环端口时,如果复用了某个编号,而中枢上另一台公网节点的正向转发已经占用了网关侧的同号端口——注意,回环地址和网关地址不同,两边互不冲突,但桥接层会撞。

结论有三条:

  • hub 经 docker 网关访问 NAT 节点探针时,桥接端口必须用网关侧独立编号,用 socat 转发到回环真端口;
  • 改了 hub 数据库里的 agent 端口后,必须重启 hub 进程才会重新解析,改配置不重启=白改;
  • 排障顺序固定:ss -tlnp 看监听归属 → 回环监听(sshd RemoteForward)→ 网关桥接(socat / -L)→ hub 数据库的端口字段。

五、防火墙:一条链管住所有监控端口

不要在 INPUT 上堆零散规则,用一条自定义链 + 端口区间覆盖:

iptables -N FLEET_AGENTS
iptables -A FLEET_AGENTS -s 127.0.0.0/8 -j ACCEPT        # 本机
iptables -A FLEET_AGENTS -s <docker 网段> -j ACCEPT       # hub 容器网段
iptables -A FLEET_AGENTS -j REJECT                       # 其余全拒
iptables -I INPUT 1 -p tcp --dport 19001:19010 -j FLEET_AGENTS

三个实践要点:

  1. 端口用区间规则一条覆盖,扩容时改范围而不是加规则;
  2. 用 oneshot systemd 单元在每次开机重放(规则不持久化的发行版全靠它自愈);
  3. 该单元若手工重启要留意依赖顺序——带 Requires= 依赖它的探针单元会被连带停掉。做法是在单元里加 ExecStartPost=systemd-run --on-active=3 systemctl try-restart <agent>,延迟触发避免死锁。

六、新机纳管:三段式 enroll

重复劳动一定要脚本化,纳管一台新机固定三步:

  1. 引导:用服务商网关或带外通道(一次性)把 agent 脚本、隧道配置、密钥对、authorized_keys、sudoers 装到机器上,全过程先备份再原子替换;
  2. 中枢授权:追加节点密钥授权行(先按注释标签去重旧行)、写 known_hosts、重建网关桥接、刷新防火墙端口范围;
  3. 起服务:节点侧 enable + start 探针与反向隧道,中枢侧验证监听与 agent 应答。

参数化之后(基准端口、机器名),同一套脚本可以反复用。

七、故障速查

症状 先查 常见根因
派发返回 Connection refused 中枢 ss -tlnp 回环端口 反向隧道没连上(节点服务停了 / 重启后没起来)
派发返回 publickey 拒绝 节点 authorized_keys 标签行 中枢侧更新时旧行被顶掉,或重复行残留
hub 显示节点 down 桥接端口监听 + hub 库里的端口字段 撞号 / 改端口未重启 hub / 防火墙范围没覆盖
隧道频繁重连 节点 journal ExitOnForwardFailure 撞上端口占用,或心跳参数过激
agent 返回统一错误 JSON SSH_ORIGINAL_COMMAND 编码 协议前缀 / base64 不符——通道是通的,是协议层被拒

最后一条最有诊断价值:能返回统一的错误 JSON,说明网络、密钥、隧道全都通了,问题一定在应用层协议,不用再去翻防火墙。

小结

整套方案的核心不是"用 SSH 打通网络",而是三个约束:密钥按机隔离且 command= 钉死、反向密钥 permitlisten 限定、监听端口只对本机与容器网段开放。做到这三点,即使某一台机器的凭据泄露,爆炸半径也止于那台机器的那一个动作。

下一篇讲同一个控制面的前端工程:没有构建框架的情况下,怎么做单文件 bundle、版本缓存纪律和多皮肤移动端适配。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。