DEV Community

Universe Interface
Universe Interface

Posted on

unissh – a modern and secure, fully open-source SSH client with a built-in sync server

Hi everyone!

I'd like to show you unissh – a modern, simple, open source SSH client with a self-hosted zero-knowledge server for syncing your data between devices.

main app screen

Fair warning up front: the project is pure vibecode – Rust + Tauri – but my friends and I did our best to test it properly.

Let me tell you why I started building it in early June.

The first and probably main reason: I personally couldn't find a decent cross-platform SSH client with data sync between devices. For about a decade now I've had the same need – I want access to my servers from both my phone and my laptop. Carrying a MacBook everywhere just in case doesn't really appeal to me ;)

The existing options with sync support offer some combination of the following:

  1. sync via iCloud (only works within the Apple ecosystem)
  2. sync via external storage (GitHub, Google Drive, and similar)
  3. questionable design, in my opinion (and I do consider this pretty important)
  4. no encryption at all
  5. no way to export your own data without a subscription

I believe LLMs should actually produce something useful for a lot of people. So, while solving my own problem, I decided to vibecode my own SSH client, plus a server with a small web panel, purely for syncing vaults between devices.

And now, after two months of development, I'm happy to show you the results of my vibecoding:

Client features

  1. Cross-platform – Windows, macOS, Linux, iOS, Android
  2. Terminals – tab splitting in various layouts, duplicating, renaming, and the rest of the basics
  3. SFTP – upload/download files to and from servers; you can tweak the number of active SFTP connections in settings if you're moving hundreds or thousands of files; there's also a basic built-in text editor
  4. Two modes for running commands across many servers:
    1. Broadcast – opens a bunch of SSH sessions to the selected servers so you can watch the output live
    2. Fleet exec – unlike the first mode, commands are just fired at the servers and you get the exit code back from each one
  5. Secrets – SSH keys, server passwords, identities, notes; import from ~/.ssh/config is supported; secrets have version history
  6. SSH tunnels (Local, Remote, Dynamic)
  7. Session recording in asciicast v2 format
  8. Snippets – save the commands you need and run them on a server in one click
  9. Flexible UI customization: multiple themes with light/dark variants, plus the ability to build your own. Current default app themes: Mono, Nebula, Barbie (yes, a Barbie theme ;) and a few terminal themes on top
  10. Multiple sync servers at once – say, a personal server with your personal vaults, and a company/team server with your work vaults

Architecture

The shared cross-platform core (rust-core) is a workspace of nine crates that holds all the crypto, SQLCipher-backed vaults, the SSH stack (russh), a built-in in-memory SSH agent, and the sync logic. Clients talk to the core via UniFFI, and the server admin web panel goes through the very same core – just compiled to wasm. Important point: the crypto and SSH code is not rewritten per platform. The iOS client, the desktop client, and the browser admin panel all call the same code. Fewer opportunities for the slop machines to screw up ;)

The clients are Tauri v2 + React, one codebase for all five platforms (macOS, Windows, Linux, iOS, Android). The client is deliberately thin: UI, xterm.js for the terminal, and everything sensitive lives behind the FFI boundary in the core.

The server is a small Rust binary (axum + sqlx, your choice of SQLite or PostgreSQL) that you only need if you want sync between devices or team collaboration.

The main thing about the server: it knows nothing

The server is a dumb store of encrypted blobs, plus membership and versioning logic. All data is encrypted on the device before it's sent: keys are derived from a Secret Key (think Emergency Kit, like in 1Password) + your password via Argon2id. The server stores ciphertext, verifies Ed25519 signatures on records, and hands out deltas to your other devices – but it performs no cryptography over the content whatsoever. It physically cannot decrypt a vault, grant itself access, or forge a record. The worst a compromised server can do is withhold or delay your data.

The second principle: SSH traffic never goes through the sync server. Connections to your hosts always go directly from your device. The sync server only syncs encrypted vaults – if it goes down, gets lost, or gets hacked, you keep connecting to your servers like nothing happened.

