Skip to content
DevOps AI ToolKit
Newsletter
Core Guide · Cloud & Security

Kubernetes Security

A production hardening guide for Kubernetes — the threat model, RBAC and least privilege, Pod Security Standards, NetworkPolicy, admission control, and runtime security — with insecure vs. hardened YAML side by side and the consequence of each setting spelled out.

Last reviewed August 2026 Hardening guide · 27 min read

Technically validated: YAML targets Kubernetes 1.29+ (Pod Security Admission GA, NetworkPolicy v1). Manifests are illustrative hardening patterns; test against your cluster's policies before applying.

On this page

A default Kubernetes cluster is convenient and dangerously permissive: pods run as root, can talk to every other pod, and often carry more RBAC than they need. Hardening Kubernetes isn’t one setting — it’s defense in depth across the pod, the network, the API server, and the supply chain. This guide walks each layer, and for the ones that matter most it shows the insecure default beside the hardened version, with the security consequence stated plainly.

The threat model

Think about what an attacker gains at each step, because your controls should map to these:

  • A compromised container — can it become root on the node? (security context, capabilities)
  • Lateral movement — can that pod reach every other pod and the API server? (NetworkPolicy, service-account tokens)
  • Privilege escalation — does the pod’s service account have RBAC to create pods, read secrets, or edit cluster roles? (RBAC least privilege)
  • The supply chain — is the image trusted, scanned, and signed? (image provenance, admission control)
  • The control plane — is etcd encrypted, the API server audited, the kubelet locked down? (cluster config)

Every section below reduces the blast radius of one of these.

Pod security context — stop running as root

The most common finding in any cluster audit: containers running as root with a writable root filesystem and full Linux capabilities. A container breakout from that pod is root on the node.

Insecure default· yaml
# runs as root, writable FS, all caps
spec:
containers:
  - name: app
    image: myapp:1.0
    # no securityContext at all
Hardened· yaml
spec:
securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  fsGroup: 10001
  seccompProfile:
    type: RuntimeDefault
containers:
  - name: app
    image: myapp:1.0
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]

Why it matters: The hardened pod cannot escalate to root, cannot write its root filesystem (defeating many exploits and persistence techniques), drops every Linux capability, and applies the default seccomp profile that blocks dangerous syscalls. A breakout now lands as an unprivileged user in a locked-down sandbox instead of root on the node.

Pod Security Standards (PSA)

Kubernetes ships three built-in levels — privileged, baseline, restricted — enforced per namespace by the built-in Pod Security Admission controller. restricted encodes the hardened context above as a namespace policy, so violating pods are rejected at admission.

apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted   # warn on other namespaces too

RBAC and least privilege

RBAC decides what every user and service account can do. The failure mode is wildcards — a Role with verbs: ["*"] on resources: ["*"] is admin, and a pod bound to it turns a container compromise into cluster takeover.

Over-privileged· yaml
kind: Role
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
Least privilege· yaml
kind: Role
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["app-config"]
  verbs: ["get", "list", "watch"]

Why it matters: The scoped Role grants read-only access to one named ConfigMap — nothing else. If the pod using it is compromised, the attacker inherits exactly that, not the ability to read every secret or create privileged pods. Grant the minimum verbs on the minimum resources, and prefer Role (namespaced) over ClusterRole.

NetworkPolicy — default deny

By default, every pod can reach every other pod. That flat network is how one compromised service pivots to the database. NetworkPolicy is deny-by-default once a policy selects a pod, so you start by denying all traffic in a namespace, then allow only what’s needed.

# 1) Deny all ingress in the namespace...
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-ingress, namespace: payments }
spec:
  podSelector: {}
  policyTypes: [Ingress]
---
# 2) ...then allow only the API pods to reach the DB on 5432.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: db-from-api, namespace: payments }
spec:
  podSelector: { matchLabels: { app: postgres } }
  policyTypes: [Ingress]
  ingress:
    - from: [{ podSelector: { matchLabels: { app: api } } }]
      ports: [{ protocol: TCP, port: 5432 }]

Secrets and encryption at rest

Kubernetes Secrets are only base64-encoded, not encrypted — anyone who can read the Secret object or etcd can read them. Two things to fix that:

  • Encryption at rest for etcd (an EncryptionConfiguration with a KMS provider) so Secrets aren’t plaintext on disk.
  • Least-privilege RBAC on Secrets — very few service accounts should be able to get/list them.
  • For real secret management, integrate an external store (Vault, cloud secret managers) via a CSI driver or operator, so secrets are pulled at runtime rather than living in the cluster.

Admission control and policy as code

Pod Security Admission covers pod hardening; for everything else — “no latest tags,” “images only from our registry,” “every namespace has a NetworkPolicy” — use a policy engine that validates (or mutates) objects at admission:

  • Kyverno — policies written as Kubernetes YAML; gentle learning curve.
  • OPA/Gatekeeper — policies in Rego; more powerful, steeper curve.
