TL;DR: eBPF is perfect for watching and shaping packets, but when you need to create, sign, and protect the identities behind them, you need the kernel itself.
Whenever we explain Riptides to customers, at conferences or meetups, there’s always one question: “Why a kernel module instead of eBPF?”
It’s a fair one, as eBPF has transformed observability and network control in Linux. It’s safe, flexible, and powerful for tracing, filtering, and enforcing lightweight policies. But when it comes to cryptography, identity, and deep kernel integration, eBPF’s sandboxed nature becomes a limiting factor. This post breaks down that choice - why Riptides runs a kernel module instead of eBPF, what that enables us to do, and what eBPF’s sandboxed model still can’t. We’ll look at concrete use cases as key generation, TLS orchestration, credential injection, and just-in-time credential delivery and show where eBPF reaches its limits.
Our kernel module operates at the TCP layer and intercepts incoming/outgoing connections. It issues ephemeral X.509 certificates and binds those keys/certs to the process that initiated the connection. On a new TCP connection the driver:
Since enterprises today are tightly coupled with cloud providers, external federation—enabling systems to trust identities without long-lived secrets—is critical. We’ve written why cloud-native federation isn’t enough for non-human identities, but in short: workloads often authenticate with short-lived ID tokens from external providers. These tokens must be securely retrieved and rotated. While cloud providers offer SDKs and tools, they still require a root secret to bootstrap token retrieval, leaving gaps in distribution and isolation.
Riptides solves this in two ways:
Riptides is the non-human identity fabric for workloads and AI agents. We eliminate credential sprawl by issuing and rotating short-lived SPIFFE based identities automatically, moving access control from the network to the workload using familiar primitives — X.509 certs, JWT tokens, TLS, etc - without requiring application changes.
A good technical overview of our solution is described in the Seamless Kernel-Based Non-Human Identity with kTLS and SPIFFE post
Our architecture consists of both a kernel module and a user-space agent. The kernel module operates at the TCP layer, intercepting new connections. When a workload innitiates a connection, the kernel module:
Riptides issues ephemeral X.509 certificates and binds them to the process that initiated the connection, ensuring that identities are both short-lived and process-scoped. As most enterprises today use cloud providers, federation is critical as well. Riptides can also federate accross cloud providers, or inject secrets/credentials towards 3rd parties, without needing a bootstrap secret. We do this in two different ways:
The Linux kernel crypto API provides certificate generation or big-integer asymmetric key generation. The asymmetric-key subsystem expects keys to be supplied/loaded from userspace — i.e., the kernel API can sign/verify with a key that’s already present, or create a signature blob (create_signature), but it won’t create RSA/ECDSA keypairs or produce CSRs for you.
We needed in-kernel key generation to tightly bind identities to processes and to avoid exposing long-lived private material in userspace. Instead of reinventing crypto primitives, we ported a small crypto library to run inside the kernel:
We keep CSR issuance in the control plane (userspace CA or external CA) but generate keys and create CSRs in privileged kernel context so private material never leaves the kernel memory.

At the time of publishing this blogpost, the kernel crypto API is unable to handle TLS handshake directly inside the kernel. There was/is a debate on the mailing list whether implement the TLS 1.3 handshake inside the kernel.
kTLS exists in Linux but it handles only the record layer - encryption/decryption of TLS records - not the full handshake. The kernel networking stack offers a handshake offload API: a socket file descriptor can be passed to a userspace handshake agent (via netlink), the agent completes the handshake, then returns the socket back to the kernel and sets the TLS ULP (userland handshake model). Projects like Oracle’s ktls-utils show that pattern in practice. Others like Tempesta releasing tech studies about the performance of the handshake happening inside the kernel.
We chose the second approach for Riptides: we perform the TLS handshake inside the kernel module using the in-kernel crypto stack that we ported. Doing the handshake inside the kernel:
We need to inject or replace authentication headers (for AWS, GCP, etc.) so workloads never hold long-lived cloud credentials. To do this transparently we must:
I am not going to dive into all the details if you are interested check out our blogpost about credential injection.
Implementation highlights:

Data is encrypted in user space.

