Skip to content
Published on

云上的 bastion 是什么,在 AWS 上怎么用才好 — ProxyJump 实测、SSM Session Manager、EC2 Instance Connect Endpoint

分享
Authors

只有一扇门的楼

把服务器放进私有子网,互联网就够不到它。这正是目的。可是运维人员得进去。解决这个矛盾的经典答案就是 bastion host:在公有子网放一台小主机,对互联网开放的门只有它的 SSH 端口,其余服务器只接受来自 bastion 的连接。

用楼来比喻,就是把出入口减到一个,只在那扇门上安排门卫。门卫只有一个,出入记录也就集中在一处。代价是那扇门一旦被攻破,整栋楼都被攻破。所以 bastion 是要加固得最狠、打补丁最勤、记录最仔细的那台主机。

经典的 bastion 用一行 SSH 就能做出来

OpenSSH 内置了这个模式。ssh_config 手册对 ProxyJump 的说明如下。

Setting this option will cause ssh(1) to connect to the target host by
first making an ssh(1) connection to the specified ProxyJump host and
then establishing a TCP forwarding to the ultimate target from there.

先 SSH 到跳板机,再从那里向最终目标打开 TCP 转发。命令行上是 -J。我在家庭实验室里实际做了一遍:从我的 Mac(192.168.219.101)经 cubi01(192.168.219.120)进入 nuc1(192.168.219.116)。

ssh -J cubi01 nuc1 'hostname; echo $SSH_CONNECTION'

第一次失败了。

channel 0: open failed: connect failed: Temporary failure in name resolution
stdio forwarding failed

加上 -v 就能看到 ssh 在做什么。

debug1: Setting implicit ProxyCommand from ProxyJump: ssh -v -W '[%h]:%p' cubi01
debug1: Executing proxy command: exec ssh -v -W '[nuc1]:22' cubi01
Authenticated to cubi01 ([192.168.219.120]:22) using "publickey".
debug1: channel_connect_stdio_fwd: nuc1:22

nuc1 这个名字在我 Mac 的 /etc/hosts 里有,在 cubi01 上却没有(那里 getent hosts nuc1 返回为空)。目标的名字是由跳板机解析的,不是你的笔记本。 这是用 bastion 的人最先撞上的事实。改成 IP,并加上 HostKeyAlias 让 known_hosts 里按名字记录的条目继续生效,就能通过。

ssh -o HostKeyAlias=nuc1 -J cubi01 192.168.219.116 'hostname; echo $SSH_CONNECTION'
nuc1
SSH_CONNECTION=192.168.219.120 53994 192.168.219.116 22

和不经跳板直接连接时对比,差别就出来了。

SSH_CONNECTION=192.168.219.101 55330 192.168.219.116 22

目标服务器认为连接来自 bastion(…120)。 我 Mac 的地址(…101)哪里都没有。只看目标服务器的日志,只知道"cubi01 进来了"。到底是谁进来的,只存在于 bastion 的日志里。bastion 的审计日志必须保留并送到外部,理由就在这一行。

这个家庭实验室有五个节点在 192.168.219.0/24 内,从互联网开放的只有网关的 80(用于 ACME HTTP-01)和 443。运维命令都是在局域网内通过 ssh cubi01 进去执行的。也就是说 cubi01 是局域网内的管理节点,不是互联网可见的 bastion。从必须向互联网开放 22 端口的那一刻起才是真正的 bastion,从那一刻起上面的担忧全都成真。

自己运维 bastion 要背负的东西

  • 入站 22 端口。 对互联网开放的 SSH 整天被敲。开着的门本身就是攻击面。
  • 密钥管理。 有人离职,就得修改 bastion 和所有目标上的 authorized_keys。实际上很少有人做。
  • 打补丁。 bastion 是公有子网里的一台普通 EC2。内核或 OpenSSH 出了漏洞,它必须最先打补丁。
  • 日志盲区。 如上所见,目标只看到 bastion 的地址。bastion 上不记录会话,"谁做了什么"就消失了。
  • 单点故障。 bastion 挂了谁也进不去。可放两台,管理对象就变成两个。

AWS 几乎把这份清单原样写进了文档,并给出两个替代方案。

AWS 的答案 1:SSM Session Manager

Systems Manager 文档把 Session Manager 介绍为"无需开放入站端口、维护 bastion 主机或管理 SSH 密钥"就能管理节点的功能。实例里的 SSM Agent 向外 连接 Systems Manager 端点,运维人员顺着这条连接进去。一条入站规则都不需要。

aws ssm start-session --target i-0123456789abcdef0

权限全靠 IAM。谁能进哪台实例由标签或资源 ARN 决定,会话内容送到 S3 或 CloudWatch Logs,API 调用记进 CloudTrail。没有密钥文件,也就没有离职者的密钥要删。

端口转发也行。把实例的 80 端口拉到笔记本的 56789,

aws ssm start-session --target i-0123456789abcdef0 \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{"portNumber":["80"], "localPortNumber":["56789"]}'

