Security model and trust boundaries

For security reviewers.

In short

CryptoRail is built so that the dangerous capabilities are few, located, and checked. Only one kind of process can ask for a signature, and it never holds the key that produces it. Every process connects to the database with a role limited to its job. Secrets are references into your secret manager, never values in a file. What CryptoRail stores to authenticate you, or to sign the webhooks it sends you, is useless to whoever reads the database. And what a deployment may load is a declared profile the process checks when it starts.

No external security audit has been carried out yet. Self-hosted deployments and pilots are offered today; running CryptoRail for you in production waits for that audit. The last section lists what it must cover.

Trust boundaries

flowchart LR
    subgraph YOU[Your side]
        SYS[Your systems]
        SIG[Your HSM, KMS key or signer]
        APR[Your approvers]
        SEC[Your secret manager]
        RCV[Your webhook receivers]
    end
    subgraph CR[CryptoRail]
        API[API]
        W[Worker]
        IX[Indexer]
        DB[(PostgreSQL)]
    end
    APR -->|EIP-712 signatures| SYS
    SYS -->|API key over HTTPS| API
    API --> DB
    W --> DB
    IX --> DB
    W <-->|bytes to sign, signature| SIG
    CR -. secret references .-> SEC
    W -->|signed webhooks| RCV
    IX -->|signed webhooks| RCV
    W --> NET[Nodes and the network]
    IX --> NET

Three facts frame the rest of the page:

  • Keys stay on your side. CryptoRail sends a digest or the bytes to sign and receives a signature, which it verifies before use.
  • Nothing trusts a message because of where it came from. Calls carry an API key and a role; signatures and approvals posted back are bound to the request they answer; webhooks carry a signature the receiver checks.
  • The processes share only the database, and each reaches it with its own limited role.

Keys never enter CryptoRail: signing happens in your HSM, your cloud KMS or your own signer, and only the development signer ever touches a key.

Every signature is checked against the expected address before anything is sent.

Only the development signer's package may touch key material, enforced by a dependency check on every change.

What each process can and cannot reach

ProcessCan reachHoldsCannot
APIInbound HTTP from your systems; PostgreSQL under its role; nodes for readiness and readsThe API key store’s hashes and its pepperBuild a signer, allocate a nonce, change the schema
WorkerPostgreSQL under its role; nodes, bundlers, your webhook receivers; it opens no portNo signer: it queues signing requestsBuild a signer, read API keys, change the schema
Signing gatePostgreSQL under its role; your signer; nothing else, it opens no portSigner references and the credentials to reach your signerReach a node, read API keys, write operations, change the schema
IndexerPostgreSQL under its role; nodes, your webhook receivers; it opens no portNo signerWrite operations, nonces, identities or signing requests; change the schema

The API process builds no signer, and a split deployment that configures one for it is refused at startup.

One process, or separate API, worker, signing gate and indexer processes sharing only the database, each with a database role limited to what it needs.

The signing gate is the process to protect most: it is the only one that can ask your signer for a signature, and it re-checks every request (approvals, policy, limits) against the exact payload before it signs. It reaches PostgreSQL and your signer and nothing else, so a compromised API or worker can queue a request but cannot sign anything the gate refuses. Where an approval must hold even against a compromised gate, a guarded smart account enforces the quorum on chain; the signing deep dive explains how.

In a split deployment the signing gate is a process of its own, the only one with signer credentials: it reaches the database and your signer and nothing else, and re-checks every request before it signs.

Database least privilege

Each role is a PostgreSQL group role created by a migration, with no login and no password: your platform creates the user and grants it membership. A refused statement is reported as a permission error and never retried, so a deployment mistake stays visible instead of turning into a busy loop.

RoleMay writeRefused
APIOperations it accepts, identities, contracts, approvers, approvals, signing requests, API keysAllocating a nonce; deleting an API key
WorkerEverything operationalReading API keys, even their hashes
IndexerSubscription cursors and outbox entriesOperations, nonces, identities, signing requests, API keys
All threeAny change to the schema or to the migration record

