Skip to main content

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​

LayerEnforcesWhere
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_open command.
  • 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-*, and X-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* and auth-url annotations
  • 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 args and command, which operators need
  • exec commands of liveness, readiness and startup probes, and of postStart / preStop lifecycle hooks
  • a CSI volume's volumeAttributes and a flexVolume's options, which conventionally reference secrets through *SecretRef rather 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 InsecureSkipVerify outside 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_request audit 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 as kube_token_issued.
  • The cluster-onboarding round-trip is audited separately as kube_probe on kube_cluster — named beside kube_request, not folded into it, because it is a fixed GET /version issued 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/attach sessions 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 no kube_request event. The cluster's k8s audit log records each one under the calling user's email, or proxima-l1-investigator for 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:

IDRequirement
S1resourceNames-restricted impersonate ClusterRole (never blanket)
S2Strip all client Authorization / Impersonate-* / X-Proxima-* at both edges
S3Validate impersonated identity (reject CR/LF/NUL/control chars)
S4Fail-closed, per-request token + live RBAC on the target cluster's client
S5Token bound to cluster + audience; cross-cluster/cross-surface rejected
S6Agent verifies apiserver TLS via the in-cluster CA
S7Read vs exec enforced by two distinct k8s groups derived server-side
S8Long streams survive the write timeout; per-user stream limit
S9Exec recorded + encrypted; k8s audit log authoritative
S10One-cluster-one-client (documented limit)