Home

Awesome

nerves_initramfs

CircleCI

This project creates a tiny initramfs that supports cross-platform failure recovery and non-trivial root filesystem setups on devices using Nerves.

The way it works is this:

  1. The device's bootloader (U-Boot, Barebox, RPi, grub, etc.) boots the platform-specific Linux kernel and the initramfs from this project
  2. The init process provided by this project handles any recovery tasks, and then mounts the real root filesystem, and runs the real init process (erlinit for Nerves)

This project is focused on recovery and root filesystem mounts. No other functionality is likely to be added to this project to keep it small, fast, and easy to audit.

Failure recovery at this level means that if a Nerves device fails catastrophically after the Linux kernel starts, code here can revert devices to previous firmware or start recovery mechanisms. This project does not protect against Linux kernel initialization crashes. This is a tradeoff to avoid maintaining an implementation for every bootloader where this logic is time consuming to verify. You can, of course, transition away from this code as needed.

Root filesystem mount functionality is handled here as well since the initramfs is typically where this feature lives for when the root filesystem is encrypted. Additionally, if the root filesystem fails to decrypt, the recovery mechanisms described previously can be triggered.

Boot configuration

nerves_initramfs is configured via rules in a file called /nerves_initramfs.conf in the initramfs. Be aware that nerves_initramfs will delete that file when it cleans up before switching to the true root filesystem.

The nerves_initramfs.conf file uses a simple language for specifying rules and setting variables. Rules are composed of a condition and a list of actions . Here's an example:

!fw_validated && fw_booted -> { fwup_revert(); reboot(); }

Real configuration files contain rules to handle fallback logic or set up root filesystem mounts that Linux wouldn't be able to do without help.

Variables

Variables can be defined and used as needed like other languages. The following variables have special uses:

VariableDescription
rootfs.fstypeRoot filesystem time. Defaults to "squashfs"
rootfs.pathRoot filesystem path or spec. Defaults to "/dev/mmcblk0p2"
rootfs.encryptedTrue if the filesystem is encrypted. Defaults to false
rootfs.cipherThe cipher used to encrypt the filesystem. For example, "aes-cbc-plain"
rootfs.secretThe secret key as hex digits
uboot_env.pathThe location for U-Boot environment data. Defaults to "/dev/mmcblk0"
uboot_env.loadedTrue if the U-Boot environment block has been loaded.
uboot_env.modifiedTrue if something has modified the U-Boot block and it differs from what's on disk
uboot_env.startThe block offset of the U-Boot environment. (512 byte blocks)
uboot_env.countThe number of blocks in the environment. Defaults to 256.
run_replTrue to run a REPL before booting. This is useful for debug. Defaults to false

Variables can be overridden using the Linux commandline. See your platform's bootloader documentation for how to pass options to Linux. At the end of the commandline, add a -- and then add variable and variable=value strings. The -- will stop the Linux kernel from processing parameters so make sure that everything intended for Linux is on the left side and everything for nerves_initramfs is on the right side.

For example, the following Linux commandline sets the Linux console and then passes a root filesystem path to nerves_initramfs and starts a repl.

console=tty1 -- rootfs.path=/dev/sda2 run_repl

Functions

It's also possible to call built-in functions:

FunctionDescription
blkid()Print out information about all block devices
cmd()Run an external program. The first argument is the path to the program, the next is the first argument, and so on.
env()Print out all loaded U-Boot variables
fwup_revert()Run fwup with the appropriate parameters to revert to the previous firmware. Reboots on success.
getenv(key)Get the value of a U-Boot variable
help()Print out help when running in the REPL
print(...)Print one or more strings and variables
loadenv()Load a U-Boot environment block. Set up uboot_env.path, uboot_env.start and uboot_env.count first.
ls()List files a directory
poweroff()Power off the device
readfile(path)Read a file (truncates long files)
reboot()Reset the device
saveenv()Save all U-Boot variables back to storage
setenv(key, value)Set a U-Boot variable. It is not saved until you call saveenv()
sleep(timeout)Wait for the specified milliseconds
vars()Print out all known variables and their values

