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>
141 lines
6.5 KiB
Markdown
141 lines
6.5 KiB
Markdown
---
|
||
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 3–5 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. |