From 4f4cd1a1d79b6f047d059e508348fac859055287 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 11 Jun 2026 13:49:09 -0700 Subject: [PATCH 1/6] X-Smart-Branch-Parent: master From 0bd2dd64a1d9a47b9e313c703134ab820a39b5b2 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 11 Jun 2026 13:50:49 -0700 Subject: [PATCH 2/6] Drop unnecessary capabilities after BPF initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After BPF programs are loaded and attached, collector no longer needs CAP_BPF, CAP_PERFMON, or CAP_SYS_RESOURCE. Drop all capabilities except CAP_SYS_PTRACE (needed for ongoing /proc scraping) immediately after InitKernel() succeeds. On kernels < 5.8 (RHEL 8), also keep CAP_SYS_ADMIN since the BPF subsystem may check it on runtime operations. This makes collector self-hardening regardless of how it is deployed — even with privileged:true, the process capability set is minimized before entering the event loop. libcap-ng was already linked but unused; this is the first actual use. --- collector/collector.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/collector/collector.cpp b/collector/collector.cpp index e9ff40bd9b..bce94a409a 100644 --- a/collector/collector.cpp +++ b/collector/collector.cpp @@ -120,6 +120,26 @@ void initialChecks() { } } +void DropCapabilities() { + auto kv = HostInfo::Instance().GetKernelVersion(); + bool has_discrete_bpf = (kv.kernel > 5) || (kv.kernel == 5 && kv.major >= 8); + + capng_clear(CAPNG_SELECT_CAPS); + capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_SYS_PTRACE, -1); + + if (!has_discrete_bpf) { + capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_SYS_ADMIN, -1); + CLOG(INFO) << "Kernel " << kv.release << " lacks discrete CAP_BPF, keeping CAP_SYS_ADMIN"; + } + + if (capng_apply(CAPNG_SELECT_CAPS) < 0) { + CLOG(WARNING) << "Failed to drop capabilities"; + } else { + CLOG(INFO) << "Dropped capabilities after BPF initialization, keeping CAP_SYS_PTRACE" + << (has_discrete_bpf ? "" : " and CAP_SYS_ADMIN"); + } +} + void RunService(CollectorConfig& config) { auto& startup_diagnostics = StartupDiagnostics::GetInstance(); CollectorService collector(config, &g_control, &g_signum); @@ -134,6 +154,8 @@ void RunService(CollectorConfig& config) { startup_diagnostics.Log(); + DropCapabilities(); + collector.RunForever(); } From 36d48ebf05e204c9c60e09a8564a78b10d7936eb Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 11 Jun 2026 14:15:36 -0700 Subject: [PATCH 3/6] Fix capng_type_t enum cast for C++ strict typing --- collector/collector.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/collector/collector.cpp b/collector/collector.cpp index bce94a409a..c07c32a192 100644 --- a/collector/collector.cpp +++ b/collector/collector.cpp @@ -125,10 +125,10 @@ void DropCapabilities() { bool has_discrete_bpf = (kv.kernel > 5) || (kv.kernel == 5 && kv.major >= 8); capng_clear(CAPNG_SELECT_CAPS); - capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_SYS_PTRACE, -1); + capng_updatev(CAPNG_ADD, static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED), CAP_SYS_PTRACE, -1); if (!has_discrete_bpf) { - capng_updatev(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_SYS_ADMIN, -1); + capng_updatev(CAPNG_ADD, static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED), CAP_SYS_ADMIN, -1); CLOG(INFO) << "Kernel " << kv.release << " lacks discrete CAP_BPF, keeping CAP_SYS_ADMIN"; } From e06443e58849980de429a0bb36e51ac6a2a97cca Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 11 Jun 2026 14:30:20 -0700 Subject: [PATCH 4/6] Keep CAP_BPF and CAP_PERFMON at runtime for safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code audit of falcosecurity-libs found two runtime paths that may need BPF capabilities: 1. bpf_map_lookup_elem() in pman_get_scap_stats() — called periodically for metrics collection via CollectorStatsExporter 2. sinsp::restart_capture() — can re-attach BPF programs via bpf_program__attach() on SCAP_UNEXPECTED_BLOCK events Drop CAP_SYS_RESOURCE (only needed for RLIMIT_MEMLOCK at init) and all other capabilities not in the keep list. On kernel >= 5.8 keep SYS_PTRACE + BPF + PERFMON; on older kernels keep SYS_PTRACE + SYS_ADMIN. This still drops ~38 of 41 capabilities on modern kernels, eliminating NET_RAW, SYS_MODULE, DAC_OVERRIDE, CHOWN, FOWNER, MKNOD, SYS_RAWIO, and all other unneeded privileges. --- collector/collector.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/collector/collector.cpp b/collector/collector.cpp index c07c32a192..f81c2544e8 100644 --- a/collector/collector.cpp +++ b/collector/collector.cpp @@ -125,18 +125,28 @@ void DropCapabilities() { bool has_discrete_bpf = (kv.kernel > 5) || (kv.kernel == 5 && kv.major >= 8); capng_clear(CAPNG_SELECT_CAPS); - capng_updatev(CAPNG_ADD, static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED), CAP_SYS_PTRACE, -1); - if (!has_discrete_bpf) { - capng_updatev(CAPNG_ADD, static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED), CAP_SYS_ADMIN, -1); - CLOG(INFO) << "Kernel " << kv.release << " lacks discrete CAP_BPF, keeping CAP_SYS_ADMIN"; + auto caps = static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED); + + // CAP_SYS_PTRACE: ongoing /proc// reads across namespaces + capng_updatev(CAPNG_ADD, caps, CAP_SYS_PTRACE, -1); + + if (has_discrete_bpf) { + // CAP_BPF: runtime BPF map lookups (stats) and potential capture restart + // CAP_PERFMON: BPF program re-attachment on capture restart + capng_updatev(CAPNG_ADD, caps, CAP_BPF, -1); + capng_updatev(CAPNG_ADD, caps, CAP_PERFMON, -1); + } else { + // On kernels < 5.8, CAP_SYS_ADMIN covers BPF and perf operations + capng_updatev(CAPNG_ADD, caps, CAP_SYS_ADMIN, -1); } if (capng_apply(CAPNG_SELECT_CAPS) < 0) { CLOG(WARNING) << "Failed to drop capabilities"; + } else if (has_discrete_bpf) { + CLOG(INFO) << "Dropped capabilities, keeping CAP_SYS_PTRACE, CAP_BPF, CAP_PERFMON"; } else { - CLOG(INFO) << "Dropped capabilities after BPF initialization, keeping CAP_SYS_PTRACE" - << (has_discrete_bpf ? "" : " and CAP_SYS_ADMIN"); + CLOG(INFO) << "Dropped capabilities, keeping CAP_SYS_PTRACE, CAP_SYS_ADMIN"; } } From cb7895ce98ffe734fb5b159000378f02c8908c67 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 11 Jun 2026 14:43:00 -0700 Subject: [PATCH 5/6] Modular per-thread capability dropping Add DropCapabilities() utility in DropCapabilities.h and apply it at each thread entry point with only the capabilities that thread needs: - Main thread (event loop): BPF, PERFMON, SYS_PTRACE - CollectorStatsExporter: BPF (map lookups for metrics) - NetworkStatusNotifier: SYS_PTRACE (/proc reads) - SignalServiceClient: none (gRPC only) - ConfigLoader: none (inotify + yaml only) Uses CAPNG_SELECT_ALL to also clear the bounding set, preventing capability regain via execve. Based on prior work by Dmitrii Dolgov in PR #1908. --- collector/collector.cpp | 46 ++++++++---------------- collector/lib/CollectorStatsExporter.cpp | 3 ++ collector/lib/ConfigLoader.cpp | 2 ++ collector/lib/DropCapabilities.h | 32 +++++++++++++++++ collector/lib/NetworkStatusNotifier.cpp | 2 ++ collector/lib/SignalServiceClient.cpp | 2 ++ 6 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 collector/lib/DropCapabilities.h diff --git a/collector/collector.cpp b/collector/collector.cpp index f81c2544e8..912d3941a6 100644 --- a/collector/collector.cpp +++ b/collector/collector.cpp @@ -34,6 +34,7 @@ extern "C" { #include "CollectorVersion.h" #include "Control.h" #include "Diagnostics.h" +#include "DropCapabilities.h" #include "EventNames.h" #include "FileSystem.h" #include "GRPC.h" @@ -120,36 +121,6 @@ void initialChecks() { } } -void DropCapabilities() { - auto kv = HostInfo::Instance().GetKernelVersion(); - bool has_discrete_bpf = (kv.kernel > 5) || (kv.kernel == 5 && kv.major >= 8); - - capng_clear(CAPNG_SELECT_CAPS); - - auto caps = static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED); - - // CAP_SYS_PTRACE: ongoing /proc// reads across namespaces - capng_updatev(CAPNG_ADD, caps, CAP_SYS_PTRACE, -1); - - if (has_discrete_bpf) { - // CAP_BPF: runtime BPF map lookups (stats) and potential capture restart - // CAP_PERFMON: BPF program re-attachment on capture restart - capng_updatev(CAPNG_ADD, caps, CAP_BPF, -1); - capng_updatev(CAPNG_ADD, caps, CAP_PERFMON, -1); - } else { - // On kernels < 5.8, CAP_SYS_ADMIN covers BPF and perf operations - capng_updatev(CAPNG_ADD, caps, CAP_SYS_ADMIN, -1); - } - - if (capng_apply(CAPNG_SELECT_CAPS) < 0) { - CLOG(WARNING) << "Failed to drop capabilities"; - } else if (has_discrete_bpf) { - CLOG(INFO) << "Dropped capabilities, keeping CAP_SYS_PTRACE, CAP_BPF, CAP_PERFMON"; - } else { - CLOG(INFO) << "Dropped capabilities, keeping CAP_SYS_PTRACE, CAP_SYS_ADMIN"; - } -} - void RunService(CollectorConfig& config) { auto& startup_diagnostics = StartupDiagnostics::GetInstance(); CollectorService collector(config, &g_control, &g_signum); @@ -164,7 +135,20 @@ void RunService(CollectorConfig& config) { startup_diagnostics.Log(); - DropCapabilities(); + // Drop capabilities no longer needed after BPF initialization. + // The main thread keeps BPF + PERFMON (runtime map lookups, potential + // capture restart) and SYS_PTRACE (/proc reads). Individual worker + // threads drop further in their own entry points. + auto kv = HostInfo::Instance().GetKernelVersion(); + bool has_discrete_bpf = (kv.kernel > 5) || (kv.kernel == 5 && kv.major >= 8); + + if (has_discrete_bpf) { + DropCapabilities({CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE}); + CLOG(INFO) << "Dropped capabilities, keeping CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE"; + } else { + DropCapabilities({CAP_SYS_ADMIN, CAP_SYS_PTRACE}); + CLOG(INFO) << "Kernel " << kv.release << " lacks discrete CAP_BPF, keeping CAP_SYS_ADMIN, CAP_SYS_PTRACE"; + } collector.RunForever(); } diff --git a/collector/lib/CollectorStatsExporter.cpp b/collector/lib/CollectorStatsExporter.cpp index ee628c8eb6..9ed0b3bfed 100644 --- a/collector/lib/CollectorStatsExporter.cpp +++ b/collector/lib/CollectorStatsExporter.cpp @@ -5,6 +5,7 @@ #include #include "Containers.h" +#include "DropCapabilities.h" #include "EventNames.h" #include "Logging.h" #include "Utility.h" @@ -46,6 +47,8 @@ class CollectorTimerGauge { }; void CollectorStatsExporter::run() { + collector::DropCapabilities({CAP_BPF}); + auto& collectorEventCounters = prometheus::BuildGauge() .Name("rox_collector_events") .Help("Collector events") diff --git a/collector/lib/ConfigLoader.cpp b/collector/lib/ConfigLoader.cpp index 199792b47b..d3d8b23720 100644 --- a/collector/lib/ConfigLoader.cpp +++ b/collector/lib/ConfigLoader.cpp @@ -6,6 +6,7 @@ #include "internalapi/sensor/collector.pb.h" +#include "DropCapabilities.h" #include "EnvVar.h" #include "Logging.h" @@ -527,6 +528,7 @@ sensor::CollectorConfig ConfigLoader::NewRuntimeConfig() { } void ConfigLoader::WatchFile() { + DropCapabilities({}); const auto& file = parser_.GetFile(); if (!inotify_.IsValid()) { diff --git a/collector/lib/DropCapabilities.h b/collector/lib/DropCapabilities.h new file mode 100644 index 0000000000..55cf27a608 --- /dev/null +++ b/collector/lib/DropCapabilities.h @@ -0,0 +1,32 @@ +#ifndef _DROP_CAPABILITIES_H_ +#define _DROP_CAPABILITIES_H_ + +#include + +extern "C" { +#include +} + +#include "Logging.h" + +namespace collector { + +// Drop all Linux capabilities except those specified. +// Clears the bounding set too, preventing regain via execve. +// Logs the result but does not abort on failure. +inline void DropCapabilities(std::initializer_list keep) { + capng_clear(CAPNG_SELECT_ALL); + + auto caps = static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED); + for (auto cap : keep) { + capng_update(CAPNG_ADD, caps, cap); + } + + if (capng_apply(CAPNG_SELECT_ALL) != 0) { + CLOG(WARNING) << "Failed to drop capabilities"; + } +} + +} // namespace collector + +#endif // _DROP_CAPABILITIES_H_ diff --git a/collector/lib/NetworkStatusNotifier.cpp b/collector/lib/NetworkStatusNotifier.cpp index 760a431101..b208b09c22 100644 --- a/collector/lib/NetworkStatusNotifier.cpp +++ b/collector/lib/NetworkStatusNotifier.cpp @@ -3,6 +3,7 @@ #include #include "CollectorStats.h" +#include "DropCapabilities.h" #include "DuplexGRPC.h" #include "GRPCUtil.h" #include "Logging.h" @@ -110,6 +111,7 @@ void NetworkStatusNotifier::ReceiveIPNetworks(const sensor::IPNetworkList& netwo } void NetworkStatusNotifier::Run() { + DropCapabilities({CAP_SYS_PTRACE}); Profiler::RegisterCPUThread(); auto next_attempt = std::chrono::system_clock::now(); diff --git a/collector/lib/SignalServiceClient.cpp b/collector/lib/SignalServiceClient.cpp index 18a03b016a..c7b36cc175 100644 --- a/collector/lib/SignalServiceClient.cpp +++ b/collector/lib/SignalServiceClient.cpp @@ -2,6 +2,7 @@ #include +#include "DropCapabilities.h" #include "GRPCUtil.h" #include "Logging.h" #include "ProtoUtil.h" @@ -43,6 +44,7 @@ bool SignalServiceClient::EstablishGRPCStreamSingle() { } void SignalServiceClient::EstablishGRPCStream() { + DropCapabilities({}); while (EstablishGRPCStreamSingle()); CLOG(INFO) << "Signal service client terminating."; } From 47fe2e9bef92b69f0b56f91e2aa48088e13673d2 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Thu, 11 Jun 2026 15:21:06 -0700 Subject: [PATCH 6/6] Fix worker thread cap drops failing due to lost CAP_SETPCAP The main thread drops the bounding set (CAPNG_SELECT_ALL) which requires CAP_SETPCAP. After that, worker threads can no longer modify the bounding set. Fix by having only the main thread clear the bounding set (clear_bounding=true) while worker threads only modify effective + permitted (CAPNG_SELECT_CAPS). --- collector/collector.cpp | 4 ++-- collector/lib/DropCapabilities.h | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/collector/collector.cpp b/collector/collector.cpp index 912d3941a6..4e12e9bc87 100644 --- a/collector/collector.cpp +++ b/collector/collector.cpp @@ -143,10 +143,10 @@ void RunService(CollectorConfig& config) { bool has_discrete_bpf = (kv.kernel > 5) || (kv.kernel == 5 && kv.major >= 8); if (has_discrete_bpf) { - DropCapabilities({CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE}); + DropCapabilities({CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE}, true); CLOG(INFO) << "Dropped capabilities, keeping CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE"; } else { - DropCapabilities({CAP_SYS_ADMIN, CAP_SYS_PTRACE}); + DropCapabilities({CAP_SYS_ADMIN, CAP_SYS_PTRACE}, true); CLOG(INFO) << "Kernel " << kv.release << " lacks discrete CAP_BPF, keeping CAP_SYS_ADMIN, CAP_SYS_PTRACE"; } diff --git a/collector/lib/DropCapabilities.h b/collector/lib/DropCapabilities.h index 55cf27a608..dfec7b8d70 100644 --- a/collector/lib/DropCapabilities.h +++ b/collector/lib/DropCapabilities.h @@ -12,17 +12,20 @@ extern "C" { namespace collector { // Drop all Linux capabilities except those specified. -// Clears the bounding set too, preventing regain via execve. +// If clear_bounding is true, also clears the bounding set (requires +// CAP_SETPCAP — use only on the first drop before other caps are lost). // Logs the result but does not abort on failure. -inline void DropCapabilities(std::initializer_list keep) { - capng_clear(CAPNG_SELECT_ALL); +inline void DropCapabilities(std::initializer_list keep, + bool clear_bounding = false) { + auto scope = clear_bounding ? CAPNG_SELECT_ALL : CAPNG_SELECT_CAPS; + capng_clear(scope); auto caps = static_cast(CAPNG_EFFECTIVE | CAPNG_PERMITTED); for (auto cap : keep) { capng_update(CAPNG_ADD, caps, cap); } - if (capng_apply(CAPNG_SELECT_ALL) != 0) { + if (capng_apply(scope) != 0) { CLOG(WARNING) << "Failed to drop capabilities"; } }