In short

Label each application namespace to enforce the Restricted Pod Security Standard, then make every pod pass it: run as a non-root UID, set allowPrivilegeEscalation: false, drop all capabilities, use the RuntimeDefault seccomp profile, and add a read-only root filesystem with a small writable volume. Give the pod its own service account bound to a Role that names exactly the resources and verbs it uses. Prove the result with kubectl auth can-i and checks inside the running container.

  • Pod Security Standards define three levels: Privileged, Baseline and Restricted; Pod Security Admission enforces them per namespace.
  • Restricted accepts runAsNonRoot at pod level but requires allowPrivilegeEscalation: false and drop: ["ALL"] on every container, init and ephemeral containers included.
  • A non-root, read-only container with a wildcard Role is still an overpowered API client; review RBAC and the pod spec together.
  • resourceNames limits a Role to named objects; it cannot limit create, and list or watch then succeed only with a matching metadata.name field selector.
  • Check the admitted pod, not the manifest: admission can mutate it, and only the running process shows the real UID and capabilities.

Verify it

  1. Run kubectl auth can-i get configmap/status-settings -n demo --as=system:serviceaccount:demo:status and expect yes; expect no for list configmaps, get secret/... and create pods.
  2. Run kubectl exec with id and grep CapEff /proc/1/status and expect UID 10001 and CapEff: 0000000000000000.
  3. Write to /tmp and to /etc inside the container: the first must succeed and the second must fail with "Read-only file system".
  4. Apply a pod with a privileged init container with --dry-run=server and expect "violates PodSecurity restricted".
  5. Run kubectl label --dry-run=server --overwrite ns demo pod-security.kubernetes.io/enforce=restricted to list existing pods that would violate the level before enforcing it.
Applies to
  • Kubernetes 1.37.0 (tested with kind 0.33.0); Pod Security Admission is stable since 1.25

Kubernetes workload privilege review. A valid manifest does not prove admission or runtime enforcement.

Part of
Learning path: Runtime and cloud defense (step 2 of 13). Workflows: Fix a finding, Review a design
Learning goals and prerequisitesAbout 27 minutes: 7 to read and 20 for the exercise

What you will learn

  • Separate Linux process privileges from Kubernetes API permissions
  • Explain each hardening field using a concrete workload requirement
  • Test effective authority without breaking necessary application behavior

Before you start

Developers and platform engineers who write Kubernetes manifests and RBAC rules for their own services

  • Read a Pod specification, security context, service account and namespace.
Go to the guided practice

A pod has operating-system and API privileges

A workload can reach beyond its intended task through Linux capabilities, host mounts, privileged execution or a powerful Kubernetes identity. Reducing one surface does not automatically reduce the others. Review the pod specification, node/runtime behavior and service-account permissions as one deployment boundary.

Pin the Kubernetes version and runtime used for the assessment. The Pod Security Standards define three policy levels: Privileged, which is unrestricted, Baseline, which blocks known privilege escalations, and Restricted, which follows current hardening practice. Pod Security Admission applies a level per namespace through labels, in enforce, audit or warn mode; setting a namespace to privileged turns enforcement off for it. An admitted pod is not proof of complete tenant isolation: shared kernels, network access, secrets and administrator powers still matter. Higher-risk mutually untrusted workloads may need stronger runtime or cluster separation, such as the sandboxed node pools in the managed Kubernetes hardening guide at /domains/runtime/containers/managed-kubernetes-hardening/.

Worked design: a status service that reads one ConfigMap

The status process listens on port 8080, reads ConfigMap status-settings in namespace demo through the Kubernetes API, and writes a bounded cache under /tmp. It needs no host files, Secret reads, workload creation or cluster administration. Treat these as explicit requirements. A non-root container with a wildcard service-account role is still an overpowered API client; a narrow role does not make a privileged host-mounted process safe.

The manifest below sets a non-root numeric UID, prevents privilege escalation, drops all capabilities, uses the runtime's default seccomp profile and makes the root filesystem read-only. The /tmp emptyDir supplies the one legitimate writable location with a size limit. The UID must work with the actual image and volume ownership; otherwise the repair produces a crashing service. Do not restore root to silence a permission error before locating the file or directory the process needs.

The separate Role grants get only for the named ConfigMap. A client implemented as get-by-name can use this. A client that lists or watches every ConfigMap will fail, which is an implementation-policy mismatch to resolve deliberately. A namespaced Role does not authorize another namespace; the RoleBinding must name the dedicated status service account in demo. Because this workload actually calls the API, removing its token mount without providing a supported credential path would break it.

The namespace labels enforce the Restricted standard pinned to v1.37 and also warn and audit at the same level. On a Kubernetes 1.37 cluster this manifest, with a test image, was admitted and ran as UID 10001 with no effective capabilities; a write to /tmp succeeded and a write to /etc failed. Review image provenance, node and runtime configuration separately, and record the admitted pod, because an admission controller can mutate the submitted spec. A manifest linter establishes properties of a document, not the behavior of the running workload.

