wiki-vault/Sources/Homelab/sshkm-local-ssh-key-manager.md
Travis Herbranson 34a268d8dc migration: copy 62 notes from pbs-projects and homelab-projects
Two-layer structure: Sources (raw notes) + Wiki (compile output)
Four domains: Dev (40), Venture (3), Homelab (23), Reference (0)
Includes CLAUDE.md spec, index pages at all levels, compile log

Co-Authored-By: Lovebug <lovebug@herbylab.dev>
2026-05-08 17:21:13 -04:00

141 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
created: '2026-05-08'
path: Sources/Homelab
project: sshkm
status: active
tags:
- python
- automation
- ansible
type: project-plan
updated: '2026-05-08'
---
# sshkm — local SSH key manager
## Goal
A small Python CLI that handles the day-to-day SSH key lifecycle (generate, register, deploy, list, remove) without the ceremony of a full CA. Solves the `~/.ssh/` graveyard problem today; designed to coexist with a future `step-ca` deployment rather than be replaced by it.
## Design principles
- **Capability/authorization split.** Generation is local-only file I/O — safe for agents to drive. Deployment requires interactive auth (target server password / agent unlock) — human-only by design.
- **Explicit state transitions.** A key is `floating`, `bound`, or `deployed`. Every transition is its own command. No silent rebinds.
- **Plain text everything.** Markdown ledger, plain `~/.ssh/config` blocks, no SQLite, no daemon. Inspect with `cat`, edit at your own risk.
- **Coexists with future CA.** This tool manages the long-lived keys that survive the cleanup; the CA project will replace human and host identities, leaving sshkm for the truly static service-key tier.
## Architecture
- **Typer + Rich** for the CLI surface
- **`ssh-keygen` / `ssh-copy-id`** as the heavy lifters (no paramiko, no shell-out reinvention)
- **`~/.ssh/managed/`** as sshkm's territory: keys, ledger, never touches anything outside
- **`~/.ssh/config`** gets `# managed-by-sshkm: <label>` blocks the tool can find and edit
- **Ledger** is markdown table at `~/.ssh/managed/ledger.md`, symlinkable into Obsidian
## Key lifecycle
```
floating ──bind──▶ bound ──push──▶ deployed
▲ │
└────rebind──────┘ (--rebind flag, v1 errors out)
```
- **floating** — keypair exists, no target declared yet (LXC isn't built yet, etc.)
- **bound** — target host/user declared, ssh config block written, key not yet on target
- **deployed** — ssh-copy-id succeeded, connection test passed
## Commands (v1)
```
sshkm generate -l <label> [-h <host> -u <user> -a <alias> -p <purpose>]
# No host/user → floating. With host/user → bound. Either way, key is created.
sshkm push <label> [-h <host> -u <user> -a <alias>]
# Floating: requires host/user, transitions to deployed.
# Bound: requires nothing, transitions to deployed.
# Deployed: errors out (use --rebind in v2).
sshkm list [--json]
# Shows label, status, alias, host, user, created, purpose.
sshkm show <label> [--json]
# Full detail including key paths and fingerprint.
sshkm remove <label> [--force]
# Local-only: removes config block, ledger row, key files.
# Warns that remote authorized_keys is untouched.
sshkm import <key-path> -l <label> [-h <host> -u <user> --move]
# Adopts existing on-disk keys. Marks as deployed (assumes already in use).
```
## Agent boundary
- **`generate`, `list`, `show`, `import`** — agent-friendly. `--json` + `--yes` flags everywhere.
- **`push`, `remove`** — human-only. Belt-and-suspenders enforcement:
- `sys.stdin.isatty()` check — refuses to run in non-interactive shells
- `SSHKM_AGENT=1` env-var bypass for legitimate scripted use, paired with logging
- Confirmation prompt by default, `--yes` only allowed when env-var set
## Phases
### Phase 1 — Core CLI
- [ ] **1.1** Project skeleton: `pyproject.toml`, Typer + Rich, `uv tool install` target
- [ ] **1.2** `models.py``ManagedKey` dataclass with `Status` enum (`floating`, `bound`, `deployed`)
- [ ] **1.3** `paths.py`, `keygen.py`, `deploy.py` — filesystem and subprocess wrappers
- [ ] **1.4** `config.py` — render/append/remove blocks marked with `# managed-by-sshkm:`
- [ ] **1.5** `ledger.py` — append/read/update rows in markdown table
### Phase 2 — Commands
- [ ] **2.1** `generate` — handle both floating and bound modes
- [ ] **2.2** `push` — state-aware: validates current status, accepts host/user only when floating
- [ ] **2.3** `list` / `show` — Rich table for humans, `--json` for machines
- [ ] **2.4** `remove` — local cleanup with clear messaging about remote leftovers
- [ ] **2.5** `import` — adopt existing keys, status defaults to `deployed`
### Phase 3 — Agent boundary
- [ ] **3.1** TTY check + env-var override pattern for `push` and `remove`
- [ ] **3.2** `--json` output on read commands and on `generate` (so agents can chain)
- [ ] **3.3** `--yes` flag, gated on env-var
- [ ] **3.4** Document the boundary in README — explicit "what agents can/can't do"
### Phase 4 — Real-world burn-in
- [ ] **4.1** Install on tower via `uv tool install`
- [ ] **4.2** Use it for the next 35 real keys you'd otherwise create manually
- [ ] **4.3** Note friction points in a session note
- [ ] **4.4** Decide what (if anything) goes into v2
## v2 candidates (not in v1)
- `rotate <label>` — generate fresh key, push, retire old one (needs remote cleanup)
- `--rebind` flag on push — explicitly allow same key on multiple hosts
- `--purge-remote` flag on remove — SSH in with the key and yank its own authorized_keys line
- Passphrase support — only matters once you're moving keys into KeePassXC's agent
- `audit` command — diff ledger against actual `~/.ssh/managed/` contents, find orphans
## Non-goals
- Replacing `ssh-agent`. KeePassXC handles that separately.
- Replacing the future CA. This tool manages static keys; the CA will issue ephemeral certs.
- Managing remote `authorized_keys` files as the source of truth. We touch the remote exactly once (push), never again.
- Cross-machine sync. The ledger is per-machine. If you want a unified view, that's a future MCP-or-script job, not this tool's concern.
## Success criteria
- `~/.ssh/` on the tower has only `config`, `known_hosts`, and `managed/` after the cleanup phase
- New key + new server flow takes one command (bound generate + push) instead of the manual dance
- Every key in `~/.ssh/managed/` has a corresponding ledger row — no orphans
- An agent driving Claude Code can prep a key for you without touching push
## Interaction with the CA project
When `step-ca` lands, this tool's role contracts:
- Human keys → migrate to OIDC-issued certs, sshkm becomes irrelevant for them
- Host keys → migrate to host certs, sshkm never managed these anyway
- Service keys with no good cert story → stay in sshkm
So the CA project doesn't deprecate sshkm, it shrinks its surface to exactly the tier where short-lived certs don't fit. Good outcome either way.