前言
Jumpserver 作為國內流行的開源堡壘機,很多公司都在嘗試使用,同時 Jumpserver 為了契合眾多公司的用戶認證,也提供了 LDAP 的用戶認證方式,作為 Jumpserver 的用戶,大家可能知道了 Jumpserver 的 LDAP 認證方式,僅是作為 登錄Jumpserver Web UI、登錄 Jumpserver 終端(COCO) 的用戶認證,進入 Jumpserver 終端(COCO)后,再而跳到目標主機,卻需要使用Jumpserver 創建的系統用戶,也就是 登錄Jumpserver 和 Jumpserver登錄目標主機 是需要兩個完全沒有關系的用戶,對於很多基於LDAP用戶登錄主機的場景,Jumpserver 這種雙用戶認證概念顯得有點雞肋,既然接入了 LDAP, 我們希望做到 登錄Jumpserver 和 Jumpserver跳轉主機都使用 LDAP 完成認證登錄,帶着這一想法,便開始了對 Jumpserver 終端核心 COCO 進行了部分修改。
COCO前后對比
注:LDAP 用戶登錄Jumpserver coco、選擇登錄主機后,直接使用登錄coco 的用戶進行登錄主機,取消了選擇系統用戶的步驟。
詳細流程
注1:用戶名密碼登錄 Jumpserver 時,COCO 處理線程存儲用戶名密碼,用於SSH 連接目標主機;
注2:公鑰登錄 Jumpserver 時,COCO 處理線程存儲用戶名和空密碼,SSH 連接目標主機時,根據用戶名從COCO本地查找密碼,有則使用,無則提示輸出密碼;
注3:SSH 連接認證失敗可嘗試輸入密碼嘗試三次,認證成功則向本地存儲最近一次連接成功的加密密碼。
代碼實現
修改 coco/models.py,添加 password 參數
1 class Request: 2 def __init__(self, addr): 3 self.type = [] 4 self.meta = {"width": 80, "height": 24} 5 self.user = None 6 self.password = '' # @ 周旺 7 self.addr = addr 8 self.remote_ip = self.addr[0] 9 self.change_size_event = threading.Event() 10 self.date_start = datetime.datetime.now() 11 12 13 class Client: 14 def __init__(self, chan, request): 15 self.chan = chan 16 self.request = request 17 self.user = request.user 18 self.password = request.password # @ 周旺 19 self.addr = request.addr
修改 coco/interface.py, 賦值 request.password
1 class SSHInterface(paramiko.ServerInterface): 2 def validate_auth(self, username, password="", public_key=""): 3 info = app_service.authenticate( 4 username, password=password, public_key=public_key, 5 remote_addr=self.request.remote_ip 6 ) 7 user = info.get('user', None) 8 if user: 9 self.request.user = user 10 self.request.password = password # request password 賦值 @ 周旺 11 self.info = info 12 13 seed = info.get('seed', None) 14 token = info.get('token', None) 15 if seed and not token: 16 self.otp_auth = True 17 18 return user
修改 coco/interactive.py
1 class InteractiveServer: 2 def display_search_result(self): 3 sort_by = current_app.config["ASSET_LIST_SORT_BY"] 4 self.search_result = sort_assets(self.search_result, sort_by) 5 fake_data = [_("ID"), _("Hostname"), _("IP"), _("LoginAs")] 6 id_length = max(len(str(len(self.search_result))), 4) 7 hostname_length = item_max_length(self.search_result, 15, 8 key=lambda x: x.hostname) 9 sysuser_length = item_max_length(self.search_result, 10 key=lambda x: x.system_users_name_list) 11 size_list = [id_length, hostname_length, 16, sysuser_length] 12 header_without_comment = format_with_zh(size_list, *fake_data) 13 comment_length = max( 14 self.request.meta["width"] - 15 size_of_str_with_zh(header_without_comment) - 1, 16 2 17 ) 18 size_list.append(comment_length) 19 fake_data.append(_("Comment")) 20 self.client.send(wr(title(format_with_zh(size_list, *fake_data)))) 21 for index, asset in enumerate(self.search_result, 1): 22 # data = [ # 注釋主機顯示列表 @ 周旺 23 # index, asset.hostname, asset.ip, 24 # asset.system_users_name_list, asset.comment 25 # ] 26 27 data = [ # 主機顯示列表 @ 周旺 28 index, asset.hostname, asset.ip, 29 self.client.user.username, asset.comment 30 ] 31 32 self.client.send(wr(format_with_zh(size_list, *data))) 33 self.client.send(wr(_("總共: {} 匹配: {}").format( 34 len(self.assets), len(self.search_result)), before=1) 35 ) 36 37 def proxy(self, asset): 38 # system_user = self.choose_system_user(asset.system_users_granted) # 注釋 @ 周旺 39 # if system_user is None: 40 # self.client.send(_("沒有系統用戶")) 41 # return 42 system_user = self.client.user # 修改系統用戶為登錄用戶 @ 周旺 注: 仍保持system_user 變量名,后面所有 system_user 皆是登錄用戶 43 password = self.client.password # 密碼 @ 周旺 --> by client -> by request 44 forwarder = ProxyServer(self.client) 45 forwarder.proxy(asset, system_user, password) # password @ 周旺
修改 coco/proxy.py
1 class ProxyServer: 2 def proxy(self, asset, system_user, password=''): # 添加 password 參數 @ 周旺 3 #self.get_system_user_auth(system_user) # 注釋 @ 周旺 4 5 if not password: # 添加46-74行 @ 周旺 6 with open('/opt/pwd/%s.pwd' % system_user.username, 'ab+') as pwd: # 查找本地緩存密碼 7 pwd.seek(0) 8 try: 9 password = base64.b64decode(pwd.read().strip()).decode().strip() 10 # password = pwd.read().strip() 11 except: 12 password = '' 13 14 if not password: 15 prompt = "{}@{} password: ".format(system_user.username, asset.ip) 16 password = net_input(self.client, prompt=prompt, sensitive=True) 17 18 for n in range(4): 19 self.connecting = True 20 self.send_connecting_message(asset, system_user) 21 self.server = self.get_server_conn(asset, system_user, password) 22 if self.server: 23 with open('/opt/pwd/%s.pwd' % system_user.username, 'wb') as pwd: # 保存最后一次的正確密碼 24 pwd.write(base64.b64encode(password.encode(encoding='utf-8'))) 25 #pwd.write(password) 26 break 27 28 if n < 3: 29 prompt = "{}@{} password({}/3): ".format(system_user.username, asset.ip, n+1) 30 password = net_input(self.client, prompt=prompt, sensitive=True) 31 else: 32 return False 33 34 35 # self.send_connecting_message(asset, system_user) # 注釋 @ 周旺 36 # self.server = self.get_server_conn(asset, system_user, password) 37 38 command_recorder = current_app.new_command_recorder() 39 replay_recorder = current_app.new_replay_recorder() 40 session = Session( 41 self.client, self.server, 42 command_recorder=command_recorder, 43 replay_recorder=replay_recorder, 44 ) 45 current_app.add_session(session) 46 self.watch_win_size_change_async() 47 session.bridge() 48 self.stop_event.set() 49 self.end_watch_win_size_change() 50 current_app.remove_session(session) 51 52 def get_server_conn(self, asset, system_user, password=''): # 添加 password 參數 @ 周旺 53 logger.info("Connect to {}".format(asset.hostname)) 54 # if not self.validate_permission(asset, system_user): # 注釋 @ 周旺 55 # self.client.send(warning('No permission')) 56 # return None 57 # if True: 58 # server = self.get_ssh_server_conn(asset, system_user) 59 # else: 60 # server = self.get_ssh_server_conn(asset, system_user) 61 62 server = self.get_ssh_server_conn(asset, system_user, password) # password @ 周旺 63 return server 64 65 def get_ssh_server_conn(self, asset, system_user, password=''): # 添加 password 參數 @ 周旺 66 request = self.client.request 67 term = request.meta.get('term', 'xterm') 68 width = request.meta.get('width', 80) 69 height = request.meta.get('height', 24) 70 ssh = SSHConnection() 71 chan, sock, msg = ssh.get_channel( 72 asset, system_user, term=term, width=width, height=height, password=password) # password @ 周旺 73 if not chan: 74 self.client.send(warning(wr(msg, before=1, after=0))) 75 server = None 76 else: 77 server = Server(chan, sock, asset, system_user) 78 self.connecting = False 79 self.client.send(b'\r\n') 80 return server 81 82 def send_connecting_message(self, asset, system_user): 83 def func(): 84 delay = 0.0 85 self.client.send('Connecting to {}@{} {:.1f}'.format( 86 system_user.username, asset.ip, delay) # 修改為 用戶名,ip地址 @ 周旺 87 ) 88 while self.connecting and delay < TIMEOUT: 89 self.client.send('\x08\x08\x08{:.1f}'.format(delay).encode()) 90 time.sleep(0.1) 91 delay += 0.1 92 thread = threading.Thread(target=func) 93 thread.start()
修改 coco/connection.py
1 class SSHConnection: 2 def get_ssh_client(self, asset, system_user, password=''): # 添加 password 參數 @ 周旺 3 ssh = paramiko.SSHClient() 4 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 5 sock = None 6 7 # if not system_user.password and not system_user.private_key: # 注釋 @ 周旺 8 # self.get_system_user_auth(system_user) 9 10 if asset.domain: 11 sock = self.get_proxy_sock_v2(asset) 12 try: 13 ssh.connect( 14 asset.ip, port=asset.port, username=system_user.username, 15 #password=system_user.password, pkey=system_user.private_key, # 注釋 @ 周旺 16 password=password, # password @ 周旺 17 timeout=TIMEOUT, compress=True, auth_timeout=TIMEOUT, 18 look_for_keys=False, sock=sock 19 ) 20 except (paramiko.AuthenticationException, 21 paramiko.BadAuthenticationType, 22 SSHException) as e: 23 # password_short = "None" # 注釋 @ 周旺 注:感覺沒啥用 24 # key_fingerprint = "None" 25 # if system_user.password: 26 # password_short = system_user.password[:5] + \ 27 # (len(system_user.password) - 5) * '*' 28 # if system_user.private_key: 29 # key_fingerprint = get_private_key_fingerprint( 30 # system_user.private_key 31 # ) 32 # 33 # logger.error("Connect {}@{}:{} auth failed, password: \ 34 # {}, key: {}".format( 35 # system_user.username, asset.ip, asset.port, 36 # password_short, key_fingerprint, 37 # )) 38 return None, None, str(e) 39 except (socket.error, TimeoutError) as e: 40 return None, None, str(e) 41 return ssh, sock, None 42 43 def get_channel(self, asset, system_user, term="xterm", width=80, height=24, password=''): # password 參數 @ 周旺 44 ssh, sock, msg = self.get_ssh_client(asset, system_user, password) # password @ 周旺 45 if ssh: 46 chan = ssh.invoke_shell(term, width=width, height=height) 47 return chan, sock, None 48 else: 49 return None, sock, msg
效果展示
后記
以上僅適用 jumpserver 終端命令行,沒有涉及對jumpserver web 終端及SFTP的修改。
謝 jumpserver 團隊:http://www.jumpserver.org/