Finding your way through Yocto
Author: Steff Richards, Systems Engineer at emlix GmbH
The Yocto Project produces tools and processes for building product-specific embedded Linux distributions (Board Support Packages) suitable for industrial use. The tooling is fully featured and highly customisable, which inevitably means it can be complex to make it do what you need. However, by following established industrial best practices, Yocto Linux can be used to build highly secure, highly maintainable embedded Linux distributions for almost any target board, taking care that only the required software is included in an image and nothing more.
Getting started
There are a number of questions which arise when starting a new project with Yocto, such as:
- What is the best starting point?
- How can you build a “hardened” Linux system for your target?
- Which packages does your image actually contain?
- How do you configure your project to be maintainable?
This article provides a summary of key aspects of developing Yocto Linux for use in industrial products. Martin Homuth has outlined the implementation of Yocto platform strategies for various boards, products and product families in our white paper ‘emlix Yocto Framework Design, composition and maintenance of Yocto-based Linux systems’.
Starting point
One of the strengths of the Yocto Project is the documentation. Not only do they provide the wonderful all-in-one mega-manual, which works great as a reference to search when developing with Yocto, they also provide more focused documentation such as the Development Tasks Manual which explains many of the common tasks involved in developing a Yocto-based embedded system. Another good source of information for beginners and advanced users alike is the official YouTube channel.
However, all this information can be confusing when just starting out, so what are the first steps that need to be taken to configure a working Yocto-based embedded system?
Setup
Setting up the build environment, including the build machine and the initial source files, is well documented in the Development Tasks Manual Start chapter. Creating a setup which runs inside a docker container is preferable, since this makes it easier for other developers and CI build agents to work from a common build environment.
Once the build environment has been set up, the layers can be selected. For a minimal setup, a good starting point is the poky layer from the Yocto Project. This brings the necessary tools and scripts, as well as many recipes for commonly used software packages, which will enable building a minimal setup. The meta-openembedded layer, which is actually a collection of layers, is another high quality, highly recommended layer, bringing more recipes for commonly used software packages.
The consideration of which hardware layers to choose is tightly coupled to the hardware itself, but in order to try things out without hardware, the poky layer provides good support for hardware emulation using QEMU.
To compose a layer setup for a Yocto build, each community provided layer needs to be made available in the folder structure alongside any project specific layers. A path to each layer must then be added to the BBLAYERS variable in bblayers.conf. In each layer, there must exist a conf/layer.conf which lists the location of all the recipes made available in that layer in BBFILES. These configurations are used by BitBake to know which directories to traverse to find recipes during a build.
local.conf contains configurations for the build environment, for example, the target machine (MACHINE), the package types to use (PACKAGE_CLASS), where to place downloads (DL_DIR), and where to place shared state files for speeding up builds (SSTATE_DIR). The template which comes from the Yocto Project includes good documentation for usual configurations set here.
Configuration
distro.conf configures what this new Yocto Linux Board Support Package - will contain. In here decisions are made on system-wide features and configuration options, such as
- what the Board Support Package should be called,
- the init manager to use, systemd, busybox, sysvinit, or something more specific,
- the libc to include,
- the general compiler settings to use when building packages,
and so much more. distro.conf can inherit from one of the configurations provided with poky, allowing a quicker start, but it should be remembered that this may enable unwanted features.
A machine configuration is used to describe the product’s target platform the project is aimed at and is the place to add configuration options specific to the target platform. Yocto allows multiple machine configurations to be created, making it easy to support multiple target platforms.
To make use of the QEMU support provided by Yocto, include IMAGE_CLASSES += “qemuboot” in the machine config. This pulls in many defaults which can then be customised, for example, the CPU architecture we want to emulate, be it x86-64 or arm64, the amount of RAM QEMU should make available, and the network device to emulate.
The machine config is also the place to choose the Linux kernel to use. This could be one of the kernels brought in by poky, e.g. the linux-yocto-tiny kernel, a minimal, all no config kernel or the linux-yocto kernel, which provides more functionality and support for different hardware, but may contain unnecessary features as a result. Alternatively a custom kernel recipe can be created, starting from a defined kernel config, e.g. an all no config, and then providing a minimal set of Kconfig options to enable only the necessary features.
Just as important as the Linux kernel that will be available is the root file system or rootfs. A rootfs can be defined in its own recipe, specifying which software packages to include in the rootfs using the IMAGE_INSTALL or CORE_IMAGE_EXTRA_INSTALL variables, the formats in which the rootfs should be created during build with IMAGE_FSTYPES, and much more. Common packages to include in a rootfs are base-files, which contains a basic filesystem hierarchy for a Linux system, and busybox, which provides many basic utilities including a shell, file management tools, and networking tools.
High-level image features can be added through the IMAGE_FEATURES variable. These features include making the rootfs read-only with read-only-rootfs and installing debug symbols for all software packages included in the rootfs with dbg-pkgs. Image features enable extra software packages to be included in the image or set configuration options for included software packages to fulfil the feature, sometimes both. Image features can be very useful during development, especially during the early stages, but should be fully understood and well considered before being left enabled in production, especially when hardening is a concern.
Brief aside: Yocto layers
A layer in Yocto Linux typically contains instructions to build lots of different software from a variety of sources. These instructions, referred to as recipes, are used by bitbake, a make-like build tool at the heart of most Yocto project, to build software packages. A recipe includes where to obtain the source code of the software from, any dependencies it requires, as well as how to configure, compile, and package it.
A Yocto Board Support Package can be made up of multiple layers from various sources, depending on what the target hardware is, which software packages are needed, and even what tooling is required for building or testing. Layers are maintained by a variety of 3rd parties, which means that layers can be of varying quality. For example, the meta-openembedded layer is maintained by the OpenEmbedded project to a very high standard, and provides recipes for many software packages commonly required for an embedded Linux distribution.
Due to the open nature of Yocto Linux, lower quality layers also exist. This can happen when, for example, a hardware vendor wants to support a newly released board and makes the corresponding layer available but doesn’t invest the necessary time or resources to maintain this layer long term. Therefore, when choosing which layers to use in a Yocto project, the quality and maintenance should be taken into account.
The OpenEmbedded layer index is an excellent starting point for searching for layers and also individual recipes.
Building a hardened Linux distro

