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)
  • kubectl installed 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.

    Further Reading

  • Kubernetes RBAC Documentation
  • Pod Security Standards
  • NetworkPolicy Guide