本文旨在为中高级工程师与技术负责人提供一份关于堡垒机(Bastion Host)运维审计系统的深度技术剖析。我们将超越传统的功能介绍,从操作系统内核的 PTY/TTY 机制、网络代理模型等第一性原理出发,探讨一个现代、高可用的堡垒机系统在架构设计、核心模块实现、性能优化与高可用演进路径中的关键决策与技术权衡,目标是构建一个既安全又高效的统一基础设施访问入口。
现象与问题背景
在任何一家拥有一定规模服务器资产的公司,基础设施的访问管理都会迅速演变成一个棘手的混乱问题。当团队只有少数几位工程师和几台服务器时,直接使用 SSH 密钥对进行登录似乎简单高效。但随着业务扩张,服务器数量增长到成百上千,工程师团队也随之扩大,混乱的序幕就此拉开:
- 账号管理混乱:公用密钥(`id_rsa`)在团队内部传来传去,一旦发生人员变动,密钥回收成为巨大难题,甚至根本无法执行。每个服务器上都散落着大量已离职员工的公钥,构成严重的安全隐患。
- 权限失控:为了方便,工程师往往被授予目标服务器的 root 权限。这意味着任何一次误操作(例如 `rm -rf /`)都可能是灾难性的,而且缺乏最小权限原则的管控,内部安全风险剧增。
- 审计缺失:当线上服务出现故障或数据被篡改,溯源变得极其困难。谁在什么时间、在哪台服务器上、执行了哪些命令?没有集中的审计日志,事后追责几乎等同于“猜谜游戏”。这在金融、安全等合规性要求高的行业是完全不可接受的。
- 安全风险敞口:将成百上千台服务器的 22 端口直接暴露在公网或办公网络中,极大地增加了攻击面。任何一个 SSH 弱密码或协议漏洞都可能导致整个集群的沦陷。
这些问题的核心症结在于缺乏一个统一的、收敛的访问控制和审计层。堡垒机(Bastion Host),或称之为“跳板机”(Jumpserver),正是为了解决这一系列问题而诞生的架构模式。它作为所有运维流量的唯一入口,强制执行 认证(Authentication)、授权(Authorization) 和 审计(Auditing),即经典的 AAA 模型。
关键原理拆解
一个健壮的堡垒机系统并非简单的网络代理,其背后依赖于计算机科学中几个坚实的基础原理。作为架构师,理解这些原理是做出正确技术选型和设计的基石。
原理一:受控的“中间人攻击”(Man-in-the-Middle)模型
从本质上讲,堡垒机实现运维审计的核心,是对 SSH、RDP 等协议进行了一次“善意”的中间人攻击。传统的 SSH 连接是客户端到服务器的端到端加密。为了审计,堡垒机必须能够解密并解析流量。这是通过中断并重构连接实现的:
- 第一段连接:用户的 SSH 客户端连接到堡垒机。这是一个标准的 SSH 连接,堡垒机作为 SSH 服务端。
- 第二段连接:堡垒机在验证完用户的身份和权限后,作为 SSH 客户端,发起一个到目标服务器的新连接。
- 数据转发与嗅探:堡垒机在内存中维护着这两段连接的会话,并将数据从一段连接的输入流(InputStream)复制到另一段的输出流(OutputStream),反之亦然。正是在这个数据复制的过程中,堡垒机能够完整地记录所有交互命令和返回结果。
这种模型将原本不可见的端到端加密通道,变成了两个可被中间节点(堡垒机)完全控制和审查的独立通道,这是实现审计功能的根本前提。
原理二:操作系统的伪终端(Pseudo-Terminal, PTY)机制
当我们谈论录制一个交互式会话(Session Recording),比如一个 `vim` 操作或者一个 `top` 命令的动态输出,仅仅记录标准输入输出(stdin/stdout)的文本流是远远不够的。终端交互包含了大量的控制字符(Control Characters),用于控制光标位置、文本颜色、清屏等。为了完美复现会话,必须捕获这些原始的终端数据流。
这就要回到操作系统的终端子系统。在 Linux 中,每个终端会话都与一个设备相关联。物理终端是 TTY(Teletypewriter),而远程登录(如 SSH)使用的是伪终端 PTY。PTY 由一对虚拟设备构成:
- Master (PTM):主设备,由终端模拟器(在堡垒机场景下,就是堡垒机的代理程序)持有。写入 PTM 的数据会成为 Slave 端的输入,从 PTM 读取的数据则是 Slave 端的输出。
- Slave (PTS):从设备,用户的 shell 进程(如 bash)会与它进行交互,认为自己正与一个真实的终端对话。
堡垒机在与目标服务器建立连接后,会为该会话请求一个 PTY。之后,所有从目标服务器返回的数据流(包含了文本和控制字符)都会被写入 PTM。堡垒机的代理程序只需从 PTM 读取这个完整的数据流,就能精确地记录下整个会话的每一个细节。这就是操作录屏(Session Replay)的技术基石。
系统架构总览
一个生产级的堡垒机系统,需要考虑可扩展性、高可用性和组件解耦。我们可以将其抽象为以下几个核心组件,这并非一个物理部署图,而是一个逻辑功能划分图:
- 接入网关(Access Gateway):这是系统的入口,通常是高可用的集群。它负责处理用户的 SSH、RDP、VNC 等连接请求。它本身是无状态的,可以水平扩展。一个 L4 负载均衡器(如 LVS、HAProxy)会把外部连接分发到不同的网关节点上。
- 核心服务(Core Service):系统的“大脑”。它处理所有的业务逻辑,包括用户认证(可对接 LDAP/OAuth2)、资产管理、权限策略计算、审计日志索引等。通常以 API 服务的形式存在,供 Web 控制台和接入网关调用。
- Web 控制台(Web Console):提供给管理员和用户的图形化界面。管理员在这里配置用户、资产、授权规则;普通用户在这里查看自己有权限的资产列表,并可以下载会话录像、查看命令记录。
- 数据库(Database):用于存储系统的元数据,如用户信息、资产信息、权限关系、命令日志等。通常选用关系型数据库如 MySQL 或 PostgreSQL,因为这些数据具有强一致性的要求。
- 会话录像存储(Session Recording Storage):用于存放大量的会话录像文件。这些文件体积大,增长快,对低成本、高可靠、可扩展的存储有强烈需求。对象存储(如 AWS S3、MinIO)是此场景下的理想选择。
- 任务队列与工作节点(Task Queue & Workers):用于处理异步任务,例如录像文件的后期处理(压缩、转码)、审计报告生成等,避免阻塞核心服务。
整个工作流程如下:用户通过 SSH 客户端连接接入网关。网关调用核心服务的 API,进行用户认证和权限校验。校验通过后,网关建立到目标服务器的连接,并开始代理和记录会话。会话数据流被实时或准实时地发送到会话录像存储。同时,解析出的命令文本被写入数据库,以供快速检索。
核心模块设计与实现
在这里,我们用“极客工程师”的视角,深入几个关键模块的实现细节和坑点。
SSH 协议代理与会话拦截
这是堡垒机的技术核心。用 Go 语言来实现这个逻辑非常清晰。Go 的 `crypto/ssh` 包提供了强大的 SSH 协议支持,其并发模型也天然适合处理网络 I/O。
基本思路是启动一个 SSH 服务器监听用户连接,在身份验证回调中,再启动一个 SSH 客户端连接到后端真实服务器。然后,在两个连接之间“缝合”数据流。
// 简化版核心代理逻辑
func handleConnection(userConn net.Conn) {
// 1. 与用户建立 SSH 服务端连接
sshServerConn, chans, reqs, err := ssh.NewServerConn(userConn, serverConfig)
if err != nil {
log.Printf("Failed to handshake: %v", err)
return
}
defer sshServerConn.Close()
// ... 此处应有基于 sshServerConn.User() 的权限校验逻辑 ...
// 2. 作为 SSH 客户端连接到目标服务器
targetConn, err := ssh.Dial("tcp", "target-server:22", clientConfig)
if err != nil {
log.Printf("Failed to dial target: %v", err)
return
}
defer targetConn.Close()
// 3. 在用户和目标之间建立通道 (Channel)
// 这里只处理最常见的 session channel
for newChannel := range chans {
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
userChannel, userRequests, err := newChannel.Accept()
if err != nil {
log.Printf("Could not accept channel: %v", err)
return
}
targetChannel, targetRequests, err := targetConn.OpenChannel("session", nil)
if err != nil {
log.Printf("Could not open target channel: %v", err)
return
}
// 4. 核心:在两个 channel 之间双向复制数据,并进行拦截
go proxyRequests(userRequests, targetChannel)
go proxyRequests(targetRequests, userChannel)
go proxyAndRecordIO(userChannel, targetChannel)
}
}
func proxyAndRecordIO(userChannel ssh.Channel, targetChannel ssh.Channel) {
defer userChannel.Close()
defer targetChannel.Close()
// sessionRecorder 是一个自定义的 writer,负责将会话数据写入文件或对象存储
// io.MultiWriter 会把所有写操作同时分发到 targetChannel 和 sessionRecorder
// 这就是“嗅探”和“录制”的关键!
go io.Copy(targetChannel, io.TeeReader(userChannel, commandParser)) // user -> target, 同时解析命令
// 从目标服务器返回的数据,同时写给用户和录制器
// asciinemaRecorder 实现了 io.Writer 接口,将数据流按 asciinema 格式打包
recorder := NewAsciinemaRecorder("session-id.cast")
w := io.MultiWriter(userChannel, recorder)
io.Copy(w, targetChannel) // target -> user, 同时录制
}
工程坑点:
- Channel 类型处理:SSH 协议除了 `session`,还有 `direct-tcpip`(用于端口转发)等多种 Channel。一个完备的堡垒机需要正确处理或拒绝这些 Channel,否则可能导致客户端行为异常或留下安全后门。
- PTY 请求:客户端请求 PTY 时(`pty-req`),堡垒机需要向目标服务器透传这个请求,并协商一个兼容的终端类型(如 `xterm-256color`),否则 `vim`、`htop` 等程序的界面会显示错乱。
- 窗口大小变更:当用户调整本地终端窗口大小时,客户端会发送 `window-change` 请求。堡垒机必须捕获此请求并转发给目标服务器,否则远端的 shell 不会感知到窗口变化,导致命令行换行异常。
会话录制与回放
录制不仅仅是把字节流存盘。为了能在 Web 端流畅回放,我们需要一种结构化的格式。`asciinema` 是一个优秀的开源格式,它本质上是一个 JSON 文件,记录了每一帧输出的时间戳和内容。
一个录制文件 `session.cast` 的内容类似这样:
{"version": 2, "width": 120, "height": 30, "timestamp": 1678886400, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}}
[1.051254, "o", "Hello, world!"]
[2.345678, "o", "\u001b[1;31mThis is red.\u001b[0m"]
[3.123456, "i", "ls -l\r\n"]
这里的 `[timestamp, type, data]` 数组就是事件流。`type` 为 `o` 代表输出,`i` 代表输入。堡垒机的 `recorder` 组件就需要实时地将从 PTY Master 端读到的数据流,封装成这种格式。在 Web 端,使用 `xterm.js` 这样的前端终端模拟器,就可以精确地按时间戳“喂”给它这些数据,从而完美重现整个会话。
工程坑点:
- I/O 性能:会话录制是 I/O 密集型操作。如果同步写入磁盘或网络存储,可能会因为 I/O 抖动导致用户交互卡顿。必须使用内存缓冲区和异步 I/O 策略,将录制操作与实时交互流解耦。
- 存储成本:未压缩的录像文件非常庞大。一个活跃的运维团队每天可能产生几十 GB 的数据。必须在录制结束后,异步地对文件进行压缩(如 `gzip`),并设置生命周期策略,定期归档或删除旧的录像。
性能优化与高可用设计
当堡垒机成为所有运维流量的必经之路时,其自身的性能和可用性就变得至关重要。一个单点的、性能低下的堡垒机,会成为整个运维体系的瓶颈和风险点。
性能优化:
- 网关无状态化与水平扩展:将接入网关设计为完全无状态的节点。用户的认证信息、会话元数据等都通过调用核心服务 API 或查询外部缓存(如 Redis)来获取。这样,我们就可以在 L4 负载均衡器后面部署任意多个网关节点,线性提升并发连接处理能力。
- CPU 密集型操作卸载:SSH 的加解密是 CPU 密集型操作。选择合适的加密算法套件(Cipher Suite)可以在安全性和性能之间取得平衡。例如,`[email protected]` 通常比 `aes256-cbc` 性能更好。对于大规模部署,可以考虑使用支持硬件加解密加速的服务器。
- I/O 优化:前面提到的异步录制是关键。此外,对于命令审计,可以将解析出的命令先写入消息队列(如 Kafka),再由下游消费者慢慢写入数据库,削峰填谷,避免数据库成为写入瓶颈。
高可用设计:
- 网关层高可用:使用 `LVS/DR + Keepalived` 或云厂商提供的负载均衡器(LB),实现对后端多个网关节点的健康检查和流量分发。任何一个网关节点宕机,LB 会自动将其摘除,对用户透明。
- 核心服务层高可用:核心服务是无状态的 API,同样可以部署多个实例,通过服务发现机制(如 Consul, Nacos)注册,由网关或其他服务调用。
- 数据存储层高可用:这是最复杂但也最成熟的部分。数据库采用主从复制+哨兵(Sentinel)或高可用集群(如 MGR, Galera Cluster)。会话录像存储使用 MinIO 集群或云厂商的对象存储服务,它们天然具备高可用和数据冗余能力。
权衡分析(Trade-off):一个常见的权衡是在会话中断时的处理。如果一个网关节点宕机,当前通过它的所有 SSH 会话都会中断。实现会话迁移(Session Migration)的复杂度极高,需要跨节点同步 TCP 连接状态和 SSH 会话上下文,工程代价巨大。对于绝大多数场景,接受“节点宕机,会话中断,用户重连”这个设定是更务实的选择。堡垒机 HA 的主要目标是保证“入口可用”,而非“单个会话永不中断”。
架构演进与落地路径
对于一个从零开始引入堡垒机的团队,不必一步到位追求终极架构。一个分阶段的演进路径更为实际。
第一阶段:MVP – 解决核心痛点
- 目标:快速上线,解决账号、权限、审计的有无问题。
- 方案:选择一款成熟的开源堡垒机系统,如 Jumpserver。采用单机部署模式,将所有组件(Web, Core, Gateway)部署在同一台高性能服务器上。
- 重点:完成对核心服务器资产的纳管,制定初步的权限分配策略,让所有运维人员习惯通过堡垒机进行操作。这个阶段,可用性可以暂时依赖云服务器的快照和快速恢复能力。
第二阶段:生产化 – 追求高可用与稳定性
- 目标:消除单点故障,提升系统性能,满足大规模使用需求。
- 方案:进行组件化拆分部署。部署至少两个网关节点,并使用负载均衡器。将数据库、对象存储等替换为高可用方案(如 RDS、S3)。核心服务也进行多实例部署。
- 重点:建立完善的监控告警体系,覆盖所有组件的健康状态、系统负载、连接数等关键指标。与公司的统一身份认证系统(如 LDAP、AD)进行集成。
第三阶段:云原生与零信任 – 面向未来
- 目标:适应云原生时代的基础设施,向零信任安全模型演进。
- 方案:
- 容器化访问:堡垒机需要支持 `kubectl exec/logs` 等命令的代理和审计,成为 K8s 集群的统一访问入口。
- 数据库代理:将能力扩展到数据库协议(如 MySQL, PostgreSQL),实现对数据库操作的审计。
- Just-in-Time (JIT) 访问:与工单系统集成,实现临时、短期的权限授予。用户需要访问某台机器时,通过审批流程获得一个几小时内有效的访问凭证,而不是永久权限。
- 凭证管理:堡垒机可以与 Vault 等密钥管理系统集成,动态地为会话生成和注入临时访问凭证,彻底消灭静态密码和密钥。
- 重点:此时的堡垒机,已经从一个单纯的“跳板机”,演进为企业内部基础设施的统一“身份感知代理”(Identity-Aware Proxy),是实现零信任网络访问(ZTNA)架构中的关键一环。
通过这样的演进,堡垒机系统不再仅仅是一个被动的审计工具,而是主动的安全控制中枢,为日益复杂和动态的现代IT环境提供了坚实的安全基座。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。