解决 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/