堡垒机

堡垒机功能需求分析和实现

 

 

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.\t%s[%s]'%(index,host_group_obj.name,host_group_obj.host_to_remote_users.count()))

                # 打印所有未分组的主机,注意:数据库里要保证单独分给用户的主机不在分组里
                print('z.\t未分组主机[%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.\t%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!\n")
        # 开始连接,记录登陆日志
        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("\r\n*** EOF\r\n")
                        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 == '\r':
                    # 回车打印完整命令
                    # print('输入命令>>:',''.join(cmd))
                    # log = '%s  %s\n'%(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.\r\n\r\n"
    )

    def writeall(sock):
        while True:
            data = sock.recv(256)
            if not data:
                sys.stdout.write("\r\n*** EOF ***\r\n\r\n")
                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

 

posted @ 2019-03-16 21:15  Charlie大夫  阅读(421)  评论(0编辑  收藏  举报