通过使用paramiko和sqlalchemy实现堡垒机功能
主要功能实现:
1、用户登录堡垒机后,无需知道密码或密钥可以SSH登录远端服务器;
2、用户对一个组内所有主机批量执行指定命令,获取格式化输出;
3、针对远端主机,可以进行上传下载文件;
4、用户在远端主机上执行的命令,均被记录并入库,实现审计功能;
主要参考了alex的开源代码jumpserver,并添加部分功能。
目前代码仍不尽人如意,即在SSH的交互模式interactive下,将输入缓冲区置为0,但无法还原,故交互结束后,有报错;
故临时解决方法:使用try和except方法,将OSError类的报错,强制exit程序(使用pass,coutinue,break等重置、跳出while无法解决)
结果:经调试将stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0,closefd=False),exit后,正常回到循环。BUG解决了。
一、堡垒机具体介绍:
1、堡垒机功能 :
堡垒机,也称为跳板机,多用于系统运维环境中。指的是在一个特定的网络环境下,为了保障网络和数据不受来自外部和内部用户的入侵和破坏,而运用各种技术手段实时收集和监控网络环境中每一个组成部分的系统状态、安全事件、网络活动,以便集中报警、及时处理及审计定责。
从功能上分析,它综合了核心系统运维和安全审计管控两大主要功能;
从技术实现上分析,它通过切断终端计算机对网络和服务器资源的直接访问,而采用协议代理的方式,接管了终端计算机对网络和服务器的访问。
2、堡垒机服务器 :
IP :192.168.x.x
数据库:mysql5.6
程序开发环境:python3
3、权限说明 :
对访问权限细分了用户、帐号、服务器三块,具体如下:
(1)、用户:即登录堡垒机的ssh 帐号,分配给开发、QA、运维;
(2)、帐号:即堡垒机访问业务服务器的ssh帐号,例:root、ubuntu、只读帐号等;
(3)、远程业务主机:通过绑定主机和主机组跟用户相关联;
三者关系说明:
<1>.同类型业务的主机划分到同一主机组下;方便批量给同一开发小组用户赋权;
<2>.多个用户使用同一帐号来访问同一台业务主机或同一个组下的多台主机;
<3>.一个用户使用多个帐号来访问同一台业务主机或同一个组下的多台主机
<4>.优先以主机组的方式赋权给用户,单个主机赋权次之(无组的主机在登录界面会归于ungroupped hots)
4、安全审计说明:
(1)、通过对远程业务主机进行源地址限制,可以对外拦截非法访问和恶意攻击;
(2)、对登录服务器的操作命令和输入字符进行记录,可以对内部人员误操作和非法操作进行审计监控,以便事后责任追踪;
(3)、堡垒机登录业务主机主要使用ssh公私钥,避免了业务主机的密码泄漏及内部员工离职的密码更新;
5、流程安全管理:
想要正确可靠的发挥堡垒机的作用,只靠堡垒机本身是不够的, 还需要对用户进行安全上的限制,堡垒机部署后,要确保你的系统达到以下条件:
- 所有人包括运维、开发等任何需要访问业务系统的人员,只能通过堡垒机访问业务系统
- 回收所有对业务系统的访问权限,做到除了堡垒机管理人员,没有人知道业务系统任何机器的登录密码
- 网络上限制所有人员只能通过堡垒机的跳转才能访问业务系统
- 确保除了堡垒机管理员之外,所有其它人对堡垒机本身无任何操作权限,只有一个登录跳转功能
- 确保用户的操作纪录不能被用户自己以任何方式获取到并篡改,达到安全审计的作用。
二、堡垒机登录操作流程:
1、管理员为用户在堡垒机服务器上创建账号(使用用户名密码或公钥);
2、用户登录堡垒机,输入堡垒机用户名密码,显示当前用户可管理的服务器得列表;
3、用户选择主机组;
4、用户选择具体的主机,并自动登录;
5、执行操作并同时将用户操作记录;
6、用户exit退出业务主机后,返回到堡垒机,程序结束;
7、程序启动命令:jump
具体截图如下:
<1> . 选择组:
<2> . 批量执行命令:
<3> . 选择主机,输入「t」,可以传输文件:
<4> . 输入其他字符即正常登录主机,退出及重启程序:
业务主机自身的命令记录:
三、代码具体说明如下:
目录结构: 1、bin (主程序目录) 2、conf (配置文件目录) 3、modules (具体模块目录) 4、share (数据表目录) 一、bin目录下的litter_finger.py主程序: 1、添加当前目录到环境变量; 2、从modules.actions文件里加载excute_from_command_line 3、执行该模块excute_from_command_line(sys.argv) 二、conf目录: 1、目录下的settings.py: (1)、定义了BASE_DIR上一级路径; (2)、定义了DB_CONN的连接变量; (3)、定义了WELCOME_MSG变量,显示欢迎界面; (4)、定义了USER_GROUP_MSG变量,显示主机组名称; (5)、定义了GROUP_LIST变量,显示主机组列表; (6)、定义了HOST_LIST变量,显示主机列表; (7)、定义了SSH_SFTP_HELPINFO变量,显示SFTP帮助信息; 2、action_registers.py: (1)、加载modules目录下的views模块; (2)、定义了actions字典,使用反射指定执行启动、停止、同步、创建用户,组,绑定主机; start_session,stop,syncdb,create_users,create_groups,create_hosts ,create_bindhosts,create_remoteusers; 三、modules目录: 1、目录下的actions.py: (1)、导入conf模块下的settings和action_registers二个函数; (2)、导入modules模块下的utils函数; (3)、定义help_msg函数,输出执行的命令参数; (4)、定义excute_from_command_line(argvs)函数: 如果参数长度小于2,则输出帮助信息,并结束; 第一个位置参数不在指定名称中,则调用utils的错误输出函数; 执行指定的命令action_registers.actions[argvs[1]](argvs[1:]), 例:createusers -f <the new users file>; 2、目录下的utils.py: (1)、导入conf模块下的settings; (2)、导入yaml模块; (3)、定义print_err(msg,quit=False)函数,如果quit为真,则exit;否则print; (4)、定义yaml_parser(yml_filename)解析函数,文件为share目录下的yml格式文件; 3、目录下的db_conn.py: (1)、导入sqlalchemy和sqlalchemy.orm模块; (2)、导入conf目录下的setting配置文件的DB变量; (3)、创建数据库连接engine和session; 4、目录下的models.py: (1)、导入连接数据库的各种模块; (2)、导入密码hash模块werkzeug.security; (3)、生成一个ORM基类 Base = declarative_base(); (3)、创建BindHost2Group、BindHost2UserProfile和Group2UserProfile 三个关联表; (4)、定义用户配置文件UserProfile表,并调用relationship定义关联groups、bind_hosts和audit_logs; (5)、定义远端用户RemoteUser表,其中定义了ssh二种认证方法password和key; (6)、定义主机Host表,字段为主机名,IP和端口; (7)、定义组Group表,其中调用relationship定义关联bind_hosts和user_profiles; (8)、定义绑定主机BindHost表,其中host_id外键与host表关联;remoteuser_id外键与remoteuser表关联;并调用relationship定义了groups、user_profiles进行关联; (9)、定义审计AuditLog表,其中user_id和bind_host_id用外键关联; 5、目录下的common_filters.py: (1)、导入models,db_conn,utils三个模块; (2)、定义函数bind_hosts_filter(vals),判断一个主机是不是在绑定主机表中,在则返回,否则退出; (3)、定义函数user_profiles_filter(vals),判断一个用户是否在用户配置表中,在则返回,否则退出; (4)、定义函数groups_filter(vals),判断一个组是否在组配置表中,在则返回,否则退出; 6、目录下的ssh_login.py: (1)、导入base64,getpass,socket,traceback,paramiko,datetime模块; (2)、导入models模块; (3)、导入interactive模块; (4)、定义ssh_login(user_obj,bind_host_obj,mysql_engine,log_recording)函数 定义client = paramiko.SSHClient(); 允许连接不在know_hosts文件中的主机ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()); 定义client.connect(),参数为bind_host表中的主机IP,端口,用户名和密码(如果使用key,则为key的创建密码); 定义cmd_caches变量为空列表; 调用client的invoke_shell(指定终端的宽度和高度)函数; cmd_caches列表追加AuditLog的记录; 调用log_recording函数进行日志记录 调用interactive里的interacitive_shell 关闭chan和client 7、目录下的interactive.py: (1)、导入socket,os,sys,paramiko,datetime,select,fcntl,signal,struct等模块; (2)、导入models模块; (3)、使用try导入termios和tty模块,因windows下没有此模块; (4)、定义ioctl_GWINSZ,getTerminalSize,resize_pty三个函数,主要目的:终端大小适应。paramiko.channel会创建一个pty(伪终端),有个默认的大小(width=80, height=24),所以登录过去会发现能显示的区域很小,并且是固定的。编 辑vim的时候尤其痛苦。channel中有resize_pty方法,但是需要获取到当前终端的大小。经查找,当终端窗口发生变化时,系统会给前台进程组发送SIGWINCH信号,也就> 是当进程收到该信号时,获取一下当前size,然后再同步到pty中,那pty中的进程等于也感受到了窗口变化,也会收到SIGWINCH信号。 (5)、定义交互shell函数,如果是linux,则执行posix_shell;否则执行windows_shell函数; (6)、定义posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording)函数,进行输入输出操作交互; (7)、定义windows的交互模式,目前此功能没有使用到 8、目录下的ssh_cmd.py: (1)、导入traceback,paramiko模块; (2)、定义ssh_cmd(bind_host_obj,cmd)函数 定义client = paramiko.SSHClient(); 允许连接不在know_hosts文件中的主机ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()); 定义client.connect(),参数为bind_host表中的主机IP,端口,用户名和密码(如果使用key,则为key的创建密码); 调用stdin, stdout, stderr = client.exec_command(cmd) ,并返回命令结果; 9、目录下的ssh_sftp.py: (1)、导入traceback,paramiko,stat模块; (2)、定义ssh_sftp(object)类 (3)、定义类下的__init__(self, host_obj,timeout=30),定义远端主机变量; (4)、显示文件传输帮助信息help(self); (5)、定义get下载单个文件函数; (6)、定义put上传单个文件函数; (7)、定义获取远端linux主机上指定目录及其子目录下的所有文件函数; (8)、定义下载远端主机指定目录到本地目录函数 ; (9)、定义获取本地指定目录及其子目录下的所有文件函数; (10)、定义上传本地目录下的文件到远程主机指定目录函数; 10、目录下的主程序views.py: (1)、导入os,sys,getpass,time (2)、导入modules目录下的models、common_filters、ssh_login等模块; (3)、导入db_conn模块中的变量engine和session; (4)、导入utils里的print_err和yaml_parser函数; (5)、定义自登录login函数(获取当前系统用户名自登录) 获取当前登录跳板机的用户; 在数据库的user_profile表中,根据此用户进行查找匹配; 如果不存在,则调用auth认证函数; 正常返回user_obj; (6)、定义认证函数auth: 循环3次,输入用户名和密码; 定义user_obj变量,即去数据库取userprofile表中的用户和密码进行比对 ; 或不为空,则成功返回;为空则继续循环; (7)、定义框架函数,输出登录信息framework(user,group); (8)、定义显示组函数,输出主机组信息show_group(user); (9)、定义显示主机函数,输出主机信息show_host(user_obj,group_name,bind_hosts); (10)、定义日志记录到数据库的函数log_recording(user_obj,bind_host_obj,logs); (11)、定义调用多线程函数执行命令make_threading(func,tuple_args); (12)、定义文件上传下载的try和except报错函数try_exec(client_mode,first_file,second_file); (13)、定义调用文件传输函数ssh_transport(host_obj),主要有:ldir,rdir,put,get四种方式; (14)、定义启动会话函数start_session(argvs); 调用登录login及认证auth函数,获取用户信息实例; 进行第一层循环,显示登录信息框架; 显示组列表信息,并手工输入组编号,进行一系列判断; 显示主机列表进行第二层循环,并手工输入主机编号,如果输入e,则批量执行命令; 输入正确主机编号后,输入t,则进行文件传输,输入其他则登录主机,进行命令交互; (15)、定义创建本地登录用户函数create_users(argvs),读取指定的yaml文件,将数据导入数据库; (16)、定义创建主机组函数create_groups(argvs),读取指定的yaml文件,将数据导入数据库; (17)、定义创建远程主机函数create_hosts(argvs),读取指定的yaml文件,将数据导入数据库; (18)、定义创建远程主机的登录信息函数create_remoteusers(argvs),读取指定的yaml文件,将数据导入数据库; (19)、定义创建绑定主机函数create_bindhosts(argvs),读取指定的yaml文件,将数据导入数据库; (20)、定义创建所有表结构函数syncdb(argvs); 代码介绍
四、代码BUG记录
环境说明: 1、服务器IP:192.168.4.208 2、运行环境:python3 3、代码目录:/usr/local/jumpserver 4、程序运行方式:/usr/bin/python3 /usr/local/jumpserver/bin/jumpserver.py start_session 或jump (已定义别名:alias jump='/usr/local/jumpserver/bin/jumpserver.py start_session') 5、调用了python下的ssh访问模块paramiko和交互模块interactive,interactive功能通过读取键盘输入命令,并将结果返回到屏幕输出,同时将输入的字符记录并导入到数据库。 故障说明及解决方法: 1、在调用ssh_login和interactive.py里,ssh交互执行命令的功能已经实现,就是有点在对上下左右控制键上的小问题:1、在命令行按向上键查看历史命令时,第一次按向上键没有输出显示,但回车键可以正常执行命令 2、在vim编辑状态下上下左右键的处理,有时候会回显A,B,C,D 故障原因: 本程序最大BUG: 当读取键盘输入时,方向键会有问题,因为按一次方向键会产生3个字节数据,我的理解是按键一次会被select捕捉一次标准输入有变化,但是我每次只处理1个字节的数据,其他的数据会存放在输入缓冲区中,等待下次按键的时候一起发过去。这就导致了本来3个字节才能完整定义一个方向键的行为,但是我只发过去一个字节,所以终端并不知道我要干什么。所以没有变化,当下次触发按键,才会把上一次的信息完整发过去,看起来就是按一下方向键有延迟。多字节的粘贴也是一个原理。 故障解决: 解决办法是将输入缓冲区置为0,这样就没有缓冲,有多少发过去多少,这样就不会有那种显示的延迟问题了。 stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0) fd = stdin.fileno() 备注:解决方案见https://blog.csdn.net/zlucifer/article/details/70858491 2、Traceback (most recent call last): File "/usr/local/jumpserver/bin/jumpserver.py", line 12, in <module> excute_from_command_line(sys.argv) File "/usr/local/jumpserver/modules/actions.py", line 26, in excute_from_command_line action_registers.actions[argvs[1]](argvs[1:]) File "/usr/local/jumpserver/modules/views.py", line 251, in start_session host_choice = input("[(b)back,(q)quit, select host to login]:").strip() OSError: [Errno 9] Bad file descriptor 故障原因: 在解决故障1(本程序最大BUG)上产生的一系列新BUG(包括以下全部BUG) (1)、 在设置了缓冲区为0后,导致在业务主机和堡垒机之间的标准输入sys.stdin不一致,但尝试了N多方法,也无法还原标准输入; 在业务主机上输入exit后,结束SSH登录后,python程序无法识别新的stdin,程序报如上错误; 经调试将stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0,closefd=False),exit后,正常回到循环。 (2)、 因python3环境下的标准输入为bytes字节后,读入内存需要decode为unicode编码,保存到文件又需要encode为bytes编码,针对普通字符和汉字产生N多编码问题; 故障解决: (1)、临时解决方案:使用try和except方法,将OSError类的报错,强制exit程序(使用pass,coutinue,break等重置、跳出while无法解决) (2)、编码解决方案见如下具体故障; 3、*** Caught exception: <class 'ValueError'>: can't have unbuffered text I/O Traceback (most recent call last): File "/usr/local/jumpserver/modules/ssh_login.py", line 54, in ssh_login interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording) File "/usr/local/jumpserver/modules/interactive.py", line 62, in interactive_shell posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording) File "/usr/local/jumpserver/modules/interactive.py", line 77, in posix_shell stdin = os.fdopen(sys.stdin.fileno(), 'r', 0) File "/usr/lib/python3.4/os.py", line 980, in fdopen return io.open(fd, *args, **kwargs) ValueError: can't have unbuffered text I/O 故障原因: 因python3默认使用的是str类型对字符串编码,默认使用bytes操作二进制数据流,故在标准输入时,打开方式应为二进制; 故障解决: stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0) 同时在读取标准输入时,应转化为str编码: x = stdin.read(1).decode('utf-8',errors='ignore') 4、Caught exception: <class 'UnicodeDecodeError'>: 'utf-8' codec can't decode bytes in position 1022-1023: unexpected end of data Traceback (most recent call last): File "/usr/local/jumpserver/modules/ssh_login.py", line 53, in ssh_login interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording) File "/usr/local/jumpserver/modules/interactive.py", line 61, in interactive_shell posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording) File "/usr/local/jumpserver/modules/interactive.py", line 110, in posix_shell x = u(chan.recv(1024)) File "/usr/local/lib/python3.4/dist-packages/paramiko/py3compat.py", line 143, in u return s.decode(encoding) UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 1022-1023: unexpected end of data 故障原因: python3环境下,因在interactive交互过程中,标准输出需要转化为str编码,文件中的汉字无法decode; 故障解决: 使用x = chan.recv(1024).decode('utf-8',errors='ignore')替换原有的 x = u(chan.recv(1024)),即在decode中,如有报错,则忽略; 5、'utf-8' codec can't encode character 'udc9b' in position 0: surrogates not allowed Traceback (most recent call last): File "/usr/lib/python3/dist-packages/CommandNotFound/util.py", line 24, in crash_guard callback() File "/usr/lib/command-not-found", line 90, in main if not cnf.advise(args[0], options.ignore_installed) and not options.no_failure_msg: File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 265, in advise packages = self.getPackages(command) File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 157, in getPackages result.update([(pkg, db.component) for pkg in db.lookup(command)]) File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 85, in lookup result = self.db.lookup(command) File "/usr/lib/python3/dist-packages/CommandNotFound/CommandNotFound.py", line 41, in lookup key = key.encode('utf-8') UnicodeEncodeError: 'utf-8' codec can't encode character 'udc9b' in position 0: surrogates not allowed 故障原因: python3环境下,因在interactive交互过程中,标准输入为bytes字节码,需要转化为str编码,但一个汉字需要3个字节,故原有的x = u(stdin.read(1))会导致汉字无法获取,故报错; 故障解决: 临时解决方案:x = u(stdin.read(3)) 6、Caught exception: <class 'UnicodeEncodeError'>: 'ascii' codec can't encode character 'u4f60' in position 0: ordinal not in range(128) Traceback (most recent call last): File "/usr/local/jumpserver/modules/ssh_login.py", line 53, in ssh_login interactive.interactive_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording) File "/usr/local/jumpserver/modules/interactive.py", line 62, in interactive_shell posix_shell(chan,user_obj,bind_host_obj,cmd_caches,log_recording) File "/usr/local/jumpserver/modules/interactive.py", line 126, in posix_shell sys.stdout.write(x) UnicodeEncodeError: 'ascii' codec can't encode character 'u4f60' in position 0: ordinal not in range(128) 故障原因: 因mac下的ssh访问终端工具iTerm2编码不是utf-8,故有此报错; 故障解决: 将终端编码更换为utf-8; 7、/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py:166: Warning: (1265, "Data truncated for column 'cmd' at row 1") result = self._query(query) 故障原因: 因输出获取以1024字节,偶尔会出现超过1024字节的这类警告; 故障解决: 因偶尔出现,暂不影响使用,故此warning暂无处理; 将audit_log表中的cmd字段属性改为text类型即可解决。
五、代码如下:
目录结构:
<1>、bin (主程序目录)
<2>、conf (配置文件目录)
<3>、modules (具体模块目录)
<4>、share (数据表目录)
1、bin目录下主程序jumpserver.py代码:
#!/usr/bin/python3 #_*_coding:utf-8_*_ __author__ = 'Kevin Wang' import os,sys,readline ###定义上级目录### BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ###将上级目录添加到环境变量PATH中### sys.path.append(BASE_DIR) ###主程序,调用actions下的函数excute_from_command_line### if __name__ == '__main__': from modules.actions import excute_from_command_line excute_from_command_line(sys.argv)
2、conf目录下settings.py代码:
定义些显示输出的变量
#!/usr/bin/python3 #_*_coding:utf-8_*_ __author__ = 'Kevin Wang' ###MYSQL数据库连接地址### DB_CONN = "mysql+pymysql://user:password@192.168.4.x:3306/db_name?charset=utf8" ###菜单显示主界面,会显示登录的用户名和当前的时间### WELCOME_MSG = '''