📋Table of Contents
- SSH Keys Shouldn't Need Another Cloud Account
- The Problem Wasn't SSH
- Local First Means Local First
- The Vault
- And No, It Doesn't Defeat a Compromised Operating System
- Electron, But With the Sharp Objects Kept Away From the UI
- Generating Keys
- Then There Is PuTTY
- OpenSSH Interoperability Matters More Than Inventing My Own Format
- Deploying Keys Without Turning ~/.ssh/config Into a Crime Scene
- I Chose sql.js for a Boring Reason
- Bitwarden and Vaultwarden Sync, But Only If You Ask for It
- The Audit Log
- What SSHSpan Is Not
- Why I Built It
- You Can Use It, Inspect It or Break It
- My Take
SSH Keys Shouldn't Need Another Cloud Account
There is a point in managing enough servers where ~/.ssh stops being a directory and becomes archaeology.
id_ed25519.
id_ed25519_old.
customer-prod.
customer-prod-new.
server-key-final.
server-key-final2.
A PuTTY .ppk somebody created in 2017.
A key on the Windows machine that isn't on the Linux machine.
Another one sitting in a backup because you're reasonably sure it still opens something important, but you'd rather not find out by deleting it.
SSH itself is wonderfully boring.
Managing SSH keys eventually isn't.
So I built SSHSpan.
It is a local-first desktop SSH key manager for Windows and Linux that generates, imports, organizes, encrypts, exports and deploys SSH keys.
No SSHSpan account.
No mandatory cloud.
No telemetry.
No subscription.
No server somewhere that needs to remain alive so I can access the credentials I use to administer my own servers.
That last part was fairly important to me.
The Problem Wasn't SSH
SSH already solved remote administration rather well.
I wasn't interested in replacing ssh, inventing a new protocol, wrapping every connection in some proprietary tunnel or building a platform around something OpenSSH has handled perfectly well for decades.
The problem is everything around the keys.
Generate one with:
ssh-keygen -t ed25519
Done.
Beautiful.
Now do that repeatedly across years, customers, servers, environments and operating systems.
Remember which key belongs where.
Maintain the corresponding Host entries.
Move keys between machines.
Deal with OpenSSH formats on one system and PuTTY formats on another.
Work out whether that old .ppk is version 2 or 3.
Keep private keys protected.
Rotate things without accidentally locking yourself out of a machine 2,000 kilometres away.
The individual operations are simple.
The accumulated mess isn't.
I wanted one place where I could see the keys, understand what they were for, generate new ones, import the old crap I've accumulated over the years and deploy them without manually editing the same files again.
So that became SSHSpan.
Local First Means Local First
One of my first requirements was that SSHSpan must work without an internet connection. Not "offline mode." Not "offline except for analytics." Not "offline until the application checks whether your subscription still exists." Offline. By default SSHSpan makes no network requests at all. Your vault lives locally in:
~/.sshspan/sshspan.db
There is no SSHSpan backend.
There is no account database.
There is no machine identifier being sent somewhere.
There is no telemetry SDK quietly explaining that it only collects "anonymous product improvement data."
The application doesn't need to know who you are.
It manages SSH keys.
That's enough responsibility already.
This is something I think modern software has forgotten: not every application needs to become a service.
If I build a calculator tomorrow, I shouldn't need Kubernetes to keep the + button operational.
The Vault
Private keys are encrypted individually with AES-256-GCM before they are stored.
The vault master password itself is never written to disk.
SSHSpan derives the encryption material from that password using scrypt, stores verification material so it can determine whether the password is correct, and keeps the actual password only in memory while the vault is unlocked.
By default, the vault automatically locks after 15 minutes of inactivity.
Lock it manually and the in-memory password is wiped.
The SQLite database itself is not encrypted as one giant opaque file.
That distinction matters.
The private keys inside it are encrypted. Public keys, fingerprints, tags, settings and audit information are not.
I could put another encryption layer around the entire database and advertise "FULL DATABASE ENCRYPTION" in large letters.
It would look nice on a feature comparison table.
It would also obscure the actual security model.
The data that must remain secret is encrypted. Metadata is not.
If someone obtains the database, they can potentially learn that a key exists, its fingerprint, its tags and possibly what it is associated with.
They cannot simply open the database and copy the private key.
If hiding even that metadata matters in your threat model, use full-disk encryption as well.
Which you should probably be using anyway.
And No, It Doesn't Defeat a Compromised Operating System
This deserves its own section because security software has a bad habit of promising protection against attackers who already own the machine. SSHSpan does not possess magic. While the vault is unlocked, secret material necessarily exists in memory because the application has to use it. If malware is running as you, a debugger can inspect the process, your kernel is compromised, or someone has hardware-level access to the running machine, SSHSpan cannot somehow create a private universe inside your RAM. Neither can most software. The threat model is much more practical: protect private keys at rest, reduce accidental exposure, keep secret material out of the renderer, limit what the UI process can access, and avoid creating unnecessary external dependencies. Security should start with stating what a system doesn't protect against. Otherwise we're doing marketing.
Electron, But With the Sharp Objects Kept Away From the UI
Yes, SSHSpan is an Electron application.
I can already hear somebody typing.
The sensitive part of the application lives in the Electron main process. The renderer doesn't get Node.js access, doesn't get filesystem access and doesn't get private key material handed to it because a button happened to ask nicely.
nodeIntegration is disabled.
contextIsolation is enabled.
The renderer talks through a deliberately limited preload bridge.
The UI can ask the main process to perform operations.
It cannot simply rummage through the filesystem and key database itself.
There are also no remote fonts, remote JavaScript frameworks or CDN assets loaded into the application at runtime.
Again: an SSH key manager does not need seventeen websites involved in drawing its buttons.
Generating Keys
SSHSpan can generate:
- Ed25519
- RSA from 3072 to 8192 bits
- ECDSA nistp256, nistp384 and nistp521 Ed25519 is the sensible default for most new deployments. RSA is still there because reality exists. There are old systems, appliances, customers and interoperability requirements that don't disappear because somebody on Hacker News declared them aesthetically unpleasant. A tool intended to manage real infrastructure needs to deal with the infrastructure people actually have. Not the infrastructure we'd like everyone to have.
Then There Is PuTTY
Windows administrators will know where this is going.
.ppk.
PuTTY's private key format.
SSHSpan supports current PuTTY v3 keys, including encrypted files using Argon2-based key derivation, and it can export keys back to PPK v3.
Then I ran into the obvious problem.
Old keys exist.
Quite a lot of them.
PuTTY v2 used old SHA-1-based mechanisms that I would absolutely not choose for a new format today.
The easy answer would have been:
Unsupported. Convert it somewhere else. Technically clean. Operationally annoying. So SSHSpan can now import PuTTY v2 files as well, including passphrase-protected ones. But it will not create new v2 files. Import the old key and export it again, and SSHSpan writes the current v3 format. That's how I prefer handling legacy technology: understand enough of it to help people escape from it. Don't perpetuate it.
OpenSSH Interoperability Matters More Than Inventing My Own Format
SSHSpan fingerprints keys the same way ssh-keygen does.
Public keys can be exported as normal authorized_keys lines.
Private keys can be exported in OpenSSH format.
It understands OpenSSH private keys, PEM formats and PuTTY keys.
The project also has an automated test suite that cross-checks its output against the real ssh-keygen.
This is intentional.
I don't want SSHSpan to become an ecosystem.
The moment you uninstall it, your SSH keys should remain SSH keys.
You should still be able to use:
ssh
ssh-keygen
authorized_keys
~/.ssh/config
Exactly as before. Software earning a place in my infrastructure means it makes existing standards easier to operate. It does not mean I have to marry it.
Deploying Keys Without Turning ~/.ssh/config Into a Crime Scene
SSHSpan can deploy selected keys and create the corresponding SSH configuration. Managed entries go between explicit markers:
# >>> SSHSpan managed >>>
Host web-prod
HostName web01.example.com
IdentityFile ~/.sshspan/keys/<key-id>
IdentitiesOnly yes
# <<< SSHSpan managed <<<
That matters more than it looks.
I don't want an application "managing" a configuration file by taking ownership of the whole bloody thing.
You may already have aliases, ProxyJump configuration, tunnels, custom algorithms, connection multiplexing or twenty years of carefully accumulated SSH weirdness in there.
SSHSpan owns its block.
Your configuration remains yours.
Remove the managed block and you're back where you started.
That is what reversible automation looks like.
On Unix-like systems deployed private keys get mode 0600.
On Windows, SSHSpan applies restricted ACLs compatible with OpenSSH for Windows.
Nothing revolutionary.
Just the boring details that determine whether software works outside a screenshot.
I Chose sql.js for a Boring Reason
The vault uses SQLite through sql.js.
Could I have used better-sqlite3?
Of course.
It is an excellent project.
It also means native bindings.
Native bindings mean platform-specific builds, ABI compatibility and another source of packaging problems between Windows, Linux, Node and Electron versions.
SSHSpan is not a high-frequency trading engine.
It stores SSH keys.
A WebAssembly SQLite implementation is more than fast enough, avoids a native dependency and makes distribution simpler.
The database is persisted using temporary-file writes followed by an atomic rename.
Less clever.
Fewer moving parts.
I'll take it.
Bitwarden and Vaultwarden Sync, But Only If You Ask for It
Purely local storage is useful until you genuinely need the same keys on multiple machines. So SSHSpan has optional two-way synchronization with Bitwarden and Vaultwarden SSH key items. Optional is doing some work in that sentence. Nothing connects anywhere until you configure it. When enabled, the key data is encrypted client-side using the Bitwarden-compatible encryption scheme before being sent to the configured server. The server gets ciphertext. SSHSpan can push local keys, pull remote keys and match them by fingerprint. When both copies change, conflict handling is explicit. Automatic deletions are deliberately not propagated. Deleting the wrong SSH key automatically across several machines is the kind of convenience I can live without. I would much rather clean up one stale entry manually than discover that synchronization very efficiently removed my last working credential to a remote server. Technology should eliminate boring work. It should not automate catastrophes. There are limitations. Two-factor Bitwarden accounts are not currently supported by the sync implementation. Self-hosted Vaultwarden also needs to be reachable through a public hostname because the sync client deliberately rejects localhost, LAN and private/reserved destinations as part of its SSRF protection. Those constraints are documented. I'd rather have an annoying documented limitation than quietly weaken the security boundary to make a checkbox work.
The Audit Log
SSHSpan keeps an append-only operational history for things like:
- creating keys
- updating keys
- deleting keys
- locking and unlocking the vault
- deploying configuration
- synchronization activity This isn't SIEM. It doesn't need Elasticsearch. It doesn't need Grafana. It doesn't need twelve containers. It's a record of what happened to the key vault. Sometimes a table is enough.
What SSHSpan Is Not
SSHSpan is not an enterprise privileged-access-management platform.
It doesn't have approval chains.
It doesn't have ten administrator roles.
It doesn't rotate credentials across 40,000 endpoints.
It doesn't require three consultants and a two-hour onboarding call.
If you need CyberArk, SSHSpan is not going to replace CyberArk.
That's fine.
Most people do not need CyberArk to remember which Ed25519 key belongs to web03.
SSHSpan is for administrators, developers, homelab users and infrastructure people who have enough SSH keys for manual management to become irritating, but don't want the solution to become another infrastructure project.
That is a fairly deliberate boundary.
Why I Built It
A lot of my infrastructure philosophy eventually comes back to the same point: technology should earn its complexity. SSH key management had become more complicated than the actual SSH connections. The obvious solutions often introduced accounts, browser extensions, cloud dependencies, proprietary formats, subscriptions or another service that needed to remain available. I wanted something much more boring. Open application. Unlock vault. Find key. Deploy key. Close application. Come back six months later and still understand what it did. That became SSHSpan.
You Can Use It, Inspect It or Break It
SSHSpan is open source under the MIT license. The repository is here: github.com/AGSQ11/SSHSpan Windows installers, a portable Windows build, Linux AppImage and Debian packages are published through the project's GitHub Releases. The repository also contains the architecture document, security model, privacy documentation, test suite and changelog. I'm deliberately publishing the security assumptions as part of the project rather than asking people to infer them from a lock icon. If you find a bug, interoperability problem or a key format from some ancient appliance that SSHSpan manages to choke on, open an issue. Infrastructure has spent decades producing creative garbage. I'm sure there is plenty left to discover.
My Take
I don't think SSH keys need reinventing. OpenSSH is one of those pieces of infrastructure that has survived because the underlying model is simple, portable and understood practically everywhere. What needed improvement, for me, was the boring human layer around it. Where is this key? What server is it for? Is this the public or private half? Why do I have three copies? Which one is the PPK? Can I deploy it without hand-editing another config block? Can I move it to another machine without emailing the bloody thing to myself? SSHSpan is my answer to those questions. It stores the keys locally. It encrypts the private parts. It speaks the formats the existing ecosystem already understands. It can optionally sync through infrastructure you explicitly choose. And if SSHSpan disappeared tomorrow, the underlying keys would still work with OpenSSH. That's important. Good infrastructure tooling should make itself useful. It should not make itself indispensable.