Skip to content
Hosting Operations11 min read

Kubernetes Security Risks: 8 Vulnerabilities [Solved]

Fix the eight most common Kubernetes security misconfigurations with tested YAML patches and runtime checks that harden your cluster.

Written by Abdul AbrorTechnical Hosting Support Engineer
A security and privacy dashboard with its status.
On this page

TL;DR — Key takeaways

  • Default service accounts grant unnecessary API access; create read-only accounts per namespace and bind them explicitly.
  • Privileged containers bypass kernel isolation; use securityContext with allowPrivilegeEscalation: false and drop all capabilities except what you need.
  • Publicly exposed API servers accept anonymous requests; enforce authentication, enable audit logs, and restrict CIDR ranges with network policies.
  • Unpatched images run known exploits; scan with trivy before deployment and pin digests instead of mutable tags.
  • Missing resource limits let one pod starve the node; set requests and limits for CPU and memory in every deployment spec.

Kubernetes defaults prioritize convenience over isolation, and that gap shows up fast in production. Overly permissive service accounts, privileged containers, and exposed API endpoints create entry points that let an attacker move from one compromised pod to full cluster control.

These eight misconfigurations appear in most clusters I audit. Each one has a direct fix you can apply today—no vendor tools required, just tested YAML and runtime checks.

Why do Kubernetes security risks matter for production workloads?

A single misconfigured pod can escalate to node access, then spread laterally across the cluster by abusing API permissions or shared secrets. Container isolation depends on seccomp, AppArmor, and cgroups, but those protections collapse the moment you enable privileged mode or mount the Docker socket.

The blast radius grows when workloads share a namespace without NetworkPolicies. One compromised service can pivot to every other pod in that namespace because Kubernetes allows all traffic by default. In support tickets I handled, the usual culprit was a team testing with cluster-admin rights and forgetting to lock down RBAC before go-live.

1. Overly permissive service accounts grant API access to every pod

Every pod runs with a service account, and the default account in each namespace gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token. That token can list or modify resources based on the RBAC bindings attached to the account. If the default service account has cluster-admin or even just edit permissions, a compromised pod can alter deployments, read secrets, or delete resources cluster-wide.

Create a dedicated service account per workload and bind it to a role that grants only the verbs and resources the application actually calls. For a web app that reads ConfigMaps, that might be get and list on configmaps in one namespace. Use kubectl auth can-i --as=system:serviceaccount:namespace:accountname to test permissions before you deploy.

  • Create a service account: kubectl create serviceaccount readonly-app -n production
  • Write a Role that allows get, list on configmaps in the production namespace
  • Bind the role: kubectl create rolebinding readonly-app-binding --role=readonly-role --serviceaccount=production:readonly-app -n production
  • Set serviceAccountName: readonly-app in your pod spec
  • Verify with: kubectl auth can-i get configmaps --as=system:serviceaccount:production:readonly-app -n production

2. Privileged containers bypass kernel isolation and access the host

A container with privileged: true gets all Linux capabilities and can access host devices, load kernel modules, and modify iptables rules. It effectively runs as root on the node. Attackers use privileged containers to escape to the host filesystem, install rootkits, or pivot to other nodes via the Kubelet API.

Drop all capabilities by default and add back only what the workload needs. Most applications need nothing beyond the default unprivileged set. If you must mount a device, use securityContext to grant specific capabilities like CAP_NET_ADMIN instead of full privilege.

  • Set allowPrivilegeEscalation: false in securityContext
  • Add readOnlyRootFilesystem: true to prevent filesystem writes
  • Drop all capabilities: capabilities: { drop: ["ALL"] }
  • Run as a non-root user: runAsNonRoot: true, runAsUser: 1000
  • Block privileged pods cluster-wide with Pod Security Standards: kubectl label namespace production pod-security.kubernetes.io/enforce=restricted

3. Exposed API servers accept unauthenticated requests from the internet

The Kubernetes API server listens on port 6443 by default, and if you expose it without authentication or with anonymous access enabled, anyone can query cluster state or attempt privilege escalation. Check --anonymous-auth=false in your API server flags and review authentication modes with kubectl cluster-info dump | grep authentication-mode.

