一个安全的 Web-Console 的实现思路
使用 xterm.js+Go-Gin 实现 Web-Console 的 SSH 登录
该文转自:熊喵君的博客
0x00 基础
本文将描述如何实现一个具备安全认证的 WebConsole,基于 Golang-SSH 库 实现。WebConsole 的核心实现是打通了 WebSocket+
SSH 的输入输出流,使得用户直接使用浏览器就可以运行 SSH 终端,非常适合于轻便运维的场景。WebSocket 基于 TCP 传输协议,并复用 HTTP 的握手通道,关于 WebSocket 和 Golang 的开发可以参见:How to Use Websockets in Golang: Best Tools and Step-by-Step Guide。
0x01 WebConsole 数据流
一个具备远程登陆的功能的 Web-Console,其数据流向大概如下:
User<--->
Browser<--->
WebSocket<--->
SSH<--->
(TTY)RemoteServer
|
|
数据流
中间的 Proxy 代理层,负责将 websocket 流转换为 SSH 流(核心是输入和输出的转发):
0x02 实现
作为一个 SSH 远程登陆系统,认证是及其重要的一环,我们将上面的数据流扩展下,加入必要的身份及票据认证,如下图
组件
- CGI+WEB,采用开源的框架 Gin 实现
- Websocket
- SSH
- 认证我们采用临时(一次性)Token 兑换真实 Token(如 SSH 证书 / 秘钥 / 口令等)的方式,这种方式简单易理解,当然了也可以使用
OAuth2/OpenID
这种开放认证协议
基本实现流程
-
用户 A 申请某一台机器的登录权限,后台服务返回一个一次性 token 构造的 url 给用户,如
https://1.2.3.4/cgi-bin/webconsole/check?token=token1
(这里假设以 GET 方式请求) -
在 IOA 认证(为了获取合法用户的身份信息,或者
Oauth2
认证)的前提下,用户使用浏览器访问上述登录 url, 后台服务先校验用户cookies
及 HTTP 签名,然后再校验token1
是否合法(使用次数 + 有效时间) -
当前一步的认证通过后,后台服务返回 Websocket 的地址,再加上另一个一次性票据,如
token2
,如ws://1.2.3.4/cgi-bin/webconsole/login?token=token2
,改地址返回给用户端浏览器 -
当 Websocket 请求(上一步的Response)到达后端,服务校验票据
token2
,校验通过后,后台将 HTTP 请求升级为 WebSocket 协议, 后续的数据交换则遵照 WebSocket 协议(这里就得到一个和浏览器数据交换的连接通道) -
后台服务使用
token2
换取真实票据后, 与远端 Server 建立一个 SSH Channel。然后后台将终端的大小等信息通过 SSHChannel
请求远程主机创建PTY
, 请求启动默认Shell
-
后台服务通过 Socket 连接通道获取用户(键盘)输入, 再通过 SSH
Channel
将输入传给PTY
,PTY
将这些数据交给远程主机处理后按照前面指定的终端标准输出到 SSHChannel
中 -
后台服务从 SSH
Channel
中拿到按照终端大小的标准输出后又通过 Socket 连接将输出返回给浏览器,至此一个 Web Terminal 建立成功
0x03 一些代码细节
升级 TCP 连接为 SSH 连接
对 Tcp 连接进行升级,这是 Golang 中非常常见的做法:
|
|
升级 HTTP 连接为 Web Socket
定义常量,web socket 升级器:
|
|
SSH 的层次结构 Client/Channel/Request
下图直观展示了 SSH 的架构:
- Client: 实现了 SSH 抽象的客户端
- Channel 和 Request:
这二者是 SSH 协议里面的链接层, 该层主要是将多个加密隧道分成逻辑通道,通道可以复用
常见通道类型有:session
、x11
、forwarded-tcpip
、direct-tcpip
。通道里面的 Requests
是用于接收创建 SSH Channel
的请求的,而 SSH Channel
就是里面的 Connection
, 数据的交互是基于 Connection
完成。
构建非交互式 SSH 客户端
现在看下如何创建一个非交互式的 SSH 客户端,作为 WebConsole 和用户的交互模块:
1、通过 ssh.Dial()
创建一个 SSH 客户端连接
|
|
2、通过 SSH 客户端创建 SSH Channel, 并请求一个 pty 伪终端, 并开启用户的默认 Shell
|
|
Remote Server 与 Browser 实时数据交换
现在为止建立了两个 IO
通道,一个是 WebSocket
通道,另外一个是 SSH Channel
。由于需要双向转发数据,这里新建 2
个 groutine
:
groutine1
不停的从 WebSocket 通道里读取用户的输入, 并通过 SSH Channel 传给远程主机groutine2
负责将远程主机的数据(主要是终端屏显数据)传递给浏览器
1、groutine1
主要完成从 Websocket
中读取数据,通过 ssh 的 Channel
发送到目标服务器
|
|
2、groutine2
主要完成从 SSH Channel
中读取数据,写入 Websocket
,这样用户在浏览器上可以看到操作回显了:
|
|
0x04 登录效果验证
直接在浏览器中输入已认证的 url
,成功通过 WebConsole
连上远端的 SSH 服务器,大功告成
0x05 总结
- 在整个系统中,最关键的点是怎样防止用户的身份被伪造,直观点,就是在第 2 步中,后台服务如何确定,当前的接口调用方就是用户 A。另外,我们如何解决共享权限的场景,假设 A 申请了某台机器的登录权限,假设 A 授权 B 也可以使用该票据登录,那么我们的系统的认证如何完成呢?这个是很有趣的问题,待后面在工作中慢慢思考和实现吧。
- 此外,作为 SSH 连接代理的服务(本文中以
CGI
服务承担)的稳定性也很重要,因为 WebConsole 的所有流量都会经由 SSH 连接代理转发,TCP 连接也由代理维持,一旦代理故障,所有的 WebConsole 连接都会断开,所以可用性的设计也是非常重要的一环。 - 整个 Web 页面需要前置认证机制,比如接入 Github 的
Oauth
、Onelogin
等等