422 lines
16 KiB
Markdown
422 lines
16 KiB
Markdown
# 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:
|
|
|
|
1. **No pending update**: `os_version` and `os_version_next` are identical.
|
|
This means no update has been downloaded or is waiting for reboot.
|
|
|
|
2. **Pending update**: `os_version_next` would differ from `os_version`,
|
|
indicating a new build has been downloaded to the standby partition.
|
|
|
|
3. **Auto-reboot disabled**: `auto_reboot: false` means 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).
|
|
|
|
4. **No reboot needed**: `needs_reboot: false` confirms no downloaded
|
|
update is waiting for a reboot to activate.
|
|
|
|
5. **Independent node updates**: node-01 is on `202602240349` while
|
|
nodes 02/03 are on `202602230420`. This proves each node checks for
|
|
and applies updates independently. There is no cluster-wide coordinated
|
|
update mechanism at the OS level.
|
|
|
|
6. **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**:
|
|
1. Record current versions: `incusos-health --update`
|
|
2. Trigger update via Operations Center UI, or wait for 6h check interval
|
|
3. Monitor via Grafana for download activity and node state changes
|
|
4. When `needs_reboot: true`, reboot one node at a time
|
|
5. After reboot, verify new version: `incusos-health --status`
|
|
6. 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**:
|
|
1. Take Proxmox snapshot of node-03: `incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/snapshot -d '{"snapname":"pre-break-fix-2"}'`
|
|
2. Trigger an update on node-03 (if one is available)
|
|
3. 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`
|
|
4. Wait 10 seconds, then start node-03:
|
|
`incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/start`
|
|
5. Monitor boot via console screenshots: `incusos/helpers/proxmox-screenshot 902`
|
|
6. After boot, check version: `incusos-health --status` on node-03
|
|
|
|
**Expected behavior**:
|
|
- Node boots from the previous (known-good) partition
|
|
- Failed partition is marked as bad / not bootable
|
|
- `os_version` returns 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**:
|
|
1. Evacuate workloads from node-03:
|
|
`incus cluster evacuate oc-node-03 --target oc-node-01`
|
|
2. Disconnect node-03 NIC via Proxmox API:
|
|
`incusos/helpers/proxmox-api PUT /nodes/pve/qemu/902/config -d '{"net0":"virtio=...,link_down=1"}'`
|
|
3. Observe in Grafana: OVN Geneve tunnel loss, cluster detecting missing node
|
|
4. Wait 2-5 minutes for cluster to mark node-03 as offline
|
|
5. Reconnect NIC: set `link_down=0` via Proxmox API
|
|
6. Monitor recovery: node-03 should rejoin cluster, OVN tunnels re-establish
|
|
7. 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**:
|
|
1. Evacuate workloads: `incus cluster evacuate oc-node-03`
|
|
2. Take Proxmox snapshot of node-03
|
|
3. Hard-stop node-03 via Proxmox:
|
|
`incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/stop`
|
|
4. Verify cluster continues operating:
|
|
- `incus cluster list` should show 2 online, 1 offline
|
|
- Existing workloads on node-01/02 remain accessible
|
|
- OVN gateway should failover if node-03 was a gateway
|
|
5. Wait 5 minutes, observing Grafana metrics
|
|
6. Restart node-03:
|
|
`incusos/helpers/proxmox-api POST /nodes/pve/qemu/902/status/start`
|
|
7. Monitor boot: `incusos/helpers/proxmox-screenshot 902`
|
|
8. Measure time from start to cluster rejoin (node shows online)
|
|
9. 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:
|
|
|
|
1. **Never hard-stop during first boot.** First boot seals TPM measurements
|
|
and writes encryption keys. Interrupting this corrupts TPM state
|
|
permanently, requiring full reinstallation.
|
|
|
|
2. **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.
|
|
|
|
3. **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.
|
|
|
|
4. **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.
|
|
|
|
5. **Monitor via Grafana during all exercises.** Visual monitoring catches
|
|
issues that API polling might miss (e.g., OVN tunnel flapping,
|
|
storage I/O spikes).
|
|
|
|
6. **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.
|
|
|
|
7. **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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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.
|