或者以实例为踏板,拉到它后面的 RDS。远程主机不需要注册到 Systems Manager。

aws ssm start-session --target i-0123456789abcdef0 \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["mydb.example.us-east-2.rds.amazonaws.com"],"portNumber":["3306"], "localPortNumber":["3306"]}'

想继续用原来的 sshscp,在 ~/.ssh/config 里放一条 ProxyCommand 即可。节点的 22 端口仍然关着(文档原话是 "You can close inbound ports on the node")。

# SSH over Session Manager
Host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
    User ec2-user

这里有一个文档明确写出的陷阱。通过 SSH 或端口转发建立的会话不记录内容。 SSH 在 TLS 隧道里再加了一层加密,Session Manager 只充当隧道。"谁在何时连了哪台实例"在 CloudTrail 里有,"敲了什么命令"只有普通会话(只用 start-session)才会记录。若以审计为目的,可以用 IAM 拒绝经 Session Manager 的 SSH,文档给出了策略。

{
  "Effect": "Deny",
  "Action": "ssm:StartSession",
  "Resource": "arn:aws:ssm:*:*:document/AWS-StartSSHSession"
}

AWS 的答案 2:EC2 Instance Connect Endpoint

Session Manager 需要实例里有代理。对于装不了代理的实例,或者就想用普通的 SSH、RDP 的场合,AWS 提供了 EC2 Instance Connect Endpoint(EICE)。文档称之为 "identity-aware TCP proxy"。在 VPC 的某个子网里建一个端点,用 IAM 凭据认证的隧道就会从你的电脑打开到该端点,再从那里以 TCP 连到 VPC 内的实例。

文档里确认的性质如下。

  • 实例 不需要公网 IP,VPC 也不需要互联网网关。
  • 无论成功与否,所有连接尝试都记进 CloudTrail
  • 没有额外费用(连接另一个可用区的实例时只收跨 AZ 传输费)。
  • 每个 VPC、每个子网只能建一个,一个端点最多 20 个并发连接
  • 隧道 最长 1 小时,可用 IAM 策略的 maxTunnelDuration 条件强制更短。即使 IAM 凭据先过期,隧道也会维持到上限。
  • 只用于管理流量。大量传输会被限流。
  • 默认情况下实例看到的客户端是端点 ENI 的地址。开启客户端 IP 保留后能看到真实地址,但仅限 IPv4 且必须在同一 VPC。

用法只是换一条 SSH 的 ProxyCommand。

Host i-*
    ProxyCommand aws ec2-instance-connect open-tunnel --instance-id %h

或者让 CLI 代劳。

aws ec2-instance-connect ssh --instance-id i-0123456789abcdef0 --connection-type eice

Windows 实例用 --remote-port 3389 打开隧道,再把 RDP 客户端指向本地端口。

三者并排

EC2 bastion(自建)SSM Session ManagerEC2 Instance Connect Endpoint
对互联网开放的端口22无(代理向外连接)无(端点在 VPC 内)
实例上需要的东西带公网 IP 的 bastion + 目标的 SG 规则SSM Agent + IAM 角色无(只要密钥对)
认证SSH 密钥IAMIAM + SSH 密钥
连接记录bastion 的 sshd 日志(自行管理)CloudTrail + 会话内容(S3/CloudWatch)CloudTrail(所有尝试)
会话内容记录需要另外的工具仅普通会话,经 SSH/转发不行不行(TCP 代理)
上限每子网 1 个、并发 20、1 小时
费用EC2 实例费用无(跨 AZ 传输除外)
适合代理和端点都用不了的情况需要审计与命令记录的运维普通 SSH/RDP、没有代理的 AMI

在 AWS 上这样用才好

首选是 Session Manager。 没有入站端口、没有密钥、连命令都有记录。bastion 背负的五种负担全部消失。只需给实例装上 SSM Agent,并附加带 AmazonSSMManagedInstanceCore 权限的角色。私有子网没有互联网路由的话,放上 Systems Manager 的 VPC 端点(PrivateLink),代理就在 VPC 内连接。

需要 SSH 本身时用 EICE。 rsyncscp、IDE 远程开发、RDP 这类需要真正 TCP 连接的工作,就用 EICE 打开隧道,照常使用惯用的工具。记住每子网一个、并发 20、1 小时的上限,并用 maxTunnelDuration 绑得更短。

bastion EC2 是最后的选择。 若仍要放一台,就守住实测教给我们的两点。目标服务器只看到 bastion 的地址,所以 把 bastion 的 sshd 日志送到外部(例如 CloudWatch Logs),目标的安全组只允许来自 bastion 安全组的 22 端口。再给那台 bastion 也装上 SSM Agent,哪天关掉 22 端口也还有路可进。

一句话

bastion 是"把门减到一扇"的想法,在 AWS 上把这个想法实现得最好的已经不是 bastion 主机,而是 不开入站端口、凭 IAM 进入的 Session Manager。需要真正的 SSH 就用 EICE;自建 bastion 放在最后选,并且别忘了目标只看得到 bastion 的地址。