[AVAILABLE] for everything below, this is what actually runs today. Cloud/KMS/multi-instance/HA deployment is [ROADMAP], see Roadmap.
Goal
Stand up a real deployment with the storage backend you actually want, and understand exactly which keys the server needs, where they live, and what each one is for.Prerequisites
- Read Gateway attestation, this guide’s key management section assumes you know the gateway keypair is separate from the authorization key and why.
Steps
1. Choose a storage provider
memory and supabase have a real class behind them, postgres and sqlite are
declared in the type union and throw immediately at startup, by design, fails closed rather
than silently falling back to something else.
PARMANA_STORAGE is the only variable read for this. An older DATABASE_PROVIDER variable
existed briefly as a second, disconnected path to the same setting and is now retired: if
it’s present in the environment, config loading fails at startup naming PARMANA_STORAGE as
the replacement, rather than silently ignoring it.
2. Generate both required keypairs
Two, deliberately separate:PARMANA_KEY_DIR (defaults to ./keys). default.*.pem signs and verifies
execution authorizations. gateway.*.pem signs
gateway attestations. They protect different things and are
never the same key, mixing them up is not just a style choice, assertKeyType fails closed
if you sign with the wrong one for a configured provider.
3. Generate a caller API key for every system that will call this API
PARMANA_API_KEYS. The raw key is never
written to disk by the script and cannot be recovered afterward, hand it to the calling
system now. Repeat once per caller (your AI orchestration service, an internal dashboard,
anything else that will call this API), and repeat again per caller whenever you rotate a
key, see Rotating a caller’s key below.
4. Set the full environment checklist
PARMANA_API_KEYS is a JSON array. Multiple entries may share the same callerId, that is
how rotation works, see below. The server refuses to start with this unset or empty, unless
PARMANA_AUTH_DISABLED=true is set explicitly for local development, see
Caller authentication.
5. Run it
Verify
Confirm the server actually started and is enforcing what you expect:PARMANA_KEY_DIR is missing gateway.private.pem, the server refuses to start with a
message naming the exact missing path, it does not start in a degraded, gateway-less mode,
there is no such mode, see The gateway.
What PARMANA_GATEWAY_KEY_ID is for
Rotating the gateway’s key without touching default.*.pem or restarting with a completely
new key directory: generate a second gateway keypair under a different ID (for example
gateway-v2), verify it works, then flip PARMANA_GATEWAY_KEY_ID=gateway-v2 and restart.
The old gateway.*.pem files can stay in place until you’re confident in the rotation, they
simply won’t be read.
Caller authentication
Every route exceptGET /health requires a caller credential: a bearer key generated in
step 3, sent as Authorization: Bearer <key>. Only a SHA-256 hash of each key is ever held
by the server, comparisons run in constant time against that hash, and the raw key is never
logged or written to disk anywhere in this codebase.
This is only safe over TLS. A bearer key sent over plaintext HTTP is readable by anything
that can observe the connection. Terminate TLS in front of this server (a reverse proxy, load
balancer, or service mesh sidecar in your own infrastructure, since Parmana does not run a
hosted service for you), and never expose the API on plaintext HTTP outside localhost. This
is not a suggestion, it is the entire security value of the bearer-key scheme.
Caller authentication is a distinct layer from everything else Parmana does. It runs first,
before a Business Transaction is even constructed, and answers only “should this HTTP
request be entertained at all.” It is independent of, and cannot substitute for, policy
evaluation (a well-authenticated caller submitting a
policy-rejected transaction is still rejected) or gateway
attestation (which proves the gateway itself released a
specific execution, not who called the API). Keeping these layers separate is deliberate,
see How Parmana thinks.
Rotating a caller’s key
No downtime, no restart required beyond a config reload:- Generate a new key for the same
callerId:npm run generate:api-key -- --caller-id orchestrator-1. - Add the new
{ callerId, keyHash }entry toPARMANA_API_KEYSalongside the old one. Both are active during migration. - Hand the new raw key to the caller, confirm it works.
- Remove the old entry from
PARMANA_API_KEYS. The old key is now revoked and stops authenticating immediately on the next config reload.
Upgrading beyond static keys
Caller authentication is implemented behind one interface,CallerAuthenticator
(packages/api/src/auth/CallerAuthenticator.ts), with StaticKeyAuthenticator as the only
implementation today. If your environment requires mutual TLS instead (common in banks that
already run internal PKI for service-to-service traffic), that is a second class implementing
the same interface, swapped in at one place
(packages/api/src/bootstrap/createCallerAuthenticator.ts), not an architecture change: the
middleware, the audit trail, and every route’s behavior stay exactly as documented above.
Disabling authentication (local development only)
What this deployment does not give you
- No KMS/HSM. Both private keys are files on disk, exactly the exposure that produced
the incident noted on Security.
KeyProvidersdeclaresaws-kms,azure-key-vault,gcp-kms, andhsmas config values with zero implementing classes, settingKEY_PROVIDERto one of these today does nothing useful. - Single process, single instance. No shared
NonceStoreacross replicas, a fleet of these processes would each accept the same authorization once, not once fleet-wide (CLAIMS.md 3.2). - Only one connector registered.
vendor-payment, a reference mock, see Add a connector with the Connector SDK for what adding another requires today.
Troubleshoot
Gateway private key not found. Runnpm run generate:gateway-keys, or confirmPARMANA_KEY_DIRpoints at the directory you actually generated into.Key directory does not exist.PARMANA_KEY_DIR(or the./keysdefault) must exist before the server starts,FileKeyProviderdoesn’t create it.- Config loading fails naming
DATABASE_PROVIDER. Remove that variable from your environment, usePARMANA_STORAGEinstead, see step 1. Postgres storage provider not implemented.Expected,postgresandsqliteare declared but not built, usememoryorsupabase.No caller authentication keys are configured.PARMANA_API_KEYSis unset or[]. Generate a key (step 3) and add it, or setPARMANA_AUTH_DISABLED=truefor local development only.401 authentication requiredon every request. Missing or malformedAuthorizationheader, confirm you’re sendingAuthorization: Bearer <key>with the exact raw key from step 3, not the hash.
Next
Choose a signature provider
What
PRIMARY_SIGNATURE_PROVIDER actually changes, and its real cost.Roadmap
What’s designed but not built yet for production deployment: KMS, HA, network enforcement.