Skip to content

Yocto integration

The meta-otapulse Yocto layer packages the OTA agent (soc-ota-agent), its configuration template (otapulse.conf), and a systemd service unit into your image. This guide assumes you already have a working Yocto build environment and walks through everything from cloning the layer to confirming the agent is running on-device.

Yocto release support

ReleaseCompatible
Kirkstone (4.0 LTS)Yes
Langdale (4.1)Yes
Mickledore (4.2)Yes
Nanbield (4.3)Yes
Scarthgap (5.0 LTS)Yes
Earlier releasesNot tested

Compatibility is declared in the layer’s LAYERSERIES_COMPAT_meta-soc-monitoring variable in conf/layer.conf.

Build host

Ubuntu 22.04 or later is recommended. Install the standard Yocto build host packages before continuing. Allow at least 50 GB of free disk space for a fresh build; a warm sstate-cache cuts subsequent builds to 10–20 minutes.

Systemd is required. The layer registers soc-ota-agent.service and depends on systemd as the init manager. If your distro config does not already set INIT_MANAGER = "systemd", add it to local.conf (see below).

What protects a board when a new image installs but cannot boot differs per board. A bootloader revert means the bootloader itself returns to the previous slot before Linux runs, so a board whose new root filesystem will not mount recovers without anyone touching it. Only the boards marked “Proven on hardware” have been shown to do this on a real board.

BoardBootloader revertStatus
Orange Pi Zero 2WYes. A U-Boot boot-count check in boot.scr.Proven on hardware (attended run, 2026-09-25).
NXP i.MX8MP FRDMYes. The same U-Boot boot-count check.Proven on hardware (attended run, 2026-09-25).
BeaglePlayYes. The same U-Boot boot-count check.Proven on hardware (attended run, 2026-09-25).
Radxa CM5 (RK3588)Yes. A boot-count script in U-Boot. It needs the OTA-Pulse U-Boot build, which is flashed to the board’s uboot partition. An OTA update does not deliver it.Proven on hardware (2026-10-03).
Raspberry Pi 4 (64-bit)Yes. The Raspberry Pi firmware’s one-shot tryboot boot.Proven on hardware (2026-08-22).
Radxa Dragon Q6ADesigned: systemd-boot automatic boot assessment.Not verified on hardware. Do not rely on it.
NVIDIA Jetson Xavier NXTegra-native A/B slots (nvbootctrl).Rollback not re-verified on hardware since the last reflash. Do not rely on it.

The U-Boot boot-count check lives in the boot script and bootloader that you flash to the board. An OTA update does not replace them. A board flashed with an older image may not have the check.

Raspberry Pi Zero 2 W and ODROID C4 have no bootloader revert and are not part of our nightly validation. The Raspberry Pi Zero 2 W chip has no tryboot, so it boots straight from cmdline.txt. No iMX8MM board has been validated.

Other machines your BSP layer supports can use the layer, but we have not validated them. Before you ship, check that your board’s bootloader returns to the previous slot when a new image fails to boot. A bootloader that does not do this can leave a board unbootable after a bad update.

If your board already boots through U-Boot or GRUB with an A/B environment, see Bootloader compatibility mode. In that mode the bootloader owns the slot switch and the rollback. Two QEMU targets are qualified. No physical board is qualified in that mode yet, and Raspberry Pi 4 in that mode is not qualified. The table above describes native mode only.

  1. Clone the repository

    From the directory that contains poky/ and meta-openembedded/:

    Terminal window
    git clone https://github.com/binaryblack/OTA-Pulse.git ota-pulse

    The Yocto layer lives at ota-pulse/meta-otapulse/ inside the cloned repository.

  2. Add the layer to bblayers.conf

    Edit conf/bblayers.conf in your build directory. Append meta-otapulse and meta-oe (required by the layer):

    BBLAYERS ?= " \
    /path/to/poky/meta \
    /path/to/poky/meta-poky \
    /path/to/poky/meta-yocto-bsp \
    /path/to/meta-openembedded/meta-oe \
    /path/to/ota-pulse/meta-otapulse \
    "

    Replace /path/to/ with the absolute path on your build host. You can also use bitbake-layers add-layer to append the path:

    Terminal window
    bitbake-layers add-layer /path/to/ota-pulse/meta-otapulse

Add the following to conf/local.conf in your build directory. Only OTA_SERVER_URL is strictly required for a first build; the signing variables are optional until you enable signature verification.