I'm particularly happy with how new-device onboarding turned out: you sign in from the new device via escrow sign-in (handle + password (if set) + Secret Key), and the keyset is recovered and decrypted right on the device – it never reaches the server. It's one account across all devices, so if your teammates grant you access to a vault, it works everywhere immediately. At the same time every device has its own id, so a lost phone can be revoked individually without nuking the whole account.

Zero-knowledge does not mean "the server sees nothing." Metadata is visible by design: vault and item ids, versions, tombstones, members' public keys, roles, sync targets, blob sizes, and sync timings. What it can't see: names, contents, vault keys, item keys, private keys. In other words, a compromised server learns that you have 3 vaults and 47 items that you updated yesterday at midnight – but not what's in them.

And second: not all guarantees are cryptographic – some are server-trusted, meaning they rely on the server behaving correctly. The big one is access revocation: when you kick a member out, the immediate lockout is enforced by the server. Cryptographic revocation exists too – via vault key rotation and epoch floors on the clients – but that's the second line of defense, not the instant one.

We try to draw a clear line between these two classes of guarantees in the docs; the full breakdown lives in THREAT_MODEL.md and server/README.md in the repo.

Security

Slopus and I are not inventing our own cryptography – everything is built on a proven foundation: RustCrypto, hpke, SQLCipher, Argon2id for key derivation, Ed25519 (verify_strict) for signatures.

Vaults are zero-knowledge: all content is encrypted on the client before upload. On disk the vault sits in SQLCipher, with per-item keys wrapped under a per-vault key: compromising one item doesn't expose its neighbors, and the server only ever stores ciphertext plus service metadata, performing no crypto over the content.

Secrets don't get smeared across memory and disk: they're zeroed after use (zeroize), plaintext private keys are never written to disk by the app itself – only via explicit export from the secrets menu. Memory pages holding keys are mlock'ed where possible so they don't end up in swap. The Secret Key lives only in the system keychain / Secure Enclave. A nice small touch: configurable clipboard auto-clear after copying a password.

The secret boundary is locked down by a contract and a test: secrets leave the core only on your explicit call – show a password or a note, export a key, make an encrypted backup. That list is pinned down in one place, by a test that names every method returning secrets and verifies their type-gating live: asking a note item for its password, or exporting a "key" that's actually a password, is an error. Any new method that returns secrets must be added to that list explicitly – so the leak surface stays visible and can't quietly grow. Even snippet listing is honestly on that list, with a comment noting that saved commands routinely contain tokens and hostnames.

Host keys are pinned, classic TOFU: the host key is remembered on first connection, and if it ever changes, the connection is forcibly stopped and you get a HostKeyMismatch prompt with the option to accept the new key.

Integrity can be verified locally: the core can check signatures across all record versions, including history and tombstones, and there's check_consistency for structural database checks.

Transport is TLS 1.3 only: in-process rustls, Caddy, or your own reverse proxy.

About unsigned binaries: I honestly still haven't figured out what to do here, so releases are not signed with Apple/Microsoft certificates, and your OS will complain on first launch.

Instead of a certificate, every release ships with three verification mechanisms:

  1. a SHA256SUMS file covering all artifacts
  2. a minisign signature over it – the public key is in SECURITY.md, so checksums tampered with right on the release page can be detected
  3. SLSA provenance via GitHub attestations: gh attestation verify <file> --repo goduni/unissh proves the artifact was built by public CI from a public commit, not on my laptop

Desktop client auto-updates are verified with a separate Ed25519 key baked into the app. The key that signs "what you downloaded by hand" deliberately differs from the key that authorizes "what runs on your machine without asking" – merging those two blast radii into one would be a bad idea.

And the most basic path is always there: build everything from source and trust only your compiler.

Self-hosting the server in five minutes

Once more: the server is optional and entirely unnecessary. The client works in local mode out of the box – if you don't need sync, you don't have to run anything, and you can skip this section entirely ;)

But you and I remember what this was all about – syncing between a phone and a laptop – so let's bring it up.

The fastest way to get a server running:

