Skip to content
Operate

Security

Scoped keys, delegation, revocation, legacy keys, error handling and webhook signatures.

Full guide in the repository: docs/security.md

Enable server.secure: true and keep the API behind TLS. Requests authenticate with X-LedgerForge-Key; the master secret has deployment-wide administrator power, so give applications and agents scoped keys. /health is public and /metrics uses a separate bearer token. Report vulnerabilities privately to security@ledgerforge.io as described in SECURITY.md.

Scoped keys and delegation

  • POST /api-keys returns the plaintext key only once. Store it in a secret manager; listings never include key material.
  • Authentication checks the bcrypt hash, scope, expiry and revocation against PostgreSQL on each request.
  • A non-master key needs api-keys:writeto delegate, creates keys only for its own owner, grants only concrete scopes it already holds and can't set a later expiry than its own.
  • Listing and revocation derive the owner from the authenticated key; an ownerquery can't select someone else's keys.
  • Delegation ancestry is recorded and immutable. Descendants inherit intersected policies and share ancestor budgets. Keys created before ancestry was recorded are treated as roots, so rotate old delegated keys before relying on family rules.

Revocation and rotation

DELETE /api-keys/:idrevokes a key. Authentication reads PostgreSQL rather than a cached decision, so a stale Redis entry can't restore permission. Revocation stops future requests but doesn't cancel operations already accepted, and it applies to that key only: revoke descendants separately. To rotate, create the replacement with the right policy, deploy it, check access, then revoke the old key.

Older keys without a stored lookup prefix still work through a bounded compatibility path: at most two legacy scans run per process, and excess requests receive 429. After migrating clients, set server.allow_legacy_api_keys: false on every API and MCP process.

Input and errors

Request bodies and uploads have configurable size limits, and invalid amount, precision and overdraft controls are rejected before execution. Responses keep stable error codes and deliberate validation messages; unstructured backend detail is replaced with a generic message. Public metadata updates can't change reserved execution, lineage, policy or recovery keys, so customer metadata can't turn a bounded transfer into an unbounded overdraft or grant approval.

Webhook signatures

Notification webhooks and transaction hooks are signed with notification.webhook.signing_secret, which takes precedence over the master secret. Set a dedicated value so receivers never hold API administrator credentials. Configured headers can't override the signature or timestamp.

Delivery headers
X-LedgerForge-Timestamp: <unix seconds>X-LedgerForge-Signature: hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
  • Verify the exact received bytes before parsing JSON, and compare signatures in constant time.
  • Reject missing or malformed headers and timestamps outside a freshness window, such as five minutes.
  • Delivery can be duplicated. Make the receiver idempotent and acknowledge with 2xx only after persisting.
The webhook verification example includes freshness, raw-body and wrong-secret tests. Destination controls are covered on the deployment page.