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

      

posted @ 2018-07-14 15:35 卑鄙的wo 阅读(...) 评论(...) 编辑 收藏