improved error handling and better session management
Added - New ClearSession parameter to remove the stored Vaultwarden session from the Windows Credential Manager. - New Test-Prerequisites function to check if the required tools (ssh-add, Bitwarden CLI) are installed and available. - New Get-BWSession function to retrieve the Vaultwarden session from the Windows Credential Manager, and fallback to getting a new session if the stored one is invalid. - New Clear-SensitiveData function to clear sensitive data (SecureString, string) from memory. - New Clear-BWSession function to remove the stored session from the Windows Credential Manager. Changed - The Test-VaultwardenConfig function now prompts the user to enter the Vaultwarden server URL if it's not configured, rather than throwing an error. - The Get-FolderId and Get-FolderItems functions now take the session as a parameter, rather than relying on a global session variable. - The Get-PrivatePublicKey function now uses the --raw flag to retrieve the private key attachment, instead of joining the attachment content. The main script execution has been reorganized to handle the session retrieval and management more explicitly. - The script now includes a detailed help section at the top, providing information about the script's usage and parameters. Removed
This commit is contained in:
90
README.md
90
README.md
@@ -6,27 +6,19 @@ A PowerShell script that automatically loads SSH keys from your Vaultwarden vaul
|
||||
|
||||
- PowerShell 5.1 or later
|
||||
- [Bitwarden CLI](https://bitwarden.com/help/cli/) installed and available in PATH
|
||||
- OpenSSH installed on your system
|
||||
- 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
|
||||
```
|
||||
Download the script file (`vaultwarden-ssh-agent.ps1`) to your preferred location
|
||||
|
||||
## Configuration
|
||||
|
||||
### Initial Setup
|
||||
|
||||
Create a folder named 'ssh-agent' in your Vaultwarden vault
|
||||
|
||||
For each SSH key you want to manage:
|
||||
- Create a folder (default name: 'ssh-agent') in your Vaultwarden vault
|
||||
- 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
|
||||
@@ -36,57 +28,77 @@ Note: If you haven't configured the Bitwarden CLI for your Vaultwarden instance
|
||||
### Key Item Structure
|
||||
|
||||
Each SSH key in Vaultwarden should be structured as follows:
|
||||
- Folder: Must be in the 'ssh-agent' folder
|
||||
- Folder: Must be in the 'ssh-agent' folder (or your custom folder name)
|
||||
- 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.
|
||||
## Usage
|
||||
### Basic usage
|
||||
.\vaultwarden-ssh-agent.ps1
|
||||
|
||||
### Session Management:
|
||||
### Run with debug information
|
||||
.\vaultwarden-ssh-agent.ps1 -Debug
|
||||
|
||||
- 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
|
||||
### Use a custom folder name
|
||||
.\vaultwarden-ssh-agent.ps1 -FolderName "my-ssh-keys"
|
||||
|
||||
### Security Features
|
||||
- Secure session key storage using Windows Credential Manager
|
||||
- Secure handling of private keys using `SecureString`
|
||||
- Proper cleanup of sensitive data from memory
|
||||
- Session token management
|
||||
- Key format validation before loading
|
||||
|
||||
### Clearing Stored Session Token
|
||||
The script stores the Bitwarden session token in the Windows Credential Manager. This allows the script to reuse the session, avoiding the need to unlock the vault on every run.
|
||||
|
||||
If you need to clear the stored session for any reason, you can run the following command: `.\Vaultwarden_ssh-agent.ps1 -ClearSession`
|
||||
|
||||
This will remove the stored session token from the Windows Credential Manager. The next time you run the script, it will need to unlock the vault with your master password before proceeding.
|
||||
|
||||
Clearing the stored session may be necessary to ensure a fresh session is obtained.
|
||||
|
||||
### Error Handling
|
||||
The script includes comprehensive error handling:
|
||||
- Validates Vaultwarden configuration
|
||||
- Verifies key formats
|
||||
- Reports failed operations
|
||||
- Provides detailed debug output when needed
|
||||
- Prerequisites validation
|
||||
- Vaultwarden configuration verification
|
||||
- Key format validation
|
||||
- Detailed debug output when needed
|
||||
- Operation results reporting
|
||||
- Troubleshooting
|
||||
|
||||
## Common Issues
|
||||
### Common Issues
|
||||
**"SSH agent is not running"**
|
||||
|
||||
"ssh-agent folder not found"
|
||||
Start the SSH agent service:
|
||||
```
|
||||
Set-Service ssh-agent -StartupType Automatic
|
||||
Start-Service ssh-agent
|
||||
```
|
||||
|
||||
- Create a folder named ssh-agent in your Vaultwarden vault
|
||||
**"Folder not found"**
|
||||
|
||||
"Failed to add key"
|
||||
- Create the folder in your Vaultwarden vault
|
||||
- Verify the folder name matches the script parameter (default: "ssh-agent")
|
||||
|
||||
**"Failed to add key"**
|
||||
- Verify the SSH agent is running
|
||||
- Check key format
|
||||
- Check key format in Vaultwarden
|
||||
- Run with -Debug flag for more information
|
||||
- Verify private key file attachment
|
||||
|
||||
## 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`
|
||||
**"Bitwarden CLI not found"**
|
||||
- Ensure Bitwarden CLI is installed
|
||||
- Verify bw is in your system PATH
|
||||
- Debug ModeRun the script with the -Debug switch for detailed operation information:.\vaultwarden-ssh-agent.ps1 -Debug
|
||||
|
||||
## Security Notes
|
||||
- Never share your private keys
|
||||
- Regularly rotate your SSH keys
|
||||
- Session key are stored securely in Windows Credential Manager
|
||||
- Private keys are handled as `SecureString` during processing
|
||||
- All sensitive data is cleared from memory after use
|
||||
- Monitor SSH agent contents with `ssh-add -l`
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
# Vaultwarden SSH Agent Script
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Adds SSH keys from Vaultwarden to the SSH agent.
|
||||
.DESCRIPTION
|
||||
Retrieves SSH keys stored in Vaultwarden and adds them to the running SSH agent.
|
||||
Public keys should be stored in the item's notes and private keys as attachments.
|
||||
.PARAMETER Debug
|
||||
Enables debug output.
|
||||
.PARAMETER FolderName
|
||||
The name of the Vaultwarden folder containing SSH keys. Defaults to "ssh-agent".
|
||||
.PARAMETER ClearSession
|
||||
Removes the stored Vaultwarden session from the Windows Credential Manager.
|
||||
.EXAMPLE
|
||||
.\Vaultwarden_ssh-agent.ps1
|
||||
Imports all SSH keys from Vaultwarden to the SSH agent.
|
||||
.EXAMPLE
|
||||
.\Vaultwarden_ssh-agent.ps1 -FolderName "my-ssh-keys"
|
||||
Imports SSH keys from a custom folder name.
|
||||
.EXAMPLE
|
||||
.\Vaultwarden_ssh-agent.ps1 -ClearSession
|
||||
Removes the stored Vaultwarden session from the Windows Credential Manager.
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$Debug
|
||||
[switch]$Debug,
|
||||
[string]$FolderName = "ssh-agent",
|
||||
[switch]$ClearSession
|
||||
)
|
||||
|
||||
# Set debug preference
|
||||
@@ -11,10 +34,67 @@ if ($Debug) {
|
||||
$DebugPreference = 'SilentlyContinue'
|
||||
}
|
||||
|
||||
# Constants
|
||||
$FolderName = "ssh-agent"
|
||||
# Add CredManager type definition at script start
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public class CredManager {
|
||||
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
||||
public static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError=true)]
|
||||
public static extern void CredFree(IntPtr buffer);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
||||
public struct CREDENTIAL {
|
||||
public int Flags;
|
||||
public int Type;
|
||||
public string TargetName;
|
||||
public string Comment;
|
||||
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
||||
public int CredentialBlobSize;
|
||||
public IntPtr CredentialBlob;
|
||||
public int Persist;
|
||||
public int AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
public string TargetAlias;
|
||||
public string UserName;
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
function Clear-BWSession {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Write-Debug "Removing stored session from Windows Credential Manager"
|
||||
& cmdkey /delete:"Vaultwarden_Session" | Out-Null
|
||||
}
|
||||
|
||||
function Test-Prerequisites {
|
||||
# Check if ssh-add exists
|
||||
if (-not (Get-Command "ssh-add" -ErrorAction SilentlyContinue)) {
|
||||
throw "ssh-add command not found. Please ensure OpenSSH is installed."
|
||||
}
|
||||
|
||||
# Check if SSH agent is running
|
||||
$process = Get-Process ssh-agent -ErrorAction SilentlyContinue
|
||||
if (-not $process) {
|
||||
throw "SSH agent is not running. Please start the SSH agent service."
|
||||
}
|
||||
|
||||
# Check if bw CLI is available
|
||||
if (-not (Get-Command "bw" -ErrorAction SilentlyContinue)) {
|
||||
throw "Bitwarden CLI not found. Please install the Bitwarden CLI."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Test-VaultwardenConfig {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ConfigPath = "$env:APPDATA\Bitwarden CLI\data.json"
|
||||
if (Test-Path $ConfigPath) {
|
||||
$Config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
||||
@@ -28,51 +108,99 @@ function Test-VaultwardenConfig {
|
||||
Write-Host "Vaultwarden server is not configured. Please enter the server URL:"
|
||||
$ServerUrl = Read-Host
|
||||
& bw config server $ServerUrl
|
||||
Write-Host "" # Add line break after failed attempt
|
||||
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
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Write-Debug "Retrieving session from Windows Credential Manager"
|
||||
|
||||
$CredPtr = [IntPtr]::Zero
|
||||
$Result = [CredManager]::CredRead("Vaultwarden_Session", 1, 0, [ref]$CredPtr)
|
||||
|
||||
if ($Result) {
|
||||
try {
|
||||
$Cred = [System.Runtime.InteropServices.Marshal]::PtrToStructure($CredPtr, [Type][CredManager+CREDENTIAL])
|
||||
|
||||
if (-not $Cred.CredentialBlobSize) {
|
||||
Write-Debug "No stored session found"
|
||||
return $null
|
||||
}
|
||||
|
||||
$SecretBytes = New-Object byte[] $Cred.CredentialBlobSize
|
||||
[System.Runtime.InteropServices.Marshal]::Copy($Cred.CredentialBlob, $SecretBytes, 0, $Cred.CredentialBlobSize)
|
||||
$Session = [System.Text.Encoding]::Unicode.GetString($SecretBytes)
|
||||
|
||||
# Check session status
|
||||
Write-Debug "Stored session found"
|
||||
# bw status is always locked when using --session
|
||||
# https://github.com/bitwarden/clients/issues/9254
|
||||
[Environment]::SetEnvironmentVariable("BW_SESSION", $Session, [System.EnvironmentVariableTarget]::Process)
|
||||
$StatusResult = & bw status 2>&1
|
||||
[Environment]::SetEnvironmentVariable("BW_SESSION", $null, [System.EnvironmentVariableTarget]::Process)
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$Status = $StatusResult | ConvertFrom-Json
|
||||
if ($Status.status -eq "unlocked") {
|
||||
Write-Debug "Stored session is valid"
|
||||
Write-Host "Using stored session"
|
||||
return $Session
|
||||
}
|
||||
}
|
||||
|
||||
Write-Debug "Stored session is invalid, removing"
|
||||
& cmdkey /delete:"Vaultwarden_Session" | Out-Null
|
||||
}
|
||||
finally {
|
||||
[CredManager]::CredFree($CredPtr)
|
||||
if ($Session) {
|
||||
Clear-SensitiveData $Session
|
||||
}
|
||||
}
|
||||
Write-Host "Existing session invalid: $SyncResult"
|
||||
}
|
||||
|
||||
# Get new session
|
||||
Write-Debug "No existing Bitwarden session found"
|
||||
Write-Host "Getting new session"
|
||||
|
||||
$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
|
||||
# Check current login status
|
||||
Write-Debug "Checking login status"
|
||||
$Status = & bw status | ConvertFrom-Json
|
||||
|
||||
if ($Status.status -eq "unauthenticated") {
|
||||
Write-Debug "Not logged in, attempting API key login"
|
||||
& bw login --apikey
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Login failed"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Session) {
|
||||
Write-Host "" # Add line break after failed attempt
|
||||
throw "Failed to get Bitwarden session"
|
||||
else {
|
||||
Write-Debug "Already logged in as $($Status.userEmail)"
|
||||
}
|
||||
|
||||
Write-Host "Authentication successful"
|
||||
[Environment]::SetEnvironmentVariable("BW_SESSION", $Session, "User")
|
||||
$env:BW_SESSION = $Session
|
||||
& bw sync --session $Session | Out-Null
|
||||
return $Session
|
||||
|
||||
Write-Debug "Unlocking vault"
|
||||
$NewSession = & bw unlock --raw
|
||||
if (-not $NewSession) {
|
||||
throw "Failed to unlock vault"
|
||||
}
|
||||
|
||||
# Store session in Windows Credential Manager
|
||||
Write-Debug "Storing session in Windows Credential Manager"
|
||||
$Result = & cmdkey /generic:"Vaultwarden_Session" /user:$($Status.userEmail) /pass:$NewSession
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to store session: $Result"
|
||||
}
|
||||
|
||||
Write-Host "New session established"
|
||||
return $NewSession
|
||||
}
|
||||
|
||||
function Clear-SensitiveData {
|
||||
param([System.Object]$Variable)
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[System.Object]$Variable
|
||||
)
|
||||
|
||||
if ($Variable -is [SecureString]) {
|
||||
$Variable.Dispose()
|
||||
@@ -85,8 +213,12 @@ function Clear-SensitiveData {
|
||||
}
|
||||
|
||||
function Test-SSHKey {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$KeyContent,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateSet('Public', 'Private')]
|
||||
[string]$KeyType
|
||||
)
|
||||
@@ -100,10 +232,14 @@ function Test-SSHKey {
|
||||
}
|
||||
|
||||
function Get-FolderId {
|
||||
param($Session)
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Session
|
||||
)
|
||||
|
||||
Write-Debug "Getting folder: $FolderName"
|
||||
$Folders = & bw list folders --search $FolderName --session $Session | ConvertFrom-Json
|
||||
$Folders = & bw list folders --session $Session --search $FolderName | ConvertFrom-Json
|
||||
$Folder = $Folders | Where-Object { $_.name -eq $FolderName }
|
||||
|
||||
if (-not $Folder) {
|
||||
@@ -113,21 +249,27 @@ function Get-FolderId {
|
||||
}
|
||||
|
||||
function Get-FolderItems {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
$Session,
|
||||
$FolderId
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Session,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$FolderId
|
||||
)
|
||||
|
||||
Write-Debug "Getting items from folder: $FolderId"
|
||||
$Items = & bw list items --folderid $FolderId --session $Session | ConvertFrom-Json
|
||||
$Items = & bw list items --session $Session --folderid $FolderId | ConvertFrom-Json
|
||||
Write-Debug "Found $($Items.Count) items"
|
||||
return $Items
|
||||
}
|
||||
|
||||
function Get-PrivatePublicKey {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
$Session,
|
||||
$Item
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Session,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[PSObject]$Item
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -150,7 +292,7 @@ function Get-PrivatePublicKey {
|
||||
throw "No attachment found"
|
||||
}
|
||||
|
||||
$PrivateKey = & bw get attachment $Attachment.id --itemid $Item.id --raw --session $Session
|
||||
$PrivateKey = & bw get attachment $Attachment.id --session $Session --itemid $Item.id --raw
|
||||
$PrivateKey = $PrivateKey -join "`n"
|
||||
|
||||
if (-not (Test-SSHKey -KeyContent $PrivateKey -KeyType 'Private')) {
|
||||
@@ -173,7 +315,10 @@ function Get-PrivatePublicKey {
|
||||
}
|
||||
|
||||
function Add-PrivateKeyToSSHAgent {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNull()]
|
||||
[PSCustomObject]$SSHKey
|
||||
)
|
||||
|
||||
@@ -224,13 +369,27 @@ function Add-PrivateKeyToSSHAgent {
|
||||
|
||||
# Main script execution
|
||||
try {
|
||||
if ($ClearSession) {
|
||||
Clear-BWSession
|
||||
Write-Host "Stored session has been cleared"
|
||||
return
|
||||
}
|
||||
|
||||
Test-Prerequisites
|
||||
|
||||
Write-Debug "Checking Vaultwarden configuration"
|
||||
Test-VaultwardenConfig
|
||||
|
||||
Write-Host "Connect to Vaultwarden"
|
||||
$session = Get-BWSession
|
||||
Write-Debug "Session = $session"
|
||||
|
||||
|
||||
# Get session
|
||||
$Session = Get-BWSession
|
||||
|
||||
# Sync vault
|
||||
Write-Debug "Syncing vault"
|
||||
& bw sync --session $Session --quiet
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to sync vault"
|
||||
}
|
||||
|
||||
$FolderId = Get-FolderId -Session $Session
|
||||
$Items = Get-FolderItems -Session $Session -FolderId $FolderId
|
||||
|
||||
@@ -269,3 +428,8 @@ catch {
|
||||
Write-Error "Critical error: $_"
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
if ($Session) {
|
||||
Clear-SensitiveData $Session
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user