Kubernetes Security Hardening (NetworkPolicies) & HPA
Deny traffic between Pods by default, then allow only what the application needs — and scale it under load.
- Time
- 47 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Low cost
Where this fits in the platform
Already built
This lab adds
- Default-deny networking, admission-enforced hardening, autoscaling
Before you start
Cost — Low cost
Depends on an existing cluster. NetworkPolicies and HPA objects are free; the cluster and any nodes the HPA scales up are not.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
Every Pod can reach every other Pod, in every namespace. A compromise of the public-facing service is a port scan away from the database.
The application is also fixed at two replicas, so traffic either wastes money or drops requests.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
Create the cluster with `./egykode cluster calico` for this one. kind's default CNI accepts NetworkPolicy objects and enforces none of them, so your policies would appear to apply while blocking nothing. HPA also needs metrics-server, which the same command installs.
You will need:
- docker
- kubectl
- kind
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start k8s
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
Default deny — start by breaking it
Step 1 of 3
What you are building#
Two independent controls that are often taught together and solve different problems: NetworkPolicy decides what a Pod may talk to; HPA decides how many of it there are.
ingress controller ---> api ---> postgres
|
+------> kube-dns :53 (easy to forget)
everything else --X--> api default denyBuild it#
What you are proving: You can apply a default-deny policy and demonstrate that traffic is genuinely blocked, not merely accepted
Marking this settles success criteria 1 and 4.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: platform
spec:
podSelector: {} # every Pod in the namespace
policyTypes: ["Ingress", "Egress"]An empty podSelector selects everything; no rules means nothing is allowed.
Apply this first and confirm the application breaks — a default-deny you cannot
prove is doing anything is indistinguishable from one that is silently ignored.
What you are proving: You can allow exactly the traffic an application needs, including the DNS everyone forgets
Marking this settles success criterion 2.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow
namespace: platform
spec:
podSelector:
matchLabels: { app: api }
policyTypes: ["Ingress", "Egress"]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
ports:
- protocol: TCP
port: 8000
egress:
# DNS first. Without it nothing resolves and every symptom is misleading.
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- podSelector:
matchLabels: { app: postgres }
ports:
- protocol: TCP
port: 5432Forgetting DNS egress is the classic self-inflicted outage. Everything still works by IP, so the application looks reachable, while every hostname lookup times out — and the symptom is slow failures rather than refusals, which sends people to look at the database.
Two more things worth holding onto:
- Policies are additive, and there is no deny rule. A Pod's allowed traffic is the union of every policy selecting it. You restrict by not allowing.
- A Pod selected by no policy at all is unrestricted. That is why the default-deny exists; without it, adding a policy to one Pod leaves every other Pod wide open.
What you are proving: You can scale a workload on CPU, and explain why an HPA with no requests does nothing
Marking this settles success criterion 3.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
namespace: platform
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # react quickly
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # leave slowly
policies:
- type: Pods
value: 1
periodSeconds: 60averageUtilization: 70 is a percentage of the CPU request, not of the
node. A Pod requesting 100m and using 70m is at 100% by this measure. An
HPA on a Deployment with no CPU request cannot compute a ratio and does
nothing, reporting <unknown> — which is the most common reason an HPA appears
inert.
The asymmetric behavior is deliberate: scale up fast because the alternative
is dropped requests, scale down slowly because flapping is worse than a few
minutes of extra capacity.
metrics-server must be installed, or there is no CPU reading at all.
Verify it worked#
# The negative test — this is the actual security claim
kubectl run intruder --rm -it --image=curlimages/curl --restart=Never -n platform -- \
curl -s --max-time 5 http://api:80/ # must time out
# The positive test, from a Pod the policy allows
kubectl run probe --rm -it --image=curlimages/curl --restart=Never \
-n ingress-nginx -- curl -s -o /dev/null -w '%{http_code}\n' http://api.platform/
# DNS still resolves from the app
kubectl exec -n platform deploy/api -- nslookup postgres.platform.svc.cluster.local
# HPA has a reading, not <unknown>
kubectl get hpa -n platform
kubectl top pods -n platform
# Generate load and watch it scale
kubectl run load --rm -it --image=busybox:1.36 --restart=Never -n platform -- \
sh -c 'while true; do wget -q -O- http://api:80/ >/dev/null; done'
kubectl get hpa api -n platform -w
kubectl get deploy api -n platform -wBoth the timeout and the success matter. Proving traffic flows is easy; proving that traffic which should not flow does not is the claim you are actually making.
The policy has no effect and everything still connects
The CNI must enforce NetworkPolicy, and not all do. kind's default CNI accepts the objects and ignores them entirely — no error, no warning. Calico, Cilium and the AWS VPC CNI (with policy enforcement enabled) do enforce. Check what the cluster runs before concluding the policy is wrong.
Everything broke and the errors mention timeouts
DNS egress. Add UDP and TCP 53 to kube-system.
A policy in another namespace does not apply
NetworkPolicies are namespaced and select Pods in their own namespace only. The
namespaceSelector matches the other end of the connection.
namespaceSelector matches nothing
It matches namespace labels, not names. Kubernetes 1.22+ sets
kubernetes.io/metadata.name automatically; older clusters need a label added.
HPA shows <unknown> for the metric
metrics-server is not installed, or the Deployment has no CPU request. Both
are required.
It scales up and immediately back down
Add a scaleDown stabilization window. Without one, a brief dip removes the
capacity you just added.
Clean up#
Destructive — This removes real resources. Check which environment you are in first.
kubectl delete networkpolicy --all -n platform
kubectl delete hpa --all -n platform
kubectl delete namespace platformCost of this lab: Free on kind or minikube with a CNI that enforces policy. On EKS the cluster bills either way; these objects add nothing.
Maintained by others, on Killercoda. Useful for extra repetition on one tool — it does not complete this lab or settle any criterion above.
- Kyverno scenariospolicy enforcement beyond NetworkPolicy
- Falco scenariosruntime threat detection
Success criteria
0 of 4
The concept behind it
Phase complete · 06 Kubernetes
You can now: The application runs on Kubernetes with storage, routing, scoped permissions, network policy and autoscaling.
Next phase
Lab 40 of 59 on the project path