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:
- The device's bootloader (U-Boot, Barebox, RPi, grub, etc.) boots the platform-specific Linux kernel and the initramfs from this project
- The
init
process provided by this project handles any recovery tasks, and then mounts the real root filesystem, and runs the realinit
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.
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 can be defined and used as needed like other languages. The following variables have special uses:
Variable | Description |
---|---|
rootfs.fstype | Root filesystem time. Defaults to "squashfs" |
rootfs.path | Root filesystem path or spec. Defaults to "/dev/mmcblk0p2" |
rootfs.encrypted | True if the filesystem is encrypted. Defaults to false |
rootfs.cipher | The cipher used to encrypt the filesystem. For example, "aes-cbc-plain" |
rootfs.secret | The secret key as hex digits |
uboot_env.path | The location for U-Boot environment data. Defaults to "/dev/mmcblk0" |
uboot_env.loaded | True if the U-Boot environment block has been loaded. |
uboot_env.modified | True if something has modified the U-Boot block and it differs from what's on disk |
uboot_env.start | The block offset of the U-Boot environment. (512 byte blocks) |
uboot_env.count | The number of blocks in the environment. Defaults to 256. |
run_repl | True 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
It's also possible to call built-in functions:
Function | Description |
---|---|
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 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.
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.
The following strings must be in your kernel configuration:
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_RD_<compression>=y
- gzip compression is enabled by default, but if you want lz4, for example, you'll need to enable it.
To mount encrypted filesystems, you'll need these additional configuration strings:
CONFIG_BLK_DEV_LOOP=y
CONFIG_MD=y
CONFIG_BLK_DEV_DM=y
CONFIG_DM_CRYPT=y
- Onlydm-crypt
is supported.cryptoloop
andloop-aes
are not supported.CONFIG_CRYPTO_USER=y
CONFIG_CRYPTO_USER_API_HASH=y
CONFIG_CRYPTO_USER_API_SKCIPHER=y
CONFIG_CRYPTO_AES=y
- Make sure to enable the cryptographic algorithms that you're using. Hardware acceleration options may exist as well.
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
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.
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:
- Create your root filesystem as normal
- 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. - Make sure that your device's Linux kernel supports the chosen encryption algorithms
- 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.