解決 Jumpserver coco 使用登錄用戶(ldap)進行SSH連接目標主機,忽略系統用戶


前言

  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/

      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM