fix(security): constrain SOPS writes and custody argument handoff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
This commit is contained in:
tegwick 2026-10-13 21:03:24 +02:00
parent b01a30bd49
commit 8f808577d6
8 changed files with 573 additions and 44 deletions

View file

@ -53,39 +53,39 @@ hooks-test: ## Test that the pre-commit hook blocks plaintext secrets
sops-setup: ## Validate the registered public age recipient; never install a private key
@python3 sso-mfa/bootstrap/sops-private.py setup
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)
# Raw values prevent both Make-function evaluation and shell interpolation.
unexport FILE INPUT OUTPUT COMMAND OPERATOR_AGE_PUBKEY
sops-edit sops-encrypt sops-decrypt sops-rotate: export NK_SOPS_FILE := $(value FILE)
sops-edit: export NK_SOPS_INPUT := $(value INPUT)
sops-decrypt: export NK_SOPS_OUTPUT := $(value OUTPUT)
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-edit: ## Replace encrypted content from private INPUT outside Git; explicit custody required
@python3 sso-mfa/bootstrap/sops-write.py edit
sops-encrypt: ## Encrypt private, untracked FILE in place with the registered public recipient
@python3 sso-mfa/bootstrap/sops-write.py encrypt
sops-decrypt: export NK_SOPS_FILE = $(FILE)
sops-decrypt: export NK_SOPS_OUTPUT = $(OUTPUT)
sops-decrypt: ## Decrypt age-only JSON/YAML to new private OUTPUT outside Git; explicit custody required
@python3 sso-mfa/bootstrap/sops-private.py decrypt
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)"
sops-rotate: ## Rotate the SOPS data key; recipient changes require a separate reviewed procedure
@python3 sso-mfa/bootstrap/sops-write.py rotate
sops-custody-check: ## Validate a custody age key against keys/age.pub without keeping it on disk
sops-custody-check sops-custody-shell sops-custody-run: export NK_CUSTODY_RECIPIENT := $(value OPERATOR_AGE_PUBKEY)
sops-custody-run: export NK_CUSTODY_COMMAND := $(value COMMAND)
sops-custody-check: ## Validate a custody age key without keeping it on disk
@bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient "$(OPERATOR_AGE_PUBKEY)" \
--check-only
--expected-recipient "$$NK_CUSTODY_RECIPIENT" --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)"
--expected-recipient "$$NK_CUSTODY_RECIPIENT"
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"
sops-custody-run: ## Run COMMAND inside the temporary custody handoff
@[[ -n "$$NK_CUSTODY_COMMAND" ]] || (echo "Usage: make sops-custody-run COMMAND='<approved command>'"; exit 1)
@bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient "$$NK_CUSTODY_RECIPIENT" -- bash -lc "$$NK_CUSTODY_COMMAND"
check-secrets: ## Fail if any file under secrets/ is not SOPS-encrypted
@echo "Checking for unencrypted files under secrets/..."
@ -100,7 +100,7 @@ check-secrets: ## Fail if any file under secrets/ is not SOPS-encrypted
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>"; \
echo "ERROR: Unencrypted file detected. For private untracked input: make sops-encrypt FILE=<file>; tracked plaintext requires incident review."; \
exit 1; \
fi; \
echo "✔ All secrets/ files appear SOPS-encrypted"