Keep email runtime configuration out of Vault

This commit is contained in:
Stephan D committed 2026-09-12 23:45:58 +02:00
1 parent 9d57a65010
commit 1fe173397f
12 files changed
+84 -25

No files matched your search

+6
View File
@@ -19,6 +19,12 @@ messaging:
templates: "/etc/sxchnge/templates.json"
callers:
product: "SXCHNGE_NOTIFICATIONS_PRODUCT_TOKEN"
email:
driver: resend
settings:
api_key_env: EMAIL_API_KEY
sender_address: "" # Set your verified Resend sender here, then run make build.
sender_name: "SXCHNGE"
# Use replicas: 3 and environment: production for a three-node NATS cluster.
# mode: ops validates resources without modifying them; app provisions them.
queue:
+6 -8
View File
@@ -98,20 +98,19 @@ up() {
random | jq -Rs --arg field "$field" '{($field):rtrimstr("\n")}' | put "$path"
fi
done
printf '{"driver":"resend","api_key":"","sender_address":"notifications@localhost.test","sender_name":"SXCHNGE local"}' | ensure services/notifications/email/resend
printf '{"api_key":""}' | ensure services/notifications/email/resend
provision_mongo
provision_agents
compose up -d --wait --wait-timeout 120 nats
retire_secrets
kv services/notifications/email/resend | email_valid || fail 'Vault kv/services/notifications/email/resend needs a Resend api_key and verified sender_address. Run make email, then make up.'
normalize_email_secret
kv services/notifications/email/resend | email_valid || fail 'Vault kv/services/notifications/email/resend needs api_key. Run make email; configure the verified sender in infra/local/notifications/runtime.yml, then make build && make up.'
compose up -d --wait --wait-timeout 180
export_ca
echo 'Local stack ready: https://localhost:5126; Vault: http://127.0.0.1:18200'
}
email() {
existing
printf 'Verified Resend sender address: ' >&2
IFS= read -r address
printf 'Resend API key (hidden): ' >&2
tty_state=$(stty -g)
trap 'stty "$tty_state"; cleanup' EXIT
@@ -120,9 +119,8 @@ email() {
stty "$tty_state"
printf '\n' >&2
[ -n "$key" ] || fail 'A key is required'
case "$address" in *@*) ;; *) fail 'A sender email address is required';; esac
printf '%s' "$key" > "$WORK/email-key"
printf '%s' "$address" > "$WORK/email-address"
jq -n --rawfile key "$WORK/email-key" --rawfile address "$WORK/email-address" '{driver:"resend",api_key:$key,sender_address:$address,sender_name:"SXCHNGE"}' | put services/notifications/email/resend
echo 'Email configuration saved in Vault. Run make up.'
normalize_email_secret
jq -n --rawfile key "$WORK/email-key" '{api_key:$key}' | put services/notifications/email/resend
echo 'API key saved in Vault. Set the verified sender in infra/local/notifications/runtime.yml, then make build && make up.'
}
+1 -1
View File
@@ -57,7 +57,7 @@ ensure() (
# Never forward mongosh errors: authentication/command errors may include secrets.
mongo() { compose exec -T mongo mongosh --quiet --file /dev/stdin > "$WORK/mongo-result" 2> "$WORK/mongo-error" || return 1; cat "$WORK/mongo-result"; }
admin_auth() { kv infrastructure/mongo/bootstrap | jq -r '"const admin=db.getSiblingDB(\"admin\"); admin.auth(\"local-admin\","+(.password|tojson)+");"'; }
email_valid() { jq -e '(.api_key|type=="string") and (.api_key|test("\\S")) and (.sender_address|type=="string") and (.sender_address|test("\\S")) and .sender_address!="notifications@localhost.test"' >/dev/null; }
email_valid() { jq -e '(.api_key|type=="string") and (.api_key|test("\\S"))' >/dev/null; }
export_ca() {
compose exec -T frontend cat /data/caddy/pki/authorities/local/root.crt | write "$STATE/ca.pem" 644
}
+13
View File
@@ -55,3 +55,16 @@ retire_secrets() (
# and history are retained until the owner explicitly chooses to destroy them.
echo 'PASS: Vault secrets migrated and verified; obsolete KV entries retired'
)
# Only credentials belong in Vault; driver/sender settings are runtime YAML.
normalize_email_secret() (
path=services/notifications/email/resend
vault "kv/data/$path" GET yes > "$WORK/email-current"
[ "$(cat "$WORK/email-current")" != null ] || exit 0
jq -e '(.data.data|keys)-["api_key","driver","sender_address","sender_name"]|length==0' "$WORK/email-current" >/dev/null || fail 'Unexpected Resend secret fields; refusing to discard them'
if jq -e '.data.data|keys==["api_key"]' "$WORK/email-current" >/dev/null; then exit 0; fi
archive_secret "$path"
jq '{data:{api_key:(.data.data.api_key // "")},options:{cas:.data.metadata.version}}' "$WORK/email-current" | vault "kv/data/$path" POST >/dev/null
vault "kv/data/$path" | jq -e --slurpfile original "$WORK/email-current" '.data.data|keys==["api_key"] and .api_key==($original[0].data.data.api_key // "")' >/dev/null || fail 'Resend migration verification failed'
echo 'Resend secret now contains only api_key; configure driver/sender in notifications runtime.yml'
)
+1 -4
View File
@@ -2,9 +2,6 @@
set -eu
umask 077
if [ "$SXCHNGE_SERVICE" = notifications ]; then
printf '%s\n' "$EMAIL_CONFIG" > /run/sxchnge/email.yml
unset EMAIL_CONFIG
cat /etc/sxchnge/notifications.yml /run/sxchnge/email.yml > /run/sxchnge/config.yml
exec service -config.file=/run/sxchnge/config.yml
exec service -config.file=/etc/sxchnge/notifications.yml
fi
exec service -config.file=/etc/sxchnge/bff.yml
+15
View File
@@ -59,3 +59,18 @@ printf '{"api_key":"different-fixture"}' > "$WORK/destination"
if (migrate_secrets) >/dev/null 2>&1; then fail 'Migration overwrote conflicting destination'; fi
jq -e '.api_key=="different-fixture"' "$WORK/destination" >/dev/null
echo 'PASS: secret migration is repeatable and refuses conflicting destination values'
# Removing non-secret fields retains the key and checks the current KV version.
vault() {
if [ "${2:-GET}" = GET ]; then cat "$WORK/email-fixture"; else
jq -e '.options.cas==42 and .data=={api_key:"preserved-fixture-key"}' > /dev/null || return 1
printf '{"data":{"metadata":{"version":43},"data":{"api_key":"preserved-fixture-key"}}}' > "$WORK/email-fixture"
fi
}
printf '{"data":{"metadata":{"version":42},"data":{"api_key":"preserved-fixture-key","driver":"resend","sender_address":"sender@example.test","sender_name":"Fixture"}}}' > "$WORK/email-fixture"
normalize_email_secret
normalize_email_secret
jq -e '.data.data=={api_key:"preserved-fixture-key"}' "$WORK/email-fixture" >/dev/null
printf '{"data":{"metadata":{"version":43},"data":{"api_key":"preserved-fixture-key","unexpected":"must-not-drop"}}}' > "$WORK/email-fixture"
if (normalize_email_secret) >/dev/null 2>&1; then fail 'Unknown secret field was silently dropped'; fi
if grep -q EMAIL_CONFIG infra/local/vault/notifications.hcl infra/local/scripts/start-service.sh; then fail 'Email configuration is still rendered from Vault'; fi
echo 'PASS: email migration preserves API key, removes configuration fields and rejects unknown fields'
+1
View File
@@ -50,6 +50,7 @@ product() {
echo 'PASS: six healthy services, frontend and proxied BFF use verified TLS; no email sent'
}
layout() {
kv services/notifications/email/resend | jq -e 'keys==["api_key"]' >/dev/null || fail 'Non-secret email configuration remains in Vault'
for old in bootstrap/mongo bff caller email nats tls frontend-tls notifications-tls; do
[ "$(vault "kv/metadata/$old" GET yes)" = null ] || fail "Obsolete KV entry remains: $old"
done
-4
View File
@@ -31,10 +31,6 @@ env_template "EMAIL_API_KEY" {
error_on_missing_key = true
}
env_template "EMAIL_CONFIG" {
contents = "{{ with secret \"kv/data/services/notifications/email/resend\" }}email:\n driver: {{ .Data.data.driver | toJSON }}\n settings:\n api_key_env: EMAIL_API_KEY\n sender_address: {{ .Data.data.sender_address | toJSON }}\n sender_name: {{ .Data.data.sender_name | toJSON }}\n{{ end }}"
error_on_missing_key = true
}
exec {
command = ["/usr/local/bin/start-service"]