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 out | Who exhausts it | Primary control | Secondary / detection |
|---|---|---|---|
| CPU, memory on a node | one container without limits | resources.limits, LimitRange defaults | ResourceQuota, QoS, node-pressure eviction |
| Process IDs on a node | fork bomb, runaway worker pool | podPidsLimit, pid= reservation | pid.available eviction |
| Node disk, inodes | logs, emptyDir, image layers | ephemeral-storage limits, sizeLimit | nodefs/imagefs eviction, log rotation |
| API server concurrency | hot-looping controller, misbehaving client, unauthenticated flood | API Priority and Fairness | --anonymous-auth=false, RBAC, 429 metrics |
| etcd storage | event storms, object churn, huge objects | --quota-backend-bytes, --event-ttl, separate events etcd | EventRateLimit admission, compaction/defrag |
| Cluster network | traffic floods, DNS floods, conntrack exhaustion | NetworkPolicy, ingress rate limits, upstream DDoS protection | conntrack 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:
- 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. - LimitRange in each namespace sets
defaultanddefaultRequestso that a Pod with noresourcesblock still gets bounded, andmax/min/maxLimitRequestRatioso a tenant cannot request a whole node in one Pod. - 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. - 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:
podPidsLimitin theKubeletConfiguration(default-1, unlimited). The kubelet applies it as apidscgroup limit on each Pod, so a fork bomb inside one Pod hitsfork: retry: Resource temporarily unavailablewhile the rest of the node continues. - Node-level reservation:
pid=<n>insidesystemReservedandkubeReservedcarves 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-storageper container. The kubelet sums the writable layer, logs, andemptyDirusage 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.sizeLimitbounds a single volume; exceeding it also evicts the Pod. For memory-backedemptyDir(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%. Whennodefsis 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
containerLogMaxSizeandcontainerLogMaxFiles, so that a chatty container cannot fill the disk through stdout alone. - ResourceQuota on
requests.storageandpersistentvolumeclaimsso 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:
- Pods whose usage exceeds their requests (which necessarily means
BestEffortorBurstable), ordered by priority and then by how far over their requests they are. GuaranteedPods andBurstablePods 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:
FlowSchemaclassifies incoming requests by subject (user, group, or service account), verbs, resources, namespaces or non-resource URLs, assigns them to a priority level, and sets adistinguisherMethod(ByUserorByNamespace) that splits the matched traffic into flows so that one noisy user cannot crowd out another in the same level.PriorityLevelConfigurationdefines the level:type: Exempt(never limited) ortype: LimitedwithnominalConcurrencyShares(its share of total concurrency), optionallendablePercent/borrowingLimitPercentfor sharing idle capacity between levels, andlimitResponseof eitherReject(immediate 429) orQueue(shuffle-sharded fair queues withqueues,handSize, andqueueLengthLimit).
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=falseand 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 (
kubeAPIQPS50,kubeAPIBurst100 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-maxbackupand--audit-log-maxagefor 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-ttlon 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. EventRateLimitadmission plugin (alpha since v1.13, but recommended by the CIS Kubernetes Benchmark). It applies token buckets atServer,Namespace,User, orSourceAndObjectgranularity through anEventRateLimitconfiguration file referenced from--admission-control-config-file.- Kubelet event throttling via
eventRecordQPS(default 50) andeventBurst(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
NetworkPolicystops 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
LoadBalancerService 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.maxPerCoredefaults to 32768 entries per core (with a floor given byconntrack.min). A flood of short-lived connections, or a DNS-heavy workload, can fill the table and causenf_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:5resolver 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 thendotsbehaviour. hostNetworkandhostPort. A Pod on the host network can bind host ports and consume host sockets, bypassing the Pod network entirely; thebaselinePod 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:
| Layer | What to watch | Where it comes from |
|---|---|---|
| CPU/memory | container OOM kills, CPU throttling, kubectl top outliers | kubelet / cAdvisor metrics, kube_pod_container_status_last_terminated_reason |
| PIDs | PIDPressure node condition, pid.available | kubectl describe node, kubelet eviction metrics |
| Disk | DiskPressure, image GC events, evicted Pods with Evicted reason | node conditions, Pod events |
| API server | apiserver_flowcontrol_rejected_requests_total, 429 rate, request latency | kube-apiserver /metrics |
| etcd | etcd_mvcc_db_total_size_in_bytes near quota, NOSPACE alarm | etcdctl alarm list, etcd metrics |
| Network | conntrack usage, CoreDNS error rate, ingress 5xx | node 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:
- Every namespace gets a
LimitRangewithdefaultanddefaultRequestfor CPU and memory, plus aResourceQuotaon compute, object counts, storage, and (for multi-tenant clusters) PriorityClass scopes. - Every node’s kubelet sets
podPidsLimit, reservespid,cpu,memoryandephemeral-storageforsystemReserved/kubeReserved, configures hard and soft eviction thresholds, and rotates container logs. - Workloads declare
ephemeral-storagelimits andemptyDir.sizeLimit, run asGuaranteedor well-sizedBurstable, and critical services use an appropriate PriorityClass. - kube-apiserver runs with anonymous auth disabled, APF left enabled with any custom
FlowSchemas for known-noisy clients, sane--max-requests-inflighttotals, and audit logging that cannot block. - etcd has backups, compaction and defragmentation scheduled, a quota sized for the cluster, a separate events store on large clusters, and
--event-ttlleft at a sane value. - Network has default-deny policies in every tenant namespace, rate limiting at the ingress layer,
baselineor stricter Pod Security Standards (nohostNetwork/hostPort), and conntrack sized for the workload. - 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.