Files
mesh-catalog/modules/ssh-client/README.md
T
jochen add923c74a ssh-client: the mesh's region first in ~/.ssh/config, its hosts in config.d, tools in Go
The region at the end let earlier Host lines win over the mesh's (research 027/03). A
roster fact cannot be placed at the start, so the region holds one Include of config.d,
and the hosts are config.d/00-mesh, read first. Eight tools; authorized_keys and
known_hosts stay found until the controller holds those facts.
2026-10-04 12:37:44 +02:00

7.0 KiB

ssh-client

The operator account's ~/.ssh as a module (novox/hq research 027/03, ADR 0182; to-be 42 phase 1, item 9). sshd is the machine's side.

What it declares, by ADR 0182's classes

path class how
~/.ssh/ owned directory, the account's, mode 0700
~/.ssh/config.d/ owned directory, the account's, mode 0700. This is ssh's own drop-in directory: another module that needs a host (a forge, a work bastion) places its own file here
~/.ssh/config.d/00-mesh owned a Host block per other machine of the mesh (a roster fact), regenerated whenever a machine joins, leaves or is renamed. It begins with the mesh's header
~/.ssh/config written into, at the start the mesh's region, # BEGIN mesh ssh-client.config … # END …. It holds one Include ~/.ssh/config.d/*. Every line below the region is the operator's, kept byte for byte
~/.ssh/authorized_keys found see The gap
~/.ssh/known_hosts found see The gap
private keys found never read, never written. ssh_client_keys and ssh_client_check read a file's first line to recognise a key, and ask ssh-keygen about it

Why an include, not Host blocks in the region

ssh takes the first value it finds for each option. Research 027/03 asks that the mesh's hosts come before anything else in ~/.ssh/config. Before this change, the region was written at the end: a predecessor's hosts above it won, for the same machines.

A roster fact cannot be placed at the start. The controller gives facts no at, and the host leaves an existing region where it is. So the region at the start holds only the include, and the hosts are a whole file in config.d that the include reads first. 00- sorts it before any other drop-in.

ssh restores the including file's section after an Include (OpenSSH readconf.c). So an operator line just below the region is global again, as it was at the top of the file. This module's tests parse the declared region and check it, and ssh -G was compared before and after on every machine.

The old region, ssh-client.fact-ssh-config at the end of ~/.ssh/config, is no longer declared, so the host removes it, and only it, at the first push.

The gap: authorized keys and known hosts

Research 027/03 gives the mesh a region in authorized_keys (the operator's keys as the mesh records them) and one in known_hosts (every machine's host key). The controller holds neither fact.

  • A roster template sees each machine's name, mesh name, address and account, and nothing else (mesh-controller roster.go).
  • No machine fact carries an ssh host key.
  • The only key the controller knows for the operator is a sealing key for secrets, not an ssh key.

So both files stay found, and this module invents nothing.

What would close it:

  1. The host reports its machine's public host keys in its profile, and the roster gains a field for them.
  2. The operator's public keys become a mesh record: per account, with a comment saying whose and where.

Each region is then one more into: block resource here. Until then, ssh_client_check reads the files as they are, and ssh_client_revoke / ssh_client_known_host act on them by hand.

The one-off clean-up (ADR 0182: the operator removes a predecessor's output, once)

The mesh removes nothing it did not make. After the first push, a machine that had the predecessor's layout still holds:

  1. The predecessor's ~/.ssh/config.d/mesh. Its hosts repeat the mesh's, with the same values, plus one forge host that no module carries yet. Move that host to your own part of ~/.ssh/config, or to a work module's drop-in, then delete the file.
  2. The predecessor's header at the top of ~/.ssh/config, now just below the mesh's region: the comment beginning "Mesh Host blocks are GENERATED" and its Include ~/.ssh/config.d/mesh line.
  3. A predecessor's block of mesh hosts marked managed by sshd, on the machines that still have one.
  4. Backups beside the live files (config.bak-*, known_hosts.old, removed-*). ssh_client_check lists them as debris.

Until you do, ssh_client_hosts and ssh_client_check list the repeated hosts as duplicates. The mesh's still win, because they are read first.

Tools

They run as the operator account and never escalate. No answer carries a key: fingerprints only.

tool what
ssh_client_hosts r every Host and Match section in the order ssh reads it, each with file, line and source (mesh, drop-in, operator); duplicates; what is set outside any section
ssh_client_resolve r ssh -G for one host: what ssh would use, and which sections matched
ssh_client_check r modes of the directory and every file; keys with no passphrase (ssh-keygen -y -P "", output used only to compare with the .pub), weak, old, unused, or whose .pub is another key's; one key under several names; the mesh's region first with its include; duplicate hosts; known_hosts for the mesh's machines, missing or stale (scanned live, 5 s each in parallel, unless scan is false); authorized keys without a comment; debris. It says what it did not check
ssh_client_keys r every private key, by its public half: type, size, fingerprint, comment, mode, age, passphrase, whether ssh offers it by itself
ssh_client_authorized r authorized_keys by fingerprint, type, size, comment and options
ssh_client_revoke a removes every line with one fingerprint, keeping the file first as authorized_keys.revoked-<time>. It refuses the last key (a lockout) and a key in a mesh region (a push would write it back)
ssh_client_known_host r/a known entries against what the host offers now: matches, changed, not known or unreachable. With refresh, it replaces the host's entries through ssh-keygen -R (which keeps known_hosts.old) with what was scanned. That trusts whatever answers
ssh_client_test r one batch-mode connection that runs only true: the address reached, the method and key that authenticated, or why not and which keys were offered. A key with a passphrase works only through an agent. The runtime has none in its environment, so the tool looks for the account's agent socket under /run/user/<uid> and names the one it used, or says there was none

Tests

go test ./...

The tests use a temporary home and a fake runner, with public keys made for the tests only. They cover:

  • reading order and source across includes, with an operator line after the include being global again;
  • the declared region parsed as ssh reads it;
  • duplicates;
  • keys: fingerprints computed as ssh-keygen -l does, RSA size, passphrase asked and never prompted, a mismatched .pub;
  • authorized never printing a key;
  • revoke: the backup, the mode kept, a lockout refused, a mesh region refused;
  • known_host matching, changed and refreshed, and an unreachable host never refreshed;
  • test: success, and failure with the agent named;
  • check's findings;
  • that the tools served are the manifest's tools.