预置known_hosts如何防止中间人攻击(MITM)?(服务器公钥、服务器私钥)Runner、DNS污染、BGP劫持、同网络ARP欺骗、SSH握手

  • 为什么这条最容易被忽略、却最重要(指将服务器主机公钥配置到github上):每次 CD 都是一台全新的 runner,(Github)~/.ssh/known_hosts 是空的。如果不预置主机公钥,就只能用「首次连接即信任」------而每次部署都是一次「首次连接」,等于每次都开一个中间人窗口。有人劫持了到那个 IP 的连接,你的 runner 会毫无察觉地把私钥用于认证。预置之后,工作流里的 StrictHostKeyChecking=yes 才有意义:主机公钥对不上就直接断开。

预置known_hosts如何防止中间人攻击(MITM)?

文章目录

不是"伪造 IP"那么简单,是中间人攻击(MITM)

让我用一个具体场景解释:


你的 Runner 连接服务器的过程

复制代码
GitHub Runner (美国某个数据中心)
    │
    │  ssh deploy@your-server.com
    │
    │  第一步:DNS 解析 your-server.com → 1.2.3.4
    │  第二步:TCP 连接 1.2.3.4:22
    │  第三步:SSH 握手,服务器返回主机公钥
    │  第四步:Runner 用私钥认证
    │  第五步:执行部署命令
    │
    ▼
你的服务器 (1.2.3.4)

攻击者在哪里动手脚?

攻击者不需要入侵你的服务器 ,只需要在网络路径上做手脚:

场景一:DNS 污染

Runner 做 DNS 解析时,攻击者返回一个假 IP:

复制代码
Runner 查询:your-server.com 的 IP 是多少?
攻击者回答:是 6.6.6.6(攻击者的服务器)

场景二:BGP 劫持

攻击者在互联网路由层面宣告"我能到达 1.2.3.0/24 这个网段",把流量引到自己那里。这是真实发生过的攻击,Cloudflare、AWS 都遭遇过。

场景三:同网络 ARP 欺骗

GitHub 的 Runner 跑在共享数据中心,如果同网络有恶意机器,可以声称自己是网关,截获 Runner 的出站流量。


攻击成功后会发生什么?

复制代码
Runner                          攻击者 (6.6.6.6)                你的服务器
  │                                  │                              │
  │── SSH 连接 ─────────────────────▶│                              │
  │                                  │                              │
  │◀─ 返回攻击者的主机公钥 ──────────│                              │
  │                                  │                              │
  │   (StrictHostKeyChecking=no)     │                              │
  │   "管他呢,接受!"                 │                              │
  │                                  │                              │
  │── 用私钥签名认证 ───────────────▶│  ← 攻击者看到你用哪个密钥、
  │                                  │    哪个用户名、部署什么代码
  │                                  │                              │
  │◀─ 攻击者假装是你的服务器 ────────│                              │
  │   "部署成功!"                    │                              │
  │                                  │                              │
  │   Runner 以为部署成功了           │  实际上代码根本没到你的服务器
  │   开心地结束了                    │

最坏的情况是什么?

攻击者不只是"看",他可以:

攻击者能做的 后果
看到部署的代码 泄露你的源码
看到你用的 SSH 密钥名和用户名 为进一步攻击收集信息
返回假的"部署成功" 你以为上线了,其实服务器没更新
执行恶意操作后假装成功 比如在你的服务器上留后门,然后告诉你"部署正常"

注意:SSH 协议本身不会直接泄露你的私钥(私钥只在本地签名,不会传输)。但攻击者能看到所有其他信息,而且如果你的服务器配置了密码认证,密码会被捕获。


预置 known_hosts 后怎么防住的?

复制代码
Runner                          攻击者 (6.6.6.6)
  │                                  │
  │── SSH 连接 ─────────────────────▶│
  │                                  │
  │◀─ 返回攻击者的主机公钥 ──────────│
  │                                  │
  │   (StrictHostKeyChecking=yes)    │
  │   对比 known_hosts:              │
  │   期望: AAAAC3NzaC1lZDI1...      │
  │   实际: BBBBD4NzbC1lZDI1...      │
  │                                  │
  │   "公钥不匹配!断开连接!" ❌      │
  │                                  │
  │   部署失败,你收到告警             │
  │   攻击者什么也得不到               │

(注意:因为服务器公钥是公开的,即使攻击者拿到了服务器真实公钥,与Github known_hosts中的服务器公钥对比后一致,后续也会验证服务器私钥,若私钥不匹配,仍会断开连接)


总结

复制代码
没有预置 known_hosts:
  Runner 对任何自称是你服务器的机器都信任
  = 在公共网络上裸奔

预置 known_hosts + StrictHostKeyChecking=yes:
  Runner 只信任持有特定公钥的机器
  = 即使网络被劫持,也能立刻发现并断开

