13 Aug 2026

Kubernetes on Oxide: How Customer Needs Shaped Our Integrations

Author image for Matthew Sanabria
Matthew Sanabria
Solutions Software Engineer

In late 2024, customers and prospects were eager to run Kubernetes on Oxide, but we had no supported integrations to help them do it.

Kubernetes and Oxide are a natural fit. Kubernetes defines the infrastructure behavior it expects through standard extension points, while Oxide exposes the primitives needed to implement that behavior through APIs. The foundation for integration was there. What was missing was the software and an understanding of which integrations customers actually needed.

That was the situation when I joined Oxide as its first Solutions Software Engineer,[1] focused on building software to solve customer problems. My first assignment was to make it easier to deploy and operate Kubernetes on Oxide.

In my first week, I was handed two resources to help me get started:

  1. A customer-submitted pull request for a Rancher node driver

  2. An early draft of RFD 493 Initial Kubernetes Integrations

What began with those two resources grew into a team effort, using a feedback loop guided by customer problems. This post describes how we solved each customer problem, focusing on the category of Kubernetes integration rather than chronological order of development.

How do I provision a Kubernetes cluster on Oxide?

No single provisioning approach fit all customers' workflows, so we ended up publishing three integrations.

Rancher Node Driver

Our first goal was to unblock the customer that submitted the Rancher node driver pull request. Before we could merge the pull request, we needed to understand how it worked. I had never used Rancher or worked with a node driver, so reviewing the contribution meant learning both.

I learned that a Rancher node driver is a binary that allows Rancher to provision virtual machines as nodes in a Rancher-managed Kubernetes cluster. The Oxide Rancher node driver is Oxide’s implementation.

Testing confirmed that the customer’s contribution worked, so I merged the pull request, added CI/CD and documentation improvements, and published the initial release. Oxide officially had its first Kubernetes integration—​and a customer was already using it successfully in production!

If you’re a Rancher shop looking to run Kubernetes on Oxide, see our Rancher guide to get started.

Omni Infrastructure Provider

Customers expressed interest in using Sidero Labs' Omni to provision Kubernetes clusters running Talos Linux. Omni connects to infrastructure platforms through infrastructure providers, programs that create Talos Linux instances and register them with Omni.

With KubeCon North America 2025 a few months away, we saw an opportunity to partner with Sidero Labs to build and showcase an Oxide infrastructure provider for Omni. We had seven weeks to complete it before our Oxide+Sidero event.[2] Building against a second provisioning platform would also test Oxide’s APIs across distinct customer workflows.

The integration work uncovered several issues across Omni and Talos Linux. I brought those issues to Sidero Labs in siderolabs/omni#1633, where their team was eager to work with us—​a lovely reminder of RFD 68 Partnership as Shared Values.

The most memorable issue was siderolabs/talos#11948. Oxide uses a FAT12 filesystem for cloud-init user-data, not ISO 9660, but Talos’s filesystem probe only attempted to read an ISO 9660 superblock from the NoCloud configuration disk. When that read failed, the probe stopped instead of trying other formats such as VFAT or MS-DOS. As a result, Talos never read the Oxide user-data containing the configuration needed to join Omni. The fix would not be released in time for KubeCon, leaving us with a rather funny workaround.

The workaround right now is to pad the user-data with comments to increase its size enough that it uses an ISO 9660 superblock.

KubeCon arrived and we hosted an Oxide+Sidero event to showcase the Oxide infrastructure provider for Omni. Customers could now use this infrastructure provider to provision Oxide instances running Talos Linux as nodes in Omni-managed Kubernetes clusters.

If you’re an Omni or Talos Linux shop looking to run Kubernetes on Oxide, see our Omni guide to get started.

Cluster API Provider

We knew we wanted to build a Kubernetes Cluster API (CAPI) infrastructure provider when we first wrote RFD 493 Initial Kubernetes Integrations. Cluster API offered something our first two integrations didn’t—​an upstream, provider-extensible API for managing clusters without requiring a third-party platform like Rancher or Omni.

Building a CAPI infrastructure provider is a significant investment. At the time, customer demand and engineering capacity did not justify the investment, so I deferred the project.

Eventually, customers began asking for a CAPI infrastructure provider and the Solutions Software Engineering team grew. My teammates Josh and Brandon took ownership of the project and released Cluster API Provider Oxide (CAPOx), giving customers a Kubernetes-native way to provision clusters on Oxide.

