Introduction
Kubernetes security starts with controlling who can do what and what your pods are allowed to run. In this tutorial, we'll implement RBAC (Role-Based Access Control) and Pod Security Standards to lock down a Kubernetes cluster.
Prerequisites
- A running Kubernetes cluster (minikube, kind, or managed)
kubectlinstalled and configured- Basic Kubernetes knowledge (pods, namespaces, deployments)
1. Understanding RBAC Components
Kubernetes RBAC has four key objects:
| Object | Scope | Purpose |
|--------|-------|---------|
| Role | Namespace | Defines permissions within a namespace |
| ClusterRole | Cluster | Defines permissions cluster-wide |
| RoleBinding | Namespace | Grants a Role to a user/group |
| ClusterRoleBinding | Cluster | Grants a ClusterRole cluster-wide |
2. Creating a Namespace for Our Lab
# Create a dedicated namespace
kubectl create namespace security-lab
# Verify
kubectl get namespaces | grep security-lab
3. Creating RBAC Roles
Read-Only Role
Create a role that only allows viewing resources:
# read-only-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: security-lab
name: pod-reader
rules:
apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list"]
Developer Role with Limited Write Access
# developer-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: security-lab
name: developer
rules:
apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
apiGroups: [""]
resources: ["pods/exec", "pods/portforward"]
verbs: ["create"]
# Explicitly deny delete on critical resources
apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list"]
# Apply both roles
kubectl apply -f read-only-role.yaml
kubectl apply -f developer-role.yaml
4. Creating Service Accounts and Bindings
# service-accounts.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: viewer-sa
namespace: security-lab
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: developer-sa
namespace: security-lab
# role-bindings.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: viewer-binding
namespace: security-lab
subjects:
kind: ServiceAccount
name: viewer-sa
namespace: security-lab
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: security-lab
subjects:
kind: ServiceAccount
name: developer-sa
namespace: security-lab
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
kubectl apply -f service-accounts.yaml
kubectl apply -f role-bindings.yaml
5. Testing RBAC Permissions
Use kubectl auth can-i to verify permissions:
# Test viewer permissions
kubectl auth can-i get pods \
--namespace security-lab \
--as system:serviceaccount:security-lab:viewer-sa
# Output: yes
kubectl auth can-i create pods \
--namespace security-lab \
--as system:serviceaccount:security-lab:viewer-sa
# Output: no
# Test developer permissions
kubectl auth can-i create deployments \
--namespace security-lab \
--as system:serviceaccount:security-lab:developer-sa
# Output: yes
kubectl auth can-i delete persistentvolumeclaims \
--namespace security-lab \
--as system:serviceaccount:security-lab:developer-sa
# Output: no
6. RBAC Audit Script
Create a script to audit who has access to what:
#!/bin/bash
# rbac-audit.sh — Audit RBAC permissions in a namespace
NAMESPACE=${1:-security-lab}
echo "=== RBAC Audit for namespace: $NAMESPACE ==="
echo ""
echo "--- Roles ---"
kubectl get roles -n $NAMESPACE -o custom-columns=\
NAME:.metadata.name,\
RESOURCES:.rules[*].resources,\
VERBS:.rules[*].verbs
echo ""
echo "--- RoleBindings ---"
kubectl get rolebindings -n $NAMESPACE -o custom-columns=\
NAME:.metadata.name,\
ROLE:.roleRef.name,\
SUBJECTS:.subjects[*].name
echo ""
echo "--- ClusterRoleBindings (affecting this namespace) ---"
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.subjects[]?.namespace == "'$NAMESPACE'") | .metadata.name'
chmod +x rbac-audit.sh
./rbac-audit.sh security-lab
7. Pod Security Standards (PSS)
Kubernetes 1.25+ replaces PodSecurityPolicy with Pod Security Standards, enforced via namespace labels.
Three Security Levels
| Level | Description |
|-------|-------------|
| Privileged | No restrictions (default) |
| Baseline | Prevents known privilege escalations |
| Restricted | Heavily restricted, security best practices |
Enforcement Modes
| Mode | Behavior |
|------|----------|
| enforce | Rejects violating pods |
| audit | Logs violations, allows pods |
| warn | Shows warnings, allows pods |
8. Applying Pod Security Standards
# Apply restricted security standard to our namespace
kubectl label namespace security-lab \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
# Verify labels
kubectl get namespace security-lab --show-labels
9. Testing Pod Security Enforcement
This Pod Will Be REJECTED (privileged container):
# privileged-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: privileged-test
namespace: security-lab
spec:
containers:
- name: nginx
image: nginx:latest
securityContext:
privileged: true
kubectl apply -f privileged-pod.yaml
# Error: pods "privileged-test" is forbidden:
# violates PodSecurity "restricted:latest":
# privileged (container "nginx" must not set securityContext.privileged=true)
This Pod Will SUCCEED (restricted-compliant):
# secure-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-nginx
namespace: security-lab
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:latest
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: run
emptyDir: {}
kubectl apply -f secure-pod.yaml
# pod/secure-nginx created
kubectl get pods -n security-lab
# NAME READY STATUS RESTARTS AGE
# secure-nginx 1/1 Running 0 10s
10. Secure Deployment Template
Here's a production-ready deployment with full security context:
# secure-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
namespace: security-lab
labels:
app: secure-app
spec:
replicas: 2
selector:
matchLabels:
app: secure-app
template:
metadata:
labels:
app: secure-app
spec:
serviceAccountName: developer-sa
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
fsGroup: 10000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: python:3.12-slim
command: ["python", "-m", "http.server", "8080"]
ports:
- containerPort: 8080
protocol: TCP
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
- name: log-sidecar
image: busybox:latest
command: ["sh", "-c", "while true; do echo heartbeat; sleep 60; done"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
capabilities:
drop: ["ALL"]
resources:
requests:
memory: "16Mi"
cpu: "10m"
limits:
memory: "32Mi"
cpu: "50m"
---
apiVersion: v1
kind: Service
metadata:
name: secure-app-svc
namespace: security-lab
spec:
selector:
app: secure-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: secure-app-netpol
namespace: security-lab
spec:
podSelector:
matchLabels:
app: secure-app
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: security-lab
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
kubectl apply -f secure-deployment.yaml
kubectl get all -n security-lab
11. Security Checklist
Use this checklist for every namespace:
#!/bin/bash
# k8s-security-check.sh
NS=${1:-default}
echo "🔒 Kubernetes Security Checklist — Namespace: $NS"
echo "================================================="
# Check Pod Security Standards
LABELS=$(kubectl get ns $NS -o jsonpath='{.metadata.labels}')
if echo "$LABELS" | grep -q 'pod-security'; then
echo "✅ Pod Security Standards applied"
else
echo "❌ No Pod Security Standards — apply with:"
echo " kubectl label ns $NS pod-security.kubernetes.io/enforce=baseline"
fi
# Check for privileged pods
PRIV=$(kubectl get pods -n $NS -o json | \
jq '[.items[].spec.containers[] | select(.securityContext.privileged==true)] | length')
if [ "$PRIV" -gt 0 ]; then
echo "❌ $PRIV privileged container(s) found"
else
echo "✅ No privileged containers"
fi
# Check for root containers
ROOT=$(kubectl get pods -n $NS -o json | \
jq '[.items[].spec | select(.securityContext.runAsNonRoot!=true)] | length')
if [ "$ROOT" -gt 0 ]; then
echo "⚠️ $ROOT pod(s) may run as root"
else
echo "✅ All pods run as non-root"
fi
# Check for NetworkPolicies
NP=$(kubectl get networkpolicies -n $NS --no-headers 2>/dev/null | wc -l)
if [ "$NP" -gt 0 ]; then
echo "✅ $NP NetworkPolicy(ies) found"
else
echo "❌ No NetworkPolicies — pods have unrestricted network access"
fi
# Check resource limits
NO_LIMITS=$(kubectl get pods -n $NS -o json | \
jq '[.items[].spec.containers[] | select(.resources.limits==null)] | length')
if [ "$NO_LIMITS" -gt 0 ]; then
echo "⚠️ $NO_LIMITS container(s) without resource limits"
else
echo "✅ All containers have resource limits"
fi
echo ""
echo "Done. Fix ❌ items first, then ⚠️ items."
Summary
In this tutorial, you learned to:
1. Create RBAC Roles with least-privilege access
2. Bind roles to service accounts
3. Audit permissions with scripts and can-i checks
4. Enforce Pod Security Standards at the namespace level
5. Write secure pod specs that pass restricted policies
6. Add NetworkPolicies to control traffic flow
7. Automate security checks with audit scripts
Kubernetes security is layered — RBAC controls the API plane, Pod Security controls the runtime plane, and NetworkPolicies control the network plane. Implement all three for defense in depth.