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

6.5 KiB
Raw Permalink Blame History

created path project status tags type updated
2026-05-08 Sources/Homelab sshkm active
python
automation
ansible
project-plan 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.pyManagedKey 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.