Security Model¶
KubeManta is secure by default: a vanilla install cannot change your workloads, has no cluster-admin access, and sends no cluster data outbound.
Stated precisely, because "read-only" would overstate it. A default install holds no write verb on anything you run — no create, update, patch or delete on pods, deployments, or any other workload kind. It does write inside its own namespace, and only there: it manages its own Kubernetes Secrets (its session and encryption material) and the installer patches its own Deployment. Nothing in that reaches your applications.
Authentication¶
| Layer | Detail |
|---|---|
| Login | Password authentication — passwords are bcrypt-hashed, never stored in plaintext. Sessions use signed tokens with an 8-hour idle timeout (security.sessionIdleTimeout) and a 7-day absolute ceiling; whichever is stricter applies. |
| Recovery | kubectl exec -it -n kubemanta-system deploy/kubemanta-agent -- python3 -m app.recover_admin --username <you> --password-stdin — resets the password and also clears a lost second factor, a deactivated account, IP bans, and disabled password login, since all of those look identical at the login form. |
| Lockout | 6 failed attempts against one account from one address triggers a 15-minute lockout. Failed passwords never ban an address: the only automatic IP ban (auth_flood, in Admin → Access Control) counts forged or malformed session tokens — never a missing cookie and never a benignly expired session — so no honest client can trigger it. Lockout is deliberately not keyed on the address alone — your users typically share one office egress, VPN concentrator or ingress controller, so an address-only threshold eventually locks out everyone behind it over one person's bad password. Volumetric defence belongs on your own edge or WAF. |
| Second factor | TOTP, available to local accounts and — off by default — to LDAP accounts. Codes are single-use (a replay inside the same time window is refused) with a ±30s clock tolerance. Enrolment is not active until one code is confirmed, so a mis-scanned QR cannot lock an account out. SAML/OIDC accounts are not offered it: the identity provider enforces MFA and KubeManta does not ask for a second code on top. TOTP is the only factor offered — email, SMS and push each depend on something outside the cluster and fail during exactly the outage a break-glass second factor exists for. See Users & Access. |
| Programmatic access | Per-user API keys (X-API-Key header), created per account in Admin → Users. No shared secret exists. See Programmatic access. |
RBAC — read-only by default¶
| ServiceAccount | Role | When active |
|---|---|---|
kubemanta |
kubemanta-reader (ClusterRole) |
Default — get/list/watch on pods, logs, services, configmaps, events, deployments, statefulsets, daemonsets, ingresses, jobs, cronjobs, nodes, namespaces. With rbac.clusterWide=false the workload half becomes per-namespace Roles over tenantNamespaces instead, and the ClusterRole keeps only genuinely cluster-scoped reads (nodes, namespace list, PersistentVolumes, CRDs, cluster RBAC metadata). |
kubemanta |
+ write verbs | Only when rbac.allowWrites=true: adds pods create/delete, pods/exec, secrets create/update/patch, deployments patch/update, namespaces create — all of it across the namespaces the reader can see |
kubemanta |
own-namespace Role | Always. Full Secret CRUD scoped to KubeManta's OWN namespace, for its session and encryption material. It cannot reach a Secret in yours at any setting unless rbac.allowWrites is on. |
kubemanta-expert |
cluster-admin |
Only when expertMode.enabled=true. The token lives only in a separate, isolated pod — the main agent pod never mounts it. |
Secrets read (get/list on secrets) is gated by rbac.readSecrets, default true. An opt-in default here would be incoherent rather than strict: Secret write rides rbac.allowWrites, not this flag, so an install could create and patch a Secret it was forbidden to read. Secret values remain protected on the paths that matter — every AI/MCP tool result is redacted at a single boundary before it can reach a model. Set false to withhold the grant, or scope it to named namespaces with rbac.secretsNamespaces. Secret DELETE is never granted at any setting.
rbac.readSecrets is not the only sensitive-data switch
Pod logs and ConfigMaps are readable by default and routinely carry
tokens, connection strings and keys — the same material a Secret holds. If
that data must not be reachable, scope the install with
rbac.clusterWide=false + tenantNamespaces rather than relying on
the Secret flag alone. That mode emits per-namespace Roles, so the API
server itself refuses all workload data outside the namespaces you list.
Note the two scoping controls are different boundaries. KubeManta's
namespace allowlist filters its own HTTP, AI and MCP surfaces — a real 403,
but enforced by the product. The browser terminal is a real shell, so
kubectl there is bounded by the ServiceAccount's Kubernetes permissions,
not by that allowlist. A boundary that must hold against a terminal user has
to be expressed in RBAC.
Namespace blocklist¶
The following namespaces are always hidden from the UI and excluded from RBAC — they cannot be observed, diagnosed, or modified through KubeManta regardless of settings:
kube-system · kube-public · kube-node-lease · istio-system · paas-core · cert-manager · ingress-nginx · traefik · monitoring · logging · flux-system · argo · argocd · kubemanta-system
All namespaces with the prefixes gke-*, gmp-*, kube-* are also blocked by prefix match.
Programmatic access¶
API access and the MCP server authenticate with per-user API keys. There is no
shared secret: the previous security.apiKey Helm value was removed because a
single cluster-wide token could not be attributed to a person, could not be revoked
without a redeploy, and bypassed the seat model.
Creating a key. An admin opens Admin → Users, expands a user's API keys, and creates one. The secret is displayed exactly once — only a SHA-256 hash is stored, so a lost key must be revoked and replaced rather than recovered.
Format is km_<prefix>_<secret>: a 160-bit CSPRNG secret, looked up by the indexed
prefix and compared with a constant-time check.
A key always acts as its owner, right now. The owner's role is read from the
users table on every request:
| What you do to the user | What happens to their existing keys |
|---|---|
| Demote admin → viewer | Keys survive and immediately act as a viewer |
| Promote viewer → admin | Keys immediately regain admin reach |
| Deactivate the user | Every key returns 401 |
| Revoke one key | That key returns 401; the owner's other keys are unaffected |
Viewers may hold keys — they are simply read-only.
Keys can never manage identity or billing. Even for an admin owner, a key is
refused (403 api_key_forbidden) on /admin/users*, /admin/api-keys*,
/me/api-keys*, /admin/sso*, /admin/sessions*, /admin/license*, and writes to
/admin/seats. This is enforced in middleware, before any handler runs, so a stolen
key cannot mint another key or escalate its own account.
Every API-key request is attributed in the audit log as <username>@<ip>, which is
what makes credential sharing visible.
For what a key can actually do — the endpoints behind each screen, with request and response examples, and the full refusal list — see the API reference.
AI key handling¶
LLM API keys are passed per-request in the request body. They are:
- Never written to disk or SQLite
- Never logged
- Never sent to KubeManta infrastructure
The PTY terminal is BYO-LLM: ANTHROPIC_API_KEY (and every other secret-bearing variable) is stripped from the terminal environment before a session spawns. Claude Code in the terminal authenticates per-user via its own claude OAuth login (persisted in ~/.claude on the PVC) or a key the operator exports themselves. A server-side Anthropic key (Helm agent.agentApiKey, stored in a Kubernetes Secret) is used only by the agent's autonomous alert investigation and scheduled reports — never exposed to terminal sessions.
Three-pod isolation¶
KubeManta splits privilege across three distinct security contexts so that no single compromised process holds everything:
| Pod / context | Holds | Reaches |
|---|---|---|
| Agent | The database, encryption key, session secrets, SSO config | Read-only reader ServiceAccount (writes only with rbac.allowWrites) |
| Expert-exec pod | The cluster-admin token (only when expertMode.enabled=true) |
Full cluster — but only via bearer-authenticated calls from the agent over a NetworkPolicy-restricted hop, using an ephemeral kubeconfig that is discarded after every operation |
| Terminal sidecar | Only its own storage (Claude Code session + caches) | No cluster-admin token, no database, no encryption key — runs as a distinct, lower-privilege user in its own process namespace |
So the cluster-admin token is never in the agent pod, the terminal process cannot reach the agent's secrets or database, and privileged operations are delegated over an authenticated, network-policy-restricted hop. The runtime Expert-Mode toggle is enforced on both the privileged HTTP/Helm endpoints and the AI agent write tools.
The terminal WebSocket is authenticated before the connection is accepted — an unauthenticated upgrade is refused, no session spawned — and is subject to the same IP access control as the rest of the product.
Autonomous agent write safety¶
When the AI agent is asked to perform a write operation (delete pod, restart deployment, scale):
- Normal mode (default): the agent pauses and waits for explicit human approval in the UI before executing
- Auto mode: executes writes without approval — requires explicit user opt-in; never the default
- Blast radius limit: hard cap of 5 write operations per session (
agent.blastRadiusLimit) - Circuit breaker: trips after 3 consecutive write failures (
agent.circuitBreakerThreshold); blocks all writes until auto-reset (agent.circuitBreakerResetSecs = 300s) - Max remediation cycles: 2 autonomous cycles per app per hour (anti-flap throttle)
Data storage¶
| Data | Where stored | Notes |
|---|---|---|
| Workload data, logs, events | Never stored — fetched live from the K8s API | |
| Audit log, alert history, settings | Embedded database on the PVC | In-cluster only |
| Terminal home (Claude Code session, kubeconfig, history) | PVC | Persistent across pod restarts |
| Cloud credentials (AWS/GCP/Azure) | Embedded database, AES-256-GCM encrypted | Masked •••• in all API responses |
| Helm config/data/cache | PVC | |
| LLM API keys | Never persisted | Per-request only |
| Admin password | bcrypt hash, in-cluster only |
Encryption at rest¶
Cloud credentials, Helm registry passwords, the AI provider key and integration secrets are encrypted with AES-256-GCM before being written to the embedded database.
The AI provider key is worth naming explicitly, because an earlier version of
this page said KubeManta stores no LLM keys. It does — encrypted, server-side,
and deliberately. GET /settings/ai no longer returns the key at all, so it is
never handed back to a browser and a bearer token cannot take it with one GET.
The agent keeps its own encrypted copy instead, which is the copy it was always
going to send to the provider. The key stops being distributed to every browser
that signs in; it does not stop existing. If you would rather KubeManta held no
provider key at any time, point it at a local Ollama, which needs none.
A random 256-bit encryption key is generated on first install, stored in a
Kubernetes Secret, and preserved across upgrades and uninstalls. You can supply
your own key (bring-your-own-key) via security.encryptionKey.
To disable (plaintext storage): --set security.encryptionAtRest=false (not recommended).
Pod hardening¶
| Control | Detail |
|---|---|
| seccompProfile | RuntimeDefault on all pods |
| PodSecurity | baseline enforce + restricted warn on kubemanta-system (applied manually via kubectl label namespace) |
| NetworkPolicy | Blocks all egress to 169.254.169.254 (cloud Instance Metadata Service) from all pods in kubemanta-system. Disable with security.blockImds=false only if your workloads need IMDS for Workload Identity bootstrap. |
| readOnlyRootFilesystem | true on the UI container; /tmp overlay via emptyDir |
| Non-root | Every container runs as a non-root user; the terminal sidecar runs as a distinct, lower-privilege user in its own process namespace |
Supply-chain integrity¶
All published images (kubemanta-agent, kubemanta-ui) are keyless-signed with cosign by digest and ship an SPDX SBOM attestation, both bound to the GitHub Actions OIDC identity. You can cosign verify any image before deploying it. See Supply Chain.
Tamper-evident audit¶
Every audited action is recorded in an append-only audit log that is hash-chained — each entry cryptographically covers the one before it, so any edit or interior deletion is detectable on demand, with no configuration. Actor attribution is derived from the real client IP (security.trustedProxyCidrs governs when X-Forwarded-For is trusted) and is email-aware for SSO sessions. An optional off-box WORM export (audit.worm) writes each entry to your own Object-Lock S3 bucket to close the trimming-the-newest-rows gap. See Activity & Audit.
Access & exposure¶
An in-product IP access layer (allowlist / denylist / durable auto-ban), a sliding session idle-timeout, and a service-only exposure mode let you front KubeManta with your own edge/WAF. All Free. See Access Control.
Enterprise SSO¶
Authenticate operators through your own identity provider — SAML 2.0, OIDC, or LDAP — with JIT provisioning and group→role mapping; the local admin password is always retained as break-glass. See Enterprise SSO.
AI Guardrails¶
Org-wide AI policy (master on/off switch, data residency, cost caps, write safety, namespace scope, egress allowlist, prompt audit, redaction) is configurable in Admin → Guardrails. See AI Guardrails.