Skip to content

Commit 0c42938

Browse files
authored
Release v5.3.3: SSH policy fix (revert AutoAddPolicy default + add --ssh-tofu opt-in)
- Default SSH policy back to RejectPolicy (safe; reverts v5.3.2 regression) - New --ssh-tofu flag opts into AutoAddPolicy with a yellow warning on each connect - Helpful error when RejectPolicy refuses: shows ssh-keyscan line + --ssh-tofu hint - 5.3.2 -> 5.3.3 (5.3.2 permanently retired on PyPI per file-name-reuse policy) - No behaviour change for any other flag, check, or report
1 parent 8a50f90 commit 0c42938

2 files changed

Lines changed: 40 additions & 6 deletions

File tree

hardax/__init__.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
# VERSION & CONSTANTS
4444
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4545

46-
__version__ = "5.3.2"
46+
__version__ = "5.3.3"
4747

4848
REQUIRED_CHECK_KEYS = {"category", "label", "command", "safe_pattern", "level", "description"}
4949

@@ -642,7 +642,8 @@ def idString(self) -> str:
642642
class SshDevice(Device):
643643
"""Execute commands on a device over SSH (paramiko)."""
644644

645-
def __init__(self, host: str, port: int, user: str, password: str):
645+
def __init__(self, host: str, port: int, user: str, password: str,
646+
trust_on_first_use: bool = False):
646647
try:
647648
import paramiko
648649
except Exception:
@@ -657,12 +658,40 @@ def __init__(self, host: str, port: int, user: str, password: str):
657658

658659
self.client = self.paramiko.SSHClient()
659660
self.client.load_system_host_keys()
660-
self.client.set_missing_host_key_policy(self.paramiko.AutoAddPolicy())
661+
if trust_on_first_use:
662+
# User explicitly opted in via --ssh-tofu. Print a clear warning
663+
# so this never happens by accident or unnoticed.
664+
print(
665+
f"{Colors.YELLOW}⚠ SSH host-key trust-on-first-use enabled (--ssh-tofu).{Colors.RESET}\n"
666+
f"{Colors.YELLOW} First connections will silently trust any host key; "
667+
f"vulnerable to MITM during the first handshake.{Colors.RESET}\n"
668+
f"{Colors.YELLOW} Only use this on controlled lab / CI networks.{Colors.RESET}",
669+
file=sys.stderr,
670+
)
671+
self.client.set_missing_host_key_policy(self.paramiko.AutoAddPolicy())
672+
else:
673+
# Safe default: refuse to connect to hosts whose key is not
674+
# already in the user's known_hosts. Pre-populate with
675+
# 'ssh-keyscan -H host >> ~/.ssh/known_hosts' or pass --ssh-tofu.
676+
self.client.set_missing_host_key_policy(self.paramiko.RejectPolicy())
661677
try:
662678
self.client.connect(hostname=host, port=port, username=user,
663679
password=password, look_for_keys=False,
664680
allow_agent=False, timeout=20)
665-
except (paramiko.AuthenticationException, paramiko.SSHException, OSError) as e:
681+
except self.paramiko.SSHException as e:
682+
msg = str(e)
683+
if "not found in known_hosts" in msg.lower() or "Server" in msg:
684+
print(
685+
f"ERROR: SSH host key for {host}:{port} is not in known_hosts.\n"
686+
f" Either pre-populate it on the auditor machine:\n"
687+
f" ssh-keyscan -H -t ed25519,rsa {host} >> ~/.ssh/known_hosts\n"
688+
f" Or, if you accept the trust-on-first-use risk, pass --ssh-tofu.",
689+
file=sys.stderr,
690+
)
691+
else:
692+
print(f"ERROR: SSH connection failed: {e}", file=sys.stderr)
693+
sys.exit(1)
694+
except (paramiko.AuthenticationException, OSError) as e:
666695
print(f"ERROR: SSH connection failed: {e}", file=sys.stderr)
667696
sys.exit(1)
668697

@@ -2082,6 +2111,10 @@ def main():
20822111
ap.add_argument("--port", type=int, default=22, help="SSH port (or overridden by UART)")
20832112
ap.add_argument("--ssh-user", help="SSH username")
20842113
ap.add_argument("--ssh-pass", help="SSH password")
2114+
ap.add_argument("--ssh-tofu", action="store_true",
2115+
help="SSH trust-on-first-use: silently accept and store unknown host keys. "
2116+
"Convenient for CI / lab networks auditing many fresh devices; "
2117+
"weakens the SSH MITM guarantee on the first connection. Off by default.")
20852118
ap.add_argument("--uart-port", help="UART serial port (e.g. /dev/ttyUSB0, /dev/ttyS0, COM3)")
20862119
ap.add_argument("--baud", type=int, default=0, help="UART baud rate (0 = auto-detect, common: 115200, 9600)")
20872120
ap.add_argument("--out", default="hardax_output", help="Output directory")
@@ -2159,7 +2192,8 @@ def main():
21592192
or os.environ.get("HARDAX_SSH_PASS")
21602193
or getpass.getpass(f"SSH password for {args.ssh_user}@{args.host}: ")
21612194
)
2162-
device = SshDevice(args.host, args.port, args.ssh_user, ssh_pass)
2195+
device = SshDevice(args.host, args.port, args.ssh_user, ssh_pass,
2196+
trust_on_first_use=args.ssh_tofu)
21632197

21642198
else: # uart
21652199
if not args.uart_port:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "hardax"
7-
version = "5.3.2"
7+
version = "5.3.3"
88
description = "Hardening Audit eXaminer: security configuration auditor for Android-based devices (POS, IoT, automotive, medical, kiosk)"
99
readme = "README.md"
1010
requires-python = ">=3.10"

0 commit comments

Comments
 (0)