Skip to main content

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.

TypeData requirements (on create, update, and restore)
OpaqueNo additional constraints
kubernetes.io/basic-authAt least one of username or password; empty is valid
kubernetes.io/ssh-authNonempty ssh-privatekey
kubernetes.io/tlsBoth 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": []
}
  • name must be a DNS subdomain (lowercase, a-z0-9-.); type defaults to Opaque; data defaults to {}; an extra stringData map 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 data to create/change binary values.
  • binaryData is a read-only response field; including it in a write request returns 400 rather than silently ignoring the intended values.
  • allowedGroups and allowedUsers each 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) → 403 when excluded, 503 when required policy is unavailable or invalid.
  • Answers 201 { "secret": SecretSummary }, stamping krypton.io/managed-by: krypton and krypton.io/version: 1.
  • 409 when the name already exists; 400 on invalid individual data maps or malformed JSON/UTF-8; 413 on 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"
}
  • resourceVersion is required (optimistic concurrency): a mismatch with the live object is 409 with a reload prompt, and a true write race at the K8s API is a second 409 path. Omitted description/allowedGroups/ allowedUsers keep 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 data to update metadata while preserving all stored bytes. If supplied, data replaces the entire data map; {} clears a mutable text Secret. binaryData is a response field, not a write input (400 if supplied); the create-only stringData alias is also rejected on PUT. A Secret containing binary values rejects any plaintext data replacement with 422; its UI data controls are read-only and metadata saves omit data.
  • 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/version and last-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-level version counter or a DELETE body. Missing/blank or duplicate values return 400.
  • keepHistory: optional true or false, default false; other or duplicate values return 400. true skips 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​

  1. List only eligible bound snapshots for the authorized live UID.
  2. 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.
  3. 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.
  4. 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.