Problema

En muchos homelabs y pequeños clusters el hipervisor se trata como una máquina más del inventario: se actualiza el paquete del kernel, se instala un nuevo módulo y se confía en que todo seguirá funcionando. Esa confianza genera dos patrones de fallo recurrentes:

  1. Drift de configuración y versión – Cada nodo puede terminar con versiones diferentes del kernel o de paquetes críticos, lo que produce comportamientos sutiles y difíciles de reproducir.
  2. Actualizaciones que dejan el host inoperativo – Un kernel que rompe el módulo ZFS, una dependencia que desaparece o una configuración de boot que falla pueden dejar el nodo sin acceso, sin una forma sencilla de volver al estado anterior.

El resultado es tiempo de inactividad inesperado, necesidad de restaurar desde backups y una carga operativa que crece con cada nodo añadido.

Causa

Los orígenes de este problema son estructurales:

  • Base mutable: la mayoría de los hipervisores (por ejemplo, Proxmox basado en Debian) utilizan un sistema de archivos raíz que se escribe en tiempo de ejecución. Cada apt upgrade o pveupgrade modifica el árbol del sistema, creando puntos de divergencia.
  • Seguridad opcional: Secure Boot, LUKS y TPM suelen estar disponibles pero no son obligatorios. En entornos domésticos rara vez se habilitan, lo que deja el arranque sin mediciones de integridad.
  • Ausencia de rollback automático: los gestores de paquetes no guardan una instantánea del kernel ni del bootloader. Si la actualización falla, la única salida es revertir manualmente o reinstalar.
  • Configuración dispersa: los archivos de red, firewall y storage se editan directamente en el host. Un reinicio o una actualización pueden sobrescribir cambios no versionados.

Estos factores se combinan para que el “estado deseado” del host sea difícil de garantizar entre reinicios y actualizaciones.

Solución

Adoptar un modelo immutable con particiones A/B elimina la mayoría de los síntomas descritos. El concepto es sencillo: el sistema operativo reside en dos particiones (slot A y slot B). Cada actualización escribe en la partición inactiva, se marca como activa en el bootloader y, si el arranque falla, el gestor vuelve automáticamente a la partición anterior.

Pasos clave

  1. Crear particiones A/B

    • Reserva dos particiones del mismo tamaño (por ejemplo, /dev/sda1 y /dev/sda2).
    • Usa LVM o ZFS para exponer cada una como raíz (/) en modo read‑only.
  2. Configurar bootloader para selección automática

    • Con GRUB, habilita GRUB_DEFAULT=saved y GRUB_SAVEDEFAULT=true.
    • Añade una entrada para cada slot (Ubuntu‑A, Ubuntu‑B o la distro que corresponda).
  3. Habilitar Secure Boot y TPM‑bound LUKS

    • Genera una clave de cifrado protegida por TPM2 (tpm2_createprimary).
    • Configura LUKS con la opción --tpm2-device=auto.
    • Firma los binarios del kernel y del bootloader con una clave propia y registra la firma en la base de datos de Secure Boot.
  4. Implementar el proceso de actualización

    • Descarga la nueva imagen del hipervisor (por ejemplo, un tarball de IncusOS o una ISO de Proxmox).
    • Descomprime en la partición inactiva.
    • Ejecuta grub-set-default para apuntar al nuevo slot y reinicia.
  5. Rollback automático

    • Si el arranque detecta fallos críticos (kernel panic, falta de rootfs), un script de systemd marca el slot como “bad” y ejecuta grub-reboot al slot anterior.
    • No se necesita intervención humana; el nodo vuelve a un estado funcional en segundos.
  6. Mantener la raíz inmutable en tiempo de ejecución

    • Monta / como ro y usa overlayfs o /etc en una partición separada para los cambios de configuración que deben ser persistentes.
    • Los contenedores o VMs se almacenan en ZFS datasets o LVM volumes externos, aislados del host.

Herramientas útiles

  • systemd-boot o efibootmgr para manipular variables UEFI.
  • tpm2-tools para gestionar claves TPM.
  • zfs rollback o zfs snapshot para snapshots de datos de VM sin tocar el host.
  • incus CLI (si se usa IncusOS) para gestionar contenedores y VMs con una única API.

