Store a secret through the API
A document never carries a credential. It names one, api_key: ${secret:openai}, and each node resolves the name from wherever the
platform delivered it, in a fixed order:
- a file
openaiunder the node’ssecrets_dir(a mounted Kubernetes Secret, the operator’sCredential, External Secrets, a Vault agent); - an environment variable
openai(the chart’senvFrom); - the cluster’s secret store, this page.
The first two need somebody who can put a value on a node. The store is for the person who has a console and cannot.
$TOKEN is a JWT from the issuer this gateway trusts, there is no
admin secret to hold. pistra login caches one and pistra admin -url
presents it for you. The curl form is here because it is what a script
or an SDK sends. See
Sign in from the command line.
Store it
Section titled “Store it”Run this command:
$ curl -X PUT https://pistra-admin.example/admin/v1/secrets/openai \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"value":"sk-…"}'{"secret":"openai","written_at":"2026-08-27T12:00:00Z","applied":41}Or the console’s Secrets page. The name is letters, digits, dot, underscore and dash. The value is stored exactly as sent, untrimmed. Writing to a name that already exists replaces it, there is no precondition, because “set it to this” means the same thing whatever it held before.
If the response carries "shadowed_by": "file" or "env", the node
that took the write already has that name delivered locally, and the
local value is the one in effect there. The store never overrides a
delivery. GET /admin/v1/secrets lists every name this node resolves
and which layer answers it.
Reference it
Section titled “Reference it”Add this configuration:
providers: - preset: openai api_key: ${secret:openai}in any source, and in any of the fields that hold a credential:
api_key, client_secret, access_key_id, secret_access_key,
session_token, token, a reconcile provider’s admin_key, the
guardrails fpe_key and pseudonym_key, and a credential header in a
detector’s headers. The console’s credential fields offer the
names this node resolves as suggestions. A reference to a name nothing
delivers and nothing stores is refused at the source write, with the
PUT that would fix it in the message.
Rotate it
Section titled “Rotate it”Write the new value to the same name. Every node rebuilds, and every source that references the name serves the new value. No document changes and no revision moves, and the audit trail records who rotated what and when, never the value. Later delivering the same name as a file, moving to External Secrets, say, takes over on the next rebuild without touching the document either, because of the fixed order.
Remove it
Section titled “Remove it”Run this command:
$ curl -X DELETE https://pistra-admin.example/admin/v1/secrets/openai -H "Authorization: Bearer $TOKEN"A stored secret a source still references, and nothing else delivers, is refused with a 422 naming the reference. A source holding the credentials the providers name cannot be removed from under them for the same reason. Take the reference out first, or deliver the name another way.
What the store is, and is not
Section titled “What the store is, and is not”The value is sealed by the node that takes the write, before it becomes a raft entry. The seal is AES-GCM under a key derived from the cluster secret every member holds, the same barrier the ACME certificate storage uses. It rests on every node’s disk as ciphertext and is opened on each node on the way into that node’s snapshot. No operation returns it.
The key arrangement is Vault’s. A data key seals values, and the data
key is held in a replicated keyring wrapped under one or more
key-encryption keys, by default the cluster secret’s, optionally a
key file’s (cluster.kek_file). Rotating either is three small writes
and never touches a sealed value. See Rotate the cluster
secret.
It is not a secrets manager. There are no versions, no leases, no
dynamic credentials, and no key that never rests on a node. A KMS KEK
is the shape the keyring is built for and does not have yet. A
deployment with Vault or External Secrets should keep delivering
files. Its documents already say ${secret:NAME}, and the store never
answers.
Policy
Section titled “Policy”The store is its own resource class, secrets, with the secret’s
name as the instance:
@id("platform-holds-the-keys")permit(principal in Role::"platform", action, resource in Resource::"secrets");@id("team-a-rotates-its-own")permit(principal in Role::"team-a", action == Action::"putSecret", resource == Resource::"secrets/team-a-openai");A grant on config does not carry it. Being allowed to write a team’s
source is not being allowed to overwrite the platform’s provider keys.