Architecture
Krypton is a stateless Next.js App Router service. All persistent state lives in the Kubernetes API; the app holds no database connection or disk cache. Namespace policy uses a 60-second in-memory ConfigMap read-through cache.
Request flow
Browser ──cookie (HS256 session JWT)──▶ Next.js route handler
│ 1. requireUser() → SessionUser
│ 2. assertNamespaceAllowed()
│ 3. loadAuthorizedLiveSecret()
│ ├─ read current Secret via CoreV1Api
│ ├─ live visibility (404 for history; managed-only gate)
│ └─ canAccessSecret() (admin → ACL → policy → confirmed open)
▼
Kubernetes API (ServiceAccount token in-cluster,
~/.kube/config locally)
Named-secret GET/PUT/DELETE/history/restore/restart handlers authenticate the
session and check namespace scope before using the shared loader in
lib/secret-access.ts. It rejects history objects in both managed-only modes,
then evaluates current live authorization before any history access or mutation.
Lists use the same live-visibility predicate; creation uses namespace policy alone.
Restart also requires its deployment feature flag. Visibility/ACL denials and
successful mutations use metadata-only audit events.
The loader composes authz.ts, secrets.ts, and HTTP errors; the policy and secret
storage modules do not import the loader. This keeps route enforcement shared
without a dependency cycle through storage and authorization.
Namespace policy cache (lib/authz.ts)
- Admins and explicit live-secret ACLs are decided before namespace fallback.
- Only a ConfigMap read returning 404 means absence. Valid empty/omitted rules
remain intentionally open. Missing payload, invalid JSON/schema, and read
failures become a controlled
503 Namespace access policy is unavailable. - Successful policy reads and confirmed absence are cached per process for 60s from completion. Concurrent misses share one pending read. Expired entries are discarded before refresh; errors neither revive stale policy nor populate the cache, and the next request retries.
- Non-admin collection requests also need namespace policy for the
canCreatehint, so an outage returns 503 for the whole list rather than partial results. - Failure logs contain only a category and optional numeric HTTP status, never ConfigMap payloads, SDK error objects, or transport headers.
Kubernetes client (lib/k8s.ts)
- In-cluster: uses the auto-mounted ServiceAccount token
(
/var/run/secrets/kubernetes.io/serviceaccount/token) vialoadFromCluster(). No credentials to manage; RBAC comes from the boundRole/ClusterRole. - Local dev: falls back to
loadFromDefault()(~/.kube/config). - Optional impersonation:
KRYPTON_IMPERSONATION=truesends the session email and every group for human-authorized requests, including workload lists/patches. A per-request SDKhttpApitransport uses raw repeated group headers and retains the SDK's TLS/auth configuration. The SA needs impersonation permission for the corresponding human RBAC subjects. The access-policy ConfigMap remains a service read with the shared cache above. Default isfalse(hybrid mode); explicitAUTH_DISABLED=truebypasses human impersonation. - Error normalization: the shared numeric-only status classifier produces
fixed
Kubernetes API (status): guidanceresponses. It never unwraps raw Status messages, bodies, or headers. A 403 can be human RBAC/SA impersonation permission denial or hybrid ServiceAccount RBAC; guidance refers to the effective identity. - Collections:
lib/k8s-list.tscompletes serial 100-object pages with finite aggregate limits. Live reads exclude history server-side without replacing annotation-based managed visibility or ACLs. UID-bound history and each workload kind share the reader and preserve full per-user request options. Missing/invalid pages, bad/repeated tokens, or exhaustion fail instead of returning partial results. Metadata cleanup sizes/fingerprints projections without visiting Secret values. The SDK buffers each upstream response first; budgets bound application work/accumulation, not streaming transport responses. - HTTP:
lib/http.tsbounds streamed JSON to 16 MiB, sanitizes validation/error envelopes, and explicitly marks API JSON/auth redirectsCache-Control: no-store.
Scopes (lib/namespaces.ts)
| Scope | Sees | RBAC |
|---|---|---|
namespace (default) | Only the pod's own namespace | Role + binding |
cluster, allowlisted | Configured names, checked locally | Resource permissions in those namespaces |
cluster, unrestricted | Complete bounded namespace inventory | Cluster namespace-list + resource permissions |
KRYPTON_NAMESPACES="team-a,team-b" resolves without namespace-list calls. Names are
deduplicated/sorted; actual resource calls still enforce existence and RBAC.
Unrestricted discovery errors surface explicitly instead of falling back to an
own-namespace subset. Policy lookups use own properties, including constructor.
Versioning model (lib/snapshot-store.ts)
- The focused store owns raw/metadata records, identity checks, retry verification,
and cleanup.
secrets.tsconverts raw records to decoded response DTOs only at the authorized route boundary; restore copies raw data and cleanup reads metadata. krypton.io/versionstarts at 1. Adoption treats missing/old-zero counters as logical v1: archive v1, write v2, then archive v2, write v3. PUT/restore supply their loaded resourceVersion; update/restore replacements retain the live UID/RV for native CAS.- New snapshots are immutable Opaque Secrets named
krypton-h-<UID hash>-vN(at most 60 characters). Boundedsource-uidandsource-resource-versionhash labels support valid indexing for long live names and opaque revisions. Full UID, source resourceVersion, original name/type, format, version, actor/time, and source description/ACL annotations are checked; live controller/lifecycle metadata is not copied. - A snapshot-create 409 is a retry only when the occupied object has the same full source identity and actual immutable byte/key content. Replica retries share that archive, then race through Kubernetes CAS. Different-source collisions block writing.
KRYPTON_HISTORY_LIMITis 1–100, default 20. Pruning uses serial UID/RV-conditional deletes, confirms absence, and reports failed/pending cleanup before permitting replacement. Snapshots and pruning can persist after a failed live write; these pre-write captures are not a transaction or an external-change watcher.- History is exposed only through an authorized live secret's version/restore endpoints. Snapshot names cannot be used as live route targets, and copied snapshot ACLs do not determine current access. List/restore/prune/delete require the live UID; retained predecessors never attach to a recreated name. Legacy and malformed history is quarantined with no automatic name-based migration.
lib/secret-mutations.tschecks caller revisions and orchestrates live deletion. DELETE requires the expected resourceVersion, cleans bound history first, and rechecks the authorized live UID/RV before each cleanup mutation and before its native UID/RV-conditional delete. Cleanup failures leave the live object available for retry. A final read distinguishes confirmed absence (200) from pending live finalizers (202); uncertain outcomes are errors.- These are not cross-object transactions: earlier cleanup can survive conflicts, and a concurrent writer can add a snapshot after the last inventory. Retained, predecessor, legacy/quarantined, and late copies can require operator cleanup. See the history guide.
lib/secret-queries.tsprovides explicit client recovery that bypasses query staleTime and HTTP caching. Restore/delete conflicts await fresh live/history data; failed reloads retain the error. Confirmed deletion cancels reads and removes detail/history caches; pending deletion refreshes them without navigation.
Consumer restarts
Environment-variable consumers need new containers; ordinary Secret/projected
mounts usually sync after a kubelet delay (subPath and application file reloads
need special care). lib/consumers.ts scans Deployment, StatefulSet, and DaemonSet
templates for exact Secret names in env/envFrom on normal/init/ephemeral
containers, normal/projected Secret volumes, and imagePullSecrets. It then
requests rollouts with a krypton.io/restarted-at JSON-Patch pod-template patch,
bound to each inspected workload UID/resourceVersion via test preconditions and
allowing at most four patches in flight per request across all kinds. Lists and
patches retain the full request-local impersonated identity.
Only acknowledged patches are successful requests. Typed results distinguish
requested (200), complete no_targets (200), partial (207), and failed (503).
Failed kind inventories are unknown and never count as empty; other kinds may
still be patched. Audit and UI preserve sanitized per-workload and discovery
failures. Rollout completion, health, and credential uptake are not checked.
See consumer rollout limits and retries.
The feature is disabled by default
(KRYPTON_RESTART_ENABLED=false: button hidden, API returns 403, no apps
permissions needed).
Project layout
app/ # Next.js App Router
app/api/namespaces/… # secrets CRUD, versions, restore, restart-consumers
app/api/auth/… # OIDC login/callback/logout/me
app/secrets/[ns]/… # detail / edit / history views
lib/ # k8s client, OIDC+session, authz, snapshots, consumers, validation
components/ # dashboard, tables, forms, masked values, history
k8s/ # namespace, SA, Role/ClusterRole, configs, deployment
helm/ # helm/main chart + helm/krypton subchart
sandbox/ # kind cluster + Dex + fixtures + helper scripts
website/ # this documentation site (Docusaurus)
Related pages
- Security Model for guarantees and hardening.
- Authentication and Authorization for the SSO + hybrid authz design.
- Versioning & History for snapshots, conflicts, and restores.