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:[email protected]/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()View Code
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 <[email protected]> 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 passView Code
paramiko庫裏面的文件,稍微修改一下,就可以記錄cmd命令了,再寫入數據庫。
可以去官網下載,文件名interactive.py
Python 13 簡單項目-堡壘機