Each process has its own database role: the API cannot allocate a nonce, the worker cannot read API keys, the indexer cannot touch transactions, identities or signing requests, and no process can change the schema.

Runs in your infrastructure, as a container image over your PostgreSQL, with schema migrations applied by an explicit command.

Secrets by reference

The configuration never holds a secret, only a reference to one: an environment variable, a file under a configured directory, or a HashiCorp Vault entry. HSM PINs, database passwords, the API key pepper, the webhook encryption key and your signer’s credentials all arrive that way, and a literal value where a reference is expected is refused at startup. Secrets are kept out of every log sink, which a redaction test checks.

Secrets passed by reference and never written to logs, traces, metrics or responses.

A secret written into the configuration instead of a reference to your secret manager is refused at startup.

Health and readiness that tell the truth, and a configuration checked at startup with every problem listed at once.

API keys under a pepper

An API key has a public lookup id and a 256-bit random secret. What the database stores is an HMAC of both under a pepper, a server-side secret that comes from your secret manager and is never written to the store. A slow password hash would buy nothing against a secret of that entropy; what matters is that something necessary to verify a key is not in the database. Comparison is constant time, a credential is shown once when it is created, and rotation keeps the old key working through an overlap window.

flowchart LR
    K[API key: lookup id and secret] --> H[HMAC-SHA256 under the pepper]
    P[pepper, from your secret manager] --> H
    H --> C{equals the stored value?}
    S[(key store: lookup id and HMAC only)] --> C
    C -->|yes| R[role and tenant of the key]
    C -->|no| X[refused, same answer as no key]

API keys are stored only as a keyed hash under a pepper kept in your secret manager, so whoever reads the database cannot call the API with what they find.

A production deployment refuses to start with authentication on but no pepper, or without a webhook encryption key, rather than pretend to a protection it lacks.

Who may do what is checked per call: read-only, submitter, signer, sponsor, operator and admin roles.

A tenant sees only its own resources, proven on every route of the API on every change.

Per-tenant limits set by the operator, with refusals that name the limit reached.

Every approval, signature and decision is recorded with who, when and what, and every change made through the API lands in an audit log.

Authentication with mutual TLS or OIDC tokens, beside API keys. On request

Signed webhooks

Every delivery is signed with HMAC-SHA256 over the delivery id, a timestamp and the body, in the Standard Webhooks format, so a receiver can verify it with existing libraries and reject a timestamp outside a five-minute window before computing anything. The delivery id stays the same across retries, so a receiver can drop duplicates.

sequenceDiagram
    participant CR as CryptoRail
    participant R as Your receiver
    CR->>CR: open the endpoint's sealed secret with the key from your secret manager
    CR->>R: POST body, with id, timestamp, signature
    R->>R: timestamp within five minutes?
    R->>R: HMAC over id, timestamp and body matches?
    R-->>CR: 2xx, or a retry later
    Note over CR,R: during a secret rotation each delivery carries both signatures

The secret is generated by CryptoRail, shown once, and stored sealed with AES-256-GCM under an encryption key that comes from your secret manager. Rotating an endpoint’s secret keeps the old one valid through an overlap window in which every delivery carries both signatures; rotating the encryption key re-seals secrets as endpoints are written, without a gap.

Each webhook endpoint gets a secret shown once, sealed at rest under a key from your secret manager, and rotated without a gap; endpoints must be HTTPS unless the deployment explicitly allows otherwise.

Signed webhooks, retried for about three days, then held in a dead-letter queue an operator can redeliver from.

Signatures and approvals coming back

The inbox through which your signer and your approvers answer is authenticated and bound: an answer must name a pending request, carry the bytes that were requested, and recover to the expected key. A signature by the wrong key is refused with an alert and the request stays open for the right one.

Signatures and approvals posted back are authenticated and bound to the one request they answer.

At the last moment before signing, a signing gate checks the exact payload against what was approved: same digest, gas within the cap, not expired, not used by another transaction, quorum still valid, policy still allowing it.

Three deployment profiles

Topologies say where the processes run; the deployment profile says who operates them, and so what they may load. The profile is checked in every process before anything is built, and every refusal is listed at once.

