generated from coulomb/repo-seed
Clarifications on sops
This commit is contained in:
17
Makefile
17
Makefile
@@ -49,7 +49,7 @@ hooks-test: ## Test that the pre-commit hook blocks plaintext secrets
|
|||||||
@rm -rf sso-mfa/bootstrap/secrets/_hooktest
|
@rm -rf sso-mfa/bootstrap/secrets/_hooktest
|
||||||
|
|
||||||
# ── SOPS / age ────────────────────────────────────────────────────────────────
|
# ── SOPS / age ────────────────────────────────────────────────────────────────
|
||||||
sops-setup: ## Copy age key to SOPS default path (~/.config/sops/age/keys.txt)
|
sops-setup: ## Legacy dev-local setup: copy age key to SOPS default path
|
||||||
@mkdir -p ~/.config/sops/age
|
@mkdir -p ~/.config/sops/age
|
||||||
@if [[ -f ~/.config/age/key.txt ]]; then \
|
@if [[ -f ~/.config/age/key.txt ]]; then \
|
||||||
cp -n ~/.config/age/key.txt ~/.config/sops/age/keys.txt || true; \
|
cp -n ~/.config/age/key.txt ~/.config/sops/age/keys.txt || true; \
|
||||||
@@ -81,6 +81,21 @@ sops-rotate: ## Rotate SOPS recipients after updating .sops.yaml: make sops-rota
|
|||||||
sops --rotate --in-place $(FILE)
|
sops --rotate --in-place $(FILE)
|
||||||
@echo "✔ Recipients rotated for $(FILE)"
|
@echo "✔ Recipients rotated for $(FILE)"
|
||||||
|
|
||||||
|
sops-custody-check: ## Validate a custody age key against keys/age.pub without keeping it on disk
|
||||||
|
@bash sso-mfa/bootstrap/sops-custody-unlock.sh \
|
||||||
|
--expected-recipient "$(OPERATOR_AGE_PUBKEY)" \
|
||||||
|
--check-only
|
||||||
|
|
||||||
|
sops-custody-shell: ## Open a shell with a temporary SOPS_AGE_KEY_FILE from custody material
|
||||||
|
@bash sso-mfa/bootstrap/sops-custody-unlock.sh \
|
||||||
|
--expected-recipient "$(OPERATOR_AGE_PUBKEY)"
|
||||||
|
|
||||||
|
sops-custody-run: ## Run COMMAND with a temporary custody age key: make sops-custody-run COMMAND='make -C ../inter-hub recovery-drill'
|
||||||
|
@[[ -n "$(COMMAND)" ]] || (echo "Usage: make sops-custody-run COMMAND='make -C ../inter-hub recovery-drill'"; exit 1)
|
||||||
|
@COMMAND='$(COMMAND)' bash sso-mfa/bootstrap/sops-custody-unlock.sh \
|
||||||
|
--expected-recipient "$(OPERATOR_AGE_PUBKEY)" \
|
||||||
|
-- bash -lc "$$COMMAND"
|
||||||
|
|
||||||
check-secrets: ## Fail if any file under secrets/ is not SOPS-encrypted
|
check-secrets: ## Fail if any file under secrets/ is not SOPS-encrypted
|
||||||
@echo "Checking for unencrypted files under secrets/..."
|
@echo "Checking for unencrypted files under secrets/..."
|
||||||
@bad=0; \
|
@bad=0; \
|
||||||
|
|||||||
@@ -70,3 +70,63 @@ directly. Encryption does not require the private key.
|
|||||||
`sso-mfa/bootstrap/decrypt-secrets.sh` requires the private key path and should
|
`sso-mfa/bootstrap/decrypt-secrets.sh` requires the private key path and should
|
||||||
be used only in an explicit unlock/apply ceremony. After apply, plaintext files
|
be used only in an explicit unlock/apply ceremony. After apply, plaintext files
|
||||||
must be shredded.
|
must be shredded.
|
||||||
|
|
||||||
|
## Custody Unlock Helper
|
||||||
|
|
||||||
|
Use `sso-mfa/bootstrap/sops-custody-unlock.sh` when an operator needs to run a
|
||||||
|
SOPS-backed drill, recovery, or lockdown command but the private key is only in
|
||||||
|
the password safe or offline custody packet.
|
||||||
|
|
||||||
|
The helper:
|
||||||
|
|
||||||
|
- reads the private age key from a hidden prompt, stdin, or a source file;
|
||||||
|
- derives the public age recipient with `age-keygen -y`;
|
||||||
|
- refuses to continue unless it matches the expected recipient;
|
||||||
|
- writes a `0600` temporary `SOPS_AGE_KEY_FILE`;
|
||||||
|
- runs the requested command or opens a temporary custody shell; and
|
||||||
|
- shreds/removes the temporary key file when the command or shell exits.
|
||||||
|
|
||||||
|
It must not be used to install the private key permanently on a workstation or
|
||||||
|
server. The source of truth remains the password safe/offline custody packet.
|
||||||
|
|
||||||
|
### Validate Custody Material
|
||||||
|
|
||||||
|
From the NetKingdom repo, validate the supplied private key against `keys/age.pub`
|
||||||
|
without keeping it on disk:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make sops-custody-check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run A One-Shot Recovery Drill
|
||||||
|
|
||||||
|
For inter-hub:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make sops-custody-run COMMAND='make -C /home/worsch/inter-hub recovery-drill'
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper prompts for the `AGE-SECRET-KEY-1...` line from the password safe or
|
||||||
|
offline custody packet, validates it against the registered public recipient,
|
||||||
|
sets `SOPS_AGE_KEY_FILE` for the command, and removes the temporary key after
|
||||||
|
the drill exits.
|
||||||
|
|
||||||
|
If using a password-manager CLI, pipe only the private key field:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
op read 'op://Platform/NetKingdom custodian age key/private-key' \
|
||||||
|
| sso-mfa/bootstrap/sops-custody-unlock.sh \
|
||||||
|
--from-stdin \
|
||||||
|
-- make -C /home/worsch/inter-hub recovery-drill
|
||||||
|
```
|
||||||
|
|
||||||
|
### Open An Incident Shell
|
||||||
|
|
||||||
|
For multi-step recovery or lockdown work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make sops-custody-shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the required SOPS, recovery, or lockdown commands inside that shell. Exit
|
||||||
|
the shell to shred the temporary key file.
|
||||||
|
|||||||
@@ -115,3 +115,24 @@ See `../vault/` (created in T01 Phase 0b) for:
|
|||||||
- ESO (External Secrets Operator) configuration
|
- ESO (External Secrets Operator) configuration
|
||||||
- Vault secret path layout
|
- Vault secret path layout
|
||||||
- Migration procedure: KeePassXC → Vault
|
- Migration procedure: KeePassXC → Vault
|
||||||
|
|
||||||
|
## Custody Age Key Unlock
|
||||||
|
|
||||||
|
For SOPS-backed drills and incident work, do not copy the custodian age private
|
||||||
|
key into a permanent workstation path. Use the custody helper from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make sops-custody-run COMMAND='make -C /home/worsch/inter-hub recovery-drill'
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper prompts for the `AGE-SECRET-KEY-1...` line from the password safe or
|
||||||
|
offline custody packet, validates it against `keys/age.pub`, writes a temporary
|
||||||
|
`SOPS_AGE_KEY_FILE`, runs the command, and removes the temporary file on exit.
|
||||||
|
|
||||||
|
For multi-step recovery work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make sops-custody-shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit the shell when finished so the temporary key file is removed.
|
||||||
|
|||||||
187
sso-mfa/bootstrap/sops-custody-unlock.sh
Executable file
187
sso-mfa/bootstrap/sops-custody-unlock.sh
Executable file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sops-custody-unlock.sh -- run SOPS/age operations with a temporary custody key.
|
||||||
|
#
|
||||||
|
# The private age key is supplied only for this invocation, validated against an
|
||||||
|
# expected public recipient, written to a 0600 temp file, and removed on exit.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
EXPECTED_RECIPIENT="${EXPECTED_AGE_RECIPIENT:-}"
|
||||||
|
SOURCE_FILE=""
|
||||||
|
INPUT_MODE=""
|
||||||
|
CHECK_ONLY=false
|
||||||
|
COMMAND=()
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
sops-custody-unlock.sh [options] [-- command ...]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--expected-recipient age1... Required recipient. Defaults to keys/age.pub.
|
||||||
|
--from-file PATH Read custody age key material from PATH.
|
||||||
|
--from-stdin Read custody age key material from stdin.
|
||||||
|
--prompt Prompt for one AGE-SECRET-KEY-1... line.
|
||||||
|
--check-only Validate the supplied key and exit.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Run an inter-hub recovery drill after pasting the private key line.
|
||||||
|
sso-mfa/bootstrap/sops-custody-unlock.sh \
|
||||||
|
--expected-recipient age1aq8twfd78wvpra0had8cezcnj96tj4q0068edrz5jez8d6xwmflqdepsh4 \
|
||||||
|
-- make -C /home/worsch/inter-hub recovery-drill
|
||||||
|
|
||||||
|
# Pipe a password-manager field into the helper without printing it.
|
||||||
|
op read 'op://Platform/NetKingdom custodian age key/private-key' \
|
||||||
|
| sso-mfa/bootstrap/sops-custody-unlock.sh --from-stdin -- make recovery-drill
|
||||||
|
|
||||||
|
If no command is supplied, an interactive shell is opened with SOPS_AGE_KEY_FILE
|
||||||
|
set. Exit that shell to remove the temporary key file.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'ERROR: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--expected-recipient)
|
||||||
|
[[ $# -ge 2 ]] || fail "--expected-recipient requires a value"
|
||||||
|
EXPECTED_RECIPIENT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--expected-recipient=*)
|
||||||
|
EXPECTED_RECIPIENT="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--from-file)
|
||||||
|
[[ $# -ge 2 ]] || fail "--from-file requires a path"
|
||||||
|
SOURCE_FILE="$2"
|
||||||
|
INPUT_MODE="file"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--from-file=*)
|
||||||
|
SOURCE_FILE="${1#*=}"
|
||||||
|
INPUT_MODE="file"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--from-stdin)
|
||||||
|
INPUT_MODE="stdin"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--prompt)
|
||||||
|
INPUT_MODE="prompt"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--check-only)
|
||||||
|
CHECK_ONLY=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
COMMAND=("$@")
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$EXPECTED_RECIPIENT" && -f "$REPO_ROOT/keys/age.pub" ]]; then
|
||||||
|
EXPECTED_RECIPIENT="$(grep -m1 '^age1' "$REPO_ROOT/keys/age.pub" || true)"
|
||||||
|
fi
|
||||||
|
[[ "$EXPECTED_RECIPIENT" == age1* ]] || fail "expected age recipient not set; pass --expected-recipient age1..."
|
||||||
|
|
||||||
|
command -v age-keygen >/dev/null 2>&1 || fail "age-keygen not found; install age before running custody unlock"
|
||||||
|
|
||||||
|
if [[ -z "$INPUT_MODE" ]]; then
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
INPUT_MODE="prompt"
|
||||||
|
else
|
||||||
|
INPUT_MODE="stdin"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/netkingdom-sops-age.XXXXXX")"
|
||||||
|
KEY_FILE="$TMP_DIR/keys.txt"
|
||||||
|
cleanup() {
|
||||||
|
if [[ -f "$KEY_FILE" ]]; then
|
||||||
|
if command -v shred >/dev/null 2>&1; then
|
||||||
|
shred -u "$KEY_FILE" 2>/dev/null || rm -f "$KEY_FILE"
|
||||||
|
else
|
||||||
|
rm -f "$KEY_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rmdir "$TMP_DIR" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
case "$INPUT_MODE" in
|
||||||
|
file)
|
||||||
|
[[ -f "$SOURCE_FILE" ]] || fail "custody age key file not found: $SOURCE_FILE"
|
||||||
|
cp "$SOURCE_FILE" "$KEY_FILE"
|
||||||
|
chmod 600 "$KEY_FILE"
|
||||||
|
;;
|
||||||
|
stdin)
|
||||||
|
cat > "$KEY_FILE"
|
||||||
|
chmod 600 "$KEY_FILE"
|
||||||
|
;;
|
||||||
|
prompt)
|
||||||
|
[[ -t 0 ]] || fail "cannot prompt for key because stdin is not a terminal; use --from-stdin or --from-file"
|
||||||
|
printf 'Paste custody age private key line (input hidden): ' >&2
|
||||||
|
IFS= read -r -s key_line
|
||||||
|
printf '\n' >&2
|
||||||
|
[[ -n "$key_line" ]] || fail "no key material supplied"
|
||||||
|
printf '%s\n' "$key_line" > "$KEY_FILE"
|
||||||
|
unset key_line
|
||||||
|
chmod 600 "$KEY_FILE"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
fail "unsupported input mode: $INPUT_MODE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ -s "$KEY_FILE" ]] || fail "no key material supplied"
|
||||||
|
if ! grep -q 'AGE-SECRET-KEY-1' "$KEY_FILE"; then
|
||||||
|
fail "supplied material does not contain an AGE-SECRET-KEY-1 private key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
DERIVED_RECIPIENT="$(age-keygen -y "$KEY_FILE" 2>/dev/null || true)"
|
||||||
|
[[ "$DERIVED_RECIPIENT" == age1* ]] || fail "could not derive an age public recipient from supplied key"
|
||||||
|
|
||||||
|
if [[ "$DERIVED_RECIPIENT" != "$EXPECTED_RECIPIENT" ]]; then
|
||||||
|
printf 'ERROR: custody key does not match expected recipient\n' >&2
|
||||||
|
printf ' expected: %s\n' "$EXPECTED_RECIPIENT" >&2
|
||||||
|
printf ' derived : %s\n' "$DERIVED_RECIPIENT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'OK: custody age key matches expected recipient %s\n' "$EXPECTED_RECIPIENT" >&2
|
||||||
|
|
||||||
|
if [[ "$CHECK_ONLY" == true ]]; then
|
||||||
|
printf 'OK: check complete; temporary key file removed on exit\n' >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
export SOPS_AGE_KEY_FILE="$KEY_FILE"
|
||||||
|
export NETKINGDOM_CUSTODY_AGE_KEY_FILE="$KEY_FILE"
|
||||||
|
|
||||||
|
if [[ ${#COMMAND[@]} -gt 0 ]]; then
|
||||||
|
printf 'Running command with temporary SOPS_AGE_KEY_FILE. Key file will be removed after command exit.\n' >&2
|
||||||
|
"${COMMAND[@]}"
|
||||||
|
else
|
||||||
|
printf 'Opening custody shell with temporary SOPS_AGE_KEY_FILE=%s\n' "$SOPS_AGE_KEY_FILE" >&2
|
||||||
|
printf 'Run recovery or lockdown commands, then exit the shell to remove the temp key.\n' >&2
|
||||||
|
bash -i
|
||||||
|
fi
|
||||||
36
workplans/ADHOC-2026-06-14.md
Normal file
36
workplans/ADHOC-2026-06-14.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
id: ADHOC-2026-06-14
|
||||||
|
type: workplan
|
||||||
|
title: "Ad hoc NetKingdom operator usability fixes"
|
||||||
|
domain: netkingdom
|
||||||
|
repo: net-kingdom
|
||||||
|
status: finished
|
||||||
|
owner: codex
|
||||||
|
topic_slug: netkingdom
|
||||||
|
created: "2026-06-14"
|
||||||
|
updated: "2026-06-14"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ad hoc NetKingdom operator usability fixes
|
||||||
|
|
||||||
|
## SOPS Custody Unlock Helper
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: ADHOC-2026-06-14-T01
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Added a custody unlock helper for SOPS/age operations so drills and incident
|
||||||
|
commands can use the password-safe/offline custody age private key without
|
||||||
|
installing it permanently on a workstation.
|
||||||
|
|
||||||
|
The helper validates the supplied private key against the expected public age
|
||||||
|
recipient, writes a temporary `0600` `SOPS_AGE_KEY_FILE`, runs the requested
|
||||||
|
command or opens an incident shell, and removes the temporary key on exit.
|
||||||
|
|
||||||
|
Documented the inter-hub recovery-drill path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make sops-custody-run COMMAND='make -C /home/worsch/inter-hub recovery-drill'
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user