curl -O https://raw.githubusercontent.com/goduni/unissh/main/compose.prod.yml
curl -o .env https://raw.githubusercontent.com/goduni/unissh/main/deploy/.env.example

# in .env, set UNISSH_DOMAIN=ssh.example.com
docker compose -f compose.prod.yml up -d
Enter fullscreen mode Exit fullscreen mode

The compose file includes Caddy – it fetches a certificate from Let's Encrypt on its own, terminates TLS, and serves the web admin panel too.

No domain, running on a LAN? Set UNISSH_TLS_DIRECTIVE="tls internal" and Caddy will issue a self-signed cert.

The default database is SQLite in a Docker volume, and for personal use that's plenty. Want something more serious – Postgres is a couple of lines in .env. Only ports 80/443 are exposed, the server runs non-root on a read-only rootfs, and metrics plus the internal port stay inside the compose network. My hope is that this deploy won't embarrass you in front of your security team (if you have one) ;)

You know those wonderful quests along the lines of "create the first admin via an env variable, then delete the variable, then change the password"? None of that here – though there is a slightly different quest ;)

The server prints a one-time setup code to its logs:

docker compose logs server 2>&1 | grep -i "setup code"
Enter fullscreen mode Exit fullscreen mode

Open the client or the admin panel, enter your server address and that code – you're now the owner of the instance. Need to invite someone into the workspace? That's done via invite links (or wire up corporate SSO via OIDC if you're doing things seriously) – they don't need the code. Automating deployment with Ansible/Terraform? The code can be pinned deterministically via UNISSH__SETUP__CODE.

To check that everything's alive: curl -k https://your-domain/healthz

Server admin panel

screenshot of unissh admin panel

The web panel is an SPA running the exact same Rust core as the clients, just compiled to wasm. Meaning all the crypto happens in your browser: you sign in with handle + password (if set) + Secret Key, the keyset is recovered and decrypted right on the page and never reaches the server. Hit the Lock button – and it's wiped from memory. No "please import this key file" nonsense (though, in fairness, I had to work for this – that flow genuinely used to exist): a new browser is confirmed via a QR code from an already trusted device, messenger-style.

Inside you get workspaces, members, devices and sessions, invites, vaults with grants, and audit logs. There's an optional ops token for break-glass scenarios, but it only unlocks infrastructure endpoints and decrypts absolutely nothing.

One thing worth highlighting about backups: if you restore the server from an old snapshot, clients will notice and refuse to sync with a TransportRollback error – until you explicitly bump the version via seq-bump. Also, an old restore can resurrect a deleted item. These aren't bugs – that's the anti-rollback protection doing its job. But it's better to learn about it from the README than during an incident ;)

In lieu of a conclusion

Two months ago I just wanted an SSH client that doesn't force me to choose between "convenient," "secure," and "not paying a subscription to access my own servers." Now I have one – and so do you, if you want it.

What can I say: LLMs have matured to the point where a couple of months of evenings is enough to build a cross-platform client, a server, and an admin panel – but you still need a human who understands, at least somewhat, the problem being solved. Above all, you need to be able to ask the uncomfortable questions, so the model can fix the problems those uncomfortable questions uncover.

The project is fully open source, dual-licensed MIT / Apache-2.0. There is no paid version, no cloud subscription, no enterprise features behind a paywall – and none are planned. This is a tool I built for myself, and you host the server yourself anyway.

I'd love for you to try the client – all five platforms are in the releases, and the server comes up in five minutes from prebuilt images or the instructions above.

Bugs, feature requests, and everything else – file issues or drop by the project's Telegram chat. Vulnerabilities – not in the issues, please; ideally to uni@goduni.me. I mean it: this project was written by an LLM, the cryptography has had no external audit, and every person who takes a careful look at the source makes it better. A special invitation goes out to those already drafting the "you can't trust vibecode with secrets" comment – here's the open code, THREAT_MODEL.md, and an email for reports.

The near-term plan is to collect your feedback – in the hope that this turns out useful to more than just me – and keep polishing it based on your wishes.

Links:

Thanks for reading! Stars on the repo, comments, and feedback are all very welcome!

Top comments (0)