KubeIntellect V2 — Security Model¶
Table of Contents¶
- API authentication
- Role capabilities
- Kubernetes RBAC tiers
- HITL — Human-in-the-Loop gate
- Secret protection — why users can't steal the API key
- Shell injection prevention
- Secret hygiene checklist
1. API authentication¶
Auth is controlled by four key lists plus an optional HMAC backend. Leave all
four empty (and DEMO_KEY_HMAC_SECRET unset) to disable auth — useful for local
dev or trusted networks.
KUBEINTELLECT_SUPERADMIN_KEYS=ki-su-rootkey
KUBEINTELLECT_ADMIN_KEYS=ki-admin-abc123,ki-admin-def456
KUBEINTELLECT_OPERATOR_KEYS=ki-op-xyz789
KUBEINTELLECT_READONLY_KEYS=ki-ro-qwerty
- Comma-separated — multiple keys per role are supported (useful for rotating keys without downtime).
- Keys are passed as HTTP Bearer tokens:
Authorization: Bearer ki-admin-abc123 - The role is resolved once per request in
app/api/v1/auth.pyand injected into the LangGraph config asuser_role.
Generate a key: openssl rand -hex 20
HMAC-signed demo keys (optional)¶
For public demos (e.g. the browser terminal) where you want to issue read-only
keys without restarting the server, set AUTH_BACKEND=hmac and
DEMO_KEY_HMAC_SECRET=<random>. The auth layer then accepts any token of the
form ki-ro-<base64url(email:exp_unix)>.<hmac_sha256_hex[:32]> whose signature
verifies and whose expiry is in the future. Static *_KEYS continue to be
checked first; HMAC is only consulted for ki-ro-* tokens that miss the static
list. Rotate DEMO_KEY_HMAC_SECRET to invalidate all outstanding demo keys
instantly.
2. Role capabilities¶
| Operation | superadmin | admin | operator | readonly |
|---|---|---|---|---|
kubectl get, describe, logs, top, events |
✅ | ✅ | ✅ | ✅ |
kubectl apply, scale, patch, create, run, exec |
✅ HITL | ✅ HITL | ✅ HITL | ❌ Blocked |
kubectl delete, drain, replace, taint |
✅ HITL | ✅ HITL | ❌ Blocked | ❌ Blocked |
Writes to infrastructure namespaces (kubeintellect, monitoring, kube-system, …) |
✅ HITL | ❌ Blocked | ❌ Blocked | ❌ Blocked |
| Prometheus / Loki queries | ✅ | ✅ | ✅ | ✅ |
| Receive HITL approval prompts | ✅ | ✅ | ✅ (medium risk only) | ❌ |
superadmin is meant for the cluster owner — it bypasses the
KUBECTL_BLOCKED_NAMESPACES write-block, but it does not bypass
KUBECTL_BLOCKED_RESOURCES (Secrets and ServiceAccounts remain shielded for
all roles, including superadmin).
HITL = Human-in-the-Loop. Even superadmin and admin users cannot execute destructive commands without explicitly typing yes or /approve in the same session.
3. Kubernetes RBAC tiers¶
The app's ServiceAccount (kubeintellect-sa) has exactly the permissions listed below — nothing more. Users interact via the API; they never have direct cluster access.
cluster-ro (always enabled)¶
Read-only cluster-wide: get, list, watch on pods, nodes, services, configmaps, events, deployments, statefulsets, daemonsets, ingresses, RBAC resources, batch jobs, metrics.
Deliberately excluded: secrets — users cannot run kubectl get secrets through KubeIntellect to read API keys or credentials.
cluster-ops (enabled via rbac.createClusterOps: true)¶
Write operations, all HITL-gated: pod delete, configmap CRUD, deployment/statefulset/daemonset patch/update/delete, scale, service/PVC CRUD, job/cronjob create/delete, HPA CRUD, ingress CRUD.
cluster-exec (enabled via rbac.allowExec: true, default false)¶
pods/exec — kubectl exec into pods. Off by default in all non-dev environments.
Why it's separate: a user with exec access could kubectl exec -it <app-pod> -- env and read the Azure OpenAI API key from the container's environment. Keeping this off in production is the single most important secret-protection control.
# values-local.yaml (dev — exec allowed, no real secrets at risk)
rbac:
allowExec: true
# values-vm.yaml / values-aks.yaml (prod — exec blocked)
rbac:
allowExec: false
namespace-manager (enabled via rbac.enableNamespaceManagement: true)¶
Namespace + quota + RoleBinding CRUD. Off by default. Enable only for eval/test clusters.
4. HITL — Human-in-the-Loop gate¶
Every destructive or write operation hits four checks before executing:
1. Role check (in run_kubectl)
├─ readonly → "Permission Denied" returned, no HITL shown
├─ operator + high-risk verb → "Permission Denied" returned, no HITL shown
└─ superadmin / admin / operator + allowed verb → continue
2. Protected-resource check
└─ resource ∈ KUBECTL_BLOCKED_RESOURCES (secrets / serviceaccounts)
→ "[Protected]" returned, kubectl never called (all roles)
3. Protected-namespace check
├─ ns ∈ KUBECTL_BLOCKED_NAMESPACES + write verb
│ ├─ superadmin → allowed (bypass) — continue to HITL
│ └─ admin / operator → "[Protected]" returned, kubectl never called
└─ read-only verbs always allowed (the agent must observe its own pod
and the observability stack to diagnose issues)
4. Risk classification + interrupt()
├─ high-risk (delete, drain, replace, taint):
│ interrupt() called → graph pauses → user sees approval prompt
└─ medium-risk (patch, apply, scale, exec, create, run, set, rollout, cordon, uncordon):
interrupt() called → graph pauses → user sees approval prompt
User response in same session (X-Session-ID header):
"yes" / "approve" / "/approve" → Command(resume=True) → command executes
"no" / "deny" / "/deny" → Command(resume=False) → "Action cancelled by user."
anything else → treated as denial
The graph is frozen in the checkpoint store (PostgreSQL or SQLite) during the wait. No timeout — the approval can come hours later.
Auto-approve (auto_approve=true)¶
Setting auto_approve: true in the chat-completions request body bypasses
step 4 entirely (steps 1–3 still apply). The Proactive Fix Mode prompt block
is appended to the coordinator's system prompt so it knows it should apply
fixes immediately and verify after every mutation. This is used by the
evaluation harness and trusted automation. Per-session bypass
is also enabled when the user types "approve all" in the chat — handled by
is_auto_approve_request in app/agent/hitl.py.
5. Secret protection — why users can't steal the API key¶
The Azure OpenAI API key lives in a Kubernetes Secret (kubeintellect-secrets) and is mounted into the pod as environment variables via envFrom: secretRef.
Attack surface analysis¶
| Attack vector | Protection layer | Status |
|---|---|---|
User asks: get secrets in kubeintellect namespace |
kubectl_tool blocked resources — secrets is in KUBECTL_BLOCKED_RESOURCES; tool returns [Protected] before calling kubectl |
✅ Blocked in-app |
User asks: list all resources in kubeintellect namespace |
kubectl_tool blocked namespaces — kubeintellect is in KUBECTL_BLOCKED_NAMESPACES; tool rejects -n kubeintellect |
✅ Blocked in-app |
User asks: get secrets in monitoring namespace |
kubectl_tool blocked namespaces — monitoring is blocked (contains Langfuse keys) |
✅ Blocked in-app |
User asks: kubectl get serviceaccounts |
kubectl_tool blocked resources — SA tokens could impersonate the app | ✅ Blocked in-app |
kubectl exec into pod → env |
rbac.allowExec: false in prod → Kubernetes API server rejects the exec call |
✅ Blocked by RBAC |
SSH into VM → read .env |
.env owned by deploy user, chmod 600 |
✅ Protected by OS |
| Shell history on VM leaking keys | make vm-deploy sources .env internally — key never appears in the shell command string |
✅ Not in history |
How the kubectl blocklist works¶
app/tools/kubectl_tool.py runs two checks before calling kubectl and before showing any HITL prompt:
User query → coordinator → run_kubectl("kubectl get secrets -n kubeintellect")
│
▼ _check_protected_access()
resource = "secrets" → in KUBECTL_BLOCKED_RESOURCES?
│ YES → return "[Protected]..." immediately
│ kubectl never called
▼
namespace = "kubeintellect" → in KUBECTL_BLOCKED_NAMESPACES?
│ YES → return "[Protected]..."
│
▼ (only reaches here if both checks pass)
role check → HITL → subprocess kubectl
The blocklists are configured in app/core/config.py and can be overridden per-deployment via env vars:
# Default — protects all infrastructure namespaces
KUBECTL_BLOCKED_NAMESPACES=kubeintellect,monitoring,kube-system,kube-public,kube-node-lease,ingress-nginx,cert-manager
# Default — protects secrets and SA tokens
KUBECTL_BLOCKED_RESOURCES=secret,secrets,serviceaccount,serviceaccounts
Why this is the right layer: RBAC controls what the ServiceAccount can do at the Kubernetes API level. The in-app blocklist controls what the AI agent is allowed to ask for — catching it before the API call, before HITL, and returning a clear refusal to the user's session.
Further hardening (Azure Key Vault — Phase 2)¶
The above controls are sufficient for a demo / early production system. For stricter production security, move to Azure Key Vault + Secrets Store CSI Driver:
- Secrets are fetched at runtime by the pod using Workload Identity (federated OIDC)
- Injected as files at
/mnt/secrets/, not as env vars envinside the pod shows no secrets- The Kubernetes Secret object is never created
This is logged as a future roadmap item. The current model is pragmatic and secure against the realistic threat (public API users).
6. Shell injection prevention¶
run_kubectl (app/tools/kubectl_tool.py) has multiple layers preventing command injection:
Layer 1 — metacharacter guard
Reject any command containing: ; & ` $ \
Pipe (|) is allowed and handled in Python (not the shell).
< and > are intentionally allowed — they are only dangerous for shell I/O
redirection, which is impossible under shell=False, and excluding them allows
--from-literal values that contain HTML / template content.
Layer 2 — rejected verbs
`kubectl edit` is hard-blocked at parse time — it requires an interactive
terminal that is never available in the container or pip install.
Layer 3 — shlex.split (shell=False)
subprocess is called with a list of args, never a shell string.
The shell is never invoked. No interpolation possible.
Layer 4 — pipe emulation
Only `grep` is supported after `|`. Any other command is rejected.
grep is reimplemented in Python using re — no subprocess involved.
Supported flags: -v / --invert-match, -i / --ignore-case, -E.
Layer 5 — YAML pre-validation
stdin YAML is parsed with yaml.safe_load_all before being passed to kubectl.
Parse warnings are logged but do not fail the call (kubectl is the source of
truth for its own validation — Python's parser is sometimes stricter).
Layer 6 — namespace output filter
`kubectl get namespaces` output is post-filtered to strip blocked namespaces
from the table / -o name / jsonpath outputs the user sees.
Layer 7 — output cap
Output is truncated at 8 000 characters regardless of what kubectl returns.
Prevents memory exhaustion from pathological outputs and includes an explicit
"[TRUNCATED: N chars omitted]" marker so the coordinator surfaces the warning
to the user.
7. Secret hygiene checklist¶
Local dev¶
-
.envis in.gitignore— never committed - Use weak dev passwords (
changeme) — no real keys in local.env -
rbac.allowExec: trueis fine — no real secrets in the Kind cluster
Azure VM (production)¶
-
chmod 600 .env— only the deploy user can read it -
values-vm.yamlis in.gitignore— never committed -
rbac.allowExec: falseinvalues-vm.yaml— verify before deploy - Admin API key shared only with the cluster owner; operator/readonly keys shared with users
- Prefix
make vm-deploywith a space to keep it out of shell history, or useHISTCONTROL=ignorespace - Rotate keys: update
.envand redeploy — old keys stop working immediately - Langfuse admin password is a strong generated password (
openssl rand -base64 16)