diff --git a/AGENTS.md b/AGENTS.md index 8fc574f..8b145d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -176,7 +176,7 @@ Keep infrastructure Dockerfiles under `infra/images/` and local Compose, service configuration and shell tooling under `infra/local/`. Keep product source under `backend/` and `frontend/`. Organize Vault KV paths by owning service under `services//`, infrastructure under `infrastructure//`, -and bootstrap administration under `bootstrap/`. Caller credentials +and bootstrap administration under `infrastructure//bootstrap`. Caller credentials belong to the receiving service at `services//callers/`. Use Caddy for local browser HTTPS and certificate management. Treat the private local Docker network as trusted; do not add internal gRPC TLS or Vault-managed diff --git a/docs/implementation.md b/docs/implementation.md index 349e31d..b7955f8 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -743,3 +743,11 @@ and 1,083 expected HTTP responses, zero failures. Role TTLs were restored and temporary BFF/frontend containers removed. `make up` intentionally stops at the missing Resend credential check; no real email, optional security scan or merge was performed. Earlier supplementary legacy test limitations remain recorded. + +Mongo bootstrap credentials now live at `kv/infrastructure/mongo/bootstrap`, +consistent with component ownership. The live `kv/bootstrap/mongo` value was +copied, compared and archived before removing the old path. Bootstrap and admin +checks read the new path; application identities are denied both paths. Shell +checks and live infrastructure validation (including Mongo transactions, lease +renewal/revocation and obsolete-path absence) passed. `make up` still stops at the +expected missing-Resend check. No backend code or product behavior changed. diff --git a/docs/local-development.md b/docs/local-development.md index 5639c9d..fe030fc 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -41,10 +41,9 @@ kv/ │ └── notifications/ │ ├── callers/bff # token; read by BFF and notifications │ └── email/resend # driver, api_key, sender_address, sender_name -├── infrastructure/ -│ └── nats/auth # token; read by NATS and notifications -└── bootstrap/ - └── mongo # local administrator and replica-set bootstrap material +└── infrastructure/ + ├── nats/auth # token; read by NATS and notifications + └── mongo/bootstrap # local administrator and replica-set bootstrap material ``` Every secret has an owning service or infrastructure component. Service policies @@ -130,7 +129,8 @@ still interrupt work. Repository consumers use `source.Source` operation scopes; ## Migration and persistence Bootstrap migrates the old flat `bff`, `caller`, `email` and `nats` paths into the -hierarchy above. It copies with CAS protection, compares values, and refuses to +hierarchy above, and moves `bootstrap/mongo` to `infrastructure/mongo/bootstrap`. +It copies with CAS protection, compares values, and refuses to overwrite conflicting destinations. After consumers use the new paths, it removes old KV metadata and obsolete `tls`, `frontend-tls`, `notifications-tls` entries. Readable old version history and metadata are archived privately in diff --git a/infra/local/scripts/bootstrap.sh b/infra/local/scripts/bootstrap.sh index 33e3585..3a5ecc7 100644 --- a/infra/local/scripts/bootstrap.sh +++ b/infra/local/scripts/bootstrap.sh @@ -28,11 +28,11 @@ mounts() { fi } provision_mongo() { - if [ "$(kv bootstrap/mongo)" = null ]; then + if [ "$(kv infrastructure/mongo/bootstrap)" = null ]; then random > "$WORK/password"; random 384 > "$WORK/keyfile"; random > "$WORK/manager" - jq -n --rawfile password "$WORK/password" --rawfile keyfile "$WORK/keyfile" --rawfile manager "$WORK/manager" '{password:($password|rtrimstr("\n")),keyfile:($keyfile|rtrimstr("\n")),manager_password:($manager|rtrimstr("\n"))}' | put bootstrap/mongo + jq -n --rawfile password "$WORK/password" --rawfile keyfile "$WORK/keyfile" --rawfile manager "$WORK/manager" '{password:($password|rtrimstr("\n")),keyfile:($keyfile|rtrimstr("\n")),manager_password:($manager|rtrimstr("\n"))}' | put infrastructure/mongo/bootstrap fi - kv bootstrap/mongo > "$WORK/mongo-secret" + kv infrastructure/mongo/bootstrap > "$WORK/mongo-secret" jq -r '.password' "$WORK/mongo-secret" | write "$STATE/mongo/password" jq -r '.keyfile' "$WORK/mongo-secret" | write "$STATE/mongo/keyfile" compose up -d mongo diff --git a/infra/local/scripts/common.sh b/infra/local/scripts/common.sh index 522bfb0..8d8c004 100644 --- a/infra/local/scripts/common.sh +++ b/infra/local/scripts/common.sh @@ -56,7 +56,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 bootstrap/mongo | jq -r '"const admin=db.getSiblingDB(\"admin\"); admin.auth(\"local-admin\","+(.password|tojson)+");"'; } +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; } export_ca() { compose exec -T frontend cat /data/caddy/pki/authorities/local/root.crt | write "$STATE/ca.pem" 644 diff --git a/infra/local/scripts/secrets.sh b/infra/local/scripts/secrets.sh index 264a1d2..d39b7cd 100644 --- a/infra/local/scripts/secrets.sh +++ b/infra/local/scripts/secrets.sh @@ -5,6 +5,7 @@ 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. diff --git a/infra/local/scripts/verify.sh b/infra/local/scripts/verify.sh index c378101..9441e1c 100644 --- a/infra/local/scripts/verify.sh +++ b/infra/local/scripts/verify.sh @@ -6,7 +6,7 @@ policies() ( directory="$STATE/agents/$service" login=$(jq -n --rawfile role "$directory/role-id" --rawfile secret "$directory/secret-id" '{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' | vault auth/approle/login POST) identity=$(printf '%s' "$login" | jq -er '.auth.client_token') - printf '%s' "$login" | jq '{token:.auth.client_token,paths:["kv/data/bootstrap/mongo","sys/init","kv/data/bff","kv/data/caller","kv/data/email","kv/data/nats","kv/data/tls"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Service can access bootstrap credentials' + printf '%s' "$login" | jq '{token:.auth.client_token,paths:["kv/data/bootstrap/mongo","kv/data/infrastructure/mongo/bootstrap","sys/init","kv/data/bff","kv/data/caller","kv/data/email","kv/data/nats","kv/data/tls"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Service can access bootstrap credentials' VAULT_TOKEN=$(printf '%s' "$login" | jq -er '.auth.client_token') printf '{}' | vault auth/token/revoke-self POST >/dev/null VAULT_TOKEN=$root_token @@ -50,10 +50,10 @@ product() { echo 'PASS: six healthy services, frontend and proxied BFF use verified TLS; no email sent' } layout() { - for old in bff caller email nats tls frontend-tls notifications-tls; do + 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 - for path in services/bff/jwt services/notifications/callers/bff services/notifications/email/resend infrastructure/nats/auth bootstrap/mongo; do + for path in services/bff/jwt services/notifications/callers/bff services/notifications/email/resend infrastructure/nats/auth infrastructure/mongo/bootstrap; do vault "kv/metadata/$path" | jq -e '.data.current_version>0' >/dev/null || fail "Missing canonical KV entry: $path" done echo 'PASS: canonical Vault hierarchy present; obsolete flat and TLS entries absent'