Keystore #
The keystore is a node-local file of secrets referenced from
pizza.yml — API keys and tokens never have to live in the config
file itself. Entries resolve through ${keystore:NAME} references:
node:
embedding:
endpoints:
- name: openai
url: https://api.openai.com/v1/embeddings
api_key: ${keystore:openai}
The file is <config dir>/keystore.json — next to the config file it
serves, so one instance per config (multi-instance hosts keep separate
keystores) and it never enters the data directory’s lifecycle. It is
written 0600 and replaced atomically; entries never enter cluster
metadata, raft logs, or API responses — views answer with a masked
hint only (sk-…4f2a).
Resolution order for a config value: -E CLI overrides (not
substituted) > environment references (${VAR}) > keystore references
the literal value in the file. References fail fast at startup when the entry or variable is missing — see the configuration reference for the full syntax.
CLI #
Managed through subcommands — no config load, no server required;
-c/--config selects which keystore:
pizza keystore add <name> # hidden prompt, enter + confirm
pizza keystore add <name> - # one line from stdin (CI, scripts)
pizza keystore add-file <name> <file> # contents become the value
pizza keystore list # names + masked hints + timestamps
pizza keystore show <name> # print the value in the clear
pizza keystore remove <name>
pizza keystore path # which file this node would use
Examples:
# interactive — the value never touches shell history or the terminal
pizza keystore add openai
# from a pipe
echo "$OPENAI_API_KEY" | pizza keystore add openai -
# a certificate / key file
pizza keystore add-file snapshot_crt /etc/pizza/certs/tls.crt
addrefuses a literal value argument on purpose — arguments land in shell history; use the prompt or stdin.- Re-adding an existing name replaces the entry (rotation = re-add + reload).
- Keystore edits re-resolve into the running config on the next
reload (the console triggers one automatically);
only
pizza.ymlitself keeps read-once, restart-to-apply semantics. The embedding registry (see AI Services) keeps its own dynamic layer over the yml/keystore baseline without a restart.
Keystore API #
A masked view of the keystore file as it is on disk now — what the
${keystore:...} references resolve against (on boot, and again on
every reload). Values are write-only: responses carry a masked hint,
never the secret.
Get Keystore #
GET /_node/_local/keystore
On a peer node through the unified forward:
GET /_node/<node_id>/keystore
Response:
{
"path": "/etc/pizza/keystore.json",
"read_only": false,
"entries": [
{
"name": "openai",
"hint": "sk-…4f2a",
"created_at": "2026-09-27T14:00:00.123456+08:00"
}
]
}
Put Keystore Entry #
Add or replace one entry. On a read-only keystore the call fails with 400.
PUT /_node/_local/keystore/entries/<name>
Content-Type: application/json
{"value": "sk-..."}
The value is write-only — the response is the updated masked view (same shape as the GET above), never the stored value. On a peer node:
PUT /_node/<node_id>/keystore/entries/<name>
Delete Keystore Entry #
Remove one entry; 404 when the name does not exist. The response is the updated masked view.
DELETE /_node/_local/keystore/entries/<name>
The console entry points (PUT/DELETE) re-resolve the running
config’s keystore references immediately on success — a rotated
embedding key applies to the next inference call, a rotated join token
to the next join attempt. A reference whose entry was just removed
keeps its running value (the reload logs and reports it) until the
entry returns or the node restarts. The CLI remains the writer of
record for scripted setups; the HTTP surface exists for the console
(below) and is subject to the API port’s auth middleware like every
other endpoint.
Reload Keystore #
Re-reads keystore.json from disk and re-resolves the running config’s
${keystore:...} references — the pickup path for out-of-band changes:
CLI edits, or a mounted Secret replaced at its source. Works on a
read-only keystore (that is exactly the mounted-secret case).
POST /_node/_local/keystore/_reload
On a peer node:
POST /_node/<node_id>/keystore/_reload
Response — key names only, never values:
{
"entries": 3,
"read_only": false,
"references": 2,
"changed": ["node.embedding.endpoints", "catalog.join_token"],
"unchanged": 0,
"failed": []
}
references— config keys carrying${keystore:...}references;changed— keys whose re-resolved value differs from the running one;failed— references that could not be re-resolved (their entry is gone); each keeps the running value until the entry returns.
Only values that referenced the keystore hot-apply: pizza.yml itself
still changes at boot, environment references stay boot-frozen, and
-E CLI overrides are never re-resolved.
Console #
The keystore is node-local, so the web console manages it on the node it belongs to: open Nodes → (a node) → Keystore — a tab next to Configuration — for the same visual management as the CLI:
- the tab edits that node’s own
keystore.jsonin place; remote nodes are reached through the unified forward, so a multi-node cluster edits the right file by visiting each node’s page; - the entries table shows names, masked hints, and creation times — values are never displayed;
- Add entry / Replace prompt for the value twice (the confirm
guards typos) and show the
${keystore:NAME}reference to paste intopizza.yml; - delete removes an entry the same way
pizza keystore removewould; - Reload re-reads the file on the node and re-resolves the running config’s references — the pickup path for CLI edits and replaced mounted Secrets, and the only control that also works on a read-only keystore;
- a read-only keystore (a mounted Kubernetes Secret, say) renders the editing controls read-only instead of failing edits one by one.
Edits through either surface land in the same 0600 file; console edits
re-resolve into the running config immediately, other changes on the
next Reload — only pizza.yml itself still needs a restart.
Read-only mounts #
When keystore.json is not writable (a CSI-mounted Kubernetes Secret,
for example) the node starts normally and logs that the keystore is
read-only; keystore add/remove fail with a pointer to update the
file at its source. This makes “mount the Secret as the keystore” a
zero-integration way to feed secrets from an external secret manager —
and when the mount is replaced, Reload picks the new file up
without touching the node.