Organize local infrastructure and Vault secrets by service ownership
This commit is contained in:
1 parent
1b8800ee73
commit
9dbf0bd80c
44 files changed
+496
-359
No files matched your search
+114
-170
@@ -3,219 +3,163 @@
|
||||
Task [#42](https://git.sendico.io/tech/sxchnge/issues/42),
|
||||
[PR #43](https://git.sendico.io/tech/sxchnge/pulls/43), branch `dev-env-42`.
|
||||
|
||||
## Build and bootstrap
|
||||
## Build and start
|
||||
|
||||
Requires Make, a POSIX shell and a local Docker engine with Compose v2 and
|
||||
BuildKit. Docker builds Go, protobuf bindings and Flutter web; host Go, protoc
|
||||
and Flutter installations are not required for `make build && make up`. The
|
||||
tooling container includes Docker CLI, curl, jq, Git and OpenSSL. No host Python,
|
||||
jq, curl or Git installation is needed for these Make targets. It mounts
|
||||
the local Docker socket and workspace to orchestrate Compose; a remote Docker
|
||||
daemon is not supported. The first Flutter SDK download can
|
||||
take several minutes. Both ARM64 and AMD64 build from the same source definitions.
|
||||
BuildKit. No host Python, Go, Flutter, jq or curl is required. Make runs bootstrap
|
||||
tooling in Docker with the local Docker socket and workspace mounted. Remote
|
||||
Docker engines are unsupported.
|
||||
|
||||
```sh
|
||||
make build && make up
|
||||
```
|
||||
|
||||
`make build` resolves the current stable upstream releases, pulls infrastructure
|
||||
and builds NATS packaging, notifications, BFF and frontend images. Vault and Mongo
|
||||
use upstream images directly. Resolution excludes prereleases and selects Nginx's
|
||||
stable channel. The exact versions and manifest digests are saved in the ignored
|
||||
`.local/dev/images.env`; Flutter's stable release is pinned by Git commit. Build
|
||||
fails if upstream metadata cannot be resolved. Run `make build` again to refresh
|
||||
versions. Review database release upgrade requirements before updating a populated
|
||||
local volume across major versions; the tooling does not perform data migrations.
|
||||
The build resolves the latest stable releases and pins their digests in ignored
|
||||
`.local/dev/images.env`. Prereleases are excluded, resolution failures stop the
|
||||
build, and previous pins are retained on failure. Flutter is pinned to the stable
|
||||
release commit. Tooling uses a freshly pulled stable Docker CLI digest. Generated
|
||||
protobuf bindings and Flutter output are built locally/in Docker and never tracked.
|
||||
|
||||
`make up` starts persistent Vault, initializes/unseals it, provisions secrets and
|
||||
AppRoles, initializes an authenticated single-member MongoDB replica set, configures
|
||||
Vault's MongoDB database plugin, and starts authenticated NATS JetStream. It then
|
||||
starts notifications, BFF and the frontend when the required email settings exist.
|
||||
Compose is at `infra/local/compose.yml`; Make supplies the repository root as its
|
||||
project directory. Dockerfiles are in `infra/images/`. Runtime and startup files
|
||||
are grouped by service under `infra/local/`. The stack consists of Vault, Mongo,
|
||||
NATS, notifications, BFF and Caddy serving the frontend.
|
||||
|
||||
**On the first run, `make up` intentionally exits with an error after infrastructure
|
||||
bootstrap because Resend credentials have not been supplied.** No placeholder API
|
||||
key, mock delivery mode or embedded credential is installed. The three infrastructure
|
||||
services remain running so you can configure Vault, then rerun `make up`.
|
||||
On first startup, bootstrap initializes/unseals persistent Vault, provisions
|
||||
AppRoles and secrets, initializes Mongo replica set `rs0`, configures Vault's Mongo
|
||||
plugin, and starts authenticated NATS JetStream. Product startup fails explicitly
|
||||
until Resend credentials exist. Infrastructure remains available for provisioning.
|
||||
|
||||
## Configure Resend in Vault
|
||||
## Vault layout
|
||||
|
||||
The default email driver is `resend`. In the Vault UI at
|
||||
[http://127.0.0.1:18200](http://127.0.0.1:18200), edit the KV v2 secret **`kv/email`**
|
||||
(API path `kv/data/email`):
|
||||
Paths below are inside the KV v2 mount `kv`; the API inserts `data/` after `kv/`.
|
||||
|
||||
```text
|
||||
kv/
|
||||
├── services/
|
||||
│ ├── bff/jwt # jwt
|
||||
│ └── 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
|
||||
```
|
||||
|
||||
Every secret has an owning service or infrastructure component. Service policies
|
||||
permit only the exact paths they consume. Applications cannot read bootstrap
|
||||
credentials. Mongo application users are dynamic secrets under
|
||||
`database/creds/bff` and `database/creds/notifications`, not KV entries. They have
|
||||
`readWrite` only on their respective `sxchnge` / `sxchnge_notifications` databases.
|
||||
Vault uses `mongodb-database-plugin` and its own `vault-manager` account, whose
|
||||
password is rotated immediately after initial configuration.
|
||||
|
||||
There are no frontend certificates, notification certificates or generic `tls`
|
||||
entries in KV. Browser certificates belong to Caddy; internal gRPC is plaintext
|
||||
on the private Docker network and retains caller-token authentication.
|
||||
|
||||
## Resend
|
||||
|
||||
Open [Vault](http://127.0.0.1:18200) and edit
|
||||
`kv/services/notifications/email/resend`:
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| `driver` | `resend` |
|
||||
| `api_key` | Your Resend API key |
|
||||
| `sender_address` | Your verified Resend sender email |
|
||||
| `sender_name` | `SXCHNGE` or your desired display name |
|
||||
| `sender_address` | Your verified sender address |
|
||||
| `sender_name` | Your display name |
|
||||
|
||||
Local administrative login material is in the private, ignored
|
||||
`.local/dev/admin/init.json` (`root_token`). Keep that file private. Product
|
||||
containers never mount it. You can also provision the same secret with a hidden
|
||||
terminal prompt, without placing the API key in shell history:
|
||||
Local administrator login material is in private `.local/dev/admin/init.json`.
|
||||
Product containers cannot access it. Alternatively use the hidden terminal prompt:
|
||||
|
||||
```sh
|
||||
make email
|
||||
make up
|
||||
```
|
||||
|
||||
The startup check rejects an empty key or the bootstrap sender placeholder. It
|
||||
does not validate the key against Resend or prove domain verification; an invalid
|
||||
key fails at delivery. Vault Agent fetches the key and email settings and injects
|
||||
them into the notifications process. There is no `.env` secret fallback. Changing
|
||||
`kv/email` restarts the notification process automatically within about 10 seconds.
|
||||
Bootstrapping again preserves your existing email secret.
|
||||
No placeholder key or delivery fallback is installed. Startup validates presence;
|
||||
it does not send an email or validate domain ownership with Resend. Updating email
|
||||
settings causes Agent to restart notifications. Existing values survive bootstrap.
|
||||
|
||||
## Open the product
|
||||
## Browser HTTPS and internal connections
|
||||
|
||||
After providing Resend credentials:
|
||||
Caddy serves Flutter files and proxies `/api/*` to BFF over HTTP. Open
|
||||
[https://localhost:5126](https://localhost:5126). HTTPS preserves Secure/HttpOnly
|
||||
cookies and same-origin API access. Caddy uses `tls internal` and manages its local
|
||||
CA, certificates and renewal in the `caddy-data` volume. The `caddy-config` volume
|
||||
stores Caddy runtime configuration. Neither volume nor Caddy has Vault access.
|
||||
|
||||
- Frontend and proxied API: [https://localhost:5126](https://localhost:5126).
|
||||
- BFF health: [https://localhost:5126/api/v1/health](https://localhost:5126/api/v1/health).
|
||||
- Vault UI/API: [http://127.0.0.1:18200](http://127.0.0.1:18200).
|
||||
|
||||
Trust `.local/dev/ca.pem` in your local browser/OS certificate store to use HTTPS
|
||||
without a certificate error. Trust installation is a separate local action, not
|
||||
performed by the bootstrap. For a command-line check without changing trust:
|
||||
After frontend starts, Make exports only the public Caddy CA to `.local/dev/ca.pem`.
|
||||
Trust it in your browser/OS to avoid certificate errors. This is a new CA when
|
||||
migrating from Vault-issued browser certificates; the old CA will not trust it.
|
||||
Trust installation is not automated. A CLI check can use:
|
||||
|
||||
```sh
|
||||
curl --cacert .local/dev/ca.pem https://localhost:5126/api/v1/health
|
||||
```
|
||||
|
||||
HTTPS keeps the existing Secure/HttpOnly refresh cookie contract. Nginx serves
|
||||
Flutter and proxies `/api/` to the BFF on the same origin. The bundle selects the
|
||||
existing backend auth adapter with the public client ID `sxchnge-local-web`.
|
||||
Bootstrap inserts this public browser client if absent; it has no client secret.
|
||||
No frontend widget, service or provider architecture changes are needed.
|
||||
BFF calls notifications over plaintext gRPC with its caller token. Mongo and NATS
|
||||
also stay within the private Compose network; no internal service publishes a host
|
||||
port. Only frontend and Vault bind host loopback ports. TLS remains optional in
|
||||
the reusable notification client/server for deployments that explicitly configure
|
||||
it; local runtime configuration contains no certificate settings.
|
||||
|
||||
The rest of the app retains its existing capabilities and limitations: wallet,
|
||||
KYC and settings fixtures are not turned into production integrations by this
|
||||
container setup. Full email signup/login requires a working Resend account.
|
||||
Caddy behavior follows its [internal TLS](https://caddyserver.com/docs/caddyfile/directives/tls)
|
||||
and [automatic HTTPS](https://caddyserver.com/docs/automatic-https) documentation.
|
||||
|
||||
## Secret and connection lifecycle
|
||||
## Mongo transactions and credential rotation
|
||||
|
||||
| Consumer | Vault access |
|
||||
| --- | --- |
|
||||
| BFF | `database/creds/bff`, JWT key, notification caller token and CA |
|
||||
| Notifications | `database/creds/notifications`, NATS token, caller token, TLS certificate/key and email settings |
|
||||
| NATS | NATS authentication token only |
|
||||
| Bootstrap | Infrastructure administration, PKI and initial identities |
|
||||
Mongo is an authenticated single-node replica set (`rs0`), supporting multi-document
|
||||
transactions. It has no failover or redundant data copy. A Mongo restart interrupts
|
||||
access even though application credential replacement does not.
|
||||
|
||||
The database engine is configured at `database/config/mongo` with
|
||||
`mongodb-database-plugin`. It uses its own `vault-manager` Mongo user, whose
|
||||
password is rotated by Vault immediately after configuration. The manager has
|
||||
`userAdminAnyDatabase`; applications never receive that credential. The independent
|
||||
local Mongo administrator and replica-set key come from Vault's random generator
|
||||
and are retained in `kv/bootstrap/mongo` for local provisioning.
|
||||
The shared database wrapper authenticates with each service's AppRole and obtains
|
||||
an independent token/lease for each pool. At half the shorter token/lease lifetime,
|
||||
it verifies a replacement pool before publishing it. Active operations, cursors
|
||||
and full transactions retain the old pool until they finish, then it closes and
|
||||
its token is revoked. Nested stores and commit retries keep the same pool.
|
||||
|
||||
The BFF role has `readWrite` only on `sxchnge`; notifications has `readWrite` only
|
||||
on `sxchnge_notifications`. Mongo URIs come from dynamic Vault credentials, not
|
||||
static application users. Leases default to one hour with a 24-hour maximum.
|
||||
The shared database wrapper obtains credentials directly from Vault using the
|
||||
service's AppRole. At half the effective lifetime (the shorter of the database
|
||||
lease and owning token TTL), it authenticates independently and prepares a new
|
||||
Mongo pool. Only a successful primary ping permits publication. New operations
|
||||
use that pool; active operations, cursors and complete transactions, including
|
||||
nested stores and commit retries, retain the old pool until they finish. The old
|
||||
pool then closes and its dedicated token is revoked. Mongo credential replacement
|
||||
never restarts BFF or notifications. Each generation has a separate token so
|
||||
cleanup cannot revoke the replacement or Vault Agent's identity.
|
||||
Failed refreshes retain valid credentials and retry with bounded backoff. After
|
||||
expiry, database operations fail closed until automatic recovery. Administrative
|
||||
revocation, infrastructure outages and operations longer than their lease can
|
||||
still interrupt work. Repository consumers use `source.Source` operation scopes;
|
||||
`source.Fixed` adapts externally owned static/test connections.
|
||||
|
||||
Refresh failures keep the current valid pool and retry with bounded backoff.
|
||||
Operations are bounded by credential expiry. If Vault remains unavailable beyond
|
||||
that deadline, new database operations fail closed; refresh continues and access
|
||||
recovers automatically without restarting the service. Administrative revocation,
|
||||
operations exceeding their lease and infrastructure failures cannot be made
|
||||
uninterrupted by pool rotation. Shutdown rejects new scopes and drains existing
|
||||
work. Failed cleanup relies on Vault expiration as a backstop.
|
||||
## Migration and persistence
|
||||
|
||||
Repositories and transaction factories share `source.Source` rather than retaining
|
||||
concrete Mongo collections. Low-level consumers must acquire a scope, use its
|
||||
returned context and release it after all work completes; never retain its client
|
||||
or context beyond release. `source.Fixed` adapts externally owned test/static
|
||||
connections. Database credentials are no longer Agent environment templates;
|
||||
Agent continues to fetch non-database secrets and may restart children when those
|
||||
secrets change.
|
||||
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
|
||||
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
|
||||
`.local/dev/migrations/organized-v1/` before removal. That archive is administrative
|
||||
recovery material; applications do not mount it. Old deleted/destroyed versions
|
||||
are not recreated. The old PKI engine is not used or provisioned by this stack;
|
||||
an existing PKI mount is retained to avoid destroying its non-exportable signing
|
||||
key and history as part of a KV reorganization.
|
||||
|
||||
Each agent authenticates using its own AppRole with a renewable periodic token.
|
||||
Local SecretIDs are reusable for container restarts; each container mounts only
|
||||
its own identity directory under `.local/dev/agents/`. Revoking an AppRole SecretID
|
||||
requires removing its matching local `secret-id` file and rerunning `make up` to
|
||||
issue a replacement. Revoking an active token/lease terminates its credentials;
|
||||
The database wrapper retries with a fresh identity; Agent independently retries
|
||||
its non-database secrets when Vault is reachable.
|
||||
Vault, Mongo, JetStream and Caddy data use project-scoped named volumes. Private
|
||||
bootstrap material lives in `.local/dev/`. `make down` preserves both. Vault's local
|
||||
unseal file allows automatic unsealing on restart. This local convenience and
|
||||
loopback HTTP Vault endpoint are for development. Keep bootstrap files paired
|
||||
with their matching volumes; do not discard one side independently.
|
||||
|
||||
The notification gRPC connection uses a Vault-issued certificate for
|
||||
`notifications` and the BFF validates the Vault CA and hostname. Frontend HTTPS
|
||||
certificates also come from Vault PKI. Leaf certificates last 30 days; `make up`
|
||||
refreshes them when less than seven days remain and reloads Nginx. Run it regularly
|
||||
for a development stack kept online for weeks. Agents restart when their TLS KV
|
||||
secret changes. Application secrets are never put into the frontend bundle.
|
||||
|
||||
Vault, Mongo and JetStream data use project-scoped named Docker volumes. Local
|
||||
Vault unseal material, administrator login, per-service bootstrap identities and
|
||||
frontend TLS material are under ignored `.local/dev/`, outside image build contexts.
|
||||
The local Vault automatically unseals using its local key on container restart.
|
||||
These convenience credentials and loopback HTTP Vault access are for this local
|
||||
machine; this is not a production Vault deployment. Do not share `.local/dev/`.
|
||||
Keep it paired with its matching Docker volumes for recovery.
|
||||
|
||||
Mongo and NATS expose no host ports. The `sxchnge-local` network, volumes and
|
||||
container names are independent of existing Sendico stacks. Only the frontend
|
||||
and Vault bind host loopback ports. No Sendico database, message bus or secrets
|
||||
are consumed.
|
||||
|
||||
## Manage and verify
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
make ps
|
||||
make logs
|
||||
make down # preserves named volumes and local bootstrap material
|
||||
make up # idempotent provisioning and readiness checks
|
||||
make test-dev-tooling # offline shell and failure-path checks
|
||||
make test-dev-infra # works before Resend provisioning
|
||||
make test-dev-rotation # real Vault rotations under BFF HTTP traffic
|
||||
make test-dev # requires all six services running
|
||||
make down
|
||||
make test-dev-tooling # shell analysis, failure paths, migration conflict checks
|
||||
make test-dev-infra # policy isolation, real Mongo transactions and lease lifecycle
|
||||
make test-dev-rotation # temporarily deploy BFF/frontend and exercise short leases
|
||||
make test-dev # requires all six services; verified frontend/API HTTPS
|
||||
```
|
||||
|
||||
The infrastructure check exercises AppRole access restrictions, real Vault
|
||||
MongoDB credential issuance, per-database access, lease renewal and user deletion
|
||||
on revocation. The complete check also verifies service health and frontend/API
|
||||
HTTPS with the generated CA. These checks never send a real email.
|
||||
`test-dev-rotation` temporarily sets a 12-second BFF lease, deploys the built BFF
|
||||
and frontend, and requires three pool replacements with unchanged process ID and
|
||||
zero unexpected HTTP responses. Its invalid-token requests exercise Mongo-backed
|
||||
transactions without sending email. The check restores the role TTL and removes
|
||||
services it started; it recreates BFF/frontend at the beginning, so run it as a
|
||||
development validation command, not against active work you need to preserve.
|
||||
Race-enabled backend tests additionally hold a transaction and cursor across a
|
||||
replacement and exercise failed candidate validation, outage recovery, rollback,
|
||||
expiry rejection and graceful shutdown.
|
||||
|
||||
Backend regression tests use fresh `mongo:latest` and `nats:latest` official
|
||||
stable images, pulled before `make test-backend`. Normal validation remains
|
||||
`make test` and `make lint-backend` and requires the host SDKs described in the
|
||||
repository guides.
|
||||
|
||||
Deleting Docker volumes removes local data; `make down` deliberately keeps them.
|
||||
If starting from scratch, stop the stack and explicitly remove this project's
|
||||
three volumes **and** its matching `.local/dev/` bootstrap state together. Never
|
||||
reuse old unseal material with a newly initialized Vault volume, or retain Mongo
|
||||
users while discarding the Vault data that manages their leases.
|
||||
|
||||
## Sources and checked releases
|
||||
|
||||
On 2026-09-12 the resolver selected Vault 2.1.0, MongoDB 8.3.9, NATS 2.14.6,
|
||||
Go 1.27.1, Nginx stable 1.30.4, Ubuntu 26.04 and Flutter stable 3.47.4. These are
|
||||
verification records, not fixed future defaults. Release selection follows the
|
||||
[Vault releases](https://releases.hashicorp.com/vault/),
|
||||
[MongoDB stable release notes](https://www.mongodb.com/docs/manual/release-notes/),
|
||||
[NATS releases](https://github.com/nats-io/nats-server/releases/latest),
|
||||
[Nginx downloads](https://nginx.org/en/download.html),
|
||||
[Go releases](https://go.dev/dl/) and
|
||||
[Flutter stable channel](https://github.com/flutter/flutter/tree/stable).
|
||||
The implementation follows HashiCorp's
|
||||
[MongoDB database plugin](https://developer.hashicorp.com/vault/docs/secrets/databases/mongodb)
|
||||
and [Agent process supervisor](https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent/process-supervisor)
|
||||
contracts.
|
||||
Rotation validation temporarily shortens the BFF role TTL, recreates BFF/frontend,
|
||||
and requires three replacements with unchanged BFF PID and zero unexpected
|
||||
transaction-backed HTTP responses. It restores TTLs and removes services it
|
||||
started. No verification command sends email. Backend validation remains
|
||||
`make test` and `make lint-backend`, using the host SDKs documented in module guides.
|
||||
Reference in new issue
Block a user