PI-GROUPE
Edge ComputingSecurityCybersecurityKubernetesVirtualisationArchitectureInfrastructureOpen Source

Kairos and vk-cocoon: a fleet of edge clusters, isolated down to the kernel

Kairos holds the physical host as an immutable system, vk-cocoon isolates every workload in a MicroVM. Two complementary layers for running untrusted code on sites nobody guards.

The two previous articles each dealt with half the problem. Kairos solves the management of a distributed physical fleet. vk-cocoon solves workload isolation. Combining them produces an architecture that answers one precise and increasingly common case: running code whose origin you do not control, on machines nobody watches physically.

That is the situation of an operator hosting customer processing at the edge, of a manufacturer welcoming supplier software modules onto its sites, or of a public body pushing processing out to accessible premises.

How the roles divide

text
┌────────────────────────────────────────────────────────────┐
│  SITE PHYSIQUE — local technique, usine, emprise           │
│                                                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  Kairos — système immuable, image OCI signée          │  │
│  │  • racine en lecture seule, deux partitions           │  │
│  │  • démarrage vérifié, TPM, disque chiffré             │  │
│  │  • starts K3s and joins the site mesh                 │  │
│  │                                                       │  │
│  │   ┌─────────────────────────────────────────────┐    │  │
│  │   │  K3s — the site autonomous cluster          │    │  │
│  │   │                                             │    │  │
│  │   │   Charges internes ──▶ conteneurs standard  │    │  │
│  │   │                                             │    │  │
│  │   │   Charges third-party  ──▶ vk-cocoon            │    │  │
│  │   │                        └─▶ MicroVM          │    │  │
│  │   │                            noyau dédié      │    │  │
│  │   └─────────────────────────────────────────────┘    │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘
                            │ GitOps sortant uniquement
                            ▼
              Plan de contrôle central (Fleet / ArgoCD)

Each layer answers a distinct threat.

ThreatLayer that answersMechanism
System tamperingKairosRead-only root, signed image, verified boot
Physical theft of the machineKairosDisk encryption sealed to the TPM
Container escapevk-cocoonA separate kernel per workload
Lateral movement between workloadsvk-cocoon + network policiesHypervisor isolation and segmentation
Control-plane compromiseArchitectureThe site only calls out, never in

Preparing the host inside the image

The hypervisor, the guest kernel and the base file system all go into the Kairos image. Nothing gets installed at boot.

dockerfile
FROM quay.io/kairos/ubuntu:24.04-standard-amd64-generic-v3.6.0-k3sv1.31.4-k3s1

