# Canonical paths follow ownership. Shared callers live under the receiving service. secret_moves() { cat <<'PATHS' bff services/bff/jwt caller services/notifications/callers/bff email services/notifications/email/resend nats infrastructure/nats/auth bootstrap/mongo infrastructure/mongo/bootstrap PATHS } # Archive readable version history privately before removing an obsolete KV path. archive_secret() ( old=$1 metadata=$(vault "kv/metadata/$old" GET yes) [ "$metadata" != null ] || exit 0 directory="$STATE/migrations/organized-v1/$old" mkdir -p "$directory" printf '%s' "$metadata" | write "$directory/metadata.json" printf '%s' "$metadata" | jq -r '.data.versions|to_entries[]|select(.value.destroyed==false and .value.deletion_time=="")|.key' > "$WORK/versions" while IFS= read -r version; do vault "kv/data/$old?version=$version" | write "$directory/version-$version.json" done < "$WORK/versions" ) migrate_secrets() ( secret_moves > "$WORK/moves" while read -r old new; do kv "$old" > "$WORK/old" [ "$(cat "$WORK/old")" != null ] || continue archive_secret "$old" kv "$new" > "$WORK/new" if [ "$(cat "$WORK/new")" = null ]; then # CAS=0 prevents overwriting a destination created concurrently. jq '{data:.,options:{cas:0}}' "$WORK/old" | vault "kv/data/$new" POST >/dev/null fi kv "$new" > "$WORK/new" jq -e -s '.[0]==.[1]' "$WORK/old" "$WORK/new" >/dev/null || fail "Conflicting Vault values at $old and $new; no source removed" done < "$WORK/moves" ) retire_secrets() ( # Call only after templates, policies and deployed images use canonical paths. secret_moves > "$WORK/moves" while read -r old new; do kv "$old" > "$WORK/old" [ "$(cat "$WORK/old")" != null ] || continue kv "$new" > "$WORK/new" jq -e -s '.[0]==.[1]' "$WORK/old" "$WORK/new" >/dev/null || fail "Vault migration verification failed for $old" archive_secret "$old" printf '{}' | vault "kv/metadata/$old" DELETE >/dev/null done < "$WORK/moves" for old in tls frontend-tls notifications-tls; do archive_secret "$old" if [ "$(vault "kv/metadata/$old" GET yes)" != null ]; then printf '{}' | vault "kv/metadata/$old" DELETE >/dev/null; fi done # The obsolete PKI mount is deliberately not disabled: its private signing key # 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' )