From d3b28ef3762b445611b449f4bca2a81802d6a5c3 Mon Sep 17 00:00:00 2001 From: Ben Cressey Date: Wed, 25 Mar 2026 22:43:20 +0000 Subject: [PATCH 1/4] rottweiler: add BPF protect feature After locking a directory, its key is no longer present in the kernel and can no longer be unsealed from the TPM2 device. This is done to prevent further modifications to persisted settings in the datastore. A higher-level goal is to block changes to the effective settings on the running system. To accomplish this, it's necessary to make `/etc` and its child mounts read-only - so that configuration files cannot be modified directly - and to make `/.bottlerocket` read-only, so the datastore cannot be replaced by a directory with different contents. To block the filesystems from being mounted read-write again, install a BPF LSM hook that blocks remounting, unmounting, or mounting over the protected directories. Signed-off-by: Ben Cressey --- Cargo.lock | 1 + packages/rottweiler/Cargo.toml | 1 + packages/rottweiler/rottweiler.spec | 6 + sources/Cargo.lock | 261 +++++++++++++++++----- sources/Cargo.toml | 2 + sources/clarify.toml | 34 +++ sources/deny.toml | 8 +- sources/rottweiler/Cargo.toml | 2 + sources/rottweiler/README.md | 3 + sources/rottweiler/bpf/protect_dirs.bpf.c | 111 +++++++++ sources/rottweiler/bpf/vmlinux_minimal.h | 41 ++++ sources/rottweiler/build.rs | 14 ++ sources/rottweiler/src/bpf.rs | 66 ++++++ sources/rottweiler/src/directory.rs | 66 ++++++ sources/rottweiler/src/main.rs | 83 +++++++ sources/rottweiler/src/mount_point.rs | 111 ++++++++- 16 files changed, 745 insertions(+), 65 deletions(-) create mode 100644 sources/rottweiler/bpf/protect_dirs.bpf.c create mode 100644 sources/rottweiler/bpf/vmlinux_minimal.h create mode 100644 sources/rottweiler/src/bpf.rs diff --git a/Cargo.lock b/Cargo.lock index 8f81c62cf..0e4eb6824 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,7 @@ name = "rottweiler" version = "0.1.0" dependencies = [ "glibc", + "libbpf", ] [[package]] diff --git a/packages/rottweiler/Cargo.toml b/packages/rottweiler/Cargo.toml index 6177dc2bc..44783a7a5 100644 --- a/packages/rottweiler/Cargo.toml +++ b/packages/rottweiler/Cargo.toml @@ -16,3 +16,4 @@ source-groups = [ [build-dependencies] glibc = { path = "../glibc" } +libbpf = { path = "../libbpf" } diff --git a/packages/rottweiler/rottweiler.spec b/packages/rottweiler/rottweiler.spec index 0290ce327..f5089cf93 100644 --- a/packages/rottweiler/rottweiler.spec +++ b/packages/rottweiler/rottweiler.spec @@ -8,6 +8,10 @@ --build-dir %{_builddir}/sources \ --spdx --cyclonedx} +# Skip the FIPS check, which has a false positive because of a symbol that +# starts with the "ring" prefix. +%undefine cross_check_fips + Name: %{_cross_os}rottweiler Version: 0.1.0 Release: 1%{?dist} @@ -16,7 +20,9 @@ License: Apache-2.0 OR MIT URL: https://github.com/bottlerocket-os/bottlerocket BuildRequires: %{_cross_os}glibc-devel +BuildRequires: %{_cross_os}libbpf-devel Requires: %{_cross_os}cryptsetup +Requires: %{_cross_os}libbpf Requires: %{_cross_os}systemd-cryptsetup Requires: %{_cross_os}tpm2-tools diff --git a/sources/Cargo.lock b/sources/Cargo.lock index fe2fce5cf..06486bcbd 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -108,7 +108,7 @@ dependencies = [ "bytestring", "derive_more", "encoding_rs", - "foldhash 0.1.5", + "foldhash", "futures-core", "http 0.2.12", "httparse", @@ -217,7 +217,7 @@ dependencies = [ "cfg-if", "derive_more", "encoding_rs", - "foldhash 0.1.5", + "foldhash", "futures-core", "futures-util", "impl-more", @@ -411,7 +411,7 @@ dependencies = [ "libc", "log", "models", - "nix", + "nix 0.26.4", "olpc-cjson", "rand 0.8.5", "reqwest", @@ -452,7 +452,7 @@ dependencies = [ "log", "maplit", "models", - "nix", + "nix 0.26.4", "num", "rand 0.8.5", "serde", @@ -654,11 +654,11 @@ dependencies = [ [[package]] name = "aws-lc-fips-sys" -version = "0.13.12" +version = "0.13.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed8cd42adddefbdb8507fb7443fa9b666631078616b78f70ed22117b5c27d90" +checksum = "df6ea8e07e2df15b9f09f2ac5ee2977369b06d116f0c4eb5fa4ad443b73c7f53" dependencies = [ - "bindgen", + "bindgen 0.72.1", "cc", "cmake", "dunce", @@ -668,9 +668,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -680,11 +680,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -789,9 +789,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.121.0" +version = "1.119.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61948728b681f88a1e49b9500469cf9e36575a424e745e2c5a651a42386e7d9c" +checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" dependencies = [ "aws-credential-types", "aws-runtime", @@ -801,7 +801,6 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-json", - "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -979,9 +978,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.13" +version = "0.63.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23374b9170cbbcc6f5df8dc5ebb9b6c5c28a3c8f599f0e8b8b10eb6f4a5c6e74" +checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -1280,6 +1279,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.116", + "which", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -1289,13 +1311,13 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", "syn 2.0.116", ] @@ -1393,7 +1415,7 @@ dependencies = [ "bottlerocket-settings-models", "constants", "generate-readme", - "itertools", + "itertools 0.13.0", "log", "serde", "serde_json", @@ -1639,7 +1661,7 @@ name = "brush" version = "0.1.0" dependencies = [ "generate-readme", - "itertools", + "itertools 0.13.0", "pathdiff", "serde", "shlex", @@ -1786,6 +1808,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + [[package]] name = "cargo-readme" version = "3.3.1" @@ -1800,6 +1840,20 @@ dependencies = [ "toml", ] +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "cbor-diag" version = "0.1.12" @@ -2111,9 +2165,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -2126,14 +2180,15 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.9.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" dependencies = [ "crc", "digest", + "rand 0.9.2", + "regex", "rustversion", - "spin", ] [[package]] @@ -2670,12 +2725,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -3007,7 +3056,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.1.5", + "foldhash", ] [[package]] @@ -3015,11 +3064,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] [[package]] name = "headers" @@ -3476,7 +3520,7 @@ dependencies = [ "base64", "generate-readme", "log", - "nix", + "nix 0.26.4", "serde", "serde_json", "snafu", @@ -3546,6 +3590,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3617,6 +3670,54 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libbpf-cargo" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c6fbcb5088716de86d0ccbdccedc17b13e59f41a605a3274029335e71fcbb" +dependencies = [ + "anyhow", + "cargo_metadata", + "clap", + "libbpf-rs", + "libbpf-sys", + "memmap2", + "serde", + "serde_json", + "tempfile", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "libbpf-rs" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e23d252d93e246c8787198369f06806c99c5077b5295be29505295f4e5426dc4" +dependencies = [ + "bitflags 2.11.0", + "libbpf-sys", + "libc", + "vsprintf", +] + +[[package]] +name = "libbpf-sys" +version = "1.5.1+v1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912fae30b08bcbdb861d4b85bd09c05352c0ac9d7b93765ced5ca23709e7e590" +dependencies = [ + "cc", + "nix 0.30.1", + "pkg-config", +] + [[package]] name = "libc" version = "0.2.182" @@ -3750,11 +3851,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.15.5", ] [[package]] @@ -3813,6 +3914,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.7.1" @@ -3868,7 +3978,7 @@ dependencies = [ "lz4", "maplit", "models", - "nix", + "nix 0.26.4", "pentacle", "percent-encoding", "rand 0.8.5", @@ -3973,6 +4083,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -4357,6 +4479,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "pluto" version = "0.1.0" @@ -4435,7 +4563,7 @@ dependencies = [ "generate-readme", "log", "maplit", - "nix", + "nix 0.26.4", "schnauzer", "serde", "serde_json", @@ -4514,7 +4642,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.31", "socket2 0.6.2", "thiserror 2.0.18", @@ -4525,16 +4653,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", - "rustc-hash", + "rustc-hash 2.1.1", "rustls 0.23.31", "rustls-pki-types", "slab", @@ -4848,7 +4976,9 @@ dependencies = [ "generate-readme", "hex", "hkdf", - "nix", + "libbpf-cargo", + "libbpf-rs", + "nix 0.26.4", "serde", "sha2", "snafu", @@ -4887,6 +5017,12 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -5841,12 +5977,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" - [[package]] name = "spki" version = "0.6.0" @@ -6058,7 +6188,7 @@ version = "0.1.0" dependencies = [ "generate-readme", "log", - "nix", + "nix 0.26.4", "serde", "simplelog", "snafu", @@ -6078,11 +6208,11 @@ dependencies = [ "generate-readme", "handlebars", "http 0.2.12", - "itertools", + "itertools 0.13.0", "log", "maplit", "models", - "nix", + "nix 0.26.4", "schnauzer", "serde_json", "simplelog", @@ -6101,7 +6231,7 @@ dependencies = [ "fs2", "generate-readme", "log", - "nix", + "nix 0.26.4", "num-derive", "num-traits", "semver", @@ -6555,6 +6685,7 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "chrono", "matchers", "nu-ansi-term", "once_cell", @@ -6862,6 +6993,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vsprintf" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec2f81b75ca063294776b4f7e8da71d1d5ae81c2b1b149c8d89969230265d63" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -7008,7 +7149,7 @@ dependencies = [ "generate-readme", "indexmap", "log", - "nix", + "nix 0.26.4", "serde", "serde_repr", "simplelog", diff --git a/sources/Cargo.toml b/sources/Cargo.toml index 202e49ac1..8e70e654b 100644 --- a/sources/Cargo.toml +++ b/sources/Cargo.toml @@ -170,6 +170,8 @@ indexmap = "2" ipnet = "2" itertools = "0.13" lazy_static = "1" +libbpf-cargo = { version = "0.25", default-features = false } +libbpf-rs = { version = "0.25", default-features = false } libc = "0.2" log = "0.4.28" lz4 = "1" diff --git a/sources/clarify.toml b/sources/clarify.toml index 1e40c7804..ef98a7ac4 100644 --- a/sources/clarify.toml +++ b/sources/clarify.toml @@ -254,3 +254,37 @@ license-files = [ { path = "LICENSE-APACHE", hash = 0xbaee0da5 }, { path = "LICENSE-MIT", hash = 0x386ca1bc }, ] + +[clarify.libbpf-cargo] +expression = "LGPL-2.1-only OR BSD-2-Clause" +license-files = [ + { path = "LICENSE", hash = 0xa9d8035d }, + { path = "LICENSE.LGPL-2.1", hash = 0x9bb7c9c7 }, + { path = "LICENSE.BSD-2-Clause", hash = 0x783f4e3e }, +] + +[clarify.libbpf-rs] +expression = "LGPL-2.1-only OR BSD-2-Clause" +license-files = [ + { path = "LICENSE", hash = 0xa9d8035d }, + { path = "LICENSE.LGPL-2.1", hash = 0x9bb7c9c7 }, + { path = "LICENSE.BSD-2-Clause", hash = 0x783f4e3e }, +] + +[clarify.libbpf-sys] +expression = "BSD-2-Clause" +license-files = [ + { path = "LICENSE", hash = 0x35005f19 }, +] +skip-files = [ + "elfutils/COPYING", + "elfutils/COPYING-GPLV2", + "elfutils/COPYING-LGPLV3", + "elfutils/doc/COPYING-GFDL", + "elfutils/doc/man3/COPYING-GFDL", + "elfutils/doc/man7/COPYING-GFDL", + "libbpf/LICENSE", + "libbpf/LICENSE.BSD-2-Clause", + "libbpf/LICENSE.LGPL-2.1", + "zlib/LICENSE", +] diff --git a/sources/deny.toml b/sources/deny.toml index fbe4487cd..f1c83e343 100644 --- a/sources/deny.toml +++ b/sources/deny.toml @@ -7,7 +7,7 @@ confidence-threshold = 0.93 # Commented license types are allowed but not currently used allow = [ "Apache-2.0", - # "BSD-2-Clause", + "BSD-2-Clause", "BSD-3-Clause", "BSL-1.0", # "CC0-1.0", @@ -46,7 +46,7 @@ license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] multiple-versions = "deny" wildcards = "deny" -deny = [{ name = "structopt" }, { name = "clap", wrappers = ["cargo-readme"] }] +deny = [{ name = "structopt" }, { name = "clap", wrappers = ["cargo-readme", "libbpf-cargo"] }] skip = [ # `aws-sigv4 v1.2.6` depends on both `crypto-bigint v0.5.5` and `crypto-bigint v0.4.9` @@ -69,7 +69,9 @@ skip-tree = [ { name = "rand", version = "=0.8" }, # zbus introduces newer versions of crates that are either # incompatible or way ahead from the versions used in other crates - { name = "zbus", version = "=5.13.2" } + { name = "zbus", version = "=5.13.2" }, + # libbpf-sys wants a newer version of nix + { name = "nix", version = "=0.30.1" }, ] [bans.workspace-dependencies] diff --git a/sources/rottweiler/Cargo.toml b/sources/rottweiler/Cargo.toml index ac78d48d3..0592e8c2d 100644 --- a/sources/rottweiler/Cargo.toml +++ b/sources/rottweiler/Cargo.toml @@ -11,6 +11,7 @@ bottlerocket-image-features.workspace = true envy.workspace = true hex.workspace = true hkdf = { workspace = true, features = ["std"] } +libbpf-rs.workspace = true nix = { workspace = true, features = ["fs", "ioctl", "mount"] } serde = { workspace = true, features = ["derive"] } sha2.workspace = true @@ -20,3 +21,4 @@ zeroize = { workspace = true, features = ["alloc", "derive"] } [build-dependencies] generate-readme.workspace = true +libbpf-cargo.workspace = true diff --git a/sources/rottweiler/README.md b/sources/rottweiler/README.md index 596a1898b..0189951f0 100644 --- a/sources/rottweiler/README.md +++ b/sources/rottweiler/README.md @@ -27,7 +27,10 @@ interface for encrypting and managing encrypted storage resources including: - `encrypt directory ` - Encrypt a directory using fscrypt - `lock directory ` - Lock an encrypted directory (remove key) - `unlock directory ` - Unlock an encrypted directory (add key) +- `protect directory ` - Write-protect a directory using BPF LSM hooks (requires `bpf` feature) +- `unprotect directory ` - Remove write protection from a directory (requires `bpf` feature) - `check directory encrypted|unencrypted` - Check directory encryption state +- `check directory protected|unprotected` - Check directory protection state (requires `bpf` feature) #### TPM Measurement Operations - `measure settings` - Measure OS settings into PCR 8 diff --git a/sources/rottweiler/bpf/protect_dirs.bpf.c b/sources/rottweiler/bpf/protect_dirs.bpf.c new file mode 100644 index 000000000..e920e5429 --- /dev/null +++ b/sources/rottweiler/bpf/protect_dirs.bpf.c @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "vmlinux_minimal.h" +#include +#include +#include + +/* Constants not in vmlinux.h (preprocessor macros) */ +#define EPERM 1 + +#define MAX_PROTECTED_DEVICES 1024 + +/* Map of protected device IDs */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_PROTECTED_DEVICES); + __type(key, __u32); + __type(value, __s32); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} protected_devices SEC(".maps"); + +/* Prevent remounting protected filesystems */ +SEC("lsm/sb_remount") +int BPF_PROG(block_remount, struct super_block *sb, void *mnt_opts) +{ + __u32 dev = 0; + __s32 *val = NULL; + + if (!sb) + return 0; + + /* Get device ID */ + if (bpf_core_read(&dev, sizeof(dev), &sb->s_dev)) + return 0; + + /* Check if this device is protected */ + val = bpf_map_lookup_elem(&protected_devices, &dev); + if (!val) + return 0; + + bpf_printk("Blocked remount on protected device %u", dev); + return -EPERM; +} + +/* Prevent mounting over protected directories */ +SEC("lsm/move_mount") +int BPF_PROG(block_move_mount, const struct path *from_path, const struct path *to_path) +{ + struct vfsmount *mnt = NULL; + struct super_block *sb = NULL; + __u32 dev = 0; + __s32 *val = NULL; + + if (!to_path) + return 0; + + /* Get the vfsmount of the target mount point */ + if (bpf_core_read(&mnt, sizeof(mnt), &to_path->mnt)) + return 0; + if (!mnt) + return 0; + + /* Get the superblock of the target mount point */ + if (bpf_core_read(&sb, sizeof(sb), &mnt->mnt_sb)) + return 0; + if (!sb) + return 0; + + /* Get device ID of the target mount point's filesystem */ + if (bpf_core_read(&dev, sizeof(dev), &sb->s_dev)) + return 0; + + /* Check if target is on a protected device */ + val = bpf_map_lookup_elem(&protected_devices, &dev); + if (!val) + return 0; + + bpf_printk("Blocked mount over protected device %u", dev); + return -EPERM; +} + +/* Prevent unmounting protected filesystems */ +SEC("lsm/sb_umount") +int BPF_PROG(block_umount, struct vfsmount *mnt, int flags) +{ + struct super_block *sb = NULL; + __u32 dev = 0; + __s32 *val = NULL; + + if (!mnt) + return 0; + + /* Get the superblock */ + if (bpf_core_read(&sb, sizeof(sb), &mnt->mnt_sb)) + return 0; + if (!sb) + return 0; + + /* Get device ID */ + if (bpf_core_read(&dev, sizeof(dev), &sb->s_dev)) + return 0; + + /* Check if this device is protected */ + val = bpf_map_lookup_elem(&protected_devices, &dev); + if (!val) + return 0; + + bpf_printk("Blocked umount on protected device %u", dev); + return -EPERM; +} + +char _license[] SEC("license") = "GPL"; diff --git a/sources/rottweiler/bpf/vmlinux_minimal.h b/sources/rottweiler/bpf/vmlinux_minimal.h new file mode 100644 index 000000000..a9a950c22 --- /dev/null +++ b/sources/rottweiler/bpf/vmlinux_minimal.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __VMLINUX_MINIMAL_H__ +#define __VMLINUX_MINIMAL_H__ + +typedef signed char __s8; +typedef unsigned char __u8; +typedef signed short __s16; +typedef unsigned short __u16; +typedef signed int __s32; +typedef unsigned int __u32; +typedef signed long long __s64; +typedef unsigned long long __u64; + +typedef __u16 __be16; +typedef __u32 __be32; +typedef __u32 __wsum; + +enum bpf_map_type { + BPF_MAP_TYPE_HASH = 1, +}; + +typedef __u32 dev_t; + +struct block_device { + dev_t bd_dev; +} __attribute__((preserve_access_index)); + +struct super_block { + dev_t s_dev; + struct block_device *s_bdev; +} __attribute__((preserve_access_index)); + +struct vfsmount { + struct super_block *mnt_sb; +} __attribute__((preserve_access_index)); + +struct path { + struct vfsmount *mnt; +} __attribute__((preserve_access_index)); + +#endif /* __VMLINUX_MINIMAL_H__ */ diff --git a/sources/rottweiler/build.rs b/sources/rottweiler/build.rs index 5b3a661c3..8d9571022 100644 --- a/sources/rottweiler/build.rs +++ b/sources/rottweiler/build.rs @@ -1,3 +1,17 @@ +use libbpf_cargo::SkeletonBuilder; +use std::env; +use std::path::PathBuf; + fn main() { generate_readme::from_main().unwrap(); + + println!("cargo:rerun-if-changed=bpf/protect_dirs.bpf.c"); + println!("cargo:rerun-if-changed=bpf/vmlinux_minimal.h"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + SkeletonBuilder::new() + .source("bpf/protect_dirs.bpf.c") + .build_and_generate(out_dir.join("bpf.skel.rs")) + .unwrap(); } diff --git a/sources/rottweiler/src/bpf.rs b/sources/rottweiler/src/bpf.rs new file mode 100644 index 000000000..955aeeb84 --- /dev/null +++ b/sources/rottweiler/src/bpf.rs @@ -0,0 +1,66 @@ +use libbpf_rs::skel::{OpenSkel, Skel, SkelBuilder}; +use snafu::prelude::*; +use std::path::Path; + +mod skel { + include!(concat!(env!("OUT_DIR"), "/bpf.skel.rs")); +} + +type Result = std::result::Result; + +const BPF_PIN_PATH: &str = "/sys/fs/bpf/rottweiler"; +const PROTECTED_MOUNTS_MAP: &str = "protected_devices"; + +pub fn open_protected_mounts_map() -> Result { + libbpf_rs::MapHandle::from_pinned_path(Path::new(BPF_PIN_PATH).join(PROTECTED_MOUNTS_MAP)) + .with_whatever_context(|_| "failed to open pinned protected_mounts map") +} + +pub fn load_bpf() -> Result { + if Path::new(BPF_PIN_PATH).join("block_remount").exists() + && Path::new(BPF_PIN_PATH).join("block_move_mount").exists() + && Path::new(BPF_PIN_PATH).join("block_umount").exists() + { + return open_protected_mounts_map(); + } + + let skel_builder = skel::ProtectDirsSkelBuilder::default(); + let mut open_object = std::mem::MaybeUninit::uninit(); + + let mut open_skel = skel_builder + .open(&mut open_object) + .with_whatever_context(|_| "failed to open BPF skeleton")?; + + open_skel + .maps + .protected_devices + .set_pin_path(Path::new(BPF_PIN_PATH).join(PROTECTED_MOUNTS_MAP)) + .with_whatever_context(|_| "failed to set map pin path")?; + + let mut skel = open_skel + .load() + .with_whatever_context(|_| "failed to load BPF program")?; + + skel.attach() + .with_whatever_context(|_| "failed to attach BPF program")?; + + std::fs::create_dir_all(BPF_PIN_PATH) + .with_whatever_context(|_| "failed to create link directory")?; + + if let Some(ref mut link) = skel.links.block_remount { + link.pin(Path::new(BPF_PIN_PATH).join("block_remount")) + .with_whatever_context(|_| "failed to pin block_remount link")?; + } + + if let Some(ref mut link) = skel.links.block_move_mount { + link.pin(Path::new(BPF_PIN_PATH).join("block_move_mount")) + .with_whatever_context(|_| "failed to pin block_move_mount link")?; + } + + if let Some(ref mut link) = skel.links.block_umount { + link.pin(Path::new(BPF_PIN_PATH).join("block_umount")) + .with_whatever_context(|_| "failed to pin block_umount link")?; + } + + open_protected_mounts_map() +} diff --git a/sources/rottweiler/src/directory.rs b/sources/rottweiler/src/directory.rs index 2f650c57e..5f0b1c4d6 100644 --- a/sources/rottweiler/src/directory.rs +++ b/sources/rottweiler/src/directory.rs @@ -1,8 +1,10 @@ +use nix::mount::MsFlags; use snafu::{Whatever, prelude::*}; use std::path::PathBuf; use crate::fscrypt::*; use crate::key; +use crate::{bpf, mount_point::MountPoint}; type Result = std::result::Result; @@ -60,3 +62,67 @@ pub fn is_encrypted(path: PathBuf) -> Result { Err(_) => Ok(false), } } + +/// Collect all unique mount points (parents and children) for the given paths. +fn collect_mounts(paths: &[PathBuf]) -> Result> { + let mut mounts = std::collections::HashMap::new(); + + for path in paths { + let parent = MountPoint::from_path(path)?; + let children = parent.find_children()?; + + mounts.entry(parent.device_id).or_insert(parent); + for child in children { + mounts.entry(child.device_id).or_insert(child); + } + } + + Ok(mounts) +} + +/// Write-protect a directory using BPF LSM hooks +pub fn protect(paths: Vec) -> Result<()> { + let map = bpf::load_bpf()?; + let mounts = collect_mounts(&paths)?; + + for mount in mounts.values() { + // Check for inconsistent state: in map but not read-only + if mount.is_in_map(&map)? && !mount.is_readonly()? { + whatever!( + "device {} is in protected map but not read-only - inconsistent state", + mount.device_id + ); + } + + if !mount.is_in_map(&map)? { + mount.remount(MsFlags::MS_RDONLY)?; + mount.add_to_map(&map)?; + } + } + + Ok(()) +} + +/// Remove write protection from a directory +pub fn unprotect(paths: Vec) -> Result<()> { + let map = bpf::load_bpf()?; + let mounts = collect_mounts(&paths)?; + + for mount in mounts.values() { + mount.remove_from_map(&map)?; + } + + for mount in mounts.values() { + mount.remount(MsFlags::empty())?; + } + + Ok(()) +} + +/// Check if a directory is write-protected (read-only and in BPF map) +pub fn is_protected(path: PathBuf) -> Result { + let map = bpf::load_bpf()?; + let mount = MountPoint::from_path(&path)?; + + Ok(mount.is_readonly()? && mount.is_in_map(&map)?) +} diff --git a/sources/rottweiler/src/main.rs b/sources/rottweiler/src/main.rs index 4cd893dcc..62f402e17 100644 --- a/sources/rottweiler/src/main.rs +++ b/sources/rottweiler/src/main.rs @@ -24,7 +24,10 @@ interface for encrypting and managing encrypted storage resources including: - `encrypt directory ` - Encrypt a directory using fscrypt - `lock directory ` - Lock an encrypted directory (remove key) - `unlock directory ` - Unlock an encrypted directory (add key) +- `protect directory ` - Write-protect a directory using BPF LSM hooks (requires `bpf` feature) +- `unprotect directory ` - Remove write protection from a directory (requires `bpf` feature) - `check directory encrypted|unencrypted` - Check directory encryption state +- `check directory protected|unprotected` - Check directory protection state (requires `bpf` feature) ### TPM Measurement Operations - `measure settings` - Measure OS settings into PCR 8 @@ -44,6 +47,7 @@ use snafu::Whatever; use std::path::{Path, PathBuf}; mod block_device; +mod bpf; mod directory; mod fscrypt; mod key; @@ -64,6 +68,7 @@ fn main() -> Result<()> { Some("encrypt" | "attach" | "detach" | "resize" | "lock" | "unlock" | "check") => { Some(2) } + Some("protect" | "unprotect") => Some(2), _ => None, }; if let Some(pos) = resource_pos @@ -111,6 +116,12 @@ fn main() -> Result<()> { Command::Unlock(cmd) => match cmd.resource { UnlockResource::Directory(cmd) => directory::unlock(cmd.path, cmd.key_id), }, + Command::Protect(cmd) => match cmd.resource { + ProtectResource::Directory(cmd) => directory::protect(cmd.paths), + }, + Command::Unprotect(cmd) => match cmd.resource { + UnprotectResource::Directory(cmd) => directory::unprotect(cmd.paths), + }, Command::Check(cmd) => match cmd.resource { CheckResource::BlockDevice(cmd) => { let path = cmd.path; @@ -148,6 +159,20 @@ fn main() -> Result<()> { false, "encrypted", ), + CheckDirectoryState::Protected(_) => handle_check( + directory::is_protected(path.clone())?, + "directory", + &path, + true, + "protected", + ), + CheckDirectoryState::Unprotected(_) => handle_check( + directory::is_protected(path.clone())?, + "directory", + &path, + false, + "protected", + ), } } }, @@ -199,6 +224,8 @@ enum Command { Resize(ResizeCmd), Lock(LockCmd), Unlock(UnlockCmd), + Protect(ProtectCmd), + Unprotect(UnprotectCmd), Check(CheckCmd), Measure(MeasureCmd), } @@ -367,6 +394,50 @@ struct UnlockDirectoryCmd { key_id: String, } +#[derive(FromArgs)] +#[argh(subcommand, name = "protect")] +/// Protect a resource +struct ProtectCmd { + #[argh(subcommand)] + resource: ProtectResource, +} + +#[derive(FromArgs)] +#[argh(subcommand)] +enum ProtectResource { + Directory(ProtectDirectoryCmd), +} + +#[derive(FromArgs)] +#[argh(subcommand, name = "directory")] +/// Write-protect a directory using BPF LSM hooks +struct ProtectDirectoryCmd { + #[argh(positional)] + paths: Vec, +} + +#[derive(FromArgs)] +#[argh(subcommand, name = "unprotect")] +/// Unprotect a resource +struct UnprotectCmd { + #[argh(subcommand)] + resource: UnprotectResource, +} + +#[derive(FromArgs)] +#[argh(subcommand)] +enum UnprotectResource { + Directory(UnprotectDirectoryCmd), +} + +#[derive(FromArgs)] +#[argh(subcommand, name = "directory")] +/// Remove write protection from a directory +struct UnprotectDirectoryCmd { + #[argh(positional)] + paths: Vec, +} + #[derive(FromArgs)] #[argh(subcommand, name = "check")] /// Check resource state @@ -426,6 +497,8 @@ struct CheckDirectoryCmd { enum CheckDirectoryState { Encrypted(CheckDirectoryEncryptedCmd), Unencrypted(CheckDirectoryUnencryptedCmd), + Protected(CheckDirectoryProtectedCmd), + Unprotected(CheckDirectoryUnprotectedCmd), } #[derive(FromArgs)] @@ -438,6 +511,16 @@ struct CheckDirectoryEncryptedCmd {} /// Check if unencrypted struct CheckDirectoryUnencryptedCmd {} +#[derive(FromArgs)] +#[argh(subcommand, name = "protected")] +/// Check if protected +struct CheckDirectoryProtectedCmd {} + +#[derive(FromArgs)] +#[argh(subcommand, name = "unprotected")] +/// Check if unprotected +struct CheckDirectoryUnprotectedCmd {} + #[derive(FromArgs)] #[argh(subcommand, name = "measure")] /// Measure data into TPM PCR diff --git a/sources/rottweiler/src/mount_point.rs b/sources/rottweiler/src/mount_point.rs index 3b8487a8b..083713f00 100644 --- a/sources/rottweiler/src/mount_point.rs +++ b/sources/rottweiler/src/mount_point.rs @@ -1,12 +1,31 @@ +use libbpf_rs::MapCore; +use nix::mount::{MsFlags, mount}; +use nix::sys::statvfs::{FsFlags, statvfs}; use snafu::prelude::*; +use std::collections::HashSet; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; +use walkdir::WalkDir; type Result = std::result::Result; -/// Represents a filesystem mount point with its path +const NONE: Option<&'static [u8]> = None; +const PROTECTED: i32 = 1; + +/// Convert device number from stat encoding to kernel encoding +/// +/// stat() returns dev_t as u64 using old encoding: (major & 0xff) << 8 | (minor & 0xff) | ((minor & 0xfff00) << 12) +/// kernel uses u32: (major & 0xfff) << 20 | (minor & 0xfffff) +fn dev_to_kernel_encoding(stat_dev: u64) -> u32 { + let major = ((stat_dev >> 8) & 0xff) | ((stat_dev >> 32) & 0xfffff00); + let minor = (stat_dev & 0xff) | ((stat_dev >> 12) & 0xffffff00); + ((major << 20) | minor) as u32 +} + +/// Represents a filesystem mount point with its path and device ID pub struct MountPoint { pub path: PathBuf, + pub device_id: u32, } impl MountPoint { @@ -38,7 +57,10 @@ impl MountPoint { current = parent.to_path_buf(); } - Ok(Self { path: current }) + Ok(Self { + path: current.clone(), + device_id: dev_to_kernel_encoding(current_dev), + }) } /// Open the mount point path as a file descriptor @@ -47,4 +69,89 @@ impl MountPoint { format!("failed to open mount point '{}'", self.path.display()) }) } + + /// Find all child mount points within this mount point's directory tree + pub fn find_children(&self) -> Result> { + let mut child_mounts = Vec::new(); + let mut seen_devices = HashSet::new(); + seen_devices.insert(self.device_id); + + for entry in WalkDir::new(&self.path).into_iter().filter_map(|e| e.ok()) { + if !entry.file_type().is_dir() { + continue; + } + + let entry_dev = dev_to_kernel_encoding( + entry + .metadata() + .with_whatever_context(|_| { + format!("failed to get metadata for '{}'", entry.path().display()) + })? + .dev(), + ); + + if !seen_devices.contains(&entry_dev) { + seen_devices.insert(entry_dev); + child_mounts.push(Self { + path: entry.path().to_path_buf(), + device_id: entry_dev, + }); + } + } + + Ok(child_mounts) + } + + /// Remount the filesystem with the specified mount flags + pub fn remount(&self, flags: MsFlags) -> Result<()> { + mount(NONE, &self.path, NONE, MsFlags::MS_REMOUNT | flags, NONE) + .with_whatever_context(|_| format!("failed to remount '{}'", self.path.display())) + } + + /// Add this mount point's device ID to the BPF protected_mounts map + pub fn add_to_map(&self, map: &libbpf_rs::MapHandle) -> Result<()> { + map.update( + &self.device_id.to_ne_bytes(), + &PROTECTED.to_ne_bytes(), + libbpf_rs::MapFlags::ANY, + ) + .with_whatever_context(|_| { + format!( + "failed to add device {} to protected_mounts map", + self.device_id + ) + }) + } + + /// Remove this mount point's device ID from the BPF protected_mounts map + pub fn remove_from_map(&self, map: &libbpf_rs::MapHandle) -> Result<()> { + map.delete(&self.device_id.to_ne_bytes()) + .with_whatever_context(|_| { + format!( + "failed to remove device {} from protected_mounts map", + self.device_id + ) + }) + } + + /// Check if this mount point's device ID is in the BPF protected_mounts map + pub fn is_in_map(&self, map: &libbpf_rs::MapHandle) -> Result { + match map.lookup(&self.device_id.to_ne_bytes(), libbpf_rs::MapFlags::ANY) { + Ok(Some(_)) => Ok(true), + Ok(None) => Ok(false), + Err(e) => whatever!( + "failed to check if device {} is in map: {}", + self.device_id, + e + ), + } + } + + /// Check if this mount point is mounted read-only + pub fn is_readonly(&self) -> Result { + let stat = statvfs(&self.path) + .with_whatever_context(|_| format!("failed to statvfs '{}'", self.path.display()))?; + + Ok(stat.flags().contains(FsFlags::ST_RDONLY)) + } } From d936956592e5b3ad181ed70f9f1237b4f37f0a5b Mon Sep 17 00:00:00 2001 From: Ben Cressey Date: Wed, 25 Mar 2026 22:30:49 +0000 Subject: [PATCH 2/4] apiserver: protect datastore and /etc during lockdown Add rottweiler protect command to lockdown sequence to apply BPF LSM write protection to /.bottlerocket/datastore and /etc before locking the datastore encryption key. Signed-off-by: Ben Cressey --- sources/api/apiserver/src/server/mod.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/sources/api/apiserver/src/server/mod.rs b/sources/api/apiserver/src/server/mod.rs index 65d238edd..79a935b2a 100644 --- a/sources/api/apiserver/src/server/mod.rs +++ b/sources/api/apiserver/src/server/mod.rs @@ -640,12 +640,21 @@ async fn deactivate_update() -> Result { /// Locks down the machine async fn lockdown() -> Result { debug!("Locking down now"); - let commands = vec![vec![ - "rottweiler", - "lock", - "directory", - "/.bottlerocket/datastore", - ]]; + let commands = vec![ + vec![ + "rottweiler", + "protect", + "directory", + "/.bottlerocket/datastore", + "/etc", + ], + vec![ + "rottweiler", + "lock", + "directory", + "/.bottlerocket/datastore", + ], + ]; for args in commands { let output = Command::new(args[0]) From a19b7ab9196b29019a4a67fb0a9ac2ed881b5423 Mon Sep 17 00:00:00 2001 From: Ben Cressey Date: Wed, 25 Mar 2026 22:29:44 +0000 Subject: [PATCH 3/4] selinux-policy: set label for rottweiler's BPF maps Label rottweiler's BPF maps with "private_t" so that only API-related processes can alter them. Otherwise, entries could be removed from the "protected_devices" map to bypass the intended protection. Signed-off-by: Ben Cressey --- packages/selinux-policy/fs.cil | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/selinux-policy/fs.cil b/packages/selinux-policy/fs.cil index 89f78af0a..52d2065f6 100644 --- a/packages/selinux-policy/fs.cil +++ b/packages/selinux-policy/fs.cil @@ -22,6 +22,7 @@ (genfscon bdev / any) (genfscon binfmt_misc / binfmt_misc_fs) (genfscon bpf / any) +(genfscon bpf /rottweiler private) (genfscon cgroup / any) (genfscon cgroup2 / any) (genfscon debugfs / any) From 514c3d9147fc0617037af9ad0aac85b893429c64 Mon Sep 17 00:00:00 2001 From: Ben Cressey Date: Thu, 26 Mar 2026 23:38:40 +0000 Subject: [PATCH 4/4] workflows: install libbpf for rottweiler When running `cargo` builds outside the SDK, e.g. to run unit tests, rottweiler needs libbpf available. Signed-off-by: Ben Cressey --- .github/actions/setup-node/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 501e8721f..62f0da574 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -3,7 +3,7 @@ description: "Performs setup for caching and other common needs." runs: using: "composite" steps: - - run: sudo apt -y install build-essential openssl libssl-dev pkg-config liblz4-tool clang + - run: sudo apt -y install build-essential openssl libbpf-dev libelf-dev libssl-dev pkg-config liblz4-tool clang shell: bash - uses: actions/cache/restore@v4 # Restore most recent cache if available.