Skip to main content

Security Model

Read this before production. Krypton makes secret management convenient, but it cannot make Kubernetes Secrets something they are not.

The etcd truth​

Out of the box, Kubernetes Secrets are base64-encoded, not encrypted, in etcd — anyone with etcd or broad API access can decode them. Krypton therefore:

  • Never logs secret values. Audit logs record key names only (auditLog() in lib/validation.ts); grep the logs to prove it.
  • Masks values in the UI until explicitly revealed per key.
  • Validates the 1 MiB per-object limit client- and server-side (the form meter blocks; forced API calls get 413).
  • Shows a warning banner until you confirm etcd encryption-at-rest is enabled (KRYPTON_ETCD_ENCRYPTED=true once the API server uses an encryption provider such as aescbc on OpenShift).

Secrets additionally benefit from strict RBAC, redacted kubectl output, and tmpfs (RAM-backed) volume mounts that never touch node disk.

warning

KRYPTON_ETCD_ENCRYPTED is an attestation, not a detector. Set it to true only after you have verified the API server encrypts Secret resources at rest.

Local value previews​

Live and historical text previews render in the browser. Markdown images are replaced with accessible descriptive text, so opening a preview does not fetch embedded resource URLs. Raw HTML, including image/iframe/script markup, remains inert text. Unsafe link schemes such as javascript: are not activatable links. Safe Markdown links, including GFM bare-URL autolinks, require a user action and open a new tab with noopener noreferrer; that explicit navigation contacts the destination.

Before Markdown parsing or syntax highlighting, the complete value is checked against the shared 131,072 UTF-16-code-unit budget (String.length). Above that inclusive limit, Preview shows escaped, untruncated raw text and a small notice. This bounds rich-preview input while retaining the full raw display. Preview/editor modules remain lazy-loaded, and Raw/Copy preserve the source text. Binary values continue to use the separate base64 viewer.

Authentication guarantees​

  • Sessions are stateless HS256 JWTs (krypton_session cookie, HttpOnly, Secure in production, SameSite=Lax, 8-hour default). There is no session store to steal — but SESSION_SECRET must be ≥32 random characters (openssl rand -hex 32) and must never be committed.
  • OIDC uses discovery + PKCE (openid-client). The issuer must exactly match the discovery document's issuer field.
  • http:// issuers fail closed: loopback only, OIDC_ALLOW_HTTP=true only, and rejected entirely when NODE_ENV=production. This exists for sandbox Dex, not for real IdPs.
  • AUTH_DISABLED=true skips SSO with a mock admin. Local development only. The public config endpoint exposes authDisabled so deployments can assert it is false.

Authorization rules​

  1. Admins (KRYPTON_ADMIN_GROUPS/KRYPTON_ADMIN_USERS) always pass.
  2. A secret with explicit annotations is decided by those alone (override).
  3. Otherwise the namespace fallback in ConfigMap krypton-access applies.
  4. With no rules anywhere, the secret is visible to all authenticated users (open by default, restricted once rules are added).

Absence must be confirmed: a ConfigMap 404 or valid empty rules are open; malformed/missing policy data and failed reads return 503 when namespace policy is required. An expired cache is never reused after refresh failure. Admins and explicit live-secret ACLs keep their precedence.

Until any admin is configured, every authenticated user is treated as admin (bootstrap mode) — set admins promptly. See Authorization for the full matrix.

Managed-only mode (default, KRYPTON_MANAGED_ONLY=true): secrets without krypton.io/managed-by: krypton are hidden entirely — excluded from lists, 404 on direct access (read, edit, delete, history, restore, restart) — so K8s-level secrets created outside Krypton can't be touched by accident. History objects are hidden from direct routes in both visibility modes, including for admins. Version/restore endpoints authorize the current live object first; a snapshot's copied ACL cannot bypass a live ACL revocation. History must also be bound to that live object's UID. Retained predecessors and unbound legacy snapshots cannot be accessed through a recreated name; legacy recovery requires operator review.

Concurrency and integrity​

  • PUT, restore, and DELETE require the caller's expected live metadata.resourceVersion; stale requests get 409 before history side effects. Native live UID/resourceVersion CAS protects restore/delete against in-flight changes, including ACL changes and same-name replacements.
  • Update/restore archive the raw current state in an immutable Opaque snapshot bound to its UID/resourceVersion before replacement. Existing snapshot names are accepted only after exact-source and actual immutable-content verification. Restore preserves current live access/lifecycle metadata. Retention is 1–100 eligible snapshots per UID; failed/pending pruning blocks replacement.
  • Snapshot/pruning/live writes are separate operations. A failed live replacement can leave pre-write history and completed pruning. DELETE cleans eligible history first, with live-revision rechecks and per-snapshot UID/RV deletes; cleanup failures leave the live authorization object available. Pending live finalizers produce 202, not a confirmed-deleted claim. Rechecks do not lock authorization across objects, and late history writes after the final inventory can leave copies requiring operator cleanup. Retained/quarantined/predecessor copies and external backups are outside this cleanup; it is not atomic erasure. Kubernetes writers can alter metadata or delete immutable objects; history is not a tamper-proof backup.
  • Secret names must be DNS subdomains (validated by k8sName); descriptions are capped at 1024 chars; payloads are zod-validated before touching the API.
  • Streamed JSON is bounded to 16 MiB before parsing; ACL storage arrays are bounded to 256 entries of 256 UTF-8 bytes each. Secret data limits still apply separately. Shared collection budgets reject incomplete/over-budget inventories. API responses and auth redirects send no-store; validation/upstream/OIDC errors use classified guidance without raw values/headers.

Least-privilege RBAC​

  • Default scope needs only secrets (full verbs, own namespace) + configmaps (read) — see app/k8s/role.yaml.
  • The apps rule (deployments/statefulsets/daemonsets: get, list, watch, patch) is required only when KRYPTON_RESTART_ENABLED=true. It ships commented out; enabling the flag without the rule fails closed at the API.
  • Unrestricted cluster scope needs namespace-list permission. A configured KRYPTON_NAMESPACES allowlist resolves/checks locally with no namespace-list calls; actual operations still need the corresponding resource RBAC in those namespaces.
  • Optional impersonation (KRYPTON_IMPERSONATION=true) requires the SA's impersonate permission for the session user and every group, with matching human RBAC subjects. Namespace/secret/history operations and workload lists and patches use that complete identity. The access-policy ConfigMap is read as the SA and cached independently of human RBAC. See the impersonation contract.

Operational checklist​

  • SESSION_SECRET is random, ≥32 chars, stored in a K8s Secret, never in git.
  • KRYPTON_ADMIN_GROUPS/KRYPTON_ADMIN_USERS are set (no bootstrap mode).
  • KRYPTON_ETCD_ENCRYPTED=true after verifying apiserver encryption.
  • AUTH_DISABLED=false and OIDC_ALLOW_HTTP=false in real environments.
  • Ingress/Route uses TLS; OIDC redirect URI matches https://<host>/api/auth/callback.
  • apps RBAC rule present iff KRYPTON_RESTART_ENABLED=true.
  • KRYPTON_MANAGED_ONLY=true (default) unless you need the legacy adoption flow.