src/public/Security/Add-AitherSSHKey.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Add SSH public key to remote server's authorized_keys file .DESCRIPTION Adds an SSH public key to a remote server's authorized_keys file, enabling passwordless SSH access. This is essential for automation and secure remote access setup. The cmdlet can add keys via SSH (if you have password access) or by directly modifying the authorized_keys file if you have file system access. .PARAMETER KeyName Name of the SSH key to add (as created by New-AitherSSHKey). This parameter is REQUIRED and identifies which public key to add. Example: "production", "github", "deploy" .PARAMETER Target Target server hostname or IP address where the key should be added. This parameter is REQUIRED and specifies the remote server. .PARAMETER User Username on the remote server. Defaults to current user or 'root' if not specified. The key will be added to this user's authorized_keys file. .PARAMETER AuthorizedKeysPath Custom path to authorized_keys file on the remote server. Default is ~/.ssh/authorized_keys (or $env:USERPROFILE\.ssh\authorized_keys on Windows). .PARAMETER Credential PSCredential object for SSH authentication (if password access is needed). Required if you don't already have SSH key access to the server. .PARAMETER Port SSH port number. Default is 22. .PARAMETER Force Overwrite existing key entry if the same key already exists. .INPUTS System.String You can pipe key names to Add-AitherSSHKey. PSCustomObject You can pipe SSH key objects from Get-AitherSSHKey to Add-AitherSSHKey. .OUTPUTS PSCustomObject Returns result object with properties: - Success: Whether the operation succeeded - Target: Target server - User: Remote username - KeyAdded: Whether a new key was added - KeyExists: Whether the key already existed .EXAMPLE Add-AitherSSHKey -KeyName "production" -Target "server.example.com" Adds the "production" public key to server.example.com for the current user. .EXAMPLE Add-AitherSSHKey -KeyName "deploy" -Target "192.168.1.100" -User "deploy" -Credential (Get-Credential) Adds the "deploy" key to a remote server using password authentication. .EXAMPLE Get-AitherSSHKey -Name "github" | Add-AitherSSHKey -Target "github.com" -User "git" Pipes SSH key object to Add-AitherSSHKey. .EXAMPLE "server1", "server2" | ForEach-Object { Add-AitherSSHKey -KeyName "production" -Target $_ } Adds the same key to multiple servers. .NOTES This cmdlet requires: - SSH access to the remote server (password or existing key) - Write access to the remote user's .ssh directory - The .ssh directory and authorized_keys file will be created if they don't exist Security best practices: - Always verify the server's host key before adding keys - Use specific keys for specific purposes (don't reuse keys) - Regularly rotate SSH keys .LINK New-AitherSSHKey Get-AitherSSHKey Remove-AitherSSHKey Test-AitherSSHConnection #> function Add-AitherSSHKey { [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param( [Parameter(Mandatory=$false, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [AllowEmptyString()] [string]$KeyName, [Parameter(Mandatory=$false, Position = 1)] [AllowEmptyString()] [string]$Target, [Parameter()] [string]$User, [Parameter()] [System.Management.Automation.PSCredential]$Credential, [Parameter()] [ValidateRange(1, 65535)] [int]$Port = 22, [Parameter()] [string]$AuthorizedKeysPath, [Parameter()] [switch]$Force ) begin { # Check if ssh command is available if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) { throw "SSH command not found. Install OpenSSH client to use SSH key management." } } process { try { # Validate KeyName parameter if ([string]::IsNullOrWhiteSpace($KeyName)) { # During module validation, KeyName may be empty - skip validation if ($PSCmdlet.MyInvocation.InvocationName -eq '.') { return } throw "KeyName parameter is required" } # Get SSH key information $keyInfo = Get-AitherSSHKey -Name $KeyName -IncludeContent if (-not $keyInfo -or -not $keyInfo.PublicKeyPath) { throw "SSH key not found: $KeyName. Use New-AitherSSHKey to create a key first." } if (-not $keyInfo.PublicKeyContent) { $keyInfo.PublicKeyContent = Get-Content -Path $keyInfo.PublicKeyPath -Raw -ErrorAction Stop } # Determine remote user if (-not $User) { $User = if ($IsWindows) { $env:USERNAME } else { $env:USER } } # Determine authorized_keys path if (-not $AuthorizedKeysPath) { $AuthorizedKeysPath = ".ssh/authorized_keys" } # Build SSH command to add key $sshTarget = if ($User) { "${User}@${Target}" } else { $Target } $sshArgs = @() if ($Port -ne 22) { $sshArgs += '-p', $Port.ToString() } $sshArgs += $sshTarget # Check if key already exists $checkCommand = "test -f $AuthorizedKeysPath && grep -qF '$(($keyInfo.PublicKeyContent -replace "'", "'\\''"))' $AuthorizedKeysPath || echo 'NOT_FOUND'" $checkResult = if ($Credential) { $plainPass = [Runtime.InteropServices.Marshal]::PtrToStringBSTR( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) ) $checkOutput = echo $plainPass | sshpass -p $plainPass ssh @sshArgs $checkCommand 2>&1 [Runtime.InteropServices.Marshal]::ZeroFreeBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password)) $checkOutput } else { & ssh @sshArgs $checkCommand 2>&1 } $keyExists = $checkResult -notmatch 'NOT_FOUND' if ($keyExists -and -not $Force) { Write-AitherLog -Level Information -Message "SSH key already exists on $Target for user $User" -Source $PSCmdlet.MyInvocation.MyCommand.Name return [PSCustomObject]@{ Success = $true Target = $Target User = $User KeyAdded = $false KeyExists = $true } } # Add key command $addCommand = @" mkdir -p .ssh 2>/dev/null; chmod 700 .ssh; echo '$($keyInfo.PublicKeyContent)' >> $AuthorizedKeysPath; chmod 600 $AuthorizedKeysPath "@ if ($PSCmdlet.ShouldProcess("$Target ($User)", "Add SSH key $KeyName")) { $addResult = if ($Credential) { $plainPass = [Runtime.InteropServices.Marshal]::PtrToStringBSTR( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) ) $addOutput = echo $plainPass | sshpass -p $plainPass ssh @sshArgs $addCommand 2>&1 [Runtime.InteropServices.Marshal]::ZeroFreeBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password)) $addOutput } else { & ssh @sshArgs $addCommand 2>&1 } if ($LASTEXITCODE -ne 0) { throw "Failed to add SSH key: $addResult" } Write-AitherLog -Level Information -Message "Added SSH key $KeyName to $Target for user $User" -Source $PSCmdlet.MyInvocation.MyCommand.Name return [PSCustomObject]@{ Success = $true Target = $Target User = $User KeyAdded = $true KeyExists = $keyExists } } } catch { # Use centralized error handling Invoke-AitherErrorHandler -ErrorRecord $_ -CmdletName $PSCmdlet.MyInvocation.MyCommand.Name -Operation "Adding SSH key $KeyName to $Target" -Parameters $PSBoundParameters -ThrowOnError } } } |