Back to Blog

Denial of Service in Kubernetes for the KCSA Exam: Resource Exhaustion, PID & Ephemeral-Storage Limits, Node-Pressure Eviction & API Priority and Fairness

A practitioner's KCSA guide to the Denial of Service bullet of the Kubernetes Threat Model domain — how a single Pod, tenant, or client can exhaust CPU, memory, PIDs, disk, etcd, the API server, and the cluster network, and the layered controls that stop it: requests and limits, LimitRange and ResourceQuota, kubelet PID limits and node-pressure eviction, API Priority and Fairness (FlowSchema and PriorityLevelConfiguration), event rate limiting, etcd quotas, conntrack sizing, and network-level protection.

By Sailor Team , October 9, 2026

The KCSA’s Kubernetes Threat Model domain (16% of the exam) lists Denial of Service as its own bullet, between Persistence and Malicious Code Execution. Most study material handles it in a sentence: “use ResourceQuotas and LimitRanges”. That sentence is correct and incomplete. Kubernetes has at least six independent resources an attacker (or a buggy deployment) can exhaust, and each one has its own control: CPU and memory through cgroups and requests/limits, process IDs through kubelet PID limits, disk through ephemeral-storage accounting and node-pressure eviction, the API server through API Priority and Fairness, etcd through storage quotas and event TTLs, and the network through conntrack tables, DNS, and whatever sits in front of your Ingress.

Our KCSA threat model guide places DoS inside the full attacker-goal map, and the isolation and segmentation guide covers quotas and limits as a tenancy boundary. This article is the deep dive on the DoS bullet itself: what runs out, how it runs out, which control catches it, and the configuration the exam expects you to recognise. Everything here is also directly usable on a real cluster.

How the KCSA Frames Denial of Service

The exam is multiple-choice and conceptual, so it tests whether you can map a symptom to a missing control and a control to the layer it operates at. The pattern of a typical question:

A tenant’s workload begins forking processes uncontrollably. Other Pods on the same node are killed by the kernel. Which control would have limited the impact?

The tempting wrong answers are a ResourceQuota on CPU (does not bound process count), a NetworkPolicy (wrong resource entirely), and Pod Security Standards (the restricted profile does not limit PIDs). The right answer is a kubelet-level PID limit. To answer that reliably you need a mental table of resource → exhaustion path → control, which is what the rest of this guide builds.

Resource that runs outWho exhausts itPrimary controlSecondary / detection
CPU, memory on a nodeone container without limitsresources.limits, LimitRange defaultsResourceQuota, QoS, node-pressure eviction
Process IDs on a nodefork bomb, runaway worker poolpodPidsLimit, pid= reservationpid.available eviction
Node disk, inodeslogs, emptyDir, image layersephemeral-storage limits, sizeLimitnodefs/imagefs eviction, log rotation
API server concurrencyhot-looping controller, misbehaving client, unauthenticated floodAPI Priority and Fairness--anonymous-auth=false, RBAC, 429 metrics
etcd storageevent storms, object churn, huge objects--quota-backend-bytes, --event-ttl, separate events etcdEventRateLimit admission, compaction/defrag
Cluster networktraffic floods, DNS floods, conntrack exhaustionNetworkPolicy, ingress rate limits, upstream DDoS protectionconntrack sizing, NodeLocal DNSCache

Layer 1: CPU and Memory Exhaustion

A container with no limits can consume all CPU and memory on its node. The kernel OOM killer then chooses victims using the oom_score_adj values the kubelet sets per QoS class: Guaranteed Pods get −997, BestEffort get 1000, and Burstable get a value between 2 and 999 that shrinks as the memory request grows. The practical meaning: a BestEffort attacker is killed first, but a BestEffort victim is killed first too. If your critical service has no requests, it is as exposed as the attacker.

