Skip to main content
mastering ckad certified kubernetes application developer

Security and RBAC Solutions

7 min read Chapter 57 of 87
Summary

Step-by-step solutions for Exercises 3 and 4: creating...

Step-by-step solutions for Exercises 3 and 4: creating a locked-down Pod with non-root user, read-only root filesystem, and dropped capabilities; and creating a ServiceAccount with a Role granting get/list on pods, bound via RoleBinding and assigned to a Pod.

Security and RBAC Solutions

Solution: Exercise 3 — Locked-Down Security Context

This exercise requires creating a Pod with strict security constraints: non-root execution, read-only root filesystem, no privilege escalation, all capabilities dropped, and a writable emptyDir for temporary files.

Step 1: Write the Pod Manifest

apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 1000
    runAsNonRoot: true
  volumes:
    - name: tmp-dir
      emptyDir: {}
  containers:
    - name: app
      image: busybox
      command: ["sh", "-c", "sleep 3600"]
      securityContext:
        readOnlyRootFilesystem: true
        allowPrivilegeEscalation: false
        capabilities:
          drop:
            - ALL
      volumeMounts:
        - name: tmp-dir
          mountPath: /tmp

Key decisions:

  • Pod-level securityContext: Sets runAsUser: 1000, runAsGroup: 1000, and runAsNonRoot: true. These apply to all containers in the Pod.
  • Container-level securityContext: Sets readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, and drops all Linux capabilities. These fields only exist at the container level.
  • emptyDir volume: Mounted at /tmp to provide writable space. Without this, any write operation (including to /tmp) fails.

Step 2: Apply

kubectl apply -f secure-pod.yaml
kubectl wait --for=condition=ready pod/secure-pod --timeout=30s

Step 3: Verify User and Group

kubectl exec secure-pod -- id

Expected output:

uid=1000 gid=1000 groups=1000

The process runs as UID 1000 with GID 1000 — no root access.

kubectl exec secure-pod -- whoami

Expected output:

whoami: unknown uid 1000

This is normal — busybox’s /etc/passwd does not contain an entry for UID 1000. The process runs as UID 1000 regardless of whether a username mapping exists.

Step 4: Verify Read-Only Root Filesystem

kubectl exec secure-pod -- touch /test

Expected output:

touch: /test: Read-only file system

The root filesystem is mounted read-only. Attempting to write anywhere outside the emptyDir mount fails.

Step 5: Verify Writable emptyDir

kubectl exec secure-pod -- touch /tmp/test
kubectl exec secure-pod -- ls -la /tmp/test

Expected output:

-rw-r--r--    1 1000     1000             0 ... /tmp/test

The file is created successfully. It is owned by UID 1000 and GID 1000 (matching the Pod’s runAsUser and runAsGroup).

Step 6: Verify Dropped Capabilities

kubectl exec secure-pod -- cat /proc/1/status | grep -i cap

Expected output:

CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000

All capability sets are zeroed out — no Linux capabilities are available to the container process.

Step 7: Verify No Privilege Escalation

kubectl exec secure-pod -- cat /proc/1/status | grep NoNewPrivs

Expected output:

NoNewPrivs:	1

The no_new_privs flag is set, preventing setuid/setgid binaries from granting elevated privileges.

Troubleshooting

Pod enters CrashLoopBackOff: The busybox image works with runAsUser: 1000. Some images (notably nginx:1.25 without modification) fail when run as non-root because they try to bind to port 80 or write to privileged directories. If using nginx, add writable emptyDir volumes for /var/cache/nginx, /var/run, and /tmp, and configure nginx to listen on a non-privileged port (above 1024).

touch /tmp/test fails with permission denied: Verify the emptyDir volume is mounted at /tmp. Check volumeMounts[].mountPath matches exactly. Also verify that runAsUser is set — without it, the container may run as root on some runtimes but the emptyDir might have unexpected permissions.

Capabilities still showing non-zero values: Ensure capabilities.drop: ["ALL"] is at the container level securityContext, not the Pod level. Capabilities are a container-level field and are silently ignored if placed at the Pod level.


Solution: Exercise 4 — ServiceAccount with RBAC

This exercise requires creating a namespace with a ServiceAccount, a Role granting read access to Pods, a RoleBinding connecting them, and a Pod using the ServiceAccount.

Step 1: Create the Namespace

kubectl create namespace rbac-lab

Step 2: Create the ServiceAccount

kubectl create serviceaccount pod-inspector -n rbac-lab

Verify:

kubectl get serviceaccount pod-inspector -n rbac-lab
NAME             SECRETS   AGE
pod-inspector    0         3s