Fixedstatus.yamlYAML
apiVersion: v1
kind: Namespace
metadata:
  name: demo
  labels:
    # Reject pods that violate the Restricted standard; pin the version you tested
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.37
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: status
  namespace: demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: read-status-settings
  namespace: demo
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["status-settings"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: status-reads-settings
  namespace: demo
subjects:
  - kind: ServiceAccount
    name: status
    namespace: demo
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: read-status-settings
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: status
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels: {app: status}
  template:
    metadata:
      labels: {app: status}
    spec:
      serviceAccountName: status
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile: {type: RuntimeDefault}
      containers:
        - name: status
          image: registry.example/status@sha256:IMAGE_DIGEST
          ports: [{containerPort: 8080}]
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: {drop: ["ALL"]}
          resources:
            requests: {cpu: 50m, memory: 64Mi}
            limits: {memory: 128Mi}
          volumeMounts: [{name: scratch, mountPath: /tmp}]
      volumes:
        - name: scratch
          emptyDir: {sizeLimit: 32Mi}

Choose explicit restrictions and necessary exceptions

For a Linux application that supports it, run as a non-root user, prevent privilege escalation, drop unnecessary capabilities and select an appropriate seccomp profile. Avoid privileged containers, host namespaces and hostPath mounts unless the workload's actual role requires them. Apply the review to init and ephemeral containers as well as the main application container.

A read-only root filesystem can reduce writable executable locations, but provide bounded writable volumes for legitimate temporary work. Set memory, CPU and storage expectations and test failure behavior under pressure. Constraints are useful only if the program starts, writes to its intended locations and handles limits safely; do not solve a deployment failure by granting broad privilege without examining the cause.

Enforce the Restricted standard at admission

Pod Security Admission is built into the API server, so the namespace labels in the manifest are the whole configuration. In the test cluster, a pod whose init container set privileged: true was rejected before it was stored, with the message violates PodSecurity "restricted:v1.37" listing three violations: the privileged flag, a missing allowPrivilegeEscalation: false and capabilities not dropped. Restricted checks init and ephemeral containers as well as application containers.

Roll the level out without breaking running workloads: a server-side dry run of the label change lists the existing pods that would violate it, and warn and audit modes report violations without blocking. Pin enforce-version so that a cluster upgrade cannot tighten or loosen the policy without review. The privileged container lab at /labs/kubernetes-policy/ requires every container to declare runAsNonRoot itself; Restricted also accepts the pod-level setting used in this manifest, so the lab rule is deliberately stricter than the standard.

Treat the service account as an application credential

Give each workload the Kubernetes API access it needs, scoped to appropriate resources, verbs and namespaces. Avoid wildcard roles and unnecessary token mounting. If the application never calls the API, disable automount where appropriate. Review roles that can read secrets, create workloads, impersonate identities or modify bindings because they can provide indirect access beyond an obvious resource list.

Separate the application's cloud identity from its Kubernetes RBAC permissions while reviewing how the two are bound. Protect credentials consumed by the pod and verify rotation behavior; the runtime secrets guide at /domains/identity-crypto/secrets/secrets-in-kubernetes-and-runtime/ covers how. A deployment controller with broad rights may be necessary operationally, but its service account should not be reused as the ordinary application's identity.

Test the effective deployment

Inspect the rendered manifest and admitted pod, then test authorized and forbidden API calls using the workload identity. Confirm actual user, capabilities, mounts and writable locations inside the running container. A client-side policy check can miss admission mutations or cluster settings. Retain exceptions with a workload-specific reason and a test that demonstrates why the narrower configuration cannot yet work. The commands below produced the expected result on each line against the test cluster; images without a shell need kubectl debug instead of exec.

Testverify-status.shShell
# API authority of the workload identity (expected answer at the end of each line)
SA=system:serviceaccount:demo:status
kubectl auth can-i get configmap/status-settings -n demo --as=$SA   # yes
kubectl auth can-i list configmaps -n demo --as=$SA                 # no
kubectl auth can-i get secret/database-password -n demo --as=$SA    # no
kubectl auth can-i create pods -n demo --as=$SA                     # no

# Process identity, capabilities and writable paths in the running pod
POD=$(kubectl -n demo get pod -l app=status -o name)
kubectl -n demo exec "$POD" -- id                                 # uid=10001 gid=10001
kubectl -n demo exec "$POD" -- grep CapEff /proc/1/status         # CapEff: 0000000000000000
kubectl -n demo exec "$POD" -- sh -c 'echo ok > /tmp/cache'       # succeeds
kubectl -n demo exec "$POD" -- sh -c 'echo x > /etc/new-config'   # Read-only file system

# Admission: a pod with a privileged init container must be rejected
kubectl apply --dry-run=server -f bad-init.yaml   # violates PodSecurity "restricted:v1.37"

Diagnose incomplete fixes

runAsNonRoot, readOnlyRootFilesystem and allowPrivilegeEscalation protect different things. A read-only root does not prevent reading a mounted secret or writing to a separate writable volume. Preventing privilege escalation does not remove privileges the process already holds. seccomp constrains system calls under the selected profile but is not an authorization policy for cloud APIs.

The strongest RBAC review follows indirect authority. Permission to create a pod under a powerful service account, bind roles, impersonate identities or read credentials can exceed what the obvious verb suggests. Prefer a dedicated service account and review the deployer separately. If the application never calls the API, disabling automatic token mounting reduces exposure; that advice does not apply blindly to this API-reading example.

Check the result against concrete cases

These outcomes belong to the status example, and the verification commands above observed each of them on Kubernetes 1.37; a different cluster needs its own run.

Check the result against concrete cases
CaseExpected resultWhat it establishes
GET demo/status-settings ConfigMapAllow for bound status service accountRequired API use
LIST all ConfigMapsDeny in this exampleNamed-object policy is not a list grant
GET demo/database-password SecretDenyNo Secret read authority
Write /tmp/cacheAllow within volume limitsLegitimate process behavior survives
Write /etc/new-configFail on read-only rootRoot filesystem is constrained
Init container requests privileged=trueReject under enforced policyAll container lists are covered

Guided practice

Repair a status workload using the supplied requirements and compare its observed behavior with the desired boundary. The exercise is a manifest and access-decision review, requiring no cluster.

Supplied scenario material
Plain text
Requirements: GET only demo/status-settings; HTTP 8080; temporary writes under /tmp; no host or Secret access.
Original: privileged=true, UID=0, hostPath / mounted at /host, default service account, resources=* verbs=*.
Patch A: UID=10001 and readOnlyRootFilesystem=true; all other original settings retained; no scratch volume.
Patch B: fragments above; dedicated status service account and exact RoleBinding.
Observed under B: startup tries writing /var/cache/status; then API client tries LIST ConfigMaps.
Alternative implementation: write /tmp/status; use GET demo/status-settings.
  1. 1. Explain why A remains unsafe

    Name two excessive authority paths that survive UID=10001, then identify the new availability problem.

    Expected result and reasoning

    The wildcard Kubernetes role remains, and privileged execution plus the host mount still expose dangerous host capabilities. A read-only root without the required writable scratch path can break startup. A changed UID alone does not meet the workload contract.

  2. 2. Map every field to a requirement

    For B, connect the scratch volume, capability drop and resourceNames restriction to the requirement they serve.

    Expected result and reasoning

    Scratch allows bounded temporary writes without making the root writable; dropping capabilities reduces unnecessary Linux privileges; resourceNames plus get narrows Kubernetes reads to status-settings. These are independent controls, so one successful check cannot stand in for all three.

  3. 3. Diagnose the two B failures

    Decide whether to widen access when startup writes /var/cache/status and the client lists ConfigMaps.

    Expected result and reasoning

    The supplied requirements support neither behavior. Configure the cache path as /tmp/status and the client to get the known ConfigMap. If the real product truly needs another operation, revise the requirement and assess its narrow permission; do not restore wildcard rights as the default repair.

  4. 4. Build a meaningful acceptance record

    State expected results for the alternative implementation, Secret access, /etc writes and a privileged init container.

    Expected result and reasoning

    Normal startup and the named ConfigMap read must work; Secret reads and root filesystem writes must fail; an enforced privilege rule must reject the init container too. A service that simply fails every operation has not passed the exercise.

Your deliverable: A repaired status workload design, a field-to-requirement map, decisions for the two B failures and an acceptance record with allowed and denied cases.

The exercise is a manifest and access-decision review on supplied facts; the verification commands on this page show how to confirm the same outcomes on a real cluster.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Repair the code

Privileged container lab: Reject a privileged workload before deployment

NetworkPolicy lab: Lock down a shop namespace

Run a local project

Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.

Node.js 22+ offline · kind/Docker/kubectl for live cluster · 45–60 min

Verify Kubernetes admission and network boundaries

Repair warning-only admission and an overbroad network rule, then apply them to a disposable cluster and probe allowed and denied paths.

Needs: The live-cluster steps (2 to 5 in README.txt) need Docker with Linux containers, kind v0.31.0, kubectl v1.35, internet access and another 75–120 minutes. The offline tests need only Node.js.

In the extracted folder, run:

Shell
node --test exercise.test.mjs
node render.mjs
# Follow README.txt for pinned cluster setup and live probes

12 regression tests · The starter intentionally fails some cases.

Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.

The offline manifest checks run in the site test suite. The live kind and Calico path, with pinned setup, probes and cleanup, has been run end to end; it is a single-node teaching fixture, not a production baseline.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. Kubernetes Pod Security Standards. Kubernetes v1.37.
  2. Kubernetes RBAC good practices. Kubernetes v1.37.
  3. Kubernetes security checklist. Kubernetes v1.37.
  4. Kubernetes Pod Security Admission. Kubernetes v1.37.
  5. Kubernetes: enforce Pod Security Standards with namespace labels. Kubernetes v1.37.
  6. kubectl auth can-i reference. Kubernetes v1.37.

Reference information

Download this guide as Markdown