From d8e5346e42e166fdc97de7f512512ab353cbbf96 Mon Sep 17 00:00:00 2001 From: Oli Date: Thu, 24 Oct 2024 00:47:38 +0200 Subject: [PATCH] first commit --- README.md | 98 +++++++++++++- vaultwarden_ssh-agent.ps1 | 271 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 vaultwarden_ssh-agent.ps1 diff --git a/README.md b/README.md index a035adf..31efdad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,98 @@ -# vaultwarden-ssh-agent +# Vaultwarden SSH Agent Script +A PowerShell script that automatically loads SSH keys from your Vaultwarden vault into your SSH agent. This script provides a secure way to manage and load your SSH keys by storing them in your Vaultwarden vault and loading them into your SSH agent when needed. + +## Prerequisites + +- PowerShell 5.1 or later +- [Bitwarden CLI](https://bitwarden.com/help/cli/) installed and available in PATH +- SSH agent running on your system +- A Vaultwarden (self-hosted Bitwarden) instance + +## Installation + +1. Download the script file (`vaultwarden-ssh-agent.ps1`) to your preferred location +2. Ensure you have the Bitwarden CLI installed: + +```powershell +winget install Bitwarden.CLI +# or +choco install bitwarden-cli +``` + +## Configuration + +### Initial Setup + +Create a folder named 'ssh-agent' in your Vaultwarden vault + +For each SSH key you want to manage: +- Create a new item in the 'ssh-agent' folder +- Paste the public key in the notes field +- Attach the private key as a file attachment + +Note: If you haven't configured the Bitwarden CLI for your Vaultwarden instance yet, don't worry! The script will automatically prompt you for your server URL during the first run. + +### Key Item Structure + +Each SSH key in Vaultwarden should be structured as follows: +- Folder: Must be in the 'ssh-agent' folder +- Name: A descriptive name for your SSH key +- Notes: Contains the public key (required) +- Attachment: The private key file (required) + +### Usage +- Basic Usage: `.\vaultwarden-ssh-agent.ps1` +- To run with detailed debugging information use `-Debug` switch. + +### Session Management: + +- Reuses existing sessions when available +- Automatically handles login/unlock operations +- Stores session token as environment variable + +### Key Validation: +- Validates both public and private key formats +- Secure Memory Handling: Implements secure handling for sensitive data +- Detailed Reporting: Provides summary of successful and failed key additions + +### Security Features +- Secure handling of private keys using `SecureString` +- Proper cleanup of sensitive data from memory +- Session token management +- Key format validation before loading + +### Error Handling +The script includes comprehensive error handling: +- Validates Vaultwarden configuration +- Verifies key formats +- Reports failed operations +- Provides detailed debug output when needed +- Troubleshooting + +## Common Issues + +"ssh-agent folder not found" + +- Create a folder named ssh-agent in your Vaultwarden vault + +"Failed to add key" + +- Verify the SSH agent is running +- Check key format +- Run with -Debug flag for more information + +## Note on Security +Never share your private keys +Keep your Vaultwarden master password secure +Regularly rotate your SSH keys +Monitor SSH agent contents with `ssh-add -l` + +## Contributing +Contributions are welcome! Please feel free to submit a Pull Request. + +## Support +If you encounter any issues or have questions, please open an issue in the repository. + +## License +This script is available under the MIT License. See the LICENSE file for more details. \ No newline at end of file diff --git a/vaultwarden_ssh-agent.ps1 b/vaultwarden_ssh-agent.ps1 new file mode 100644 index 0000000..0dbf512 --- /dev/null +++ b/vaultwarden_ssh-agent.ps1 @@ -0,0 +1,271 @@ +# Vaultwarden SSH Agent Script + +param( + [switch]$Debug +) + +# Set debug preference +if ($Debug) { + $DebugPreference = 'Continue' +} else { + $DebugPreference = 'SilentlyContinue' +} + +# Constants +$FolderName = "ssh-agent" + +function Test-VaultwardenConfig { + $ConfigPath = "$env:APPDATA\Bitwarden CLI\data.json" + if (Test-Path $ConfigPath) { + $Config = Get-Content $ConfigPath -Raw | ConvertFrom-Json + if ($Config.global_environment_environment.region -eq "Self-hosted" -and + $Config.global_environment_environment.urls.base) { + Write-Debug "Vaultwarden server is configured: $($Config.global_environment_environment.urls.base)" + return + } + } + + Write-Host "Vaultwarden server is not configured. Please enter the server URL:" + $ServerUrl = Read-Host + & bw config server $ServerUrl + Write-Host "Vaultwarden server configured to: $ServerUrl" +} + +function Get-BWSession { + # Check for existing permanent session + $PersistentSession = [Environment]::GetEnvironmentVariable("BW_SESSION", "User") + if ($PersistentSession) { + Write-Debug "Existing Bitwarden session found" + $env:BW_SESSION = $PersistentSession + + # Verify session with sync + $SyncResult = & bw sync --session $PersistentSession | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host "Using existing session" + return $PersistentSession + } + Write-Host "Existing session invalid: $SyncResult" + } + + # Get new session + Write-Debug "No existing Bitwarden session found" + + $null = & bw login --check + if ($LASTEXITCODE -eq 0) { + Write-Debug "User logged in, unlocking vault" + $Session = & bw unlock --raw + } else { + Write-Debug "User not logged in, starting login" + $Session = & bw login --raw + } + + if (-not $Session) { + Write-Host "" # Add line break after failed attempt + throw "Failed to get Bitwarden session" + } + + Write-Host "Authentication successful" + [Environment]::SetEnvironmentVariable("BW_SESSION", $Session, "User") + $env:BW_SESSION = $Session + & bw sync --session $Session | Out-Null + return $Session +} + +function Clear-SensitiveData { + param([System.Object]$Variable) + + if ($Variable -is [SecureString]) { + $Variable.Dispose() + } + elseif ($Variable -is [string]) { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR( + [Runtime.InteropServices.Marshal]::StringToBSTR($Variable) + ) + } +} + +function Test-SSHKey { + param( + [string]$KeyContent, + [ValidateSet('Public', 'Private')] + [string]$KeyType + ) + + $Patterns = @{ + Public = '^(ssh-rsa|ssh-dss|ssh-ed25519|ecdsa-sha2-nistp\d+)\s+[A-Za-z0-9+/=]+\s*.*$' + Private = '(?sm)^-----BEGIN\s+(OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----.*-----END\s+(OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----$' + } + + return $KeyContent -match $Patterns[$KeyType] +} + +function Get-FolderId { + param($Session) + + Write-Debug "Getting folder: $FolderName" + $Folders = & bw list folders --search $FolderName --session $Session | ConvertFrom-Json + $Folder = $Folders | Where-Object { $_.name -eq $FolderName } + + if (-not $Folder) { + throw "'$FolderName' folder not found" + } + return $Folder.id +} + +function Get-FolderItems { + param( + $Session, + $FolderId + ) + + Write-Debug "Getting items from folder: $FolderId" + $Items = & bw list items --folderid $FolderId --session $Session | ConvertFrom-Json + Write-Debug "Found $($Items.Count) items" + return $Items +} + +function Get-PrivatePublicKey { + param( + $Session, + $Item + ) + + try { + # Get and validate public key + $PublicKey = if ($Item.notes -is [array]) { + $Item.notes -join "`n" + } else { + $Item.notes + } + + if (-not (Test-SSHKey -KeyContent $PublicKey -KeyType 'Public')) { + throw "Invalid public key format in notes" + } + + Write-Debug "Valid public key found for: $($Item.name)" + + # Get and validate private key + $Attachment = $Item.attachments | Select-Object -First 1 + if (-not $Attachment) { + throw "No attachment found" + } + + $PrivateKey = & bw get attachment $Attachment.id --itemid $Item.id --raw --session $Session + $PrivateKey = $PrivateKey -join "`n" + + if (-not (Test-SSHKey -KeyContent $PrivateKey -KeyType 'Private')) { + throw "Invalid private key format" + } + + Write-Debug "Valid private key found for: $($Item.name)" + + return @{ + PublicKey = $PublicKey + PrivateKey = ConvertTo-SecureString $PrivateKey -AsPlainText -Force + Name = $Item.name + } + } + finally { + if ($PrivateKey) { + Clear-SensitiveData $PrivateKey + } + } +} + +function Add-PrivateKeyToSSHAgent { + param( + [PSCustomObject]$SSHKey + ) + + try { + Write-Debug "Adding key: $($SSHKey.Name)" + + $PrivateKeyPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto( + [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SSHKey.PrivateKey) + ) + + $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo + $ProcessInfo.FileName = "ssh-add" + $ProcessInfo.Arguments = "-" + $ProcessInfo.RedirectStandardInput = $true + $ProcessInfo.RedirectStandardOutput = $true + $ProcessInfo.RedirectStandardError = $true + $ProcessInfo.UseShellExecute = $false + + $Process = New-Object System.Diagnostics.Process + $Process.StartInfo = $ProcessInfo + + $Process.Start() | Out-Null + $Process.StandardInput.WriteLine($PrivateKeyPlain) + $Process.StandardInput.Close() + $Process.WaitForExit() + + if ($Process.ExitCode -eq 0) { + Write-Debug "Key added successfully: $($SSHKey.Name)" + return $true + } else { + Write-Warning "Failed to add key: $($SSHKey.Name)" + return $false + } + } + catch { + Write-Error "Exception adding key $($SSHKey.Name): $_" + return $false + } + finally { + if ($PrivateKeyPlain) { + Clear-SensitiveData $PrivateKeyPlain + } + if ($Process) { + $Process.Dispose() + } + } +} + +# Main script execution +try { + Write-Debug "Checking Vaultwarden configuration" + Test-VaultwardenConfig + + Write-Host "Connect to Vaultwarden" + $session = Get-BWSession + Write-Debug "Session = $session" + + $FolderId = Get-FolderId -Session $Session + $Items = Get-FolderItems -Session $Session -FolderId $FolderId + + $Results = @{ + Success = @() + Failed = @() + } + + Write-Host "Adding SSH keys to agent..." + foreach ($Item in $Items) { + try { + $SSHKey = Get-PrivatePublicKey -Session $Session -Item $Item + if (Add-PrivateKeyToSSHAgent -SSHKey $SSHKey) { + $Results.Success += $Item.name + } else { + $Results.Failed += $Item.name + } + } + catch { + Write-Warning "Failed to process $($Item.name): $_" + $Results.Failed += $Item.name + } + } + + # Report results + Write-Host "`nResults:" + Write-Host "Successfully added keys: $($Results.Success.Count)" + $Results.Success | ForEach-Object { Write-Host " - $_" } + + if ($Results.Failed.Count -gt 0) { + Write-Host "`nFailed to add keys: $($Results.Failed.Count)" + $Results.Failed | ForEach-Object { Write-Host " - $_" } + } +} +catch { + Write-Error "Critical error: $_" + exit 1 +}