Overview
The NATS Provisioner (ravenxcope-nats-provisioner) is a small Go service that gives RavenXcope a decentralized-auth NATS setup. It is the authority that mints the NATS operator, account, and user JWTs, stores the signing material, and serves the account-resolver endpoint that the NATS server calls to validate connections.
It does not carry any sensor traffic itself. Its job is to make trusted credentials exist so that the backend and the sensors can talk to NATS securely. Once credentials are issued, control-plane messages flow directly between backend ⇄ NATS ⇄ sensor (see the Control Plane scenario).
Two commands
The binary has two subcommands:
| Command | Purpose |
|---|---|
init-operator | One-time bootstrap: generates the NATS operator key pair and seeds the system (SYS) account. Run once per deployment. |
serve | Long-running HTTP service that issues accounts/users on demand and exposes the account-resolver endpoint. |
HTTP API (serve)
The service registers a small HTTP mux. Write routes are protected by a shared secret.
| Method & path | Auth | Purpose |
|---|---|---|
GET /healthz | none | PostgreSQL ping; returns 204 No Content when healthy. |
GET /jwt/v1/accounts/ | none | Account-resolver index — returns the SYS account JWT. |
GET /jwt/v1/accounts/{pubkey} | none | Returns the raw account JWT for a public key as application/jwt. The NATS server calls this to resolve accounts. |
POST /accounts | secret | Create-or-return a tenant account ({ "tenantId": "...", "name": "..." }). Returns accountPubKey + accountJWT. |
POST /users | secret | Create-or-return a sensor user and its .creds ({ "tenantId": "...", "sensorId": "..." }). |
POST /backend-user | secret | Issue the backend's control user credentials. |
POST /revoke | secret | Revoke a user by public key ({ "accountId": "...", "userPubKey": "..." }). |
Shared-secret auth
Write routes are wrapped by a middleware that requires the configured BACKEND_SHARED_SECRET. The secret is accepted in either of two headers (checked in order) and compared in constant time:
X-Backend-Shared-SecretX-Ravenxcope-Provisioner-Secret
A mismatch returns 401 Unauthorized.
Credential model
NATS decentralized auth is a three-level hierarchy: operator → account → user.
- Operator — the root identity created by
init-operator. Its seed lives atOPERATOR_SEED_PATHand must not be group/other-readable (the service refuses to start otherwise). It signs all account JWTs. - Accounts — one per tenant. Created by
POST /accounts. The account key pair's seed is encrypted with AES-GCM (ACCOUNT_SEED_ENCRYPTION_KEY, a base64 32-byte key) before being stored in Postgres. Account JWTs are stored alongside. - Users — one per sensor (and one for the backend). Created by
POST /users/POST /backend-user, signed by the relevant account key, and returned as a.credsfile the client uses to connect.
Permission scoping
User permissions are scoped to the tenant/sensor subject namespace so a sensor can only touch its own subjects:
| Principal | Publish | Subscribe |
|---|---|---|
| Backend | rxc.*.*.cmd | rxc.*.*.status, _INBOX.> |
Sensor (tenant/sensor) | rxc.<tenant>.<sensor>.status, JetStream consumer APIs for its own durable | _INBOX.> |
| Control admin | $JS.API.> | _INBOX.> |
Subjects follow the pattern rxc.<tenantId>.<sensorId>.{cmd,status}.
JetStream command stream
When STARTUP_ENSURE_COMMAND_STREAM=true, on startup the service connects to NATS as a control admin and ensures the command stream exists:
- Stream name:
COMMAND_STREAM_NAME(defaultRXC_CMD) - Subjects:
rxc.*.*.cmd - Retention: work-queue, file storage
Per-sensor durable consumers (cmd-<tenant>-<sensor>, filter subject rxc.<tenant>.<sensor>.cmd) are created so each sensor pulls only its own commands. The backend publishes commands onto this stream; sensors pull them. Sensor status is published on rxc.<tenant>.<sensor>.status (core NATS, not JetStream) and consumed by the backend.
Configuration
Loaded from environment (config.go):
| Variable | Required | Default | Purpose |
|---|---|---|---|
PG_DSN | yes (serve) | — | PostgreSQL DSN for the credential store. |
OPERATOR_SEED_PATH | yes (serve) | — | Path to the operator seed (must be 0600/no group+other read). |
ACCOUNT_SEED_ENCRYPTION_KEY | yes (serve) | — | Base64 32-byte AES-GCM key for encrypting account seeds. |
BACKEND_SHARED_SECRET | yes (serve) | — | Shared secret required on write routes. |
LISTEN_ADDR | no | :8080 | HTTP listen address. |
NATS_URL | no | nats://127.0.0.1:4222 | NATS server URL for control admin operations. |
CONTROL_CREDS_PATH | no | — | Pre-existing control-admin creds; if unset the service mints a temporary one. |
COMMAND_STREAM_NAME | no | RXC_CMD | JetStream command stream name. |
CONTROL_ACCOUNT_NAME | no | CONTROL | Account used for control-admin/JetStream operations. |
STARTUP_ENSURE_COMMAND_STREAM | no | false | Ensure the command stream on startup. |
Storage
State lives in PostgreSQL (migrations/001_init.sql), primarily the nats_accounts table keyed by account_id, storing the account public key, account JWT, and the AES-GCM-encrypted account seed. Apply the migration before first start.
Relationship to the backend
The backend never signs JWTs itself — it always calls the provisioner:
NatsProvisionerClientcallsPOST /accounts,POST /users,POST /revokewith the shared-secret header.NatsTenantProvisioningWorkerreconciles tenants that have no NATS account yet and callsCreateAccountAsync.
See Backend Control-Plane Integrations and the Control Plane scenario.