diff --git a/Sources/Homelab/hush-herby.md b/Sources/Homelab/hush-herby.md new file mode 100644 index 0000000..9ac1fdc --- /dev/null +++ b/Sources/Homelab/hush-herby.md @@ -0,0 +1,56 @@ +--- +created: '2026-07-09' +path: Sources/Homelab +project: hush-herby +tags: +- go +- cli +- secrets +- security +- automation +type: project-plan +--- + +A guided CLI (`hh`) that captures secrets and writes them to a chosen destination, driven by reusable **secret-free profiles**. Origin: hand-entering the FB/IG and basic-auth secrets during the wordpress-install staging deploy (Trellis #1000) — the friction of `read -s`, `$`-escaping, masked review, and consistent tagging is exactly what this productizes. + +## Goal + +`hh` captures secrets and writes them to a chosen destination, driven by a saved **profile** that is the *recipe* — field names, destination, format, per-field transforms and validators — and never the secret values. So a setup can be recalled and repeated without re-storing secrets. + +**Done when:** `hh` can create a profile, run it to securely capture then transform then write secrets to a `KEY: val` file and a `.env`, with disruptive versioning, forced-prompt-on-collision, atomic writes, masked confirm-back, and import-from-existing-file — shipped as a single static binary. + +## Locked Decisions + +- **Keystone invariant:** a profile NEVER contains secret values. The secret flows prompt then transform then destination only; it is zeroed from memory after write; never logged, never in shell history, never in the profile. +- **Stack:** Go + Cobra + charmbracelet `huh`. Single static binary so `hh` runs on the tower / ThinkPad / proxmox / NAS with no per-machine runtime. Binary `hh`, repo `hush-herby`. +- **Profiles:** `~/.config/hush-herby/profiles/*.toml` — secret-free, git-able, portable. +- **Per-field spec:** each field carries `name`, prompt label, `required`, `validate` (regex or named shape), `transform` (htpasswd-apr1 / htpasswd-bcrypt / compose-escape / base64 / url-encode / none), and `target` (write under a different key than the one prompted for). Every footgun becomes a declarative line instead of a bash incantation. +- **Destinations (v1):** a `KEY: val` reference sheet (staging.md style, primary) plus `.env` (`KEY=val`). The profile names the path plus format. +- **Versioning is disruptive, forced, and clean.** On a key collision `hh` HALTS and forces a decision — no silent append. The active/newest value takes the **bare key** (clean, copyable, consumable); the previous value is demoted to the next incremental tag `KEY_v1` / `KEY_v2` with a dated comment. The file is tidied — blanks and dupes removed — as part of the step. This directly targets the staging.md duplicate-key cruft. +- **Write mechanism:** surgical parse then apply then atomic write. Parse the existing file into an ordered model that preserves comments, key order, headings, and version history; apply the change; serialize; write to a temp file in the same directory at `0600`, fsync, then `rename()` over the original (atomic on POSIX, so a crash or Ctrl-C never leaves a half-written secrets file). Preserve mode and owner; optional `.bak` first. +- **Confirm-back masking:** show first 2 plus last 4 characters plus length, e.g. `EA...3xQz (214 chars)`; full-mask when the secret is under 12 characters so a short password is not exposed whole. +- **Output mode:** a `--stdout` mode emits the transformed value for piping — masked by default, `--reveal` to actually print — covering the use-once-do-not-persist case. +- **Import mode:** point `hh` at an existing file, infer keys, format, and path, then back-fill a profile. First-class, not deferred. +- **Scaffold:** via the project-scaffolding skill (Go Cobra) into `/opt/projects/homelab/hush-herby/`, gitea `petal-power/hush-herby`. zero-check applies since it is Go. + +## Open Items + +- Profile schema specifics: exact TOML keys for per-field specs, and how `validate` shapes are named — raw regex versus named presets like `jwt`, `eaa-token`, `numeric`. +- `.env` quoting rules for values containing spaces, quotes, or newlines. +- ansible-vault destination mechanics — decrypt via a vault-id, edit in memory, re-encrypt (P5). +- ssh-aware remote write, since staging.md lives on the control node not the tower (P5). +- Whether `hh` offers to git-commit the profile directory after changes (profiles are secret-free, so safe to track). + +## Build Phases + +- **P1 — Recipe engine.** Go/Cobra scaffold; `Profile` and `Field` models; TOML load and save; `hh new` / `hh list` / `hh edit` / `hh rm` skeletons. No secrets yet — just profiles. +- **P2 — Guided capture plus write (MVP).** `huh` masked forms; the `KEY: val` and `.env` writers with surgical parse, disruptive versioning (bare equals active, old demoted to `_vN`), forced-collision prompt, and file-clean; atomic `0600` write plus optional `.bak`; masked confirm-back (2 plus last 4). End-to-end `hh run ` to a secure write. +- **P3 — Transforms and validators.** `compose-escape` (double-dollar), htpasswd apr1 and bcrypt, shape validators (jwt / eaa / numeric / regex), base64 and url-encode; declarable per-field. The layer that beats `read -s`. +- **P4 — Import, output, polish.** Import-existing-file to profile; `--stdout` and `--reveal`; `--dry-run` masked preview of the write; solid errors, help, UX. +- **P5 (later) — Extensions.** ansible-vault destination; ssh-aware remote write; optional profile git-commit hook. + +## Notes + +- Origin and testbed: the wordpress-install staging deploy (Trellis #1000). Concrete footguns it must handle: compose `$`-truncation, apr1 htpasswd generation, JWT and EAA shape validation, duplicate-key cruft (the blank `IG_BUSINESS_ACCOUNT_ID`), and masked review. +- Sibling precedent: `sshkm` (local SSH key manager) — same small-local-homelab-CLI shape. +- Security posture IS the product: no echo, no history, no secret in the profile, atomic 0600 writes, memory zeroed. If any phase cannot hold that line, stop and reconsider rather than ship a leak. \ No newline at end of file