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. In its own namespace it can read its own Kubernetes Secrets (its session and encryption material, which the Helm chart creates) and can neither change nor delete them.
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), each created by its owner from the account menu → API keys. 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 rbac.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 — across the namespaces the reader can see, except Secret create/update/patch in KubeManta's own namespace: on a cluster-wide install the <release>-agent-secret-guard ValidatingAdmissionPolicy refuses them, with rbac.clusterWide=false the Role there omits them, and on a cluster that cannot run that policy the agent gets no Secret write permission at all. Pod create and pods/exec reach any pod in scope, including the Expert Mode pod, so treat allowWrites as cluster-admin-equivalent and grant KubeManta's admin role accordingly. |
kubemanta |
own-namespace Role | Always. Read-only (get/list/watch) on Secrets in KubeManta's OWN namespace, so the TLS certificate endpoint (GET /admin/tls/certificate) can list them — whatever rbac.readSecrets says. No write or delete verb. |
kubemanta-terminal |
its own ClusterRole, plus per-namespace Roles | The identity of kubectl in the browser terminal (unless terminal.clusterCredentials.enabled=false, which creates none of this). The agent's reads and, with rbac.allowWrites, its writes — but no Secret rule and no kubelet-scrape grant. Secret read (get, list, watch) only in the namespaces listed in terminal.secretsNamespaces, each of which KubeManta itself must be allowed to read; never in KubeManta's own namespace — listing it fails the install. For one more release it also still reads Secrets where rbac.secretsNamespaces (or rbac.tenantNamespaces with rbac.clusterWide=false) names a namespace; this is deprecated, and the install notes list what to copy into terminal.secretsNamespaces. With rbac.allowWrites, its writes never reach KubeManta's own namespace: there its Role is read-only with rbac.clusterWide=false, and on a cluster-wide install the <release>-terminal-guard ValidatingAdmissionPolicy (Kubernetes 1.30+) refuses every change it attempts, kubectl exec included. On a cluster that does not serve that API, its cluster-wide writes are withheld and the install notes say the terminal is read-only. KubeManta mints its token (one hour, renewed); the only grant this adds to the agent is create on that one account's serviceaccounts/token. |
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, including in KubeManta's own namespace.
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 + rbac.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, or the shell's credentials withheld with
terminal.clusterCredentials.enabled=false. The shell has its own
ServiceAccount, so it never reads Secrets in KubeManta's own namespace —
the admin password and encryption key are kept there.
Upgrading from 1.3.1 or earlier: Secrets in the terminal
Up to 1.3.1, kubectl in the browser terminal ran as KubeManta's own
ServiceAccount and could read every Secret KubeManta can, its own included.
After the upgrade it runs as <release>-terminal. On a default
(cluster-wide) install kubectl get secret in the terminal is then refused
everywhere, because Kubernetes RBAC cannot grant "every namespace except
one". If your operators rely on it, list those namespaces in
terminal.secretsNamespaces; that changes nothing KubeManta itself reads.
Namespaces already listed in rbac.secretsNamespaces (or
rbac.tenantNamespaces with rbac.clusterWide=false) stay readable from the
shell for one more release: the install notes name them and print the
--set that keeps them. With rbac.allowWrites=true, the shell can no
longer change anything in KubeManta's own namespace (create or exec into a
pod there, patch its Deployments). Everything else the shell could read or
change is unchanged. Upgrade the chart and the agent image together.
Namespace blocklist¶
The following namespaces, and KubeManta's own install namespace, are hidden from the UI unless an admin turns on Show system namespaces:
kube-system · kube-public · kube-node-lease · istio-system · paas-core · cert-manager · ingress-nginx · monitoring · logging · flux-system · argo · argocd · kubemanta-system
Namespaces starting with kube-, gke-, gmp-, istio- or knative- are hidden too. This is an application-layer filter, not an RBAC boundary: for one the API server enforces, use rbac.clusterWide=false with rbac.tenantNamespaces. AI never diagnoses KubeManta's own components.
Programmatic access¶
API access and the MCP server authenticate with per-user API keys. There is no
shared secret: the former security.apiKey Helm value no longer exists. Every
key belongs to a person, can be revoked on its own, and carries scopes chosen
when it is created — see Scopes.
Creating a key. Each user creates their own key from the account menu → API keys, from a signed-in session. Admins can see and revoke any user's keys in Admin → Users & Groups → Users, but cannot create a key for someone else. 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, credentials, licensing or safety controls.
Those surfaces map to no scope, so every key — an admin's included — is refused
(403 api_key_forbidden) there, before any handler runs: a stolen key cannot mint
another key or escalate its own account. See
what no key can do.
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¶
The AI provider key is set by an admin in AI Settings and stored encrypted (AES-256-GCM) in the embedded database. It is:
- Never returned to a browser
- 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 | Its ServiceAccount — read-only by default, 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. Its own outbound connections can be limited to the API server, cluster DNS, public chart repositories and the ranges you list with expertMode.networkPolicy.egress.enabled (off by default). |
| Terminal sidecar | Only its own storage (Claude Code session + caches) — no database, no encryption key file, no cluster-admin token. Runs as a distinct unprivileged OS user in its own container. | Admins only. kubectl runs as the terminal's own ServiceAccount, <release>-terminal: the agent's read permissions and, only with rbac.allowWrites, its write verbs — but never a Secret in KubeManta's namespace, and elsewhere only Secrets in the namespaces you list. terminal.clusterCredentials.enabled=false gives the shell no cluster credential at all. |
So the cluster-admin token is never in the agent pod, the terminal container holds no database or key file of its own, and privileged operations are delegated over an authenticated, network-policy-restricted hop. The terminal is a separate Kubernetes identity, so a terminal user cannot read the admin password, the encryption key or the Expert Mode credential that KubeManta keeps in its namespace. Every terminal session is authorised by a short-lived, single-use token that the agent signs with a key it generates in memory when it starts; the terminal container holds only the half that checks a signature, so nothing a terminal user can read opens another user's session, reads their captured commands or opens an unrecorded shell. The boundary has one exception: with rbac.allowWrites=true the shell's kubectl can create pods and exec into any pod, KubeManta's own included, which reaches the agent container, the expert-exec pod and any Secret a pod mounts. That is why that setting is cluster-admin-equivalent — give terminal access on such an install only to people you would give cluster-admin to. KubeManta's own pod-exec view always refuses its own namespace. 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 key | Embedded database, AES-256-GCM encrypted | Admin-set; never returned by the API |
| 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 |
| Pod Security | Every pod meets the Kubernetes restricted Pod Security profile. The chart does not label the namespace; label it pod-security.kubernetes.io/enforce=restricted to have the API server enforce it (install step). |
| 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. On Cilium, which cannot match the API server with a plain NetworkPolicy, the chart adds a CiliumNetworkPolicy that allows the API server on its ports and nothing else (security.cilium.apiServerPolicy). |
| 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 unprivileged OS user in its own container, with its own ServiceAccount (see Three-pod isolation) |
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 from registry.kubemanta.com 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.
The web UI sends a nonce-based Content-Security-Policy with no unsafe-inline or unsafe-eval for scripts, and allows frames only from KubeManta itself and the Grafana URL you configure.
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 → AI → Guardrails. See AI Guardrails.