The CRA for Yocto teams: what the law asks, what create-spdx gives you, and what is left
A plain-English guide for teams shipping Yocto-built Linux images into the EU: the Cyber Resilience Act dates, what the create-spdx SBOM contains in each release, why its CPEs are only as good as CVE_PRODUCT, and what to do next.
By Declarable · Checked on 11 October 2026 · Guide version 1.3.1
If you build a Linux product with the Yocto Project and sell it in the EU, the Cyber Resilience Act (CRA, Regulation (EU) 2024/2847) applies to the image you ship. This guide covers what the law asks of an embedded Linux team, what the OpenEmbedded build system already produces, where that output falls short, and a short list of what to do. It is a documentation aid, not legal advice.
The dates that matter
The reporting duty in Article 14 has applied since 11 September 2026 (Article 71(2), EUR-Lex text checked 8 October 2026), and Article 69(3) extends it to products placed on the market before 11 December 2027, so an image you shipped years ago is covered today. Almost everything else, from the essential requirements to the CE marking, applies from 11 December 2027 (Article 71(2), EUR-Lex text checked 8 October 2026; the rules on notified bodies in Chapter IV have applied since 11 June 2026). Our guide "Article 14, step by step" walks the reporting clock.
What the CRA asks of an image
For a Linux image, in our reading (argument), the work concentrates in four places, each a requirement of Annex I:
- Know what is inside (Annex I, Part II(1)). Identify and document the components and vulnerabilities, "including by drawing up a software bill of materials in a commonly used and machine-readable format covering at the very least the top-level dependencies".
- Ship without known exploitable vulnerabilities (Annex I, Part I(2)(a)).
- Fix without delay (Annex I, Part II(2)), including by providing security updates.
- Distribute updates securely (Annex I, Part II(7)).
An image is a few hundred recipes, a kernel and usually a vendor's board support layer. The build system already does most of the SBOM half. The vulnerability half is where the work is.
What create-spdx gives you
The Yocto Project manual describes the SBOM it can produce as "a description of all the components you used, their licenses, their dependencies, their sources, the changes that were applied to them and the known vulnerabilities that were fixed", in SPDX. The class that writes it is create-spdx. What you get depends on your release:
| Release | On by default? | SPDX version | Image-level file |
|---|---|---|---|
| 4.0 Kirkstone | No: add INHERIT += "create-spdx" |
2.2 | IMAGE-MACHINE.rootfs.spdx.tar.zst |
| 5.0 Scarthgap | Yes (default since 4.3) | 2.2; 3.0 available but off | IMAGE-MACHINE.rootfs.spdx.tar.zst |
| 5.1 Styhead and later | Yes | 3.0 | IMAGE-MACHINE.rootfs.spdx.json |
| 6.0 Wrynose | Yes | 3.0 only; 2.2 support removed | IMAGE-MACHINE.rootfs.spdx.json |
In the SPDX 2.2 releases the image output is a compressed archive holding an index and one document per recipe; in SPDX 3.0 it is one JSON file in tmp/deploy/images/MACHINE/. Each recipe is described with its declared licence. Under SPDX 2.2 the CVEs fixed by the recipe's patches are written into the recipe's source-information field; under SPDX 3.0 the build writes VEX statements (machine-readable "this CVE is fixed or not affected" records), controlled by SPDX_INCLUDE_VEX, whose default current covers vulnerabilities not already fixed in the upstream source.
That is a real head start: a build-accurate record of every recipe, its version, its licence and its patches, without a separate tool.
Identifiers: present, but only as good as CVE_PRODUCT
The SBOM does carry CPE identifiers (the naming scheme the US National Vulnerability Database, NVD, uses to say which software a CVE affects). The create-spdx code builds a CPE 2.3 name for every recipe from two variables, CVE_PRODUCT and CVE_VERSION, and attaches it to the recipe; the SPDX 3.0 code also attaches it to each binary package the recipe produces.
The defaults decide how useful that CPE is:
CVE_PRODUCTdefaults to${BPN}, the recipe's base name. The variable glossary says that if the name "does not match the name in the NIST CVE database or matches with multiple entries in the database, the default value needs to be changed".CVE_VERSIONdefaults to${PV}, the recipe version.- When
CVE_PRODUCTnames no vendor, the vendor field of the CPE is a wildcard (oe/cve_check.py).
So a recipe called foo at version 1.2 that sets nothing gets a CPE like cpe:2.3:*:*:foo:1.2:*:*:*:*:*:*:* on Scarthgap: any vendor's product called "foo". A recipe that sets CVE_PRODUCT = "vendor:product" gets an exact one. Many recipes in openembedded-core already set it; recipes in your own layers and in vendor board support layers often do not (argument: what we see in practice, check yours). Our guide "Firmware SBOMs that actually match CVEs" explains why a wrong product name means wrong or missing matches.
Where Declarable helps
This is the gap our import closes, and it closes part of it rather than all of it. Declarable keeps one row per recipe, matched on the recipe name your SBOM already carries, and each row records the vendor and the product a vulnerability feed knows that software by: the recipe abseil-cpp carries the vendor abseil and the product common_libraries, which is not its recipe name. The pairs are read out of the layers themselves, from the same CVE_PRODUCT and CVE_VENDOR settings bitbake's own cve-check reads, across openembedded-core, meta-openembedded and the Intel, Xilinx and Freescale board support layers at pinned release branches; each row keeps the page the pair was checked against, and the date it was checked. Some rows record a product with no vendor, because the layer itself set none; for them no vendor replaces the wildcard your build wrote. So you get the translation for layers you do not control without reading each one yourself. For the kernel, the bootloader and a short list of core recipes that no layer gives a product name at all, the pair is written by hand from NVD's CPE dictionary, and the row says that is where it came from. Where your build wrote one of the product-only CPEs above, with its wildcard vendor, the row's vendor is used for the lookup and the string the document gave is kept with the component's record.
The limits, in the same breath. A recipe we hold no row for keeps whatever CPE your build already wrote. On a Scarthgap SPDX 2.2 document that is the recipe name as the product with a wildcard vendor, and it is matched: any vendor's product of that name, held to medium confidence because the vendor did not actually agree. Only a recipe whose document carries no CPE at all is left with no matchable identifier; bitbake's own default, the recipe name as the product with a wildcard vendor, is then carried beside the component as a hint for a person to confirm, and is never matched against. No vendor is invented for a product the hand-written list does not name. And an identifier only starts a lookup. It does not settle whether a match is real for the image you ship; that is the CVE_STATUS work below, and the decision and its evidence stay yours.
What it lacks
Correct identity for recipes nobody mapped. Every recipe left on the recipe-name default is a guess. For your in-house recipes there may be no NVD product at all, which is fine; for a vendored copy of a well-known library under a different recipe name, it is a missed match.
A scanner that stays put. On Scarthgap the cve-check class matches recipes against the NVD when you add INHERIT += "cve-check". In 6.0 that class was removed and replaced by sbom-cve-check, which runs Bootlin's sbom-cve-check tool on the create-spdx output. Upgrading changes your vulnerability pipeline, not only your packages.
The kernel's volume. The security manual notes that since the Linux kernel became a CVE Numbering Authority, "the number of associated CVEs has increased dramatically". It describes two scripts for this: generate-cve-exclusions.py, which writes a cve-exclusion_<version>.inc file, and improve_kernel_cve_report.py, which uses the kernel's own vulnerability repository and, per the manual, "reduces CVE false positives by 70%-80%". Without them, the kernel alone can bury every other result.
Your decisions. A match is not a verdict. The place to record one is CVE_STATUS, in the format "reason: description"; the older CVE_CHECK_IGNORE is deprecated in favour of it. Annex I, Part II(1) requires you to identify and document vulnerabilities; our reading (argument) is that the reasoning matters as much as the list.
Binary firmware. The linux-firmware recipe and vendor board support layers install binary files under their own licence references. The SBOM names the recipe that installed them; it does not tell you which supplier advisories cover each file (argument). They need a supplier, name and version written down from the vendor's release notes.
Everything outside the SBOM. Article 14 reporting, a coordinated vulnerability disclosure policy, the support period, the technical documentation and the declaration of conformity are not produced by any build.
What your SBOM does not show
The image SBOM covers "the recipes involved in the build of the image only" (Yocto manual). Three things embedded Linux teams ask about sit at its edge.
Out-of-tree kernel modules. A driver built outside the kernel source tree has its own recipe, which uses the module class (Yocto kernel development manual). So it appears in the SBOM as that recipe, with the version and licence the recipe declares. What it usually lacks is an identity a vulnerability feed knows: CVE_PRODUCT defaults to the recipe name, and a vendor's Wi-Fi or GPU driver rarely has an NVD product of that name (argument). A module that arrives as a prebuilt .ko file copied in by a recipe shows only as the recipe that copied it. For each one, write down the supplier, name and version, as for binary firmware above, and read the vendor's advisories yourself.
Kernel code your .config does not build. The kernel is one recipe with one version, so a CVE matched against that version can sit in a driver or subsystem your configuration never compiles. The kernel's .config is "the file that the OpenEmbedded build system uses to configure the kernel during the build" (Yocto kernel development manual). The security manual describes setting SPDX_INCLUDE_COMPILED_SOURCES:pn-linux-yocto = "1" so that the kernel's SPDX records the files actually compiled, which improve_kernel_cve_report.py can then use "to ignore CVEs that are not affected because the files are not compiled"; check that your release's manual documents the variable. When you decide a kernel CVE does not apply for this reason, record it in CVE_STATUS with the reason not-applicable-config, which the security manual uses for this case. Our reading (argument): keep each release's .config with its SBOM, because it is the evidence behind every one of those decisions.
The bootloader and anything built outside the image. Our reading (argument): U-Boot, Trusted Firmware-A or OP-TEE built by the same Yocto build appear only if their recipes take part in building the image, so check that yours are in the image SBOM. If any of them is built in a separate build, or supplied as a binary by the chip vendor, it is not in this image's SBOM at all. List it beside the SBOM with supplier, name and version, and keep the SBOM of the build that made it if there is one.
Does a kernel CVE start the 24-hour clock?
Not on its own. Article 14(1) requires a manufacturer to notify "any actively exploited vulnerability contained in the product with digital elements that it becomes aware of", and Article 14(2)(a) sets the early warning "without undue delay and in any event within 24 hours of the manufacturer becoming aware of it". The CRA defines an actively exploited vulnerability as "a vulnerability for which there is reliable evidence that a malicious actor has exploited it in a system without permission of the system owner" (Article 3(42)). A published CVE says a flaw exists; it says nothing about exploitation, so the hundreds of kernel CVEs a month do not each start a clock. What starts it is becoming aware of reliable evidence that a vulnerability contained in your product has been exploited. Our reading (argument): that evidence does not have to come from your own devices, so a credible report of in-the-wild exploitation of a kernel flaw in code your image builds is the moment to treat as the start; a CVE in code your .config does not build is, in our reading, not contained in your product. The Regulation does not define "becoming aware", so write down when and how you learned of each exploitation report and what you decided. Separately, every vulnerability you do ship still has to be addressed and remediated without delay, in relation to the risk it poses to your product (Annex I, Part II(2)), exploited or not. Article 14, step by step walks the reports and their deadlines.
What to do now
- Know your release's end date. The Yocto Project makes a long-term support (LTS) release every two years and supports it for four. Scarthgap is supported until April 2028 and Wrynose until April 2030; Kirkstone has reached end of life. Article 13(8) requires a support period of at least five years (shorter only where the product is expected to be in use for less), which will outlast any LTS you start on, so plan either an upgrade path or carrying patches yourself. See "Choosing and changing your support period".
- Keep the image SBOM with every release, generated by that release's build, not regenerated later.
- Audit
CVE_PRODUCTin every layer you control or ship, starting with your own and the vendor's. Setvendor:productexactly as the NVD records it, or note that the recipe has no NVD entry. - Decide your format. If you are on Scarthgap's default SPDX 2.2 and a customer follows BSI TR-03183-2, which asks for SPDX 3.0.1 or CycloneDX 1.6 or later, plan the move to SPDX 3 output or a conversion, and validate the result.
- Run a vulnerability check on every release and on a schedule, with the kernel scripts above, and record each decision in
CVE_STATUS. - List your binary firmware with supplier, name and version, beside the SBOM.
- Be ready for Article 14 now, because it already applies.
What is settled and what is not
Settled (evidence): the CRA dates, the SBOM requirement in Annex I, Part II(1), and what create-spdx produces in each release, per the Yocto documentation and the openembedded-core source cited below.
Not settled (as of 8 October 2026): no harmonised standard for the CRA is cited in the Official Journal, and no EU implementing act fixes an SBOM format. The Yocto tooling itself is moving: the 6.0 release replaced the CVE checker. We update this guide when any of these change.
Sources
- Regulation (EU) 2024/2847 (Cyber Resilience Act), Official Journal text on EUR-Lex: https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng · checked
- Yocto Project manual, creating a Software Bill of Materials (current release): https://docs.yoctoproject.org/dev-manual/sbom.html · checked
- Yocto Project manual, creating a Software Bill of Materials (5.0 Scarthgap): https://docs.yoctoproject.org/scarthgap/dev-manual/sbom.html · checked
- Yocto Project manual, creating a Software Bill of Materials (4.0 Kirkstone): https://docs.yoctoproject.org/kirkstone/dev-manual/sbom.html · checked
- Yocto Project 4.3 release notes (SPDX generation enabled by default): https://docs.yoctoproject.org/migration-guides/release-notes-4.3.html · checked
- Yocto Project 5.1 release notes (SPDX 3.0 by default): https://docs.yoctoproject.org/migration-guides/release-notes-5.1.html · checked
- Yocto Project 6.0 migration guide (SPDX 2.2 and cve-check removed): https://docs.yoctoproject.org/migration-guides/migration-6.0.html · checked
- Yocto Project variables glossary (CVE_PRODUCT, CVE_VERSION, CVE_STATUS, SPDX_INCLUDE_VEX): https://docs.yoctoproject.org/ref-manual/variables.html · checked
- Yocto Project security manual, vulnerabilities (kernel CVE scripts): https://docs.yoctoproject.org/security-manual/vulnerabilities.html · checked
- openembedded-core (scarthgap), create-spdx-2.2.bbclass: CPE external references per recipe: https://raw.githubusercontent.com/openembedded/openembedded-core/scarthgap/meta/classes/create-spdx-2.2.bbclass · checked
- openembedded-core (scarthgap), oe/cve_check.py: wildcard vendor when CVE_PRODUCT names none: https://raw.githubusercontent.com/openembedded/openembedded-core/scarthgap/meta/lib/oe/cve_check.py · checked
- openembedded-core (master), oe/spdx30_tasks.py: CPE identifiers in SPDX 3.0 output: https://raw.githubusercontent.com/openembedded/openembedded-core/master/meta/lib/oe/spdx30_tasks.py · checked
- Yocto Project releases and their support dates: https://www.yoctoproject.org/development/releases/ · checked
- Yocto Project reference manual, release process (LTS every two years, supported four): https://docs.yoctoproject.org/ref-manual/release-process.html · checked
- BSI TR-03183, Cyber Resilience Requirements (Part 2: SBOM, version 2.1.0): https://www.bsi.bund.de/dok/TR-03183 · checked
- openembedded-core (kirkstone), oe/spdx.py: SPDX_VERSION 2.2: https://raw.githubusercontent.com/openembedded/openembedded-core/kirkstone/meta/lib/oe/spdx.py · checked
- CRA Article 3(42), definition of actively exploited vulnerability (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_3 · checked
- CRA Article 14(1) and (2), reporting obligations of manufacturers (EUR-Lex): https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng#art_14 · checked
- Yocto Project kernel development manual, common tasks (out-of-tree modules, the kernel .config): https://docs.yoctoproject.org/kernel-dev/common.html · checked
Building on this platform? Places to go next:
- The Yocto page
- SBOM check: Fix a Zephyr, Yocto or ESP-IDF SBOM and see its known vulnerabilities
Declarable prepares documentation for you to review. It is not legal advice and not certification. You sign your own declaration of conformity.