Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

Resource Limits & Denial-of-Service Prevention

A pod without resource limits can consume all CPU and memory on its node — whether by bug, misconfiguration, or deliberate attack. This is a denial-of-service condition. Kubernetes Quality of Service (QoS) classes give you three levels of resource guarantee. This lab demonstrates the noisy neighbour problem and then shows how limits isolate the blast radius.

The Noisy Neighbour Problem

Why Resource Limits Are a Security Control

An application without resource limits is a ticking clock. It might behave well under normal load — but under a bug, a traffic spike, or a deliberate attack, it will consume all available CPU and memory on the node, evicting every other pod.

This is a denial-of-service condition from inside your own cluster. It can be caused by:

  • A memory leak in your application
  • A recursive loop or runaway goroutine
  • An attacker who has compromised one pod and is deliberately starving others

Kubernetes addresses this with three things:

  1. Resource requests — the scheduler uses these to decide which node a pod runs on
  2. Resource limits — the kernel kills processes that exceed these
  3. Quality of Service (QoS) classes — Kubernetes's priority system for which pods survive when a node runs out of resources

Create the Demo Namespace

kubectl create namespace limits-demo

Deploy a Pod WITHOUT Limits

cat > unlimited-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: unlimited-app
  namespace: limits-demo
spec:
  containers:
  - name: app
    image: public.ecr.aws/nginx/nginx:1.25-alpine
EOF
kubectl apply -f unlimited-pod.yaml
kubectl wait --for=condition=Ready pod/unlimited-app -n limits-demo --timeout=60s

Check Its QoS Class

kubectl get pod unlimited-app -n limits-demo \
  -o jsonpath='{.status.qosClass}'

Expected: BestEffort

BestEffort is Kubernetes's lowest QoS class — these pods are the first to be evicted when the node runs low on memory. They have no resource guarantees whatsoever.

# Prove there are no limits
kubectl get pod unlimited-app -n limits-demo \
  -o jsonpath='{.spec.containers[0].resources}'

Expected: {} — completely empty.

The Three QoS Classes

┌─────────────────────────────────────────────────────────┐
│  Guaranteed (highest priority — last to be evicted)     │
│  requests == limits for ALL containers                  │
├─────────────────────────────────────────────────────────┤
│  Burstable (middle priority)                            │
│  at least one container has a request OR limit set      │
├─────────────────────────────────────────────────────────┤
│  BestEffort (lowest priority — first evicted)           │
│  NO requests or limits set on ANY container             │
└─────────────────────────────────────────────────────────┘

Under memory pressure, the node's Out-of-Memory (OOM) killer evicts BestEffort pods first, then Burstable, and only touches Guaranteed pods as a last resort.

Setting Limits, LimitRanges & ResourceQuotas

Burstable vs Guaranteed

Deploy the two correctly configured pods and observe how QoS class changes with resource settings:

cat > burstable-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: burstable-app
  namespace: limits-demo
spec:
  containers:
  - name: app
    image: public.ecr.aws/nginx/nginx:1.25-alpine
    resources:
      requests:
        cpu: "100m"
        memory: "64Mi"
      limits:
        cpu: "500m"
        memory: "256Mi"
EOF
kubectl apply -f burstable-pod.yaml
kubectl wait --for=condition=Ready pod/burstable-app -n limits-demo --timeout=60s
kubectl get pod burstable-app -n limits-demo -o jsonpath='{.status.qosClass}'

Expected: Burstable

Requests (100m CPU, 64Mi memory) differ from limits (500m, 256Mi). The pod is guaranteed at least the request and can burst up to the limit.

cat > guaranteed-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: guaranteed-app
  namespace: limits-demo
spec:
  containers:
  - name: app
    image: public.ecr.aws/nginx/nginx:1.25-alpine
    resources:
      requests:
        cpu: "200m"
        memory: "128Mi"
      limits:
        cpu: "200m"
        memory: "128Mi"
EOF
kubectl apply -f guaranteed-pod.yaml
kubectl wait --for=condition=Ready pod/guaranteed-app -n limits-demo --timeout=60s
kubectl get pod guaranteed-app -n limits-demo -o jsonpath='{.status.qosClass}'

Expected: Guaranteed

Requests equal limits for all resources. Kubernetes guarantees this pod its allocation; it will not be evicted until Guaranteed eviction begins (near-OOM state).

Enforce Limits Namespace-Wide with LimitRange

A LimitRange sets default limits for every container in a namespace — so a developer cannot forget to set limits, and a malicious pod cannot be deployed without them:

cat > limitrange.yaml << 'EOF'
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: limits-demo
spec:
  limits:
  - type: Container
    default:
      cpu: "200m"
      memory: "128Mi"
    defaultRequest:
      cpu: "100m"
      memory: "64Mi"
    max:
      cpu: "1"
      memory: "512Mi"
    min:
      cpu: "50m"
      memory: "32Mi"
EOF
kubectl apply -f limitrange.yaml
kubectl describe limitrange default-limits -n limits-demo

Now any container deployed without explicit resources gets the defaults. And any container requesting more than max is rejected at admission time.

Cap Total Namespace Consumption with ResourceQuota

A LimitRange controls individual containers. A ResourceQuota caps the total resources a namespace can consume — preventing a compromised namespace from using the whole cluster:

cat > resourcequota.yaml << 'EOF'
apiVersion: v1
kind: ResourceQuota
metadata:
  name: ns-quota
  namespace: limits-demo
spec:
  hard:
    requests.cpu: "2"
    requests.memory: "1Gi"
    limits.cpu: "4"
    limits.memory: "2Gi"
    pods: "10"
    secrets: "20"
    services: "5"
EOF
kubectl apply -f resourcequota.yaml
kubectl describe resourcequota ns-quota -n limits-demo

Expected output shows current vs. allowed:

Resource          Used    Hard
--------          ----    ----
limits.cpu        ...     4
limits.memory     ...     2Gi
pods              3       10
requests.cpu      ...     2
requests.memory   ...     1Gi
secrets           ...     20
services          0       5

Security Summary

ControlProtects Against
resources.limitsSingle container consuming all node resources
LimitRangeDevelopers forgetting to set limits; pods deployed without resource controls
ResourceQuotaA compromised namespace claiming the entire cluster's resources
Guaranteed QoSCritical workloads surviving under node memory pressure

Cleanup

kubectl delete namespace limits-demo