Two‑Phase eBPF Program Signing: Bridging Compilation and Load‑Time Integrity
Combining original and post‑load signatures for end‑to‑end code integrity
Truly secure deployments use code signing, which both verifies the binary’s author and ensures it hasn’t been tampered with.
But with eBPF, traditional code signing simply won’t work.
eBPF loaders like libbpf perform on‑the‑fly modifications of the compiled binary, which breaks any naïve signature scheme.
To address this, we introduce a two‑phase signing model—vendor‑signed binary before libbpf and operator‑signed binary after—providing an unbroken chain of trust.
🚀 Special thanks to Cong Wang, Entrepreneur and Angel Investor, for putting together this deep and practical guest post for eBPFChirp!
Traditionally, when you want to verify the authenticity and integrity of the program, you:
compute its cryptographic hash (e.g., SHA‑256) and compare it against the official checksum provided by the vendor, or
verify its digital signature using the vendor’s public key
But this approach breaks for eBPF due to several reasons.
When we compile an eBPF program, the resulting binary isn’t in its final form.
The eBPF loader like libbpf needs to modify this binary before it can run in the kernel, including:
Updating map FDs: During load, libbpf replaces the compiler’s placeholder map descriptors with the actual file descriptors of the newly created maps, ensuring your BPF code can correctly reference them at runtime.
Patching CO-RE relocations: It walks through the relocation entries in the eBPF object, resolving references to maps, helper functions, and global symbols by writing their real kernel addresses into the program.
Other runtime tweaks: libbpf also recalculates section sizes and instruction offsets, applies any kernel-version–specific instruction fixes, and attaches the program to its target (such as a network interface or a tracepoint) with the proper parameters.
This creates a classic catch-22 situation:
If you sign the original binary, libbpf’s necessary modifications will break the signature.
If you wait to sign until after loading, you can’t prove the program you’re gonna run is the unaltered, vendor‑supplied artifact.
Earlier efforts in the kernel community tried conventional code‑signing schemes for eBPF programs.
Other proposals even suggested moving the entire loader into the kernel, so that relocations, map creation, and all other preparatory steps happen in one trusted boundary.
So why it didn’t work?
This comes down to several reasons, including:
Expanded Privileged Code: Embedding a full-featured loader in the kernel inflates the amount of complex, privileged code, broadening the attack surface and heightening the risk of security vulnerabilities.
Compatibility Issues: While BTF provides kernel version independence for CO-RE-enabled programs, an in-kernel loader would still face compatibility challenges:
Legacy programs without BTF support require kernel-specific adjustments
Different kernel versions may require different approaches to map creation and program attachment
Programs using newer features need fallback paths for older kernels
Kernel ABI stability would become more critical as loader logic moves into the kernel
Flexibility Issues: User-space loading provides significantly more flexibility than a kernel-space approach would allow:
New program types and features can be supported without kernel updates
Programs can be preprocessed or transformed based on runtime conditions
Debug information and symbols can be handled more freely
Verification Complexity: The eBPF verifier would need to be substantially modified to verify the loader’s operations, making an already complex component even more complicated and potentially introducing new verification bypasses.
So how can we address this?
To address this dilemma, a two-phase signing approach was developed that mirrors the eBPF program’s preparation and loading process.
Think of it like a legal document that requires both initial notarization and subsequent verification of modifications.
Phase 1: The Baseline Signature
The first phase occurs when the eBPF program is initially compiled:
A PKCS#7 signature is generated for the original, unmodified program
This signature serves as proof that the original program came from a trusted source
It’s analogous to getting a document notarized before filling in the details.
Phase 2: The Modified Program Signature
The second phase happens on the operator side (whoever is trying to run the eBPF binary) after libbpf has made its necessary modifications:
A new signature is created covering both the modified program and its original signature
This establishes a chain of trust
It proves that the modifications were authorized and applied to legitimate code
The Verification Process
The kernel verifies these signatures in sequence during program loading.
First, it verifies the original program against its baseline signature.
Then, it verifies the secondary signature covering both the modified program and the original signature.
This two-step verification ensures:
The program originated from a trusted source
Any modifications were authorized
The chain of trust remains unbroken
While this might sound trivial at first, but this approach offers several advantages:
No Kernel Modifications Required: Built entirely on existing eBPF infrastructure, leveraging the standard BPF LSM
bpf()
syscall hook along with thebpf_lookup_user_key()
andbpf_verify_pkcs7_signature()
kfuncs; requires only transparent, minimally invasive changes to libbpf and remains fully compatible with existing tooling and workflows.Strong Auditability: Every failure is attributed precisely—either to the original program or to post‑compilation modifications—creating clear, actionable audit trails for security investigations.
Practical Security: Accommodates necessary runtime tweaks without weakening protections, thwarts signature‑stripping attacks, and forges a verifiable link between the vendor‑supplied code and any loader modifications.
The implementation is currently a proof of concept and not yet suitable for production use. It works using:
PKCS#7 signatures for both phases
BPF LSM hooks to intercept program loading
Standard cryptographic primitives from OpenSSL
Leverage the existing Linux kernel keyring infrastructure
For those interested in implementation details or contributing to the project, you can find the implementation at the congwang/ebpf-2-phase-singing GitHub Repository.
I hope you find this resource helpful. Keep an eye out for more updates and developments in eBPF in next week's newsletter.
Until then, keep 🐝-ing!
Warm regards, Teodor and Cong