Public/Ssh/New-VmSshClient.ps1
|
# --------------------------------------------------------------------------- # New-VmSshClient # Creates and connects a Renci.SshNet.SshClient using password # authentication. Returns the connected client; the caller is responsible # for calling Disconnect() and Dispose() in a finally block. # # SSH.NET is used directly rather than Posh-SSH cmdlets because # ConnectionInfoGenerator in Posh-SSH 3.x drops algorithm entries from # the SSH.NET ConnectionInfo, breaking key exchange against OpenSSH 9.x # (Ubuntu 24.04). Posh-SSH must still be installed - it ships the # Renci.SshNet.dll the function depends on. # # Renci types are referenced inside the function body (not as parameter # types) so the module imports cleanly on hosts without Posh-SSH. The # Assert-SshNetLoaded guard turns the otherwise opaque "type not found" # error into an actionable message naming the missing prerequisite. # # -Timeout caps the total Connect() wall-clock. SSH.NET applies the # same value to both socket-read and KEX, so a long Timeout is the # right knob when a slow server-side responder (e.g. sshd held off # by cloud-config until users are created) needs more than SSH.NET's # 30s default. The Connect call is synchronous - the caller pays a # single block of up to -Timeout with no console output in between. # Consumers waiting on multi-minute connects should print a leading # "this may take a few minutes" line so the silence does not read # like a hang. # # -KeepAliveInterval arms SSH.NET's keepalive timer so a long-lived # session survives the idle gaps between commands on a path where a NAT # or firewall middlebox would otherwise silently reap an idle-looking # connection. Defaults to 15s; pass [TimeSpan]::Zero to opt out. # # -Port selects the TCP port (default 22). It lets a caller reach an # sshd published on a non-standard port - e.g. a jump tunnel's ephemeral # loopback forward - through this one helper, so the connect/keepalive # policy is not re-implemented at each call site. # # Security: # - SSH.NET accepts any host key by default (no HostKeyReceived handler). # Equivalent to Posh-SSH's -AcceptKey. Acceptable on a private Hyper-V # network with statically provisioned IPs; do NOT use on untrusted # networks without supplying a fingerprint check. # - Password is required as a plain string by SSH.NET's # PasswordAuthenticationMethod constructor. Callers should source the # value from SecretManagement and avoid logging it. # --------------------------------------------------------------------------- function New-VmSshClient { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingPlainTextForPassword', 'Password')] # SSH.NET's PasswordAuthenticationMethod takes the username and password # as plaintext strings; a PSCredential would only be unwrapped here. The # pair is intrinsic to the library contract, so suppress the paired-param # rule (function-scoped: the suppression ID must be empty). [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingUsernameAndPasswordParams', '', Justification = 'SSH.NET requires a plaintext username/password pair')] [CmdletBinding()] param( [Parameter(Mandatory)] [string] $IpAddress, [Parameter(Mandatory)] [string] $Username, # Plain string required by SSH.NET PasswordAuthenticationMethod. [Parameter(Mandatory)] [string] $Password, # TCP port sshd is reachable on. Defaults to 22; callers connecting # through a local-forward tunnel pass the ephemeral loopback port. [int] $Port = 22, # Total Connect() wall-clock budget. Default 30s matches SSH.NET's # built-in default so existing callers are unaffected. Callers # waiting on a slow server-side responder (e.g. provisioning, # where sshd is ordered after cloud-config) pass a generous value. [TimeSpan] $Timeout = [TimeSpan]::FromSeconds(30), # SSH-level keepalive interval for the connected client. SSH.NET # sends a keepalive global-request every interval, which keeps a # long-lived session alive across the idle gaps between commands: # a NAT or firewall middlebox on the host<->VM path can otherwise # drop an idle-looking connection, surfacing mid-command as # "connection aborted by the server". Defaults to 15s, comfortably # under common middlebox idle timeouts; pass [TimeSpan]::Zero to # disable (restoring SSH.NET's no-keepalive default). [TimeSpan] $KeepAliveInterval = [TimeSpan]::FromSeconds(15) ) Assert-SshNetLoaded $auth = [Renci.SshNet.PasswordAuthenticationMethod]::new($Username, $Password) $connInfo = [Renci.SshNet.ConnectionInfo]::new($IpAddress, $Port, $Username, @($auth)) # SSH.NET applies ConnectionInfo.Timeout to both the TCP socket read # and the KEX exchange. $connInfo.Timeout = $Timeout $client = [Renci.SshNet.SshClient]::new($connInfo) # Arm keepalive before Connect so the timer starts with the session. # The disabled-on-non-positive policy lives in Set-SshClientKeepAlive. Set-SshClientKeepAlive -Client $client -Interval $KeepAliveInterval $client.Connect() $client } |