Andrii Karbanovich

nidaba

  • Coming soon
  • macOS
  • Linux
  • CLI
  • ssh-agent
  • Rust

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.

Not released yet — no download

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 directoryWhat it holds
store.cborhosts, groups and keys — ciphertext only, readable by you alone
hosts.generatedaliases, hostnames, users and ports for ssh to read; no secrets
agent.sockthe ssh-agent socket that IdentityAgent points at
ctl.socka 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 dataWhat the server will hold
Aliases, hostnames, users, ports, tags, private keysciphertext only
The key that opens the storesealed separately to each of your devices, your passkey and your recovery code
Your devicestheir names and platforms, so the device list can tell them apart
The records themselveshow 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.

CommandWhat it does
init [--import]create the encrypted store and its keys, back up ~/.ssh/config and add the Include line
importbring in existing Host blocks and the keys they name; --file reads another config, --dry-run only shows what would change
add <alias> --host Hadd a host; --port, --user, --key, --group and --tag are optional
edit <alias>edit a host as TOML in $EDITOR
lslist hosts, or only a group’s or a tag’s with --group or --tag
show, rm, rendershow one host, remove one, or regenerate hosts.generated by hand
connect [alias]connect with ssh; without an alias, the picker
keyadd, ls, rm — the keys in the store
groupadd, ls, rm, set-default — groups and the defaults their hosts inherit
agentstart, stop, status, install — run the agent, or have it start with your session
statusthe 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