Migrating SSH keys from 1Password to Bitwarden
blog ~14 m

This guide explains how to:
- Get your SSH keys out of 1Password
- Store them as SSH keys in Bitwarden
- Turn on the Bitwarden SSH agent
- Point
sshandgitat Bitwarden - Use different keys for different GitHub repos on the same host
Allow 30–45 minutes.
This is a follow‑on to Migrating from 1password to Bitwarden . That guide moved your passwords. This one moves your keys.
Prerequisites
- Bitwarden Desktop app, version 2024.8.0 or newer — the SSH agent lives in the desktop app, not the browser extension.
- 1Password still installed, with your SSH keys in it.
- An
sshclient (built in to macOS, Linux and modern Windows).
⚠️ Important ⚠️: An SSH private key is exactly as sensitive as a master password. Anyone who copies it can log in as you. Treat every export file like it is on fire.
1. Understand what moves
In 1Password an SSH key is a single item that holds a private key, a public key and a fingerprint. Bitwarden has the same item type, so a key becomes one SSH key entry in your vault.
There is no clean CSV path for SSH keys — the 1Password CSV export does not carry the private key in a form Bitwarden imports. So we move keys one at a time, by hand. If you only have two or three keys this is quick and it is the safest way.
2. Export one key from 1Password
Do this for each SSH key you own.
- Open 1Password and unlock your vault.
- Find the SSH key item.
- Click the private key field and choose Export and then Copy private key in OpenSSH format. (the exact wording depends on desktop version).
- Also note the name you used, e.g. GitHub – personal, GitHub – workco.
⚠️ Important
If you save a file rather than copying, that file holds your private key in plain text.
Do not: email it · upload it to cloud storage · leave it in Downloads.
3. Add the key to Bitwarden
- Open the Bitwarden Desktop app.
- Click + New → SSH key.
- Give it the same clear name you used in 1Password.
- Import from Clipboard or paste the private key into the private key field (wording changes depending on desktop version).
- Bitwarden fills in the public key and fingerprint for you automatically.
- Check the start and end of the fingerprint match 1Password
- Option: add a reminder of the expiry date of the key in the Notes field
- Save.
Repeat for every key. When you are done, your vault holds all your SSH keys and 1Password holds nothing you still need.
Tip: generate new keys here too
You do not have to import. In the New → SSH key dialog Bitwarden can generate a fresh Ed25519 or RSA key. This is a good moment to rotate any key that has been sitting in 1Password for years — generate a new one, add the public key to the service, then delete the old one.
4. Turn on the Bitwarden SSH agent
The SSH agent is what actually hands your keys to ssh when you connect, so you never
put a private key on disk again.
- In the Bitwarden Desktop app open Settings.
- Find SSH agent and enable Use Bitwarden as your SSH agent.
- Leave the app running — the agent only works while the desktop app is open and unlocked.
When something needs a key, Bitwarden pops up and asks you to approve the use. That approval prompt is the whole point: your key never leaves the vault unattended.
The agent listens on a socket:
macOS / Linux
| |
Windows
| |
Windows uses the standard OpenSSH named pipe, so most tools find it with no extra config.
Note: turn off the 1Password SSH agent
If you used 1Password as your SSH agent, turn it off now — otherwise two agents
fight over the same socket and ssh may keep offering keys from 1Password instead of
Bitwarden.
- Open 1Password → Settings (or Preferences).
- Go to Developer.
- Uncheck Use the SSH agent.
Then check whether 1Password left a line in ~/.ssh/config:
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
If a Host * block points at 1Password, delete it (or comment it out) — it sends
every host back to 1Password and overrides the SSH_AUTH_SOCK you set in the next step.
Also remove any SSH_AUTH_SOCK export for 1Password from your ~/.zshrc / ~/.bashrc.
Only the 1Password block goes. A Host * block is a perfectly good thing to have —
in section 5 you will add one of your own pointing at Bitwarden, and it is what makes
ssh work in processes that never read your shell config.
5. Point ssh at Bitwarden
Tell your shell to use Bitwarden’s socket by setting SSH_AUTH_SOCK.
Windows (CMD / PowerShell)
On native Windows the named pipe is the default, so git and ssh from CMD or
PowerShell usually just work — nothing to set.
Windows + WSL
WSL is the awkward one. Bitwarden runs on the Windows side and only exposes the
Windows named pipe — there is no ~/.bitwarden-ssh-agent.sock inside your Linux
distro. If you copy the macOS/Linux line above into WSL it points at a file that does
not exist, and every connection fails with Permission denied (publickey).
The fix is a small bridge: npiperelayđź”—
(a tiny Windows .exe) talks to the
pipe, and socat (in WSL) exposes it as a normal Unix socket that ssh understands.
1. Install socat inside WSL:
| |
2. Get npiperelay.exe on the Windows side. Either:
- with
Scoopđź”—
:
scoop install npiperelay, or - download
npiperelay_windows_amd64.zipfrom the npiperelay releases pageđź”— , unzip it, and dropnpiperelay.exesomewhere likeC:\Users\<you>\bin\.
3. Bridge it — add this to ~/.bashrc (or ~/.zshrc), editing the path to match
where you put the .exe:
| |
The guard looks fussy. It is fussy because the obvious version leaks relay processes, and you will not notice until you go looking:
- Ask the agent, do not test for the file. A relay that has died still leaves its
socket file on disk, so
[ -e "$SSH_AUTH_SOCK" ]says “fine” while everygitcall fails. Onlyssh-add -ltells you the truth. - Rebuild on exit 2 only.
ssh-add -lreturns0with keys,1when the agent is reachable but locked or empty, and2when it cannot be reached. Cold boot hits state1— Bitwarden is not unlocked yet — so a!test rebuilds a relay that was never broken, and every terminal you open leaks another one. rm -forphans whatever is listening. Unlinking the path does not stop the socat bound to it; it keeps running forever, holding a socket nothing can reach. So reap first, then unlink.- Take a lock. Open a VSCode window and four shells start at once, all see “no agent”, all build a relay. One wins the path, three leak.
To check for leaks on a machine that has been up a while — you want to see 1:
| |
4. Reload and check:
Windows + WSL + VSCode
Your terminal works, ssh -T git@github.com works — and then VSCode’s Source Control
panel still fails with Permission denied (publickey).
VSCode’s Git extension does not run through your shell. It spawns git from the
extension‑host process, which never sources ~/.bashrc, so it has no SSH_AUTH_SOCK
and — once you have moved your private keys off disk — no identity to fall back on
either. Everything you did above is invisible to it.
That needs two fixes, because there are two separate problems: the extension host cannot find the agent, and at login nothing has started it.
1. Let ssh find the agent without an environment variable. ssh always reads
~/.ssh/config, whoever launched it, so put the socket there. Add this at the end of
~/.ssh/config:
# Find the agent even when SSH_AUTH_SOCK is unset - the VSCode extension host,
# cron jobs and systemd units never source ~/.bashrc.
Host *
IdentityAgent ~/.ssh/bw-agent.sock
It goes at the end because ssh_config is first‑match‑wins: a Host * block at the
top would mask the specific host blocks you write in section 7.
2. Start the relay at login, not just in interactive shells. The VSCode WSL server
sources ~/.vscode-server/server-env-setup before it starts, so the extension host
inherits whatever that file sets up. Create it with the same guard you put in
~/.bashrc above — copy the block verbatim, it is safe to run twice:
Then fully restart the WSL server — reloading the window is not enough:
| |
Reopen VSCode and check that the extension host really has it:
Confirm the config half works on its own, with the variable out of the way:
| |
If Source Control still fails, turn on the extension’s own log —
Output panel → Git — and read the git fetch line it printed. It will name the
host it could not authenticate to, which is usually a host you never gave a block in
section 7.
6. Test it
⚠️ Important — do this first ⚠️
Move any old private keys out of ~/.ssh before you test. If id_ed25519,
id_rsa, a github-personal private key, etc. are still sitting on disk, ssh will
happily use those and everything will “work” — but you will have no idea whether
Bitwarden’s agent did anything or you just got lucky with a leftover file.
⚠️ Keep your *.pub files — section 7 needs them to pick the right key. Only move
the private keys (the ones with no extension). Once the agent is proven working
you can delete the private key backups.
Now, with no private keys on disk, any successful connection must be coming from Bitwarden. Try a service you use, for example GitHub:
Try a service you use, for example GitHub:
| |
Bitwarden should pop up asking you to approve the key. Approve it, and you should see your welcome message. No passphrase prompt, no key file on disk. Done.
7. Multiple keys for one host
This is the part that trips everyone up.
Say you have one key for your personal GitHub account and a separate deploy key for
a work repo — both live on github.com. When ssh connects it offers keys to the
agent in turn, and GitHub authenticates you as whichever key it recognises first.
So you push to the work repo and GitHub says “permission denied” or, worse, authenticates
you as the wrong account.
The fix is to give each key its own host alias in ~/.ssh/config and force ssh to
use only that one key.
The trick is IdentitiesOnly yes together with an IdentityFile pointing at the
public key. That public key does not unlock anything — it just tells ssh which of
the agent’s keys to offer, instead of trying them all.
# ~/.ssh/config
# Personal account
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/github-personal.pub
IdentitiesOnly yes
# Work repo deploy key
Host github-workco
HostName github.com
User git
IdentityFile ~/.ssh/github-workco.pub
IdentitiesOnly yes
# Keep this LAST - first match wins, so a Host * at the top would mask the blocks above
Host *
IdentityAgent ~/.ssh/bw-agent.sock
There is no IdentityAgent line in the per‑host blocks — the single Host * block at
the bottom covers them all. Set the path once, there and only there:
- macOS / Linux →
~/.bitwarden-ssh-agent.sock - WSL →
~/.ssh/bw-agent.sock, the bridged socket from section 5, not the macOS path - Windows CMD / PowerShell → leave
IdentityAgentout entirely, the named pipe is default
You still need the public key files on disk. Grab each one from its Bitwarden SSH key item (the public key field) and save it:
Public keys are not secret, so keeping them on disk is fine.
Now use the alias instead of github.com in your git remotes:
For a repo you already cloned, just repoint the remote:
| |
Test each alias:
Each should approve one specific key in Bitwarden and greet you as the right account.
Do this for single-key hosts too
Aliases are for two keys on one host. But a host you never gave a block to at all
gets IdentitiesOnly no and is offered every key in your vault, in turn. Ten keys in
Bitwarden and GitHub’s limit of six tries means Too many authentication failures before
it ever reaches the right one — on a host that worked fine when you had three keys.
So give every host you use a block with its own .pub and IdentitiesOnly yes, even
when it only has one key. If the host needs only one key you do not need an alias — use
the real hostname:
Host gitlab.com
HostName gitlab.com
User git
IdentityFile ~/.ssh/gitlab.pub
IdentitiesOnly yes
Check what any host will actually offer:
| |
Why not just let the agent try every key?
GitHub stops accepting keys after too many failed offers (Too many authentication failures), and deploy keys are locked to a single repo — offer a deploy key to the
wrong repo and it fails. IdentitiesOnly yes + the alias means each connection offers
exactly the right key, first time, every time.
8. Clean up 1Password
Once every key works through Bitwarden:
- Delete any exported key files from your Desktop / Downloads.
- Empty your Recycle Bin / Trash.
- Remove the plain private keys from
~/.sshif you no longer keep them on disk (the agent holds them now) — but keep the.pubfiles, your config needs them.
Keep the SSH keys in 1Password for about a week in case you find a forgotten host. After that you can delete them there and, if you like, remove 1Password.
Final check
You should now have:
- All SSH keys stored as SSH key items in Bitwarden
- The Bitwarden SSH agent enabled and approving connections
SSH_AUTH_SOCKset on macOS / Linux (the named pipe on Windows, an npiperelay + socat bridge under WSL — and the same bridge again in~/.vscode-server/server-env-setupif you use VSCode)- One
Host */IdentityAgentblock at the end of~/.ssh/config, sosshfinds the agent even whereSSH_AUTH_SOCKis unset - A
~/.ssh/configblock per host, with its.pubandIdentitiesOnly yes, so multiple keys on one host never collide and single-key hosts never run out of tries - Exactly one relay process —
pgrep -x socat | wc -lsays1, however many terminals you have opened
Your private keys now live only in your vault, guarded by an approval prompt, and follow you to every machine you sign in to.