The definition of a hardened Linux system can vary depending on the requirements of the product, but here we will consider hardening as increasing the security of a system. This means not including any unnecessary software, as well as carefully choosing which software to include based on its complexity, quality, and security awareness, not just its offered features. Configuring the included software to only provide the necessary functionality also contributes to a hardened system. Standards for the development of secure embedded systems, such as IEC 62443, IEC 62304 and EU Cyber Resilience Act (CRA), specify concrete design requirements. A hardened Linux distribution is often a minimal one, where only the necessary features are available and nothing more.
As an example, glibc is a C library which supports a lot of use cases but that means it is large and highly complex. muslc on the other hand is more minimal, meaning it’s smaller and less complex than glibc, resulting in a smaller attack surface. The number of CVEs against glibc vs muslc, 144 vs 5 according to www.cvedetails.com, reflects the difference in size and complexity. That’s not to say muslc is better software than glibc, but when the extra functionality provided by glibc isn’t strictly necessary, muslc may be the better choice when putting together a hardened system.
When configuration options to turn on or off certain features exist for a piece of software, Yocto can make use of these to allow further hardening. Continuing with the libc example from above, if glibc is chosen as the libc, it’s very unlikely that all features offered by glibc will be needed, for example profiling can be disabled through –disable-profile, as the feature to generate profiling data is unlikely to be needed, especially in a production environment.
Another good example is the the configuration of the Linux kernel. It can be tempting to turn on many unnecessary configuration options in order to provide more functionality out of the box, such as optional hardware support or additional file systems. However, this results in more code being included in the compiled kernel unnecessarily, which in turn increases the cyber security surface of the Linux kernel.
Image contents

When an image is built, a .manifest file is created listing the packages included in the rootfs. The .manifest doesn’t include all the packages used during the build of the image, nor the locations from which the source code was fetched, among other information useful for supply chain management.
The Yocto Project provides a way to generate a Software Bill of Materials (SBOM), which, for example, can be used to meet open source compliance requirements. It does this using the SPDX standard, an open standard developed by the SPDX Project, another Linux Foundation project.
The SBOM information isn’t generated during a build by default, but can easily be enabled by adding INHERIT += “create-spdx” to local.conf or to a custom distro configuration file. This causes the build to inherit the create-spdx class and will then generate SPDX files for all recipes included in the build, as well as the generated images and runtime dependencies. If the SPDX output will be read by human eyes, also adding “SPDX_PRETTY=1” in the chosen configuration file will make the output more readable.
An SPDX file for a recipe contains the download source of the software, the version of the software, the license of the software, and the dependencies needed for the build, among other things. The dependencies listed include the compiler and other tools used during building, as well as libraries needed during build time and run time. This makes it easy to tell not only which software is included in an image, but also why.
For example, a rootfs recipe will usually just specify the software packages required to provide the functionality for the embedded system, such as graphics libraries, networking tools, or custom applications. One or more of these may rely on a compression library, such as zlib, but it would be unusual to include zlib directly in the rootfs recipe. From the generated SBOM it is possible to see which software has the dependency on zlib. Furthermore, information about zlib is also generated, so it can be seen where the zlib source code comes from, which version is used, and even if it pulls in other dependencies.
The SBOM can be used also as the starting point for an in-depth security monitoring of the embedded Linux system, including classification of and possibly fixes for vulnerabilities in constituent packages. More information on CVE Security Management and Vulnerability handling can be found on the emlix website.
Maintainable Yocto distro

