16 KiB
IncusOS Break-Fix Lab -- Immutability Exploration & Resilience Testing
IncusOS is an immutable, purpose-built operating system for running Incus
clusters. This guide documents what we discovered about its internals through
the /os/1.0 API, and defines a catalog of break-fix exercises for testing
cluster resilience in a safe lab environment.
All observations come from a 3-node Proxmox-hosted cluster (VMID 900-902) running IncusOS with virtual TPM, Secure Boot, and OVN networking.
IncusOS Architecture
IncusOS is an immutable OS designed for a single purpose: running Incus.
Key architectural properties discovered via the /os/1.0 API:
- Immutable root filesystem with A/B partition scheme. The running system boots from one partition; updates install to the other. Rollback is automatic if the new partition fails validation.
- TPM-based full disk encryption -- root and swap volumes are encrypted and unlocked automatically via TPM measured boot. No passphrase required during normal operation.
- Secure Boot enforced with 4 certificates in the UEFI firmware:
- PK (Platform Key) -- root of trust
- KEK (Key Exchange Key) -- authorizes db updates
- db (2025 signing cert) -- validates current signed binaries
- db (2026 signing cert) -- validates next-year signed binaries
- Version format:
YYYYMMDDHHMI(build timestamp, not semver). Example:202602240349= 2026-02-24 at 03:49 UTC. - ZFS storage: pool "local" on a dedicated partition (raid0 on single disk, ~30 GiB usable), encrypted with its own pool recovery key.
- Update system: stable channel, 6-hour check frequency,
auto_reboot: false(updates download but do not reboot automatically).
Lab cluster state
| Node | VMID | IP | IncusOS Version |
|---|---|---|---|
| node-01 | 900 | 192.168.102.140 | 202602240349 |
| node-02 | 901 | 192.168.102.141 | 202602230420 |
| node-03 | 902 | 192.168.102.142 | 202602230420 |
node-01 received a newer build than nodes 02/03, proving that nodes update independently (rolling updates, not cluster-wide atomic upgrades).
Partition & Disk Layout
Each node has a single 64 GiB SCSI disk (QEMU HARDDISK) with this layout:
+------------------------------------------------------+
| SCSI Disk (64 GiB, QEMU HARDDISK) |
|------------------------------------------------------|
| Partition 1 EFI System Partition |
| Partition A Root filesystem (active or standby) |
| Partition B Root filesystem (standby or active) |
| Partition X Swap (encrypted, TPM-unlocked) |
| Partition 11 ZFS data pool ("local") |
+------------------------------------------------------+
Key observations from the storage API:
- Partition 11 is the ZFS data partition, hosting pool "local".
- Pool "local": raid0, single vdev, ~30 GiB total, contains volume "incus" (~10 GiB used for Incus database, images, instances).
- Root + swap: both encrypted, both unlocked by TPM at boot. No manual key entry needed unless TPM state is corrupted.
Security Chain
The boot security chain, as validated through the API:
UEFI firmware
--> Secure Boot certificate validation (PK -> KEK -> db certs)
--> Signed kernel + initrd loaded
--> TPM measured boot (PCR measurements recorded)
--> TPM validates measurements match expected policy
--> Root partition decrypted and mounted
--> Swap partition decrypted and activated
--> System boots into trusted state
API-reported security state:
| Property | Value |
|---|---|
| TPM status | ok |
| Secure Boot | enabled, enforced |
| System state | trusted |
| Encryption recovery keys | 1 key (retrievable via API) |
| Pool recovery keys | 1 key for pool "local" |
Recovery keys are a safety net. If TPM state is corrupted (e.g., by a hard-stop during first boot), the encryption recovery key allows manual unlock. The pool recovery key allows ZFS pool import on a different system.
Services Configuration
Discovered via /os/1.0/services/*:
| Service | Status | Notes |
|---|---|---|
| OVN | enabled | Connects to cluster DB at tcp:192.168.102.142:6642, Geneve tunnels |
| iSCSI | disabled | |
| LVM | disabled | |
| Multipath | disabled | |
| NVMe | disabled | |
| Tailscale | disabled | |
| USB/IP | disabled |
OVN is the only enabled service beyond Incus itself, providing the overlay network for cross-node container connectivity and network policies.
Network Configuration
Each node has a single management interface:
| Property | Value |
|---|---|
| Interface name | mgmt |
| Role | management + cluster traffic |
| Addressing | Static |
| Subnet | /22 (192.168.100.0/22) |
| DNS | 192.168.100.1 |
| Gateway | 192.168.100.1 |
Node IP assignments:
- node-01:
192.168.102.140/22 - node-02:
192.168.102.141/22 - node-03:
192.168.102.142/22
The management interface carries both management traffic (API, cluster heartbeats) and OVN Geneve tunnel traffic. In production, these should be separated.
Update Mechanics (Observed)
The IncusOS update system uses A/B partitions for safe, rollback-capable updates. Observations from querying the update API:
-
No pending update:
os_versionandos_version_nextare identical. This means no update has been downloaded or is waiting for reboot. -
Pending update:
os_version_nextwould differ fromos_version, indicating a new build has been downloaded to the standby partition. -
Auto-reboot disabled:
auto_reboot: falsemeans updates download to the standby partition but the node continues running the current version until explicitly rebooted (or until an admin triggers reboot via API / UI). -
No reboot needed:
needs_reboot: falseconfirms no downloaded update is waiting for a reboot to activate. -
Independent node updates: node-01 is on
202602240349while nodes 02/03 are on202602230420. This proves each node checks for and applies updates independently. There is no cluster-wide coordinated update mechanism at the OS level. -
Check frequency: 6 hours. The stable channel is checked automatically on this interval.
Update lifecycle (theoretical)
Check timer fires (every 6h)
--> Query stable channel for new version
--> Download new rootfs to standby partition (A or B)
--> os_version_next updated, needs_reboot = true
--> (if auto_reboot) Reboot automatically
--> (if !auto_reboot) Wait for manual reboot
--> On reboot: boot from new partition
--> TPM re-measures, validates new boot chain
--> If valid: new partition becomes active
--> If invalid: rollback to previous partition
Break-Fix Exercise Catalog
These exercises are designed to test IncusOS cluster resilience in the Proxmox lab. All exercises follow strict safety rules (see Safety Rules section below).
Current status: All exercises are defined but not yet executed.
Exercise 1: Normal Update Observation
Goal: Observe the full update lifecycle -- download, reboot, A/B partition switch, version verification.
Prerequisites:
- Proxmox snapshot of all cluster nodes (VMID 900-902)
- Grafana monitoring active (observe cluster metrics during update)
- Verify cluster health with
incusos-health --all
Steps:
- Record current versions:
incusos-health --update - Trigger update via Operations Center UI, or wait for 6h check interval
- Monitor via Grafana for download activity and node state changes
- When
needs_reboot: true, reboot one node at a time - After reboot, verify new version:
incusos-health --status - Confirm A/B partition switch:
incusos-health --partitions
What to observe:
- Download phase duration
- Reboot duration (typically 30-60s for IncusOS)
- Cluster behavior while one node reboots (2/3 quorum maintained)
- New version number in
os_version - Previous version still available on standby partition
Status: Defined, not yet executed.
Exercise 2: Simulated Failed Update (Hard-Stop Mid-Update)
Goal: Verify that IncusOS rolls back to the previous partition after a failed update (simulated by hard-stopping the VM during update).
Prerequisites:
- Proxmox snapshot of node-03 (VMID 902) -- non-leader, fewest workloads
- Verify node-03 has no critical workloads
- Confirm 3/3 nodes healthy before starting
Target node: node-03 (VMID 902) ONLY. Never the cluster leader.
Steps:
- Take Proxmox snapshot of node-03:
incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/snapshot -d '{"snapname":"pre-break-fix-2"}' - Trigger an update on node-03 (if one is available)
- During the update download or early reboot phase, hard-stop node-03
via Proxmox API:
incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/stop - Wait 10 seconds, then start node-03:
incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/start - Monitor boot via console screenshots:
incusos/helpers/proxmox-screenshot 902 - After boot, check version:
incusos-health --statuson node-03
Expected behavior:
- Node boots from the previous (known-good) partition
- Failed partition is marked as bad / not bootable
os_versionreturns to the pre-update version- Node rejoins the cluster automatically
Safety:
- ONLY node-03 -- maintains 2/3 quorum (node-01 + node-02 continue)
- Do NOT hard-stop during first boot (corrupts TPM permanently)
- If recovery fails, restore from Proxmox snapshot
Status: Defined, not yet executed.
Exercise 3: Network Isolation
Goal: Observe how the cluster handles a node losing network connectivity -- OVN tunnel loss, cluster membership changes, and automatic recovery on reconnection.
Prerequisites:
- Evacuate all workloads from node-03 before disconnecting
- Grafana monitoring active (watch OVN tunnel metrics, cluster events)
- Verify cluster health:
incusos-health --all
Steps:
- Evacuate workloads from node-03:
incus cluster evacuate oc-node-03 --target oc-node-01 - Disconnect node-03 NIC via Proxmox API:
incusos/helpers/proxmox-api PUT /nodes/pve/qemu/902/config -d '{"net0":"virtio=...,link_down=1"}' - Observe in Grafana: OVN Geneve tunnel loss, cluster detecting missing node
- Wait 2-5 minutes for cluster to mark node-03 as offline
- Reconnect NIC: set
link_down=0via Proxmox API - Monitor recovery: node-03 should rejoin cluster, OVN tunnels re-establish
- Measure total recovery time from reconnection to healthy state
Expected behavior:
- Cluster detects node-03 offline within heartbeat timeout
- OVN tunnels from/to node-03 fail (Geneve encap packets lost)
- Cluster continues operating with 2/3 quorum
- On reconnection: node-03 rejoins, tunnels re-establish, workloads can be restored
Recovery:
- If node-03 does not rejoin: check OVN service status, restart if needed
- If cluster state is inconsistent: restore from Proxmox snapshot
Status: Defined, not yet executed.
Exercise 4: Full Node Failure
Goal: Verify that the cluster survives a complete node loss and maintains operations with 2/3 quorum. Measure cluster rejoin time after node recovery.
Prerequisites:
- Proxmox snapshot of node-03 (VMID 902)
- Evacuate ALL workloads from node-03
- Verify cluster health:
incusos-health --all
Steps:
- Evacuate workloads:
incus cluster evacuate oc-node-03 - Take Proxmox snapshot of node-03
- Hard-stop node-03 via Proxmox:
incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/stop - Verify cluster continues operating:
incus cluster listshould show 2 online, 1 offline- Existing workloads on node-01/02 remain accessible
- OVN gateway should failover if node-03 was a gateway
- Wait 5 minutes, observing Grafana metrics
- Restart node-03:
incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/start - Monitor boot:
incusos/helpers/proxmox-screenshot 902 - Measure time from start to cluster rejoin (node shows online)
- Restore workloads:
incus cluster restore oc-node-03
Expected behavior:
- 2/3 quorum maintained -- all cluster operations continue
- OVN gateway failover occurs if node-03 was elected gateway
- After restart: node-03 boots, TPM unlocks, Incus starts, rejoins cluster
- Typical rejoin time: 1-3 minutes after boot
Safety:
- This is NOT a first-boot scenario -- hard-stop is safe for already- provisioned nodes (TPM state is already sealed)
- One node at a time ONLY
- If node-03 fails to rejoin after restart, restore snapshot
Status: Defined, not yet executed.
Safety Rules
These rules are non-negotiable for all break-fix exercises:
-
Never hard-stop during first boot. First boot seals TPM measurements and writes encryption keys. Interrupting this corrupts TPM state permanently, requiring full reinstallation.
-
One node at a time. A 3-node cluster requires 2/3 quorum. Never take down more than one node simultaneously, or the cluster loses quorum and all operations halt.
-
Proxmox snapshots before every destructive test. Snapshot the target node's VM (VMID 900-939) before any exercise that involves stopping, disconnecting, or modifying the node.
-
Verify cluster health before and after. Run
incusos-health --all(or equivalent API checks) before starting an exercise and after completing it. Do not proceed if the cluster is already degraded. -
Monitor via Grafana during all exercises. Visual monitoring catches issues that API polling might miss (e.g., OVN tunnel flapping, storage I/O spikes).
-
Target node-03 for destructive tests. node-03 (VMID 902) is the preferred target because it is not the cluster leader and typically has the fewest workloads. Evacuate before testing.
-
Keep recovery keys accessible. The encryption recovery key and ZFS pool recovery key should be retrieved and stored securely before any exercise that might corrupt TPM state.
Helper Script: incusos-health
The incusos/helpers/incusos-health script queries the IncusOS API on
cluster nodes to report system state. It is the primary tool for verifying
cluster health before and after break-fix exercises.
Usage
incusos/helpers/incusos-health [ACTION] [OPTIONS]
Actions
| Action | Description |
|---|---|
--status |
Basic system info: version, hostname, TPM status, Secure Boot |
--partitions |
Disk and partition layout, A/B partition state |
--tpm |
TPM details, Secure Boot certificates, encryption keys |
--services |
Enabled/disabled services (OVN, iSCSI, LVM, etc.) |
--network |
Network interface configuration, DNS, gateway |
--update |
Update channel, check frequency, pending update status |
--all |
Run all of the above in sequence |
Example output workflow
# Before exercise: verify all nodes healthy
incusos/helpers/incusos-health --all
# After exercise: verify recovery
incusos/helpers/incusos-health --status # Quick version check
incusos/helpers/incusos-health --all # Full health report
API Reference
All IncusOS system information is available via the REST API on each node.
| Endpoint | Returns |
|---|---|
/os/1.0 |
Version, hostname, basic system info |
/os/1.0/system/security |
TPM status, Secure Boot, encryption keys |
/os/1.0/system/storage |
Disks, partitions, ZFS pools |
/os/1.0/system/resources |
CPU, memory, hardware info |
/os/1.0/system/network |
Interfaces, DNS, routes |
/os/1.0/system/update |
Update channel, version, pending updates |
/os/1.0/services/ovn |
OVN configuration and status |
/os/1.0/services/iscsi |
iSCSI configuration |
/os/1.0/services/lvm |
LVM configuration |
/os/1.0/services/multipath |
Multipath configuration |
/os/1.0/services/nvme |
NVMe-oF configuration |
/os/1.0/services/tailscale |
Tailscale VPN configuration |
/os/1.0/services/usbip |
USB/IP configuration |
The API listens on the management interface, HTTPS, port 8443 (same as Incus). Authentication uses the Incus client certificate.