Python 13 简单项目-堡垒机
本节内容
项目实战:运维堡垒机开发
前景介绍
到目前为止,很多公司对堡垒机依然不太感冒,其实是没有充分认识到堡垒机在IT管理中的重要作用的,很多人觉得,堡垒机就是跳板机,其实这个认识是不全面的,跳板功能只是堡垒机所具备的功能属性中的其中一项而已,下面我就给大家介绍一下堡垒机的重要性,以帮助大家参考自己公司的业务是否需要部署堡垒机。
堡垒机有以下两个至关重要的功能:
权限管理
当你公司的服务器变的越来越多后,需要操作这些服务器的人就肯定不只是一个运维人员,同时也可能包括多个开发人员,那么这么多的人操作业务系统,如果权限分配不当就会存在很大的安全风险,举几个场景例子:
-
设想你们公司有300台Linux服务器,A开发人员需要登录其中5台WEB服务器查看日志或进行问题追踪等事务,同时对另外10台hadoop服务器有root权限,在有300台服务器规模的网络中,按常理来讲你是已经使用了ldap权限统一认证的,你如何使这个开发人员只能以普通用户的身份登录5台web服务器,并且同时允许他以管理员的身份登录另外10台hadoop服务器呢?并且同时他对其它剩下的200多台服务器没有访问权限
-
目前据我了解,很多公司的运维团队为了方面,整个运维团队的运维人员还是共享同一套root密码,这样内部信任机制虽然使大家的工作方便了,但同时存在着极大的安全隐患,很多情况下,一个运维人员只需要管理固定数量的服务器,毕竟公司分为不同的业务线,不同的运维人员管理的业务线也不同,但如果共享一套root密码,其实就等于无限放大了每个运维人员的权限,也就是说,如果某个运维人员想干坏事的话,他可以在几分钟内把整个公司的业务停转,甚至数据都给删除掉。为了降低风险,于是有人想到,把不同业务线的root密码改掉就ok了么,也就是每个业务线的运维人员只知道自己的密码,这当然是最简单有效的方式,但问题是如果你同时用了ldap,这样做又比较麻烦,即使你设置了root不通过ldap认证,那新问题就是,每次有运维人员离职,他所在的业务线的密码都需要重新改一次。
其实上面的问题,我觉得可以很简单的通过堡垒机来实现,收回所有人员的直接登录服务器的权限,所有的登录动作都通过堡垒机授权,运维人员或开发人员不知道远程服务器的密码,这些远程机器的用户信息都绑定在了堡垒机上,堡垒机用户只能看到他能用什么权限访问哪些远程服务器。
在回收了运维或开发人员直接登录远程服务器的权限后,其实就等于你们公司生产系统的所有认证过程都通过堡垒机来完成了,堡垒机等于成了你们生产系统的SSO(single sign on)模块了。你只需要在堡垒机上添加几条规则就能实现以下权限控制了:
-
允许A开发人员通过普通用户登录5台web服务器,通过root权限登录10台hadoop服务器,但对其余的服务器无任务访问权限
-
多个运维人员可以共享一个root账户,但是依然能分辨出分别是谁在哪些服务器上操作了哪些命令,因为堡垒机账户是每个人独有的,也就是说虽然所有运维人员共享了一同一个远程root账户,但由于他们用的堡垒账户都是自己独有的,因此依然可以通过堡垒机控制每个运维人员访问不同的机器。
审计管理
审计管理其实很简单,就是把用户的所有操作都纪录下来,以备日后的审计或者事故后的追责。在纪录用户操作的过程中有一个问题要注意,就是这个纪录对于操作用户来讲是不可见的,什么意思?就是指,无论用户愿不愿意,他的操作都会被纪录下来,并且,他自己如果不想操作被纪录下来,或想删除已纪录的内容,这些都是他做不到的,这就要求操作日志对用户来讲是不可见和不可访问的,通过堡垒机就可以很好的实现。
堡垒机架构
堡垒机的主要作用权限控制和用户行为审计,堡垒机就像一个城堡的大门,城堡里的所有建筑就是你不同的业务系统 , 每个想进入城堡的人都必须经过城堡大门并经过大门守卫的授权,每个进入城堡的人必须且只能严格按守卫的分配进入指定的建筑,且每个建筑物还有自己的权限访问控制,不同级别的人可以到建筑物里不同楼层的访问级别也是不一样的。还有就是,每个进入城堡的人的所有行为和足迹都会被严格的监控和纪录下来,一旦发生犯罪事件,城堡管理人员就可以通过这些监控纪录来追踪责任人。
堡垒要想成功完全记到他的作用,只靠堡垒机本身是不够的, 还需要一系列安全上对用户进行限制的配合,堡垒机部署上后,同时要确保你的网络达到以下条件:
- 所有人包括运维、开发等任何需要访问业务系统的人员,只能通过堡垒机访问业务系统
- 回收所有对业务系统的访问权限,做到除了堡垒机管理人员,没有人知道业务系统任何机器的登录密码
- 网络上限制所有人员只能通过堡垒机的跳转才能访问业务系统
- 确保除了堡垒机管理员之外,所有其它人对堡垒机本身无任何操作权限,只有一个登录跳转功能
- 确保用户的操作纪录不能被用户自己以任何方式获取到并篡改
堡垒机功能实现需求
业务需求:
- 兼顾业务安全目标与用户体验,堡垒机部署后,不应使用户访问业务系统的访问变的复杂,否则工作将很难推进,因为没人喜欢改变现状,尤其是改变后生活变得更艰难
- 保证堡垒机稳定安全运行, 没有100%的把握,不要上线任何新系统,即使有100%把握,也要做好最坏的打算,想好故障预案
功能需求:
- 所有的用户操作日志要保留在数据库中
- 每个用户登录堡垒机后,只需要选择具体要访问的设置,就连接上了,不需要再输入目标机器的访问密码
- 允许用户对不同的目标设备有不同的访问权限,例:
- 对10.0.2.34 有mysql 用户的权限
- 对192.168.3.22 有root用户的权限
- 对172.33.24.55 没任何权限
- 分组管理,即可以对设置进行分组,允许用户访问某组机器,但对组里的不同机器依然有不同的访问权限
这段sqlalchemy建立表结构的代码就有点凶残了,慢慢看,总而言之,表结构设计好了就没有什么很难的东西了。就是这样,sqlalchemy用多了就熟悉了。
1 #!/user/bin/env python 2 # -*-coding: utf-8-*- 3 4 from sqlalchemy import Table, Column, String, Integer, create_engine, ForeignKey, UniqueConstraint, DateTime 5 from sqlalchemy.orm import sessionmaker, relationship 6 from sqlalchemy.ext.declarative import declarative_base 7 from sqlalchemy_utils import ChoiceType 8 9 Base = declarative_base() # 生成一个ORM基类 10 11 userprofile_2_hostgroup = Table('userprofile_2_hostgroup', Base.metadata, # 堡垒机登录名跟主机组的关联关系 12 Column('userprofile_id', ForeignKey('user_profile.id')), 13 Column('hostgroup_id', ForeignKey('host_group.id')) 14 ) 15 16 bindhost_2_hostgroup = Table("bindhost_2_hostgroup", Base.metadata, 17 Column('bind_host_id', ForeignKey('bind_host.id')), 18 Column('host_group_id', ForeignKey('host_group.id')) 19 ) 20 21 userprofile_2_bindhost = Table('userprofile_2_bindhost', Base.metadata, 22 Column('user_profile_id', ForeignKey('user_profile.id')), 23 Column('bind_host_id', ForeignKey('bind_host.id')) 24 ) 25 26 27 class UserProfile(Base): 28 """登陆堡垒机账户表:id, 用户名, 密码""" 29 __tablename__ = "user_profile" 30 id = Column(Integer, primary_key=True, autoincrement=True) 31 username = Column(String(64), unique=True, nullable=False) 32 password = Column(String(128), unique=True, nullable=False) 33 34 host_groups = relationship('HostGroup', secondary=userprofile_2_hostgroup, backref='user_profile') 35 bind_hosts = relationship('BindHost', secondary=userprofile_2_bindhost, backref='user_profile') 36 audit_logs = relationship('AuditLog') 37 38 def __repr__(self): 39 return "<UserProfile(id='%s',username='%s')>" % (self.id, 40 self.username) 41 42 43 class HostGroup(Base): 44 """主机组:id, 主机组名称""" 45 __tablename__ = "host_group" 46 id = Column(Integer, primary_key=True, autoincrement=True) 47 name = Column(String(64), unique=True, nullable=False) 48 user_profiles = relationship('UserProfile', secondary=userprofile_2_hostgroup, backref='host_group') 49 bind_hosts = relationship('BindHost', secondary=bindhost_2_hostgroup, backref='host_group') 50 51 def __repr__(self): 52 return "<HostGroup(id='%s',name='%s')>" % (self.id, 53 self.name) 54 55 56 class Host(Base): 57 """主机列表: id, hostname, ip, port """ 58 __tablename__ = "host" 59 id = Column(Integer, primary_key=True, autoincrement=True) 60 hostname = Column(String(64), unique=True, nullable=False) 61 ip = Column(String(64), unique=True, nullable=False) 62 port = Column(Integer, default=22) 63 64 def __repr__(self): 65 return "<Host(id='%s',hostname='%s')>" % (self.id, 66 self.hostname) 67 68 69 class RemoteUser(Base): 70 __tablename__ = "remote_user" 71 AuthType = [ 72 (u'ssh-password', u'SSH/Password'), 73 (u'ssh-key', u'SSH/KEY') 74 ] 75 id = Column(Integer, primary_key=True, autoincrement=True) 76 username = Column(String(64), nullable=False) 77 password = Column(String(128)) 78 auth_type = Column(ChoiceType(AuthType)) 79 __table_args__ = (UniqueConstraint('auth_type', 'username', 'password', name="_user_psd_uc"),) 80 def __repr__(self): 81 return "<Remote User(id='%s', user='%s', auth_type='%s')>" % (self.id, self.username, self.auth_type) 82 83 84 class BindHost(Base): 85 """主机对应主机登陆名关联""" 86 __tablename__ = 'bind_host' 87 id = Column(Integer, primary_key=True, autoincrement=True) 88 host_id = Column(Integer, ForeignKey('host.id')) 89 remote_user_id = Column(Integer, ForeignKey('remote_user.id')) 90 91 host = relationship('Host') 92 remote_user = relationship('RemoteUser') 93 audit_logs = relationship('AuditLog') 94 host_groups = relationship("HostGroup", secondary=bindhost_2_hostgroup, backref='bind_host') 95 user_profiles = relationship("UserProfile", secondary=userprofile_2_bindhost, backref='bind_host') 96 97 __table_args__ = (UniqueConstraint('host_id', 'remote_user_id', name='_host_remote_user_uc'), ) 98 99 def __repr__(self): 100 return "<BindHost(ID='%s', host_name='%s', remote_user_name='%s')>" % (self.id, 101 self.host.hostname, 102 self.remote_user.username) 103 104 105 class AuditLog(Base): 106 """audit table 记录user的每一条命令""" 107 __tablename__ = 'audit_log' 108 id = Column(Integer, primary_key=True, autoincrement=True) 109 user_id = Column(Integer, ForeignKey('user_profile.id')) 110 bind_host_id = Column(Integer, ForeignKey('bind_host.id')) 111 112 action_list = [ 113 (u'cmd', u'CMD'), 114 (u'login', u'Login'), 115 (u'logout', u'Logout'), 116 ] 117 118 action_type = Column(ChoiceType(action_list)) 119 cmd = Column(String(255)) 120 action_date = Column(DateTime) 121 122 user_profiles = relationship('UserProfile') 123 bind_hosts = relationship('BindHost') 124 125 def __repr__(self): 126 return "<Audit Log(user:'%s', host:'%s', action:'%s', cmd:'%s', date time:'%s')>" % \ 127 (self.user_profiles.username, self.bind_hosts.host.name, self.action_type, self.cmd, self.action_date) 128 129 if __name__ == '__main__': 130 sql = "mysql+pymysql://root:password.1@192.168.85.100/test1?charset=utf8" 131 engine = create_engine(sql) 132 Base.metadata.create_all(engine) 133 134 SessionCls = sessionmaker(bind=engine) 135 session = SessionCls() 136 137 obj = UserProfile(username='dandy', password='password.1') 138 session.add(obj) 139 session.commit()
ssh公钥登录过程
使用密码登录,每次都必须输入密码,非常麻烦。好在SSH还提供了公钥登录,可以省去输入密码的步骤。
所谓"公钥登录",原理很简单,就是用户将自己的公钥储存在远程主机上。登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码。
这种方法要求用户必须提供自己的公钥。如果没有现成的,可以直接用ssh-keygen生成一个:
$ ssh-keygen
运行上面的命令以后,系统会出现一系列提示,可以一路回车。其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,这里可以设置一个。
运行结束以后,在$HOME/.ssh/目录下,会新生成两个文件:id_rsa.pub和id_rsa。前者是你的公钥,后者是你的私钥。
这时再输入下面的命令,将公钥传送到远程主机host上面:
$ ssh-copy-id user@host
好了,从此你再登录,就不需要输入密码了。
附上Alex的代码:https://github.com/triaquae/py3_training/tree/master/%E5%A0%A1%E5%9E%92%E6%9C%BA
1 # Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> 2 # 3 # This file is part of paramiko. 4 # 5 # Paramiko is free software; you can redistribute it and/or modify it under the 6 # terms of the GNU Lesser General Public License as published by the Free 7 # Software Foundation; either version 2.1 of the License, or (at your option) 8 # any later version. 9 # 10 # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 12 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 # details. 14 # 15 # You should have received a copy of the GNU Lesser General Public License 16 # along with Paramiko; if not, write to the Free Software Foundation, Inc., 17 # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. 18 19 20 import socket 21 import sys 22 from core import models 23 import datetime 24 from paramiko.py3compat import u 25 26 # windows does not have termios... 27 try: 28 import termios 29 import tty 30 has_termios = True 31 except ImportError: 32 has_termios = False 33 34 35 def interactive_shell(chan, user_obj, bind_host_obj, log_recording): 36 if has_termios: 37 posix_shell(chan, user_obj, bind_host_obj, log_recording) 38 else: 39 windows_shell(chan, user_obj, bind_host_obj, log_recording) 40 41 42 def posix_shell(chan, user_obj, bind_host_obj, log_recording): 43 import select 44 45 oldtty = termios.tcgetattr(sys.stdin) 46 try: 47 tty.setraw(sys.stdin.fileno()) 48 tty.setcbreak(sys.stdin.fileno()) 49 chan.settimeout(0.0) 50 cmd_list = [] 51 52 while True: 53 r, w, e = select.select([chan, sys.stdin], [], []) 54 if chan in r: 55 try: 56 x = u(chan.recv(1024)) 57 if len(x) == 0: 58 sys.stdout.write('\r\n*** EOF\r\n') 59 break 60 sys.stdout.write(x) 61 sys.stdout.flush() 62 except socket.timeout: 63 pass 64 if sys.stdin in r: 65 x = sys.stdin.read(1) 66 if len(x) == 0: 67 break 68 if x == '\r': 69 cmd = ''.join(cmd_list) 70 obj = models.AuditLog(user_id=user_obj.id, 71 bind_host_id=bind_host_obj.id, 72 action_type='cmd', 73 cmd=cmd, 74 action_date=datetime.datetime.now()) 75 log_recording(obj) 76 cmd_list.clear() 77 else: 78 cmd_list.append(x) 79 chan.send(x) 80 81 finally: 82 termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) 83 84 85 # thanks to Mike Looijmans for this code 86 def windows_shell(chan, user_obj, bind_host_obj, log_recording): 87 import threading 88 89 sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n") 90 91 def writeall(sock): 92 while True: 93 data = sock.recv(256) 94 if not data: 95 sys.stdout.write('\r\n*** EOF ***\r\n\r\n') 96 sys.stdout.flush() 97 break 98 sys.stdout.write(data) 99 sys.stdout.flush() 100 101 writer = threading.Thread(target=writeall, args=(chan,)) 102 writer.start() 103 104 try: 105 while True: 106 d = sys.stdin.read(1) 107 if not d: 108 break 109 chan.send(d) 110 except EOFError: 111 # user hit ^Z or F6 112 pass
paramiko库里面的文件,稍微修改一下,就可以记录cmd命令了,再写入数据库。
可以去官网下载,文件名interactive.py