Skip to main content
Secrets stores small sensitive values — database passwords, third-party API tokens, signing material — and hands them back only to a caller that is allowed to read them. Values are never stored in the clear: every version is encrypted before it reaches the database, and only ciphertext is persisted. The service is regionalsecrets.sa-saopaulo-1.basaltic.sh.

Metadata and value

Two endpoints, two IAM actions, and why reading the value is the audited one.

Versions

Rotation without losing what came before.

Delete and restore

The recovery window, and the one thing repeating a delete will not do.

Your own KMS key

What binding a secret to a customer-managed key actually changes.

Creating a secret

A secret is created with its first value — there is no such thing as an empty secret. The response is 201 and the secret is at version 1.
base64, 1 byte – 64 KiB
Values travel base64-encoded so arbitrary binary payloads survive JSON intact. Anything larger than 64 KiB belongs in a bucket, with the secret holding the reference and the credentials to fetch it — not crammed inline.
unique per account
Matches ^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,255}$. Slashes are allowed, which is why prod/api/stripe-key works — and because the CRN is built from the name, a path convention becomes directly policy-able: crn:secrets:sa-saopaulo-1:my-account:secret/prod/*.

The value is a separate endpoint

Describing a secret and reading it are different operations with different permissions.

GET /v1/secrets/{id}

Name, description, tags, current version number, which KMS key it is bound to, deletion state. Never the value. Guarded by secrets:DescribeSecret.

GET /v1/secrets/{id}/value

The decrypted plaintext, base64-encoded. Guarded by the separate, narrower secrets:GetSecretValue.
This split is the point of the service. A deployment tool, a dashboard or an inventory job can be given secrets:DescribeSecret across everything and still be unable to read a single value.
Every plaintext read is recorded, successful or not. A denied read, a probe for an id the caller does not own, and a decrypt that failed are all written to the audit trail with the error code that stopped them — that set is exactly what a review is looking for, and it would be invisible if only successes were logged.
Add ?version=N to read a specific version instead of the current one.

Versions

Each call allocates the next version number and makes it current; the previous version stops being current but stays readable by explicit ?version=. That is what makes rotation safe — you publish the new value, let consumers pick it up, and still have the old one to fall back to. GET /v1/secrets/{secret_id}/versions lists version metadata, highest first. Each entry carries created_by, the CRN of the principal that wrote it (crn:iam:::user/<id> or crn:iam:::service-account/<id>), so you can see who rotated what.
Version metadata never includes ciphertext. GET .../value is the only route that produces plaintext, and it is the only one that needs secrets:GetSecretValue.

Deleting and restoring

DELETE /v1/secrets/{secret_id} does not delete anything immediately. It moves the secret into a recovery window and returns deleted_at and scheduled_purge_at. Once scheduled_purge_at passes, the secret and every version are removed for good.
86400 – 2592000, default 604800
1 to 30 days, defaulting to 7. Set it at creation to give a secret its own default, or pass it on the delete call to override it for that deletion.
While a secret sits in the window:
  • GET /v1/secrets/{id} still describes it, so you can see the purge date.
  • Reading or writing the value is refused with 409 SECRET_DELETED.
  • Updating metadata is refused the same way.
  • It is hidden from GET /v1/secrets unless you pass include_deleted=true.
POST /v1/secrets/{secret_id}/restore cancels the purge and puts the secret back. Restoring one that was never deleted is a harmless no-op.
Repeating the delete does not extend the window. A second DELETE during the window keeps the original scheduled_purge_at rather than pushing it out, so a retry loop or a re-run of a teardown script cannot quietly keep a secret alive forever. If you need more time, restore the secret and delete it again with a longer recovery_window_seconds.
Deleting frees the secret’s quota straight away, so you can create a replacement without waiting out the window. Restoring has to take that quota back, which means a restore can fail with 403 QUOTA_EXCEEDED if your account has since filled the slot. Free one before restoring.

Encrypting under your own key

By default a secret’s versions are encrypted under a platform-managed key. Pass kms_key_id at creation to use one of your own KMS keys instead:
The key must be one of yours, in the same region, enabled, and pinned to encrypt_decrypt.
It must also be symmetric — aes-256. An RSA key with encrypt_decrypt usage is refused with 400 KMS_INVALID_KEY_SPEC, because every version is sealed with the secret’s identity bound in as encryption context and RSA-OAEP has nowhere to carry one. Accepting the key would drop that binding silently, so it is refused at creation instead.

What it changes

Binding a secret to your key does not change the API surface — value reads and writes look identical. What changes is that the key becomes a control you hold:
  • Disable the key and reads start failing with 409 KMS_KEY_DISABLED. That error travels to you as itself, not as a 500 — the key state is yours, and re-enabling the key restores reads.
  • Schedule the key for deletion and reads fail with 409 KMS_KEY_PENDING_DELETION for the whole window.
  • Let that window elapse and every version of every secret under that key is permanently unreadable. The ciphertext is still in the database; nothing can open it.
The binding is fixed for the life of the secret. kms_key_id is accepted only on create, and PATCH edits just description and tags — moving a secret to a different key means re-wrapping every version, so create a new secret and retire the old one instead.

Versions are bound to their secret

Whichever key is used, the ciphertext of every version is sealed with the secret’s account and secret id as encryption context. A stored blob therefore only decrypts as the version of the secret it was written for — it cannot be lifted out of the database and replayed as the value of a different secret, even one encrypted under the same key.

Secrets the platform owns

Some secrets are created by a service that then reads the value back and acts on it — a managed database’s generated user password, for instance. Those come back with managed: true. You can read them and you can delete them. PUT-style writes are refused with 403 SECRET_PLATFORM_MANAGED:
  • POST .../value — overwriting the value would not change whatever the value describes, it would make the two disagree, turning a credential the platform trusts into customer-supplied bytes.
  • PATCH /v1/secrets/{id} — including tags, because tags feed the conditions IAM policies are written against. Retagging a secret the platform reads back would let a caller move it in and out of the scope of their own policies.
The flag is on the wire so a client can explain the refusal before making the call rather than after.

Controlling access

Every operation checks a distinct IAM action. Operations on a specific secret are authorized against that secret’s CRN, with the secret’s tags available as condition context.
secrets:GetSecretValue is deliberately a different action from secrets:DescribeSecret, so the operation that returns sensitive material can be granted on its own, to the few principals that need it, on the few secrets they need. The same pattern shows up on KMS with kms:Decrypt, and on certificates with certificate:GetCertificateMaterial.
One service reading exactly the secrets under its own path prefix:
A rotation job that may write but never read:
secrets:ListSecrets is authorized against the collection rather than against individual secrets, so restricting it by CRN or tag has no effect. Scope secrets:GetSecretValue — that is the grant that matters.
See writing policies for the document format, the tag condition keys, and how a deny guardrail survives a broad allow.

Errors

Next

KMS

Creating the key a secret can be bound to, and what disabling it does.

Writing policies

Scoping secrets:GetSecretValue to a path prefix or a tag.

API reference

Every secrets operation, with request and response schemas.

Authentication

Signing requests to a regional endpoint.