Skip to main content
Documentation

System Configuration

Preview
You're viewing the Next docs — a rolling preview of in-development changes. The current release docs may differ.

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:

record

SystemConfig

System configuration.

  • config-partition (from config_partition) optional

    Config partition configuration.

    PartitionConfig
    • disabled optional

      Explicitly disable the partition.

      boolean
    • device optional

      Path to the partition block device.

      string
    • partition optional

      Partition number of the root device.

      integer
    • mount-script (from mount_script) optional

      User-defined script for mounting the partition.

      Mutually exclusive with driver.

      string
    • path optional

      Path where the partition is or should be mounted.

      string
    • protected optional

      Indicates whether the partition is write-protected.

      boolean
    • driver optional
      • 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
      • PlaintextExt4"plaintext-ext4"

        Plain Ext4 filesystem on the bare partition device.

        { "type": "plaintext-ext4", ...<payload fields> } — or "content" for non-object payloads

        PlaintextExt4DriverConfig
        • label optional

          Filesystem label assigned at format time. Defaults to data.

          string
        • additional-options (from additional_options) optional

          Additional options forwarded verbatim to mkfs.ext4.

          array<string>
          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 payloads

        Luks2PassphraseDriverConfig
        • passphrase-file (from passphrase_file) required

          Path to the file holding the passphrase. Read raw, used as-is.

          string
        • label optional

          Filesystem label assigned at mkfs time. Defaults to data.

          string
        • mapper-name (from mapper_name) optional

          Name of the dm-crypt mapper device. Defaults to rugix-data.

          string
        • additional-mkfs-options (from additional_mkfs_options) optional

          Additional options forwarded verbatim to mkfs.ext4.

          array<string>
          string
      • Luks2Tpm2"luks2-tpm2"

        LUKS2 with a key sealed by a TPM 2.0 device.

        systemd-cryptenroll seals a random LUKS2 passphrase to the TPM at format time; cryptsetup open --token-only unseals it on every boot. Production-grade: the unseal material never leaves the TPM.

        { "type": "luks2-tpm2", ...<payload fields> } — or "content" for non-object payloads

        Luks2Tpm2DriverConfig
        • pcrs optional

          PCRs 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-key for TPM2_PolicyAuthorize so PCR predictions can be re-signed per release.

          array<integer>
          integer
        • device optional

          TPM 2.0 device path. Defaults to auto.

          string
        • label optional

          Filesystem label assigned at mkfs time. Defaults to data.

          string
        • mapper-name (from mapper_name) optional

          Name of the dm-crypt mapper device. Defaults to rugix-data.

          string
        • additional-mkfs-options (from additional_mkfs_options) optional

          Additional options forwarded verbatim to mkfs.ext4.

          array<string>
          string
      • Custom"custom"

        User-defined driver implemented by external scripts.

        { "type": "custom", ...<payload fields> } — or "content" for non-object payloads

        CustomDataPartitionDriverConfig
        • format-script (from format_script) optional

          Bootstrap-time format script.

          string
        • mount-script (from mount_script) required

          Per-boot mount script.

          string
        • wipe-script (from wipe_script) optional

          rugix-ctrl data wipe script.

          string
  • data-partition (from data_partition) optional

    Data partition configuration.

    PartitionConfig
    • disabled optional

      Explicitly disable the partition.

      boolean
    • device optional

      Path to the partition block device.

      string
    • partition optional

      Partition number of the root device.

      integer
    • mount-script (from mount_script) optional

      User-defined script for mounting the partition.

      Mutually exclusive with driver.

      string
    • path optional

      Path where the partition is or should be mounted.

      string
    • protected optional

      Indicates whether the partition is write-protected.

      boolean
    • driver optional
      • 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
      • PlaintextExt4"plaintext-ext4"

        Plain Ext4 filesystem on the bare partition device.

        { "type": "plaintext-ext4", ...<payload fields> } — or "content" for non-object payloads

        PlaintextExt4DriverConfig
        • label optional

          Filesystem label assigned at format time. Defaults to data.

          string
        • additional-options (from additional_options) optional

          Additional options forwarded verbatim to mkfs.ext4.

          array<string>
          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 payloads

        Luks2PassphraseDriverConfig
        • passphrase-file (from passphrase_file) required

          Path to the file holding the passphrase. Read raw, used as-is.

          string
        • label optional

          Filesystem label assigned at mkfs time. Defaults to data.

          string
        • mapper-name (from mapper_name) optional

          Name of the dm-crypt mapper device. Defaults to rugix-data.

          string
        • additional-mkfs-options (from additional_mkfs_options) optional

          Additional options forwarded verbatim to mkfs.ext4.

          array<string>
          string
      • Luks2Tpm2"luks2-tpm2"

        LUKS2 with a key sealed by a TPM 2.0 device.

        systemd-cryptenroll seals a random LUKS2 passphrase to the TPM at format time; cryptsetup open --token-only unseals it on every boot. Production-grade: the unseal material never leaves the TPM.

        { "type": "luks2-tpm2", ...<payload fields> } — or "content" for non-object payloads

        Luks2Tpm2DriverConfig
        • pcrs optional

          PCRs 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-key for TPM2_PolicyAuthorize so PCR predictions can be re-signed per release.

          array<integer>
          integer
        • device optional

          TPM 2.0 device path. Defaults to auto.

          string
        • label optional

          Filesystem label assigned at mkfs time. Defaults to data.

          string
        • mapper-name (from mapper_name) optional

          Name of the dm-crypt mapper device. Defaults to rugix-data.

          string
        • additional-mkfs-options (from additional_mkfs_options) optional

          Additional options forwarded verbatim to mkfs.ext4.

          array<string>
          string
      • Custom"custom"

        User-defined driver implemented by external scripts.

        { "type": "custom", ...<payload fields> } — or "content" for non-object payloads

        CustomDataPartitionDriverConfig
        • format-script (from format_script) optional

          Bootstrap-time format script.

          string
        • mount-script (from mount_script) required

          Per-boot mount script.

          string
        • wipe-script (from wipe_script) optional

          rugix-ctrl data wipe script.

          string
  • slots optional

    System slots.

    object<string, SlotConfig>
    string
    SlotConfig
    • Block"block"

      Block device slot.

      { "type": "block", ...<payload fields> } — or "content" for non-object payloads

      BlockSlotConfig
      • device optional

        Path to the block device.

        string
      • partition optional

        Partition number of the block device.

        integer
      • immutable optional
        boolean
      • optional optional

        If true, the slot is allowed to be absent.

        boolean
    • File"file"

      File slot.

      { "type": "file", ...<payload fields> } — or "content" for non-object payloads

      FileSlotConfig
      • path required
        string
      • immutable optional
        boolean
    • Custom"custom"

      Custom slot.

      { "type": "custom", ...<payload fields> } — or "content" for non-object payloads

      CustomSlotConfig
      • handler required
        array<string>
        string
  • boot-groups (from boot_groups) optional

    System boot groups.

    object<string, BootGroupConfig>
    string
    BootGroupConfig
    • slots required

      Slot aliases of the boot group.

      object<string, string>
      string
      string
  • boot-flow (from boot_flow) optional

    Boot flow configuration.

    BootFlowConfig
    • RpiTryboot"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 payloads

      RaucBootFlowConfig
      • group-names (from group_names) optional

        RAUC boot group names.

        Defaults to the respective Rugix boot group names converted to uppercase.

        array<string>
        string
      • default-attempts (from default_attempts) optional

        Default 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 payloads

      RaucBootFlowConfig
      • group-names (from group_names) optional

        RAUC boot group names.

        Defaults to the respective Rugix boot group names converted to uppercase.

        array<string>
        string
      • default-attempts (from default_attempts) optional

        Default 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 payloads

      MenderBootFlowConfig
      • boot-dir (from boot_dir) optional

        Directory of the Mender boot partition.

        Defaults to /boot.

        string
      • boot-part-a (from boot_part_a) optional

        Boot Partition A.

        Defaults to 2.

        integer
      • boot-part-b (from boot_part_b) optional

        Boot 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 payloads

      MenderBootFlowConfig
      • boot-dir (from boot_dir) optional

        Directory of the Mender boot partition.

        Defaults to /boot.

        string
      • boot-part-a (from boot_part_a) optional

        Boot Partition A.

        Defaults to 2.

        integer
      • boot-part-b (from boot_part_b) optional

        Boot 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 payloads

      SystemdBootFlowConfig
      • entries required

        Mapping from boot group names to systemd-boot entry IDs.

        Example: { a = "nixos-a.efi", b = "nixos-b.efi" }

        object<string, string>
        string
        string
    • Custom"custom"

      Custom, user-defined boot flow.

      { "type": "custom", ...<payload fields> } — or "content" for non-object payloads

      CustomBootFlowConfig
      • controller required

        Path to the script implementing the boot flow.

        string

The schema is defined in system.sidex.

Footnotes

  1. If you build your system with Rugix Bakery for a generic or specific target, a working system.toml is 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.

  2. 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/system when that is present.

  3. 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.