nornir-git · sshkey — design
The ONE canonical ed25519 OpenSSH deploy-key mint (LAW #5, DRY). nornir's
generate_keypairand dwarves'service::generate_deploy_keypairwere byte-for-byte twins of the samessh-key+rand_corerecipe; this lifts that single implementation into the sharednornir-gitleaf — next to [nornir_git::ssh], the transport that authenticates git-over-SSH with exactly thisid_ed25519. Pure-Rust, zero-shell (LAW #9): nossh-keygensubprocess, ever.
f0 — the whole
service install ──► ensure_deploy_key(home, uid, gid) ──► <home>/.ssh/id_ed25519 (0600)
(nornir/dwarves) │ comment = <home-basename>@<host> id_ed25519.pub (0644)
▼ dir (0700)
generate_ed25519_openssh(dir, comment) ──► chown → uid:gid
OsRng → ssh_key::PrivateKey::random returns DeployKey { pubkey }
→ .to_openssh(LF) (in-process) ──► pubkey pasted as repo deploy key
nornir_git::ssh auths with the key
Two functions, one recipe. The primitive mints a keypair into any directory; the service helper wraps it with home-dir convention + chown and hands back the public half to register.
f1 — the API (the whole surface)
generate_ed25519_openssh(dir, comment) -> Result<bool>— the primitive. Mints<dir>/id_ed25519(0600) +id_ed25519.pub(0644), creatingdirat 0700, with the given OpenSSHcomment. Idempotent: an existing private key is preserved and it returnsOk(false); a fresh mint returnsOk(true).ensure_deploy_key(home, user_uid, gid) -> Result<DeployKey>— the service-install helper. Mints<home>/.ssh/id_ed25519, chowns the.sshdir + key files touser_uid:gid, and returns the [DeployKey] (public-key line + paths +created). The comment is derived fromhome's basename, so/home/nornir→nornir@<host>and/home/dwarves→dwarves@<host>— the exact comments the former per-service twins wrote.DeployKey { pubkey, private_path, public_path, created }— the on-disk result;pubkeyis thessh-ed25519 AAAA… <user>@<host>line to paste.
f2 — the chosen design (only the chosen one; alternatives → reasoning)
- In-process
ssh-key+rand_core, neverssh-keygen. The private key isssh_key::PrivateKey::random(&mut rand_core::OsRng, Ed25519)serialised with.to_openssh(LineEnding::LF); the public half is.public_key().to_openssh(). This is the identical recipe both twins ran — a faithful hoist, no behaviour change.ssh-keyis declared explicitly (russhalready pulls it) sosshkeyuses it directly rather than reaching throughrussh::keys. - Home lives in
nornir-git, not a new crate. The deploy key IS the git SSH identity: [nornir_git::ssh] connectsgit-upload-packwith it and [nornir_git::gitio] resolves its path. Both twin-owners (nornir + dwarves via itsgit-sourcefeature) already depend onnornir-git, so asshkeymodule here is zero new dependency edges — a leaf-within-the-right-leaf, not a fork. - Idempotent-by-default. A live deploy identity is never silently overwritten; a second call is a no-op that still returns the same pubkey. If a private key exists but its
.pubwas lost,ensure_deploy_keyreconstructs the public half from the private key rather than failing. ensure_deploy_key(home, uid, gid)signature. The user identity for the comment is derived from the home basename (not a separate arg), keeping the call site to just the three things an installer already has: the home path and the freshly-ensured service user's(uid, gid).
f3 — acceptance (tests, all RED-when-broken)
- Valid ed25519 OpenSSH: the minted private key re-parses via
ssh_key::PrivateKey::from_opensshand its.algorithm()isEd25519; the public line re-parses and matches the private key's public bytes. - 0600 / 0700 / 0644: private key is owner-only
0600(a deploy key that is world-readable is a leak),.sshdir is0700, public is0644. - Deterministic shape: the public line always starts
ssh-ed25519and carries the derived<user>@<host>comment. - Idempotent: a second mint preserves the key byte-for-byte and reports
created=false;ensure_deploy_keyreturns the samepubkey. - functional_status rows (feature
testmatrix, release strips them):ensure_deploy_key_mints,deploy_key_is_valid_ed25519,deploy_key_idempotent— driven by the real mint in a wiring test that drains the process-global buffer and asserts the rows landed.
f4 — reuse map
| Concern | Reuse | New |
| ed25519 keypair + OpenSSH (de)serialise | ssh-key 0.6 (PrivateKey::random, to_openssh) |
— |
| CSPRNG | rand_core::OsRng |
— |
| functional-status emit | nornir_testmatrix::functional_status + this crate's selftest::emit bridge |
gated sshkey::wiring test |
| git SSH identity path / auth | nornir_git::{gitio,ssh} (the consumer) |
— |
| the key-gen itself | — | generate_ed25519_openssh + ensure_deploy_key |
f5 — deferred (follow-up, other agents)
- Repoint the twins (logged in
workspace_nordisk/edda_agents_problems.md): nornir'sgenerate_keypair(src/bin/nornir.rs) and dwarves'generate_deploy_keypair/ensure_dwarves_deploy_key(service.rs) should become thin wrappers overnornir_git::sshkey, then their localssh-key/rand_coredeps drop. Left to their per-repo agents (this pass does not touch nornir or dwarves). - Optional key rotation (mint-new + retire-old) is out of scope; today's contract is mint-once / preserve.