There is a lot of overlap between maintainability and hardening embedded Linux systems. Both benefit from only including the minimum number of packages necessary. Both benefit from high quality software. The planning and implementation of maintainability is considered key to reducing life cycle costs (maintenance and security updates) for Yocto-based industrial products. In order for a Yocto BSP to be maintainable there are many good practices which should be followed. In this section we will highlight a few of these.
Use existing, well-maintained layers and recipes where possible. Recipes already exist for much of the software that is commonly included into an embedded Linux system. The benefit of relying on these rather than writing new recipes is that when the software changes upstream, the necessary changes to the recipe will be made by the recipe maintainers, thus reducing your maintenance burden.
Use appends files to customise existing recipes. If an existing recipe doesn’t quite fulfil the needs of the project it can be extended or modified by adding a custom .bbappend file. A Yocto recipe may by default enable all of the available features a piece of software offers, not all of them being necessary for all systems. A .bbappend file can be used to reduce this configuration to what’s strictly needed, hardening the system in the process. The version of the software which will be built can also be changed in the .bbappend file, allowing a newer or, as sometimes required, older version to be used. Using appends files rather than writing recipes from scratch allow a project to benefit from the upstream maintenance effort put into upstream recipes while still having full control over how its included packages are configured and built. Appends files may need to be updated when an upstream recipe changes, but the amount of effort required is far lower than keeping a bespoke recipe updated.
Keep custom recipes and appends in a project specific layer. This has two advantages. Keeping the changes separate from the upstream layers makes it much easier to see what the project specific behaviour is and it also means that when an update of the Yocto layers takes place, the changes don’t get lost.
Regularly syncing with upstream layers is also important for maintainability. Upstream syncs often bring fixes and additional features from the upstream maintainers and may even make custom overrides done in .bbappend files redundant. This raises the question, “How often is there a new release?”
Brief aside: Release cadence
The Yocto Project does a major release every 6 months, roughly in April and October. Each major release has an associated codename as well as a release number, e.g. gatesgarth 3.2 or kirkstone 4.0. Codenames are used because the release number may conflict with the versioning scheme from a layer, software package, or company.
Each release comes with strong testing and quality assurance guarantees, including testing on a variety of platforms, to ensure that no regressions creep in on update. All the testing is publicly visible and can also be used to validate projects with configurations not covered by the upstream testing.
The Yocto Project typically supports a major release with bug fixes and security fixes for seven months, but every fourth release is selected for long term support. These Long Term Support (LTS) releases are supported for four years. This allows for a more stable base while still receiving important fixes. The overlap between LTS releases is 2 years, more than enough time to prepare and carry out the upgrade.
The current LTS releases are kirkstone 4.0, released in 2022, and scarthgap 5.0, released in 2024.
Conclusion
The Yocto Project provides the tools and processes to create, configure, and customise an embedded Linux distribution to any end. The path there depends heavily on the requirements of the product and its normative requirements but in all cases there are commonalities and industrial best practices to follow which make the development and compliance with standards easier. In this paper a few of these have been explored, which hopefully clear up some common misconceptions about Yocto and make its use more appealing.
For a more process-focused discussion of using Yocto, refer to “Design, composition and maintenance of Yocto-based Linux systems”.
References
[1] https://docs.yoctoproject.org/singleindex.html
[2] https://docs.yoctoproject.org/dev/dev-manual/index.html
[3] https://www.youtube.com/@TheYoctoProject/videos
[4] https://docs.yoctoproject.org/dev/dev-manual/start.html
[5] https://github.com/openembedded/meta-openembedded/
[6] https://layers.openembedded.org/layerindex/branch/master/layers/
[7] https://emlix.com/en/frameworks-services/cve-security-monitoring/
[8] https://docs.yoctoproject.org/current/ref-manual/release-process.html#testing-and-quality-assurance
[9] https://emlix.com/fileadmin/emlix/download/pdf/White_Paper_Yocto_mhomuth.pdf