The Cluster API exercises several of our other integrations, allowing us to dogfood[3] the end-to-end cluster workflow. The Kubernetes Image Builder uses our Packer plugin to create CAPI-ready Oxide VM images, which CAPOx uses when provisioning instances. Clusters provisioned with CAPOx also use the separately installed Oxide cloud controller manager (CCM) to integrate Kubernetes with Oxide at runtime.

If you want to provision Kubernetes clusters on Oxide with Cluster API, see our Cluster API guide to get started.

How does Kubernetes track Oxide instances?

Provisioning integrations create and manage Oxide instances, but they do not reconcile those instances with Kubernetes Node objects. Without this reconciliation, a cluster cannot determine the status of an unreachable Kubernetes node.

We needed a component that ran in each cluster and used the Oxide API to reconcile Oxide infrastructure with Kubernetes state. Kubernetes provides the cloud controller manager (CCM) for exactly this purpose. A CCM integrates Kubernetes resources with an infrastructure provider’s API.

We built the Oxide cloud controller manager to integrate Kubernetes with Oxide. Its node controller keeps Kubernetes Node objects synchronized with their Oxide instances, recording details such as instance IDs, network addresses, and instance state. Kubernetes uses this information to initialize nodes and remove them from the cluster when their Oxide instances are deleted.

The CCM does not create instances or provision clusters. That remains the job of provisioning integrations such as the Rancher node driver, the Omni infrastructure provider, and CAPOx. The CCM works with all the provisioning integrations and gives us an integration point to build upon. As Oxide evolves, we can add new controllers to the CCM rather than update every provisioning integration.

With that runtime extension point in place, we could address another layer of the Kubernetes experience: exposing applications. The CCM architecture also defines a service controller for Kubernetes LoadBalancer services, giving us a place to address the next customer problem.

How do I use LoadBalancer services?

One of the capabilities customers expect from cloud-integrated Kubernetes is support for Service objects of type LoadBalancer. When a user creates one, Kubernetes asks the CCM’s service controller to provision the load balancer and publish its address in the Service status. There was just one problem: Oxide did not yet offer a native load balancer.

Oxide did, however, have floating IPs. Floating IPs are external addresses that can be attached to and detached from instances, exposing those instances outside the VPCs. I decided to use floating IPs for LoadBalancer services to work around Oxide’s lack of a native load balancer. The idea was that a floating IP would deliver traffic to a single Kubernetes node, and the Kubernetes Service would distribute that traffic to the appropriate pods.

Making that work required accounting for how Oxide floating IPs appear to an instance. They are transparent to the guest in two important ways. First, Oxide translates the destination address of inbound traffic to the instance’s internal IP before sending the traffic to the instance. Second, the instance has no network interface configured with the floating IP.

The resulting traffic flow looks like this:

Traffic flow to a LoadBalancer service using floating IPs.
┌────────────────────────────────────────────────────────────┐
│ Client │
│ Request to floating IP: 45.154.216.233:80 │
└────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────┐
│ Oxide networking │
│ Translates destination to internal IP: 172.30.0.5:80 │
└────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────┐
│ Kubernetes node │
│ Packet arrives at internal IP: 172.30.0.5:80 │
└────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────┐
│ Kubernetes Service │
│ Selects a Service endpoint │
└────────────────────────────────────────────────────────────┘


┌────────────────────────────────────────────────────────────┐
│ Pod │
│ Receives traffic on its target port │
└────────────────────────────────────────────────────────────┘

Since traffic arrives at the instance using its internal IP, the service controller publishes two entries in status.loadBalancer.ingress:[4]

  1. The attached floating IP in Proxy mode. This allows clients to see the external IP for the service.

  2. The node’s internal IP in VIP mode. This allows the Kubernetes service to accept the traffic from the floating IP.

The status entries look like this:

status:
loadBalancer:
ingress:
- ip: 45.154.216.233
ipMode: Proxy
- ip: 172.30.0.5
ipMode: VIP

This architecture also makes the kubectl output look a little unusual:

$ kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.106.122.233 45.154.216.233,172.30.0.5 80:30605/TCP 37h

Users see both the floating IP and the node’s internal IP in the EXTERNAL-IP column, even though only the floating IP is externally reachable. This is an imperfect abstraction, but it allows us to support a common Kubernetes workflow while waiting for a native Oxide load balancer.

This implementation currently supports externalTrafficPolicy: Cluster,[5] which allows the selected node to forward traffic to a Service endpoint anywhere in the cluster. If that node disappears, the CCM moves the floating IP to another eligible node and updates the internal address in the Service status.

When we introduce a native Oxide load balancer, we can update the service controller to use it without changing the Kubernetes interface. Customers will continue creating the same LoadBalancer services, this time powered by the Oxide load balancer.