# --- Required ---
# Install the OTA agent and signing-keys into the image
IMAGE_INSTALL:append = " soc-ota-agent signing-keys"
# Systemd is required by the layer
DISTRO_FEATURES:append = " systemd usrmerge"
INIT_MANAGER = "systemd"
# OTA-Pulse server URL — the agent posts here during updates
OTA_SERVER_URL = "https://your-server.example.com"
# --- Optional: signature verification ---
# Set to "1" to enable artifact signature verification on-device
SOC_OTA_SIGNATURE_VERIFICATION = "0"
# Path where signing-keys recipe installs public keys
SOC_OTA_SIGNING_KEYS_DIR = "/etc/soc-monitoring/signing-keys"
# Space-separated list of public key filenames (relative to the active/ subdirectory)
SOC_OTA_VERIFY_KEY_FILES = "production-rsa-public.pem production-ecdsa-public.pem"

The soc-ota-agent recipe reads OTA_SERVER_URL at build time and writes it into /etc/otapulse/otapulse.conf on the image. Every image and every root filesystem update artifact carries its own copy of that file, so build them all with the same production URL. See Configuration files. The configuration template also sets RootfsPartA and RootfsPartB to /dev/disk/by-partlabel/rootfs_a and rootfs_b respectively, which matches the A/B layout produced by the layer’s WIC kickstart file (see the next section).

The layer ships a WIC kickstart template (wic/soc-monitoring.wks.in) that defines the A/B partition layout the OTA agent expects:

PartitionLabelSizePurpose
/bootboot100 MBFAT32, kernel + DTB
/rootfs_a1 GBPrimary root filesystem
(unmounted)rootfs_b1 GBInactive slot for OTA writes
/datadata500 MBPersistent data across updates

To use this layout, set the WIC file in local.conf:

WKS_FILE = "soc-monitoring.wks.in"
IMAGE_FSTYPES = "wic wic.gz wic.bmap ext4"

The reference image recipe (soc-monitoring-image.bb) already sets WKS_FILE ?= "soc-monitoring.wks.in", so if you build soc-monitoring-image you get this layout automatically. If you are extending a different image recipe, set it explicitly.

Embedding signing keys

To bake your public keys into the image for signature verification, copy them into the recipe’s files directory and set the recipe variables:

Terminal window
# Copy your exported public keys into the layer
cp ~/Downloads/production-rsa-public.pem \
meta-otapulse/recipes-core/signing-keys/files/
cp ~/Downloads/production-ecdsa-public.pem \
meta-otapulse/recipes-core/signing-keys/files/

Then in local.conf:

SOC_OTA_SIGNATURE_VERIFICATION = "1"
SOC_OTA_VERIFY_KEY_FILES = "production-rsa-public.pem production-ecdsa-public.pem"

The signing-keys recipe installs both files under /etc/soc-monitoring/signing-keys/active/ with read-only permissions (0444). The agent reads this directory when ArtifactVerifyKeys is set in its configuration.

Generate the key pair yourself, for example with openssl genrsa -out release-private.pem 4096 and openssl rsa -in release-private.pem -pubout -out production-rsa-public.pem. Keep the private key secure and sign artifacts with otapulse-artifact write ... --key release-private.pem. Also import the same public key in the console (Signing Keys -> Import Public Key) so the server accepts your signed uploads; the server cannot push keys to devices, so baking it into the image as above is still required. See the End-to-end quickstart for the full flow.

Terminal window
source poky/oe-init-build-env build
bitbake soc-monitoring-image

Or, if you are extending your own image recipe:

Terminal window
bitbake <your-image-recipe>

The built WIC image lands in tmp/deploy/images/<MACHINE>/. Flash it to your device:

Terminal window
sudo dd if=tmp/deploy/images/<MACHINE>/soc-monitoring-image-<MACHINE>.wic \
of=/dev/sdX bs=4M status=progress
sync

After boot, log in with the credentials you configured in Hardening before production and verify the agent started successfully. The reference image does not ship a default or empty root password, so you must provide a root SSH key (or a root password hash) at build time before you can log in:

Terminal window
ssh -i ~/.ssh/<your-key> root@<device-ip>
systemctl status soc-ota-agent
journalctl -u soc-ota-agent --since "1 minute ago"

Within about 30 seconds of the agent starting, the device should appear with an online status badge in the OTA-Pulse console.

Once the device is registered, follow Register your first device for the console-side setup — generating an API key and verifying telemetry is flowing.

The reference soc-monitoring-image recipe does not enable Yocto’s debug-tweaks image feature. debug-tweaks gives root an empty password and sets PermitRootLogin yes and PermitEmptyPasswords yes in sshd_config, so anyone who can reach port 22 gets a root shell with no credential. Never ship a device built with it.

