Keep email runtime configuration out of Vault
This commit is contained in:
1 parent
9d57a65010
commit
1fe173397f
12 files changed
+84
-25
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in new issue
Block a user