Metadata-Version: 2.4
Name: keepassxc-ssh-agent
Version: 1.6.0
Summary: SSH IdentityAgent proxy that triggers KeePassXC database unlock (TouchID on macOS, biometric/password on Linux)
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: keepassxc-browser-api==1.5.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Dynamic: license-file

# KeePassXC SSH Agent

`keepassxc-ssh-agent` is a cross-platform (macOS and Linux) SSH `IdentityAgent` proxy that automatically triggers KeePassXC database unlock (via TouchID / Quick Unlock on macOS, biometric / password unlock on Linux) when an SSH key is needed.

Similar to how [Strongbox](https://strongboxsafe.com/) handles SSH keys, this tool sits between your SSH client and the system `ssh-agent`. When SSH requests a key that isn't loaded (because the KeePassXC database is locked), the proxy triggers KeePassXC's unlock dialog. After you authenticate with TouchID, KeePassXC pushes the keys to `ssh-agent`, and the SSH operation continues seamlessly.

![Functionality demonstration](assets/demo.gif)

KeePassXC SSH Agent based on [KeePassXC Browser API](https://github.com/mietzen/keepassxc-browser-api).

## Prerequisites

- **macOS or Linux** (uses Unix sockets and KeePassXC's browser extension socket; on Linux the socket path is resolved from `XDG_RUNTIME_DIR`, supporting native and Flatpak installs)
- **Python >= 3.10**
- **KeePassXC** with:
  - Browser Integration enabled (Settings > Browser Integration > Enable browser integration)
    ![KeePassXC Browser Integration Settings screenshot](assets/settings-browser-integration.png)
  - SSH Agent Integration enabled (Settings > SSH Agent > Enable SSH Agent integration)
    ![SSH-Agent settings screenshot](assets/settings-ssh-agent.png)
  - SSH keys configured with "Add key to agent when database is opened/unlocked"
    ![SSH-Key entry settings screenshot](assets/ssh-key-entry-settings.png)

## Usage

```
usage: keepassxc-ssh-agent [-h] [--socket SOCKET] [--config CONFIG]
                           [--timeout TIMEOUT] [-v]
                           {install,run,status,uninstall} ...

SSH IdentityAgent proxy that triggers KeePassXC database unlock via TouchID

positional arguments:
  {install,run,status,uninstall}
    install             Associate with KeePassXC and set up the auto-start service
    run                 Start the SSH agent proxy (default command)
    status              Check connection status with KeePassXC
    uninstall           Remove the auto-start service and restore SSH_AUTH_SOCK

options:
  -h, --help          show this help message and exit
  --socket SOCKET     Path for the agent Unix socket
                      (default: ~/.keepassxc/agent.sock)
  --config CONFIG     Path to config file
                      (default: ~/.keepassxc/ssh-agent.json)
  --timeout TIMEOUT   Timeout in seconds for unlock prompt (default: 30)
  -v, --verbose       Enable verbose logging
```

## How It Works

```
SSH Client ──► SSH agent protocol ──► keepassxc-ssh-agent (proxy)
                                             │
                                             ├─► SSH agent protocol ──► System ssh-agent
                                             │   (forward requests / replay after unlock)
                                             │
                                             └─► Browser extension protocol ──► KeePassXC
                                                 (trigger unlock when keys missing)
```

1. SSH client connects to the proxy socket and requests identities or a signature
2. Proxy forwards the request to the system `ssh-agent`
3. If `ssh-agent` returns keys/signature, proxy passes it through (no delay)
4. If `ssh-agent` returns empty/failure (DB is locked, keys not loaded):
   - Proxy connects to KeePassXC via the browser extension protocol
   - Sends `get-databasehash` with `triggerUnlock` to show the unlock dialog
   - Polls until the database is unlocked or timeout expires
   - KeePassXC pushes SSH keys to `ssh-agent` on unlock (asynchronously)
   - Proxy then polls `ssh-agent` until the key appears (rather than retrying
     once), so the very first request succeeds, and returns the result

### SSH_AUTH_SOCK Interception

The proxy automatically intercepts `SSH_AUTH_SOCK` on startup by renaming the system ssh-agent socket (e.g. `/tmp/com.apple.launchd.XXX/Listeners` on macOS, `/run/user/UID/ssh-agent.socket` on Linux) to a `.system` backup and placing a symlink from the original path to the proxy socket. All SSH clients then connect to the proxy transparently. The proxy forwards requests to the renamed `.system` socket.

On shutdown, the proxy restores the original socket. No separate service or SSH config is needed — the `run` command handles everything.

> **Note (Linux/systemd):** a systemd `--user` service starts with a clean environment and does not inherit your shell's `SSH_AUTH_SOCK`. `install` therefore captures the current `SSH_AUTH_SOCK` and bakes it into the generated unit (`Environment=SSH_AUTH_SOCK=...`) so interception works without any shell-profile changes.

## Install

Make sure KeePassXC is running and unlocked with browser integration enabled, then:

### Homebrew (macOS, recommended)

See **[homebrew homepage](https://brew.sh/)** on how to setup homebrew.

```shell
brew install mietzen/tap/keepassxc-ssh-agent
keepassxc-ssh-agent install --register-only
brew services start keepassxc-ssh-agent
```

This will:
- Install `keepassxc-ssh-agent` and its dependencies
- Register with KeePassXC (you'll need to approve the association in the unlocked KeePassXC window)
- Start the background service via Homebrew (auto-starts on login)

### pipx - Automatic Install (macOS and Linux)

See **[pipx installation guide](https://github.com/pypa/pipx#install-pipx)** on how to setup pipx.

```shell
pipx install keepassxc-ssh-agent
keepassxc-ssh-agent install -y
```

This will:
- Generate encryption keys for the browser protocol
- Request association with KeePassXC (you'll need to approve it in the KeePassXC window)
- Save the agent configuration to `~/.keepassxc/ssh-agent.json`
- Save the browser API credentials to `~/.keepassxc/browser-api.json` (shared with `keepassxc-cli` if installed)
- Set up an auto-start service for login: a **LaunchAgent** on macOS, or a **systemd user service** (`~/.config/systemd/user/keepassxc-ssh-agent.service`) on Linux

The `-y` flag auto-accepts all prompts. Without it, you'll be asked interactively whether to set up the auto-start service.

On Linux, manage the service with `systemctl --user`:

```shell
systemctl --user status keepassxc-ssh-agent     # check status
systemctl --user restart keepassxc-ssh-agent    # restart
journalctl --user -u keepassxc-ssh-agent        # view logs
```

> systemd `--user` services require an active user session. On headless machines (no graphical login), enable lingering so the service starts at boot: `loginctl enable-linger $USER`.

### pipx - Manual Install

If you want more control over the installation, you can split it into steps:

#### 1. Install the package

```shell
pipx install keepassxc-ssh-agent
```

#### 2. Register with KeePassXC

Register with KeePassXC without setting up the auto-start service:

```shell
keepassxc-ssh-agent install --register-only
```

You'll need to approve the association in the KeePassXC window when prompted.

#### 3. Set up auto-start (optional)

**macOS** — create a LaunchAgent plist:

```shell
cat << 'EOF' > ~/Library/LaunchAgents/org.keepassxc.ssh-agent.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>org.keepassxc.ssh-agent</string>
  <key>ProgramArguments</key>
  <array>
    <string>/path/to/keepassxc-ssh-agent</string>
    <string>run</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/tmp/keepassxc-ssh-agent.out.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/keepassxc-ssh-agent.err.log</string>
</dict>
</plist>
EOF
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.keepassxc.ssh-agent.plist
```

Replace `/path/to/keepassxc-ssh-agent` with the actual path (find it with `which keepassxc-ssh-agent`).

**Linux** — create a systemd user unit:

```shell
mkdir -p ~/.config/systemd/user
cat << EOF > ~/.config/systemd/user/keepassxc-ssh-agent.service
[Unit]
Description=KeePassXC SSH Agent proxy
After=ssh-agent.service

[Service]
Type=simple
ExecStart=$(which keepassxc-ssh-agent) run
Environment=SSH_AUTH_SOCK=${SSH_AUTH_SOCK}
Restart=on-failure

[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now keepassxc-ssh-agent.service
```

The `Environment=SSH_AUTH_SOCK=...` line captures your current agent socket so the proxy can intercept it (the service otherwise runs with a clean environment).

Or start manually without an auto-start service:

```shell
keepassxc-ssh-agent run
```

## Uninstall

### Homebrew (macOS)

```shell
brew services stop keepassxc-ssh-agent
brew uninstall keepassxc-ssh-agent
rm -rf ~/.keepassxc  # Remove config (optional)
```

### pipx

```shell
keepassxc-ssh-agent uninstall -y
pipx uninstall keepassxc-ssh-agent
```

This stops and removes the auto-start service (LaunchAgent on macOS, systemd user service on Linux), restores the original SSH_AUTH_SOCK socket, and removes the config directory (`~/.keepassxc/`). Without `-y`, you'll be asked before deleting the config directory.

### Manual Uninstall

#### 1. Stop and remove the auto-start service

**macOS:**

```shell
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/org.keepassxc.ssh-agent.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/org.keepassxc.ssh-agent.plist
```

**Linux:**

```shell
systemctl --user disable --now keepassxc-ssh-agent.service 2>/dev/null
rm -f ~/.config/systemd/user/keepassxc-ssh-agent.service
systemctl --user daemon-reload
```

#### 2. Restore SSH_AUTH_SOCK

If the agent was running, it restores the original socket on shutdown automatically. If the system socket is still symlinked (e.g. after a crash), reboot or restore manually:

```shell
# Find the original socket path
SYSTEM_AGENT=$(cat ~/.keepassxc/ssh-agent.json | python3 -c 'import json,sys; print(json.load(sys.stdin).get("system_agent_path",""))')
# Remove the symlink and restore the backup
rm -f "$SYSTEM_AGENT"
mv "${SYSTEM_AGENT}.system" "$SYSTEM_AGENT"
```

#### 3. Remove config and socket

```shell
rm -rf ~/.keepassxc
```

#### 4. Uninstall the package

```shell
pipx uninstall keepassxc-ssh-agent
```

## Known Limitations

- **macOS and Linux only**: Relies on KeePassXC's browser extension Unix socket, whose path is platform-specific (`$TMPDIR/org.keepassxc.KeePassXC.BrowserServer` on macOS, `$XDG_RUNTIME_DIR/.../org.keepassxc.KeePassXC.BrowserServer` on Linux)
- **Linux auto-start**: Uses a systemd `--user` service, which requires an active user session (use `loginctl enable-linger` for headless setups)
- **DB unlocked but agent cleared**: If the database is already unlocked but `ssh-agent` keys were manually removed (`ssh-add -D`), the proxy detects empty keys and triggers "unlock", but KeePassXC reports "already unlocked" without reloading keys. Workaround: lock and re-unlock the database in KeePassXC.
- **Multiple databases**: `triggerUnlock` only works for the currently active database tab in KeePassXC.

## Development

This package depends on [`keepassxc-browser-api`](https://github.com/mietzen/keepassxc-browser-api), which handles the KeePassXC browser extension protocol. The browser API credentials are stored in `~/.keepassxc/browser-api.json` and are shared with `keepassxc-cli` if installed.

```shell
git clone https://github.com/mietzen/keepassxc-ssh-agent
git clone https://github.com/mietzen/keepassxc-browser-api
cd keepassxc-ssh-agent

python3 -m venv .venv
source .venv/bin/activate

# Install local keepassxc-browser-api dependency first
pip install ../mietzen-keepassxc-browser-api/

# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest --tb=short -q

# Run tests with coverage
pytest --cov=keepassxc_ssh_agent --cov-report=term-missing

# Lint
ruff check --ignore=E501 --exclude=__init__.py ./keepassxc_ssh_agent
```
