Skip to content
Open the portal
Developer docs

Custody, for engineers

Engineers14 min read

Custody is where records and signing keys live, what Stratl can and cannot do with them, and how to check. The plain-words version.

Backend Records Signing key Use it for
local Folders on the API server’s disk A key file on the same disk Development and demos; the portal labels it Local dev
s3-kms One S3 bucket, one prefix per workspace One AWS KMS key per workspace, created inside KMS, never exportable Production, Stratl-managed
Customer The customer’s bucket: S3, Google Cloud Storage or Azure Blob The customer’s key: AWS KMS, Google Cloud KMS or Azure Key Vault Workspaces that have taken custody

STRATL_CUSTODY_BACKEND chooses between the first two for the deployment; taking custody is per workspace on top of either.

  • The private key never exists outside KMS: created as a non-exportable ECC P-256 key; Stratl holds the public half and can only ask KMS to sign.
  • Every signature is checked before it is accepted, against the public key, before anything is written.
  • The API cannot delete: its IAM user may put and get objects and sign with keys tagged stratl:managed; not delete objects, not delete, disable or re-policy keys. Bucket versioning keeps every version.
  • Signing is in AWS’s log too: each kms:Sign appears in CloudTrail, and the request id is on the key-usage record.
  • Optional Object Lock on the bucket makes versions undeletable for a period, by anyone in GOVERNANCE mode without a special permission, by no one in COMPLIANCE mode.

At sign-up the owner picks a region; the workspace’s key is created in that AWS region and its records, checkpoints and content are stored in a bucket there. Only configured regions are offered:

Terminal window
STRATL_CUSTODY_REGIONS=us=us-east-1:records-us,eu=eu-west-1:records-eu,in=ap-south-1:records-in

Each workspace remembers its own region and bucket, so editing this later adds or retires choices for new workspaces and never moves an existing one. The promise covers records, content, checkpoints and keys; the PostgreSQL index and the API run in one location for all workspaces.

The service asks the key service to sign at most twice per record: the record, and later the checkpoint that covers it, plus a manifest signature per exported pack. Each request is noted at the moment it happens with what was signed, the hash, the key id, the backend and the provider’s request id. At each checkpoint the notes since the last one are sealed into a signed DEC-KEY-… record under the subject signing_key and the agent stratl-custody, one action event per use. The ledger’s own signature is a use too and appears in the next ledger; an idle workspace does not write ledgers about ledgers. The Custody page shows the count and links to the latest.

Any cloud that provides a non-exporting key service and versioned, no-delete object storage can hold a workspace’s custody.

Cloud Storage Key Stratl’s identity Set-up
AWS S3 bucket with versioning and Object Lock KMS ECC_NIST_P256 sign/verify key A role in your account that Stratl assumes, with your workspace id as external id deploy/cloudformation/byo-custody.yaml
Google Cloud Cloud Storage bucket with versioning and a retention policy Cloud KMS EC_SIGN_P256_SHA256 key Stratl’s service account granted objectCreator, objectViewer, signerVerifier, publicKeyViewer; or a service-account key by secret reference deploy/gcp/byo-custody.sh
Azure Blob container with versioning and an immutability policy Key Vault EC P-256 key Stratl’s Entra application granted a create-and-read blob role and Crypto User on the key; or your own app registration by secret reference deploy/azure/byo-custody.sh

The Stratl identity to grant is shown in the Take custody dialog and by GET /v1/custody/providers. Secret references (env:NAME, aws-sm:ARN) are resolved at use and never stored.

Only the workspace owner can do this. From the Custody page, Take custody.

  1. Run the set-up in your account. Each script or template creates the bucket, the key and a least-power identity, and prints the values to paste. Read it before running it; it is short.

  2. Paste the values into the dialog: for AWS the region, bucket, key ARN and role ARN; for Google Cloud the project, bucket and key version resource name; for Azure the tenant id, storage account, container, vault URL and key name.

  3. Prove it. Stratl, with the identity you granted, signs a probe and verifies it against the key’s public half, writes a small object under _stratl/probes/ and reads it back, then tries to delete it. The delete must be refused. All four steps are shown; a configuration that fails any of them is refused and nothing changes. POST /v1/custody/test runs the probe alone.

  4. Take custody. A transfer statement naming the old custody, the new custody, the chain head and the last checkpoint is signed by both keys and written into both storages at transfers/<id>.json with both public keys. The first record signed with your key is DEC-CT-…, carrying the statement’s fingerprint. From here every new record is signed with your key, stored in your bucket and labelled customer.

  5. History is copied. Every earlier record and its signature, every checkpoint, every public key that ever signed and every original is written into your storage by the scheduler, a batch per tick. The Custody page shows progress. History is never re-signed: the old public key stays in every bundle, so old records still verify.

The CloudFormation template creates a role trusted by Stratl’s AWS account, conditioned on sts:ExternalId equal to your workspace id, with a policy allowing s3:PutObject, s3:PutObjectRetention, s3:GetObject, s3:ListBucket on the bucket and kms:Sign, kms:GetPublicKey, kms:DescribeKey on the key. No delete, no key management.

STRATL_TSA_URL is a comma-separated list of RFC 3161 authorities tried in order; the default pairs a public authority with a commercial CA’s public server. The token records which answered. A checkpoint no authority stamps is marked failed and retried on later ticks against the whole list.

Each record and checkpoint carries the key_id that signed it, and every public key that has ever signed for a workspace is kept in the registry and next to the records. A bundle carries the key of the record it contains. One current limit: a bundle holds a single key, so if a record was signed by one key and its checkpoint by a later key, the checkpoint check does not run offline; seal pending records with a checkpoint before changing keys.

Proving managed custody against a real account

Section titled “Proving managed custody against a real account”
Terminal window
uv run --directory api python scripts/check_custody.py

In every configured region it signs in both KMS modes, writes and reads S3, and confirms the API is not allowed to delete. It uses a throwaway workspace name and never touches a real workspace’s chain. The API also refuses to start if a configured bucket is unreachable, and says why.