Security

How kube-workspaces is hardened at the deployment level, and the reasoning behind each decision. For authentication and authorisation of users, see authentication.md.

ServiceAccount tokens

Every component runs under a dedicated ServiceAccount with least-privilege RBAC, and none relies on Kubernetes’ legacy token automounting.

Component ServiceAccount Token
controller kube-workspaces-controller (Helm: release fullname) projected, 1h, auto-rotated
api same as controller projected, 1h, auto-rotated
proxy kube-workspaces-proxy projected, 1h, auto-rotated
frontend kube-workspaces-frontend none

Why the frontend has no token

The frontend is a UI and reverse proxy. It has no Kubernetes client — there is no @kubernetes/* dependency in its package.json and it makes no API-server calls; everything goes through the API service. It previously ran on the default ServiceAccount and was handed a token it could not use, so it now has its own ServiceAccount with no Role or ClusterRole bindings and automountServiceAccountToken: false on both the ServiceAccount and the pod.

Why the others use projected tokens

The three components that do call the API server would end up with a token either way. A projected token is better than the automounted one because it is:

  • short-lived and auto-rotated (1 hour), rather than a long-lived Secret-backed token that stays valid until deleted
  • explicitly declared, so the mount is auditable rather than implicit
  • bound to the pod, so it stops being valid when the pod goes away
automountServiceAccountToken: false
volumes:
  - name: kube-api-access
    projected:
      defaultMode: 420
      sources:
        - serviceAccountToken:
            path: token
            expirationSeconds: 3600
        - configMap:
            name: kube-root-ca.crt
            items: [{ key: ca.crt, path: ca.crt }]
        - downwardAPI:
            items: [{ path: namespace, fieldRef: { fieldPath: metadata.namespace } }]

Two details matter, and both are enforced by tests:

The path layout is exact. client-go’s in-cluster config reads token, ca.crt and namespace from /var/run/secrets/kubernetes.io/serviceaccount. A partial mount fails authentication at pod startup, not at build time, so make test-lint asserts all three files are present in both render paths.

No audience is pinned. It is tempting to set audience: https://kubernetes.default.svc, but the correct value is cluster-specific — on kind the issuer is https://kubernetes.default.svc.cluster.local, and a mismatch fails authentication. Omitting it makes the API server issue for its own default audience, which is what client-go expects. Verified with a TokenReview: an audience-less projected token authenticates and reports audiences: ["https://kubernetes.default.svc.cluster.local"].

make test-lint fails if an audience appears.

RBAC

Both install paths grant identical effective permissions. They name the ServiceAccount differently (kube-workspaces-controller under Kustomize, the release fullname under Helm) and group their ClusterRole rules differently, so a textual diff is meaningless — the manager role is 152 lines one side and 45 the other.

scripts/compare-rbac.py flattens both renders to {apiGroup/resource: sorted(verbs)} and compares that, so a divergence in privilege is caught even though the text differs. It runs in make test-lint.

make test-smoke additionally verifies effective permissions on a live cluster with kubectl auth can-i, rather than inferring them from bindings.

What is deliberately not hardened

The workspace pods themselves. A workspace is a user-supplied container image running user code; the Workspace CRD accepts a full PodSpec. Constraining it is a policy decision for the cluster operator — use PodDefaults, admission control (Kyverno, Gatekeeper) or Pod Security Standards on the workspaces namespace.

Virtual machines. spec.type: vm runs a KubeVirt VirtualMachine whose root disk comes from the workspace’s image as an ephemeral containerDisk. The guest runs as the unprivileged qemu user inside a virt-launcher pod; it is not a sandbox boundary beyond what KubeVirt provides. The controller needs virtualmachines/virtualmachineinstances RBAC for this, which is granted unconditionally — if you do not want VM workspaces, restrict who can set spec.type via admission policy.

Network policy. None is shipped. The proxy needs to reach arbitrary workspace pods, so a useful default policy depends on how workspaces are namespaced in your cluster.

Reporting a vulnerability

See SECURITY.md.


This guide is also rendered on the Kube Workspaces website.