As Kubernetes clusters grow from experimental deployments to production workloads, security becomes the single most critical concern. In this lesson, we dive deep into the three pillars of Kubernetes security: Role-Based Access Control (RBAC) for managing who can do what, Network Policies for controlling pod-to-pod traffic, and Pod Security Standards for enforcing how containers run. By the end, you will have a practical security posture you can apply to any cluster.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Understand and implement Kubernetes RBAC with ServiceAccounts, Roles, and RoleBindings
- Define Network Policies to isolate and secure pod-to-pod traffic
- Apply Pod Security Standards (Privileged, Baseline, Restricted) to enforce container security
- Combine all three layers to build a defence-in-depth security posture
- Troubleshoot common security misconfigurations and access denied errors
2. Why This Matters
In 2023, a misconfigured Kubernetes RBAC policy allowed attackers to pivot from a compromised web pod to a production database, exfiltrating millions of customer records. The breach was not a zero-day exploit — it was a default-excessive permission that was never reviewed.
Kubernetes security is not optional. By default, a freshly provisioned cluster grants wide-open network communication between all pods and full API access to any authenticated user. Without RBAC, Network Policies, and Pod Security Standards, your cluster is one misconfigured deployment away from a data breach or cryptojacking incident. This lesson gives you the tools to close those gaps methodically.
3. Core Concepts
3.1 Role-Based Access Control (RBAC)
RBAC in Kubernetes governs who can perform what actions on which resources. It consists of four key API objects:
- ServiceAccount — An identity for processes running in a pod. Unlike regular users, ServiceAccounts are Kubernetes resources scoped to a namespace.
- Role / ClusterRole — A set of permissions (verbs on resources). Role is namespace-scoped; ClusterRole is cluster-scoped.
- RoleBinding / ClusterRoleBinding — Grants the permissions in a Role/ClusterRole to a subject (ServiceAccount, user, or group).
The standard RBAC verbs are: get, list, watch, create, update, patch, and delete. Resources include pods, deployments, services, configmaps, secrets, and many more.
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-reader
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: pod-reader
rules:
- apiGroups: [""] # core API group
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: app-reader
namespace: production
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Key principle: least privilege. Grant only the minimum permissions needed. Never use cluster-admin for application workloads. For cluster-scoped resources (nodes, PersistentVolumes, ClusterRoles), you must use ClusterRole and ClusterRoleBinding.
3.2 Network Policies
By default, all pods in a Kubernetes cluster can communicate with each other. Network Policies change that by acting as a pod-level firewall. They are implemented by a Container Network Interface (CNI) plugin such as Calico, Cilium, or Weave Net.
A Network Policy specifies:
- podSelector — Which pods the policy applies to (labels)
- policyTypes —
Ingress,Egress, or both - ingress — Rules for allowed inbound traffic (from, ports)
- egress — Rules for allowed outbound traffic (to, ports)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Network Policies are whitelist-only. Once any policy selects a pod, all traffic not explicitly allowed is denied. This is known as the default-deny behaviour. You can create a default-deny-all policy first, then selectively allow traffic.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
3.3 Pod Security Standards
Pod Security Standards (PSS) replaced the deprecated PodSecurityPolicy (PSP) in Kubernetes v1.25+. They define three levels of container security, enforced via Pod Security Admission labels on namespaces:
- Privileged — Unrestricted. For system-level workloads like CNI plugins or monitoring agents that need host access.
- Baseline — Minimally restrictive, prevents known privilege escalations. Good default for most applications.
- Restricted — Heavily locked down. Follows pod hardening best practices. For multi-tenant or sensitive workloads.
apiVersion: v1
kind: Namespace
metadata:
name: critical-app
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
When a namespace is labelled pod-security.kubernetes.io/enforce: restricted, any pod that violates Restricted-level rules (e.g., runs as root, uses host networking, mounts host paths) will be rejected at admission time. The audit label logs violations, and warn shows a warning to the user without blocking.
4. Hands-On Practice
4.1 Create a Restricted ServiceAccount for an Application
# 1. Create a namespace with Restricted Pod Security
kubectl create namespace secure-app
kubectl label namespace secure-app \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest
# 2. Create a dedicated ServiceAccount
kubectl create serviceaccount my-app -n secure-app
# 3. Create a Role with minimal permissions
cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: secure-app
name: my-app-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get"]
EOF
# 4. Bind the Role to the ServiceAccount
kubectl create rolebinding my-app-binding \
--role=my-app-role \
--serviceaccount=secure-app:my-app \
-n secure-app
# 5. Deploy an application using the ServiceAccount
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: secure-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
serviceAccountName: my-app
securityContext:
runAsNonRoot: true
runAsUser: 1001
containers:
- name: app
image: nginx:alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
EOF
4.2 Isolate a Multi-Tier Application with Network Policies
# 1. Create a default-deny policy for the namespace
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: secure-app
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
# 2. Allow DNS egress (required by most pods)
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: secure-app
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
EOF
# 3. Allow frontend to reach backend
# (Deploy frontend and backend pods with labels app=frontend, app=backend first)
cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: secure-app
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
EOF
5. Common Errors & Solutions
Error 1: Forbidden! User "system:serviceaccount:xxx:yyy" cannot list resource "pods"
Cause: The ServiceAccount lacks a Role or RoleBinding granting the required verb on the resource.
Solution: Verify the Role exists (kubectl get role -n <namespace>) and that the RoleBinding connects the correct ServiceAccount to it (kubectl get rolebinding -n <namespace> -o yaml).
Error 2: Pod stuck in Pending with violates PodSecurity "restricted:latest"
Cause: The pod spec violates Restricted-level rules — e.g., runAsNonRoot: false, allowPrivilegeEscalation: true, or missing seccompProfile.
Solution: Review the pod's securityContext. Set runAsNonRoot: true, allowPrivilegeEscalation: false, and drop all capabilities. Use a container image that does not require root (e.g., nginxinc/nginx-unprivileged instead of nginx).
Error 3: Network Policy seems to have no effect
Cause 1: The CNI plugin does not support Network Policies (e.g., Flannel in its default mode).
Solution: Verify with kubectl get networkpolicies and test connectivity with a temporary pod. Install Calico, Cilium, or Weave Net if needed.
Cause 2: The podSelector labels do not match the target pod.
Solution: Check pod labels with kubectl get pods --show-labels -n <namespace> and ensure the Network Policy selector matches.
Error 4: Error from server (Forbidden): pods is forbidden: User cannot create resource "pods"
Cause: The ServiceAccount used by the CI/CD pipeline lacks create permission on pods (or deployments).
Solution: Extend the Role's rules array to include the missing verb: verbs: ["get", "list", "watch", "create", "update", "patch"]. For CI/CD pipelines, consider using a dedicated deployment Role.
Error 5: connection refused between services after applying Network Policies
Cause: The egress rule was defined but the destination is matched incorrectly, or the port is missing.
Solution: Check the egress rule's to selector and ports. Add temporary diagnostic pods with labels matching the egress policy. Use kubectl run tmp --image=nicolaka/netshoot -it -- /bin/bash and test with curl or nc.
6. Summary Checklist
- Created dedicated ServiceAccounts for each application (no default ServiceAccount)
- Applied least-privilege RBAC: every Role grants only the necessary verbs on specific resources
- Never used
cluster-adminfor application workloads - Applied a default-deny-all NetworkPolicy to every namespace
- Explicitly allowed DNS egress for every namespace
- Used podSelectors and ports to limit allowed traffic to what is strictly necessary
- Labelled each namespace with the appropriate Pod Security Standard level
- Adjusted pod securityContexts to comply with the enforced PSS level
- Tested RBAC permissions with
kubectl auth can-i --as=system:serviceaccount:ns:sa - Verified Network Policies with temporary netshoot pods
7. Practice Exercise
Scenario: You are the platform engineer for a company deploying a three-tier application: a React frontend, a Node.js API, and a PostgreSQL database — all in the production namespace.
Tasks:
- Create a namespace
productionwith Restricted Pod Security enforced - Create a dedicated ServiceAccount for each tier:
frontend-sa,api-sa,db-sa - Create a Role for each tier granting only the permissions it needs (e.g., the API needs to list pods, the database needs nothing on the API side)
- Apply a default-deny NetworkPolicy to
production - Allow DNS egress for all pods
- Allow: frontend → API (port 3000), API → database (port 5432)
- Deny all other traffic
- Deploy each tier with its corresponding ServiceAccount and a securityContext that passes Restricted-level checks
Test your configuration:
kubectl auth can-i --list --as=system:serviceaccount:production:api-sato verify permissions- Exec into a temporary netshoot pod and verify that frontend can reach the API, but cannot reach the database directly
- Check that a pod without Restricted compliance (e.g., running as root) is rejected
8. Next Steps
In this lesson, you secured your cluster with RBAC, Network Policies, and Pod Security Standards. These three layers form the foundation of a defence-in-depth security strategy. In the next lesson, we will explore Kubernetes Autoscaling: Horizontal Pod Autoscaler, Vertical Pod Autoscaler, and Cluster Autoscaler — learning how to automatically scale your workloads based on resource demand while keeping costs under control.
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment