Skip to main content

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:

CommandPurpose
init-operatorOne-time bootstrap: generates the NATS operator key pair and seeds the system (SYS) account. Run once per deployment.
serveLong-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 & pathAuthPurpose
GET /healthznonePostgreSQL ping; returns 204 No Content when healthy.
GET /jwt/v1/accounts/noneAccount-resolver index — returns the SYS account JWT.
GET /jwt/v1/accounts/{pubkey}noneReturns the raw account JWT for a public key as application/jwt. The NATS server calls this to resolve accounts.
POST /accountssecretCreate-or-return a tenant account ({ "tenantId": "...", "name": "..." }). Returns accountPubKey + accountJWT.
POST /userssecretCreate-or-return a sensor user and its .creds ({ "tenantId": "...", "sensorId": "..." }).
POST /backend-usersecretIssue the backend's control user credentials.
POST /revokesecretRevoke 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:

  1. X-Backend-Shared-Secret
  2. X-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 at OPERATOR_SEED_PATH and 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 .creds file 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:

PrincipalPublishSubscribe
Backendrxc.*.*.cmdrxc.*.*.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 (default RXC_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):

VariableRequiredDefaultPurpose
PG_DSNyes (serve)PostgreSQL DSN for the credential store.
OPERATOR_SEED_PATHyes (serve)Path to the operator seed (must be 0600/no group+other read).
ACCOUNT_SEED_ENCRYPTION_KEYyes (serve)Base64 32-byte AES-GCM key for encrypting account seeds.
BACKEND_SHARED_SECRETyes (serve)Shared secret required on write routes.
LISTEN_ADDRno:8080HTTP listen address.
NATS_URLnonats://127.0.0.1:4222NATS server URL for control admin operations.
CONTROL_CREDS_PATHnoPre-existing control-admin creds; if unset the service mints a temporary one.
COMMAND_STREAM_NAMEnoRXC_CMDJetStream command stream name.
CONTROL_ACCOUNT_NAMEnoCONTROLAccount used for control-admin/JetStream operations.
STARTUP_ENSURE_COMMAND_STREAMnofalseEnsure 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:

  • NatsProvisionerClient calls POST /accounts, POST /users, POST /revoke with the shared-secret header.
  • NatsTenantProvisioningWorker reconciles tenants that have no NATS account yet and calls CreateAccountAsync.

See Backend Control-Plane Integrations and the Control Plane scenario.