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()inlib/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=trueonce the API server uses an encryption provider such asaescbcon OpenShift).
Secrets additionally benefit from strict RBAC, redacted kubectl output, and
tmpfs (RAM-backed) volume mounts that never touch node disk.
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_sessioncookie,HttpOnly,Securein production,SameSite=Lax, 8-hour default). There is no session store to steal — butSESSION_SECRETmust 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'sissuerfield. http://issuers fail closed: loopback only,OIDC_ALLOW_HTTP=trueonly, and rejected entirely whenNODE_ENV=production. This exists for sandbox Dex, not for real IdPs.AUTH_DISABLED=trueskips SSO with a mock admin. Local development only. The public config endpoint exposesauthDisabledso deployments can assert it isfalse.
Authorization rules
- Admins (
KRYPTON_ADMIN_GROUPS/KRYPTON_ADMIN_USERS) always pass. - A secret with explicit annotations is decided by those alone (override).
- Otherwise the namespace fallback in ConfigMap
krypton-accessapplies. - 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) — seeapp/k8s/role.yaml. - The
appsrule (deployments/statefulsets/daemonsets: get, list, watch, patch) is required only whenKRYPTON_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_NAMESPACESallowlist 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'simpersonatepermission 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_SECRETis random, ≥32 chars, stored in a K8s Secret, never in git. -
KRYPTON_ADMIN_GROUPS/KRYPTON_ADMIN_USERSare set (no bootstrap mode). -
KRYPTON_ETCD_ENCRYPTED=trueafter verifying apiserver encryption. -
AUTH_DISABLED=falseandOIDC_ALLOW_HTTP=falsein real environments. - Ingress/Route uses TLS; OIDC redirect URI matches
https://<host>/api/auth/callback. -
appsRBAC rule present iffKRYPTON_RESTART_ENABLED=true. -
KRYPTON_MANAGED_ONLY=true(default) unless you need the legacy adoption flow.