堡垒机的核心武器:WebSSH录像实现
WebSSH终端录像的实现终于来了
前边写了两篇文章『Asciinema:你的所有操作都将被录制』和『Asciinema文章勘误及Web端使用介绍』深入介绍了终端录制工具Asciinema,我们已经可以实现在终端下对操作过程的录制,那么在WebSSH中的操作该如何记录并提供后续的回放审计呢?
一种方式是『Asciinema:你的所有操作都将被录制』文章最后介绍的自动录制审计日志的方法,在主机上添加个脚本,每次连接自动进行录制,但这样不仅要在每台远程主机添加脚本,会很繁琐,而且录制的脚本文件都是放在远程主机上的,后续播放也很麻烦
那该如何更好处理呢?下文介绍一种优雅的方式来实现,核心思想是不通过录制命令进行录制,而在Webssh交互执行的过程中直接生成可播放的录像文件
设计思路
通过上边两篇文章的阅读,我们已经知道了Asciinema录像文件主要由两部分组成:header头和IO流数据
header头位于文件的第一行,定义了这个录像的版本、宽高、开始时间、环境变量等参数,我们可以在websocket连接创建时将这些参数按照需要的格式写入到文件
header头数据如下,只有开头一行,是一个字典形式
{"version": 2, "width": 213, "height": 55, "timestamp": 1574155029.1815443, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}, "title": "ops-coffee"}
整个录像文件除了第一行的header头部分,剩下的就都是输入输出的IO流数据,从websocket连接建立开始,随着操作的进行,IO流数据是不断增加的,直到整个websocket长连接的结束,那就需要在整个WebSSH交互的过程中不断的往录像文件追加输入输出的内容
IO流数据如下,每一行一条,列表形式,分别表示操作时间,输入或输出(这里我们为了方便就写固定字符串输出),IO数据
[0.2341010570526123, "o", "Last login: Tue Nov 19 17:11:30 2019 from 192.168.105.91\r\r\n"]
似乎很完美,按照上边的思路录像文件就应该没有问题了,但还有一些细节需要处理
首先是需要历史连接列表,在这个列表里可以看到什么时间,哪个用户连接了哪台主机,当然也需要提供回放功能,新建一张表来记录这些信息
class Record(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
host = models.ForeignKey(Host, on_delete=models.CASCADE, verbose_name='主机')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户')
filename = models.CharField(max_length=128, verbose_name='录像文件名称')
def __str__(self):
return self.host
其次还需要考虑的一个问题是header和后续IO数据流要写入同一个文件,这就需要在整个websocket的连接过程中有一个固定的文件名可被读取,这里我使用了主机+用户+当前时间作为文件名,同一用户在同一时间不能多次连接同一主机,这样可保证文件名不重复,同时避免操作写入错误的录像文件,文件名在websocket建立时初始化
def __init__(self, host, user, websocket):
self.host = host
self.user = user
self.time = time.time()
self.filename = '%s.%s.%d.cast' % (host, user, self.time)
IO流数据会持续不断的写入文件,这里以一个独立的方法来处理写入
def record(self, type, data):
RECORD_DIR = settings.BASE_DIR + '/static/record/'
if not os.path.isdir(RECORD_DIR):
os.makedirs(RECORD_DIR)
if type == 'header':
Record.objects.create(
host=Host.objects.get(id=self.host),
user=self.user,
filename=self.filename
)
with open(RECORD_DIR + self.filename, 'w') as f:
f.write(json.dumps(data) + '\n')
else:
iodata = [time.time() - self.time, 'o', data]
with open(RECORD_DIR + self.filename, 'a', buffering=1) as f:
f.write((json.dumps(iodata) + '\n'))
record接收两个参数type和data,type标识本次写入的是header头还是IO流,data则是具体的数据
header只需要执行一次写入,所以将其放在ssh的connect方法中,只在ssh连接建立时执行一次,在执行header写入时同时往数据库插入新的历史记录数据
调用record方法写入header
def connect(self, host, port, username, authtype, password=None, pkey=None,
term='xterm-256color', cols=80, rows=24):
...
# 构建录像文件header
self.record('header', {
"version": 2,
"width": cols,
"height": rows,
"timestamp": self.time,
"env": {
"SHELL": "/bin/bash",
"TERM": term
},
"title": "ops-coffee"
})
IO流数据则需要与返回给前端的数据保持一致,这样就能保证前端显示什么录像就播放什么了,所以所有需要返回前端数据的地方都同时写入录像文件即可
调用record方法写入io流数据
def connect(self, host, port, username, authtype, password=None, pkey=None,
term='xterm-256color', cols=80, rows=24):
...
# 连接建立一次,之后交互数据不会再进入该方法
for i in range(2):
recv = self.ssh_channel.recv(65535).decode('utf-8', 'ignore')
message = json.dumps({'flag': 'success', 'message': recv})
self.websocket.send(message)
self.record('iodata', recv)
...
def _ssh_to_ws(self):
try:
with self.lock:
while not self.ssh_channel.exit_status_ready():
data = self.ssh_channel.recv(1024).decode('utf-8', 'ignore')
if len(data) != 0:
message = {'flag': 'success', 'message': data}
self.websocket.send(json.dumps(message))
self.record('iodata', data)
else:
break
except Exception as e:
message = {'flag': 'error', 'message': str(e)}
self.websocket.send(json.dumps(message))
self.record('iodata', str(e))
self.close()
由于命令执行与返回都是多线程的操作,这就会导致在写入文件时出现文件乱序影响播放的问题,典型的操作有vim、top等,通过加锁self.lock
可以顺利解决
最后历史记录页面,当用户点击播放按钮时,调用js弹出播放窗口
<div class="modal fade" id="modalForm">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-body" id="play">
</div>
</div>
</div>
</div>
// 播放录像
function play(host,user,time,file) {
$('#play').html(
'<asciinema-player id="play" title="WebSSH Record" author="ops-coffee.cn" author-url="https://ops-coffee.cn" author-img-url="/static/img/logo.png" src="/static/record/'+file+'" speed="3" '+
'idle-time-limit="2" poster="data:text/plain,\x1b[1;32m'+time+
'\x1b[1;0m用户\x1b[1;32m'+user+
'\x1b[1;0m连接主机\x1b[1;32m'+host+
'\x1b[1;0m的录像记录"></asciinema-player>'
)
$('#modalForm').modal('show');
}
asciinema-player标签的详细参数介绍可以看这篇文章『Asciinema文章勘误及Web端使用介绍』
演示与总结
在写入文件的方案中,考虑了实时写入和一次性写入,实时写入就像上边这样,所有的操作都会实时写入录像文件,好处是录像不丢失,且能在操作的过程中进行实时的播放,缺点也很明显,就是会频繁的写文件,造成IO开销
一次性写入可以在用户操作的过程中将录像数据写入内存,在websocket关闭时一次性异步写入到文件中,这种方案在最终写入文件时可能因为种种原因而失败,从而导致录像丢失,还有个缺点是当你WebSSH操作时间过长时,会导致内存的持续增加
两种方案一种是对磁盘的消耗另一种是对内存的消耗,各有利弊,当然你也可以考虑批量写入,例如每分钟写一次文件,一分钟之内的保存在内存中,平衡内存和磁盘的消耗,期待你的实现
相关文章推荐阅读:
堡垒机的核心武器:WebSSH录像实现的更多相关文章
- 主机管理+堡垒机系统开发:webssh(十)
一.安装shellinabox 1.安装依赖工具 yum install git openssl-devel pam-devel zlib-devel autoconf automake libtoo ...
- 堡垒机WebSSH进阶之实时监控和强制下线
这个功能我可以不用,但你不能没有 前几篇文章实现了对物理机.虚拟机以及Kubernetes中Pod的WebSSH操作,可以方便的在web端对系统进行管理,同时也支持对所有操作进行全程录像,以方便后续的 ...
- IronFort---基于Django和Websocket的堡垒机
WebSSH有很多,基于Django的Web服务也有很多,使用Paramiko在Python中进行SSH访问的就更多了.但是通过gevent将三者结合起来,实现通过浏览器访问的堡垒机就很少见了.本文将 ...
- 开发基于Django和Websocket的堡垒机
WebSSH有很多,基于Django的Web服务也有很多,使用Paramiko在Python中进行SSH访问的就更多了.但是通过gevent将三者结合起来,实现通过浏览器访问的堡垒机就很少见了.本文将 ...
- 基于python的堡垒机
一 堡垒机的架构 堡垒机的核心架构通常如下图所示: 二.堡垒机的一般执行流程 管理员为用户在服务器上创建账号(将公钥放置服务器,或者使用用户名密码) 用户登陆堡垒机,输入堡垒机用户名密码,显示当前用户 ...
- Python之路——堡垒机原理及其简单实现
1 堡垒机基本概述 其从功能上讲,它综合了核心系统运维和安全审计管控两大主干功能,从技术实现上讲,通过切断终端计算机对网络和服务器资源的直接访问,而采用协议代理的方式,接管了终端计算机对网络和服务器的 ...
- 使用开源软件 jumpserver 搭造自己的堡垒机
使用开源软件 jumpserver 搭造自己的堡垒机 开软地址:https://github.com/jumpserver/jumpserver 目前版本:1.5.2 测试的时候有少许BUG,但功能却 ...
- jumpserver堡垒机(2.4)部署
jumpserver 2.4.0 部署 jumpserver 官网: https://www.jumpserver.org/ Jumpserver介绍 JumpServer 是全球首款完全开源的堡垒机 ...
- 堡垒机环境-jumpserver部署
1:安装数据库 这里是提前安装,也可以不安装,在安装jumpserver主程序的时候,他会询问你是否安装 yum -y install ncurses-devel cmake echo 'export ...
随机推荐
- BeetleX服务网关之服务发现与泛域名路由
在新版本的服务网关中提供了服务发现和泛域名路由解决功能,服务发现可以在无须配置的情况下实现服务自动注册到网关中解脱对服务配置的繁琐工作:而泛域名路由则可以针对不同的域名制定不同的负载规则. 使用con ...
- Linux本地内核提权漏洞复现(CVE-2019-13272)
Linux本地内核提权漏洞复现(CVE-2019-13272) 一.漏洞描述 当调用PTRACE_TRACEME时,ptrace_link函数将获得对父进程凭据的RCU引用,然后将该指针指向get_c ...
- 云计算 docker 容器部署
什么是docker容器: 容器就是在隔离的环境中运行的一个进程,如果进程停止,容器就会退出.隔离的环境拥有自己的系统文件,ip地址,主机名等kvm虚拟机,linux,系统文件 容器和虚拟化的区别 : ...
- 百万年薪python之路 -- 运算符及while的练习
1.判断下列逻辑语句的结果,一定要自己先分析 1)1 > 1 or 3 < 4 or 4 > 5 and 2 > 1 and 9 > 8 or 7 < 6 1 &g ...
- 百万年薪python之路 -- 小数据池和代码块练习
1.请用代码验证 "alex" 是否在字典的值中? info = {'name':'王刚蛋','hobby':'铁锤','age':'18',...100个键值对} info = ...
- C#事件浅淡(1)
最近在写C#,感觉事件这个机制很好,可是怎么实现自己定义的事件呢?查了资料有的不全有的不完整,有的太深,自己写一个简单的例子. 原则 1,定义一个事件信息类(标准的都继承EventArgs) 2.定义 ...
- day05整理
目录 一.上节课回顾 (一)数据类型 (1)数字类型 (2)字符串类型str (3)列表类型list (4)字典类型dict (二)jieba模块 (三)wordcloud模块 二.文本处理 (一)什 ...
- 一般链表实现集合运算(C语言)
最近在学习数据结构,遇到以下问题: 假设集合A = (c, b, e, g, f, d),B = (a, b, n, f),利用一般线性链表实现集合运算(A-B)∪(B-A). 分析: 上面的问题只要 ...
- ansible之roles
基于之前的博客介绍,我们已经了解了 tasks 和 handlers,那怎样组织 playbook 才是最好的方式呢?简单的回答就是:使用 roles ! Roles 基于一个已知的文件结构,去自动的 ...
- 自建windows服务器如何部署egg应用
1. 使用IE浏览器登陆VPN 2. 远程登陆 3. 在服务器安装最新的node.js,git等 4. 下载源码 > git clone ****.git 5. npm安装依赖 > cd ...