Security and RBAC Solutions
SummaryStep-by-step solutions for Exercises 3 and 4: creating...
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: SetsrunAsUser: 1000,runAsGroup: 1000, andrunAsNonRoot: true. These apply to all containers in the Pod. - Container-level
securityContext: SetsreadOnlyRootFilesystem: true,allowPrivilegeEscalation: false, and drops all Linux capabilities. These fields only exist at the container level. - emptyDir volume: Mounted at
/tmpto 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.