Restrict access with firewall rules or a bastion host. If you must expose the API externally, use client certificates or OIDC tokens and enable audit logging to track every API call. The audit log file (set with --audit-log-path) records who accessed what resource and when.

  • Verify API server flags: ps aux | grep kube-apiserver | grep anonymous-auth
  • Enable audit logging: --audit-log-path=/var/log/kubernetes/audit.log --audit-policy-file=/etc/kubernetes/audit-policy.yaml
  • Restrict CIDR ranges with NetworkPolicy or cloud firewall rules to allow only trusted IPs
  • Review authentication modes: kubectl cluster-info dump | grep -A5 authentication
  • Test anonymous access: curl -k https://<api-server>:6443/api/v1/namespaces (should return 401 or 403)

4. Unpatched container images run exploits published months ago

Base images inherit vulnerabilities from upstream OS packages. An nginx:1.21 image might contain a libc CVE that was patched two months ago but still ships in the cached layer you pulled last quarter. Mutable tags like latest get overwritten by new builds, so you can't reproduce a deployment or verify what you actually ran.

Pin every image by SHA256 digest and scan it before deployment. Tools like trivy, grype, or your registry's built-in scanner output a list of CVEs with severity ratings. Block HIGH and CRITICAL findings in CI so vulnerable images never reach production.

  • Install trivy: brew install aquasecurity/trivy/trivy (or download binary)
  • Scan an image: trivy image nginx@sha256:abc123...
  • Pin digests in your manifests: image: nginx@sha256:abc123...
  • Automate scanning in CI: trivy image --severity HIGH,CRITICAL --exit-code 1 yourimage
  • Re-scan weekly and rebuild images when base layers publish patches

5. Missing resource limits let one workload starve the entire node

Without CPU and memory limits, a single pod can consume all available resources on a node and crash every other workload scheduled there. Kubernetes does not enforce limits by default. A memory leak or a fork bomb will take down the node.

Set requests to reserve the baseline resources your app needs, and set limits to cap maximum usage. The scheduler uses requests to decide node placement; limits trigger the OOM killer or CPU throttling when a container exceeds them.

  • Define resources in your pod spec: resources: { requests: { memory: "128Mi", cpu: "100m" }, limits: { memory: "256Mi", cpu: "200m" } }
  • Use LimitRange to enforce defaults per namespace: kubectl apply -f limitrange.yaml
  • Monitor resource usage: kubectl top pods -n production
  • Review pods without limits: kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.containers[].resources.limits == null) | .metadata.name'
  • Test limits by running a stress container and watching it get throttled or OOM-killed

So what if the API is locked down but pods can still talk to each other freely?

Kubernetes allows all pod-to-pod traffic by default. An attacker who compromises one pod can scan internal services, connect to databases, or exfiltrate data through other workloads. NetworkPolicies define rules that act like firewall rules at the pod level, isolating workloads by namespace or label.

Start with a default-deny policy in each namespace, then whitelist the specific ingress and egress flows your app requires. For example, a frontend pod might only need egress to the backend service on port 8080 and ingress from an ingress controller on port 80.

  • Create a default-deny policy: kubectl apply -f - <<EOF apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-all namespace: production spec: podSelector: {} policyTypes: - Ingress - Egress EOF
  • Allow specific ingress: add a second NetworkPolicy with podSelector matching your app label and ingress.from matching the source
  • Allow DNS egress: permit egress to kube-dns on port 53 UDP
  • Test policies by deploying a debug pod and attempting connections that should be blocked
  • Review existing policies: kubectl get networkpolicies --all-namespaces

6. Secrets stored in etcd are readable by anyone with API access

Kubernetes stores secrets as base64-encoded values in etcd, which is not encryption. Anyone with read access to the Secret resource can decode them with a one-liner: kubectl get secret mysecret -o jsonpath='{.data.password}' | base64 -d. If an attacker gains API access through a compromised service account, they can dump every secret in the cluster.

Enable encryption at rest by configuring the API server with an EncryptionConfiguration resource and a KMS provider or AES key. Rotate the encryption key periodically. For production workloads, migrate secrets to an external manager like Vault, AWS Secrets Manager, or Google Secret Manager and inject them as environment variables or mounted files.

  • Check if encryption is enabled: ps aux | grep kube-apiserver | grep encryption-provider-config
  • Create an EncryptionConfiguration: /etc/kubernetes/encryption-config.yaml with a random 32-byte key
  • Add --encryption-provider-config=/etc/kubernetes/encryption-config.yaml to API server flags
  • Rotate secrets: update the secret value, then recreate all pods to pick up the new version
  • Audit secret access: grep 'secrets' /var/log/kubernetes/audit.log