Controls, from the innermost out:

  1. Per-container resources.limits. The memory limit becomes a cgroup limit; exceeding it OOM-kills the container. The CPU limit becomes a CFS quota, so the container is throttled rather than killed. For the exam, memory limits kill, CPU limits throttle.
  2. LimitRange in each namespace sets default and defaultRequest so that a Pod with no resources block still gets bounded, and max/min/maxLimitRequestRatio so a tenant cannot request a whole node in one Pod.
  3. ResourceQuota caps the namespace total (requests.cpu, limits.memory, count/pods). A quota without a LimitRange is a known trap: once any quota on CPU or memory exists in a namespace, Pods that omit requests/limits are rejected at admission, so you need the LimitRange defaults or every unbounded deploy fails.
  4. Admission policy to refuse workloads without limits (ValidatingAdmissionPolicy or a policy engine; our admission control guide covers the mechanics).

The request/limit field syntax and the QoS classes are developer-level material covered in the CKAD resource management guide; the KCSA tests the security consequence, which is that the absence of limits converts any compromised container into a node-level DoS tool.

Layer 2: PID Exhaustion and Fork Bombs

CPU and memory limits do not stop a fork bomb. A process that forks in a loop uses little memory per child, and the kernel’s global PID table (/proc/sys/kernel/pid_max, often a low default such as 32768 on some distributions) fills up before any memory threshold trips. Once PIDs are exhausted, nothing on the node can start a process, including the kubelet’s own health probes and the container runtime.

Kubernetes has two independent PID controls, both GA since v1.20:

  • Per-Pod limit: podPidsLimit in the KubeletConfiguration (default -1, unlimited). The kubelet applies it as a pids cgroup limit on each Pod, so a fork bomb inside one Pod hits fork: retry: Resource temporarily unavailable while the rest of the node continues.
  • Node-level reservation: pid=<n> inside systemReserved and kubeReserved carves PIDs out for the OS and for Kubernetes daemons so that Pods collectively cannot starve the kubelet.
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
podPidsLimit: 4096
systemReserved:
  pid: "1000"
kubeReserved:
  pid: "1000"
evictionHard:
  pid.available: "10%"

The pid.available eviction signal is the third piece. When free PIDs fall below the threshold, the node reports PIDPressure and the kubelet evicts Pods. Two details the exam likes: eviction based on PIDs ignores Pod priority for ordering because PIDs have no request to compare against, and eviction is reactive, not a hard cap. If PIDs grow faster than the kubelet’s eviction loop, the kernel still runs out. That is why podPidsLimit (a hard cgroup limit) is the primary control and eviction is the backstop.

Note that securityContext has no field for PID limits, and Pod Security Standards do not cover them. A question that offers “set the restricted Pod Security Standard” as a fork-bomb defence is offering a distractor.

Layer 3: Disk, Inodes and Ephemeral Storage

Disk fills quietly. Container logs written to stdout accumulate on the node filesystem, emptyDir volumes grow until the disk is full, and image pulls consume the image filesystem. A full disk prevents the kubelet from writing logs, the runtime from pulling images, and etcd (if co-located on a control-plane node) from committing writes.

Controls:

  • resources.limits.ephemeral-storage per container. The kubelet sums the writable layer, logs, and emptyDir usage for the Pod; exceeding the Pod’s total limit evicts the Pod. This is enforced by periodic measurement, so it is slower than a cgroup limit but it does stop the growth.
  • emptyDir.sizeLimit bounds a single volume; exceeding it also evicts the Pod. For memory-backed emptyDir (medium: Memory) the size counts against the container’s memory limit, which is a favourite trick question.
  • Kubelet node-pressure eviction with the default hard thresholds on Linux: memory.available<100Mi, nodefs.available<10%, imagefs.available<15%, nodefs.inodesFree<5%. When nodefs is under pressure the kubelet first reclaims node-level resources (garbage-collects dead containers, then unused images), and only then evicts Pods.
  • Log rotation via kubelet containerLogMaxSize and containerLogMaxFiles, so that a chatty container cannot fill the disk through stdout alone.
  • ResourceQuota on requests.storage and persistentvolumeclaims so a tenant cannot provision unlimited persistent volumes, which is a billing DoS as much as an availability one.

