Kubernetes Security Baseline: RBAC, Pod Security, and Default-Deny
Out of the box a cluster lets almost anything run as root with a mounted token and an open network. Three labels, one role cleanup, and a default-deny policy fix most of it.
A fresh cluster is not secure. It is permissive, in the specific sense that the defaults assume you are the only person using it and you are trusted. Pods may run as root, service account tokens are mounted into everything by default, and every pod can talk to every other pod. That is a reasonable starting point for a laptop and a poor one for anything shared.
You do not need a security platform to fix it. You need three changes you can apply in an afternoon, in roughly this order.
1. Turn on Pod Security Admission
Pod Security Admission is built in — no agent to install. You opt namespaces into one of three levels:
| Level | What it enforces |
|---|---|
privileged | Everything (the default) |
baseline | Blocks the known-dangerous: host PID/IPC, host networking, privileged containers, hostPath |
restricted | The real target: run as non-root, drop all capabilities, no hostPath, seccomp set |
Start by labelling your workload namespaces at baseline and your platform namespaces at restricted:
kubectl label ns payments \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=baseline
I run warn and audit alongside enforce for a sprint before tightening. Enforcing a rule the instant you discover it produces a wall of denials nobody reads. The warning column tells you what you are about to break, while nothing is broken yet.
2. Audit who can do what, not just what runs
RBAC is where clusters actually leak. Two habits cover most of it.
No human holds cluster-admin. Bind humans to specific roles in specific namespaces. If someone needs cluster-wide read for a dashboard, give them a read-only ClusterRole, not the keys to everything. And check for bindings you inherited:
kubectl get clusterrolebindings -o json \
| jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'
That command has produced an uncomfortable conversation in more than one cluster I have taken over.
Do not use the default service account. Anything that never needed a token still gets one, and a token is a credential lying around in a filesystem. For workloads that do not call the API:
automountServiceAccountToken: false
For the ones that do, give them a dedicated service account bound to exactly the verbs they call. A pod that only reads ConfigMaps should have no verb that writes anything.
3. Default-deny, then open what you meant to open
Network policies have no effect until you write the deny rule. Start there:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
podSelector: {} matches every pod, and with both policy types listed and no rules, everything is blocked. Now the cluster behaves like a firewall that shipped switched off — you have to say what passes.
Then add the exceptions you actually need, one at a time:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
namespace: payments
spec:
podSelector:
matchLabels: { app: payments-api }
policyTypes: ["Egress"]
egress:
- to:
- podSelector:
matchLabels: { app: postgres }
ports:
- protocol: TCP
port: 5432
Watch the egress rules in particular. A pod that can reach the internet on any port is a data-exfiltration path with a pleasant error page.
What I would not build yet
A service mesh on day one, custom admission webhooks you maintain yourself, and a policy bundle with 400 rules you cannot explain. Baseline admission, narrow RBAC bindings, and a default-deny policy give you most of the protection for about a tenth of the operational weight.
Summary
Kubernetes defaults optimise for convenience, not containment. Label your namespaces into a Pod Security level and enforce it gradually rather than instantly. Remove inherited cluster-admin bindings, stop the default service account from carrying a token, and scope each workload identity to the verbs it uses. Then write the default-deny network policy and re-open only the flows you can name. None of this is exotic — it is three files and a pair of kubectl commands, and together they close the gaps an attacker looks for first.
SDP Clouds Team
DevOps and cloud engineers writing practical, battle-tested guides on CI/CD, Kubernetes, infrastructure as code, and production operations — every article is based on real incidents and real pipelines, not docs-page rewrites.
More about us →