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:
110
.githooks/pre-commit
Executable file
110
.githooks/pre-commit
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# net-kingdom pre-commit hook — blocks plaintext secrets from entering git.
|
||||
#
|
||||
# Enabled by: make hooks (sets core.hooksPath = .githooks)
|
||||
# Tested by: make hooks-test
|
||||
#
|
||||
# BLOCKS:
|
||||
# 1. Any file under secrets/ without a SOPS marker
|
||||
# 2. Any *.env file outside sso-mfa/bootstrap/ (env files contain raw secrets)
|
||||
# 3. Any staged file containing known plaintext secret patterns
|
||||
#
|
||||
# WARNS:
|
||||
# - Bundle archives (*-bundle*.tar.age) — these belong offsite, not in git
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail=0
|
||||
|
||||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||||
staged_files() {
|
||||
git diff --cached --name-only --diff-filter=ACMR
|
||||
}
|
||||
|
||||
file_content() {
|
||||
git show ":$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Check 1: secrets/ files must be SOPS-encrypted ───────────────────────────
|
||||
secrets_files=$(staged_files | grep -E '(^|/)secrets/' || true)
|
||||
|
||||
if [[ -n "$secrets_files" ]]; then
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
content="$(file_content "$f")"
|
||||
[[ -z "$content" ]] && continue
|
||||
# SOPS-encrypted files contain a top-level 'sops:' or '"sops":' key
|
||||
if echo "$content" | grep -qE '^[[:space:]]*sops:[[:space:]]*$|"sops"[[:space:]]*:'; then
|
||||
continue
|
||||
fi
|
||||
# Also allow binary age-encrypted or gpg-encrypted blobs
|
||||
case "$f" in
|
||||
*.age|*.gpg) continue ;;
|
||||
esac
|
||||
echo " [secrets/] not SOPS-encrypted: $f"
|
||||
fail=1
|
||||
done <<< "$secrets_files"
|
||||
fi
|
||||
|
||||
# ── Check 2: *.env files outside sso-mfa/bootstrap/ ─────────────────────────
|
||||
env_files=$(staged_files | grep -E '\.env$' | grep -v '^sso-mfa/bootstrap/' || true)
|
||||
|
||||
if [[ -n "$env_files" ]]; then
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
echo " [*.env outside bootstrap/] $f"
|
||||
fail=1
|
||||
done <<< "$env_files"
|
||||
fi
|
||||
|
||||
# ── Check 3: known plaintext secret patterns in any staged file ───────────────
|
||||
# These patterns appear only in generated secrets files — never in code.
|
||||
FORBIDDEN_PATTERNS=(
|
||||
'PI_SECRET_KEY='
|
||||
'PI_PEPPER='
|
||||
'LLDAP_JWT_SECRET='
|
||||
'AUTHELIA_JWT_SECRET='
|
||||
'AUTHELIA_SESSION_SECRET='
|
||||
'AUTHELIA_STORAGE_ENCRYPTION_KEY='
|
||||
'AUTHELIA_OIDC_HMAC_SECRET='
|
||||
'AUTHELIA_KEYCAPE_CLIENT_SECRET='
|
||||
'BREAKGLASS_PASSWORD='
|
||||
'PG_ROOT_PASSWORD='
|
||||
)
|
||||
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
content="$(file_content "$f")"
|
||||
[[ -z "$content" ]] && continue
|
||||
for pat in "${FORBIDDEN_PATTERNS[@]}"; do
|
||||
if echo "$content" | grep -qF "$pat"; then
|
||||
echo " [secret pattern '$pat'] $f"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
done <<< "$(staged_files)"
|
||||
|
||||
# ── Warning: bundle archives in git ──────────────────────────────────────────
|
||||
bundle_files=$(staged_files | grep -E '\-bundle.*\.tar\.age$' || true)
|
||||
|
||||
if [[ -n "$bundle_files" ]]; then
|
||||
echo ""
|
||||
echo "WARN: ops bundle archives detected — these belong offsite, not in git:"
|
||||
while IFS= read -r f; do
|
||||
echo " $f"
|
||||
done <<< "$bundle_files"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Result ────────────────────────────────────────────────────────────────────
|
||||
if [[ "$fail" -ne 0 ]]; then
|
||||
echo ""
|
||||
echo "Commit blocked: plaintext secret(s) detected (see above)."
|
||||
echo ""
|
||||
echo "To encrypt a file with SOPS: sops --encrypt --in-place <file>"
|
||||
echo "To edit an encrypted file: sops <file>"
|
||||
echo "To bypass (never in prod): git commit --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user