# Hypervisor and MicroVM tooling
RUN apt-get update && apt-get install -y --no-install-recommends       cloud-hypervisor qemu-utils iproute2     && rm -rf /var/lib/apt/lists/*

# Minimal guest kernel, built for fast boot
COPY --chmod=0644 vmlinux-6.12-microvm /var/lib/cocoon/vmlinux-6.12-microvm
COPY --chmod=0644 rootfs-base.img       /var/lib/cocoon/rootfs-base.img

# The kvm group exists before the first boot
RUN groupadd -f -g 993 kvm

# Modules loaded at boot
RUN printf 'kvm
kvm_intel
vhost_net
tun
' > /etc/modules-load.d/cocoon.conf

# Hardening: no cross-process ptrace, no unprivileged BPF
COPY durcissement/99-cocoon.conf /etc/sysctl.d/

The image weighs around 1.6 GiB. That is the price of autonomy: a machine that boots is immediately able to run MicroVMs, with no download and no configuration.

The site configuration

yaml
#cloud-config

install:
  device: "auto"
  auto: true
  reboot: true
  image: "oci:registry.interne/edge/platform-cocoon:v1.4.0"
  encrypted_partitions: ["COS_PERSISTENT"]   # sealed to the TPM 2.0

kairos:
  network_token: "{{ site_mesh_token }}"

k3s:
  enabled: true
  args:
    - --disable=traefik
    - --disable=servicelb
    - --flannel-backend=wireguard-native
    - --node-label=site={{ code_site }}
    - --node-label=cocoon.io/hypervisor=enabled
    - --secrets-encryption                    # secrets encrypted at rest in etcd

stages:
  boot:
    - name: "Horloge"
      commands: ["chronyc waitsync 30 0.1 || true"]
    - name: "Vérifier KVM"
      commands:
        - |
          if [ ! -c /dev/kvm ]; then
            logger -t cocoon "KVM unavailable — check virtualisation in the BIOS"
            exit 1
          fi
  after-install:
    - name: "Taint the virtual node"
      commands:
        - kubectl taint node cocoon-$(hostname) cocoon.io/microvm=true:NoSchedule --overwrite

Encryption sealed to the TPM deserves a pause. The decryption key is released only if the boot chain matches the expected measured state. A disk pulled out of the machine stays unreadable; a modified system does not boot.

Isolating third-party workloads on the network

Hypervisor isolation offers no protection on the network. One compromised MicroVM that reaches the Kubernetes API or the industrial network cancels the benefit.

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cloisonnement-payloads-third-party
  namespace: payloads-externes
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { role: passerelle-entry }
      ports: [{ protocol: TCP, port: 8080 }]
  egress:
    # Résolution DNS interne seulement
    - to:
        - namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }
          podSelector: { matchLabels: { k8s-app: kube-dns } }
      ports: [{ protocol: UDP, port: 53 }]
    # Result collector, nothing else
    - to:
        - podSelector: { matchLabels: { app: collecteur-results } }
      ports: [{ protocol: TCP, port: 9443 }]
    # Everything else is denied: no Kubernetes API, no industrial network, no internet

The policy gets completed by a Kyverno rule that rejects any pod in that namespace which does not tolerate the Cocoon taint.

yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: payloads-third-party-en-microvm
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: exiger-tolerance-cocoon
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: ["payloads-externes", "payloads-partenaires"]
      validate:
        message: "Toute payload de cet espace doit s'exécuter en MicroVM Cocoon."
        pattern:
          spec:
            tolerations:
              - key: cocoon.io/microvm
                operator: Equal
                value: "true"
                effect: NoSchedule
            =(hostNetwork): "false"
            =(hostPID): "false"
            =(hostIPC): "false"

Without that rule, a manifest with no toleration would land on an ordinary node and run as a classic container — exactly what we set out to prevent. The declarative policy closes the door the scheduler leaves open.

Running the fleet

Every site carries an autonomous K3s cluster. Rancher Fleet reconciles them from the centre, pulling the configuration rather than pushing it.

yaml
apiVersion: fleet.cattle.io/v1alpha1
kind: GitRepo
metadata:
  name: platform-edge
  namespace: fleet-default
spec:
  repo: https://gitlab.interne/edge/platform-edge.git
  branch: main
  paths: ["platform/", "politiques/"]
  targets:
    - name: canari
      clusterSelector:
        matchLabels: { canal-maj: canari }
    - name: stable
      clusterSelector:
        matchLabels: { canal-maj: stable }
  correctDrift:
    enabled: true          # any local change gets reverted

The direction of flow is structural. Sites pull their configuration; the centre opens no inbound connection. A compromised site grants no access to the control plane nor to the other sites.

What the stack costs

ItemOverhead
Memory per third-party workload25 to 40 MiB (guest kernel)
Workload start-up+100 to 250 ms
System image weight1.6 GiB instead of 1.2 GiB
CPU2 to 5 %
Operational complexitytwo layers to master

On a site running forty third-party workloads, the memory overhead reaches about 1.6 GiB. On a 128 GiB server that is marginal. On an 8 GiB box at the constrained edge, it counts.

When not to do it

This architecture is justified when you run third-party code on physically exposed hardware. In every other case, it adds complexity for nothing in return.

If all your workloads are internal, Kairos alone already brings the essentials: immutability, atomic upgrade, the end of configuration drift. The MicroVM layer only earns its place against a real escape threat.

If your machines sit in a guarded data centre, physical theft is not a question, and TPM-sealed encryption loses its main purpose.

The decision criterion. Ask yourself one question: if one of these workloads escapes its container, what does it reach? If the answer is "the host kernel, then the other workloads on the site", the stack is justified. If the answer is "a test environment with no data", it is not.