Secrets API
Text values cross the API boundary as plain strings. Non-text values have an
explicit base64 representation in authorized detail/history responses.
All routes require a session and enforce namespace scope. Named-secret
routes then enforce live visibility (404 for history objects in either mode, and
for unmanaged secrets when KRYPTON_MANAGED_ONLY=true) → hybrid authorization
(403 for denied rules; 503 if required namespace policy is unavailable).
Success and error responses use Cache-Control: no-store. Shared
request/collection budgets apply.
Shapes
SecretSummary (lists, creates):
{
"namespace": "krypton-test",
"name": "database-credentials",
"type": "Opaque",
"keys": ["DATABASE_URL", "DB_PASSWORD"],
"keyCount": 2,
"description": "Primary database credentials",
"version": 3,
"resourceVersion": "123456",
"creationTimestamp": "2026-01-04T12:00:00Z",
"allowedGroups": ["dev-team"],
"allowedUsers": [],
"managedByKrypton": true,
"immutable": false,
"binaryKeys": []
}
SecretDetail (read, update, restore) adds disjoint value maps:
{
"secret": {
"...summary fields": "...",
"keys": ["DATABASE_URL", "archive.bin"],
"binaryKeys": ["archive.bin"],
"data": { "DATABASE_URL": "postgres://…" },
"binaryData": { "archive.bin": "AP8=" }
}
}
data contains only losslessly decoded Unicode text. Non-UTF-8 bytes and binary
controls (U+0000–0008, U+000B–000C, U+000E–001F, U+007F) appear only in
binaryData, using their original Kubernetes base64. Tabs, CR/LF, Unicode BOMs,
empty strings, and literal � remain text. keys/keyCount cover both maps;
binaryKeys names the latter. Summaries include no values in either encoding.
The UI masks both kinds and provides an explicitly labeled read-only base64
viewer/copy control for binary data.
Summary/detail responses also include optional deletionTimestamp while Kubernetes
deletion is pending. Its presence does not mean the Secret has disappeared.
Shared key and type rules
Keys are 1–253 ASCII characters from letters, digits, ., _, and -. . and
keys beginning with .. are invalid. The valid keys __proto__, constructor,
and toString are preserved as own properties through JSON, validation, encoding,
and forms. Client code should use Object.hasOwn, Object.entries, and safe map
construction instead of inherited-property checks or prototype-setting assignment.
| Type | Data requirements (on create, update, and restore) |
|---|---|
Opaque | No additional constraints |
kubernetes.io/basic-auth | At least one of username or password; empty is valid |
kubernetes.io/ssh-auth | Nonempty ssh-privatekey |
kubernetes.io/tls | Both tls.crt and tls.key; empty is valid, contents are not verified |
kubernetes.io/dockerconfigjson | .dockerconfigjson: a JSON object or null |
kubernetes.io/dockercfg | .dockercfg: a JSON object or null |
These checks match native Kubernetes data rules, not credential validity.
Additional keys are allowed. Custom types have no application allowlist or
64-character cap; bootstrap token types retain Kubernetes' unconstrained data
semantics. Empty create types default to Opaque.
kubernetes.io/service-account-token creation, editing, and restore are unsupported.
Create, PUT (even metadata-only), and restore return 422 with TokenRequest/controller guidance
before history or live writes. Their controller-owned lifecycle must be managed
through native token APIs/controllers; the Opaque archive format does not enable
token mutation support. Deletion retains the normal authorization policy.
List secrets
GET /api/namespaces/:namespace/secrets
{
"namespace": "krypton-test",
"secrets": [{ "...": "SecretSummary[]" }],
"canCreate": true
}
History snapshots are excluded at Kubernetes with <annotation-prefix>/kind!=history.
The server follows all pages within 5,000 objects / 32 MiB / 100 pages, keeping
complete per-user options. Annotation-only managed Secrets remain eligible: the
selector does not require a managed label. Current live ACLs and the managed-by
annotation are checked after collection, including in legacy mode. Identical
duplicates appear once; failed/inconsistent/over-budget lists return an error, not
a truncated result. Unmanaged secrets are excluded when
KRYPTON_MANAGED_ONLY=true. canCreate is a UX hint (namespace rules only) —
POST still enforces server-side.
For non-admins, an unavailable/invalid namespace policy returns
503 { "error": "Namespace access policy is unavailable" } for the whole list,
even if there are no items or all items have explicit ACLs: canCreate requires
namespace policy. No partial list or guessed creation hint is returned.
Create secret
POST /api/namespaces/:namespace/secrets
{
"name": "demo-secret",
"type": "Opaque",
"data": { "API_KEY": "<api-key-value>" },
"description": "Demo",
"allowedGroups": ["dev-team"],
"allowedUsers": []
}
namemust be a DNS subdomain (lowercase,a-z0-9-.);typedefaults toOpaque;datadefaults to{}; an extrastringDatamap is merged in, taking precedence for overlapping keys. Both maps are plaintext, not base64.- Invalid Unicode, binary controls, and invalid keys return 400; built-in data
requirements are checked after the merge and return 422. Use Kubernetes tooling
with base64
datato create/change binary values. binaryDatais a read-only response field; including it in a write request returns 400 rather than silently ignoring the intended values.allowedGroupsandallowedUserseach allow at most 256 entries of at most 256 UTF-8 bytes before trimming, including surrounding whitespace. Entries are trimmed; comma-containing, empty, whitespace-only, or oversized entries return 400 before writes, without echoing values. Internal whitespace/case and array order/duplicates are preserved. Groups match exactly; user emails ignore case. Native annotations are comma-separated with no comma quoting/escaping, so["team-a,team-b"]is rejected rather than expanded into two principals.- Omitted create ACLs default to
[]; empty arrays store empty annotations. If both are empty, namespace fallback applies and may intentionally be open. - Creation is gated by namespace rules alone (the secret has no annotations
yet) →
403when excluded,503when required policy is unavailable or invalid. - Answers
201 { "secret": SecretSummary }, stampingkrypton.io/managed-by: kryptonandkrypton.io/version: 1. 409when the name already exists;400on invalid individual data maps or malformed JSON/UTF-8;413on an over-16-MiB stream/envelope or an oversized merged data map. The native/conservative data limit remains separate from JSON escaping overhead; see the shared budgets.
Read secret
GET /api/namespaces/:namespace/secrets/:name
Answers { "secret": SecretDetail } — text data and base64 binaryData included. 403 when the
user fails the secret / namespace rules. Admins and explicit live ACLs take
precedence over namespace policy availability; fallback decisions return 503
on policy failure. Only confirmed 404 absence or valid empty rules are open.
Using a snapshot name as :name returns 404 for every named-secret operation,
including history, restore, and restart. Use the live name's /versions or
/restore endpoint; authorization always uses the current live object.
Update secret
PUT /api/namespaces/:namespace/secrets/:name
{
"data": { "API_KEY": "<rotated-value>" },
"description": "Demo",
"allowedGroups": ["dev-team"],
"allowedUsers": [],
"resourceVersion": "123456"
}
resourceVersionis required (optimistic concurrency): a mismatch with the live object is409with a reload prompt, and a true write race at the K8s API is a second409path. Omitteddescription/allowedGroups/allowedUserskeep their previous values.- Submitted ACL arrays use the same validation and trimming as create, before any
snapshot or live write.
[]clears just that annotation; omitted fields retain their stored values without rewriting them. Clearing both selects namespace fallback. - Omit
datato update metadata while preserving all stored bytes. If supplied,datareplaces the entire data map;{}clears a mutable text Secret.binaryDatais a response field, not a write input (400 if supplied); the create-onlystringDataalias is also rejected on PUT. A Secret containing binary values rejects any plaintextdatareplacement with 422; its UI data controls are read-only and metadata saves omitdata. - Immutable Secrets accept metadata-only updates or byte-identical data. Additions, removals, and changed values return 422 before snapshots/writes. Create a new Secret and update consumers to change immutable data.
- Preserves live type, UID, owner references, finalizers, immutable state, and other native lifecycle metadata. The API does not expose a way to unset immutable.
- After validation, snapshots the current state (reversible history), then replaces the
live object with a bumped
krypton.io/versionandlast-modified-by. - Snapshots are immutable Opaque archives bound to the live UID and exact source resourceVersion, with raw native data and the original type annotation. A create 409 is accepted only after verifying the occupied archive's binding and actual immutable content. Different-source/unrelated collisions return 409 before replacement; history creation/pruning failures return 503 before replacement. Snapshot/pruning effects can remain if the subsequent live CAS write fails.
- Adoption treats an absent/old-zero counter as v1: first edit saves v1 and writes v2, next edit saves v2 and writes v3. Native intermediate edits are not watched. A native edit after a failed snapshot-backed write can require reviewed counter recovery; see history semantics.
- Answers
{ "secret": SecretDetail }; the shared input/data budgets apply before snapshots or writes, including streamed 400/413 handling.
Metadata-only example:
{ "description": "Updated operator notes", "resourceVersion": "123456" }
Upgrade semantics
Re-fetch DTOs after upgrading. Clients that assume every keys entry exists in
plaintext data must handle binaryKeys/binaryData; they must not submit those
base64 values as plaintext. Omit data for binary/immutable metadata edits.
Previously unchecked built-in payloads now fail before side effects, and
controller-owned service-account token writes fail explicitly. The upgrade does
not rewrite existing bytes; earlier lossy writes require an intact history copy
or external backup for recovery.
Previously accepted comma-containing and whitespace-only ACL entries now return 400. Existing annotations retain their CSV split/trim/drop-blank read semantics; there is no format migration or automatic recovery of earlier array intent. Review affected live ACLs and update API callers/all writer replicas. Namespace ConfigMap JSON arrays still support exact comma-containing principals. The UI still parses comma-list text into separate nonblank entries; see ACL representation and upgrade details.
Delete secret
DELETE /api/namespaces/:namespace/secrets/:name?resourceVersion=123456[&keepHistory=true]
Required query contract
resourceVersion: required, nonblank opaque string from the loaded live detail. URL-encode it; do not send the app-levelversioncounter or a DELETE body. Missing/blank or duplicate values return 400.keepHistory: optionaltrueorfalse, default false; other or duplicate values return 400.trueskips all history listing/deletion and retains copies for operator recovery.- After live authorization, a stale revision returns 409 before history access or any mutation. Each retry must be an explicit intent based on reloaded data. A fresh authorization denial still returns 403; hidden live objects return 404.
Ordered cleanup and native preconditions
- List only eligible bound snapshots for the authorized live UID.
- Recheck that live UID/resourceVersion before each snapshot delete. Delete each snapshot with its own UID/resourceVersion preconditions, then confirm absence. A final eligible inventory must be empty before proceeding.
- Recheck the authorized live revision, then DELETE with live UID and resourceVersion preconditions. These protect a concurrent ACL/data change or same-name replacement even if it races the recheck.
- Confirm live absence by reading the name. A successful Kubernetes DELETE alone is insufficient: finalizers may leave a terminating Secret.
History list/delete/absence-check failures, still-present snapshots, or newly added history return 503 without attempting live deletion. Snapshot precondition conflicts and changed/disappeared live objects during the operation return 409. Cleanup may already be partial, but this request leaves the live authorization object available if cleanup fails, allowing a fresh request to retry remaining work. If a live deletion itself fails or cannot be confirmed, the result is an error; its outcome may be uncertain, so reload before retrying.
Result union
200, confirmed absent after deletion:
{ "status": "deleted", "name": "demo-secret", "deleted": "demo-secret", "uid": "source-uid", "history": "cleaned" }
202, deletion requested but the same UID is still present:
{ "status": "pending", "name": "demo-secret", "uid": "source-uid", "resourceVersion": "123457", "deletionTimestamp": "2026-10-12T12:00:00.000Z", "history": "cleaned" }
deletionTimestamp is optional. The pending result has no deleted field;
clients must not report permanent deletion or remove the live row on 202. GET/list
also report the optional timestamp. Reload to check progress and obtain the latest
resourceVersion, including changes caused by finalizers. A different UID observed
after the accepted DELETE returns 409 rather than reporting that replacement as deleted.
history is "retained" for keepHistory=true, otherwise "cleaned". Cleaned
means the eligible inventory was confirmed empty before live deletion, not an
atomic erase of all copies. Snapshot cleanup and live deletion are separate
Kubernetes requests. Earlier deletions are not rolled back after a later conflict;
a concurrent writer holding an earlier live read may create history after the
last inventory, even after live deletion. Rechecks do not lock authorization
across objects. Such leftovers need operator cleanup if the live object is gone.
Predecessor UIDs, kept history, quarantined legacy/malformed objects, and external
backups are outside this cleanup. Retained snapshots never attach to a recreated name.
Upgrade and UI recovery
Older clients must supply the expected revision and handle the 200/202 result union. The UI offers Reload latest on errors: it fetches fresh live and history data, then closes the confirmation for review; a failed reload retains the error. Confirmed deletion cancels outstanding reads, removes the relevant detail/history caches, and refreshes the namespace list. Pending deletion refreshes those queries and remains visible as pending.
Related pages
- Versions and Restore for history.
- Authorization for the 403/404 gating order.