flowchart LR
    C[configuration] --> P{profile}
    P -->|self-hosted| OK[start]
    P -->|run for you| H{any key of its own?}
    H -->|yes| X[refuse, every reason listed]
    H -->|no| OK
    P -->|pilot sandbox| W[warn: a sandbox, never for value] --> OK
ProfileWho operates itAdmitsRefuses
Self-hostedYouEvery signerNothing beyond each adapter’s own checks
Run for youCryptoRail’s operator, for youYour external signer; KMS keys in your cloud accountDevelopment signers, PKCS#11, any KMS key in the operator’s own account or named without an account, authentication switched off, no webhook encryption key
Pilot sandboxCryptoRail’s operator, for a pilotEverything, including keys the service generates and keeps sealedNothing; it warns at every start and declares itself

Run for you in production without ever holding a key of its own: signing stays on your side or in your own cloud account. Available for pilots

The hosted profile refuses to start if it is given a key of its own, listing every reason.

A pilot environment stood up in minutes with nothing installed on your side.

A sandbox says it is one: in its logs, on its health endpoint and on every screen built over it. It is never supported for value.

Run for you, the service sees intents, call data, addresses, the payloads it asks to have signed and the signatures. It never has a private key or a credential that signs with a key in its own account. With a KMS key in your account it can sign while you let it, under a permission you can revoke at any moment, and every signature appears in your own audit trail; with the external signer it never has anything to sign with.

The sandbox’s protection is awareness, not a technical lock: by the owner’s decision it may point at any network, and it says what it is in its logs, on its health and readiness answers and on every screen built over it.

Supply chain

Dependencies scanned on every change; a high or critical vulnerability blocks the release.

What an external audit must cover

No external audit has been done. The one that precedes running CryptoRail for customers in production must cover at least:

  • The signing path: signature assembly and verification, the signing gate’s checks, and the separation that keeps key material in the development signer’s package only.
  • The delegated KMS path of the run-for-you profile: the account check at startup and what a service with a customer’s grant can and cannot do.
  • The inbox for signatures and approvals: authentication, binding to the request, and the field-by-field comparison of what comes back.
  • Authentication and tenancy: API key storage under the pepper, roles, tenant isolation across every route, quotas.
  • Webhooks: signing, secret sealing and both rotations.
  • The database roles and their grants, per process.
  • The deployment profile checks, and the sealing of the keys a sandbox generates.
  • The guarded-account configuration used for on-chain enforcement, including the validator version the owner has yet to decide on.
  • The dependency tree and the build.

What it does not do

  • It has not been audited externally. The tests, journeys and requirements are the evidence today; the audit is still to come.
  • It does not encrypt your operational data itself. Operations, addresses and payloads are stored as they are; encryption at rest is your database platform’s. Only webhook secrets and sandbox keys are sealed by CryptoRail.
  • It does not lock a sandbox to test networks. A sandbox is never supported for value, and says so everywhere.
  • It does not authenticate with mutual TLS or OIDC today; those authenticators are built on request.
  • Custody of your end customers' keys. Not supported

Evidence

Every claim above links to its evidence on the evidence page. The main ones:

SubjectRequirements and decisionsJourneys
Keys out of CryptoRail, verificationNFR-SEC-01, NFR-SEC-02, FR-SIG-09J12
Process roles, database least privilegeFR-OPS-04, NFR-SEC-06J27, J38
Secrets by reference, redactionFR-OPS-03, NFR-SEC-04J18, J22
API keys, roles, tenancy, audit logFR-API-04, FR-API-05, FR-API-06, FR-API-14, FR-API-16, ADR-0002J26, J41, J44
Webhook signatures and secretsFR-EVT-09, FR-API-12, NFR-SEC-03, ADR-0004J34
Inbox for signatures and approvalsNFR-SEC-03, FR-APR-05, FR-APR-07J19, J20, J25, J41
Deployment profilesNFR-SEC-07, NFR-SEC-10, ADR-0005J42, J43
Dependency auditNFR-SEC-05