README
¶
bwsshd
Keep your ssh_config in sync with the SSH keys stored in your Bitwarden vault.
One key per host, signed from the vault, never written to disk.
bwsshd watches the Bitwarden desktop SSH agent and generates a per-host
ssh_config that pins exactly one key per host.
SSH then offers the right key first, which matters on hardened servers where
MaxAuthTries is low (3 or less) and offering a dozen agent keys gets you
disconnected before the correct one is tried.
Why this exists
The Bitwarden desktop app can act as an SSH agent: your private keys never leave the vault and signing happens in the app.
The catch is that the agent offers every key it holds, in no host-aware order.
With many keys and a hardened server, SSH hits the auth-attempt limit before
reaching the right key and the connection fails with
Permission denied (publickey).
bwsshd fixes this by reading the key list from the agent and writing one
config block per host with IdentitiesOnly yes, so SSH offers a single,
deterministic key per host.
One key, one attempt, no wasted tries.
It also works the same whether your Bitwarden desktop app is the native package, the Flatpak or the Snap, and whether your backend is Bitwarden cloud or a self-hosted Vaultwarden.
The SSH agent socket is created by the desktop client, so the backend does not change anything.
Linux and Windows. On Linux/macOS the agent is a unix socket; on Windows it
is the named pipe \\.\pipe\openssh-ssh-agent, which the Bitwarden desktop app
registers as the standard OpenSSH agent. bwsshd auto-discovers whichever applies,
and on Windows writes the generated config with the forward-slash paths and the
relative Include that the built-in OpenSSH client expects.
How it works
The name you give an SSH key item in Bitwarden becomes the key comment in the agent.
bwsshd reads that comment, extracts a target from it, and writes a block like
this:
Host pve1.example.com
HostName pve1.example.com
User debian
IdentityAgent <bitwarden-agent-socket>
IdentityFile ~/.ssh/bwsshd-keys/pve1.example.com.pub
IdentitiesOnly yes
The private key stays in the vault.
The .pub file only tells SSH which key fingerprint to request a signature for.
When you connect, Bitwarden pops up to approve the signature, the key never touches disk.
Keys whose Bitwarden name has no hostname in it (for example a shared ansible
key used on many servers) are written as .pub files and listed as comments in
the generated file.
Add a manual Host block for those in your own ~/.ssh/config, referencing the
.pub that bwsshd dropped.
Naming convention
Name your Bitwarden SSH key items with the target.
The first token shaped like [user@]host[:port] is used: the first hostname or
IP becomes the host, an optional user@ prefix sets the SSH user, and a
trailing :port sets the port (otherwise the SSH default is used).
Examples:
pve1.example.comssh key ops pve1.example.comprod pve1.example.com rootdebian@pve1.example.com(also setsUser debian)debian@pve1.example.com:2222(also setsUser debianandPort 2222)
Version-like tokens such as v1.2 are ignored, so they are never mistaken for a
hostname.
Add [nobwsshd] anywhere in the name to make bwsshd skip a key entirely.
Install
Grab a prebuilt binary from the releases
(.tar.gz for Linux, .zip for Windows), or install with Go:
go install github.com/Dxsk/bwsshd@latest
Or build from source into dist/:
make build
On Windows, run make build from Git Bash (the Makefile uses a POSIX shell);
a plain go build -o dist/bwsshd.exe . works from any shell too.
On first run, bwsshd adds an Include of the generated file to the top of your
~/.ssh/config automatically (only once), so plain ssh host picks up the
generated blocks. On Linux that line is an absolute path:
Include /home/you/.ssh/config.automatic.bw
On Windows OpenSSH resolves a relative Include against ~/.ssh, so it is
written relative instead:
Include config.automatic.bw
Usage
Run once to generate the config now:
bwsshd
Run as a background daemon that regenerates on every key change:
bwsshd -watch
Run at login (systemd user service)
make install
This builds a stripped static binary into ~/.local/bin, installs the user
service, and enables it.
Follow the logs with:
journalctl --user -u bwsshd -f
Run at login (Windows)
There is no systemd on Windows, and a classic Windows service is the wrong
tool here: it runs at boot as LocalSystem in session 0, where it would see
neither your per-session Bitwarden agent pipe nor your ~/.ssh. The correct
equivalent of the Linux systemctl --user service is a per-user Scheduled
Task at logon, which the bundled script sets up for you (no admin needed):
powershell -ExecutionPolicy Bypass -File .\packaging\windows\install.ps1
It registers a task named bwsshd that runs bwsshd -watch at logon, in your
session, hidden (no console window), and restarts it if it crashes. Logs go
to %LOCALAPPDATA%\bwsshd\bwsshd.log:
Get-Content -Wait "$env:LOCALAPPDATA\bwsshd\bwsshd.log"
Remove it with:
powershell -ExecutionPolicy Bypass -File .\packaging\windows\uninstall.ps1
The daemon polls the agent and rewrites the config only when the key set changes.
When the Bitwarden app is closed the socket is absent, bwsshd clears the generated config and removes its key directory until the app comes back.
Flags
| Flag | Default | Description |
|---|---|---|
-sock |
auto-discover | SSH agent endpoint; empty auto-discovers the Bitwarden socket (native/Flatpak/Snap on Linux) or named pipe (Windows) |
-out |
~/.ssh/config.automatic.bw |
Generated ssh_config file |
-keydir |
~/.ssh/bwsshd-keys |
Where public keys are written |
-ssh-config |
~/.ssh/config |
ssh_config to add the Include line to |
-watch |
off | Loop instead of a single run |
-interval |
10s |
Poll interval in watch mode |
Auto-discovery also honors BW_SSH_SOCK and BITWARDEN_SSH_AUTH_SOCK if set.
Safety notes
- Refuses to run as root, all files belong to your user.
- Config and keys are written
0600, the key directory is0700. - The key directory is fully managed by bwsshd via a marker file, and it refuses to touch a directory it does not own, so it can never delete your own keys.
Requirements
- Bitwarden desktop app with the SSH agent enabled
(
Settings > SSH agent > Enable SSH agent) - see enabling the SSH agent - SSH key items in your vault, named with their target host - see storing an SSH key
- An OpenSSH client. Linux/macOS ship one; on Windows it is the built-in OpenSSH client included with Windows 10/11
Contributing
Issues and pull requests are welcome.
For a bug or a behavior change, opening an issue first helps, a small reproduction goes a long way.
A few things keep the pipeline happy:
- Keep the code
gofmt-clean andgo vet-clean, CI checks both. - Add a test when you change behavior, the filesystem paths are covered with
t.TempDir()and an in-process SSH agent, follow the same style. - Use Conventional Commits for commit
messages (
fix:,feat:, ...), the changelog and version bumps are generated from them automatically.
CI builds and tests every push and pull request, and releases are cut automatically once a release PR is merged.
Thanks
Thanks to @Andralax for the careful bug reports and review that shaped the first releases.
Support
If this saved you some headaches, you can buy me a coffee.
License
MIT