Before you build an image for production:

  1. Do not enable debug-tweaks. Make sure no local.conf, image recipe, .bbappend or .inc in your build sets EXTRA_IMAGE_FEATURES += "debug-tweaks" or lists debug-tweaks in IMAGE_FEATURES. You can check the final value with bitbake -e soc-monitoring-image | grep -E '^(IMAGE_FEATURES|EXTRA_IMAGE_FEATURES)='.

  2. Use key-only root SSH, or set a root password hash. Bake your public key into the image and force PermitRootLogin prohibit-password and PermitEmptyPasswords no. Add this to a soc-monitoring-image.bbappend in your own layer, and set OTAPULSE_ROOT_SSH_PUBKEY in local.conf to the contents of your public key file:

    OTAPULSE_ROOT_SSH_PUBKEY ?= ""
    ROOTFS_POSTPROCESS_COMMAND += "otapulse_harden_root_ssh;"
    otapulse_harden_root_ssh() {
    sed -i -e 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' \
    -e 's/^#\?PermitEmptyPasswords.*/PermitEmptyPasswords no/' \
    ${IMAGE_ROOTFS}${sysconfdir}/ssh/sshd_config
    if [ -n "${OTAPULSE_ROOT_SSH_PUBKEY}" ]; then
    install -d -m 0700 ${IMAGE_ROOTFS}${ROOT_HOME}/.ssh
    echo "${OTAPULSE_ROOT_SSH_PUBKEY}" > ${IMAGE_ROOTFS}${ROOT_HOME}/.ssh/authorized_keys
    chmod 0600 ${IMAGE_ROOTFS}${ROOT_HOME}/.ssh/authorized_keys
    fi
    }

    If you prefer a root password, generate a SHA-512 hash with openssl passwd -6 and set it with the extrausers class. Escape every $ in the hash as \$:

    INHERIT += "extrausers"
    EXTRA_USERS_PARAMS = "usermod -p '\$6\$<salt>\$<hash>' root;"
  3. Keep SSH reachable only where you need it. Restrict port 22 with your firewall or network policy. The OTA agent only makes outbound connections, so the device does not need inbound SSH from the internet.

  4. Verify on a built image before you flash a fleet: grep '^root:' /etc/shadow must not show an empty hash, and sshd -T | grep -Ei 'permitrootlogin|permitemptypasswords' must show prohibit-password and no.

ERROR: Nothing PROVIDES 'dlt-daemon'

meta-oe is missing from bblayers.conf — the layer’s DLT logging support (dlt-daemon) is provided by meta-oe, not meta-otapulse itself. Add it:

BBLAYERS += "/path/to/meta-openembedded/meta-oe"

ERROR: Nothing PROVIDES 'soc-ota-agent'

The layer path in bblayers.conf is wrong or the layer was never added. Verify with bitbake-layers show-layers — meta-soc-monitoring must appear in the output (this is the internal Bitbake collection name for the meta-otapulse layer). If it does not, re-run:

Terminal window
bitbake-layers add-layer /path/to/ota-pulse/meta-otapulse

ERROR: LIC_FILES_CHKSUM for soc-ota-agent_1.0.0.bb does not match

A local edit changed the Apache-2.0 license file checksum. Restore the common license file to its canonical content, or update the checksum in the recipe:

Terminal window
md5sum ${COMMON_LICENSE_DIR}/Apache-2.0
# Update LIC_FILES_CHKSUM in soc-ota-agent_1.0.0.bb to match

soc-ota-agent.service: Failed to start — can't open /etc/otapulse/otapulse.conf

The configuration file was not generated during the build. This usually means OTA_SERVER_URL was not set in local.conf when the recipe ran, so the template substitution produced an empty or invalid file. Set the variable and do a clean rebuild:

Terminal window
bitbake -c cleanall soc-ota-agent
bitbake soc-ota-agent

OTA update fails with “signature verification failed” after flashing a new artifact

Either the public key in the image does not match the private key used to sign the artifact, or SOC_OTA_SIGNATURE_VERIFICATION is set to "1" but signing-keys was not included in IMAGE_INSTALL. Confirm both:

Terminal window
# On device — check keys are present
ls -la /etc/soc-monitoring/signing-keys/active/
# In your build — check both packages were included
bitbake -e soc-monitoring-image | grep '^IMAGE_INSTALL'

If the key files are missing, add signing-keys to IMAGE_INSTALL:append and rebuild.