堡垒机表结构设计
前言: 好几天没写博客了哈,这篇博客主要介绍堡垒机的功能与作用。之前没听过堡垒机的可以看看,还较详细地讲了数据库表结构的设计,写完这篇博客,感觉数据库真是博大精深……本来还想把这个项目做完的……but ...待更新吧……
一、前景介绍
到目前为止,很多公司对堡垒机依然不太感冒,其实是没有充分认识到堡垒机在IT管理中的重要作用的,很多人觉得,堡垒机就是跳板机,其实这个认识是不全面的,跳板功能只是堡垒机所具备的功能属性中的其中一项而已,下面我就给大家介绍一下堡垒机的重要性,以帮助大家参考自己公司的业务是否需要部署堡垒机。堡垒机有以下两个至关重要的功能:
- 权限管理
- 审计功能
1. 权限管理
当你公司的服务器变的越来越多后,需要操作这些服务器的人就肯定不只是一个运维人员,同时也可能包括多个开发人员,那么这么多的人操作业务系统,如果权限分配不当就会存在很大的安全风险,举几个场景例子:
- 设想你们公司有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账户,但由于他们用的堡垒账户都是自己独有的,因此依然可以通过堡垒机控制每个运维人员访问不同的机器。
2. 审计管理
审计管理其实很简单,就是把用户的所有操作都纪录下来,以备日后的审计或者事故后的追责。在纪录用户操作的过程中有一个问题要注意,就是这个纪录对于操作用户来讲是不可见的,什么意思?就是指,无论用户愿不愿意,他的操作都会被纪录下来,并且,他自己如果不想操作被纪录下来,或想删除已纪录的内容,这些都是他做不到的,这就要求操作日志对用户来讲是不可见和不可访问的,通过堡垒机就可以很好的实现。
二、堡垒机的架构
堡垒机的主要作用权限控制和用户行为审计,堡垒机就像一个城堡的大门,城堡里的所有建筑就是你不同的业务系统 , 每个想进入城堡的人都必须经过城堡大门并经过大门守卫的授权,每个进入城堡的人必须且只能严格按守卫的分配进入指定的建筑,且每个建筑物还有自己的权限访问控制,不同级别的人可以到建筑物里不同楼层的访问级别也是不一样的。还有就是,每个进入城堡的人的所有行为和足迹都会被严格的监控和纪录下来,一旦发生犯罪事件,城堡管理人员就可以通过这些监控纪录来追踪责任人。
防火墙:
配置acl(安全控制):只能从外进入堡垒机,看到主机列表,选择后可进入。
堡垒要想成功完全起到他的作用,只靠堡垒机本身是不够的, 还需要一系列安全上对用户进行限制的配合,堡垒机部署上后,同时要确保你的网络达到以下条件:
- 所有人包括运维、开发等任何需要访问业务系统的人员,只能通过堡垒机访问业务系统
- 回收所有对业务系统的访问权限,做到除了堡垒机管理人员,没有人知道业务系统任何机器的登录密码
- 网络上限制所有人员只能通过堡垒机的跳转才能访问业务系统
- 确保除了堡垒机管理员之外,所有其它人对堡垒机本身无任何操作权限,只有一个登录跳转功能
- 确保用户的操作纪录不能被用户自己以任何方式获取到并篡改
三、堡垒机功能实现需求
业务需求:
- 兼顾业务安全目标与用户体验,堡垒机部署后,不应使用户业务系统的访问变的复杂,否则工作将很难推进,因为没人喜欢改变现状,尤其是改变后生活变得更艰难
- 保证堡垒机稳定安全运行, 没有100%的把握,不要上线任何新系统,即使有100%把握,也要做好最坏的打算,想好故障预案
功能需求:
- 所有的用户操作日志要保留在数据库中
- 每个用户登录堡垒机后,只需要选择具体要访问的设置,就连接上了,不需要再输入目标机器的访问密码
- 允许用户对不同的目标设备有不同的访问权限,例: 对10.0.2.34 有mysql 用户的权限
- 对192.168.3.22 有root用户的权限
- 对172.33.24.55 没任何权限
看到上面的需求后,我彻底懵比了 )/_\(
看到我上面写那么多文字,不知你看懂没,反正我刚开始没看懂,后来研究下表结构后,再来看,感觉意会到了不少……注意我上面加红色的字……
四、设计表结构
所有涉及mysql等数据库的产品,都必须先设计好表结构。怎么设计呢? 先大体看下面这个图:
主机表Host:
id | hostname | ip_addr | port |
组表Group:
id | name |
用户表UserInfor:
id | usrename | password |
主机帐户表HostUser: egA主机下多个帐户(eg:mysql,root...)
id | username | password | host_id |
日志表AuditLog:
id | user_id | hostuser_id | action_type | contain | date |
1. 先来讲主机与用户:
主机 | 主机帐户 | 密码 |
以第一条为例,代表A主机下有个root帐户,该帐户密码为123。
若此时再增入一条记录(第4条数据):
主机 | 主机帐户 | 密码 |
此时问题来了,第3条数据说: B主机下有个mysql帐户,密码为123;而第4条数据说: A主机下有个root帐户,密码为aaa。此时我懵比了,我到底该听谁的?? 实际上A主机上不可能有两个root用户!!
解决方法:
对主机(host_id)与主机帐户(username)进行联合唯一。下面的代码是放在HostUser表结构中: 需要导入UniqueConstraint,重点看下第4,18行代码:
class HostUser(Base): #机器帐户(eg:mysql,root...)信息表 __tablename__ = "host_user" id = Column(Integer,primary_key=True) host_id = Column(Integer, ForeignKey("host.id")) #关联外键主机(A主机下的mysql帐户...) #登陆方式有两种,通过密码,或通过KEY AuthTypes = [ (u'ssh-passwd',u'SSH/Password'), (u'ssh-key',u'SSH/KEY'), ] username = Column(String(64), unique=True, nullable=False) password = Column(String(255)) #255最长了,先做明文密码,后面改进再做密文,password可为空,当选KEY时 #HostUser通过groups可查看组的信息,反之通过hostuser_list可查看组对应主机帐户的信息 groups = relationship("Group", #关联Group表 secondary = UserInfor2HostUser, #关联第三方表 backref = "hostuser_list") #双向关联,不用在Group类中再加这句代码 #联合唯一, 约束(host_id, username)是唯一的;name是自己取的名 __table_args__ = (UniqueConstraint("host_id", "username", name="_host_username_uc"),) def __repr__(self): return "<id=%s, username=%s>" % (self.id, self.username)
2. 权限分配
因为之前做过主机管理工具,所以对于主机与组,我立马就想到主机与组进行关联。主机B可以属于多个组g1,g2; 一个组g1可以对应多个主机A,B; GOOD! 多对多关联呗。
主机名 | 组名 |
打脸时该到了,B机器有3个帐户(eg: root,mysql,ngnix), 此时代表只有有g1组的访问权限就拥有B机器下所有帐户的访问权限。这属于没仔细分析需求,得把代码删掉重新写过……
需求是最最重要的! 前面有这样一条需求: 可以对设置进行分组,允许用户访问某组机器,但对组里的不同机器依然有不同的访问权限。
解决方法:
A.root | g1 |
根据上表,A机器的root权限分配给g1; 用户若有g1的访问权限,就有A的root, B的ngnix权限。
注意: 可看前面的第4行代码,主机帐户有外键关联主机。
host_id = Column(Integer, ForeignKey("host.id")) #关联外键主机(A主机下的mysql帐户...)
上表是主机帐户表与组表的关联,属于多对多关联,因为没用到django的orm,我是用sqlalchemy,所以需要自己生成中间表HostUser2Group. 至此,组与主机的权限分配OK.
3. 组与用户的权限
这个点挺简单的啦。
g1 | u1 |
用户u1有g1,g2组的访问权限,用户u2有g1的访问权限。g1对应u1,u2; u1对应g1,g2。显然是多对多关联啦。我定义中间表UserInfor2Group。
4. 主机帐户与用户关联
需求场景:
现在一个组有100台主机,其中一台主机A的oracle出现了不可言传的损坏。公司是中移动这样的大公司,此时急需oracle厂商的技术支持,现在打电话给oracle的技术人员O,O当然需要登陆主机A进行修复查看啦。按我之前的实现,此时堡垒机的管理者要先为O分配一个组,组包含该损坏的主机A。这这这,太麻烦了,而且O一修复完,堡垒机的管理者还需要将刚刚为O分配的组删除。
解决方法:
将特定的用户与主机帐户表直接进行关联,不用再通过组。
B.oracle | u1 |
显然,这是多对多关联~~ 我定义了中间表UserInfor2HostUser
5. 日志表
关于日志表的需求有: 所有的用户操作日志要保留在数据库中。
#日志表 class AuditLog(Base): __tablename__ = 'audit_log' id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey('user_infor.id')) #关联主机帐户表 hostuser_id = Column(Integer, ForeignKey('host_user.id')) """ action_choices = [ (0, 'CMD'), (1, 'Login'), (2, 'Logout'), (3, 'GetFile'), (4, 'SendFile'), (5, 'Exception'), ] """ action_choices2 = [ (u'cmd', u'CMD'), #cmd是存到数据库中的,CMD是显示给用户看的 (u'login', u'Login'), (u'logout', u'Logout'), # (3,'GetFile'), # (4,'SendFile'), # (5,'Exception'), ] #ChoiceType是第三方插件 action_type = Column(ChoiceType(action_choices2)) # action_type = Column(String(64)) contain = Column(String(255)) #包含命令cmd,登陆login,logout... date = Column(DateTime) #关联用户表 user_infor = relationship("UserInfor")
日志表,肯定要记录是哪个用户操作的吧! 因此外键关联用户的id
user_id = Column(Integer, ForeignKey('user_infor.id'))
你肯定要知道用户是在哪台机器上操作的吧,单单知道在哪台机器操作是不够的,还要知道在该机器下哪个帐户(eg:root,mysql...), 因此需要外键关联主机帐户表。
hostuser_id = Column(Integer, ForeignKey('host_user.id'))
表结构总代码:
""" 表结构 """ from sqlalchemy import create_engine, Table from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String,ForeignKey,UniqueConstraint,\ DateTime from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy_utils import ChoiceType Base = declarative_base() # 生成一个SqlORM 基类(已经封闭metadata) #直接创建中间间表并返回表的实例 HostUser2Group主动关联HostUser与Group(被关联) #机器的帐户(eg:A.root,A.mysql,B.root...)与主机组(g1,g2...)多对多关联 HostUser2Group = Table('hostuser_to_group', Base.metadata, Column('hostuser_id', ForeignKey('host_user.id'), primary_key=True), Column('group_id', ForeignKey('group.id'), primary_key=True), #一个表为什么能创建两个主键(其实是两个列同时作为主键,非空且唯一) #PRIMARY KEY (hostuser_id, group_id), ) #堡垒机用户信息表(eg:运维人员,u1,u2...)与主机组(g1,g2...)多对多关联 UserInfor2Group = Table('userinfor_to_group', Base.metadata, Column('userinfor_id', ForeignKey('user_infor.id'), primary_key=True), Column('group_id', ForeignKey('group.id'), primary_key=True), ) #堡垒机用户信息表(eg:运维人员,u1,u2...)与主机的帐户(eg:oracle...)多对多关联 #需求,主机的oracle坏了,需求厂家的工作人员的维修。此时工作人员帐户跳过分组进入主机的oracle帐户 UserInfor2HostUser = Table('userinfor_to_hostuser', Base.metadata, Column('userinfor_id', ForeignKey('user_infor.id'), primary_key=True), Column('hostuser_id', ForeignKey('host_user.id'), primary_key=True), ) class Host(Base): #主机表 __tablename__ = "host" #表名 id = Column(Integer, primary_key=True, autoincrement=True) # 默认自增 hostname = Column(String(64), unique=True, nullable=False) #唯一且不为空 ip_addr = Column(String(128), unique=True, nullable=False) port = Column(Integer, default=22) def __repr__(self): return "<id=%s, hostname=%s, ip_addr=%s, port=%s>" % (self.id, self.hostname, self.ip_addr, self.port) class Group(Base): #主机组 __tablename__ = "group" id = Column(Integer,primary_key=True) name = Column(String(64), unique=True, nullable=False) def __repr__(self): return "<id=%s, name=%s>" % (self.id, self.name) class UserInfor(Base): #堡垒机用户信息表(eg:运维人员...) __tablename__ = "user_infor" id = Column(Integer,primary_key=True) username = Column(String(64), unique=True, nullable=False) password = Column(String(255), nullable=True) #255最长了,先做明文密码,后面改进再做密文 groups = relationship("Group", #关联Group表 secondary = UserInfor2Group, #关联第三方表 backref = "user_list") #双向关联,不用在Group类中再加这句代码 hostuser_list = relationship("HostUser", # 关联Group表 secondary=HostUser2Group, # 关联第三方表 backref="user_list") # 双向关联,不用在Group类中再加这句代码 def __repr__(self): return "<id=%s, username=%s>" % (self.id, self.username) class HostUser(Base): #机器帐户(eg:mysql,root...)信息表 __tablename__ = "host_user" id = Column(Integer,primary_key=True) host_id = Column(Integer, ForeignKey("host.id")) #关联外键主机(A主机下的mysql帐户...) #登陆方式有两种,通过密码,或通过KEY AuthTypes = [ (u'ssh-passwd',u'SSH/Password'), (u'ssh-key',u'SSH/KEY'), ] username = Column(String(64), unique=True, nullable=False) password = Column(String(255)) #255最长了,先做明文密码,后面改进再做密文,password可为空,当选KEY时 #HostUser通过groups可查看组的信息,反之通过hostuser_list可查看组对应主机帐户的信息 groups = relationship("Group", #关联Group表 secondary = UserInfor2HostUser, #关联第三方表 backref = "hostuser_list") #双向关联,不用在Group类中再加这句代码 #联合唯一, 约束(host_id, username)是唯一的;name是自己取的名 __table_args__ = (UniqueConstraint("host_id", "username", name="_host_username_uc"),) def __repr__(self): return "<id=%s, username=%s>" % (self.id, self.username) #日志表 class AuditLog(Base): __tablename__ = 'audit_log' id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey('user_infor.id')) #关联主机帐户表 hostuser_id = Column(Integer, ForeignKey('host_user.id')) """ action_choices = [ (0, 'CMD'), (1, 'Login'), (2, 'Logout'), (3, 'GetFile'), (4, 'SendFile'), (5, 'Exception'), ] """ action_choices2 = [ (u'cmd', u'CMD'), #cmd是存到数据库中的,CMD是显示给用户看的 (u'login', u'Login'), (u'logout', u'Logout'), # (3,'GetFile'), # (4,'SendFile'), # (5,'Exception'), ] #ChoiceType是第三方插件 action_type = Column(ChoiceType(action_choices2)) # action_type = Column(String(64)) contain = Column(String(255)) #包含命令cmd,登陆login,logout... date = Column(DateTime) #关联用户表 user_infor = relationship("UserInfor") #echo=True可以查看创建表的过程 engine = create_engine("mysql+pymysql://root:root@localhost:3306/fortress_machine", echo=True) Base.metadata.create_all(engine) # 创建所有表结构View Code
五、补充
1. 补充-1
Python 中也可以所用 sys 的 sys.argv 来获取命令行参数:
- sys.argv 是命令行参数列表。
- len(sys.argv) 是命令行参数个数。
import sys print("A:", len(sys.argv)) print("B:", str(sys.argv))
输出:
C:\Users\Administrator\PycharmProjects\laonanhai\堡垒机\LittleFinger\modules>python test.py aa bb A: 3 B: ['test.py', 'aa', 'bb'] C:\Users\Administrator\PycharmProjects\laonanhai\堡垒机\LittleFinger\modules>python test.py aa bb A: 3 B: ['test.py', 'aa', 'bb']
若加上下面这句代码:
print("C:",sys.argv[1])
输出:
C:\Users\Administrator\PycharmProjects\laonanhai\堡垒机\LittleFinger\modules>python test.py aa bb A: 3 B: ['test.py', 'aa', 'bb'] C: aa
2. 补充-2
ssh公钥登录过程
使用密码登录,每次都必须输入密码,非常麻烦。好在SSH还提供了公钥登录,可以省去输入密码的步骤。
所谓"公钥登录",原理很简单,就是用户将自己的公钥储存在远程主机上。登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码。
这种方法要求用户必须提供自己的公钥。如果没有现成的,可以直接用ssh-keygen生成一个:
$ ssh-keygen
运行上面的命令以后,系统会出现一系列提示,可以一路回车。其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,这里可以设置一个。运行结束以后,在$HOME/.ssh/目录下,会新生成两个文件:id_rsa.pub和id_rsa。前者是你的公钥,后者是你的私钥。
这时再输入下面的命令,将公钥传送到远程主机host上面:
$ ssh-copy-id user@host
好了,从此你再登录,就不需要输入密码了。
参考博客: http://www.cnblogs.com/alex3714/articles/5286889.html
坑爹啊,alex大王的表结构设计和我的不一样……代码看得我一脸蒙比。
完整示例代码: https://github.com/triaquae/py3_training/tree/master/%E5%A0%A1%E5%9E%92%E6%9C%BA
如果有做过这个项目或将要做这个小项目的,博客写得比较详细的,可以发博客链接给我,让我学习学习,万分感谢。
待更新……