RBAC and Security

advanced kubernetes rbac security network-policies

By default, Kubernetes is pretty open — Pods can talk to any other Pod, and users with cluster access can do almost anything. In production, that’s a security nightmare. We need to lock things down: who can do what (RBAC), how Pods are allowed to run (PodSecurity), and which Pods can talk to which (NetworkPolicies).

RBAC — Role-Based Access Control

RBAC answers the question: “Can this user/service do this action on this resource?” It has four key objects.

RBAC Model
Namespace-scoped
Role — what actions are allowed
RoleBinding — who gets the Role
Cluster-wide
ClusterRole — what actions are allowed (all namespaces)
ClusterRoleBinding — who gets the ClusterRole

The only difference between Role and ClusterRole is scope. A Role works within a single namespace. A ClusterRole works across the entire cluster (and can also cover non-namespaced resources like nodes).

Creating a Role and RoleBinding

Let’s say we want to give a developer read-only access to Pods in the dev namespace.

# Step 1: Define what's allowed
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: dev
rules:
  - apiGroups: [""]             # core API group
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
# Step 2: Bind it to a user or ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: dev-pod-reader
  namespace: dev
subjects:
  - kind: User
    name: alice
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Now alice can read Pods in the dev namespace but nothing else. That’s the principle of least privilege — give only the minimum permissions needed.

ServiceAccounts

Humans use user accounts. Pods use ServiceAccounts. Every namespace has a default ServiceAccount, but it has limited permissions. For Pods that need to talk to the Kubernetes API (like monitoring tools or CI runners), we create a dedicated ServiceAccount and bind a Role to it.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: monitoring-sa
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: monitoring-access
subjects:
  - kind: ServiceAccount
    name: monitoring-sa
    namespace: monitoring
roleRef:
  kind: ClusterRole
  name: view                    # built-in read-only ClusterRole
  apiGroup: rbac.authorization.k8s.io
# Check what a ServiceAccount can do
kubectl auth can-i list pods --as=system:serviceaccount:monitoring:monitoring-sa

Pod Security Standards

We don’t want Pods running as root, mounting the host filesystem, or running in privileged mode. Kubernetes defines three security profiles:

  • Privileged — no restrictions at all (only for system-level Pods like CNI plugins)
  • Baseline — blocks the most dangerous settings (no privileged containers, no hostNetwork)
  • Restricted — the strictest. Must run as non-root, read-only root filesystem, drop all capabilities

We enforce these at the namespace level using labels:

# Enforce restricted profile on the production namespace
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted

SecurityContext

We can also harden individual Pods with a SecurityContext. This is where we set the fine-grained security settings.

spec:
  securityContext:
    runAsNonRoot: true            # Pod must not run as root
    runAsUser: 1000               # run as UID 1000
    fsGroup: 2000                 # group ownership for mounted volumes
  containers:
    - name: app
      image: my-app:latest
      securityContext:
        readOnlyRootFilesystem: true    # can't write to container FS
        allowPrivilegeEscalation: false # prevent gaining more privileges
        capabilities:
          drop: ["ALL"]           # drop all Linux capabilities

These settings should be our defaults for production Pods. If our app needs to write files, we mount a writable volume instead of making the entire root filesystem writable.

NetworkPolicies — Pod-Level Firewall

By default, every Pod can talk to every other Pod in the cluster. NetworkPolicies let us restrict that. Think of them like firewall rules.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api                   # applies to Pods with this label
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend      # only frontend Pods can reach API
      ports:
        - port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database      # API can only talk to database
      ports:
        - port: 5432

This policy says: the api Pods can only receive traffic from frontend Pods on port 8080, and can only send traffic to database Pods on port 5432. Everything else is blocked.

One important thing: NetworkPolicies need a CNI plugin that supports them (Calico, Cilium, Weave). The default kubenet plugin doesn’t enforce them — the policies will be created but silently ignored.

A common starting pattern is a default deny rule that blocks all traffic, then we add specific allow rules:

# Default deny all ingress traffic in a namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}               # matches ALL Pods in namespace
  policyTypes:
    - Ingress                   # no ingress rules = deny all

In simple language, RBAC controls who can do what in the cluster, PodSecurity controls how containers are allowed to run, and NetworkPolicies control which Pods can talk to each other. Layer all three for defense in depth.