Block device specifications

Block devices, such as /dev/sda2, are either specified as absolute paths or using the Linux match syntax. Currently only PARTUUID is supported for identifying a partition by its UUID. Use the blkid function or the blkid commandline utility in Linux to list information about block devices.

If you are only using one storage device, using absolute paths to block devices is fine. If you have more than one storage device, Linux sometimes can enumerate them in a different order so /dev/sda could be /dev/sdb sometimes. The way around this is to identify devices by UUID.

Building

Users should prefer to use pre-built releases. To build your own, you will need to use Linux and install the Buildroot required packages.

Change to the builder directory and run ./build-all.sh to build for all platforms.

If you're only building for one platform, run the following:

./create-build.sh configs/nerves_initramfs_arm_defconfig o/arm
cd o/arm
make

The nerves_initramfs images will be in the images directory. You can also run make menuconfig to enable other applications and libraries that may be useful for your particular setup.

Linux kernel configuration

The following strings must be in your kernel configuration:

To mount encrypted filesystems, you'll need these additional configuration strings:

Preparing files for boot

initramfs requires a single file in cpio format. If you have multiple files, they need to be converted to cpio format and concatenated together with the build.

The compressed build files are already in cpio format, but you will need to convert your nerves_initramfs.confg and any other files (such as revert.fw) then concatenate them together before using on boot. The helper script file-to-cpio.sh was created for this and is included with the release so it can be used as needed.

Your implementation may differ, but a simple usage example is:

$ file-to-cpio.sh nerves_initramfs.conf nerves_initramfs.conf.cpio
$ file-to-cpio.sh revert.fw revert.fw.cpio

# Then concatenate files together
# The build .gz/.xz is already cpio format
$ cat nerves_initramfs_arm.gz nerves_initramfs.conf.cpio revert.fw.cpio > nerves_initramfs
$ cp nerves_initramfs /path/to/your/boot/partition

Raspberry Pi configuration

The Raspberry Pi's bootloader supports loading initramfs images off the boot partition that contains the Linux kernel. Since gzip compression is pretty much always enabled in Linux kernels, the nerves_initramfs_arm.gz is safe to use.

First, follow the steps in Preparing files for boot, copy the nerves_initramfs file to the boot partition (in Nerves, you can do this manually or edit the fwup.conf to automate prepping and copying the files), then add this line to config.txt:

initramfs nerves_initramfs followkernel

See the official config.txt boot documentation for more information.

U-boot configuration

Grub configuration

Mounting an encrypted file system

This project mounts encrypted file systems by using the Linux kernel's dm-crypt module. As such, the web contains a lot of information on this feature. The main difference, though, is that devices using this project are expected to be unattended. In other words, a human can not enter a password when the device boots. This ends up simplifying the options that can be used, but the handling of the secret can become a major issue.

Storing the secret securely depends on your hardware and your needs. It is expected that some projects will need to fork this repository to access their secrets. The examples shown here are meant to be informative on the process, but they are not secure. They are probably more accurately referred to as ways to obfuscate the root filesystem.

Here's the general idea:

  1. Create your root filesystem as normal
  2. Write the filesystem to the destination media. If using a MicroSD card, this could be done by mounting it on a Linux PC using dm-crypt and writing the root filesystem via that. fwup also supports encryption.
  3. Make sure that your device's Linux kernel supports the chosen encryption algorithms
  4. Configure this project with how to find or generate the secret key

Here's an example configuration file with the secret key stored as plain text:

rootfs.encrypted = true
rootfs.cipher = "aes-cbc-plain"
rootfs.secret = "8e9c0780fd7f5d00c18a30812fe960cfce71f6074dd9cded6aab2897568cc856"

This is illustrative, but obviously quite insecure. The current route to obtaining the secret key is to edit the C code to this project to integrate it with platform-specific way of keeping or hiding secrets. It is hoped that alternatives can be shared in the future.