Node-Pressure Eviction: the Backstop Everyone Misunderstands

Eviction is where the three node-level layers converge, so it is worth being precise. The kubelet watches eviction signals (memory.available, nodefs.available, nodefs.inodesFree, imagefs.available, pid.available, and on newer nodes containerfs.*) against hard thresholds (evict immediately, no grace period beyond evictionMaxPodGracePeriod rules) and soft thresholds (wait evictionSoftGracePeriod first). When a threshold is met the node sets a condition (MemoryPressure, DiskPressure, PIDPressure), the scheduler stops placing new Pods that would worsen it, and the kubelet picks victims in this order:

  1. Pods whose usage exceeds their requests (which necessarily means BestEffort or Burstable), ordered by priority and then by how far over their requests they are.
  2. Guaranteed Pods and Burstable Pods using less than their requests, last, ordered by priority.

Two consequences matter for DoS. First, a correctly sized Guaranteed service survives a neighbour’s rampage; an unsized one does not. Second, PriorityClass influences who dies, which is why control-plane components run as system-node-critical and why a tenant that can create high-priority Pods can turn eviction into a weapon against other tenants. Restricting who may use high PriorityClasses (through ResourceQuota scopes on priorityClass) is a DoS control in its own right; our Pod priority and preemption guide explains the scheduling side.

evictionPressureTransitionPeriod (default 5m) keeps the node condition set for a while after recovery so that the scheduler does not immediately refill the node and cause oscillation. evictionMinimumReclaim makes each eviction reclaim a meaningful amount instead of evicting one Pod at a time right at the threshold.

Layer 4: Denial of Service Against the API Server

Every component in the cluster talks to kube-apiserver. If it is saturated, kubelets cannot report status, controllers cannot reconcile, and operators cannot run kubectl. The attack surface is wide: a controller stuck in a reconcile loop, a dashboard listing every Pod every second, a CI system hammering kubectl get across hundreds of namespaces, or an unauthenticated client that discovered the endpoint.

Historically the only defences were two flags: --max-requests-inflight (default 400) and --max-mutating-requests-inflight (default 200). They set a global ceiling and reject anything above it with HTTP 429. The problem: a flood of low-value list requests from one client could consume the entire ceiling and starve the kubelets’ heartbeat, which then marks nodes NotReady and cascades into Pod rescheduling, a self-inflicted DoS.

API Priority and Fairness (APF)

API Priority and Fairness, stable since v1.29, replaces the single ceiling with classified, fairly queued concurrency. With APF enabled the two flags are still read, but their sum becomes the server’s total concurrency and the mutating/non-mutating distinction disappears; the total is divided among priority levels. Two API objects in flowcontrol.apiserver.k8s.io/v1 configure it:

  • FlowSchema classifies incoming requests by subject (user, group, or service account), verbs, resources, namespaces or non-resource URLs, assigns them to a priority level, and sets a distinguisherMethod (ByUser or ByNamespace) that splits the matched traffic into flows so that one noisy user cannot crowd out another in the same level.
  • PriorityLevelConfiguration defines the level: type: Exempt (never limited) or type: Limited with nominalConcurrencyShares (its share of total concurrency), optional lendablePercent/borrowingLimitPercent for sharing idle capacity between levels, and limitResponse of either Reject (immediate 429) or Queue (shuffle-sharded fair queues with queues, handSize, and queueLengthLimit).

The default (suggested) configuration groups traffic into levels, approximately in order of importance: exempt (requests from system:masters bypass flow control), node-high (node health updates), system (other kubelet traffic), leader-election (control-plane leases), workload-high (built-in controllers), workload-low (other service accounts), global-default (everything else, including ordinary users), and a mandatory catch-all with a tiny concurrency share that rejects rather than queues, used only for requests nothing else matched. Unauthenticated requests that reach the server land in the lowest tiers, so even with anonymous access enabled they cannot starve kubelets.

