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
RBAC example: ServiceAccount, Role, and RoleBinding for read-only pod access

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)
  • policyTypesIngress, 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
NetworkPolicy allowing only frontend pods to reach the API server on 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
Default-deny-all NetworkPolicy — blocks all ingress and egress traffic to every pod in the namespace

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
Namespace-level Pod Security Admission labels enforcing Restricted mode

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
Complete workflow: create namespace, ServiceAccount, Role, RoleBinding, and deploy with Restricted security context

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
Step-by-step network isolation: default-deny, DNS allow, and frontend-to-backend communication

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-admin for 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:

  1. Create a namespace production with Restricted Pod Security enforced
  2. Create a dedicated ServiceAccount for each tier: frontend-sa, api-sa, db-sa
  3. 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)
  4. Apply a default-deny NetworkPolicy to production
  5. Allow DNS egress for all pods
  6. Allow: frontend → API (port 3000), API → database (port 5432)
  7. Deny all other traffic
  8. 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-sa to 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.

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen July 26, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment