System Configuration
Rugix Ctrl needs an accurate picture of how a device is laid out before it can update it. That picture is the device’s system configuration, and it lives in a single file, /etc/rugix/system.toml.1
Key Concepts
An update has to go somewhere specific on the device, and the device has to be able to boot the result. system.toml describes the layout that makes both possible.
A slot is a named update target. When Rugix Ctrl installs a bundle, each of its payloads is written to the slot it is addressed to. A slot is usually a partition holding one part of the system, such as the kernel or the root filesystem, but it can also be a file, or a custom handler that installs the update data.
A complete operating system normally spans several slots, for instance a kernel slot and a root filesystem slot. The slots that form one such system are gathered into a boot group. The familiar A/B layout is simply a device with two boot groups: an update is written into the slots of one group while the device keeps running the other, and a reboot switches between them (see System Updates).
Carrying out that switch is the bootloader’s job, and bootloaders differ from device to device. A boot flow is the integration that lets Rugix Ctrl drive one particular bootloader, so that it can select which boot group to start and switch between them.
Config and Data Partitions
In addition to slots, Rugix Ctrl recognizes two optional partitions with a fixed role. The config partition holds data shared across all versions of the system, most importantly the bootloader’s configuration, and is left untouched by updates. The data partition holds persistent state, such as user and application data, that has to survive updates; it is the foundation for state management.
By default, Rugix Ctrl locates both partitions on its own: it takes the first partition of the device’s root device2 as the config partition, and the sixth (on a GPT partition table) or seventh (on an MBR one) as the data partition. A device that follows this conventional layout needs no config-partition or data-partition section at all.
When a device’s layout differs from this, declare the partitions explicitly in the config-partition and data-partition sections of system.toml. Each is identified either by device, the path to a block device, or by partition, a partition number on the root device:
#:schema https://raw.githubusercontent.com/rugix/rugix/refs/heads/main/schemas/rugix-ctrl-system.schema.json
[config-partition]
# The config partition is the block device `/dev/sda1`.
device = "/dev/sda1"
[data-partition]
# The data partition is the seventh partition of the root device.
partition = 7
A partition can be marked absent with disabled = true. The config partition is required for most boot flows and for bootstrapping; the data partition is required for state management.
When state management is enabled, Rugix Ctrl mounts the partitions for you:
/run/rugix/mounts/config: the config partition, usually read-only./run/rugix/mounts/data: the data partition, read-write./run/rugix/mounts/system: the running system partition, read-only.
Without state management, Rugix Ctrl does not mount them; mount them yourself, and use the path setting to tell Rugix Ctrl where each one is.
Beyond a plain partition, the data partition can be placed under a driver that manages its filesystem, including at-rest encryption. See Data Partition Driver.
Update Slots
A slot is declared under slots.<name>, where <name> is a name you choose. Every slot has a type:
block: a block device, typically a partition. This is the usual slot for system updates.file: a single regular file.custom: a slot whose update logic you provide yourself.
Block Slots
A block slot is a block device, specified by device (a path such as /dev/sda2) or partition (a partition number on the root device). A typical slot definition for an A/B device with separate boot partitions may look as follows:
#:schema https://raw.githubusercontent.com/rugix/rugix/refs/heads/main/schemas/rugix-ctrl-system.schema.json
[slots.boot-a]
type = "block"
device = "/dev/sda2"
immutable = true
[slots.boot-b]
type = "block"
device = "/dev/sda3"
immutable = true
[slots.system-a]
type = "block"
partition = 4
immutable = true
[slots.system-b]
type = "block"
partition = 5
immutable = true
immutable = true declares that the slot’s contents only ever change through a Rugix Ctrl update.
File Slots
A file slot is a single regular file, given by an absolute path:
[slots.my-file]
type = "file"
path = "/run/rugix/mounts/data/my-file"
A file slot is only as durable as the file behind it: if the path is not preserved across reboots, the slot’s contents are lost when the device restarts. File slots therefore usually point at a file on the data partition or in the state directory, or are paired with a state management configuration that persists the file. Like block slots, file slots accept immutable.
A file slot is written in place: Rugix Ctrl truncates the existing file and overwrites it as the update streams in, rather than replacing it atomically. An interrupted install therefore leaves the file partially written. If atomic replacement matters, use a custom slot whose handler writes to a temporary file and renames it into place.
To update a whole directory rather than a single file, use a custom slot with a tar archive as its payload and extract it.3
Custom Slots
A custom slot hands the update payload to a command of your choosing. Rugix Ctrl streams the payload to the command on standard input, and the command, given by handler, decides what to do with it. For example, to extract a tar archive into a directory:
[slots.app-data]
type = "custom"
handler = ["tar", "xf", "-", "-C", "/run/rugix/mounts/data/app/my-app-data"]
Because Rugix Ctrl does not control how a custom handler writes the payload, custom slots cannot use block deduplication (see Update Bundles).
Boot Groups
A boot group gathers slots into one bootable system. It is declared under boot-groups.<name>, and its slots setting maps aliases to slot names:
[boot-groups.a]
slots = { boot = "boot-a", system = "system-a" }
[boot-groups.b]
slots = { boot = "boot-b", system = "system-b" }
An update is always installed into one boot group. Each of its payloads names a target slot, either directly or through an alias such as boot or system that Rugix Ctrl resolves against the chosen boot group. Because both groups above expose the same aliases, the same bundle installs correctly into either one.
Choose the target boot group with the --boot-group option of rugix-ctrl update install. If it is omitted and the device has exactly two boot groups, Rugix Ctrl installs to the inactive one automatically. The slots of the currently booted boot group are active, and Rugix Ctrl never installs over an active slot.
Boot Flow
A boot flow is the integration between Rugix Ctrl and a particular bootloader. It is configured in the boot-flow section, whose type selects the integration:
[boot-flow]
type = "grub"
Rugix Ctrl ships boot flows for the common bootloaders:
uboot: U-Boot (A/B setups only).grub: GRUB, using grubenv files on the config partition (A/B setups only).systemd-boot: systemd-boot, using EFI variables (A/B setups only).
For drop-in compatibility with other OTA solutions, it also ships RAUC- and Mender-compatible flows (rauc-uboot, rauc-grub, mender-uboot, mender-grub), and for Raspberry Pi devices, flows built on U-Boot or the tryboot mechanism (rpi-uboot, rpi-tryboot). When none of these fit, a custom boot flow integrates with any bootloader through a script of your own.
The Boot Flows section documents each boot flow in detail, including the settings that some of them take.
Configuration Reference
For the complete set of options, here is the full schema for system.toml:
SystemConfig
System configuration.
Fields (JSON object)
config-partition(fromconfig_partition) optionalConfig partition configuration.
PartitionConfig
Fields (JSON object)
disabledoptionalExplicitly disable the partition.
boolean
deviceoptionalPath to the partition block device.
string
partitionoptionalPartition number of the root device.
integer
mount-script(frommount_script) optionalUser-defined script for mounting the partition.
Mutually exclusive with
driver.string
pathoptionalPath where the partition is or should be mounted.
string
protectedoptionalIndicates whether the partition is write-protected.
boolean
driveroptional- Unstable
Driver responsible for formatting, mounting, and wiping the partition.
Only valid on the data partition. Mutually exclusive with
mount_script. When unset, the partition is treated as a plain Ext4 filesystem.DataPartitionDriverConfig
Cases internally — tag field
typePlaintextExt4→"plaintext-ext4"Plain Ext4 filesystem on the bare partition device.
{ "type": "plaintext-ext4", ...<payload fields> } — or "content" for non-object payloadsPlaintextExt4DriverConfig
Fields (JSON object)
labeloptionalFilesystem label assigned at format time. Defaults to
data.string
additional-options(fromadditional_options) optionalAdditional options forwarded verbatim to
mkfs.ext4.array<string>
Items
string
Luks2Passphrase→"luks2-passphrase"LUKS2 with a passphrase loaded from a file at unlock time.
Not a stand-alone production encryption mode: a passphrase living on plaintext flash defeats the encryption. Useful for testing and for out-of-band-delivered keys never persisted locally.
{ "type": "luks2-passphrase", ...<payload fields> } — or "content" for non-object payloadsLuks2PassphraseDriverConfig
Fields (JSON object)
passphrase-file(frompassphrase_file) requiredPath to the file holding the passphrase. Read raw, used as-is.
string
labeloptionalFilesystem label assigned at mkfs time. Defaults to
data.string
mapper-name(frommapper_name) optionalName of the dm-crypt mapper device. Defaults to
rugix-data.string
additional-mkfs-options(fromadditional_mkfs_options) optionalAdditional options forwarded verbatim to
mkfs.ext4.array<string>
Items
string
Luks2Tpm2→"luks2-tpm2"LUKS2 with a key sealed by a TPM 2.0 device.
systemd-cryptenrollseals a random LUKS2 passphrase to the TPM at format time;cryptsetup open --token-onlyunseals it on every boot. Production-grade: the unseal material never leaves the TPM.{ "type": "luks2-tpm2", ...<payload fields> } — or "content" for non-object payloadsLuks2Tpm2DriverConfig
Fields (JSON object)
pcrsoptionalPCRs to bind the unseal policy to. Empty list means no PCR binding.
Update warning: PCRs in the 0-9 range typically change with firmware/bootloader/kernel updates and will brick the device on every update if bound literally. PCR 7 (Secure Boot policy state) is the most stable choice for static binding. For stronger binding, future revisions will add
tpm2-public-keyforTPM2_PolicyAuthorizeso PCR predictions can be re-signed per release.array<integer>
Items
integer
deviceoptionalTPM 2.0 device path. Defaults to
auto.string
labeloptionalFilesystem label assigned at mkfs time. Defaults to
data.string
mapper-name(frommapper_name) optionalName of the dm-crypt mapper device. Defaults to
rugix-data.string
additional-mkfs-options(fromadditional_mkfs_options) optionalAdditional options forwarded verbatim to
mkfs.ext4.array<string>
Items
string
Custom→"custom"User-defined driver implemented by external scripts.
{ "type": "custom", ...<payload fields> } — or "content" for non-object payloadsCustomDataPartitionDriverConfig
Fields (JSON object)
format-script(fromformat_script) optionalBootstrap-time format script.
string
mount-script(frommount_script) requiredPer-boot mount script.
string
wipe-script(fromwipe_script) optionalrugix-ctrl data wipescript.string
data-partition(fromdata_partition) optionalData partition configuration.
PartitionConfig
Fields (JSON object)
disabledoptionalExplicitly disable the partition.
boolean
deviceoptionalPath to the partition block device.
string
partitionoptionalPartition number of the root device.
integer
mount-script(frommount_script) optionalUser-defined script for mounting the partition.
Mutually exclusive with
driver.string
pathoptionalPath where the partition is or should be mounted.
string
protectedoptionalIndicates whether the partition is write-protected.
boolean
driveroptional- Unstable
Driver responsible for formatting, mounting, and wiping the partition.
Only valid on the data partition. Mutually exclusive with
mount_script. When unset, the partition is treated as a plain Ext4 filesystem.DataPartitionDriverConfig
Cases internally — tag field
typePlaintextExt4→"plaintext-ext4"Plain Ext4 filesystem on the bare partition device.
{ "type": "plaintext-ext4", ...<payload fields> } — or "content" for non-object payloadsPlaintextExt4DriverConfig
Fields (JSON object)
labeloptionalFilesystem label assigned at format time. Defaults to
data.string
additional-options(fromadditional_options) optionalAdditional options forwarded verbatim to
mkfs.ext4.array<string>
Items
string
Luks2Passphrase→"luks2-passphrase"LUKS2 with a passphrase loaded from a file at unlock time.
Not a stand-alone production encryption mode: a passphrase living on plaintext flash defeats the encryption. Useful for testing and for out-of-band-delivered keys never persisted locally.
{ "type": "luks2-passphrase", ...<payload fields> } — or "content" for non-object payloadsLuks2PassphraseDriverConfig
Fields (JSON object)
passphrase-file(frompassphrase_file) requiredPath to the file holding the passphrase. Read raw, used as-is.
string
labeloptionalFilesystem label assigned at mkfs time. Defaults to
data.string
mapper-name(frommapper_name) optionalName of the dm-crypt mapper device. Defaults to
rugix-data.string
additional-mkfs-options(fromadditional_mkfs_options) optionalAdditional options forwarded verbatim to
mkfs.ext4.array<string>
Items
string
Luks2Tpm2→"luks2-tpm2"LUKS2 with a key sealed by a TPM 2.0 device.
systemd-cryptenrollseals a random LUKS2 passphrase to the TPM at format time;cryptsetup open --token-onlyunseals it on every boot. Production-grade: the unseal material never leaves the TPM.{ "type": "luks2-tpm2", ...<payload fields> } — or "content" for non-object payloadsLuks2Tpm2DriverConfig
Fields (JSON object)
pcrsoptionalPCRs to bind the unseal policy to. Empty list means no PCR binding.
Update warning: PCRs in the 0-9 range typically change with firmware/bootloader/kernel updates and will brick the device on every update if bound literally. PCR 7 (Secure Boot policy state) is the most stable choice for static binding. For stronger binding, future revisions will add
tpm2-public-keyforTPM2_PolicyAuthorizeso PCR predictions can be re-signed per release.array<integer>
Items
integer
deviceoptionalTPM 2.0 device path. Defaults to
auto.string
labeloptionalFilesystem label assigned at mkfs time. Defaults to
data.string
mapper-name(frommapper_name) optionalName of the dm-crypt mapper device. Defaults to
rugix-data.string
additional-mkfs-options(fromadditional_mkfs_options) optionalAdditional options forwarded verbatim to
mkfs.ext4.array<string>
Items
string
Custom→"custom"User-defined driver implemented by external scripts.
{ "type": "custom", ...<payload fields> } — or "content" for non-object payloadsCustomDataPartitionDriverConfig
Fields (JSON object)
format-script(fromformat_script) optionalBootstrap-time format script.
string
mount-script(frommount_script) requiredPer-boot mount script.
string
wipe-script(fromwipe_script) optionalrugix-ctrl data wipescript.string
slotsoptionalSystem slots.
object<string, SlotConfig>
Keys
string
Values
SlotConfig
Cases internally — tag field
typeBlock→"block"Block device slot.
{ "type": "block", ...<payload fields> } — or "content" for non-object payloadsBlockSlotConfig
Fields (JSON object)
deviceoptionalPath to the block device.
string
partitionoptionalPartition number of the block device.
integer
immutableoptionalboolean
optionaloptionalIf true, the slot is allowed to be absent.
boolean
File→"file"File slot.
{ "type": "file", ...<payload fields> } — or "content" for non-object payloadsFileSlotConfig
Fields (JSON object)
pathrequiredstring
immutableoptionalboolean
Custom→"custom"Custom slot.
{ "type": "custom", ...<payload fields> } — or "content" for non-object payloadsCustomSlotConfig
Fields (JSON object)
handlerrequiredarray<string>
Items
string
boot-groups(fromboot_groups) optionalSystem boot groups.
object<string, BootGroupConfig>
Keys
string
Values
BootGroupConfig
Fields (JSON object)
slotsrequiredSlot aliases of the boot group.
object<string, string>
Keys
string
Values
string
boot-flow(fromboot_flow) optionalBoot flow configuration.
BootFlowConfig
Cases internally — tag field
typeRpiTryboot→"rpi-tryboot"Tryboot boot flow.
{ "type": "rpi-tryboot" }RpiUboot→"rpi-uboot"U-Boot boot flow.
{ "type": "rpi-uboot" }Uboot→"uboot"Generic U-boot boot flow.
{ "type": "uboot" }Grub→"grub"Grub (EFI) boot flow.
{ "type": "grub" }RaucUboot→"rauc-uboot"RAUC-compatible U-Boot boot flow.
{ "type": "rauc-uboot", ...<payload fields> } — or "content" for non-object payloadsRaucBootFlowConfig
Fields (JSON object)
group-names(fromgroup_names) optionalRAUC boot group names.
Defaults to the respective Rugix boot group names converted to uppercase.
array<string>
Items
string
default-attempts(fromdefault_attempts) optionalDefault number of attempts left when committing or marking a boot group as good.
Defaults to 3.
Note that this is only used by U-Boot (and potentially Barebox in the future).
integer
RaucGrub→"rauc-grub"RAUC-compatible Grub boot flow.
{ "type": "rauc-grub", ...<payload fields> } — or "content" for non-object payloadsRaucBootFlowConfig
Fields (JSON object)
group-names(fromgroup_names) optionalRAUC boot group names.
Defaults to the respective Rugix boot group names converted to uppercase.
array<string>
Items
string
default-attempts(fromdefault_attempts) optionalDefault number of attempts left when committing or marking a boot group as good.
Defaults to 3.
Note that this is only used by U-Boot (and potentially Barebox in the future).
integer
MenderGrub→"mender-grub"Mender-compatible Grub boot flow.
{ "type": "mender-grub", ...<payload fields> } — or "content" for non-object payloadsMenderBootFlowConfig
Fields (JSON object)
boot-dir(fromboot_dir) optionalDirectory of the Mender boot partition.
Defaults to
/boot.string
boot-part-a(fromboot_part_a) optionalBoot Partition A.
Defaults to 2.
integer
boot-part-b(fromboot_part_b) optionalBoot partition B.
Defaults to 3.
integer
MenderUboot→"mender-uboot"Mender-compatible U-Boot boot flow.
{ "type": "mender-uboot", ...<payload fields> } — or "content" for non-object payloadsMenderBootFlowConfig
Fields (JSON object)
boot-dir(fromboot_dir) optionalDirectory of the Mender boot partition.
Defaults to
/boot.string
boot-part-a(fromboot_part_a) optionalBoot Partition A.
Defaults to 2.
integer
boot-part-b(fromboot_part_b) optionalBoot partition B.
Defaults to 3.
integer
SystemdBoot→"systemd-boot"Systemd-boot boot flow.
Uses EFI variables (LoaderEntryDefault, LoaderEntryOneShot) to control which boot entry is selected by systemd-boot. Requires a mapping from boot group names to systemd-boot entry IDs.
{ "type": "systemd-boot", ...<payload fields> } — or "content" for non-object payloadsSystemdBootFlowConfig
Fields (JSON object)
entriesrequiredMapping from boot group names to systemd-boot entry IDs.
Example:
{ a = "nixos-a.efi", b = "nixos-b.efi" }object<string, string>
Keys
string
Values
string
Custom→"custom"Custom, user-defined boot flow.
{ "type": "custom", ...<payload fields> } — or "content" for non-object payloadsCustomBootFlowConfig
Fields (JSON object)
controllerrequiredPath to the script implementing the boot flow.
string
The schema is defined in system.sidex.
Footnotes
-
If you build your system with Rugix Bakery for a generic or specific target, a working
system.tomlis generated for you and Rugix Ctrl picks it up automatically. You only need to write the configuration by hand when these defaults do not fit your device. ↩ -
Precisely, the root device is the parent block device of whatever is mounted at
/, or at Rugix Ctrl’s system partition mount point/run/rugix/mounts/systemwhen that is present. ↩ -
There are two reasons why there are no native directory slots: First, we want to keep things simple within Rugix Ctrl and directory slots can trivially be implemented with custom handlers. Second, in contrast to directories, files are more directly usable for dynamic delta updates by computing an index over them. That’s why we support them natively. ↩