From a919ea7b52c6f8fb31225fe959477a70d08dc921 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Tue, 30 Jun 2026 23:19:05 -0400 Subject: [PATCH] docs(operator): OPERATOR_SETUP guide + fix .env.example peppers + README bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .env.example: add the required at-rest identity secrets (SEED_FINGERPRINT_PEPPER, REFRESH_TOKEN_PEPPER, IP_SALT). They were missing, so `cp .env.example .env` on a fresh setup boot-failed closed on the first required-secret check. - README: correct the first-admin bootstrap to `smirk-admin setup --pubkey`. create-admin-wallet registers a pending key but does not latch, so on a fresh instance it leaves the bootstrap uninitialized. - docs/operations/OPERATOR_SETUP.md: full deployment guide — database, secrets, per-chain backends, run + verify, admin bootstrap, registration policy, and smoke-testing an instance (SMOKE_API_STYLE=namespaced). Surfaced by a clean-room operator setup against a fresh instance. --- .env.example | 8 +++ README.md | 9 ++- docs/operations/OPERATOR_SETUP.md | 108 ++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 docs/operations/OPERATOR_SETUP.md diff --git a/.env.example b/.env.example index c527127..2fb62a7 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,14 @@ DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk JWT_SECRET= JWT_EXPIRY_HOURS=24 +# ── At-rest identity secrets (REQUIRED; generate each: openssl rand -hex 32) ─ +# HMAC peppers keep stored identity values (pubkey_hash, seed_fingerprint) +# non-reproducible from a candidate seed; the salt unlinks client IPs in abuse +# tables. Boot fails closed if any is unset or too short. +SEED_FINGERPRINT_PEPPER= +REFRESH_TOKEN_PEPPER= +IP_SALT= + # ── Proof-of-work signup gate (ALTCHA v2) ───────────────────────────────── ALTCHA_HMAC_KEY= POW_REQUIRED=false diff --git a/README.md b/README.md index 3850a7a..963beee 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,16 @@ inconsistent feature configuration rather than booting with a control silently defeated. Generate secrets with `openssl rand -hex 32`. See [`.env.example`](.env.example) for the full, documented configuration surface. -First admin (headless — the supported bootstrap): +First admin (headless bootstrap): ```sh -smirk-admin create-admin-wallet --out admin.secret # generates a key, registers the pubkey -# import admin.secret into your NIP-98 signer; it activates on first admin login +smirk-admin setup --pubkey # seed the first admin + latch the bootstrap ``` +`` is the admin's Sign-in-with-Smirk (Nostr) public key; the admin +then authenticates with the matching key over NIP-98. Full deployment guide: +[docs/operations/OPERATOR_SETUP.md](docs/operations/OPERATOR_SETUP.md). + ## Security posture - **Fail-closed configuration** — the only place that reads the environment; diff --git a/docs/operations/OPERATOR_SETUP.md b/docs/operations/OPERATOR_SETUP.md new file mode 100644 index 0000000..48fc37c --- /dev/null +++ b/docs/operations/OPERATOR_SETUP.md @@ -0,0 +1,108 @@ +# Operator setup + +Deploy a `smirk-backend-core` instance. It holds no seed and no spend key; it +forwards view credentials to your chain backends per request and relays signed +bytes. Run your own for full privacy. + +## Prerequisites + +- Rust (stable) and PostgreSQL 14+. +- A chain backend for each chain you enable (all optional, feature-flagged): + - Bitcoin / Litecoin — an Electrum/Fulcrum server (your own, or public). + - Monero / Wownero — a light-wallet-server (LWS) and a daemon. + - Grin — grin-wallet (owner/foreign API) and a grin node. +- A reverse proxy terminating TLS in front of the public port (production). + +## 1. Database + +```sh +createdb smirk_backend_core # or point DATABASE_URL at an existing database +``` + +Migrations run automatically on first start. + +## 2. Secrets and configuration + +```sh +cp .env.example .env +``` + +Generate each required secret with `openssl rand -hex 32`: + +- `JWT_SECRET`, `SEED_FINGERPRINT_PEPPER`, `REFRESH_TOKEN_PEPPER`, `IP_SALT` — always required. +- `ADMIN_JWT_SECRET`, `ADMIN_KEY_INTEGRITY_SECRET` — required when `ADMIN_ENABLED=true`. Back up `ADMIN_KEY_INTEGRITY_SECRET`; losing it locks out admin. + +Then set: + +- `DATABASE_URL` — the database from step 1. +- `PUBLIC_API_URL` — the public absolute base URL, e.g. `https://backend.example.org/api/v1`. It is the canonical value NIP-98 tokens bind (never the request `Host`), and is required for Nostr identity and erasure. +- `ENVIRONMENT=production` — enforces HTTPS on external URLs and rejects placeholder secrets. + +The server fails closed: it refuses to start on a missing or weak secret, or an +inconsistent feature configuration. `.env.example` documents every key. + +## 3. Chain backends + +Enable a chain with its `FEATURE_*` flag and configure its source. A chain whose +flag is on but whose source is unconfigured reports `enabled: false` via +`/capabilities` instead of failing at call time. + +| Chain | Keys | +|-------|------| +| BTC / LTC | `{BTC,LTC}_ELECTRUM_URL` (primary) and `{BTC,LTC}_ELECTRUM_FALLBACKS` (comma-separated). `ssl://` / `tls://` verify TLS with hostname checking; `tcp://` is plaintext (LAN only). Primary is tried first, then fallbacks in random order. | +| XMR / WOW | `{XMR,WOW}_LWS_URL`, `{XMR,WOW}_LWS_ADMIN_URL`, `{XMR,WOW}_LWS_ADMIN_KEY`, `{XMR,WOW}_DAEMON_URL`. | +| Grin | `GRIN_OWNER_API_URL` + `GRIN_OWNER_API_SECRET`, `GRIN_WALLET_PASSWORD`, `GRIN_FOREIGN_API_URL`, and the node's `GRIN_NODE_*`. | + +## 4. Run + +```sh +cargo run --release # or run the built ./target/release/smirk-backend-core +``` + +Verify: + +```sh +curl -s localhost:8080/health +curl -s localhost:8080/api/v1/capabilities # enabled chains, features, restore + registration policy +``` + +## 5. First admin (optional) + +Admin functions — key management, erasure, tamper-evident audit — run on a +separate loopback listener; the public API needs no admin. To enable it, set +`ADMIN_ENABLED=true` and the two admin secrets, then bootstrap: + +```sh +smirk-admin setup --pubkey # seed the first admin + latch the bootstrap +smirk-admin doctor # database, live keys, audit chain +``` + +`` is the admin's Sign-in-with-Smirk (Nostr) public key; the admin +authenticates with the matching key over NIP-98. The bootstrap latch is +MAC-protected and one-way — a live deployment is adopted as already-bootstrapped, +and tampering fails closed. + +Expose the admin plane over a Tor onion or an SSH tunnel; never bind it to a +public interface. `ADMIN_BIND` defaults to loopback, and a non-loopback bind +requires an explicit `ADMIN_ALLOW_PUBLIC_BIND=true`. + +## 6. Registration policy (optional) + +Gates are composable and advertised via `/capabilities`; returning wallets and +self-hosting bypass them. All default off or open. + +- **Invite codes** — `REGISTRATION_REQUIRE_INVITE=true`; mint with `smirk-admin mint-invite --count `. +- **Proof-of-work** — `FEATURE_POW=true`, `POW_REQUIRED=true`, `ALTCHA_HMAC_KEY`. +- **Restore policy** — `WALLET_RESTORE_POLICY=create-only|bounded|unlimited` (default `create-only`), with optional proof-of-work pricing on restore depth. +- **Pay-to-register** — `REGISTRATION_REQUIRE_PAYMENT=true` against a BTCPay-compatible processor (`PAYMENT_*`); a new wallet settles an invoice, paid to your own wallet, before it can register. + +## Testing an instance + +The `@smirk/smoke-tests` harness drives the API client end-to-end. Point it at an +instance with `SMOKE_BACKEND_URL`, and set `SMOKE_API_STYLE=namespaced` — this +backend serves namespaced `/wallet/utxo/*` routes, not the legacy flat paths: + +```sh +SMOKE_BACKEND_URL=https://backend.example.org/api/v1 SMOKE_API_STYLE=namespaced \ + npm run check-balance -w @smirk/smoke-tests +```