Kubernetes Security Best Practices 2026: Hardening Your K8s Cluster
Kubernetes misconfigurations drive a significant share of cloud security incidents. This guide covers full-depth hardening: RBAC design, Pod Security Standards, default-deny network policies, secrets management, image supply chain, runtime detection, audit logging, common failure modes, and a managed-vs-DIY tradeoff framework, with practical YAML examples throughout.

Why Kubernetes security matters
Kubernetes is the default runtime for containerized workloads at scale and consistently among the top sources of cloud security incidents. Most Kubernetes security problems are caused by misconfiguration, not novel attacks.
What is Kubernetes security hardening?
Kubernetes security hardening is the process of configuring a cluster's identity, network, workload, and supply chain controls so that a single compromised container, credential, or misconfiguration cannot escalate into full cluster or data compromise. In practice that means combining RBAC, Pod Security Standards, network policies, secrets management, image scanning, and runtime detection, rather than relying on any single control.
1. RBAC: least privilege at the API level
Every API call to the cluster goes through RBAC authorization. The common mistake is giving service accounts cluster-admin or using wildcard permissions.
# Minimal role for an app that only reads ConfigMaps
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: configmap-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
resourceNames: ["app-config"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-configmaps
namespace: production
subjects:
- kind: ServiceAccount
name: my-app
namespace: production
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.ioTwo design habits keep RBAC from sprawling as a cluster grows. First, default to namespaced Roles instead of ClusterRoles: a Role only grants permissions inside its own namespace, so a compromised service account in one namespace cannot read Secrets or ConfigMaps in another. Reserve ClusterRoles for genuinely cluster-scoped resources (nodes, PersistentVolumes, CustomResourceDefinitions) or for read-only aggregated roles. Second, most workloads never call the Kubernetes API at all, yet Kubernetes automatically mounts a service account token into every pod unless you opt out. Set automountServiceAccountToken to false on the service account or pod spec unless the workload genuinely calls the API server.
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: production
automountServiceAccountToken: falseKey practices for keeping RBAC from drifting back toward over-permissioned:
- Audit ClusterRoleBindings and RoleBindings quarterly, not just at cluster setup
- Never bind cluster-admin to a service account, only to break-glass human accounts with just-in-time elevation
- Prefer namespaced Roles over ClusterRoles for anything workload-specific
- Set automountServiceAccountToken to false by default and opt in per workload that needs it
- Use a tool like kubectl-who-can or rbac-lookup to answer "who can do X" before granting a new permission, not after an incident
2. Pod Security Standards and admission control
Since Kubernetes 1.25, Pod Security Admission replaces PodSecurityPolicy:
| Profile | Use case | What it prevents |
|---|---|---|
| **Privileged** | System-level workloads | Nothing |
| **Baseline** | Standard applications | Privilege escalation, host namespace access |
| **Restricted** | Sensitive workloads | All of baseline, plus drops all capabilities |
Pod Security Admission supports three independent modes per namespace: enforce (blocks non-compliant pods), audit (allows the pod but records a violation in the audit log), and warn (allows the pod but returns a warning to kubectl). The practical rollout pattern is to set warn and audit to your target level first, watch violations for a sprint or two, fix what breaks, then flip enforce to match. Jumping straight to enforce: restricted on a namespace with existing workloads is the most common way Pod Security Standards adoption stalls, because it silently blocks new deployments.
# Phase 1: observe only, nothing is blocked yet
kubectl label namespace production \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restrictedOnce violations are cleared, promote to enforce:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restrictedSet a compliant security context:
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]Pod Security Standards cover a fixed, opinionated baseline. For anything more specific to your organization, such as requiring resource limits on every container, blocking a particular registry, or requiring a specific label schema, use a policy engine like Kyverno or OPA Gatekeeper alongside PSA rather than instead of it. On AKS, Azure Policy for AKS wraps Gatekeeper and ships prebuilt policy initiatives, which is usually the faster path for teams that are Azure-native rather than already deep in the Kyverno or OPA ecosystem.
3. Network policies: default deny and namespace isolation
By default, all pods can communicate with all other pods. Fix this with a default-deny policy and explicit allow rules for required traffic, the same default-deny principle behind zero trust network design applied inside the cluster instead of just at its perimeter:
# Default deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow app to reach its database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: my-app
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432Note: requires a CNI plugin that supports Network Policies (Calico, Cilium).
A default-deny egress policy also blocks DNS resolution, since DNS queries to CoreDNS in kube-system are just another network connection. This is the most common reason teams roll back default-deny minutes after applying it: pods start failing with name resolution errors. Add an explicit egress rule allowing DNS before enforcing default-deny anywhere:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53For multi-tenant clusters, combine podSelector with namespaceSelector to isolate whole namespaces from each other by default, then punch explicit holes for shared services (ingress controllers, monitoring scrapers, DNS). Label namespaces consistently (team, environment, tier) so NetworkPolicies can select by label rather than hardcoded namespace names, which breaks the first time a namespace gets renamed or cloned for a new environment.
4. Secrets management: beyond plain environment variables
Kubernetes Secrets are base64-encoded, not encrypted, by default. Anyone with read access to etcd or sufficient RBAC permissions on the secrets resource can decode them in one command. Mounting Secrets as environment variables makes the problem worse: env vars are visible via process inspection inside the container and are easy to accidentally forward to a crash report or error-tracking service.
Two independent fixes matter here, and most clusters are missing at least one. First, enable encryption at rest for the Secrets resource in etcd using an EncryptionConfiguration on the API server, so Secret data is not stored in plaintext even at the storage layer. Second, stop putting sensitive values in raw Kubernetes Secrets for production workloads, and sync them from an external secrets manager instead. On AKS, the Azure Key Vault Provider for Secrets Store CSI Driver mounts secrets from Key Vault directly into pods as files, with no long-lived copy sitting in etcd. For a cloud-agnostic approach, External Secrets Operator does the same sync pattern against Vault, AWS Secrets Manager, or Key Vault and keeps a native Kubernetes Secret in sync behind the scenes.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-db-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: azure-keyvault-store
kind: SecretStore
target:
name: app-db-credentials
data:
- secretKey: password
remoteRef:
key: db-passwordThis gives you centralized management, audit logging, and automatic rotation at the source, not just at the point Kubernetes happens to read the value.
5. Image and supply chain security
For the broader pipeline context these scans fit into, see DevSecOps: integrating security into CI/CD pipelines.
Scan in CI/CD with Trivy:
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
exit-code: '1'
severity: 'CRITICAL,HIGH'
ignore-unfixed: trueScanning catches known CVEs, but it does not prove the image running in production is the one your pipeline actually scanned and built. Two additional controls close that gap: generate a software bill of materials (SBOM) at build time so you can quickly answer whether you are affected the next time a widely used library has a new critical CVE, and sign images with Sigstore Cosign so admission control can cryptographically verify an image was not swapped or tampered with between the registry and the cluster.
# Sign the image after a successful build and scan
cosign sign --key cosign.key myregistry.azurecr.io/myapp:${GIT_SHA}
# Verify at deploy time, or wire into an admission controller
cosign verify --key cosign.pub myregistry.azurecr.io/myapp:${GIT_SHA}Block non-compliant images with Kyverno, and extend the same policy to require an approved registry:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-approved-registry
spec:
validationFailureAction: Enforce
rules:
- name: check-registry
match:
resources:
kinds: [Pod]
validate:
message: "Images must be from mycompany.azurecr.io"
pattern:
spec:
containers:
- image: "mycompany.azurecr.io/*"Kyverno and OPA Gatekeeper can also enforce cosign signature verification directly at admission time (Kyverno's verifyImages rule type), so an unsigned image is rejected before it ever schedules, not just flagged after the fact.
6. Runtime security and threat detection
Every control so far reduces attack surface before deployment. Runtime security assumes something will still get through and focuses on catching it while it is happening: a shell spawned inside a container that never runs a shell, a process reading a credential file outside of expected startup, an outbound connection to a newly registered domain, or a container writing to a path it has no business writing to. Falco is the most widely deployed open-source option, watching kernel syscalls (via eBPF or a kernel module) against a rule set and alerting on matches.
- rule: Unexpected shell in container
desc: Detect a shell spawned inside a container at runtime
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh) and
not proc.pname in (allowed_parent_processes)
output: >
Shell spawned in container (user=%user.name container=%container.name
command=%proc.cmdline parent=%proc.pname)
priority: WARNINGFor AKS, Microsoft Defender for Containers provides managed runtime protection; see AKS container security with Defender for Containers for the full setup.
Newer eBPF-native tools like Cilium Tetragon go further by enforcing policy in-kernel (killing the process, not just alerting), which matters if mean time to triage an alert is measured in hours. Whichever tool you pick, tune it against a baseline of your own workloads' normal behavior first: a default rule set that fires on every legitimate exec into a debug pod trains responders to ignore alerts, which defeats the purpose.
7. Audit logging: know what happened after the fact
Every other control in this guide tries to prevent or catch an incident in progress. Audit logging is what lets you reconstruct one after the fact: which identity called the API, what they changed, and when. The Kubernetes API server supports a configurable audit policy with levels per rule: None (do not log), Metadata (log request metadata only), Request (log metadata and the request body), and RequestResponse (log everything, including the response). RequestResponse on every request is expensive and rarely necessary; scope it to what you actually need to investigate.
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Full detail on secrets and exec, the two highest-value events
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
verbs: ["get", "list", "create", "update", "delete"]
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/attach"]
# Metadata only for everything else, to keep log volume manageable
- level: Metadata
omitStages:
- RequestReceivedForward audit logs off the cluster to a SIEM you can actually query and alert on. Managed Kubernetes makes this easier than self-hosting the collection pipeline: on AKS, enable the kube-audit and kube-audit-admin diagnostic categories and route them to a Log Analytics workspace or Microsoft Sentinel. A cluster with audit logging turned on but nowhere to search it is only marginally better than no audit logging at all.
Common failure modes
- Default-deny network policies get applied once during a hardening sprint, then quietly bypassed by a new namespace that never received the same labels or policies
- Service accounts get cluster-admin "temporarily" to unblock a deploy, and the binding outlives the incident that created it by months
- automountServiceAccountToken is left at its default of true on every pod, including ones that never call the Kubernetes API
- Node images and the container runtime go unpatched because node auto-upgrade is disabled to avoid disrupting a maintenance window that never gets rescheduled
- Pod Security Standards get set to warn and audit during rollout and never promoted to enforce, so the warnings become permanent background noise
- Secrets end up in Helm values files or CI/CD environment variables outside Kubernetes entirely, which none of the in-cluster controls above can see or fix
Managed add-ons vs DIY tooling: the strictness vs velocity tradeoff
Almost every control in this guide has a managed version and a do-it-yourself version, and the right choice depends on team size and how much Kubernetes expertise you want to carry in-house. Managed add-ons (Azure Policy for AKS, Defender for Containers, the Key Vault CSI driver) trade some flexibility for lower operational burden: Microsoft operates the control-plane side of the tooling, and you consume it through policy assignments instead of running your own Gatekeeper or Falco deployment. DIY tooling (self-managed Kyverno or OPA Gatekeeper, Falco, HashiCorp Vault) gives you portability across clouds and finer control over rule logic, at the cost of someone on your team owning upgrades, tuning, and incident response for the tooling itself, not just the cluster it protects.
| Control area | Managed option (AKS/Azure) | DIY option | Choose DIY when |
|---|---|---|---|
| Policy enforcement | Azure Policy for AKS | OPA Gatekeeper / Kyverno | Multi-cloud, need custom Rego or policy logic |
| Runtime detection | Defender for Containers | Falco / Cilium Tetragon | Need in-kernel enforcement, not just alerts |
| Secrets | Key Vault CSI driver | External Secrets Operator + Vault | Multi-cloud secret store, self-hosted Vault already in use |
| Image scanning | Defender for Containers registry scan | Trivy / Grype in CI | Need scan results inside the pipeline, pre-merge |
The second tradeoff is orthogonal to managed-versus-DIY: how strict to make enforcement versus how much friction that adds to shipping. Restricted Pod Security Standards, default-deny network policies everywhere, and mandatory image signing are all correct end states, but rolling out all three in enforce mode on day one against existing workloads will break deployments and burn trust in the security team faster than any incident would. Roll out in audit or warn mode first, fix what it flags, and only then flip to enforce, namespace by namespace rather than cluster-wide.
Priority order: where to start
- RBAC audit: find and remove wildcard and cluster-admin bindings
- Encryption at rest for Secrets, or migrate to an external secrets manager
- Pod Security Standards at Baseline level, promoted from warn/audit to enforce
- Default-deny Network Policies in sensitive namespaces, with DNS egress allowed
- Image scanning in CI/CD and registry admission control
- Runtime detection (Falco or Defender for Containers)
- Audit logging at RequestResponse level on secrets and pod exec, forwarded to a SIEM you actually monitor
Benchmark with kube-bench against the CIS Kubernetes Benchmark:
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-benchKubernetes hardening maturity checklist
Use this as a working checklist rather than a one-time audit. Most clusters are not uniformly at the same maturity level across every pillar, and that is normal: prioritize whichever row is lowest relative to what that workload actually needs.
| Pillar | Level 0: unmanaged | Level 1: basic | Level 2: production-ready | Level 3: mature |
|---|---|---|---|---|
| RBAC | Wildcard/cluster-admin common | ClusterRoleBindings audited ad hoc | Namespaced least-privilege roles, quarterly audit | Automated drift detection, JIT elevation for admin access |
| Pod security | No PSA or PSP | Baseline in warn/audit | Baseline enforced, Restricted in warn | Restricted enforced on sensitive namespaces |
| Network | Flat network, no policies | Default-deny in some namespaces | Default-deny plus explicit allows cluster-wide | Policy-as-code with per-team review |
| Secrets | Plain env vars / raw Secrets | Encryption at rest enabled | External secrets manager for production | Automatic rotation, short-lived credentials |
| Supply chain | Unscanned images | CI scanning, no gate | Scanning gates the pipeline | Signed images, admission-time verification |
| Runtime | No monitoring | Logs collected, no alerting | Falco or Defender alerting on core ruleset | Tuned baseline, automated response |
| Audit logging | Disabled or default | Enabled, not forwarded | Forwarded to a SIEM | Alert rules on high-value events (secrets, exec, RBAC changes) |
References
- Kubernetes security documentation: Official Kubernetes security concepts and Pod Security Standards
- Kubernetes Pod Security Standards: Specification for the Privileged, Baseline, and Restricted profiles
- Kubernetes RBAC good practices: Official guidance on least-privilege RBAC design
- Kubernetes auditing: Official docs on audit policy levels and configuration
- NSA/CISA Kubernetes Hardening Guide: NSA hardening recommendations (August 2022)
- CIS Kubernetes Benchmark: Industry-standard security benchmark for Kubernetes
- OWASP Kubernetes Security Cheat Sheet: OWASP hardening guidance
- External Secrets Operator documentation: Reference for syncing secrets from Vault, AWS, and Azure Key Vault
- Sigstore Cosign: Container image signing and verification
- Microsoft Defender for Containers: AKS-native runtime protection and scanning
- Azure Policy for AKS: Managed Gatekeeper-based policy enforcement for AKS
Frequently asked questions
What is the most critical Kubernetes security misconfiguration to fix first?
The single highest-impact Kubernetes security issue is over-permissive RBAC, specifically service accounts and users with wildcard permissions (verbs: ["*"]) or cluster-admin bindings that are not genuinely required. Wildcard RBAC means a compromised workload can read secrets from every namespace, modify deployments, or delete critical resources. Run kubectl get clusterrolebindings and kubectl get rolebindings --all-namespaces to identify all cluster-admin bindings, then audit whether each is justified. Remove any binding where the subject does not require cluster-wide admin access.
Why should Kubernetes Secrets not be trusted as a secret store without additional controls?
Kubernetes Secrets are base64-encoded by default, not encrypted. Base64 is encoding, not encryption: anyone with read access to the etcd database or sufficient RBAC permissions can read Secret values trivially. By default, Secrets are also mounted as files or environment variables in pods, making them accessible to any process running in the container. To actually secure Kubernetes Secrets, enable etcd encryption at rest, restrict Secret read permissions in RBAC to only the service accounts that need them, and for production workloads consider an external secrets manager (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager) with the External Secrets Operator to avoid storing sensitive values in Kubernetes at all.
What are Kubernetes Pod Security Standards and which level should you use?
Pod Security Standards (PSS) are a built-in Kubernetes admission control policy framework replacing the deprecated PodSecurityPolicy. Three levels are defined: Privileged (no restrictions, for system pods), Baseline (prevents known privilege escalation paths, suitable for most workloads), and Restricted (significantly hardened, requires non-root user, drops all capabilities, requires a seccomp profile). For most production namespaces, enforce Baseline at minimum and target Restricted for namespaces running sensitive workloads. Apply PSS at the namespace level using the pod-security.kubernetes.io/enforce label. Audit mode (warn but allow) is useful during migration to understand what violations exist before enforcing.
How do Kubernetes Network Policies work and what is the most important one to implement?
Kubernetes Network Policies are namespace-scoped rules that control which pods can communicate with which other pods and external endpoints. They require a CNI plugin that supports Network Policy enforcement (Calico, Cilium, Weave, not Flannel alone). The single most important policy to implement is a default-deny policy in each production namespace: a NetworkPolicy that selects all pods with an empty podSelector and specifies no ingress or egress rules, which blocks all traffic to and from pods in that namespace by default. Additional policies then explicitly allow only the communication paths your application actually requires, limiting lateral movement if any pod is compromised.
What is container image supply chain security and how do you implement it in a Kubernetes cluster?
Container image supply chain security covers the full lifecycle from base image selection through build, registry storage, and deployment. Key controls are: use minimal base images (distroless or Alpine) to reduce the attack surface, scan images for CVEs in your CI/CD pipeline before pushing to the registry (Trivy, Grype, or Snyk), sign images at build time using Sigstore Cosign, and enforce signature verification at admission time using an admission controller (Kyverno, OPA Gatekeeper) that rejects unsigned or unverified images. The Kyverno ClusterPolicy shown earlier enforces that only images from an approved registry can run, preventing developers from deploying arbitrary public images directly to production.
How does Kubernetes audit logging differ from application logging, and what should you actually log?
Kubernetes audit logs record calls to the API server itself (who read a Secret, who exec'd into a pod, who created a RoleBinding), not what your application logged from inside its own code. The two are complementary: application logs tell you what a workload did, audit logs tell you what a human or service account did to the cluster. At minimum, log at RequestResponse level for secrets access and pod exec/attach, since those are the two audit events most directly tied to a credential leak or an attacker moving inside a compromised pod, and log at Metadata level for everything else to keep volume manageable. Forward the logs to a SIEM (Microsoft Sentinel, Splunk) rather than leaving them on the API server, since local audit logs get rotated out and are not searchable at scale.
Should you use Falco or a managed option like Microsoft Defender for Containers for runtime security?
Both watch the same category of runtime signals (unexpected process execution, suspicious network connections, file integrity), but they differ in operational model. Falco is open source, runs as a DaemonSet you manage yourself, and gives you full control over rule authoring and where alerts go, at the cost of owning rule tuning and infrastructure. Microsoft Defender for Containers is the managed equivalent for AKS: Microsoft operates the detection engine and keeps signatures current, and alerts surface directly in Microsoft Defender for Cloud alongside other Azure security findings. Teams already standardized on Azure and Microsoft Sentinel generally get faster time-to-value from Defender for Containers; teams running multi-cloud or wanting fully customizable rules tend to prefer Falco.
Cloud Security Checklist
A 20-point hardening checklist for AWS, Azure, and GCP workloads.
No spam. Unsubscribe anytime.
Continue Learning
Cloud Security Engineer Roadmap
Protect cloud workloads at scale.
Microsoft Cloud Solution Architect
Cloud Solution Architect with deep expertise in Microsoft Azure and a strong background in systems and IT infrastructure. Passionate about cloud technologies, security best practices, and helping organizations modernize their infrastructure.
Share this article
Questions & Answers
Related Articles
Need Help with Your Security?
Our team of security experts can help you implement the strategies discussed in this article.
Contact Us