A minimal custom pair that protects a cluster from an over-eager monitoring agent:

apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: PriorityLevelConfiguration
metadata:
  name: monitoring-low
spec:
  type: Limited
  limited:
    nominalConcurrencyShares: 5
    limitResponse:
      type: Queue
      queuing:
        queues: 16
        handSize: 4
        queueLengthLimit: 50
---
apiVersion: flowcontrol.apiserver.k8s.io/v1
kind: FlowSchema
metadata:
  name: monitoring-agents
spec:
  priorityLevelConfiguration:
    name: monitoring-low
  matchingPrecedence: 1000
  distinguisherMethod:
    type: ByUser
  rules:
  - subjects:
    - kind: ServiceAccount
      serviceAccount:
        name: metrics-agent
        namespace: monitoring
    resourceRules:
    - verbs: ["list", "watch", "get"]
      apiGroups: ["*"]
      resources: ["*"]
      namespaces: ["*"]

Operationally you confirm which level a request hit from the response headers X-Kubernetes-PF-FlowSchema-UID and X-Kubernetes-PF-PriorityLevel-UID, and you watch for apiserver_flowcontrol_rejected_requests_total and queue-depth metrics. A rise in 429s with Retry-After headers is the signature of APF doing its job. The KCSA does not expect you to tune queue sizes; it expects you to know that APF exists, that it protects important traffic (node heartbeats, leader election) from noisy traffic, and that it works by priority levels and fair queuing rather than one global limit.

The other API-server DoS controls

  • Authentication and authorization first. --anonymous-auth=false and tight RBAC reduce who can generate load at all. The cluster component security guide covers the flags.
  • --request-timeout (default 1m) bounds how long any single request can hold a seat; watch requests are accounted specially so that long-lived watches do not consume concurrency after their initial burst.
  • Client-side behaviour. Kubelets rate-limit themselves (kubeAPIQPS 50, kubeAPIBurst 100 by default), and well-written controllers use informers (watch + local cache) instead of polling with list. “Replace list polling with watches” is the standard remediation when APF metrics show one flow dominating.
  • Audit logging can itself become a DoS vector: a blocking log backend on a full disk stalls the API server. Use --audit-log-maxsize, --audit-log-maxbackup and --audit-log-maxage for file backends and batch mode for webhooks; see the audit logging guide.

Layer 5: etcd and Event Storms

etcd is the cluster’s single point of truth and has hard limits. Its storage quota (--quota-backend-bytes) defaults to 2 GiB with a recommended ceiling of 8 GiB; when the quota is hit etcd raises a NOSPACE alarm and the cluster becomes effectively read-only until an operator compacts, defragments, and clears the alarm. Individual requests above etcd’s request-size limit (1.5 MiB by default) are rejected, and Kubernetes separately caps ConfigMaps and Secrets at 1 MiB.

The classic way to fill etcd is an event storm: a crash-looping Deployment, a failing image pull across hundreds of Pods, or a controller emitting an event per reconcile. Controls:

  • --event-ttl on kube-apiserver (default 1h) expires Events, so they never accumulate forever.
  • A dedicated etcd for Events via --etcd-servers-overrides=/events#https://..., so event churn cannot starve the main datastore. This is standard on large clusters.
  • EventRateLimit admission plugin (alpha since v1.13, but recommended by the CIS Kubernetes Benchmark). It applies token buckets at Server, Namespace, User, or SourceAndObject granularity through an EventRateLimit configuration file referenced from --admission-control-config-file.
  • Kubelet event throttling via eventRecordQPS (default 50) and eventBurst (default 100).
  • Backup, compaction and defragmentation as routine operations so that a storm does not become permanent damage; the etcd backup and restore guide shows the commands.