Step 3: Create the Role

kubectl create role pod-reader \
  --verb=get,list \
  --resource=pods \
  -n rbac-lab

Verify:

kubectl get role pod-reader -n rbac-lab -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: rbac-lab
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]

The Role grants get and list on pods in the rbac-lab namespace. No other resources or verbs are permitted.

Step 4: Create the RoleBinding

kubectl create rolebinding pod-reader-binding \
  --role=pod-reader \
  --serviceaccount=rbac-lab:pod-inspector \
  -n rbac-lab

Verify:

kubectl get rolebinding pod-reader-binding -n rbac-lab -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader-binding
  namespace: rbac-lab
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pod-reader
subjects:
  - kind: ServiceAccount
    name: pod-inspector
    namespace: rbac-lab

The --serviceaccount format is namespace:name. This is the most common mistake — using pod-inspector instead of rbac-lab:pod-inspector binds no subject.

Step 5: Create the Pod

apiVersion: v1
kind: Pod
metadata:
  name: inspector
  namespace: rbac-lab
spec:
  serviceAccountName: pod-inspector
  containers:
    - name: inspector
      image: curlimages/curl
      command: ["sh", "-c", "sleep 3600"]

Apply:

kubectl apply -f inspector-pod.yaml
kubectl wait --for=condition=ready pod/inspector -n rbac-lab --timeout=30s

Alternatively, create the Pod imperatively:

kubectl run inspector \
  --image=curlimages/curl \
  --command -- sh -c "sleep 3600" \
  --overrides='{"spec":{"serviceAccountName":"pod-inspector"}}' \
  -n rbac-lab

Step 6: Verify Permissions

Test allowed operations:

kubectl auth can-i get pods \
  --as=system:serviceaccount:rbac-lab:pod-inspector \
  -n rbac-lab
yes
kubectl auth can-i list pods \
  --as=system:serviceaccount:rbac-lab:pod-inspector \
  -n rbac-lab
yes

Test denied operations:

kubectl auth can-i delete pods \
  --as=system:serviceaccount:rbac-lab:pod-inspector \
  -n rbac-lab
no
kubectl auth can-i get secrets \
  --as=system:serviceaccount:rbac-lab:pod-inspector \
  -n rbac-lab
no
kubectl auth can-i get pods \
  --as=system:serviceaccount:rbac-lab:pod-inspector \
  -n default
no

The ServiceAccount can get and list Pods in rbac-lab, but cannot delete Pods, access Secrets, or read Pods in other namespaces. The permissions are scoped exactly as defined by the Role and RoleBinding.

Step 7: Verify from Inside the Pod

kubectl exec inspector -n rbac-lab -- sh -c '
  TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
  APISERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}
  CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  
  echo "=== GET pods (should succeed) ==="
  curl -s -o /dev/null -w "%{http_code}" \
    --cacert $CA \
    -H "Authorization: Bearer $TOKEN" \
    ${APISERVER}/api/v1/namespaces/rbac-lab/pods
  
  echo ""
  echo "=== GET secrets (should fail with 403) ==="
  curl -s -o /dev/null -w "%{http_code}" \
    --cacert $CA \
    -H "Authorization: Bearer $TOKEN" \
    ${APISERVER}/api/v1/namespaces/rbac-lab/secrets
'

Expected output:

=== GET pods (should succeed) ===
200
=== GET secrets (should fail with 403) ===
403

HTTP 200 confirms the ServiceAccount can list Pods. HTTP 403 confirms it cannot access Secrets.

Troubleshooting

kubectl auth can-i get pods returns no for the ServiceAccount: Verify the RoleBinding exists in the correct namespace (rbac-lab, not default). Run kubectl get rolebinding -n rbac-lab to confirm. Also verify the --serviceaccount flag used the namespace:name format.

kubectl auth can-i returns yes for all operations: You may be testing as a cluster-admin user. Make sure you include the --as=system:serviceaccount:rbac-lab:pod-inspector flag.

Pod shows ErrImagePull for curlimages/curl: This image requires internet access. If your cluster has no outbound connectivity, substitute with busybox (it includes basic networking tools via wget).

Token file not found at /var/run/secrets/kubernetes.io/serviceaccount/token: Check whether automountServiceAccountToken: false is set on either the ServiceAccount or the Pod. If so, remove it — this exercise requires the token to be mounted.

RoleBinding references wrong namespace for subject: The subjects[].namespace field in the RoleBinding must match the namespace where the ServiceAccount lives. If the ServiceAccount is in rbac-lab, the subject namespace must be rbac-lab. A mismatch means the binding grants permissions to a non-existent ServiceAccount.