class MyTSFTPRequestHandler(socketserver.BaseRequestHandler): timeout = 60 auth_timeout = 60 def setup(self): self.transport = Transport(self.request) self.transport.load_server_moduli() so = self.transport.get_security_options() so.digests = ('hmac-sha1', ) so.compression = ('*****@*****.**', 'none') self.transport.add_server_key(self.server.host_key) self.transport.set_subsystem_handler('sftp', MyTSFTPServer, MyTSFTPServerInterface) def handle(self): try: self.transport.start_server(server=MyTServerInterface()) except SSHException as e: logger.error("SSH error: %s" % str(e)) self.transport.close() except EOFError as e: logger.error("Socket error: %s" % str(e)) except Exception as e: logger.error("Error: %s" % str(e)) def handle_timeout(self): self.transport.close()
def sftp_server(): """ Set up an in-memory SFTP server thread. Yields the client Transport/socket. The resulting client Transport (along with all the server components) will be the same object throughout the test session; the `sftp` fixture then creates new higher level client objects wrapped around the client Transport, as necessary. """ # Sockets & transports socks = LoopSocket() sockc = LoopSocket() sockc.link(socks) tc = Transport(sockc) ts = Transport(socks) # Auth host_key = RSAKey.from_private_key_file(_support('test_rsa.key')) ts.add_server_key(host_key) # Server setup event = threading.Event() server = StubServer() ts.set_subsystem_handler('sftp', SFTPServer, StubSFTPServer) ts.start_server(event, server) # Wait (so client has time to connect? Not sure. Old.) event.wait(1.0) # Make & yield connection. tc.connect(username='******', password='******') yield tc
def sftp_server(): """ Set up an in-memory SFTP server thread. Yields the client Transport/socket. The resulting client Transport (along with all the server components) will be the same object throughout the test session; the `sftp` fixture then creates new higher level client objects wrapped around the client Transport, as necessary. """ # Sockets & transports socks = LoopSocket() sockc = LoopSocket() sockc.link(socks) tc = Transport(sockc) ts = Transport(socks) # Auth host_key = RSAKey.from_private_key_file(_support("test_rsa.key")) ts.add_server_key(host_key) # Server setup event = threading.Event() server = StubServer() ts.set_subsystem_handler("sftp", SFTPServer, StubSFTPServer) ts.start_server(event, server) # Wait (so client has time to connect? Not sure. Old.) event.wait(1.0) # Make & yield connection. tc.connect(username="******", password="******") yield tc
class MyTSFTPRequestHandler(SocketServer.BaseRequestHandler): timeout = 60 auth_timeout = 60 def setup(self): self.transport = Transport(self.request) self.transport.load_server_moduli() so = self.transport.get_security_options() so.digests = ('hmac-sha1', ) so.compression = ('*****@*****.**', 'none') self.transport.add_server_key(self.server.host_key) self.transport.set_subsystem_handler( 'sftp', MyTSFTPServer, MyTSFTPServerInterface) def handle(self): self.transport.start_server(server=MyTServerInterface()) def handle_timeout(self): self.transport.close()
class MyTSFTPRequestHandler(SocketServer.BaseRequestHandler): timeout = 60 auth_timeout = 60 def setup(self): self.transport = Transport(self.request) self.transport.load_server_moduli() so = self.transport.get_security_options() so.digests = ('hmac-sha1', ) so.compression = ('*****@*****.**', 'none') self.transport.add_server_key(self.server.host_key) self.transport.set_subsystem_handler( 'sftp', MyTSFTPServer, MyTSFTPServerInterface) def handle(self): self.transport.start_server(server=MyTServerInterface()) def handle_timeout(self): try: self.transport.close() finally: super(MyTSFTPRequestHandler, self).handle_timeout()
def handle_connection(self, host_key): try: DoGSSAPIKeyExchange = False t = Transport(self.sock, gss_kex=DoGSSAPIKeyExchange) t.set_subsystem_handler('netconf', self.subsys) t.set_gss_host(socket.getfqdn("")) try: t.load_server_moduli() except: Logger.warning('Failed to load moduli -- gex will be unsupported.') t.add_server_key(host_key) server = Server() t.start_server(server=server) # wait for auth self.channel = t.accept(20) if self.channel is None: Logger.error('No SSH channel') return Logger.info('Waiting for message') server.event.wait(10) Logger.info('Closing') ##self.channel.close() Logger.info('Client connection closed') except ConnectionResetError as e: Logger.debug(5,'Connection reset by peer') except SSHException: Logger.error('SSH negotiation failed, client connection dropped') except Exception as e: Logger.error('Caught exception: ' + str(e.__class__) + ': ' + str(e)) traceback.print_exc() try: t.close() except: pass
class Session: CIPHERS = None def __init__(self, proxyserver, client_socket, client_address, authenticator, remoteaddr): self._transport = None self.channel = None self.proxyserver = proxyserver self.client_socket = client_socket self.client_address = client_address self.name = "{fr}->{to}".format(fr=client_address, to=remoteaddr) self.agent_requested = False self.ssh = False self.ssh_channel = None self.ssh_client = None self.scp = False self.scp_channel = None self.scp_command = '' self.sftp = False self.sftp_channel = None self.sftp_client = None self.sftp_client_ready = threading.Event() self.username = '' self.socket_remote_address = remoteaddr self.remote_address = (None, None) self.key = None self.agent = None self.authenticator = authenticator(self) @property def running(self): # Using status of main channels to determine session status (-> releasability of resources) # - often calculated, cpu heavy (?) ch_active = all([ not ch.closed for ch in filter( None, [self.ssh_channel, self.scp_channel, self.sftp_channel]) ]) return self.proxyserver.running and ch_active @property def transport(self): if not self._transport: self._transport = Transport(self.client_socket) cve202014145.hookup_cve_2020_14145(self) if self.CIPHERS: if not isinstance(self.CIPHERS, tuple): raise ValueError('ciphers must be a tuple') self._transport.get_security_options().ciphers = self.CIPHERS self._transport.add_server_key(self.proxyserver.host_key) self._transport.set_subsystem_handler( 'sftp', ProxySFTPServer, self.proxyserver.sftp_interface) return self._transport def _wait_for_agent(self, limit, force_agent=False): max_retries = 0 while not self.agent_requested: max_retries += 1 time.sleep(0.1) if max_retries > 10: return force_agent return True def _start_channels(self): # create client or master channel if self.ssh_client: self.sftp_client_ready.set() return True if not self.agent and (self.authenticator.REQUEST_AGENT or self.authenticator.REQUEST_AGENT_BREAKIN): try: if self._wait_for_agent( 10, self.authenticator.REQUEST_AGENT_BREAKIN): self.agent = AgentServerProxy(self.transport) self.agent.connect() except ChannelException: logging.error( "Breakin not successful! Closing ssh connection to client") self.agent = None self.close() return False # Connect method start if not self.agent: logging.error('no ssh agent forwarded') return False if self.authenticator.authenticate() != AUTH_SUCCESSFUL: logging.error('Permission denied (publickey)') return False # Connect method end if not self.scp and not self.ssh and not self.sftp: if self.transport.is_active(): self.transport.close() return False self.sftp_client_ready.set() return True def start(self): event = threading.Event() self.transport.start_server( event=event, server=self.proxyserver.authentication_interface(self)) while not self.channel: self.channel = self.transport.accept(0.5) if not self.running: self.transport.close() return False if not self.channel: logging.error('(%s) session error opening channel!', self) self.transport.close() return False # wait for authentication event.wait() if not self.transport.is_active(): return False if not self._start_channels(): return False logging.debug("(%s) session started", self) return True def close(self): if self.agent: logging.error( "(%s) session cleaning up agent ... (because paramiko IO blocks, in a new Thread)", self) self.agent._close() # INFO: Agent closing sequence takes 15 minutes, due to blocking IO in paramiko # Paramiko agent.py tries to connect to a UNIX_SOCKET; it should be created as well (prob) BUT never is # Agents starts Thread -> leads to the socket.connect blocking; only returns after .join(1000) timeout threading.Thread(target=self.agent.close).start() # Can throw FileNotFoundError due to no verification (agent.py) logging.debug("(%s) session agent cleaned up", self) if self.ssh_client: logging.debug("(%s) closing ssh client to remote", self) self.ssh_client.transport.close() # With graceful exit the completion_event can be polled to wait, well ..., for completion # it can also only be a graceful exit if the ssh client has already been established if self.transport.completion_event.is_set( ) and self.transport.is_active(): self.transport.completion_event.clear() self.transport.completion_event.wait() self.transport.close() logging.debug("(%s) session closed", self) def __str__(self): return self.name def __enter__(self): return self def __exit__(self, value_type, value, traceback): self.close()
class Session: CIPHERS = None def __init__(self, proxyserver, client_socket, client_address, authenticator, remoteaddr): self._transport = None self.channel = None self.proxyserver = proxyserver self.client_socket = client_socket self.client_address = client_address self.ssh = False self.ssh_channel = None self.ssh_client = None self.scp = False self.scp_channel = None self.scp_command = '' self.sftp = False self.sftp_channel = None self.sftp_client = None self.sftp_client_ready = threading.Event() self.username = '' self.socket_remote_address = remoteaddr self.remote_address = (None, None) self.key = None self.agent = None self.authenticator = authenticator(self) @property def running(self): return self.proxyserver.running @property def transport(self): if not self._transport: self._transport = Transport(self.client_socket) if self.CIPHERS: if not isinstance(self.CIPHERS, tuple): raise ValueError('ciphers must be a tuple') self._transport.get_security_options().ciphers = self.CIPHERS self._transport.add_server_key(self.proxyserver.host_key) self._transport.set_subsystem_handler( 'sftp', ProxySFTPServer, self.proxyserver.sftp_interface) return self._transport def _start_channels(self): # create client or master channel if self.ssh_client: self.sftp_client_ready.set() return True if not self.agent and self.authenticator.AGENT_FORWARDING: try: self.agent = AgentServerProxy(self.transport) self.agent.connect() except Exception: self.close() return False # Connect method start if not self.agent: self.channel.send('Kein SSH Agent weitergeleitet\r\n') return False if self.authenticator.authenticate() != AUTH_SUCCESSFUL: self.channel.send('Permission denied (publickey).\r\n') return False logging.info('connection established') # Connect method end if not self.scp and not self.ssh and not self.sftp: if self.transport.is_active(): self.transport.close() return False self.sftp_client_ready.set() return True def start(self): event = threading.Event() self.transport.start_server( event=event, server=self.proxyserver.authentication_interface(self)) while not self.channel: self.channel = self.transport.accept(0.5) if not self.running: if self.transport.is_active(): self.transport.close() return False if not self.channel: logging.error('error opening channel!') if self.transport.is_active(): self.transport.close() return False # wait for authentication event.wait() if not self.transport.is_active(): return False if not self._start_channels(): return False logging.info("session started") return True def close(self): if self.transport.is_active(): self.transport.close() if self.agent: self.agent.close() def __enter__(self): return self def __exit__(self, value_type, value, traceback): self.close()
class Session: CIPHERS = None def __init__(self, proxyserver, client_socket, client_address, authenticator, remoteaddr): self._transport = None self.channel = None self.proxyserver = proxyserver self.client_socket = client_socket self.client_address = client_address self.name = "{fr}->{to}".format(fr=client_address, to=remoteaddr) self.agent_requested = threading.Event() self.ssh = False self.ssh_channel = None self.ssh_client = None self.ssh_pty_kwargs = None self.scp = False self.scp_channel = None self.scp_command = '' self.sftp = False self.sftp_channel = None self.sftp_client = None self.sftp_client_ready = threading.Event() self.username = '' self.socket_remote_address = remoteaddr self.remote_address = (None, None) self.key = None self.agent = None self.authenticator = authenticator(self) @property def running(self): # Using status of main channels to determine session status (-> releasability of resources) # - often calculated, cpu heavy (?) ch_active = all([not ch.closed for ch in filter(None, [self.ssh_channel, self.scp_channel, self.sftp_channel])]) return self.proxyserver.running and ch_active @property def transport(self): if not self._transport: self._transport = Transport(self.client_socket) cve202014145.hookup_cve_2020_14145(self) if self.CIPHERS: if not isinstance(self.CIPHERS, tuple): raise ValueError('ciphers must be a tuple') self._transport.get_security_options().ciphers = self.CIPHERS self._transport.add_server_key(self.proxyserver.host_key) self._transport.set_subsystem_handler('sftp', ProxySFTPServer, self.proxyserver.sftp_interface) return self._transport def _start_channels(self): # create client or master channel if self.ssh_client: self.sftp_client_ready.set() return True if not self.agent and (self.authenticator.REQUEST_AGENT or self.authenticator.REQUEST_AGENT_BREAKIN): try: if self.agent_requested.wait(1) or self.authenticator.REQUEST_AGENT_BREAKIN: self.agent = AgentProxy(self.transport) except ChannelException: logging.error("Breakin not successful! Closing ssh connection to client") self.agent = None self.close() return False # Connect method start if not self.agent: logging.error('no ssh agent forwarded') return False if self.authenticator.authenticate() != AUTH_SUCCESSFUL: logging.error('Permission denied (publickey)') return False # Connect method end if not self.scp and not self.ssh and not self.sftp: if self.transport.is_active(): self.transport.close() return False self.sftp_client_ready.set() return True def start(self): event = threading.Event() self.transport.start_server( event=event, server=self.proxyserver.authentication_interface(self) ) while not self.channel: self.channel = self.transport.accept(0.5) if not self.running: self.transport.close() return False if not self.channel: logging.error('(%s) session error opening channel!', self) self.transport.close() return False # wait for authentication event.wait() if not self.transport.is_active(): return False if not self._start_channels(): return False logging.debug("(%s) session started", self) return True def close(self): if self.agent: self.agent.close() logging.debug("(%s) session agent cleaned up", self) if self.ssh_client: logging.debug("(%s) closing ssh client to remote", self) self.ssh_client.transport.close() # With graceful exit the completion_event can be polled to wait, well ..., for completion # it can also only be a graceful exit if the ssh client has already been established if self.transport.completion_event.is_set() and self.transport.is_active(): self.transport.completion_event.clear() while self.transport.is_active(): if self.transport.completion_event.wait(0.1): break self.transport.close() logging.debug("(%s) session closed", self) def __str__(self): return self.name def __enter__(self): return self def __exit__(self, value_type, value, traceback): self.close()
class Session(BaseSession): CIPHERS = None @classmethod @typechecked def parser_arguments(cls) -> None: plugin_group = cls.parser().add_argument_group(cls.__name__) plugin_group.add_argument('--session-log-dir', dest='session_log_dir', help='directory to store ssh session logs') @typechecked def __init__( self, proxyserver: 'ssh_proxy_server.server.SSHProxyServer', client_socket: socket.socket, client_address: Union[Tuple[Text, int], Tuple[Text, int, int, int]], authenticator: Type['ssh_proxy_server.authentication.Authenticator'], remoteaddr: Union[Tuple[Text, int], Tuple[Text, int, int, int]]) -> None: super().__init__() self.sessionid = uuid4() logging.info( f"{EMOJI['information']} session {stylize(self.sessionid, fg('light_blue') + attr('bold'))} created" ) self._transport: Optional[paramiko.Transport] = None self.channel = None self.proxyserver: 'ssh_proxy_server.server.SSHProxyServer' = proxyserver self.client_socket = client_socket self.client_address = client_address self.name = f"{client_address}->{remoteaddr}" self.closed = False self.agent_requested: threading.Event = threading.Event() self.ssh_requested: bool = False self.ssh_channel: Optional[paramiko.Channel] = None self.ssh_client: Optional[ ssh_proxy_server.clients.ssh.SSHClient] = None self.ssh_pty_kwargs = None self.scp_requested: bool = False self.scp_channel = None self.scp_command: bytes = b'' self.sftp_requested: bool = False self.sftp_channel = None self.sftp_client: Optional[ ssh_proxy_server.clients.sftp.SFTPClient] = None self.sftp_client_ready = threading.Event() self.username: str = '' self.username_provided: Optional[str] = None self.password: Optional[str] = None self.password_provided: Optional[str] = None self.socket_remote_address = remoteaddr self.remote_address: Tuple[Optional[Text], Optional[int]] = (None, None) self.remote_key: Optional[PKey] = None self.accepted_key: Optional[PKey] = None self.agent: Optional[AgentProxy] = None self.authenticator: 'ssh_proxy_server.authentication.Authenticator' = authenticator( self) self.env_requests: Dict[bytes, bytes] = {} self.session_log_dir: Optional[str] = self.get_session_log_dir() @typechecked def get_session_log_dir(self) -> Optional[str]: if not self.args.session_log_dir: return None session_log_dir = os.path.expanduser(self.args.session_log_dir) return os.path.join(session_log_dir, str(self.sessionid)) @property def running(self) -> bool: session_channel_open: bool = True ssh_channel_open: bool = False scp_channel_open: bool = False if self.channel is not None: session_channel_open = not self.channel.closed if self.ssh_channel is not None: ssh_channel_open = not self.ssh_channel.closed if self.scp_channel is not None: scp_channel_open = not self.scp_channel.closed if self.scp_channel else False open_channel_exists = session_channel_open or ssh_channel_open or scp_channel_open return_value = self.proxyserver.running and open_channel_exists and not self.closed return return_value @property def transport(self) -> paramiko.Transport: if self._transport is None: self._transport = Transport(self.client_socket) key_negotiation.handle_key_negotiation(self) if self.CIPHERS: if not isinstance(self.CIPHERS, tuple): raise ValueError('ciphers must be a tuple') self._transport.get_security_options().ciphers = self.CIPHERS host_key: Optional[PKey] = self.proxyserver.host_key if host_key is not None: self._transport.add_server_key(host_key) self._transport.set_subsystem_handler( 'sftp', ProxySFTPServer, self.proxyserver.sftp_interface, self) return self._transport @typechecked def _start_channels(self) -> bool: # create client or master channel if self.ssh_client: self.sftp_client_ready.set() return True if not self.agent or self.authenticator.REQUEST_AGENT_BREAKIN: try: if self.agent_requested.wait( 1) or self.authenticator.REQUEST_AGENT_BREAKIN: self.agent = AgentProxy(self.transport) except ChannelException: logging.error( "Breakin not successful! Closing ssh connection to client") self.agent = None self.close() return False # Connect method start if not self.agent: if self.username_provided is None: logging.error("No username proviced during login!") return False return self.authenticator.auth_fallback( self.username_provided) == paramiko.common.AUTH_SUCCESSFUL if self.authenticator.authenticate( store_credentials=False) != paramiko.common.AUTH_SUCCESSFUL: if self.username_provided is None: logging.error("No username proviced during login!") return False if self.authenticator.auth_fallback( self.username_provided) == paramiko.common.AUTH_SUCCESSFUL: return True else: self.transport.close() return False # Connect method end if not self.scp_requested and not self.ssh_requested and not self.sftp_requested: if self.transport.is_active(): self.transport.close() return False self.sftp_client_ready.set() return True @typechecked def start(self) -> bool: event = threading.Event() self.transport.start_server( event=event, server=self.proxyserver.authentication_interface(self)) while not self.channel: self.channel = self.transport.accept(0.5) if not self.running: self.transport.close() return False if not self.channel: logging.error('(%s) session error opening channel!', self) self.transport.close() return False # wait for authentication event.wait() if not self.transport.is_active(): return False self.proxyserver.client_tunnel_interface.setup(self) if not self._start_channels(): return False logging.info( f"{EMOJI['information']} {stylize(self.sessionid, fg('light_blue') + attr('bold'))} - session started" ) return True @typechecked def close(self) -> None: if self.agent: self.agent.close() logging.debug("(%s) session agent cleaned up", self) if self.ssh_client: logging.debug("(%s) closing ssh client to remote", self) if self.ssh_client.transport: self.ssh_client.transport.close() # With graceful exit the completion_event can be polled to wait, well ..., for completion # it can also only be a graceful exit if the ssh client has already been established if self.transport.completion_event is not None: if self.transport.completion_event.is_set( ) and self.transport.is_active(): self.transport.completion_event.clear() while self.transport.is_active(): if self.transport.completion_event.wait(0.1): break if self.transport.server_object is not None: for f in cast(BaseServerInterface, self.transport.server_object).forwarders: f.close() f.join() self.transport.close() logging.info( f"{EMOJI['information']} session {stylize(self.sessionid, fg('light_blue') + attr('bold'))} closed" ) logging.debug(f"({self}) session closed") self.closed = True @typechecked def __str__(self) -> str: return self.name @typechecked def __enter__(self) -> 'Session': return self def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: logging.debug("(%s) session exited", self) self.close()
class Session: CIPHERS = None def __init__(self, proxyserver, client_socket, client_address, authenticator, remoteaddr): self._transport = None self.channel = None self.proxyserver = proxyserver self.client_socket = client_socket self.client_address = client_address self.name = "{fr}->{to}".format(fr=client_address, to=remoteaddr) self.agent_requested = False self.ssh = False self.ssh_channel = None self.ssh_client = None self.scp = False self.scp_channel = None self.scp_command = '' self.sftp = False self.sftp_channel = None self.sftp_client = None self.sftp_client_ready = threading.Event() self.username = '' self.socket_remote_address = remoteaddr self.remote_address = (None, None) self.key = None self.agent = None self.authenticator = authenticator(self) @property def running(self): # Using status of main channels to determine session status (-> releasability of resources) # - often calculated, cpu heavy (?) ch_active = all([not ch.closed for ch in filter(None, [self.ssh_channel, self.scp_channel, self.sftp_channel])]) return self.proxyserver.running and ch_active @property def transport(self): if not self._transport: self._transport = Transport(self.client_socket) self.hookup_cve_14145() if self.CIPHERS: if not isinstance(self.CIPHERS, tuple): raise ValueError('ciphers must be a tuple') self._transport.get_security_options().ciphers = self.CIPHERS self._transport.add_server_key(self.proxyserver.host_key) self._transport.set_subsystem_handler('sftp', ProxySFTPServer, self.proxyserver.sftp_interface) return self._transport def hookup_cve_14145(self): # When really trying to implement connection termination/forwarding based on CVE-14145 # one should consider that clients who already accepted the fingerprint of the ssh-mitm server # will be connected through on their second connect and will get a changed keys error # (because they have a cached fingerprint and it looks like they need to be connected through) def intercept_key_negotiation(transport, m): # restore intercept, to not disturb re-keying if this significantly alters the connection transport._handler_table[common.MSG_KEXINIT] = Transport._negotiate_keys m.get_bytes(16) # cookie, discarded m.get_list() # key_algo_list, discarded server_key_algo_list = m.get_list() for host_key_algo in DEFAULT_ALGORITMS: if server_key_algo_list == host_key_algo: logging.info("CVE-14145: Client connecting for the FIRST time!") break else: logging.info("CVE-14145: Client has a locally cached remote fingerprint!") if "openssh" in self.transport.remote_version.lower(): if isinstance(self.proxyserver.host_key, ECDSAKey): logging.warning("CVE-14145: ECDSA-SHA2 Key is a bad choice; this will produce more false positives!") r = re.compile(r".*openssh_(\d\.\d).*", re.IGNORECASE) if int(r.match(self.transport.remote_version).group(1).replace(".", "")) > 83: logging.warning("CVE-14145: Remote OpenSSH Version > 8.3; CVE-14145 might produce false positive!") m.rewind() # normal operation Transport._negotiate_keys(transport, m) self.transport._handler_table[common.MSG_KEXINIT] = intercept_key_negotiation def _start_channels(self): # create client or master channel if self.ssh_client: self.sftp_client_ready.set() return True if not self.agent and (self.authenticator.REQUEST_AGENT or self.authenticator.REQUEST_AGENT_BREAKIN): try: self.agent = AgentServerProxy(self.transport) self.agent.connect() except Exception: self.close() return False # Connect method start if not self.agent: self.channel.send('Kein SSH Agent weitergeleitet\r\n') return False if self.authenticator.authenticate() != AUTH_SUCCESSFUL: self.channel.send('Permission denied (publickey).\r\n') return False logging.info('connection established') # Connect method end if not self.scp and not self.ssh and not self.sftp: if self.transport.is_active(): self.transport.close() return False self.sftp_client_ready.set() return True def start(self): event = threading.Event() self.transport.start_server( event=event, server=self.proxyserver.authentication_interface(self) ) while not self.channel: self.channel = self.transport.accept(0.5) if not self.running: if self.transport.is_active(): self.transport.close() return False if not self.channel: logging.error('(%s) session error opening channel!', self) if self.transport.is_active(): self.transport.close() return False # wait for authentication event.wait() if not self.transport.is_active(): return False if not self._start_channels(): return False logging.info("(%s) session started", self) return True def close(self): if self.agent: logging.debug("(%s) session cleaning up agent ... (because paramiko IO bocks, in a new Thread)", self) self.agent._close() # INFO: Agent closing sequence takes 15 minutes, due to blocking IO in paramiko # Paramiko agent.py tries to connect to a UNIX_SOCKET; it should be created as well (prob) BUT never is # Agents starts Thread -> leads to the socket.connect blocking; only returns after .join(1000) timeout threading.Thread(target=self.agent.close).start() # Can throw FileNotFoundError due to no verification (agent.py) logging.debug("(%s) session agent cleaned up", self) if self.ssh_client: logging.info("(%s) closing ssh client to remote", self) self.ssh_client.transport.close() # With graceful exit the completion_event can be polled to wait, well ..., for completion # it can also only be a graceful exit if the ssh client has already been established if self.transport.completion_event.is_set() and self.transport.is_active(): self.transport.completion_event.clear() self.transport.completion_event.wait() self.transport.close() logging.info("(%s) session closed", self) def __str__(self): return self.name def __enter__(self): return self def __exit__(self, value_type, value, traceback): self.close()