Plaintext data from user space
This is complex, but feasible in kernel space because we control handshake, keys, and send hooks.
Credential exposure via sysfs:
Acourding to linux manual page, sysfs is a kernel pseudo-filesystem exposing kernel objects. Riptides uses sysfs to present short-lived credentials just-in-time to a workload: the kernel module creates sysfs nodes with appropriate access controls and serves credentials such that only the requesting process can open/read them at the allowed time window.
Creating scoped sysfs entries and enforcing in-kernel access control is straightforward within a kernel module, let’s see how all these problems could be solved from eBPF.
eBPF is powerful for observability and safe programmability, but it runs in a sandbox with limited helpers and no arbitrary filesystem / crypto capabilities. Below I keep the same problems and discuss eBPF options and gaps.
eBPF cannot generate asymmetric keypairs or produce CSRs/certs. It lacks the math libraries, heap, and API surface required for big-integer crypto operations, and you cannot realistically port an SSL/TLS stack into eBPF. So certificate/key generation must remain in userspace, with keys shared to eBPF only in limited forms (if at all).
eBPF cannot implement a TLS handshake. Handshake logic requires stateful complex crypto and network interactions far beyond eBPF’s intended use. The kernel provides only the record-layer kTLS facilities, handshake must be performed by a TLS library (userspace or kernel). eBPF’s strength here is observation - not managing TLS state machines.
With eBPF we can also observe user data sent over the wire — this is very much eBPF’s territory. It offers multiple options, the main difference between them is where the inspection happens. Unfortunately there is no silver-bullet: a program that can inspect encrypted data generally cannot modify it, so we often need to transfer context between eBPF programs or combine eBPF with other mechanisms. eBPF hooks differ significantly depending on where they attach in the stack: we move from as close to the hardware as possible up to the user application.
XDP runs at the network device driver level and processes each incoming packet before it reaches the kernel networking stack, so it provides the highest performance with minimal overhead. Its primary use cases are things like DDoS protection (for example, Cloudflare has used XDP eBPF programs for large-scale mitigation) and high-performance filtering.
In a nutshell, XDP programs return one of several actions that indicate what the kernel should do next:
Because XDP operates at such a low level, parsing application-layer protocols (essential for HTTP header injection) is error prone and difficult. XDP runs on raw frames and processes packets individually, so reconstructing and parsing a complete HTTP request often requires complex, manual TCP reassembly and state handling when the data spans multiple segments.

Theoretically, if parsing succeeds, we then need to modify the HTTP header — which could be done in an XDP program. However, XDP provides no checksum or socket helpers, so everything must be implemented by hand. Implementing a certificate-signing mechanism here (essential for providers like Amazon) is effectively impossible in practice. For these reasons, performing L7 data processing at the XDP level is not recommended.
Moving up the stack, socket filters run after the kernel networking stack has processed packets, so they see complete packet data and are a much better fit for HTTP parsing and header modification. We can distinguish four relevant eBPF socket attach types:
setsockopt. It runs on packets delivered to that socket and decides how many bytes to accept or drop before data reaches the socket’s receive queue. Think of it like an in-kernel tcpdump-style filter.or HTTP parsing and reliable header modification the best eBPF approach is a combination of SK_SKB (receive-side parsing) and SK_MSG (send-side verdicts/rewrites). Because these hooks run after TCP reassembly, they can operate on higher-level application payloads and redirect to a peer socket that performs full header rewrites or injection. One important limitation: these hooks only see plaintext if encryption happens after the hook point. If the application performs TLS entirely in user space, the eBPF program will only see ciphertext and cannot decrypt it. Fortunately, many common user-space TLS libraries support kTLS , which moves record protection into the kernel so eBPF hooks placed before the kernel’s record-layer encryption can peek at plaintext.

In our tracing blogposts we explained how Kprobes work, here we’ll focus on Uprobes because they offer a unique approach for tracing encrypted messages. Uprobes are dynamic, per-process instrumentation points that attach to user-space function entry and exit without modifying binaries. They let you observe function arguments, return values, and relevant memory in the probed process with minimal overhead.
This makes Uprobes ideal for parsing application-layer data at functions such as SSL_write and SSL_read. SSL_write sees plaintext application bytes before encryption, and SSL_read sees plaintext after decryption, so an attached uprobe can reliably parse HTTP headers, URIs, or custom protocols. Using Uprobes bypasses the limitation of kernel-level socket hooks - which only observe ciphertext when encryption happens in user space — and provides deep observability with precise process attribution.

Relying on Uprobes lets us parse payloads in a reliable way, but there are caveats. Uprobes typically depend on specific function names and calling conventions, so they work only for particular library versions (e.g., a given OpenSSL release). Other TLS libraries (LibreSSL, BoringSSL, GnuTLS, custom vendored stacks) expose different functions and memory layouts that require different Uprobe locations and handling. Supporting every library and every version is therefore non-trivial and becomes a long-term maintenance effort, but Uprobes remain a powerful additional tool when you can target a known runtime.
Credential exposure via sysfs:
An eBPF program cannot create files in sysfs (or anywhere in the filesystem). eBPF runs in a heavily restricted, sandboxed environment with no direct ability to perform arbitrary filesystem operations like creating files, opening paths, or writing to virtual filesystems such as sysfs. It can only interact through approved helpers, maps, and event output mechanisms, none of which allow creating sysfs nodes.
Riptides’ mission requires tight integration between cryptography, policy enforcement, and workload identity — all within the Linux kernel’s trust boundary. eBPF gives developers safe, dynamic programmability for observability, filtering, and lightweight policy logic, but its sandbox stops at the edge of cryptographic and filesystem operations. In contrast, kernel modules give us:
In short: eBPF is perfect for watching and shaping packets, but when you need to create, sign, and protect the identities behind them, you need the kernel itself.