Public/cloud-sshkey.ps1
|
function Get-CloudSSHKey { <# .SYNOPSIS Gets management SSH keys from the Cloud Server. .DESCRIPTION Retrieves a list of SSH keys authorized to log in to the dashboard. The API response is normalized into individual PowerShell objects with: - Index - Type - Fingerprint - Comment - RawKey The RawKey property contains the complete SSH public key string as returned by the API. .EXAMPLE # Get all SSH keys Get-CloudSSHKey .EXAMPLE # Get the raw key for the first SSH key (Get-CloudSSHKey)[0].RawKey .EXAMPLE # Get all raw SSH keys Get-CloudSSHKey | Select-Object -ExpandProperty RawKey .EXAMPLE # Find SSH keys matching a comment Get-CloudSSHKey | Where-Object Comment -Like "*tverkade*" .EXAMPLE # Get the raw key for a specific comment Get-CloudSSHKey | Where-Object Comment -Like "*tverkade*" | Select-Object -ExpandProperty RawKey .EXAMPLE # Display only the key type and comment Get-CloudSSHKey | Format-Table Type, Comment .EXAMPLE # Display the full object including the raw key Get-CloudSSHKey | Format-List * #> $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v3-preview/management/ssh-key" $response = Invoke-CloudApiRequest ` -Uri $uri ` -Method Get $index = 0 foreach ($entry in $response.keys) { # # Some API responses may contain multiple SSH public keys concatenated # together. Split whenever another SSH key type begins. # $individualKeys = $entry -split ` '(?=(?:ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-[^\s]+)\s)' foreach ($key in $individualKeys) { $key = $key.Trim() if ([string]::IsNullOrWhiteSpace($key)) { continue } # # Standard OpenSSH public key format: # # <type> <base64-key> [comment] # if ($key -match '^(?<Type>\S+)\s+(?<KeyData>\S+)(?:\s+(?<Comment>.*))?$') { # # Save the values immediately because PowerShell's automatic # $Matches variable can be overwritten by later regex operations. # $rawType = $Matches.Type $keyData = $Matches.KeyData $comment = $Matches.Comment # # Convert the raw SSH key type into a cleaner display value. # $displayType = switch ($rawType) { 'ssh-rsa' { 'RSA' } 'ssh-ed25519' { 'ED25519' } 'ssh-dss' { 'DSA' } default { if ($rawType -like 'ecdsa-*') { 'ECDSA' } else { $rawType } } } # # Generate an OpenSSH-style SHA256 fingerprint. # try { $keyBytes = [Convert]::FromBase64String($keyData) $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $hash = $sha256.ComputeHash($keyBytes) $fingerprint = "SHA256:" + [Convert]::ToBase64String($hash).TrimEnd('=') } finally { $sha256.Dispose() } } catch { $fingerprint = "Invalid key" } # # Return one PowerShell object per SSH key. # [PSCustomObject]@{ Index = $index Type = $displayType Fingerprint = $fingerprint Comment = $comment RawKey = $key } $index++ } } } } function Add-CloudSSHKey { <# .SYNOPSIS Adds an SSH public key to the Cloud Server. .DESCRIPTION Adds an SSH public key to the Cloud Server management authorized key list. The function supports three methods: 1. Generate a new RSA 4096-bit SSH keypair. If no public key is supplied, a new keypair is created in the current user's .ssh directory and the public key is uploaded. 2. Supply a public key directly using -PublicKey. 3. Supply the path to an existing public key using -PublicKeyPath. Generated keys default to RSA 4096 for broad compatibility with environments requiring FIPS-approved cryptography. The private key is never uploaded. Only the public key is sent to the Cloud Server. .PARAMETER PublicKey An existing OpenSSH-format public key supplied as a string. .PARAMETER PublicKeyPath Path to an existing SSH public key file. .PARAMETER KeyName File name to use when generating a new SSH keypair. The key is created in: $env:USERPROFILE\.ssh\<KeyName> Default: id_cloud_rsa .PARAMETER Comment Comment added to a newly generated SSH public key. Default: <username>@<computername> .PARAMETER Force Allows an existing generated key file with the same KeyName to be replaced. This parameter only applies when generating a new key. .EXAMPLE # Generate a new RSA 4096-bit keypair and upload the public key Add-CloudSSHKey .EXAMPLE # Generate a new key with a custom file name Add-CloudSSHKey -KeyName "hypercloud" .EXAMPLE # Generate a new key with a custom comment Add-CloudSSHKey ` -KeyName "hypercloud-admin" ` -Comment "tverkade@admin-workstation" .EXAMPLE # Upload an existing public key from a file Add-CloudSSHKey ` -PublicKeyPath "$env:USERPROFILE\.ssh\id_rsa.pub" .EXAMPLE # Upload a public key supplied directly as a string Add-CloudSSHKey ` -PublicKey "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... user@host" .EXAMPLE # Generate a replacement key if the specified key already exists Add-CloudSSHKey ` -KeyName "hypercloud" ` -Force #> [CmdletBinding(DefaultParameterSetName = 'Generate')] param ( [Parameter( Mandatory = $true, ParameterSetName = 'String' )] [string]$PublicKey, [Parameter( Mandatory = $true, ParameterSetName = 'File' )] [string]$PublicKeyPath, [Parameter( Mandatory = $false, ParameterSetName = 'Generate' )] [string]$KeyName = 'id_cloud_rsa', [Parameter( Mandatory = $false, ParameterSetName = 'Generate' )] [string]$Comment = "$env:USERNAME@$env:COMPUTERNAME", [Parameter( Mandatory = $false, ParameterSetName = 'Generate' )] [switch]$Force ) # # Verify there is an active Cloud connection. # if (-not $script:CloudConnection -or -not $script:CloudConnection.Connected) { throw "No active Cloud Server connection. Run Connect-CloudServer first." } $generatedKey = $false $privateKeyPath = $null $publicKeyFilePath = $null $keyToUpload = $null switch ($PSCmdlet.ParameterSetName) { # # Public key supplied directly as a string. # 'String' { $keyToUpload = $PublicKey.Trim() } # # Public key supplied as a file. # 'File' { if (-not (Test-Path -LiteralPath $PublicKeyPath -PathType Leaf)) { throw "SSH public key file does not exist: $PublicKeyPath" } $publicKeyFilePath = (Resolve-Path -LiteralPath $PublicKeyPath).Path $keyToUpload = ( Get-Content ` -LiteralPath $publicKeyFilePath ` -Raw ).Trim() } # # No public key supplied, so generate one. # 'Generate' { $sshDirectory = Join-Path ` -Path $env:USERPROFILE ` -ChildPath '.ssh' if (-not (Test-Path -LiteralPath $sshDirectory)) { New-Item ` -Path $sshDirectory ` -ItemType Directory ` -Force | Out-Null } $privateKeyPath = Join-Path ` -Path $sshDirectory ` -ChildPath $KeyName $publicKeyFilePath = "${privateKeyPath}.pub" # # Protect against accidentally replacing an existing key. # if ( (Test-Path -LiteralPath $privateKeyPath) -or (Test-Path -LiteralPath $publicKeyFilePath) ) { if (-not $Force) { throw @" An SSH key already exists: $privateKeyPath Specify a different -KeyName or use -Force to replace it. "@ } Remove-Item ` -LiteralPath $privateKeyPath ` -Force ` -ErrorAction SilentlyContinue Remove-Item ` -LiteralPath $publicKeyFilePath ` -Force ` -ErrorAction SilentlyContinue } # # Find ssh-keygen. # $sshKeygen = Get-Command ` ssh-keygen.exe ` -ErrorAction SilentlyContinue if (-not $sshKeygen) { $sshKeygen = Get-Command ` ssh-keygen ` -ErrorAction SilentlyContinue } if (-not $sshKeygen) { throw @" ssh-keygen could not be found. Install the Windows OpenSSH Client feature before generating SSH keys. "@ } Write-Verbose "Generating RSA 4096-bit SSH key: $privateKeyPath" # # Generate an RSA 4096 key without a passphrase. # & $sshKeygen.Source ` -t rsa ` -b 4096 ` -f $privateKeyPath ` -N '""' ` -C $Comment if ($LASTEXITCODE -ne 0) { throw "ssh-keygen failed with exit code $LASTEXITCODE." } if (-not (Test-Path -LiteralPath $privateKeyPath)) { throw "ssh-keygen did not create the expected private key: $privateKeyPath" } if (-not (Test-Path -LiteralPath $publicKeyFilePath)) { throw "ssh-keygen did not create the expected public key: $publicKeyFilePath" } $keyToUpload = ( Get-Content ` -LiteralPath $publicKeyFilePath ` -Raw ).Trim() $generatedKey = $true } } # # Validate that the supplied/generated value looks like an # OpenSSH-format public key. # if ( $keyToUpload -notmatch '^(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-\S+)\s+\S+' ) { throw "The supplied value does not appear to be a valid OpenSSH public key." } # # Parse the SSH key before upload so we can calculate useful metadata. # $parts = $keyToUpload -split '\s+', 3 $rawType = $parts[0] $keyData = $parts[1] if ($parts.Count -ge 3) { $keyComment = $parts[2] } else { $keyComment = '' } $displayType = switch ($rawType) { 'ssh-rsa' { 'RSA' } 'ssh-ed25519' { 'ED25519' } 'ssh-dss' { 'DSA' } default { if ($rawType -like 'ecdsa-*') { 'ECDSA' } else { $rawType } } } # # Calculate an OpenSSH-style SHA256 fingerprint. # try { $keyBytes = [Convert]::FromBase64String($keyData) $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $hash = $sha256.ComputeHash($keyBytes) $fingerprint = "SHA256:" + [Convert]::ToBase64String($hash).TrimEnd('=') } finally { $sha256.Dispose() } } catch { throw "Unable to calculate SSH key fingerprint. The key data does not appear to be valid Base64." } # # Build the API request. # $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v3-preview/management/ssh-key" # # Keep the body as an object. # # Invoke-CloudApiRequest is responsible for serializing the request # appropriately. # $body = @{ key = $keyToUpload } Write-Verbose "Uploading SSH public key to $uri" # # Upload the public key. # # Do not return a success object if the API request fails. # try { Invoke-CloudApiRequest ` -Uri $uri ` -Method Put ` -Body $body ` -ErrorAction Stop | Out-Null } catch { throw "Failed to upload SSH key: $($_.Exception.Message)" } # # Return information about the successfully uploaded key. # [PSCustomObject]@{ Type = $displayType Fingerprint = $fingerprint Comment = $keyComment Generated = $generatedKey PrivateKey = $privateKeyPath PublicKeyFile = $publicKeyFilePath RawKey = $keyToUpload } } function Remove-CloudSSHKey { <# .SYNOPSIS Removes an SSH public key from the Cloud Server. .DESCRIPTION Removes an SSH public key from the Cloud Server management authorized key list. The SSH key can be supplied in one of three ways: 1. As a raw OpenSSH public key string using -PublicKey. 2. From an existing public key file using -PublicKeyPath. 3. By piping an object returned by Get-CloudSSHKey into Remove-CloudSSHKey. The complete public key is sent to the Cloud Server in the DELETE request. .PARAMETER PublicKey An OpenSSH-format public key supplied as a string. Example: ssh-rsa AAAAB3NzaC1yc2E... user@host .PARAMETER PublicKeyPath Path to an existing SSH public key file. Example: C:\Users\user\.ssh\id_rsa.pub .PARAMETER InputObject An SSH key object returned by Get-CloudSSHKey. The object must contain a RawKey property. .PARAMETER Force Removes the key without prompting for confirmation. .EXAMPLE # Remove a public key supplied directly as a string Remove-CloudSSHKey ` -PublicKey "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... user@host" .EXAMPLE # Remove a public key using a .pub file Remove-CloudSSHKey ` -PublicKeyPath "$env:USERPROFILE\.ssh\id_cloud_rsa.pub" .EXAMPLE # Remove the first key returned by Get-CloudSSHKey Get-CloudSSHKey | Select-Object -First 1 | Remove-CloudSSHKey .EXAMPLE # Remove a key based on its comment Get-CloudSSHKey | Where-Object Comment -Like "*LAVASTONE*" | Remove-CloudSSHKey .EXAMPLE # Remove a specific key without confirmation Get-CloudSSHKey | Where-Object Fingerprint -eq "SHA256:abc123..." | Remove-CloudSSHKey -Force .EXAMPLE # Remove a key from a public key file without confirmation Remove-CloudSSHKey ` -PublicKeyPath "$env:USERPROFILE\.ssh\tv-hypershell-test.pub" ` -Force #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'String' )] param ( [Parameter( Mandatory = $true, ParameterSetName = 'String', Position = 0 )] [string]$PublicKey, [Parameter( Mandatory = $true, ParameterSetName = 'File' )] [string]$PublicKeyPath, [Parameter( Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Object' )] [PSObject]$InputObject, [Parameter()] [switch]$Force ) process { # # Verify there is an active Cloud connection. # if (-not $script:CloudConnection -or -not $script:CloudConnection.Connected) { throw "No active Cloud Server connection. Run Connect-CloudServer first." } $keyToRemove = $null switch ($PSCmdlet.ParameterSetName) { # # Public key supplied directly. # 'String' { $keyToRemove = $PublicKey.Trim() } # # Read public key from file. # 'File' { if (-not (Test-Path -LiteralPath $PublicKeyPath -PathType Leaf)) { throw "SSH public key file does not exist: $PublicKeyPath" } $keyToRemove = ( Get-Content ` -LiteralPath $PublicKeyPath ` -Raw ).Trim() } # # Object supplied from Get-CloudSSHKey. # 'Object' { if (-not $InputObject.PSObject.Properties['RawKey']) { throw "The supplied object does not contain a RawKey property." } $keyToRemove = ([string]$InputObject.RawKey).Trim() } } # # Validate the OpenSSH public key format. # if ( $keyToRemove -notmatch '^(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-\S+)\s+\S+' ) { throw "The supplied value does not appear to be a valid OpenSSH public key." } # # Parse the key for display purposes. # $parts = $keyToRemove -split '\s+', 3 $rawType = $parts[0] $keyData = $parts[1] if ($parts.Count -ge 3) { $keyComment = $parts[2] } else { $keyComment = '' } $displayType = switch ($rawType) { 'ssh-rsa' { 'RSA' } 'ssh-ed25519' { 'ED25519' } 'ssh-dss' { 'DSA' } default { if ($rawType -like 'ecdsa-*') { 'ECDSA' } else { $rawType } } } # # Calculate the OpenSSH-style SHA256 fingerprint. # try { $keyBytes = [Convert]::FromBase64String($keyData) $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $hash = $sha256.ComputeHash($keyBytes) $fingerprint = "SHA256:" + [Convert]::ToBase64String($hash).TrimEnd('=') } finally { $sha256.Dispose() } } catch { throw "Unable to calculate SSH key fingerprint. The key data does not appear to be valid Base64." } # # Build API request. # $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v3-preview/management/ssh-key" $body = @{ key = $keyToRemove } # # Build a useful confirmation description. # if ([string]::IsNullOrWhiteSpace($keyComment)) { $description = "$displayType key [$fingerprint]" } else { $description = "$displayType key '$keyComment' [$fingerprint]" } # # -Force bypasses ShouldProcess confirmation. # $shouldRemove = $Force -or $PSCmdlet.ShouldProcess( $description, "Remove SSH key" ) if (-not $shouldRemove) { return } Write-Verbose "Removing SSH public key from $uri" try { Invoke-CloudApiRequest ` -Uri $uri ` -Method Delete ` -Body $body ` -ErrorAction Stop | Out-Null } catch { throw "Failed to remove SSH key: $($_.Exception.Message)" } # # Return information about the key that was successfully removed. # [PSCustomObject]@{ Type = $displayType Fingerprint = $fingerprint Comment = $keyComment Removed = $true RawKey = $keyToRemove } } } |