For the exam, remember the chain: event storm → etcd quota → NOSPACE alarm → cluster read-only. Any question describing “cluster accepts reads but every write fails” after a mass crash-loop is pointing at etcd capacity.

Layer 6: Network Denial of Service

The threat model bullet Attacker on the Network overlaps here, but the DoS-specific mechanics are distinct:

  • Flat Pod network. By default any Pod can send unlimited traffic to any other Pod. A default-deny NetworkPolicy stops a compromised Pod from flooding internal services it has no business reaching; our default-deny and egress guide covers the manifests. NetworkPolicy is an access control, not a rate limiter; it reduces who can attack, not how hard an allowed peer can.
  • Ingress exposure. Anything reachable through a LoadBalancer Service or Ingress is reachable by the whole internet. Rate limiting belongs in the ingress controller (most support per-client request limits through annotations or CRDs), in a service mesh, or upstream in the cloud provider’s DDoS protection. Kubernetes itself has no built-in L7 rate limiter.
  • Conntrack exhaustion. kube-proxy in iptables or IPVS mode relies on the kernel connection-tracking table; kube-proxy’s conntrack.maxPerCore defaults to 32768 entries per core (with a floor given by conntrack.min). A flood of short-lived connections, or a DNS-heavy workload, can fill the table and cause nf_conntrack: table full, dropping packet, which looks like random connection failures cluster-wide. Sizing the table and shortening TCP timeouts (conntrack.tcpEstablishedTimeout) are node-level mitigations.
  • DNS amplification inside the cluster. Pods with the default ndots:5 resolver configuration generate several lookups per name, so a workload hammering DNS can take down CoreDNS for everyone. NodeLocal DNSCache and sensible CoreDNS replica counts and caching are the standard answers; the CoreDNS troubleshooting guide explains the ndots behaviour.
  • hostNetwork and hostPort. A Pod on the host network can bind host ports and consume host sockets, bypassing the Pod network entirely; the baseline Pod Security Standard forbids both, which is one of the few places PSS is a DoS control. See the Pod Security Standards guide.

Detecting a DoS in Progress

Controls limit damage; detection shortens it. The signals that map to each layer:

LayerWhat to watchWhere it comes from
CPU/memorycontainer OOM kills, CPU throttling, kubectl top outlierskubelet / cAdvisor metrics, kube_pod_container_status_last_terminated_reason
PIDsPIDPressure node condition, pid.availablekubectl describe node, kubelet eviction metrics
DiskDiskPressure, image GC events, evicted Pods with Evicted reasonnode conditions, Pod events
API serverapiserver_flowcontrol_rejected_requests_total, 429 rate, request latencykube-apiserver /metrics
etcdetcd_mvcc_db_total_size_in_bytes near quota, NOSPACE alarmetcdctl alarm list, etcd metrics
Networkconntrack usage, CoreDNS error rate, ingress 5xxnode exporter, CoreDNS metrics, ingress controller metrics

Runtime security tooling adds the behavioural view: a rule that fires on a process spawning hundreds of children, or a container opening thousands of outbound connections, catches the fork bomb and the flood before the node-level thresholds trip. The Falco runtime security guide covers how those rules work.

A Hardened Baseline, Layer by Layer

Pulling the controls into one checklist you can defend on the exam and apply on Monday:

  1. Every namespace gets a LimitRange with default and defaultRequest for CPU and memory, plus a ResourceQuota on compute, object counts, storage, and (for multi-tenant clusters) PriorityClass scopes.
  2. Every node’s kubelet sets podPidsLimit, reserves pid, cpu, memory and ephemeral-storage for systemReserved/kubeReserved, configures hard and soft eviction thresholds, and rotates container logs.
  3. Workloads declare ephemeral-storage limits and emptyDir.sizeLimit, run as Guaranteed or well-sized Burstable, and critical services use an appropriate PriorityClass.
  4. kube-apiserver runs with anonymous auth disabled, APF left enabled with any custom FlowSchemas for known-noisy clients, sane --max-requests-inflight totals, and audit logging that cannot block.
  5. etcd has backups, compaction and defragmentation scheduled, a quota sized for the cluster, a separate events store on large clusters, and --event-ttl left at a sane value.
  6. Network has default-deny policies in every tenant namespace, rate limiting at the ingress layer, baseline or stricter Pod Security Standards (no hostNetwork/hostPort), and conntrack sized for the workload.
  7. Detection is wired to node conditions, APF rejection metrics, etcd size, and a runtime security engine.

