src/public/Security/New-AitherSSHKey.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Generate SSH key pairs for secure remote access .DESCRIPTION Creates SSH key pairs (RSA or ED25519) for secure authentication to remote systems. Keys are generated using OpenSSH and stored securely in the user's .ssh directory. This cmdlet is essential for setting up passwordless SSH access to servers, which is more secure and convenient than password authentication. Generated keys can be added to remote servers using Add-AitherSSHKey. .PARAMETER Name Name identifier for the key pair. This is REQUIRED and helps you identify which key to use later. The key files will be named based on this (e.g., id_rsa_<Name>). Examples: - "production" - Creates keys for production servers - "github" - Creates keys for GitHub access - "deploy" - Creates keys for deployment automation .PARAMETER KeyType Type of SSH key to generate. RSA keys are more widely compatible, while ED25519 keys are more secure and faster. Default is ED25519 for new keys. - RSA: Traditional key type, widely supported (2048, 3072, or 4096 bits) - ED25519: Modern, secure, and fast (recommended for new keys) .PARAMETER KeySize Key size in bits (only for RSA keys). Larger keys are more secure but slower. Valid values: 2048, 3072, 4096. Default is 4096 for RSA keys. Note: ED25519 keys have a fixed size and this parameter is ignored for ED25519. .PARAMETER KeyPath Directory where keys should be stored. Default is ~/.ssh (or $env:USERPROFILE\.ssh on Windows). The directory will be created if it doesn't exist. .PARAMETER Passphrase Optional passphrase to encrypt the private key. If not provided, the key will be unencrypted (less secure but more convenient for automation). For automation scenarios, you may want to leave this empty, but for personal keys, always use a passphrase. .PARAMETER Comment Comment to embed in the public key (usually email or description). Default is "AitherZero-generated key for <Name>". .PARAMETER Force Overwrite existing key pair if it already exists. Use with caution as this cannot be undone. .INPUTS System.String You can pipe key names to New-AitherSSHKey. .OUTPUTS PSCustomObject Returns an object with properties: - Name: Key name identifier - PrivateKeyPath: Path to private key file - PublicKeyPath: Path to public key file - KeyType: Type of key generated - Fingerprint: SSH key fingerprint - Created: Timestamp when key was created .EXAMPLE New-AitherSSHKey -Name "production" Creates a new ED25519 key pair named "production" in ~/.ssh directory. .EXAMPLE New-AitherSSHKey -Name "github" -KeyType RSA -KeySize 4096 Creates a 4096-bit RSA key pair for GitHub access. .EXAMPLE New-AitherSSHKey -Name "deploy" -Passphrase (Read-Host -AsSecureString "Enter passphrase") Creates a key pair with a passphrase for deployment automation. .EXAMPLE "server1", "server2" | New-AitherSSHKey Creates multiple key pairs by piping key names. .EXAMPLE New-AitherSSHKey -Name "test" -KeyPath "C:\Keys" -Comment "test@example.com" Creates a key pair in a custom location with a specific comment. .NOTES SSH keys are stored in: - Linux/macOS: ~/.ssh/ - Windows: $env:USERPROFILE\.ssh\ The private key should NEVER be shared or committed to version control. Only the public key (.pub file) should be distributed to remote servers. For automation scenarios, consider using an SSH agent to manage keys securely. .LINK Get-AitherSSHKey Add-AitherSSHKey Remove-AitherSSHKey Test-AitherSSHConnection #> function New-AitherSSHKey { [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory=$false, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$Name, [ValidateSet('RSA', 'ED25519')] [string]$KeyType = 'ED25519', [ValidateSet(2048, 3072, 4096)] [int]$KeySize = 4096, [string]$KeyPath, [System.Security.SecureString]$Passphrase, [string]$Comment, [switch]$Force ) begin { # Determine default key path if (-not $KeyPath) { if ($IsWindows) { $KeyPath = Join-Path $env:USERPROFILE '.ssh' } else { $KeyPath = Join-Path $env:HOME '.ssh' } } # Ensure key directory exists if (-not (Test-Path $KeyPath)) { New-Item -Path $KeyPath -ItemType Directory -Force | Out-Null } # Set proper permissions on Unix if ($IsLinux -or $IsMacOS) { # Ensure .ssh directory has correct permissions $currentPerms = (Get-Item $KeyPath).Mode if ($currentPerms -notmatch '^d[rwx-]{2}[r-][wx-][rx-]$') { chmod 700 $KeyPath } } } process { try { # During module validation, skip execution if ($PSCmdlet.MyInvocation.InvocationName -eq '.' -and -not $Name) { return $null } # Check if ssh-keygen is available if (-not (Get-Command ssh-keygen -ErrorAction SilentlyContinue)) { throw "ssh-keygen command not found. Install OpenSSH client to generate SSH keys." } $hasWriteAitherLog = Get-Command Write-AitherLog -ErrorAction SilentlyContinue # Generate key file names $keyPrefix = if ($KeyType -eq 'RSA') { "id_rsa" } else { "id_ed25519" } $privateKeyPath = Join-Path $KeyPath "${keyPrefix}_${Name}" $publicKeyPath = "${privateKeyPath}.pub" # Check if keys already exist if ((Test-Path $privateKeyPath) -or (Test-Path $publicKeyPath)) { if (-not $Force) { $errorObject = [PSCustomObject]@{ PSTypeName = 'AitherZero.Error' Success = $false ErrorId = [System.Guid]::NewGuid().ToString() Cmdlet = $PSCmdlet.MyInvocation.MyCommand.Name Operation = "Generating SSH key: $Name" Error = "Key pair already exists: $privateKeyPath. Use -Force to overwrite." Timestamp = Get-Date } Write-Output $errorObject if ($hasWriteAitherLog) { Write-AitherLog -Level Warning -Message "Key pair already exists: $privateKeyPath" -Source $PSCmdlet.MyInvocation.MyCommand.Name } return } } # Build ssh-keygen command $keygenArgs = @() if ($KeyType -eq 'RSA') { $keygenArgs += '-t', 'rsa' $keygenArgs += '-b', $KeySize.ToString() } else { $keygenArgs += '-t', 'ed25519' } $keygenArgs += '-f', $privateKeyPath if ($Passphrase) { # Write passphrase to temporary file for ssh-keygen $tempPassFile = Join-Path ([System.IO.Path]::GetTempPath()) "ssh_pass_$([System.Guid]::NewGuid()).txt" try { $plainPass = [Runtime.InteropServices.Marshal]::PtrToStringBSTR( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Passphrase) ) $plainPass | Out-File -FilePath $tempPassFile -Encoding ASCII -NoNewline $keygenArgs += '-N', $plainPass [Runtime.InteropServices.Marshal]::ZeroFreeBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Passphrase)) } finally { if (Test-Path $tempPassFile) { Remove-Item $tempPassFile -Force -ErrorAction SilentlyContinue } } } else { $keygenArgs += '-N', '""' } # Add comment if (-not $Comment) { $Comment = "AitherZero-generated key for $Name" } $keygenArgs += '-C', $Comment # Generate key if ($PSCmdlet.ShouldProcess($privateKeyPath, "Generate SSH key pair")) { $keygenProcess = Start-Process -FilePath 'ssh-keygen' -ArgumentList $keygenArgs -Wait -PassThru -NoNewWindow if ($keygenProcess.ExitCode -ne 0) { throw "ssh-keygen failed with exit code $($keygenProcess.ExitCode)" } # Set proper permissions on Unix if ($IsLinux -or $IsMacOS) { chmod 600 $privateKeyPath chmod 644 $publicKeyPath } # Get fingerprint $fingerprintOutput = & ssh-keygen -lf $publicKeyPath 2>&1 $fingerprint = if ($fingerprintOutput -match '^\d+\s+([a-f0-9:]+)') { $matches[1] } else { "Unknown" } $result = [PSCustomObject]@{ PSTypeName = 'AitherZero.SSHKey' Name = $Name PrivateKeyPath = $privateKeyPath PublicKeyPath = $publicKeyPath KeyType = $KeyType KeySize = if ($KeyType -eq 'RSA') { $KeySize } else { 256 } Fingerprint = $fingerprint Comment = $Comment Created = Get-Date } if ($hasWriteAitherLog) { Write-AitherLog -Level Information -Message "Generated SSH key pair: $Name" -Source $PSCmdlet.MyInvocation.MyCommand.Name -Data @{ KeyType = $KeyType KeySize = if ($KeyType -eq 'RSA') { $KeySize } else { 256 } Fingerprint = $fingerprint } } return $result } } catch { # Use centralized error handling $errorScript = Join-Path $PSScriptRoot '..' 'Private' 'Write-AitherError.ps1' if (Test-Path $errorScript) { . $errorScript -ErrorRecord $_ -CmdletName $PSCmdlet.MyInvocation.MyCommand.Name -Operation "Generating SSH key: $Name" -Parameters $PSBoundParameters -ThrowOnError } else { # Fallback error handling $hasWriteAitherLogFallback = Get-Command Write-AitherLog -ErrorAction SilentlyContinue $errorObject = [PSCustomObject]@{ PSTypeName = 'AitherZero.Error' Success = $false ErrorId = [System.Guid]::NewGuid().ToString() Cmdlet = $PSCmdlet.MyInvocation.MyCommand.Name Operation = "Generating SSH key: $Name" Error = $_.Exception.Message Timestamp = Get-Date } Write-Output $errorObject if ($hasWriteAitherLogFallback) { Write-AitherLog -Level Error -Message "Failed to generate SSH key $Name : $($_.Exception.Message)" -Source $PSCmdlet.MyInvocation.MyCommand.Name -Exception $_ } else { Write-AitherLog -Level Error -Message "Failed to generate SSH key $Name : $($_.Exception.Message)" -Source $PSCmdlet.MyInvocation.MyCommand.Name -Exception $_ } } throw } } } |