Skip to content

Kubernetes Security Hardening

A default Kubernetes cluster is not secure. This page covers the layered controls needed to bring a cluster to a production security posture.

Cluster infrastructure (nodes, etcd encryption, control plane)
└── Authentication & authorisation (RBAC, OIDC)
└── Admission control (OPA/Gatekeeper, Kyverno)
└── Network policies (pod-to-pod traffic)
└── Runtime security (Falco, seccomp, AppArmor)
└── Secrets management (Vault, SOPS)

Grant the minimum permissions required. Avoid cluster-admin bindings.

# Bad — don't do this
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: developer-admin
subjects:
- kind: User
name: alice
roleRef:
kind: ClusterRole
name: cluster-admin # ← too broad
apiGroup: rbac.authorization.k8s.io
---
# Good — namespace-scoped, specific verbs
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: app-prod
name: app-deployer
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]

Audit RBAC regularly:

Terminal window
kubectl auth can-i --list --as=alice
kubectl get rolebindings,clusterrolebindings -A | grep alice

By default, all pods can talk to all pods. Lock this down:

# Deny all ingress by default in a namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: app-prod
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Allow only from the frontend to the API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: app-prod
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

Network policies require a CNI plugin that supports them: Calico, Cilium, or Weave.

Replace deprecated PodSecurityPolicy with the built-in Pod Security Admission:

# Label the namespace to enforce the restricted profile
apiVersion: v1
kind: Namespace
metadata:
name: app-prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted

restricted profile requires:

  • runAsNonRoot: true
  • No privilege escalation
  • Drop ALL capabilities
  • Seccomp RuntimeDefault or Localhost

Kyverno lets you write policies as Kubernetes resources:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-latest-tag
spec:
rules:
- name: require-image-digest
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Images must use a digest, not :latest"
pattern:
spec:
containers:
- image: "*@sha256:*"

Never store secrets in plaintext in a Git repo. Options:

ApproachTrade-offs
Kubernetes Secrets (encrypted at rest via KMS)Simple, native, but secrets visible to anyone with Secret read access
Vault + External Secrets OperatorCentralised, auditable, fine-grained TTLs — higher operational complexity
SOPS + HelmEncrypted in Git, decrypted at deploy time — good for GitOps

Falco watches kernel syscalls and fires alerts on suspicious activity:

# Example Falco rule — alert if a shell is spawned inside a container
- rule: Terminal shell in container
desc: Detect a shell run in a container
condition: >
spawned_process and container
and proc.name in (shell_binaries)
and not container.image.repository in (trusted_images)
output: >
Shell spawned in container (user=%user.name container=%container.name
image=%container.image.repository proc=%proc.name)
priority: WARNING

Run kube-bench against your cluster to get a scored report:

Terminal window
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench

Aim for 0 FAIL results in the Control Plane and Node sections.