@@ -344,8 +344,34 @@ def connection(host: HostConfig) -> Generator[SSH2Connection, None, None]:
344344 auth_methods_tried = []
345345 authenticated = False
346346
347+ # For hosts that require native SSH auth (Tailscale, etc.), authenticate via ssh first
348+ if host .native_ssh_auth :
349+ logger .info ("Using native SSH for authentication..." )
350+ ssh_cmd = [
351+ "ssh" ,
352+ "-p" ,
353+ str (host .port ),
354+ "-o" ,
355+ "StrictHostKeyChecking=accept-new" ,
356+ f"{ host .user } @{ host .address } " ,
357+ "exit" ,
358+ ]
359+ result = subprocess .run (ssh_cmd )
360+ if result .returncode == 0 :
361+ # Reconnect after successful auth - session is now authorized
362+ sock .close ()
363+ sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
364+ sock .settimeout (30 )
365+ sock .connect ((host .address , host .port ))
366+ sock .settimeout (None )
367+ sock .setsockopt (socket .IPPROTO_TCP , socket .TCP_NODELAY , 1 )
368+ session = Session ()
369+ session .handshake (sock )
370+ auth_methods_tried .append ("native ssh" )
371+ # Continue to normal auth methods below (they should work now)
372+
347373 # Method 1: Explicit key file (if specified)
348- if host .key_filename :
374+ if not authenticated and host .key_filename :
349375 try :
350376 key_path = Path (host .key_filename ).expanduser ()
351377 logger .debug (f"Trying explicit key: { key_path } " )
@@ -416,46 +442,6 @@ def connection(host: HostConfig) -> Generator[SSH2Connection, None, None]:
416442 logger .debug (f"Password auth failed: { e } " )
417443 auth_methods_tried .append ("password (failed)" )
418444
419- # Method 5: Keyboard-interactive via native SSH (for Tailscale, etc.)
420- if not authenticated :
421- try :
422- available_methods = session .userauth_list (host .user )
423- if available_methods and "keyboard-interactive" in available_methods :
424- logger .info (
425- "Trying keyboard-interactive auth via native SSH (Tailscale, etc.)..."
426- )
427- # Spawn native ssh to handle interactive auth (shows URL, waits for browser)
428- ssh_cmd = [
429- "ssh" ,
430- "-p" ,
431- str (host .port ),
432- "-o" ,
433- "StrictHostKeyChecking=accept-new" ,
434- f"{ host .user } @{ host .address } " ,
435- "exit" ,
436- ]
437- result = subprocess .run (ssh_cmd )
438- if result .returncode == 0 :
439- # Auth succeeded via native SSH, retry with ssh2-python
440- # Need to create a new session since the old one may be in a bad state
441- sock .close ()
442- sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
443- sock .settimeout (30 )
444- sock .connect ((host .address , host .port ))
445- sock .settimeout (None )
446- sock .setsockopt (socket .IPPROTO_TCP , socket .TCP_NODELAY , 1 )
447- session = Session ()
448- session .handshake (sock )
449- # Retry agent auth (Tailscale adds credentials to agent after browser auth)
450- session .agent_auth (host .user )
451- authenticated = session .userauth_authenticated ()
452- if authenticated :
453- logger .info ("✓ Authenticated after keyboard-interactive flow" )
454- auth_methods_tried .append ("keyboard-interactive (native ssh)" )
455- except Exception as e :
456- logger .debug (f"Keyboard-interactive auth failed: { e } " )
457- auth_methods_tried .append ("keyboard-interactive (failed)" )
458-
459445 if not authenticated :
460446 sock .close ()
461447 methods_str = ", " .join (auth_methods_tried ) if auth_methods_tried else "none"
0 commit comments