# Kyverno: reject images not from our registry
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: allowed-registries }
spec:
  validationFailureAction: Enforce
  rules:
    - name: only-approved-registry
      match: { any: [{ resources: { kinds: [Pod] } }] }
      validate:
        message: "images must come from registry.example.com"
        pattern:
          spec:
            containers:
              - image: "registry.example.com/*"

Supply chain: scan, sign, verify

  • Scan images for known CVEs before deploy (Trivy, Grype) — wire it into the pipeline so a critical CVE fails the build.
  • Sign images (Cosign/Sigstore) and verify signatures at admission, so only images your pipeline built can run.
  • Generate an SBOM so you can answer “are we affected by CVE-X?” in minutes, not days.
  • Pull only from private/approved registries (enforced by the admission policy above).

Cluster and control-plane hardening

  • API server — no anonymous auth, audit logging enabled, and restricted network exposure.
  • etcd — encryption at rest, mTLS between members, and access limited to the API server.
  • kubelet — authentication + authorization on, --anonymous-auth=false, read-only port disabled.
  • Nodes — minimal OS, patched, no extra listening services; treat them as cattle.
  • Benchmark it — run kube-bench against the CIS Kubernetes Benchmark to get a concrete, prioritized list of gaps.

Runtime security and audit logging

Prevention isn’t perfect; you need detection too.

  • Runtime detection (Falco) watches syscalls and flags anomalies — a shell spawned in a container, a write to /etc, an unexpected outbound connection.
  • Audit logging on the API server records who did what — essential for incident response and for spotting RBAC abuse.
  • Ship both to your logging/SIEM stack so alerts are actionable.

Ingress and TLS

  • Terminate TLS at the ingress with certificates managed by cert-manager (automatic issuance and renewal from Let’s Encrypt or an internal CA).
  • Redirect HTTP→HTTPS, set HSTS, and keep the ingress controller patched — it’s internet-facing.
  • Rate-limit and, where appropriate, put a WAF in front of the ingress.

GitOps and incident response

  • GitOps (Argo CD/Flux) makes the cluster’s desired state a reviewed, audited Git history — a security control in itself (no kubectl apply from a laptop, every change is a PR). Lock down who can write to the config repo and what the GitOps controller’s RBAC allows.
  • Incident response: have a runbook to isolate a suspect pod with a deny-all NetworkPolicy, revoke a service account, capture forensics, and rotate anything the pod could touch. Practice it before you need it.

Production checklist

  • Namespaces enforce the restricted Pod Security Standard (rolled out via warn/audit first).
  • Pods set runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false, capabilities.drop: [ALL], seccompProfile: RuntimeDefault.
  • No privileged containers or host namespaces for application workloads.
  • Default-deny NetworkPolicy per namespace, on a CNI that enforces it; explicit allows only.
  • RBAC scoped to specific verbs/resources; no wildcards; automountServiceAccountToken: false where unused.
  • etcd encryption at rest; Secret access restricted; external secret store for real credentials.
  • Admission policies (Kyverno/Gatekeeper) enforce registry allow-lists and ban latest.
  • Images scanned + signed, verified at admission; SBOMs generated.
  • Falco + API audit logs shipped to a SIEM; CIS benchmark run via kube-bench.

Frequently asked questions

What’s the single most impactful Kubernetes hardening step? Enforce the restricted Pod Security Standard on your namespaces. It forces non-root, no-privilege-escalation, dropped-capabilities pods, closing the most common and most severe gap in one policy.

Are Kubernetes Secrets encrypted? No — they’re base64-encoded, which is plaintext. Enable KMS-backed encryption at rest for etcd and restrict who can read Secrets via RBAC; use an external secret store for sensitive credentials.

Why can every pod reach every other pod? Kubernetes networking is flat by default. Add a default-deny NetworkPolicy per namespace and explicitly allow only required flows — and confirm your CNI actually enforces NetworkPolicy.

How do I stop containers running as root? Set a pod/container securityContext with runAsNonRoot: true and a non-zero runAsUser, and enforce it namespace-wide with the restricted Pod Security Standard so violations are rejected at admission.

OPA/Gatekeeper or Kyverno? Both enforce policy at admission. Kyverno uses Kubernetes-native YAML and is quicker to adopt; Gatekeeper uses Rego and is more expressive. Pick based on your team’s comfort — either beats no admission policy.

How do I know how far off I am? Run kube-bench against the CIS Kubernetes Benchmark for a scored, prioritized gap list, and pair it with an image scanner (Trivy) and runtime detection (Falco).

Continue learning

Related Core Guides that build on this one.

Written by James Joyner IV, Sr. Systems Software Engineer — for engineers who run what they build.

Last reviewed August 2026. Found an error or an out-of-date command? Tell us — accuracy is the point of a Core Guide.