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.
#
# 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')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $IpAddress,

        [Parameter(Mandatory)]
        [string] $Username,

        # Plain string required by SSH.NET PasswordAuthenticationMethod.
        [Parameter(Mandatory)]
        [string] $Password
    )

    Assert-SshNetLoaded

    $auth     = [Renci.SshNet.PasswordAuthenticationMethod]::new($Username, $Password)
    $connInfo = [Renci.SshNet.ConnectionInfo]::new($IpAddress, $Username, @($auth))
    $client   = [Renci.SshNet.SshClient]::new($connInfo)
    $client.Connect()
    $client
}