PI-GROUPE
Edge ComputingSécuritéCybersécuritéKubernetesVirtualisationArchitectureInfrastructureOpen Source

Kairos et vk-cocoon : une flotte de clusters en périphérie, isolée jusqu'au noyau

Kairos tient l'hôte physique en système immuable, vk-cocoon isole chaque charge en MicroVM. Deux couches complémentaires pour exécuter du code non maîtrisé sur des sites que personne ne garde.

Les deux articles précédents traitaient chacun une moitié du problème. Kairos résout la gestion d'un parc physique distribué. vk-cocoon résout l'isolation des charges. Les combiner produit une architecture qui répond à un cas précis et de plus en plus fréquent : exécuter du code dont on ne maîtrise pas l'origine, sur des machines que personne ne surveille physiquement.

C'est la situation d'un opérateur qui héberge les traitements de ses clients en périphérie, d'un industriel qui accueille les modules logiciels de ses fournisseurs sur ses sites, ou d'une administration qui déporte des traitements sur des emprises accessibles.

La répartition des rôles

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é             │  │
│  │  • lance K3s et rejoint le maillage du site           │  │
│  │                                                       │  │
│  │   ┌─────────────────────────────────────────────┐    │  │
│  │   │  K3s — cluster autonome du site             │    │  │
│  │   │                                             │    │  │
│  │   │   Charges internes ──▶ conteneurs standard  │    │  │
│  │   │                                             │    │  │
│  │   │   Charges tierces  ──▶ vk-cocoon            │    │  │
│  │   │                        └─▶ MicroVM          │    │  │
│  │   │                            noyau dédié      │    │  │
│  │   └─────────────────────────────────────────────┘    │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘
                            │ GitOps sortant uniquement
                            ▼
              Plan de contrôle central (Fleet / ArgoCD)

Chaque couche répond à une menace distincte.

MenaceCouche qui répondMécanisme
Altération du systèmeKairosRacine en lecture seule, image signée, démarrage vérifié
Vol physique de la machineKairosChiffrement du disque scellé au TPM
Échappement d'un conteneurvk-cocoonNoyau distinct par charge
Latéralisation entre chargesvk-cocoon + politiques réseauIsolation hyperviseur et segmentation
Compromission du plan de contrôleArchitectureLe site ne fait que sortir, jamais entrer

Préparer l'hôte dans l'image

L'hyperviseur, le noyau invité et le système de fichiers de base se placent dans l'image Kairos. Rien ne s'installe au démarrage.

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

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

# Noyau invité minimal, compilé pour le démarrage rapide
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

# Le groupe kvm existe avant le premier démarrage
RUN groupadd -f -g 993 kvm

# Modules chargés à l'amorçage
RUN printf 'kvm
kvm_intel
vhost_net
tun
' > /etc/modules-load.d/cocoon.conf

# Durcissement : pas de ptrace inter-processus, pas de BPF non privilégié
COPY durcissement/99-cocoon.conf /etc/sysctl.d/

L'image pèse environ 1,6 Gio. C'est le prix de l'autonomie : une machine qui démarre est immédiatement capable d'exécuter des MicroVM, sans téléchargement ni configuration.

La configuration du site

yaml
#cloud-config

install:
  device: "auto"
  auto: true
  reboot: true
  image: "oci:registry.interne/edge/socle-cocoon:v1.4.0"
  encrypted_partitions: ["COS_PERSISTENT"]   # scellé au TPM 2.0

kairos:
  network_token: "{{ jeton_maillage_site }}"

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 chiffrés au repos dans 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 indisponible — vérifier la virtualisation dans le BIOS"
            exit 1
          fi
  after-install:
    - name: "Souiller le nœud virtuel"
      commands:
        - kubectl taint node cocoon-$(hostname) cocoon.io/microvm=true:NoSchedule --overwrite

Le chiffrement scellé au TPM mérite qu'on s'y arrête. La clé de déchiffrement ne se libère que si la chaîne de démarrage correspond à l'état mesuré attendu. Un disque retiré de la machine reste illisible ; un système modifié ne démarre pas.

Isoler le réseau des charges tierces

L'isolation par hyperviseur ne protège pas du réseau. Une MicroVM compromise qui atteint l'API Kubernetes ou le réseau industriel annule le bénéfice.

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: cloisonnement-charges-tierces
  namespace: charges-externes
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { role: passerelle-entree }
      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 }]
    # Collecteur de résultats, rien d'autre
    - to:
        - podSelector: { matchLabels: { app: collecteur-resultats } }
      ports: [{ protocol: TCP, port: 9443 }]
    # Tout le reste est refusé : ni API Kubernetes, ni réseau industriel, ni internet

La politique se complète d'une règle Kyverno qui refuse tout pod de cet espace de noms ne tolérant pas la souillure Cocoon.

yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: charges-tierces-en-microvm
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: exiger-tolerance-cocoon
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: ["charges-externes", "charges-partenaires"]
      validate:
        message: "Toute charge 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"

Sans cette règle, un manifeste sans tolérance tomberait sur un nœud ordinaire et s'exécuterait en conteneur classique — exactement ce qu'on cherchait à empêcher. La politique déclarative ferme la porte que l'ordonnanceur laisse ouverte.

Le pilotage de la flotte

Chaque site porte un cluster K3s autonome. Rancher Fleet les réconcilie depuis le centre, en tirant la configuration plutôt qu'en la poussant.

yaml
apiVersion: fleet.cattle.io/v1alpha1
kind: GitRepo
metadata:
  name: socle-peripherie
  namespace: fleet-default
spec:
  repo: https://gitlab.interne/edge/socle-peripherie.git
  branch: main
  paths: ["socle/", "politiques/"]
  targets:
    - name: canari
      clusterSelector:
        matchLabels: { canal-maj: canari }
    - name: stable
      clusterSelector:
        matchLabels: { canal-maj: stable }
  correctDrift:
    enabled: true          # toute modification locale est annulée

Le sens du flux est structurant. Les sites tirent leur configuration ; le centre n'ouvre aucune connexion entrante. Un site compromis ne donne aucun accès au plan de contrôle ni aux autres sites.

Ce que l'empilement coûte

PosteSurcoût
Mémoire par charge tierce25 à 40 Mio (noyau invité)
Démarrage d'une charge+100 à 250 ms
Poids de l'image système1,6 Gio au lieu de 1,2 Gio
Processeur2 à 5 %
Complexité d'exploitationdeux couches à maîtriser

Sur un site de quarante charges tierces, le surcoût mémoire atteint environ 1,6 Gio. Sur un serveur de 128 Gio, c'est marginal. Sur un boîtier de 8 Gio en périphérie contrainte, cela compte.

Quand ne pas le faire

Cette architecture est justifiée lorsque vous exécutez du code tiers sur du matériel physiquement exposé. Dans tous les autres cas, elle ajoute de la complexité sans contrepartie.

Si vos charges sont toutes internes, Kairos seul apporte déjà l'essentiel : immuabilité, montée de version atomique, fin de la dérive de configuration. La couche MicroVM ne se justifie que face à une menace d'échappement réelle.

Si vos machines se trouvent dans un centre de données gardé, la question du vol physique ne se pose pas, et le chiffrement scellé au TPM perd son intérêt principal.

Le critère de décision. Posez-vous une seule question : si l'une de ces charges s'échappe de son conteneur, qu'atteint-elle ? Si la réponse est « le noyau de l'hôte, puis les autres charges du site », l'empilement se justifie. Si la réponse est « un environnement de test sans données », il ne se justifie pas.