堡垒机功能需求分析和实现
1、权限管理:
-
收回所有人员的直接登录服务器的权限,所有的登录动作都通过堡垒机授权,运维人员或开发人员不知道远程服务器的密码,这些远程机器的用户信息都绑定在了堡垒机上,堡垒机用户只能看到他能用什么权限访问哪些远程服务器。
-
允许A开发人员通过普通用户登录5台web服务器,通过root权限登录10台hadoop服务器,但对其余的服务器无任务访问权限
-
多个运维人员可以共享一个root账户,但是依然能分辨出分别是谁在哪些服务器上操作了哪些命令,因为堡垒机账户是每个人独有的,也就是说虽然所有运维人员共享了一同一个远程root账户,但由于他们用的堡垒账户都是自己独有的,因此依然可以通过堡垒机控制每个运维人员访问不同的机器。
2、审计管理:
-
所有人包括运维、开发等任何需要访问业务系统的人员,只能通过堡垒机访问业务系统回收所有对业务系统的访问权限,做到除了堡垒机管理人员,没有人知道业务系统任何机器的登录密码;网络上限制所有人员只能通过堡垒机的跳转才能访问业务系统;
确保除了堡垒机管理员之外,所有其它人对堡垒机本身无任何操作权限,只有一个登录跳转功能;确保用户的操作纪录不能被用户自己以任何方式获取到并篡改
3、功能需求:
-
所有的用户操作日志要保留在数据库中
-
每个用户登录堡垒机后,只需要选择具体要访问的设置,就连接上了,不需要再输入目标机器的访问密码
-
允许用户对不同的目标设备有不同的访问权限,例:
-
对10.0.2.34 有mysql 用户的权限
-
对192.168.3.22 有root用户的权限
-
对172.33.24.55 没任何权限
-
4、分组管理,即可以对设置进行分组,允许用户访问某组机器,但对组里的不同机器依然有不同的访问权限
5、ssh公钥登录过程
不需要密码连接服务器时,在主机上生成一对秘钥(公钥和私钥),只需要把公钥给对方即可;
Linux生成秘钥的命令:
- ssh-keygen 会生成两个文件
-
- /root/.ssh/.id_rsa 私钥 (.表示隐藏文件)
-
- /root/.ssh/.id_rsa.pub 公钥
-
-
- 把公钥拷贝到要登陆的主机上,命令:scp -rp id_rsa.pub alex@192.168.10.35:/home/alex/
-
- 把远程主机上的文件拷贝到本机命令:scp -rp alex@192.168.10.35:/home/alex/id_rsa.pub /tmp
-
- id 或 whoami 查看当前用户
-
- sudo - alex 切换用户
6、表结构
7、Linux环境下运行:
settings.py修改内容:
- ALLOWED_HOSTS = ['*',] 允许所有的主机访问这个Django程序
- AUTH_USER_MODEL = 'web.UserProfile' # APP名+表名 告诉Django用自己定义的用户认证表
8、测试:
把项目拷贝到Linux环境下==>cd到目录下执行python shield_manager.py run==>xjl@126.com xjl1991 ==> 登陆堡垒机成功,开始选择远程主机
==》链接成功后,开始执行命令,日志自动记录
9、实现登录Linux就执行堡垒机脚本:
- 1、ls -a 查看当前目录有两个文件:.bashrc-登陆就会执行这个文件 .bash_logout 退出登陆就执行这个文件
- 2、vim .bashrc (比如:在最下面写上echo 'hello!',登陆就会看到),所以在最下面加入以下两行命令即可:
- python3 shield_manager.py run (登陆后会直接进入登陆堡垒机界面)
- logout (退出远程主机之后,会直接退出堡垒机,不能让用户在堡垒机上操作)
- 3、通过Linux的IP和端口登陆admin查看日志: sudo python3 manage.py runserver 0.0.0.0:8000,这样就可以用Linux自己的IP和8000端口登陆admin
10、其实源码就是修改了paramiko源码里的demo.py 和interactive.py,整个源码执行流程为:
- shield_manager.py
import sys,os if __name__ == '__main__': # 在Django环境外调用其内部的数据库,需要配置环境变量,下面三步缺一不可 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BlueShield.settings') import django django.setup() from backend import main interactive_obj = main.ArgvHandler(sys.argv) interactive_obj.call()
- main.py
class ArgvHandler(object): """接收用户参数,并调用相应的功能""" def __init__(self,sys_args): self.sys_args = sys_args def help_msg(self,error_msg=''): """打印帮助信息""" msgs = """ error:%s run 启动用户交互程序 """%error_msg exit(msgs) # 打印帮助信息 def call(self): """根据参数调用对应方法""" if len(self.sys_args) == 1: self.help_msg() if hasattr(self,self.sys_args[1]): func = getattr(self,self.sys_args[1]) func() else: self.help_msg('没有方法 %s'%self.sys_args[1]) def run(self): """启动用户交互程序""" from backend.ssh_interactive import SshHandler # 把自己的对象传入 obj = SshHandler(self) obj.interactive()
- ssh_interactive.py
from django.contrib.auth import authenticate from backend import paramiko_ssh from web import models class SshHandler(object): """堡垒机交互脚本""" def __init__(self,argv_handler_instance): self.argv_handler_instance = argv_handler_instance # 把web的数据库传给类的对象 self.models = models def auth(self): """用户认证程序""" count = 0 while count < 3: username = input('堡垒机帐号:').strip() password = input('Password:').strip() user = authenticate(username=username,password=password) if user: # 如果用户登陆成功了,就把当前堡垒机帐号赋值给这个类的实例对象 self.user = user return True else: count += 1 def interactive(self): """启动交互脚本""" # 如果用户登陆成功,进入循环程序 if self.auth(): print('Ready to print all the authorized hosts...to this user...') while True: host_group_list = self.user.host_groups.all() for index,host_group_obj in enumerate(host_group_list): print('%s. %s[%s]'%(index,host_group_obj.name,host_group_obj.host_to_remote_users.count())) # 打印所有未分组的主机,注意:数据库里要保证单独分给用户的主机不在分组里 print('z. 未分组主机[%s]' % (self.user.host_to_remote_users.count())) choice = input('请选择主机组>>:').strip() selected_host_group = '' if choice.isdigit(): choice = int(choice) # 取出用户选择的组里所有的主机名加帐号 selected_host_group = host_group_list[choice] elif choice == 'z': # 取出未分组里所有的主机名加帐号 selected_host_group = self.user while True: for index,host_to_user_obj in enumerate(selected_host_group.host_to_remote_users.all()): print('%s. %s' % (index, host_to_user_obj)) choice = input('请选择主机>>:').strip() if choice.isdigit(): choice = int(choice) selected_host_to_user_obj = selected_host_group.host_to_remote_users.all()[choice] print('going to logon %s'%selected_host_to_user_obj) # 开始连接 paramiko_ssh.ssh_connect(self,selected_host_to_user_obj) if choice == 'b': break
- paramiko_ssh.py
import base64 from binascii import hexlify import getpass import os import select import socket import sys import time import traceback from paramiko.py3compat import input import paramiko try: import interactive except ImportError: from . import interactive def manual_auth(t,username, hostname,password): default_auth = "p" # auth = input( # "Auth by (p)assword, (r)sa key, or (d)ss key? [%s] " % default_auth # ) auth = default_auth if len(auth) == 0: auth = default_auth if auth == "r": default_path = os.path.join(os.environ["HOME"], ".ssh", "id_rsa") path = input("RSA key [%s]: " % default_path) if len(path) == 0: path = default_path try: key = paramiko.RSAKey.from_private_key_file(path) except paramiko.PasswordRequiredException: password = getpass.getpass("RSA key password: ") key = paramiko.RSAKey.from_private_key_file(path, password) t.auth_publickey(username, key) elif auth == "d": default_path = os.path.join(os.environ["HOME"], ".ssh", "id_dsa") path = input("DSS key [%s]: " % default_path) if len(path) == 0: path = default_path try: key = paramiko.DSSKey.from_private_key_file(path) except paramiko.PasswordRequiredException: password = getpass.getpass("DSS key password: ") key = paramiko.DSSKey.from_private_key_file(path, password) t.auth_publickey(username, key) else: # pw = getpass.getpass("Password for %s@%s: " % (username, hostname)) t.auth_password(username, password) def ssh_connect(ssh_handler_instance,host_to_user_obj): # now connect hostname = host_to_user_obj.host.ip_addr port = host_to_user_obj.host.port username = host_to_user_obj.remote_user.username password = host_to_user_obj.remote_user.password try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((hostname, port)) except Exception as e: print("*** Connect failed: " + str(e)) traceback.print_exc() sys.exit(1) try: t = paramiko.Transport(sock) try: t.start_client() except paramiko.SSHException: print("*** SSH negotiation failed.") sys.exit(1) try: keys = paramiko.util.load_host_keys( os.path.expanduser("~/.ssh/known_hosts") ) except IOError: try: keys = paramiko.util.load_host_keys( os.path.expanduser("~/ssh/known_hosts") ) except IOError: print("*** Unable to open host keys file") keys = {} # check server's host key -- this is important. key = t.get_remote_server_key() if hostname not in keys: print("*** WARNING: Unknown host key!") elif key.get_name() not in keys[hostname]: print("*** WARNING: Unknown host key!") elif keys[hostname][key.get_name()] != key: print("*** WARNING: Host key has changed!!!") sys.exit(1) else: print("*** Host key OK.") if not t.is_authenticated(): manual_auth(t,hostname,username ,password) if not t.is_authenticated(): print("*** Authentication failed. :(") t.close() sys.exit(1) chan = t.open_session() chan.get_pty() chan.invoke_shell() # 把堡垒机帐号赋值给chan chan.shield_account = ssh_handler_instance.user chan.host_to_user_obj = host_to_user_obj chan.models = ssh_handler_instance.models print("*** Here we go! ") # 开始连接,记录登陆日志 ssh_handler_instance.models.AuditLog.objects.create( user = ssh_handler_instance.user, log_type = 0, host_to_remote_user = host_to_user_obj, content = '***user login***' ) interactive.interactive_shell(chan) chan.close() t.close() # 记录退出日志 ssh_handler_instance.models.AuditLog.objects.create( user=ssh_handler_instance.user, log_type=2, host_to_remote_user=host_to_user_obj, content='***user logout***' ) except Exception as e: print("*** Caught exception: " + str(e.__class__) + ": " + str(e)) traceback.print_exc() try: t.close() except: pass sys.exit(1)
- interactive.py
import socket import sys,time from paramiko.py3compat import u # windows does not have termios... try: import termios import tty has_termios = True except ImportError: has_termios = False def interactive_shell(chan): if has_termios: posix_shell(chan) else: windows_shell(chan) def posix_shell(chan): """Linux环境""" import select oldtty = termios.tcgetattr(sys.stdin) try: tty.setraw(sys.stdin.fileno()) tty.setcbreak(sys.stdin.fileno()) chan.settimeout(0.0) cmd = [] while True: r, w, e = select.select([chan, sys.stdin], [], []) if chan in r: try: x = u(chan.recv(1024)) if len(x) == 0: sys.stdout.write(" *** EOF ") break sys.stdout.write(x) sys.stdout.flush() except socket.timeout: pass if sys.stdin in r: x = sys.stdin.read(1) if len(x) == 0: break if x == ' ': # 回车打印完整命令 # print('输入命令>>:',''.join(cmd)) # log = '%s %s '%(time.strftime('%Y-%m-%d %X',time.gmtime()),''.join(cmd)) # 记录用户输入命令日志 chan.models.AuditLog.objects.create( user=chan.shield_account, log_type=1, host_to_remote_user=chan.host_to_user_obj, content=''.join(cmd) ) cmd = [] else: cmd.append(x) chan.send(x) finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) # thanks to Mike Looijmans for this code def windows_shell(chan): """windows环境""" import threading print(chan.shield_account,'*******') print(chan.host_to_user_obj,'-----------') sys.stdout.write( "Line-buffered terminal emulation. Press F6 or ^Z to send EOF. " ) def writeall(sock): while True: data = sock.recv(256) if not data: sys.stdout.write(" *** EOF *** ") sys.stdout.flush() break sys.stdout.write(data) sys.stdout.flush() writer = threading.Thread(target=writeall, args=(chan,)) writer.start() try: while True: d = sys.stdin.read(1) if not d: break chan.send(d) except EOFError: # user hit ^Z or F6 pass