Kubernetes Access — Security Model
pc kube is an identity-aware access proxy: it never hands out cluster
credentials, and it authorizes every request per-user. This page documents
the threat model and the hardening guarantees. The design follows Teleport's
Kubernetes-access model and closes the pitfalls its advisories surfaced.
Two-layer authorization
| Layer | Enforces | Where |
|---|---|---|
| Proxima (tenant gate) | Can this user reach this cluster at all? | Backend re-checks live ac.Can("kubernetes:read", cluster.ClientID) on every request — fail-closed, against the target cluster's owning client. |
| Kubernetes (capability gate) | What can the user do once inside? | The agent impersonates Proxima-derived k8s groups; k8s RBAC decides. |
The two layers are independent: Proxima gates which cluster; k8s RBAC gates what you may do in it.
Impersonation, not shared credentials
The agent authenticates to the apiserver with its own ServiceAccount and asserts
your identity via Impersonate-User / Impersonate-Group. Consequences:
- The cluster's audit log shows your email, not the agent's ServiceAccount.
- k8s RBAC evaluates the impersonated groups, so least-privilege is enforced by the cluster itself.
The impersonation ServiceAccount is not god-mode
Teleport's documented impersonation ClusterRole grants blanket impersonate
on users/groups — a compromised proxy could then impersonate almost any
privileged group. Proxima's impersonate ClusterRole is resourceNames-restricted
to exactly the groups it uses:
rules:
- apiGroups: [""]
resources: ["groups"]
verbs: ["impersonate"]
resourceNames: ["proxima:kube-readonly", "proxima:kube-exec", "system:authenticated"]
- apiGroups: [""]
resources: ["users"]
verbs: ["impersonate"] # users carry no privilege — audit identity only
Even a fully compromised collector can only ever impersonate those three
non-privileged groups — never system:masters or cluster-admin.
Identity cannot be smuggled
The trusted identity travels out-of-band, never in a request header:
- The backend resolves your identity + groups server-side and passes them in the
authenticated NATS
kube_opencommand. - The agent injects them into each proxied request via the request context (a Go value a caller cannot set), not a header.
- Both the backend edge and the agent strip every client-supplied
Authorization,Impersonate-*, andX-Proxima-*header before anything is forwarded.
So kubectl --as=system:masters, a raw Impersonate-Group header, or a
CRLF-smuggled header value are all stripped and ignored. Impersonated values are
additionally validated to reject control characters (CR/LF/NUL), closing the
header-injection class.
The read/execute split is enforced by k8s
kubernetes:read and kubernetes:execute map to separate k8s groups
(proxima:kube-readonly, proxima:kube-exec). The backend only asserts the
exec group for a caller who actually holds kubernetes:execute; the read-only
ClusterRole has no exec verb. A read-only user's kubectl exec therefore fails
at the apiserver (403) — the split is enforced by real k8s RBAC, not by the
proxy parsing request paths.
Exec is not "read-only"
pods/exec runs arbitrary commands inside a pod; exec into a pod with a
privileged ServiceAccount, hostPath, or hostPID is a path to node/cluster
compromise. That is why exec is a distinct, higher-privilege grant rather
than part of read. Grant kubernetes:execute accordingly.
Secrets are excluded
The proxima:kube-readonly ClusterRole grants view-style read across common
resources but never secrets. It includes get/list on
apps/controllerrevisions and argoproj.io/rollouts, which the cluster agent's live
rollout-history and manifest reads need.
This role is shared with pc kube users who hold kubernetes:read, so those users can
also kubectl get controllerrevisions and kubectl get rollouts and see them
unredacted. That includes historical pod templates with their literal env values.
The cluster agent's live reads redact; kubectl access does not. This adds no new kind of
data, because a StatefulSet's live env is already readable, but it does make past values
reachable.
The same tier backs the cluster agent's live manifest read, and that read is
narrower than the role: the agent refuses ConfigMaps and Secrets outright (only
workloads, ReplicaSets, Pods, Services, Ingresses, HPAs, Jobs, CronJobs,
NetworkPolicies and PVCs are allowed), and it redacts every object before it leaves
the cluster. metadata.managedFields and the
kubectl.kubernetes.io/last-applied-configuration annotation are removed, and each
literal env value becomes <redacted: N chars>. valueFrom references stay,
because they name a source without revealing it. The same applies to the literal
value of probe and lifecycle-hook HTTP headers (header names stay) and to container
termination messages. Annotations that embed a full copy of the object are removed:
kubectl.kubernetes.io/last-applied-configuration, kapp.k14s.io/original and
objectset.rio.cattle.io/applied. Some annotation values are replaced by
<redacted: annotation N chars>:
kubernetes.io/change-cause- nginx
*snippet*andauth-urlannotations - any value that is a JSON object or array, starts with
H4sI(gzip+base64), or is longer than 1024 bytes
Every annotation that survives those rules is also run through the agent's secret
detector, together with its key. A secret-named key such as example.com/db-password
is therefore redacted even when its value is short. The inventory snapshot uses the
same detector. A gitRepo volume's repository URL has inline credentials removed. The
reply's notes name every annotation that was removed or redacted.
The following are not redacted, so a secret placed in one of them is visible:
- container
argsandcommand, which operators need execcommands of liveness, readiness and startup probes, and ofpostStart/preStoplifecycle hooks- a CSI volume's
volumeAttributesand a flexVolume'soptions, which conventionally reference secrets through*SecretRefrather than carry them
Short-lived, cluster-bound, revocable credentials
- The credential kubectl sends is a Proxima token, not a k8s token or cert.
- It is bound to one cluster and audience (
proxima-kube): a token minted for cluster A is rejected on cluster B, and it is not usable on any other API surface. The backend compares it against the canonicalized path cluster id. - TTL is short (15 minutes); the exec-credential plugin refreshes transparently.
- The proxy treats the token as authentication only — it re-resolves your live permissions on every request. If your access is revoked or your account disabled, the next request is denied (and the in-flight window is bounded by the TTL). Revoking your Proxima session kills kube access — a stronger revocation story than long-lived client certs.
Transport & streaming
- The agent verifies the apiserver serving certificate against the in-cluster
CA (no
InsecureSkipVerifyoutside an explicit, loudly-warned dev mode). - Long-lived streams (
kubectl logs -f,--watch,exec) are not cut by the server's absolute write timeout, and each user is capped to a bounded number of concurrent streams to prevent resource exhaustion.
Console live reads (manifest, revisions, logs)
The Kubernetes pages read the cluster through the cluster agent, using the read-only tier, as described in Live reads. That tier is installed by the chart only with kubeAccess.enabled=true; without it every live read is answered "not permitted". Kubernetes: Operate (read-only) summarizes what these reads never send or store.
- Redaction. Manifests and revisions are redacted in the agent. Log lines are secret-sanitized in the backend before they are returned.
- Rate limit. Each user gets a budget: 30 log reads and 60 manifest/revisions reads a minute. The budget is counted per backend process. It is not shared across replicas, so with N replicas a user can make up to N times as many reads. It protects the apiserver from a runaway page, not from a determined caller; the per-request permission checks do that.
Audit & recording
- Every proxied request is recorded as a
kube_requestaudit event (cluster_id,client_id, user, method, path). It is written before the request is proxied, so it records the attempt, not the outcome — there is no status or response code on the entry. Token issuance is audited askube_token_issued. - The cluster-onboarding round-trip is audited
separately as
kube_probeonkube_cluster— named besidekube_request, not folded into it, because it is a fixedGET /versionissued by the wizard rather than a proxied request. Same posture: written at initiation, carrying the cluster, the onboarding attempt, the requested path and the impersonated identity, with no outcome. - Interactive
kubectl exec/attachsessions are recorded (encrypted, in R2), like terminal sessions. - The cluster's own k8s audit log is the authoritative record — because the
request runs as your impersonated identity, it attributes actions to you even
for paths the proxy doesn't parse (
exec, ephemeral/debug containers). - L1 live reads (
k8s_live_workload,k8s_live_network_policies,k8s_live_pod_logs,k8s_live_endpoints) are not proxied requests, so they produce nokube_requestevent. The cluster's k8s audit log records each one under the calling user's email, orproxima-l1-investigatorfor unattended triage, and the call appears in the investigation's tool-call record.
One cluster, one client
v1 assumes a cluster is dedicated to a single Proxima client. The read-only ClusterRole is cluster-wide, so on a cluster shared by multiple clients a user of one client could read another client's namespaces. Until namespace-scoped roles land, only enable kube access on single-tenant clusters.
Requirement traceability
The implementation maps to these hardening requirements:
| ID | Requirement |
|---|---|
| S1 | resourceNames-restricted impersonate ClusterRole (never blanket) |
| S2 | Strip all client Authorization / Impersonate-* / X-Proxima-* at both edges |
| S3 | Validate impersonated identity (reject CR/LF/NUL/control chars) |
| S4 | Fail-closed, per-request token + live RBAC on the target cluster's client |
| S5 | Token bound to cluster + audience; cross-cluster/cross-surface rejected |
| S6 | Agent verifies apiserver TLS via the in-cluster CA |
| S7 | Read vs exec enforced by two distinct k8s groups derived server-side |
| S8 | Long streams survive the write timeout; per-user stream limit |
| S9 | Exec recorded + encrypted; k8s audit log authoritative |
| S10 | One-cluster-one-client (documented limit) |