Overview
Kubernetes security rests on three independent mechanisms that each surprise people used to a simpler model: RBAC is default-deny and purely additive, permissions can only be granted, never explicitly denied, so restricting access means removing a binding, not layering a deny rule on top. Pod Security Standards define three cumulative levels, Privileged, Baseline, Restricted, each adding real hardening (non-root, no privilege escalation, dropped capabilities) on top of the last. NetworkPolicy inverts the surprise the other way: pods are wide open by default with no policy at all, and only become isolated the moment any policy selects them, which is why the very first NetworkPolicy applied to a namespace is the one most likely to unexpectedly break something.
I start with Kubernetes Fundamentals if Deployment ownership, Pod readiness, Service selectors, or EndpointSlices are unfamiliar. Security troubleshooting depends on proving the normal workload and network path first; otherwise an empty Service backend can be mistaken for a NetworkPolicy denial, or an admission rejection for an application crash.
Quick Reference
| Mechanism | Default without any policy | What applying one does |
|---|---|---|
| RBAC | Deny everything | Grants are additive; there's no explicit deny |
| Pod Security Standards | No pod-level restriction | Baseline blocks known escalations; Restricted adds real hardening |
| NetworkPolicy | Pods are non-isolated, all traffic allowed | Selected pods become isolated; only explicitly allowed traffic gets through |
| Command | Description | Copy |
|---|---|---|
kubectl auth can-i create deployments -n dev | Ask the API authorization layer whether the current identity can create Deployments in dev. | |
kubectl auth can-i --list -n dev | List the current identity's allowed API actions in one namespace. | |
kubectl get role,rolebinding -n dev | List namespace-scoped RBAC objects. | |
kubectl get clusterrole,clusterrolebinding | List cluster-scoped RBAC objects and bindings. | |
kubectl get ns --show-labels | Inspect Pod Security admission labels on namespaces. | |
kubectl get networkpolicy -A | List NetworkPolicies across all namespaces. |
Syntax
# RBAC - grants are additive; to restrict access, remove the
# binding, there's no "deny" rule to add on top of it.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: ci-deploy
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioExamples
# NetworkPolicy - the moment this selects role=db pods, they
# become isolated for ingress; only traffic matching this rule
# is allowed in, everything else that used to reach them is cut off.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-api-only
spec:
podSelector:
matchLabels: { role: db }
policyTypes: ['Ingress']
ingress:
- from:
- podSelector:
matchLabels: { role: api }The first NetworkPolicy in a namespace is the riskiest one
Pods are non-isolated until some policy selects them. Applying the first NetworkPolicy to a namespace can silently cut off traffic nobody ever had to declare before, because it was simply allowed by default. I test in staging before rolling NetworkPolicy into a previously policy-free namespace.
Verify authorization before changing RBAC
kubectl auth can-i get pods -n production
kubectl auth can-i get pods --subresource=log -n production
# Administrators can test a ServiceAccount, but impersonation itself
# requires permission.
kubectl auth can-i list secrets \
--as=system:serviceaccount:dev:api \
-n productionEvaluate Pod Security admission without enforcing it
kubectl label --dry-run=server --overwrite namespace production \
pod-security.kubernetes.io/enforce=restrictedThe server-side dry run evaluates the proposed policy and returns violations without changing the namespace. I use audit and warn modes to discover incompatible workloads before enforcement, then pin the policy version your cluster has tested rather than silently inheriting new rules.
Troubleshooting Security Controls
Step 1: Separate authentication, authorization, and admission
An unauthenticated request fails before RBAC. An authenticated but unauthorized API request
usually returns 403 Forbidden; verify it with kubectl auth can-i. A Pod Security admission
rejection happens after authorization and explains which Pod fields violate policy. Those are
three different failure layers.
Step 2: Trace every RBAC grant
I inspect the subject, Role/ClusterRole rules, and RoleBinding/ClusterRoleBinding scope. Because
RBAC grants are additive, finding one correct narrow Role does not prove the identity lacks a
broader grant elsewhere. Pay particular attention to wildcard permissions, secret access, workload
creation, impersonation, and bindings to cluster-admin.
Step 3: Test NetworkPolicy from the real source namespace
Confirm the destination Service has Ready EndpointSlices first, then run a temporary client Pod in the same namespace and with the same labels as the real caller. I check both ingress policy on the destination and egress policy on the source, including DNS access. Also confirm the cluster network provider actually implements NetworkPolicy.
kubectl get endpointslices \
-l kubernetes.io/service-name=<service> \
-n <destination-namespace>
kubectl get networkpolicy -n <source-namespace>
kubectl get networkpolicy -n <destination-namespace>
kubectl run -it --rm policy-test \
-n <source-namespace> \
--restart=Never \
--image=busybox:1.36 -- shA timeout is not proof of NetworkPolicy
A selector mismatch, failed readiness probe, wrong targetPort, DNS failure, and NetworkPolicy
denial can all look like "the Service timed out." Follow the Service troubleshooting order in
Kubernetes Fundamentals before changing policy.
Visual Diagram
Common Mistakes
- Trying to write an RBAC "deny" rule; RBAC has no such concept, restricting access means removing the grant, not layering a denial on top of it.
- Applying the Restricted Pod Security Standard cluster-wide without testing, breaking workloads that legitimately needed a capability Baseline still allowed.
- Assuming pods are isolated by default and skipping NetworkPolicy entirely, when the actual default is fully open pod-to-pod traffic.
- Applying a single NetworkPolicy to a namespace and being surprised when unrelated traffic breaks, not realizing that policy selected pods well beyond its intended scope.
- Checking only one RoleBinding and missing a broader ClusterRoleBinding that grants the same subject more access.
- Testing policy from an arbitrary debug Pod whose namespace and labels do not match the real caller, then drawing the wrong conclusion from the result.
Performance
- RBAC authorization checks happen on the API request path but are lightweight lookups against already-loaded bindings; the real cost is the audit and design effort of keeping bindings correct, not runtime latency.
- NetworkPolicy enforcement is implemented by the cluster's CNI plugin, not the API server itself, and its performance characteristics (rule-matching overhead per packet) vary by CNI implementation and policy count.
Best Practices
- Scope RBAC bindings as narrowly as possible (a Role in one namespace) and reserve ClusterRole/ClusterRoleBinding for genuinely cluster-wide needs.
- I use
kubectl auth can-iin deployment checks and runbooks to verify effective permissions rather than inferring them from one binding. - I avoid wildcard permissions and routine
cluster-adminuse; new API resources can make an old wildcard grant more powerful over time. - Apply the Restricted Pod Security Standard to namespaces running security-critical or lower-trust workloads, and Baseline as the sane default everywhere else.
- Introduce Pod Security admission through audit/warn evaluation and server-side dry runs before switching a namespace to enforce.
- Introduce NetworkPolicy deliberately and incrementally, starting with a namespace's own traffic map, rather than applying a broad policy and reacting to what breaks.
- Default every namespace to at least a basic deny-by-default NetworkPolicy plus explicit allows, rather than leaving pod-to-pod traffic fully open indefinitely.