Update System
This page explains how Strux OTA updates work internally: the .struxb bundle format, the signing and verification model, and how a device installs an update and recovers from a bad one. For the step-by-step workflow, see the Updates guide.
Experimental
Right now, System Updates are an experimental feature. We have major plans regarding this feature, so stay tuned!
The mental model
A Strux update is the whole OS, replaced atomically. There is no package manager on the device and no incremental patching — a bundle contains a complete root filesystem image, and the device keeps two copies of the OS side by side:
┌─────────────────┬─────────────────┬──────────────┐
│ strux-rootfs-a │ strux-rootfs-b │ strux-data │
│ (running OS) │ (idle slot) │ (FAT32) │
└─────────────────┴─────────────────┴──────────────┘
▲ ▲ ▲
│ │ │
booted from update written boot script +
this slot here, then slot selection
becomes active state (read by
the bootloader)
An update is written to the idle slot while the device keeps running. Only after the payload is fully verified on disk does the device ask the bootloader to try the new slot — with a bounded number of attempts, so a broken update rolls back by itself.
The bundle format (.struxb)
A .struxb file is a gzip-compressed tar archive with exactly three members:
update.struxb (tar.gz)
├── manifest.json # what's in the bundle, and how it's signed
├── manifest.sig # base64 RSA-PSS signature over manifest.json
└── rootfs.img # the complete root filesystem (ext4)
The manifest is small and human-readable:
{
"schema": "dev.strux.update.bundle.v1",
"bsp": "hd215-rk3576",
"version": "1.2.0",
"projectVersion": "1.2.0",
"struxVersion": "0.3.0",
"createdAt": "2026-06-12T15:04:05Z",
"payload": {
"type": "full-rootfs",
"file": "rootfs.img",
"size": 2147483648,
"sha256": "..."
},
"signing": {
"algorithm": "rsa-pss-sha512",
"keyBits": 4096,
"saltLength": "hash",
"signedBytes": "manifest.json"
}
}
The signature covers the manifest, and the manifest pins the payload by size and SHA-256 — so verifying the small manifest transitively verifies the multi-gigabyte payload. The member order in the archive (manifest before payload) is what makes streaming installation possible: by the time the payload bytes arrive, the device already knows their expected hash.
Bundles are created by strux update bundle (or automatically at the end of a build — see semantics below), which runs inside the builder container and signs with OpenSSL.
Signing and verification
The trust model is a single project-owned RSA keypair:
- Private key (
strux-update.key): generated bystrux initorstrux update gen-keypair, 4096-bit, kept out of git, used by the CLI to sign manifests with RSA-PSS/SHA-512 (salt length = hash length). - Public key (
strux-update.pub): committed with the project and installed into every update-enabled image at/etc/strux/update.pub.
On the device, the Strux client rejects a bundle unless all of these hold:
- The signature over
manifest.jsonverifies against/etc/strux/update.pub(RSA keys under 4096 bits are refused outright). - The manifest declares the supported schema (
dev.strux.update.bundle.v1), payload type (full-rootfs), and signing algorithm (rsa-pss-sha512). - The manifest's
bspmatches the BSP recorded in the image at/etc/strux/project.json— a bundle built for one board cannot be installed on another. - The payload's size and SHA-256 match the manifest, checked twice: once while writing, and again by reading the slot back from disk.
There is deliberately no unsigned mode — a device without a valid public key cannot install updates at all.
How the client applies an update
The on-device Strux client receives an update either as a URL to download or a local file path (today, both arrive via the dev server connection — strux update send). Installation is a streaming pipeline:
- Identify the idle slot. The kernel command line carries
strux.slot=Aorstrux.slot=B; the client targets the other one, at/dev/disk/by-partlabel/strux-rootfs-aor-b. - Stream and verify. The tar stream is decompressed on the fly: manifest and signature are read and verified first, then the rootfs payload is written directly to the idle slot's block device while being hashed. No temporary copy of the payload ever exists.
- Re-verify on disk. The slot is read back and its size and SHA-256 compared against the manifest — catching any write or storage error.
- Relabel. The new filesystem is relabeled (
e2label) to its slot's label (strux-rootfs-a/strux-rootfs-b), since the bundled image carries the build-time label. - Arm the bootloader. The client writes the boot environment on the shared data partition: the idle slot becomes pending with
strux_tries=3, and the generation counter increments. The environment is written redundantly (BOOTENV.TXTandBOOTBAK.TXT) so a power cut mid-write can't lose the boot state. - Reboot. On each boot attempt, the bootloader decrements the try counter before booting the pending slot. When the new OS comes up and the Strux client starts, it marks the slot active and clears the pending state. If the counter reaches zero without that ever happening, the bootloader boots the old slot — automatic rollback.
Throughout the install, the client writes progress to /run/strux/update-progress.json and persists slot state to /strux-data/strux/update-state.json. The Go runtime exposes both to your app under the update API namespace (progress and state queries), so your frontend can render an update screen — see the Go runtime reference.
update.enabled and auto_bundle
update:
enabled: true
auto_bundle: true
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Makes the build update-capable. BSP lifecycle scripts receive STRUX_UPDATE_ENABLED=true and switch to the A/B image layout; the public key gets baked into the image. |
auto_bundle | boolean | false | After the BSP's make_image step, automatically sign dist/output/<bsp>/rootfs.ext4 into a .struxb bundle. |
The two are independent switches but bundling requires both: the build pipeline only generates a bundle when enabled and auto_bundle are true. enabled: true with auto_bundle: false builds an update-capable image and leaves bundling to an explicit strux update bundle.
Note that enabled changes the image layout, not just a feature flag — flipping it produces a different partition table, so devices flashed without it can't be updated over the air.
Experimental — A/B dual-rootfs conventions
The dual-rootfs design is experimental and may change. However, BSPs targeting Strux v0.3.0 that want update support must follow these conventions today — the on-device client and bootloader contract depend on them:
- Partitions: a GPT disk with two equally-sized ext4 rootfs partitions whose GPT partition labels (and discoverable paths) are
strux-rootfs-a(populated) andstrux-rootfs-b(left blank), plus a shared FAT32 partition labeledstrux-data. The client resolves slots via/dev/disk/by-partlabel/. - Filesystem labels: the rootfs filesystems are labeled
strux-rootfs-a/strux-rootfs-bto match their slots. strux-datacontents: FAT32 (so the bootloader can read and write it), containing/strux/boot.scr(the compiled A/B boot script),/strux/BOOTENV.TXT+/strux/BOOTBAK.TXT(redundant text boot environment), and/strux/update-state.json.- Boot environment keys:
strux_active,strux_pending,strux_tries,strux_generation— the bootloader selects the slot, decrementsstrux_triesfor a pending slot before booting it, and falls back to the active slot when tries are exhausted. - Kernel command line: must carry
strux.slot=<A|B>so the client knows which slot it booted from, and the root device is selected by partition label (root=PARTLABEL=strux-rootfs-aor-b). - Rootfs configuration: the image mounts the data partition at
/strux-datavia fstab, installs the update public key at/etc/strux/update.pub(the build must fail ifstrux-update.pubis missing), and keeps a migration copy of the boot script at/etc/strux/boot.scr. First-boot partition resizing must be disabled — slot boundaries are fixed. - Update payload export: the
make_imagestep must also export the bare rootfs image todist/output/<bsp>/rootfs.ext4, which is whatstrux update bundlesigns by default.
For the bootloader scripting, image generation, and a complete worked example, see Dual Rootfs in the BSP docs.
Where to go next
- Updates guide — the end-to-end workflow with commands.
- Dual Rootfs — implementing the conventions in a BSP.
- Build Pipeline — where image creation and auto-bundling sit in the build.