Skip to main content

Security Contexts, Pod Security, and RBAC

What This Concept Is

Security in Kubernetes is two orthogonal questions:

  • Workload isolation. What can the process inside the container do on the node? Controlled by the Pod/container securityContext and by cluster policy (Pod Security Standards, admission webhooks).
  • API authorization. What can a user or ServiceAccount do through the api-server? Controlled by RBAC.

Pod-side levers (securityContext, per-Pod and per-container):

FieldEffect
runAsUser / runAsGroupOverride the UID/GID the process runs as
runAsNonRoot: trueRefuse to start if effective UID is 0
readOnlyRootFilesystem: trueMount / as read-only
allowPrivilegeEscalation: falseBlock setuid escalation inside
capabilities.drop: ["ALL"]Drop all Linux capabilities; add back only what's needed
seccompProfile.type: RuntimeDefaultApply the runtime's seccomp filter
privileged: trueDisable almost all isolation -- almost never correct

Pod Security Standards (PSS) are three preset levels -- privileged, baseline, restricted -- that admission controllers can enforce per namespace. The built-in Pod Security Admission controller reads a namespace label like pod-security.kubernetes.io/enforce: restricted and rejects non-compliant Pods.

RBAC has four types:

  • Role -- namespaced set of permissions (verbs × resources).
  • ClusterRole -- cluster-scoped (or aggregated across namespaces).
  • RoleBinding -- grants a Role to a subject within a namespace.
  • ClusterRoleBinding -- grants a ClusterRole cluster-wide.

Subjects are users, groups, or ServiceAccounts. Every Pod runs as a ServiceAccount (default if unspecified), which determines what it can do through the api-server.

Why It Matters Here

A production cluster has to answer:

  • "If this container is compromised, what can the attacker reach on the node? On the API?"
  • "Can this CI pipeline's ServiceAccount delete arbitrary workloads?"
  • "Does this namespace's workloads run as root?"

Without PSS, RBAC, and securityContexts, the default answers are "a lot," "yes," and "yes."

Concrete Example

A hardened Pod:

apiVersion: v1
kind: Pod
metadata:
name: hardened
spec:
serviceAccountName: web-sa
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile: { type: RuntimeDefault }
containers:
- name: app
image: myorg/app:1.2
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- { name: tmp, emptyDir: {} }

A minimal Role + RoleBinding for a CI ServiceAccount that is only allowed to apply Deployments and Services in prod:

apiVersion: v1
kind: ServiceAccount
metadata: { name: ci-deployer, namespace: prod }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: ci-deployer, namespace: prod }
rules:
- apiGroups: ["apps"]
resources: ["deployments", "deployments/scale"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: ci-deployer, namespace: prod }
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: prod
roleRef:
kind: Role
name: ci-deployer
apiGroup: rbac.authorization.k8s.io

Namespace labeled for PSS:

apiVersion: v1
kind: Namespace
metadata:
name: prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest

Common Confusion / Misconception

"runAsNonRoot is enough."

Running as UID 1000 inside the container is necessary but not sufficient. Without dropping capabilities, the process may still have CAP_NET_ADMIN or others; without readOnlyRootFilesystem, it can still write to /etc; without a seccomp profile, it can still call risky syscalls; without allowPrivilegeEscalation: false, a setuid binary can escalate. Hardening is the combination of these controls.

A second confusion: "RBAC denies by default, so I'm safe." It does deny by default, but a common mistake is to grant the built-in cluster-admin ClusterRole to a broader group than intended, or to bind the default ServiceAccount so that every Pod in a namespace inherits dangerous permissions. Audit RoleBindings and ClusterRoleBindings, not just Roles.

A third confusion: "Pod Security Policy (PSP) is still a thing." PSP was removed in Kubernetes 1.25. Its replacement is Pod Security Admission with the PSS levels above, plus external policy engines (OPA/Gatekeeper, Kyverno) for anything PSS does not cover.

How To Use It

Baseline for any new workload:

  1. Namespace enforces baseline or restricted PSS.
  2. Pod runs as non-root, drops all capabilities, read-only root FS.
  3. Pod uses a dedicated ServiceAccount with a scoped Role -- never the default ServiceAccount for anything non-trivial.
  4. Nothing is privileged: true unless there is a named exception with review.

Check Yourself

  1. What is the difference between what a securityContext controls and what RBAC controls?
  2. Why is runAsNonRoot: true not sufficient by itself?
  3. What replaced Pod Security Policy, and how is it configured per namespace?

Mini Drill or Application

Label a namespace with pod-security.kubernetes.io/enforce: restricted. Try to apply a Pod with privileged: true. Record the admission error. Now modify the Pod to pass the restricted profile (non-root, drop capabilities, seccomp RuntimeDefault, read-only root FS). Re-apply. Write a paragraph matching each admission rule to a field you set.

Policy Beyond PSS: OPA/Gatekeeper and Kyverno

Pod Security Admission handles the three PSS levels. Real clusters always want more: "images must come from our registry," "every workload must have a team label," "no Service may be type: LoadBalancer in this namespace." Two projects fill that gap:

  • OPA/Gatekeeper -- Rego-based policies enforced as admission webhooks. Mature, expressive, but Rego has a learning curve.
  • Kyverno -- YAML-native policies that read like Kubernetes manifests. Often easier to start with; supports validation, mutation, and generation.

Both are also used for auditing -- dry-run a policy, list all existing violations, fix drift, then switch to enforce. This is the fitness-function pattern from S7 M5 applied to cluster policy: write the rule once, run it on every PR and every admission.

Read This Only If Stuck