Cuándo aplicar esta solución

  • Clusters con más de un nodo donde la consistencia de versión sea crítica (por ejemplo, Ceph, GlusterFS, o replicación de VMs).
  • Entornos que requieren alta disponibilidad y no pueden permitirse tiempo de inactividad por una actualización fallida.
  • Homelabs con hardware limitado que no pueden mantener backups completos de cada nodo antes de cada upgrade.
  • Escenarios donde la seguridad del arranque es obligatoria (TPM, Secure Boot) y se desea que la política sea inmutable.

No es la solución ideal cuando:

  • Se necesita personalizar profundamente el kernel o el initramfs en cada nodo; el modelo immutable dificulta modificaciones ad‑hoc.
  • El hipervisor no ofrece una imagen pre‑construida para A/B (en ese caso se requiere crear la imagen manualmente, lo que puede ser costoso).

Código

# 1. Crear dos particiones (ejemplo con sgdisk)
sgdisk -n 1:0:+20G -t 1:8300 /dev/sda   # slot A
sgdisk -n 2:0:+20G -t 2:8300 /dev/sda   # slot B

# 2. Formatear como ext4 (puede ser btrfs, zfs, etc.)
mkfs.ext4 /dev/sda1
mkfs.ext4 /dev/sda2

# 3. Montar la partición inactiva y extraer la nueva imagen
mount /dev/sda2 /mnt
tar -xpf incusos-2024.09.tar -C /mnt

# 4. Configurar GRUB para el nuevo slot
grub-mkconfig -o /boot/grub/grub.cfg
grub-set-default "IncusOS-B"
grub-reboot "IncusOS-B"

# 5. Habilitar LUKS con TPM2 (solo una vez)
cryptsetup luksFormat --type luks2 --tpm2-device=auto /dev/sda1
cryptsetup open --type luks2 --tpm2-device=auto /dev/sda1 cryptroot

# 6. Firmar kernel y bootloader (ejemplo con sbsigntool)
sbsign --key db.key --cert db.crt --output /boot/vmlinuz-signed /boot/vmlinuz
sbsign --key db.key --cert db.crt --output /boot/efi/EFI/ubuntu/grubx64.efi /boot/efi/EFI/ubuntu/grubx64.efi

Verificación

  1. Bootloader – Ejecuta grub-editenv list y confirma que saved_entry apunta al slot activo.
  2. Integridad TPMtpm2_pcrread debe mostrar valores diferentes entre slots; cualquier cambio inesperado indica manipulación.
  3. Rollback – Simula un fallo arrancando con systemd.unit=emergency.target. El script de fallback debe cambiar automáticamente a la partición anterior; verifica con journalctl -b -1.
  4. Inmutabilidad – Intenta crear un archivo en /. Debería fallar con “Read‑only file system”.

Notas adicionales

  • Snapshots de datos: aunque el host sea immutable, los discos de VMs siguen necesitando backups. Usa ZFS snapshots y replicación incremental para minimizar la ventana de pérdida.
  • Actualizaciones de firmware: el modelo A/B no cubre firmware de la placa base. Mantén el BIOS/UEFI actualizado y habilita la verificación de firmas si el fabricante lo permite.
  • Compatibilidad con Proxmox: si prefieres seguir con Proxmox, puedes emular el flujo A/B creando dos plantillas de VM con el mismo hardware y usando pveam para actualizar la plantilla inactiva, luego cambiar el bootorder. No es tan transparente como una solución nativa, pero reduce el riesgo de drift.
  • Monitorización: registra los hashes de los kernels y de los initramfs en un repositorio Git interno; cualquier desviación se detecta rápidamente con un cron job que compare sha256sum.

Adoptar un modelo immutable con actualizaciones atómicas y rollback automático cambia la mentalidad de “parchear el host en producción” a “desplegar una nueva imagen y confiar en que el proceso de arranque la valida”. En entornos donde la disponibilidad y la seguridad son prioritarias, esa diferencia suele ser la que separa un homelab que funciona sin sobresaltos de uno que pasa noches enteras depurando versiones de kernel incompatibles.