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
| Process | Can reach | Holds | Cannot |
|---|---|---|---|
| API | Inbound HTTP from your systems; PostgreSQL under its role; nodes for readiness and reads | The API key store’s hashes and its pepper | Build a signer, allocate a nonce, change the schema |
| Worker | PostgreSQL under its role; nodes, bundlers, your webhook receivers; it opens no port | No signer: it queues signing requests | Build a signer, read API keys, change the schema |
| Signing gate | PostgreSQL under its role; your signer; nothing else, it opens no port | Signer references and the credentials to reach your signer | Reach a node, read API keys, write operations, change the schema |
| Indexer | PostgreSQL under its role; nodes, your webhook receivers; it opens no port | No signer | Write 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.
| Role | May write | Refused |
|---|---|---|
| API | Operations it accepts, identities, contracts, approvers, approvals, signing requests, API keys | Allocating a nonce; deleting an API key |
| Worker | Everything operational | Reading API keys, even their hashes |
| Indexer | Subscription cursors and outbox entries | Operations, nonces, identities, signing requests, API keys |
| All three | Any 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
| Profile | Who operates it | Admits | Refuses |
|---|---|---|---|
| Self-hosted | You | Every signer | Nothing beyond each adapter’s own checks |
| Run for you | CryptoRail’s operator, for you | Your external signer; KMS keys in your cloud account | Development 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 sandbox | CryptoRail’s operator, for a pilot | Everything, including keys the service generates and keeps sealed | Nothing; 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:
| Subject | Requirements and decisions | Journeys |
|---|---|---|
| Keys out of CryptoRail, verification | NFR-SEC-01, NFR-SEC-02, FR-SIG-09 | J12 |
| Process roles, database least privilege | FR-OPS-04, NFR-SEC-06 | J27, J38 |
| Secrets by reference, redaction | FR-OPS-03, NFR-SEC-04 | J18, J22 |
| API keys, roles, tenancy, audit log | FR-API-04, FR-API-05, FR-API-06, FR-API-14, FR-API-16, ADR-0002 | J26, J41, J44 |
| Webhook signatures and secrets | FR-EVT-09, FR-API-12, NFR-SEC-03, ADR-0004 | J34 |
| Inbox for signatures and approvals | NFR-SEC-03, FR-APR-05, FR-APR-07 | J19, J20, J25, J41 |
| Deployment profiles | NFR-SEC-07, NFR-SEC-10, ADR-0005 | J42, J43 |
| Dependency audit | NFR-SEC-05 |