To install the Oxide CCM on your cluster, see our CCM guide to get started.

How do I use Oxide storage in Kubernetes?

With clusters provisioned, reconciled with Oxide, and reachable from outside their VPCs, storage for stateful workloads became the next problem to address. Kubernetes users request persistent storage through PersistentVolumeClaim objects and expect a Container Storage Interface (CSI) plugin to create, attach, and mount the underlying volumes. Oxide had disks, but Kubernetes had no native way to manage their lifecycle.

Without an Oxide CSI plugin, customers could deploy a third-party Kubernetes storage system such as Longhorn. Longhorn provides its own CSI plugin that uses existing disks and handles its own replication. The only disk Oxide supported at the time was a distributed disk which replicates data across three sleds. Using Longhorn with distributed disks created substantial write fan-out that customers wanted to eliminate.

Oxide local disks gave us a way to reduce write amplification when using Longhorn. Local disks have no built-in replication, making them well suited for Longhorn. Our Rancher showcase uses this architecture to reduce write amplification. While this works today, customers still wanted a native Oxide CSI plugin.

My teammate Luiz wrote RFD 595 Oxide CSI Plugin, describing how an Oxide CSI plugin would work. Everything seemed straightforward on paper, a user creates a PersistentVolumeClaim and the CSI controller creates an Oxide distributed disk, attaches it to the node running the pod, and formats it for use. If the pod is rescheduled to another node, the CSI controller detaches the disk and reattaches it to the new node.

Further discussion and prototyping exposed a blocker. Oxide requires an instance to be stopped before attaching or detaching disks. Kubernetes, however, expects a CSI plugin to attach storage to a running node. Stopping the node would disrupt every other workload and could trigger cascading scheduling and attachment operations.

Before we can release our CSI plugin, we need to add support for disk hot-plug throughout the Oxide stack, from the hypervisor all the way up to the API. What began as a Kubernetes integration has turned into a project spanning multiple layers of the Oxide software stack.

Disk hot-plug and the Oxide CSI plugin remain under active development as of this writing. In the meantime, customers can use software such as Longhorn with Oxide local disks for dynamically provisioned persistent storage without stacking two layers of replication. When the native CSI plugin ships, customers will be able to use familiar Kubernetes storage APIs backed directly by Oxide distributed disks with replication and durability built in.

What’s next?

We now have a growing ecosystem of Kubernetes integrations that work well with one another. Rancher, Omni, and Cluster API solve Kubernetes provisioning, the Oxide CCM reconciles Kubernetes nodes with Oxide instances and handles LoadBalancer services. Customers already use some of these integrations in production, and we dogfood several in our own production workloads. Together, they provide a solid foundation to build on.

Our next step is to expand our dogfooding with the newly released Cluster API provider. Using it to provision and operate more of our clusters will test how these integrations work together day to day.

We still have plenty to build and polish. Our near-term work includes completing disk hot-plug and shipping the CSI plugin, adding autoscaling support, and extending the CCM service controller to support external subnets. Longer term, as we ship resource tagging, OIDC support, and native load balancing, we’ll extend our Kubernetes integrations to take advantage of them.

Building these integrations showed how the architectures of Kubernetes and Oxide complement one another. Kubernetes gives infrastructure providers standard extension points, while Oxide exposes infrastructure primitives through APIs. Oxide’s hardware and software co-design lets us address integration blockers at the layer where they belong and carry the necessary changes through the full stack.

This work also lets us exercise our SDKs and APIs from our customers' perspectives and turn customer friction into product improvements. That feedback loop is how we will continue growing this ecosystem. Customer needs shaped each integration in this post, and they will shape the next one, too.

See it in action

To see the Cluster API and cloud controller manager integrations in action, watch the video below, in which I deploy a Kubernetes cluster on Oxide.

Deploy Kubernetes on Oxide with Cluster API
  • 1

    There’s a team now! Check out the Oxide and Friends episode Solutions Software Engineering with Matthew Sanabria.

    View
  • 2

    Our Oxide+Sidero event was November 12, 2025. Work on the Omni infrastructure provider began on September 24.

    View
  • 3

    Dogfooding is the practice of using one’s own products or services. Oxide has a rack named dogfood in the office dedicated to, well, dogfooding.

    View
  • 4

    Kubernetes uses ipMode to indicate whether traffic reaches the node with the load-balancer address as its destination (VIP) or after the destination has been translated (Proxy).

    View
  • 5

    Supporting externalTrafficPolicy: Local would require the floating IP to follow nodes with local Service endpoints as pods are rescheduled, resulting in more attachment and detachment operations.

    View