generated from coulomb/repo-seed
feat(creds): implement NK-WP-0004 Credential Management Foundation
- .sops.yaml + keys/age.pub: SOPS age encryption for all secrets/ paths - .gitignore: broad secrets/ catch-all (any depth) - .githooks/pre-commit: blocks unencrypted secrets/, *.env outside bootstrap/, and known plaintext patterns (PI_SECRET_KEY=, LLDAP_JWT_SECRET=, etc.) - Makefile: full credential lifecycle (creds-init/generate/bundle/apply/verify/ status/rotate) + SOPS helpers (sops-setup/edit/encrypt/decrypt/rotate/check-secrets) + hooks/hooks-test - creds-apply.sh: runs create-secrets.sh in dependency order (postgresql → lldap → authelia → privacyidea), skips keycape with printed instructions, updates state - creds-verify.sh: checks all K8s secrets exist, updates creds-state.yaml - creds-status.sh: human-readable state table from creds-state.yaml - creds-rotate.sh: guided rotation for all 9 secret types with impact descriptions and atomic multi-component update sequences - creds-state.yaml: committable state file tracking generation, bundle, KeePassXC confirmation, per-component apply status, enckey and pi-admin bootstrap flags NK-WP-0003-T01 unblocked. /creds-bootstrap skill registered separately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
144
Makefile
Normal file
144
Makefile
Normal file
@@ -0,0 +1,144 @@
|
||||
SHELL := /usr/bin/env bash
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# Operator age public key — used as bundle encryption recipient
|
||||
OPERATOR_AGE_PUBKEY := $(shell cat keys/age.pub 2>/dev/null | tr -d '[:space:]')
|
||||
|
||||
# ── Help ──────────────────────────────────────────────────────────────────────
|
||||
help: ## Show this help
|
||||
@echo "net-kingdom — available targets"
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) \
|
||||
| sort \
|
||||
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-28s %s\n", $$1, $$2}'
|
||||
|
||||
# ── Git hooks ─────────────────────────────────────────────────────────────────
|
||||
hooks: ## Configure git to use repo-local hooks (.githooks)
|
||||
@mkdir -p .githooks
|
||||
git config core.hooksPath .githooks
|
||||
@test -f .githooks/pre-commit \
|
||||
|| (echo "ERROR: .githooks/pre-commit not found"; exit 1)
|
||||
chmod +x .githooks/pre-commit
|
||||
@echo "✔ git hooks enabled (.githooks/pre-commit is active)"
|
||||
|
||||
hooks-test: ## Test that the pre-commit hook blocks plaintext secrets
|
||||
@mkdir -p sso-mfa/bootstrap/secrets/_hooktest
|
||||
@echo 'PI_SECRET_KEY=deadbeef' > sso-mfa/bootstrap/secrets/_hooktest/test.env
|
||||
@git add sso-mfa/bootstrap/secrets/_hooktest/test.env 2>/dev/null || true
|
||||
@if git commit -m "TEST: hook must block this" 2>/dev/null; then \
|
||||
echo "FAIL: hook did NOT block plaintext commit"; \
|
||||
git reset --soft HEAD~1 2>/dev/null || true; \
|
||||
else \
|
||||
echo "✔ Hook blocked plaintext as expected"; \
|
||||
fi
|
||||
@git restore --staged sso-mfa/bootstrap/secrets/_hooktest/test.env 2>/dev/null || true
|
||||
@rm -rf sso-mfa/bootstrap/secrets/_hooktest
|
||||
|
||||
# ── SOPS / age ────────────────────────────────────────────────────────────────
|
||||
sops-setup: ## Copy age key to SOPS default path (~/.config/sops/age/keys.txt)
|
||||
@mkdir -p ~/.config/sops/age
|
||||
@if [[ -f ~/.config/age/key.txt ]]; then \
|
||||
cp -n ~/.config/age/key.txt ~/.config/sops/age/keys.txt || true; \
|
||||
chmod 600 ~/.config/sops/age/keys.txt; \
|
||||
echo "✔ SOPS key ready at ~/.config/sops/age/keys.txt"; \
|
||||
elif [[ -f ~/.config/sops/age/keys.txt ]]; then \
|
||||
echo "✔ SOPS key already present at ~/.config/sops/age/keys.txt"; \
|
||||
else \
|
||||
echo "ERROR: age key not found at ~/.config/age/key.txt"; \
|
||||
echo " Generate one with: age-keygen -o ~/.config/age/key.txt"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
sops-edit: ## Edit an encrypted file with SOPS: make sops-edit FILE=secrets/foo.yaml
|
||||
@[[ -n "$(FILE)" ]] || (echo "Usage: make sops-edit FILE=secrets/path/to/file.yaml"; exit 1)
|
||||
sops $(FILE)
|
||||
|
||||
sops-encrypt: ## Encrypt a file in place: make sops-encrypt FILE=secrets/foo.yaml
|
||||
@[[ -n "$(FILE)" ]] || (echo "Usage: make sops-encrypt FILE=secrets/path/to/file.yaml"; exit 1)
|
||||
sops --encrypt --in-place $(FILE)
|
||||
@echo "✔ Encrypted $(FILE)"
|
||||
|
||||
sops-decrypt: ## Decrypt a file to stdout (never writes plaintext to disk): make sops-decrypt FILE=secrets/foo.yaml
|
||||
@[[ -n "$(FILE)" ]] || (echo "Usage: make sops-decrypt FILE=secrets/path/to/file.yaml"; exit 1)
|
||||
sops -d $(FILE)
|
||||
|
||||
sops-rotate: ## Rotate SOPS recipients after updating .sops.yaml: make sops-rotate FILE=secrets/foo.yaml
|
||||
@[[ -n "$(FILE)" ]] || (echo "Usage: make sops-rotate FILE=secrets/path/to/file.yaml"; exit 1)
|
||||
sops --rotate --in-place $(FILE)
|
||||
@echo "✔ Recipients rotated for $(FILE)"
|
||||
|
||||
check-secrets: ## Fail if any file under secrets/ is not SOPS-encrypted
|
||||
@echo "Checking for unencrypted files under secrets/..."
|
||||
@bad=0; \
|
||||
while IFS= read -r f; do \
|
||||
[[ -z "$$f" ]] && continue; \
|
||||
if grep -qE '^[[:space:]]*sops:[[:space:]]*$$|"sops"[[:space:]]*:' "$$f" 2>/dev/null; then \
|
||||
continue; \
|
||||
fi; \
|
||||
case "$$f" in *.age|*.gpg) continue ;; esac; \
|
||||
echo " UNENCRYPTED: $$f"; bad=1; \
|
||||
done < <(git ls-files --others --cached 'secrets' 2>/dev/null | grep -v '/$'); \
|
||||
if [[ "$$bad" -ne 0 ]]; then \
|
||||
echo ""; \
|
||||
echo "ERROR: Unencrypted secret(s) detected. Encrypt with: sops --encrypt --in-place <file>"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "✔ All secrets/ files appear SOPS-encrypted"
|
||||
|
||||
# ── Credential lifecycle ──────────────────────────────────────────────────────
|
||||
creds-init: sops-setup hooks ## One-time setup: verify prerequisites, configure SOPS and git hooks
|
||||
@echo "=== creds-init: checking prerequisites ==="
|
||||
@command -v age >/dev/null 2>&1 || (echo "ERROR: age not installed (apt install age)"; exit 1)
|
||||
@command -v sops >/dev/null 2>&1 || (echo "ERROR: sops not installed (see https://github.com/getsops/sops/releases)"; exit 1)
|
||||
@command -v kubectl >/dev/null 2>&1 \
|
||||
&& echo "✔ kubectl found" \
|
||||
|| echo "WARN: kubectl not found (required for creds-apply/verify)"
|
||||
@test -f ~/.config/sops/age/keys.txt \
|
||||
|| (echo "ERROR: age key not found — run 'make sops-setup' after placing your key at ~/.config/age/key.txt"; exit 1)
|
||||
@echo ""
|
||||
@echo "✔ creds-init complete. Next: make creds-generate"
|
||||
|
||||
creds-generate: ## Generate all service secrets and print KeePassXC entry guide
|
||||
@command -v openssl >/dev/null 2>&1 || (echo "ERROR: openssl not found"; exit 1)
|
||||
cd sso-mfa/bootstrap && bash gen-secrets.sh
|
||||
@sed -i "s|^generated_at: .*|generated_at: $$(date -Iseconds)|" sso-mfa/bootstrap/creds-state.yaml
|
||||
@echo ""
|
||||
@echo "State: generated_at updated in sso-mfa/bootstrap/creds-state.yaml"
|
||||
@echo "Next : enter values into KeePassXC, then set keepass_confirmed: true"
|
||||
@echo " Run 'make creds-bundle' to create an encrypted offsite backup."
|
||||
|
||||
creds-bundle: ## Age-encrypt the ops bundle for offsite storage
|
||||
@[[ -n "$(OPERATOR_AGE_PUBKEY)" ]] \
|
||||
|| (echo "ERROR: keys/age.pub not found — cannot determine encryption recipient"; exit 1)
|
||||
@[[ -d sso-mfa/bootstrap/secrets ]] \
|
||||
|| (echo "ERROR: sso-mfa/bootstrap/secrets/ not found — run 'make creds-generate' first"; exit 1)
|
||||
cd sso-mfa/bootstrap && bash pack-bundle.sh secrets "$(OPERATOR_AGE_PUBKEY)"
|
||||
@sed -i "s|^bundle_at: .*|bundle_at: $$(date -Iseconds)|" sso-mfa/bootstrap/creds-state.yaml
|
||||
@echo ""
|
||||
@echo "State: bundle_at updated in sso-mfa/bootstrap/creds-state.yaml"
|
||||
@echo "Store the .tar.age bundle offsite (external drive, encrypted cloud, second location)."
|
||||
|
||||
creds-apply: ## Apply all create-secrets.sh scripts to the cluster in dependency order
|
||||
bash sso-mfa/bootstrap/creds-apply.sh
|
||||
|
||||
creds-verify: ## Check all expected K8s secrets exist and update creds-state.yaml
|
||||
bash sso-mfa/bootstrap/creds-verify.sh
|
||||
|
||||
creds-status: ## Print human-readable credential state from creds-state.yaml
|
||||
bash sso-mfa/bootstrap/creds-status.sh
|
||||
|
||||
creds-rotate: ## Guided rotation for one secret: make creds-rotate SECRET=<name>
|
||||
@[[ -n "$(SECRET)" ]] \
|
||||
|| (echo "Usage: make creds-rotate SECRET=<name>"; \
|
||||
echo ""; \
|
||||
echo "Known secrets:"; \
|
||||
echo " PI_SECRET_KEY PI_PEPPER PI_DB_PASSWORD LLDAP_JWT_SECRET"; \
|
||||
echo " LLDAP_LDAP_USER_PASS AUTHELIA_SESSION_SECRET"; \
|
||||
echo " AUTHELIA_KEYCAPE_CLIENT_SECRET KEYCAPE_RSA_KEY"; \
|
||||
echo " BREAKGLASS_PASSWORD"; \
|
||||
exit 1)
|
||||
SECRET=$(SECRET) bash sso-mfa/bootstrap/creds-rotate.sh
|
||||
|
||||
.PHONY: help hooks hooks-test sops-setup sops-edit sops-encrypt sops-decrypt sops-rotate \
|
||||
check-secrets creds-init creds-generate creds-bundle creds-apply creds-verify \
|
||||
creds-status creds-rotate
|
||||
Reference in New Issue
Block a user