Skip to content

Commit 954e4eb

Browse files
committed
fix: use native ssh auth first when needed
1 parent cfff947 commit 954e4eb

3 files changed

Lines changed: 33 additions & 46 deletions

File tree

src/fujin/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ class HostConfig(msgspec.Struct, kw_only=True):
212212
port: int = 22
213213
_key_filename: str | None = msgspec.field(name="key_filename", default=None)
214214
key_passphrase_env: str | None = None
215+
native_ssh_auth: bool = False # Use native ssh for auth (for Tailscale, etc.)
215216

216217
def __post_init__(self):
217218
if self._env_file and self.env_content:

src/fujin/connection.py

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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"

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)