nidaba
A manager for SSH connections. Your hosts and keys live in one
encrypted store, and you connect with plain ssh, which
reaches them through the config you already have and an agent of
nidaba’s own. Syncing that store end to end encrypted between your
machines is the part still being built.
ssh config
Your config stays yours
nidaba init makes one change to ~/.ssh/config:
it adds an Include line pointing at a file nidaba owns.
The line goes first, because in an ssh config the first value found
for a setting wins — anywhere lower, an old block of yours for the
same host would quietly take precedence. Nothing you wrote is
rewritten.
Before the line goes in, the original is copied to
~/.ssh/config.nidaba-backup.<timestamp>.
Everything nidaba knows is rendered into
hosts.generated, readable only by you, with its
directives always in the same order so two renders of the same
hosts are the same bytes. Each render is checked with
ssh -G before it replaces the old file in one atomic
swap: a file ssh would refuse never becomes the file ssh reads.
The file is output, not a place to edit. Its header says so, and
nidaba edit opens the host as TOML in your
$EDITOR instead.
# Generated by nidaba. DO NOT EDIT — changes will be overwritten.
# Source of truth: nidaba store. Use `nidaba edit <alias>`.
Host bastion
HostName 10.0.0.5
User ops
IdentityAgent ~/.config/nidaba/agent.sock
IdentitiesOnly yes
Host prod-api-1
HostName 10.0.1.12
Port 2222
User deploy
IdentityAgent ~/.config/nidaba/agent.sock
IdentitiesOnly yes
ProxyJump bastion
the config directory is ~/.config/nidaba on Linux, ~/Library/Application Support/nidaba on macOSsample hosts.generated
Bringing what you already have
nidaba import — or init --import — reads
your existing Host blocks. A key named by
IdentityFile becomes a secret in the store, once, however
many hosts share it. Wildcard blocks such as Host * are
left where they are, and a key protected by a passphrase is skipped
with a hint to add it by hand. --dry-run shows what
would happen first.
Groups
A group carries defaults — a user, a port, a key — and its hosts
inherit whatever they don’t set themselves. Twenty servers that all
log in as deploy say so once.
Connecting
Plain ssh, and a picker for the names you forget
Because the hosts are in your ssh config, ssh prod-api-1
works exactly as if you had written the block yourself — and so does
everything else that reads that config. nidaba doesn’t wrap
ssh; it only writes the file ssh reads, and signs
when ssh asks for a key.
nidaba connect without an alias opens a picker: type any
part of an alias, a hostname or a tag, move with the arrows or
Ctrl-N/Ctrl-P, and Enter
replaces nidaba with ssh <alias> — an
exec, so nothing is left sitting between you and the
session. Esc leaves without connecting.
Keys
Private keys never reach the disk in the clear
Not in plaintext, and not even for a moment in /tmp.
The store holds ciphertext only; the key that opens it lives in the
operating system’s keyring — the Keychain on macOS, the Secret
Service on Linux. A private key reaches ssh the one way
that needs no file: through the ssh-agent protocol, from an agent
that nidaba runs itself.
| In the config directory | What it holds |
|---|---|
store.cbor | hosts, groups and keys — ciphertext only, readable by you alone |
hosts.generated | aliases, hostnames, users and ports for ssh to read; no secrets |
agent.sock | the ssh-agent socket that IdentityAgent points at |
ctl.sock | a separate socket the nidaba CLI uses to control the agent |
An agent that only signs
The agent answers two questions: which keys it has, and a request to
sign. Adding or removing keys through it is refused — the store is
the only place keys come from. Control commands live on the second
socket, because agent.sock is reachable by every
process you run, including everything ssh starts.
Unlocked when first asked, for fifteen minutes
A key is unlocked on its first signature request and kept unlocked,
in memory only, for fifteen minutes.
nidaba agent stop wipes it at once.
nidaba agent install writes a launchd agent or a
systemd user unit and prints the command that loads it.
Ed25519 and RSA keys. RSA signs with SHA-2 only — rsa-sha2-256
and rsa-sha2-512; a request for the old SHA-1
ssh-rsa signature is refused rather than quietly
answered. ECDSA keys are not supported yet.
Sync · planned
A server that can’t read what it keeps
None of this part is built yet. It is the design the rest was built around: every record is encrypted on your machine before it leaves, so the sync server stores what it cannot read — not the keys, and not even the aliases or hostnames.
The store on your disk already has the shape the server will keep, one encrypted record per host, group or key, which is why adding sync means adding a transport, not a second format.
| Your data | What the server will hold |
|---|---|
| Aliases, hostnames, users, ports, tags, private keys | ciphertext only |
| The key that opens the store | sealed separately to each of your devices, your passkey and your recovery code |
| Your devices | their names and platforms, so the device list can tell them apart |
| The records themselves | how many there are, whether each is a host, a group or a key, how large, and when it changed |
Signing in with a passkey
nidaba login will open a link page at nidaba.app in
your browser. You confirm with a passkey, and the new machine
receives the store’s key sealed to it alone. A device list will
show every machine
that has access, and revoking one will cut it off.
A recovery code, shown once
Registering will show a recovery code, once, and ask you to type it back before going on. It is not optional: with end-to-end encryption nobody can reset access for you, so losing the passkey and the code together would mean losing the keys.
Records are encrypted with XChaCha20-Poly1305; the store’s key is sealed to each device with X25519, and the recovery code is stretched with Argon2id.
Commands
What works today, on one machine. Every change re-renders
hosts.generated by itself; render is there
for when you want it done by hand.
| Command | What it does |
|---|---|
init [--import] | create the encrypted store and its keys, back up ~/.ssh/config and add the Include line |
import | bring in existing Host blocks and the keys they name; --file reads another config, --dry-run only shows what would change |
add <alias> --host H | add a host; --port, --user, --key, --group and --tag are optional |
edit <alias> | edit a host as TOML in $EDITOR |
ls | list hosts, or only a group’s or a tag’s with --group or --tag |
show, rm, render | show one host, remove one, or regenerate hosts.generated by hand |
connect [alias] | connect with ssh; without an alias, the picker |
key | add, ls, rm — the keys in the store |
group | add, ls, rm, set-default — groups and the defaults their hosts inherit |
agent | start, stop, status, install — run the agent, or have it start with your session |
status | the agent and the store at a glance |
--json gives machine-readable output, for scripts.
Status
Useful today on a single machine: the store, the rendered config, the agent and the picker all work.
Next
The sync server and sync between devices, sign-in with a passkey, the recovery code, and the device list with revoke. After that, a client for iOS.
Not in the first version
Windows. Sharing hosts between accounts. Syncing
known_hosts. Wildcard hosts — a
Host * block stays in your own config, where it
already works.
There is no download yet and no public repository; the plan is to publish it under the MIT licence. If you’d like to hear when that happens, a line is enough.
Tell me when it’s out