这不是理论风险。2024 年就有安全研究员演示过通过 DNS 劫持拦截 CI/CD 管道的 SSH 连接。只是大多数人没被攻击过,就觉得"不会发生在我身上"。

在预置known_hosts过程中,服务器私钥也要参与

在这个过程中,服务器的私钥也用到了 ,而且起着决定性的作用。只是它永远留在服务器本地,不会传输到网络上,所以你在 Runner 端感知不到它。

如果只用公钥,这个安全机制就形同虚设了。下面解释为什么必须用到服务器私钥。


为什么必须用到服务器私钥?

公钥是公开的,谁都能复制。

假设验证过程只用公钥(比对字符串):

复制代码
Runner 的 known_hosts 里存着:你的服务器公钥 A

攻击者做的事:
1. 连上你的服务器,把公钥 A 抄下来
2. 放在自己的钓鱼服务器上
3. Runner 连过来时,攻击者把公钥 A 发给 Runner
4. Runner 比对:哇,一模一样!信任!

如果这样,攻击者轻易就能冒充你的服务器。

所以必须用私钥来"自证清白"。


SSH 握手时的真实过程(公钥与私钥的配合)

在 SSH 握手阶段,服务器不仅要把公钥发给 Runner,还要证明自己是这个公钥的真正主人

复制代码
你的服务器                                  GitHub Runner
    │                                          │
    │── 1. 发送:这是我的服务器公钥 ───────────▶ │
    │                                          │
    │── 2. 发送:这是我用「服务器私钥」           │
    │      对刚才的握手数据做的签名 ──────────▶  │
    │                                          │
    │                                     3. 验证:
    │                                     用 known_hosts 里的「服务器公钥」
    │                                     去解密/验证这个签名。
    │                                          │
    │                                     ├── 验证通过 ✅ 
    │                                     │   说明对方确实持有对应的私钥
    │                                     │   身份确认!
    │                                     │
    │                                     └── 验证失败 ❌ 
    │                                         说明对方只有公钥,没有私钥
    │                                         是冒牌货!断开连接!

结论:

  • 服务器私钥 :在服务器端用于签名,证明"我是我"。
  • 服务器公钥 :在 Runner 端(known_hosts)用于验证签名,确认"你是你"。

理清容易混淆的"两对密钥"

在这个 CI/CD 部署场景中,其实有两对密钥在同时工作,很多人会把它们搞混:

第一对:服务器主机密钥(Host Key)------ 证明服务器身份
  • 作用:防止中间人攻击(就是你问的这个)。
  • 服务器私钥 :存在服务器的 /etc/ssh/ssh_host_ed25519_key,用于签名。
  • 服务器公钥 :存在 Runner 的 ~/.ssh/known_hosts,用于验证。
第二对:用户登录密钥(User Key)------ 证明 Runner 身份
  • 作用:让服务器知道"这个 Runner 有权限登录并部署"。
  • Runner 私钥(部署私钥):存在 GitHub Secrets,Runner 用它签名,证明自己有权限登录。
  • Runner 公钥(部署公钥) :存在服务器的 ~/.ssh/authorized_keys,服务器用它验证 Runner 的身份。

总结

密钥 存放位置 作用 是否离开本机
服务器私钥 你的服务器 /etc/ssh/ 签名握手数据,证明服务器身份 ❌ 绝不离开服务器
服务器公钥 Runner known_hosts 验证服务器签名 ✅ 提前配置到 GitHub Secrets
部署私钥 GitHub Secrets 签名登录请求,证明 Runner 权限 ❌ 绝不离开 Runner
部署公钥 你的服务器 authorized_keys 验证 Runner 登录权限 ✅ 手动添加到服务器

所以,服务器的私钥不仅用到了,而且是整个"防伪造"机制的核心。没有它参与签名,公钥比对就只是一场毫无安全意义的字符串匹配游戏。

相关推荐
Lonely 净土2 小时前
Rocky Linux 安装教程
linux·运维·服务器
liulilittle5 小时前
UCP 控制协议 [简述]
网络·网络协议·计算机网络·udp·ip·通信
Lust Dusk6 小时前
记一次Linux应急响应题目解析
linux·运维·服务器·网络·安全·网络安全
凌肖战8 小时前
TCP网络通信中setsockopt()函数解决地址已占用问题
服务器·网络·tcp/ip
Yan_chen6668 小时前
SSH(Secure Shell)安全外壳协议详解
运维·网络协议·ssh
隔窗听雨眠8 小时前
OceanBase旁路导入与自增主键冲突深度解析:原理、排查与解决方案
java·服务器·数据库
rcms152702692188 小时前
MKS PDR-C-2C-BCD 电源模块
网络
LATASA8 小时前
【 从0到1构建 Agent Harness学习笔记】
网络·笔记·学习
数据知道9 小时前
SQL 注入从入门到精通:手工注入 + sqlmap 自动化
网络·数据库·sql·网络安全·自动化