7. Kubelet API allows anonymous read access to pod specs and logs

The Kubelet listens on port 10250 and serves pod logs, metrics, and exec endpoints. Older clusters allowed anonymous read access, which let anyone query pod specs, environment variables, and even exec into containers. Check --anonymous-auth=false and --authorization-mode=Webhook in Kubelet flags to enforce API server authentication for Kubelet requests.

If you need to debug Kubelet endpoints, use kubectl proxy or kubectl port-forward instead of exposing the Kubelet port. Restrict network access to 10250 with firewall rules so only the control plane can reach it.

  • Verify Kubelet config: cat /var/lib/kubelet/config.yaml | grep anonymous
  • Set authentication.anonymous.enabled: false and authorization.mode: Webhook
  • Restart Kubelet: systemctl restart kubelet
  • Test access: curl -k https://<node-ip>:10250/pods (should return 401 without valid client cert)
  • Block external access to 10250 with iptables or cloud security groups

8. RBAC bindings accumulate over time and grant stale permissions

Teams create ClusterRoleBindings for quick testing and forget to remove them. A developer who left the company six months ago might still have cluster-admin via a binding that references their old service account. These stale bindings compound until a single compromised identity has more access than any individual should.

Audit RBAC every quarter. Export all bindings with kubectl get clusterrolebindings,rolebindings --all-namespaces -o yaml, review the subjects, and delete bindings that reference non-existent accounts or overly broad roles. Use audit logs to confirm whether a binding is actually used before you remove it.

  • List all ClusterRoleBindings: kubectl get clusterrolebindings -o wide
  • Find bindings for a specific user: kubectl get clusterrolebindings -o json | jq '.items[] | select(.subjects[]?.name=="old-user")'
  • Delete unused bindings: kubectl delete clusterrolebinding stale-binding
  • Review who has cluster-admin: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin")'
  • Automate quarterly audits with a script that compares active users against bound subjects

Testing and rollback boundaries for security changes

Apply security policies to a single namespace first. Deploy the policy, verify existing workloads still start, then expand to other namespaces. If a PodSecurityPolicy or admission controller blocks legitimate pods, you will see admission errors in kubectl describe pod output.

Back up RBAC bindings before deletion: kubectl get clusterrolebindings,rolebindings --all-namespaces -o yaml > rbac-backup.yaml. If a workload breaks after tightening permissions, restore the binding and add audit logging to see which API calls failed.

For image scanning, start by scanning without blocking (set trivy exit code to 0) and review findings for a week. Once you understand the baseline, enable blocking for HIGH and CRITICAL CVEs. Re-scan after every base image update to catch new vulnerabilities.

Quick troubleshooting checklist

  • Audit service account permissions across all namespaces
  • Apply PodSecurityPolicy or Pod Security Standards to block privileged containers
  • Enable API server audit logging and review authentication modes
  • Scan all container images with trivy or a registry scanner before deployment
  • Set resource requests and limits in every deployment and statefulset
  • Create NetworkPolicies to isolate workloads by namespace and label
  • Rotate secrets stored in etcd and migrate to external secret managers
  • Review RBAC bindings with kubectl get clusterrolebindings and kubectl get rolebindings

FAQ

What is the most common Kubernetes security risk in production clusters?

Overly permissive RBAC configurations give service accounts cluster-admin rights they do not need. Most workloads only need to read ConfigMaps or list pods in their own namespace, but default bindings often grant full API access. Create custom roles scoped to the minimum required verbs and resources, then bind them explicitly to service accounts instead of relying on default.

How do I prevent privileged containers from running in my cluster?

Apply Pod Security Standards at the namespace level by adding a label like pod-security.kubernetes.io/enforce: restricted, which blocks privileged containers, host namespaces, and capability additions. For older clusters, deploy a PodSecurityPolicy or use an admission controller like OPA Gatekeeper or Kyverno to reject privileged pods at admission time.

Why should I avoid using latest or mutable tags for container images?

Mutable tags let an attacker replace a legitimate image with a malicious one at the same tag, and Kubernetes will pull the compromised version on the next pod restart. Pin images by SHA256 digest (image@sha256:abc123...) so every deployment references an immutable artifact. Scan the digest with trivy or your registry scanner before you deploy it to catch known CVEs.