# 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-