Cyber Intelligence
Cloud Security16 min read

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.

I
Microsoft Cloud Solution Architect
Kubernetes Security Best Practices 2026: Hardening Your K8s Cluster infographic showing key Cloud Security concepts and controls
Kubernetes Security Best Practices 2026: Hardening Your K8s Cluster infographic showing key Cloud Security concepts and controls
KubernetesK8s SecurityContainer SecurityDevSecOpsCloud SecurityRBAC

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.io

Two 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: false

Key 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:

ProfileUse caseWhat it prevents
**Privileged**System-level workloadsNothing
**Baseline**Standard applicationsPrivilege escalation, host namespace access
**Restricted**Sensitive workloadsAll 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=restricted

Once violations are cleared, promote to enforce:

kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted

Set 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: 5432

Note: 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: 53

For 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-password

This 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: true

Scanning 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: WARNING

For 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:
      - RequestReceived

Forward 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 areaManaged option (AKS/Azure)DIY optionChoose DIY when
Policy enforcementAzure Policy for AKSOPA Gatekeeper / KyvernoMulti-cloud, need custom Rego or policy logic
Runtime detectionDefender for ContainersFalco / Cilium TetragonNeed in-kernel enforcement, not just alerts
SecretsKey Vault CSI driverExternal Secrets Operator + VaultMulti-cloud secret store, self-hosted Vault already in use
Image scanningDefender for Containers registry scanTrivy / Grype in CINeed 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

  1. RBAC audit: find and remove wildcard and cluster-admin bindings
  2. Encryption at rest for Secrets, or migrate to an external secrets manager
  3. Pod Security Standards at Baseline level, promoted from warn/audit to enforce
  4. Default-deny Network Policies in sensitive namespaces, with DNS egress allowed
  5. Image scanning in CI/CD and registry admission control
  6. Runtime detection (Falco or Defender for Containers)
  7. 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-bench

Kubernetes 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.

PillarLevel 0: unmanagedLevel 1: basicLevel 2: production-readyLevel 3: mature
RBACWildcard/cluster-admin commonClusterRoleBindings audited ad hocNamespaced least-privilege roles, quarterly auditAutomated drift detection, JIT elevation for admin access
Pod securityNo PSA or PSPBaseline in warn/auditBaseline enforced, Restricted in warnRestricted enforced on sensitive namespaces
NetworkFlat network, no policiesDefault-deny in some namespacesDefault-deny plus explicit allows cluster-widePolicy-as-code with per-team review
SecretsPlain env vars / raw SecretsEncryption at rest enabledExternal secrets manager for productionAutomatic rotation, short-lived credentials
Supply chainUnscanned imagesCI scanning, no gateScanning gates the pipelineSigned images, admission-time verification
RuntimeNo monitoringLogs collected, no alertingFalco or Defender alerting on core rulesetTuned baseline, automated response
Audit loggingDisabled or defaultEnabled, not forwardedForwarded to a SIEMAlert rules on high-value events (secrets, exec, RBAC changes)

References

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.

Free download

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.

Start the Intermediate Path13h · 4 topics · 10 quiz questions
I

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

Ask a Question

0/2000 characters

Your email is used for moderation only and will not be displayed.

Related Articles

Need Help with Your Security?

Our team of security experts can help you implement the strategies discussed in this article.

Contact Us