first commit
This commit is contained in:
98
README.md
98
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.
|
||||||
271
vaultwarden_ssh-agent.ps1
Normal file
271
vaultwarden_ssh-agent.ps1
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user