堡垒机的定义:
即在一个特定的网络环境下,为了保障网络和数据不受来自外部和内部用户的入侵和破坏,而运用各种技术手段实时收集和监控网络环境中每一个组成部分的系统状态、安全事件、网络活动,以便集中报警、及时处理及审计定责。
堡垒机的功能:
-
账号管理
- 设备支持统一账户管理策略,能够实现对所有服务器、网络设备、安全设备等账号进行集中管理
- 完成对账号整个生命周期的监控
- 对设备进行特殊角色设置如:审计巡检员、运维操作员、设备管理员等自定义设置,以满足审计需求
-
账号登陆
- 支持对X11、linux、unix、数据库、网络设备、安全设备等一系列授权账号进行密码的自动化周期更改
- 简化密码管理,让使用者无需记忆众多系统密码,即可实现自动登录目标设备,便捷安全
-
身份认证
- 设备提供统一的认证接口,对用户进行认证
- 支持身份认证模式包括 动态口令、静态密码、硬件key 、生物特征等多种认证方式
- 设备具有灵活的定制接口,可以与其他第三方认证服务器之间结合
- 安全的认证模式,有效提高了认证的安全性和可靠性。
-
资源授权
- 设备提供基于用户、目标设备、时间、协议类型IP、行为等要素实现细粒度的操作授权
- 最大限度保护用户资源的安全
-
访问控制
- 设备支持对不同用户进行不同策略的制定
- 细粒度的访问控制能够最大限度的保护用户资源的安全,严防非法、越权访问事件的发生
-
操作审计
- 设备能够对字符串、图形、文件传输、数据库等全程操作行为审计
- 通过设备录像方式实时监控运维人员对操作系统、安全设备、网络设备、数据库等进行的各种操作,对违规行为进行事中控制。
- 对终端指令信息能够进行精确搜索,进行录像精确定位
下面我们就使用之前所学习到的SQL alchemy来使用创建数据库,实现一个简单的堡垒机程序。
首先需要对整个表进行一个构造。下图为我们描述了整个程序所需要的模块以及表的结构。
分模块分析:
-
堡垒机账户模块:应该包括username password bind_hosts host_group
class UserProfile(Base): __tablename__ = \'user_profile\' id = Column(Integer, primary_key=True) username = Column(String(32), unique=True) password = Column(String(128)) bind_hosts = relationship(\'BindHost\',secondary = \'user_m2m_bindhost\',backref = \'user_profiles\')#通过第三表连接与bind_host的多对多关系 host_groups = relationship(\'HostGroup\',secondary = \'user_m2m_group\',backref = \'user_profiles\')#通过第三表连接与group的多对多关系 def __repr__(self): return self.username
-
账户分组模块:应该包含所有的组名,而跟user和bind_host进行关联
class HostGroup(Base): __tablename__ = \'hostgroup\' id = Column(Integer, primary_key=True) name = Column(String(64), unique=True) bind_hosts = relationship(\'BindHost\', secondary=\'bindhost_m2m_group\', backref=\'host_groups\')#通过第三表连接与bindhost的多对多关系 def __repr__(self): return self.name
-
主机模块:应该包含主机名,主机地址,端口
class Host(Base): __tablename__ = \'host\' id = Column(Integer, primary_key=True) hostname = Column(String(64), unique=True)#主机名唯一 ip = Column(String(64), unique=True)#IP唯一 port = Column(Integer, default=22) def __repr__(self): return self.hostname
-
远端模块:应该包含远端账户名,密码,授权类型
class RemoteUser(Base): __tablename__ = \'remote_user\' __table_args__ = ( UniqueConstraint(\'auth_type\', \'username\', \'password\', name=\'_user_passwd_uc\'),) # name是存在数据库里的联合唯一键,将这三者联系在一起 AuthTypes = [ #包含password和key两种认证方式 (\'ssh-password\', \'SSHH/Password\'), # 第一个真正存到数据库里的,第二个是从SQL alchemy对外显示的 (\'ssh-key\', \'SSH/KEY\'), ] id = Column(Integer, primary_key=True) auth_type = Column(ChoiceType(AuthTypes)) username = Column(String(32)) password = Column(String(128)) def __repr__(self): return self.username
-
bind_host模块:应该包含主机id和远端id
class BindHost(Base): __tablename__ = \'bindhost\' __table_args__ = (UniqueConstraint(\'host_id\', \'remoteuser_id\', name=\'_host_remote_uc\'),) # 确保联合唯一,用host_remote_uc储存 id = Column(Integer, primary_key=True) host_id = Column(Integer, ForeignKey(\'host.id\'))#外键引入host.id remoteuser_id = Column(Integer, ForeignKey(\'remote_user.id\'))#外键引入remote_user.id host = relationship(\'Host\', backref=\'bind_hosts\')#跟Host绑定 remote_user = relationship(\'RemoteUser\', backref=\'bind_hosts\')#跟RemoteUser绑定 def __repr__(self): return \'<%s -- %s>\' % (self.host.ip,self.remote_user.username)#返回主机IP和远端用户名
-
第三方表:用于堡垒机账户与bind_host的关联,堡垒机账户的分组,分组与bind_host的关联
Table(\'user_m2m_bindhost\',#堡垒机与bindhost关联
Base.metadata,
Column(\'userprofile_id\',Integer,ForeignKey(\'userprofile.id\')),
Column(\'bindhost_id\',Integer,ForeignKey(\'bindhost.id\')),
)
Table(\'bindhost_m2m_group\',#分组与bindhost关联
Base.metadata,
Column(\'bindhost_id\',Integer,ForeignKey(\'bindhost.id\')),
Column(\'hostgroup_id\',Integer,ForeignKey(\'hostgroup.id\')),
)
Table(\'user_m2m_group\',#堡垒机账户与分组关联
Base.metadata,
Column(\'userprofile_id\',Integer,ForeignKey(\'user_profile.id\')),
Column(\'hostgroup_id\',Integer,ForeignKey(\'hostgroup.id\')),
)
表结构的完整代码为如下,并存在名为modules_v2的py文件中:
__Author__ = "Panda-J" from sqlalchemy import Table, String, Integer, ForeignKey, Column, Enum, UniqueConstraint from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base from sqlalchemy_utils import ChoiceType from sqlalchemy import create_engine Base = declarative_base() Table(\'user_m2m_bindhost\',#堡垒机与bindhost关联 Base.metadata, Column(\'userprofile_id\',Integer,ForeignKey(\'user_profile.id\')), Column(\'bindhost_id\',Integer,ForeignKey(\'bindhost.id\')), ) Table(\'bindhost_m2m_group\',#分组与bindhost关联 Base.metadata, Column(\'bindhost_id\',Integer,ForeignKey(\'bindhost.id\')), Column(\'hostgroup_id\',Integer,ForeignKey(\'hostgroup.id\')), ) Table(\'user_m2m_group\',#堡垒机账户与分组关联 Base.metadata, Column(\'userprofile_id\',Integer,ForeignKey(\'user_profile.id\')), Column(\'hostgroup_id\',Integer,ForeignKey(\'hostgroup.id\')), ) class Host(Base): __tablename__ = \'host\' id = Column(Integer, primary_key=True) hostname = Column(String(64), unique=True)#主机名唯一 ip = Column(String(64), unique=True)#IP唯一 port = Column(Integer, default=22) def __repr__(self): return self.hostname # 如何建立表结构让host对应的hostgroup访问remoteuser时只有一种权限。而不是拥有remoteuser端的所有权限 # 不要让主机关联主机组,一个主机加一个用户关联在一起属于一个主机组 class HostGroup(Base): __tablename__ = \'hostgroup\' id = Column(Integer, primary_key=True) name = Column(String(64), unique=True) bind_hosts = relationship(\'BindHost\', secondary=\'bindhost_m2m_group\', backref=\'host_groups\')#通过第三表连接与bindhost的多对多关系 def __repr__(self): return self.name class RemoteUser(Base): __tablename__ = \'remote_user\' __table_args__ = ( UniqueConstraint(\'auth_type\', \'username\', \'password\', name=\'_user_passwd_uc\'),) # name是存在数据库里的联合唯一键,将这三者联系在一起 AuthTypes = [ #包含password和key两种认证方式 (\'ssh-password\', \'SSHH/Password\'), # 第一个真正存到数据库里的,第二个是从SQL alchemy对外显示的 (\'ssh-key\', \'SSH/KEY\'), ] id = Column(Integer, primary_key=True) auth_type = Column(ChoiceType(AuthTypes)) username = Column(String(32)) password = Column(String(128)) def __repr__(self): return self.username class BindHost(Base): __tablename__ = \'bindhost\' __table_args__ = (UniqueConstraint(\'host_id\', \'remoteuser_id\', name=\'_host_remote_uc\'),) # 确保联合唯一,用host_remote_uc储存 id = Column(Integer, primary_key=True) host_id = Column(Integer, ForeignKey(\'host.id\'))#外键引入host.id remoteuser_id = Column(Integer, ForeignKey(\'remote_user.id\'))#外键引入remote_user.id host = relationship(\'Host\', backref=\'bind_hosts\')#跟Host绑定 remote_user = relationship(\'RemoteUser\', backref=\'bind_hosts\')#跟RemoteUser绑定 def __repr__(self): return \'<%s -- %s>\' % (self.host.ip,self.remote_user.username)#返回主机IP和远端用户名 class UserProfile(Base): __tablename__ = \'user_profile\' id = Column(Integer, primary_key=True) username = Column(String(32), unique=True) password = Column(String(128)) bind_hosts = relationship(\'BindHost\',secondary = \'user_m2m_bindhost\',backref = \'user_profiles\')#通过第三表连接与bind_host的多对多关系 host_groups = relationship(\'HostGroup\',secondary = \'user_m2m_group\',backref = \'user_profiles\')#通过第三表连接与group的多对多关系 def __repr__(self): return self.username # class AuditLog(Base): # pass if __name__ == \'__main__\': engine = create_engine(\'mysql+pymysql://root:123456@localhost:3306/baoleiji?charset=utf8\') Base.metadata.create_all(engine)
创建完表结构,下面往表结构中创建参数,我现在这里把主干代码写出来,import的内容暂时略过,并会在注释中说明。
本项目利用yml格式进行批量导入创建参数
-
创建表
def syncdb(argvs):
print("Syncing DB....")
modules_v2.Base.metadata.create_all(engine) #创建所有表结构
-
创建host数据:
def create_hosts(argvs):
\'\'\'
create hosts
:param argvs:
:return:
\'\'\'
if \'-f\' in argvs:
hosts_file = argvs[argvs.index("-f") +1 ]#要求用户指定一个文件名,-f类似位置指示符。
else:
print_err("invalid usage, should be:\ncreate_hosts -f <the new hosts file>",quit=True)
source = yaml_parser(hosts_file)#传文件名
if source:#如果能加载回来证明有数据
print(source)
for key,val in source.items():
print(key,val)
obj = modules_v2.Host(hostname=key,ip=val.get(\'ip\'), port=val.get(\'port\') or 22)#定义一个数据库对象
session.add(obj)#加入数据库
session.commit()
-
创建堡垒机数据
def create_users(argvs):
\'\'\'
create hosts
:param argvs:
:return:
\'\'\'
if \'-f\' in argvs:
user_file = argvs[argvs.index("-f") +1 ]
else:
print_err("invalid usage, should be:\ncreateusers -f <the new users file>",quit=True)
source = yaml_parser(user_file)
if source:
for key,val in source.items():
print(key,val)
obj = modules_v2.UserProfile(username=key,password=val.get(\'password\'))#调用UserProfile函数创建数据
if val.get(\'groups\'):#如果有用户分组
groups = common_filters.group_filter(val)#进行分组的筛选,判断是否有该group
obj.groups = groups
if val.get(\'bind_hosts\'):#如果有bind_host数据
bind_hosts = common_filters.bind_hosts_filter(val)#进行bindhost的筛选,判断是否有该group
obj.bind_hosts = bind_hosts
print(obj)
session.add(obj)
session.commit()#执行添加指令
-
创建远端账户
def create_remoteusers(argvs):
\'\'\'
create remoteusers
:param argvs:
:return:
\'\'\'
if \'-f\' in argvs:
remoteusers_file = argvs[argvs.index("-f") +1 ]
else:
print_err("invalid usage, should be:\ncreate_remoteusers -f <the new remoteusers file>",quit=True)
source = yaml_parser(remoteusers_file)
if source:
for key,val in source.items():
print(key,val)
obj = modules_v2.RemoteUser(username=val.get(\'username\'),auth_type=val.get(\'auth_type\'),password=val.get(\'password\'))
session.add(obj)
session.commit()
-
创建组数据
def create_groups(argvs):
\'\'\'
create groups
:param argvs:
:return:
\'\'\'
if \'-f\' in argvs:
group_file = argvs[argvs.index("-f") +1 ]
else:
print_err("invalid usage, should be:\ncreategroups -f <the new groups file>",quit=True)
source = yaml_parser(group_file)
if source:
for key,val in source.items():
print(key,val)
obj = modules_v2.HostGroup(name=key)
if val.get(\'bind_hosts\'):
bind_hosts = common_filters.bind_hosts_filter(val)
obj.bind_hosts = bind_hosts
if val.get(\'user_profiles\'):
user_profiles = common_filters.user_profiles_filter(val)
obj.user_profiles = user_profiles
session.add(obj)
session.commit()
-
创建bind host数据
def create_bindhosts(argvs):
\'\'\'
create bind hosts
:param argvs:
:return:
\'\'\'
if \'-f\' in argvs:#相当于一个位置符号,定位用户输入的命令
bindhosts_file = argvs[argvs.index("-f") +1 ]
else:
print_err("invalid usage, should be:\ncreate_hosts -f <the new bindhosts file>",quit=True)
source = yaml_parser(bindhosts_file)#这里是使用yml批量导入。
if source:
for key,val in source.items():#对yml中的数据进行逐行提取
#print(key,val)
host_obj = session.query(modules_v2.Host).filter(modules_v2.Host.hostname==val.get(\'hostname\')).first()#查询主机
assert host_obj#断言是否有该主机,如果没有则报错。
for item in val[\'remote_users\']:#循环远端的参数
print(item )
assert item.get(\'auth_type\')#断言是否有认证类型,没有则返回错误
if item.get(\'auth_type\') == \'ssh-password\':#如果认证类型为密码认证
remoteuser_obj = session.query(modules_v2.RemoteUser).filter(
modules_v2.RemoteUser.username==item.get(\'username\'),
modules_v2.RemoteUser.password==item.get(\'password\')
).first()#判断用户密码是否正确
else:
remoteuser_obj = session.query(modules_v2.RemoteUser).filter(
modules_v2.RemoteUser.username==item.get(\'username\'),
modules_v2.RemoteUser.auth_type==item.get(\'auth_type\'),
).first()#判断认证类型和用户名是否符合ssh-key认证
if not remoteuser_obj:#如果远端数据为空
print_err("RemoteUser obj %s does not exist." % item,quit=True )#打印远端客户不存在
bindhost_obj = modules_v2.BindHost(host_id=host_obj.id,remoteuser_id=remoteuser_obj.id)#关联并添加远端id和主机id
session.add(bindhost_obj)
if source[key].get(\'groups\'):#跟Group关联
group_objs = session.query(modules_v2.HostGroup).filter(modules_v2.HostGroup.name.in_(source[key].get(\'groups\'))).all()#查询该分组是否存在
assert group_objs#断言避免数据为空
print(\'groups:\', group_objs)#打印分组数据
bindhost_obj.host_groups = group_objs#返回对应的组id
#for user_profiles this host binds to
if source[key].get(\'user_profiles\'):#跟用户关联
userprofile_objs = session.query(modules_v2.UserProfile).filter(modules_v2.UserProfile.username.in_(source[key].get(\'user_profiles\'))).all()#查询该堡垒机账户是否存在
assert userprofile_objs#断言避免错误
print("userprofiles:",userprofile_objs)#打印账户
bindhost_obj.user_profilesuser_profiles = userprofile_objs
#print(bindhost_obj)
session.commit()
其他数据类型:
-
创建认证
def auth():
\'\'\'
do the user login authentication
:return:
\'\'\'
count = 3#尝试次数
while count !=0:
username = input("\033[32;1mUsername:\033[0m").strip()#获取输入用户名
if len(username) ==0:continue
password = input("\033[32;1mPassword:\033[0m").strip()#获取用户密码
if len(password) ==0:continue
user_obj = session.query(modules_v2.UserProfile).filter(modules_v2.UserProfile.username==username,
modules_v2.UserProfile.password==password).first()#对用户名进行匹配
if user_obj:
return user_obj#如果符合数据库中数据,则返回
else:
count -= 1#尝试次数减一
print("wrong username or password, you have %s more chances." %(3-count))#如果不符合,则打印错误,并提示尝试次数
else:
print_err("too many attempts.")#尝试次数太多跳出循环
-
欢迎页面
def welcome_msg(user):
WELCOME_MSG = \'\'\'\033[32;1m
------------- Welcome [%s] login Baoleiji -------------
\033[0m\'\'\'% user.username
print(WELCOME_MSG)
-
开始任务
def start_session(argvs):
print(\'going to start sesssion \')
user = auth()#调用认证
if user:#如果有返回即认证成功
welcome_msg(user)#调用welcome
print(user.bind_hosts)#打印bindhost列表
print(user.host_groups)#打印host group列表
exit_flag = False#设置退出标志
while not exit_flag:
if user.bind_hosts:#如果有bindhost则打印
print(\'\033[32;1mz.\tungroupped hosts (%s)\033[0m\' %len(user.bind_hosts) )#返回未分组的主机数
for index,host_group in enumerate(user.host_groups):#循环打印所在的用户组列表
print(\'\033[32;1m%s.\t%s (%s)\033[0m\' %(index,host_group.name, len(host_group.bind_hosts)) )
choice = input("[%s]:" % user.username).strip()
if len(choice) == 0:continue
if choice == \'z\':
print("------ Group: ungroupped hosts ------" )
for index,bind_host in enumerate(user.bind_hosts):#打印为绑定用户组的详细数据
print(" %s.\t%s@%s(%s)"%(index,
bind_host.remote_user.username,
bind_host.host.hostname,
bind_host.host.ip,
))
print("----------- END -----------" )
elif choice.isdigit():
choice = int(choice)
if choice < len(user.host_groups):#检查数据有效性
print("------ Group: %s ------" % user.host_groups[choice].name )
for index,bind_host in enumerate(user.host_groups[choice].bind_hosts):#打印绑定的用户组详细信息
print(" %s.\t%s@%s(%s)"%(index,
bind_host.remote_user.username,
bind_host.host.hostname,
bind_host.host.ip,
))
print("----------- END -----------" )
#host selection
while not exit_flag:#如果没有退出的情况下
user_option = input("[(b)back, (q)quit, select host to login]:").strip()
if len(user_option)==0:continue#继续循环
if user_option == \'b\':break#跳出循环
if user_option == \'q\':
exit_flag=True#退出程序
if user_option.isdigit():
#进行第二层判断,即选择主机
user_option = int(user_option)
# 检查数据有效性
if user_option < len(user.host_groups[choice].bind_hosts) :#检查数据有效性
print(\'host:\',user.host_groups[choice].bind_hosts[user_option])
print(\'audit log:\',user.host_groups[choice].bind_hosts[user_option].audit_logs)#记录日志
#开始登陆程序
ssh_login.ssh_login(user,
user.host_groups[choice].bind_hosts[user_option],
session,
log_recording)
else:
print("no this option..")
-
日志记录
def log_recording(user_obj,bind_host_obj,logs):
\'\'\'
flush user operations on remote host into DB
:param user_obj:
:param bind_host_obj:
:param logs: list format [logItem1,logItem2,...]
:return:
\'\'\'
print("\033[41;1m--logs:\033[0m",logs)
session.add_all(logs)
session.commit()
其他附属程序请私信我,我将会第一时间将源码发给你。
通过以上代码就简单的搭建了一个堡垒机账户,主要有一下几个缺点:
1 安全性差:密码直接以明文的形式存在数据库中
2 重复的多对多关系(比如UserProfile分别与group和bindhost进行多对多映射,导致了重复且未完全实现未分组host的筛选功能,待完善
3 没有修改模块,一旦创建只能手工在数据库中进行修改,效率较低,待完善