You can run pods, expose services, and store config now — but who is allowed to touch your cluster, and what are your pods allowed to do while they run? This final part of the series is about locking things down: we will write recipes for ServiceAccounts, Roles and Bindings, and Pod Security Standards. By the end you will have a working least-privilege setup and a short hygiene checklist you can reuse on every project.
Before the recipes, it helps to picture the path of every request to the Kubernetes API. When you run kubectl get pods, three gates stand between you and the answer:
- Authentication — who are you? The API server checks your identity: a client certificate, a bearer token, or a ServiceAccount token.
- Authorization — may you do this? The most common mode here is RBAC (Role-Based Access Control). RBAC rules say things like "user X may list pods in namespace Y."
- Admission control — is the object you are sending acceptable? Admission plugins can reject or mutate a request even after RBAC approves it, for example refusing a pod that wants to run as root.
Think about this like a security guard doing a threat-model exercise: who could spoof an identity, tamper with a manifest, or escalate from "read pods" to "control the node"? Every recipe below exists to shrink one of those answers. That mindset — least privilege, meaning every identity gets exactly the access it needs and nothing more — is the whole game.
What you need
- A working local cluster from Part 1 (kind or minikube), with
kubectlconfigured. - Comfort with Deployments and Pods from earlier parts.
- Nothing else — every file below is created inline.
Recipe 1: Give the app its own ServiceAccount
Problem: Every pod that talks to the Kubernetes API needs an identity. By default, pods get the namespace's default ServiceAccount, which means you cannot tell apps apart or limit them individually.
A ServiceAccount is just a named identity for a workload inside the cluster (as opposed to a human user, which Kubernetes manages differently). Create one and run a pod with it:
kubectl create serviceaccount clinerds-demo-sa apiVersion: v1 kind: Pod metadata: name: clinerds-demo spec: serviceAccountName: clinerds-demo-sa containers: - name: web image: hashicorp/http-echo:latest args: - "-text=hello from a named identity" ports: - containerPort: 5678 kubectl apply -f pod-with-sa.yaml kubectl wait --for=condition=Ready pod/clinerds-demo --timeout=60s Kubernetes mounts a token for that account into the pod automatically. Look inside:
kubectl exec clinerds-demo -- ls /var/run/secrets/kubernetes.io/serviceaccount ca.crt namespace token Now ask what this identity is allowed to do. kubectl auth can-i answers permission questions, and --as lets you impersonate another identity:
kubectl auth can-i --list --as=system:serviceaccount:default:clinerds-demo-sa Resources Non-Resource URLs ResourceNames Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] How it works: ServiceAccounts are namespaced, and the API server refers to them as system:serviceaccount:<namespace>:<name>. The token mounted in the pod is a signed JSON Web Token (JWT) that the API server can verify — that is the authentication gate. Notice the account can do almost nothing: the only line is a self-check every identity gets. A fresh ServiceAccount is powerless until you grant it permissions, which is exactly what you want. Recipe 2 adds just enough power for a real job.
- Variations: If your app never calls the Kubernetes API, set
automountServiceAccountToken: falseon the pod or ServiceAccount. No token mounted means nothing to steal.
Recipe 2: Least privilege with a Role and RoleBinding
Problem: Your app needs to list pods in its own namespace (say, to find its siblings), but it should not be able to delete anything or peek into other namespaces.
RBAC permissions come in pairs: a Role defines what is allowed, and a RoleBinding attaches that Role to an identity. Both are namespaced, so this grant can never leak into another namespace.
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: pod-reader namespace: default rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: clinerds-demo-reads-pods namespace: default subjects: - kind: ServiceAccount name: clinerds-demo-sa namespace: default roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io kubectl apply -f role-and-binding.yaml Now prove the grant does exactly what it says — no more, no less:
kubectl auth can-i get pods --as=system:serviceaccount:default:clinerds-demo-sa kubectl auth can-i delete pods --as=system:serviceaccount:default:clinerds-demo-sa kubectl auth can-i get pods --namespace=kube-system --as=system:serviceaccount:default:clinerds-demo-sa yes no no And the same check from inside the pod, using its own token against the API directly:
kubectl exec clinerds-demo -- wget -qO- --header="Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ --no-check-certificate https://kubernetes.default.svc/api/v1/namespaces/default/pods How it works: The Role's rules are a whitelist. apiGroups: [""] means the core API group (where pods live); resources and verbs narrow it to read-only access on pods. The RoleBinding is the glue: subjects points at the ServiceAccount, roleRef points at the Role. Because both objects live in default, the permission is valid only there — the third can-i above proves it. This is least privilege in practice: the app can do its job, and a compromised pod cannot delete workloads or wander into kube-system.
- When it goes wrong: A
403 Forbiddenfrom the API usually means a missing binding or a typo in the subject name.kubectl auth can-i --as=...is your debugger — ask the API directly instead of staring at YAML.
Recipe 3: ClusterRoles, and the built-in faces
Problem: Rewriting "read everything" as a Role in ten namespaces is tedious, and some resources (nodes, namespaces themselves) do not belong to any namespace at all.
Kubernetes ships a ClusterRole, which is cluster-wide, plus a set of ready-made ones. The most useful are the aggregate "faces": view (read-only on most objects), edit (view plus change), and admin (full control within a namespace, including its RBAC). You can list them:
kubectl get clusterroles | grep -E '^(admin|edit|view)\s' Here is the trick that saves the repetition: you can reference a ClusterRole from a namespaced RoleBinding. The ClusterRole supplies the rules, the RoleBinding supplies the scope. Read-only access to one namespace, no YAML rules to write:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: auditor-read-only namespace: default subjects: - kind: ServiceAccount name: clinerds-demo-sa namespace: default roleRef: kind: ClusterRole name: view apiGroup: rbac.authorization.k8s.io kubectl apply -f view-binding.yaml kubectl auth can-i get deployments --as=system:serviceaccount:default:clinerds-demo-sa # yes kubectl auth can-i get secrets --as=system:serviceaccount:default:clinerds-demo-sa # no How it works: A ClusterRole bound with a RoleBinding applies only inside the binding's namespace; bound with a ClusterRoleBinding, it applies everywhere. Note that view deliberately excludes Secrets — reading Secrets is treated as privilege escalation, since they often contain credentials. Which brings us to the big warning: cluster-admin is the built-in ClusterRole with * on everything. Binding it with a ClusterRoleBinding makes that identity root of the entire cluster. It exists for break-glass and initial setup, not for apps, CI pipelines, or teammates. If you are tempted to use it, that is a sign to write a tighter Role instead.
Recipe 4: Enforce Pod Security Standards on a namespace
Problem: RBAC controls API calls, but nothing so far stops someone with pod-creation rights from running a privileged container that can see the whole node.
Pod Security Standards are built-in admission policies with three levels: privileged (anything goes), baseline (blocks known escalation paths), and restricted (current hardening best practice). You enforce them with a namespace label:
kubectl label namespace default pod-security.kubernetes.io/enforce=restricted Now try to create a pod that violates the standard — one running as root with privilege escalation:
apiVersion: v1 kind: Pod metadata: name: naughty-pod spec: containers: - name: shell image: busybox:1.36 command: ["sleep", "3600"] securityContext: allowPrivilegeEscalation: true kubectl apply -f naughty-pod.yaml Error from server (Forbidden): error when creating "naughty-pod.yaml": pods "naughty-pod" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true The compliant version sets a securityContext at both pod and container level:
apiVersion: v1 kind: Pod metadata: name: clinerds-demo-hardened spec: serviceAccountName: clinerds-demo-sa automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: web image: hashicorp/http-echo:latest args: - "-text=hardened and happy" ports: - containerPort: 5678 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] How it works: The label turns on an admission controller that inspects every pod created in that namespace — this is the third gate from the intro doing its job. Each field answers one escalation path: runAsNonRoot keeps the process away from UID 0; seccompProfile: RuntimeDefault filters dangerous Linux system calls; allowPrivilegeEscalation: false blocks gaining extra rights mid-run; capabilities: drop: ["ALL"] strips Linux capabilities the app does not need. Try kubectl apply — the hardened pod runs fine, because hashicorp/http-echo needs none of those powers. Most well-behaved images do not either.
- Variations: Use
pod-security.kubernetes.io/enforce=baselinewhile migrating legacy workloads, pluspod-security.kubernetes.io/warn=restrictedso violations print warnings instead of failing. Audit first, enforce later.
Recipe 5: The everyday hygiene checklist
Problem: Security is not one feature; it is a pile of small habits. Here is the checklist to run on every project, with the commands you already know.
- Pin images. Never deploy
latestin anything you care about — tag and, better, pin by digest so the image cannot change under you:image: hashicorp/http-echo@sha256:.... Pull the digest withdocker inspector your registry's UI. - Drop the token when unused.
automountServiceAccountToken: false(Recipe 1) removes the most commonly stolen file from pods that do not need it. - Read-only root filesystem.
readOnlyRootFilesystem: true(Recipe 4) means malware cannot rewrite the binary it hijacked. Mount anemptyDirfor any temp space the app needs. - Firewall pod traffic with NetworkPolicy. RBAC guards the API; NetworkPolicies guard the network between pods. Start from default-deny and open what you need:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: default spec: podSelector: {} policyTypes: - Ingress - Egress Note: this needs a network plugin that enforces policies (kind's default CNI does not; minikube with --cni=calico does).
- Handle Secrets properly. Never bake credentials into images or ConfigMaps — use Secrets with RBAC tight around them, as we covered in Part 7.
- Keep the cluster updated. Control plane and node patches fix real CVEs. On managed services, enable auto-upgrade channels; on your own clusters, upgrade one minor version at a time.
How it works: Each item closes a different door — supply chain (pinned images), credential theft (no token), persistence (read-only FS), lateral movement (NetworkPolicy), secret sprawl, and known exploits (updates). No single one is sufficient; together they are a decent moat for a beginner cluster and a solid baseline for a production one.
Where to go next
You made it — all ten parts, from an empty laptop to a hardened, least-privilege cluster. You can now stand up a local cluster, run pods and deployments, wire up services and ingress, manage config, storage, and state, and lock the whole thing down. Revisit Part 1: Spin Up a Local Cluster any time you need a fresh sandbox, or browse the whole menu at the Kubernetes Cookbook series hub. When you are ready to go deeper on access control, the official RBAC reference is the canonical next read. Happy cooking — and keep those privileges least.