How to Practise This for the KCSA

The KCSA will not ask you to type a KubeletConfiguration. It will describe a symptom and offer four controls, three of which act on the wrong resource or at the wrong layer. The skill is fast, confident mapping. Build it by working through scenario questions where the distractors are deliberately plausible, then reading the explanation for why each wrong answer is wrong. The KCSA mock exam bundle is structured around the six exam domains, with threat-model scenarios like the ones in this article and explanations that reference the exact control. Pair it with the free KCSA practice questions and the KCSA exam guide for the logistics and domain weights.

Frequently Asked Questions

What counts as denial of service in the KCSA threat model?

Any action that makes cluster resources unavailable to legitimate workloads: exhausting node CPU, memory, PIDs or disk; saturating the API server or etcd; or flooding the network. The exam expects you to pair each with the control that bounds it.

Do ResourceQuotas and LimitRanges prevent fork bombs?

No. They bound CPU, memory, storage and object counts, not process counts. Fork bombs are contained by the kubelet’s podPidsLimit (a per-Pod pids cgroup limit) and PID reservations, backed by pid.available eviction.

What is API Priority and Fairness in Kubernetes?

APF (stable since v1.29) classifies API requests with FlowSchema objects into priority levels defined by PriorityLevelConfiguration, each with a share of the server’s total concurrency and fair queuing between flows. It ensures critical traffic such as node heartbeats and leader election is not starved by noisy clients, and rejects or queues excess with HTTP 429.

What happens when etcd runs out of space?

etcd raises a NOSPACE alarm and refuses writes; the cluster becomes read-only until an operator compacts, defragments, and disarms the alarm. Event storms are the most common cause, mitigated by --event-ttl, a dedicated events etcd, and the EventRateLimit admission plugin.

Which Pods does the kubelet evict first under node pressure?

Pods using more than their requests (BestEffort and over-request Burstable), ordered by priority then by overage. Guaranteed Pods and under-request Burstable Pods are evicted last. For PID and inode pressure, priority is not used for ordering.

Does a NetworkPolicy protect against DoS?

Partly. Default-deny policies stop a compromised Pod from reaching and flooding services it should not talk to, but NetworkPolicy does not rate-limit allowed traffic. Rate limiting belongs in the ingress controller, service mesh, or upstream DDoS protection.

Is memory-backed emptyDir counted against memory limits?

Yes. An emptyDir with medium: Memory is a tmpfs whose usage counts toward the container’s memory limit, so writing into it can trigger an OOM kill rather than a disk-pressure eviction.

Conclusion

Denial of service in Kubernetes is not one threat but a family, and the controls are not interchangeable. Requests and limits, LimitRanges and ResourceQuotas bound CPU and memory; kubelet PID limits and reservations bound processes; ephemeral-storage limits and node-pressure eviction bound disk; API Priority and Fairness bounds the API server; quotas, TTLs and rate limits bound etcd; and NetworkPolicy plus upstream rate limiting bound the network. The KCSA tests whether you can hear a symptom and name the right layer, and a cluster built on all seven layers of the baseline above is one where a single bad Pod stays a single bad Pod.

Continue with the KCSA threat model guide for the other attacker goals, the cluster component security guide for the API server and kubelet flags referenced here, and the KCSA study plan to schedule the rest of the domains.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now