#!/usr/bin/env bash # =========================================================================== # bw-load-ssh.sh — Load SSH keys from Bitwarden into ssh-agent # Reads SSH key items from Bitwarden vault and loads private keys into # the running ssh-agent. # # Dependencies: bw (Bitwarden CLI), jq, ssh-agent running # Usage: # 1. First, authenticate: bw login # 2. Unlock and save session: bw unlock --raw > ~/.config/Bitwarden\ CLI/.session # 3. Run: ./bw-load-ssh.sh # Or run automatically via bw-ssh-keys.service (systemd user service). # =========================================================================== set -euo pipefail CONFIG_DIR="${HOME}/.config/Bitwarden CLI" SESSION_FILE="${CONFIG_DIR}/.session" # Check session file exists if [ ! -f "$SESSION_FILE" ]; then echo "ERROR: Session file not found at $SESSION_FILE" echo "Run: bw unlock --raw > '$SESSION_FILE' && chmod 600 '$SESSION_FILE'" exit 1 fi # Check ssh-agent is running (retry in case graphical session hasn't fully started) # ssh-add -l exits 2 if agent not running, 1 if no identities (which is fine) _ssh_retries=5 _ssh_waited=0 while [ $_ssh_waited -lt $_ssh_retries ]; do _ssh_exit=0 ssh-add -l >/dev/null 2>&1 || _ssh_exit=$? if [ $_ssh_exit -eq 2 ]; then sleep 2 _ssh_waited=$((_ssh_waited + 1)) else break fi done if [ $_ssh_exit -eq 2 ]; then echo "ERROR: ssh-agent is not running after ${_ssh_retries} attempts." echo "Start it with: eval \$(ssh-agent)" exit 1 fi # Read session key BW_SESSION=$(cat "$SESSION_FILE") export BW_SESSION # Verify session is still valid if ! bw status 2>/dev/null | jq -e '.status == "unlocked"' >/dev/null 2>&1; then echo "ERROR: Session is no longer valid (vault is locked or logged out)." echo "Regenerate with: bw unlock --raw > '$SESSION_FILE' && chmod 600 '$SESSION_FILE'" exit 1 fi # Find all SSH key items echo "Fetching SSH keys from vault..." ITEMS=$(bw list items 2>/dev/null | jq -c '.[] | select(.type == 5)') if [ -z "$ITEMS" ]; then echo "No SSH keys found in vault." exit 0 fi LOADED=0 SKIPPED=0 # Use process substitution instead of pipe to avoid subshell and set -e issues while IFS= read -r item; do NAME=$(echo "$item" | jq -r '.name') PUBLIC_KEY=$(echo "$item" | jq -r '.sshKey.publicKey // ""') PRIVATE_KEY=$(echo "$item" | jq -r '.sshKey.privateKey // ""') if [ -z "$PRIVATE_KEY" ]; then echo " SKIP '$NAME' — no private key found" continue fi # Extract comment from public key for checking if already loaded COMMENT=$(echo "$PUBLIC_KEY" | awk '{print $3}' | tr -d '\n') # Check if already loaded in ssh-agent if [ -n "$COMMENT" ] && ssh-add -l 2>/dev/null | grep -q "$COMMENT"; then echo " OK '$NAME' — already loaded" SKIPPED=$((SKIPPED + 1)) continue fi # Load into ssh-agent if echo "$PRIVATE_KEY" | ssh-add - 2>/dev/null; then echo " LOAD '$NAME'" LOADED=$((LOADED + 1)) else echo " FAIL '$NAME'" fi done <<< "$ITEMS" echo "Done. Loaded: $LOADED, Skipped (already loaded): $SKIPPED"