Secure Your Secrets, Agentless
Kubernetes eBPF HTTPS interceptor. Transparent secret injection without application changes or sidecars.
Kloak transparently intercepts outbound TLS traffic in Kubernetes using eBPF uprobes, replacing placeholders with real secrets in-kernel by patching the encrypted TLS record on its way out of the pod. Applications never handle actual credentials, and no sidecars or code changes are required.
Features
- No code changes -- No SDK, no library, no application modifications. Mount a secret, make HTTPS requests, and Kloak handles the rest.
- Secret isolation -- Applications only see random shadow values (
kl::…, same length as the real value). The real value is never written into the pod's memory; it is XOR-patched into the AES-GCM ciphertext in-kernel.
- Zero overhead -- eBPF uprobes operate in kernel space with negligible latency impact. No userspace proxy or sidecar in the data path.
- Kubernetes native -- Works with standard Kubernetes Secrets. Enable with a single label.
- Per-pod secret binding -- A pod can only use the secrets it references in its spec (Secret volume,
secretKeyRef, envFrom). A placeholder sent by any other pod, even one that learned it, goes out unchanged.
- Host and IP filtering -- Secrets annotated with
getkloak.io/hosts are only sent to specific destination hostnames or IP addresses, preventing exfiltration to unauthorized servers.
- Port-based filtering -- Secrets annotated with
getkloak.io/port are restricted to connections on a specific destination port.
- Broad runtime support -- Hooks into OpenSSL, BoringSSL, and Go's native
crypto/tls. Works with Python, Node.js, Bun, Go, Rust, Ruby, PHP, curl, and any OpenSSL-linked runtime.
Quick Start
Prerequisites
- Kubernetes cluster (1.28+) with Linux kernel 5.17+ (6.6+ attaches the tc patch program via TCX; older kernels fall back to a clsact qdisc + cls_bpf filter, see
controller.ebpf.tcAttachMode)
- Helm 3.12+
kubectl configured with cluster access
Install with Helm
helm repo add kloak https://chart.getkloak.io
helm repo update
helm install kloak kloak/kloak -n kloak-system --create-namespace
kubectl get pods -n kloak-system
Labels and Annotations
| Key |
Type |
Scope |
Description |
getkloak.io/enabled=true |
Label |
Secret, Namespace, Pod |
Enables Kloak for the target resource. On namespaces and pods, controls webhook scope. On secrets, triggers shadow secret creation. |
getkloak.io/hosts=host |
Annotation |
Secret |
Restricts which destination host or IP address a secret can be sent to (single value only) |
getkloak.io/port=443 |
Annotation |
Secret |
Restricts which destination port a secret can be sent to |
getkloak.io/managed=true |
Label |
Secret |
Marks shadow secrets created by Kloak (do not set manually) |
Try the Demo
# Full demo: creates a Lima VM with K3s, deploys Kloak and sample apps
./examples/setup-demo.sh
export KUBECONFIG=/tmp/kloak-k3s.yaml
# View logs (should show real secrets in httpbin.org responses)
kubectl logs -f -l app=demo-python -n kloak-demo -c demo-app
# Cleanup
./examples/destroy-demo.sh
Architecture
Kloak consists of a control plane (controller + webhook) and an eBPF data plane that runs entirely in kernel space.
graph TD
subgraph Control_Plane [Control Plane]
C[Kloak Controller - DaemonSet]
W[Mutating Webhook - Deployment]
end
subgraph Data_Plane [Data Plane - eBPF in Kernel]
UP[TLS Uprobes]
HX[H Extract]
XOR[XOR Path]
KP[DNS Kprobe/Kretprobe]
TP[Connect/Close Tracepoints]
PROC[Process Tracepoints]
TCP[tcp_sendmsg Kprobe]
TC[TC Patch - host-side veth]
GHASH[TC GHASH Update]
end
subgraph App [Application]
P[Pod with Shadow Secret]
end
C -->|Creates shadow secrets and syncs BPF maps| UP
C -->|Attaches probes to container processes| UP
W -->|Rewrites secret references to shadow secrets| P
P -->|TLS write with kl:: placeholder| UP
UP -->|Tail call: libcrypto-hook processes| HX
UP -->|Tail call| XOR
HX -->|Tail call| XOR
XOR -->|Stores xor_pending| TCP
TCP -->|Stores tc_pending| TC
TC -->|Tail call| GHASH
GHASH -->|Patched ciphertext + tag| Internet
KP -->|Populates dns_ip_map| UP
TP -->|Populates conn_ip_map / last_verified_fd| UP
PROC -->|Tracks process lifecycle| C
Components
| Component |
Description |
| Controller (DaemonSet) |
Watches Secrets labeled getkloak.io/enabled=true, creates shadow secrets with random kl::… placeholders of the same length as each real value, syncs real values into eBPF maps, and attaches TLS uprobes to container processes via cgroup discovery. |
| Webhook (Deployment) |
Mutating admission webhook that intercepts Pod creation. Rewrites Secret volume mounts and environment variable references (secretKeyRef, envFrom) to point to shadow secrets. Evaluates enablement through pod labels or namespace labels. Rejects pods if the shadow secret has not been created yet (fail-closed). Two webhook entries ensure only kloak-enabled namespaces and pods are affected; non-kloak workloads are never impacted, even when the webhook is down. |
| TLS Uprobes |
Attach to SSL_write / SSL_write_ex (OpenSSL/BoringSSL), crypto/tls.(*Conn).Write (Go native), and SSL_write at a pre-computed file offset in symbol-stripped Bun binaries. Intercept outbound TLS writes and scan the plaintext for kl:: placeholders. The plaintext buffer is only read, never modified. |
| H Extract |
Recovers the connection's AES-GCM GHASH key H, which is needed to fix the authentication tag after patching. For processes whose libcrypto exposes EVP_CipherInit_ex, a uprobe captures it at cipher init. Otherwise the tcp_sendmsg kprobe walks the library's TLS structs to it. BoringSSL keeps only the AES key schedule, so H = AES_K(0) is recomputed in-kernel. |
| XOR Path + TC Patch |
Used for every runtime. Checks the destination against the secret's host/IP/port policy, then computes shadow XOR real for each matched placeholder. This bridges through the tcp_sendmsg kprobe to a tc program on the host side of the pod's veth pair, which XOR-patches the AES-GCM ciphertext in the outgoing packet and recomputes the GHASH tag via a tail call to tc_ghash_update. The real secret is never written to user-space memory, and patching outside the pod's network namespace keeps the patched ciphertext out of reach of in-pod packet capture. |
| DNS Kprobe |
Kprobe/kretprobe on udp_recvmsg captures DNS responses system-wide. Parses A/AAAA records for watched hostnames and populates dns_ip_map (IP to hostname) with TTL tracking. |
| Connect/Close Tracepoints |
Hooks sys_enter/exit_connect to track TCP connections (fd to destination IP in conn_ip_map). When the destination matches a DNS-verified hostname, caches the fd in last_verified_fd. Hooks sys_enter_close to clean up stale entries. |
| Process Tracepoints |
Hooks sched_process_exec and sched_process_exit to track container process lifecycle for uprobe attachment and cleanup. |
Supported Runtimes
| Runtime |
TLS Library |
Hook Point |
| Python, Rust, Ruby, PHP, curl |
OpenSSL (libssl.so) |
SSL_write / SSL_write_ex uprobe |
| Node.js |
OpenSSL (statically linked into node) |
SSL_write uprobe on the main executable |
| Apps linked against BoringSSL |
BoringSSL (libssl.so) |
SSL_write uprobe |
| Bun (incl. single-executable apps) |
BoringSSL (statically linked, symbols stripped) |
SSL_write uprobe at a per-version file offset (tools/bun-offsets/) |
| Go |
crypto/tls (native) |
crypto/tls.(*Conn).Write uprobe |
Secrets are patched into AES-128-GCM and AES-256-GCM records (TLS 1.2 and 1.3). Other ciphers (e.g. ChaCha20-Poly1305) and some libraries (GnuTLS, Go built with GOEXPERIMENT=boringcrypto) are not supported yet.
DNS-Verified Trust Chain
Secrets with getkloak.io/hosts annotations are only rewritten when the destination is verified through the full DNS resolution chain. This prevents secret exfiltration to unauthorized servers, even if an application is compromised.
sequenceDiagram
participant App as Application
participant DNS as DNS Server
participant KP as eBPF kprobe
participant TP as eBPF tracepoint
participant UP as eBPF uprobe
participant XOR as XOR Path
participant TC as tc patch (host veth)
participant Srv as api.stripe.com
Note over App,Srv: 1. DNS Resolution
App->>DNS: resolve api.stripe.com
DNS-->>App: A 52.55.108.115
KP->>KP: dns_ip_map[52.55.108.115] = api.stripe.com
Note over App,Srv: 2. TCP Connect
App->>Srv: connect(fd=7, 52.55.108.115:443)
TP->>TP: conn_ip_map[tgid,fd=7] = 52.55.108.115
TP->>TP: IP in dns_ip_map -> mark fd as verified
Note over App,Srv: 3. TLS Write (Allowed)
App->>UP: SSL_write with kl::a1b2c3d4
UP->>UP: resolve_host -> api.stripe.com
UP->>XOR: Tail call
XOR->>XOR: allowed_host matches -> stage XOR delta
App->>TC: Encrypted record leaves the pod
TC->>TC: XOR-patch ciphertext, recompute GCM tag
TC->>Srv: Real secret decrypted only by api.stripe.com
Note over App,Srv: 4. TLS Write (Blocked)
App->>UP: SSL_write to evil.com with kl::a1b2c3d4
UP->>UP: resolve_host -> evil.com
UP->>XOR: Tail call
XOR->>XOR: allowed_host mismatch -> nothing staged
App->>Srv: Placeholder sent as-is
How Host Verification Works
-
DNS capture -- A kprobe on udp_recvmsg intercepts DNS responses on the node. For hostnames listed in getkloak.io/hosts, resolved IPs are stored in dns_ip_map with TTL tracking.
-
Connection tracking -- Tracepoints on sys_enter/exit_connect record each TCP connection's fd-to-destination-IP mapping in conn_ip_map. If the destination IP exists in dns_ip_map, the fd is marked as verified.
-
Host resolution -- At SSL_write time, resolve_host() chains through verified fd, conn_ip_map, and dns_ip_map to determine the hostname of the current TLS connection.
-
Secret filtering -- The XOR path compares the resolved hostname (and IP/port, if set) against the secret's policy. Match: a patch is staged for the tc program. Mismatch: nothing is staged and the placeholder is sent as-is, keeping the secret safe.
-
TTL enforcement -- DNS entries include a TTL. Expired entries are skipped on lookup, requiring re-verification through fresh DNS responses.
How It Works
1. Label and Annotate Your Secrets
Add getkloak.io/enabled=true as a label to enable Kloak. Use annotations for host and port filtering. Kloak generates a shadow secret whose values are random kl::… placeholders of exactly the same length as the originals.
apiVersion: v1
kind: Secret
metadata:
name: api-credentials
labels:
getkloak.io/enabled: "true"
annotations:
getkloak.io/hosts: "api.stripe.com"
getkloak.io/port: "443"
data:
api-key: c2stbGl2ZS14eXoxMjM= # sk-live-xyz123
2. Deploy Your Application
The webhook automatically rewrites the pod's secret references (volume mounts, secretKeyRef, envFrom) to use the shadow secret. Your application sees only kl::… placeholders and never handles real credentials.
# What the application reads from the mounted secret (same length as the real value):
kl::q7XmB2rTa9
Only the pods that reference a secret this way can use it. The controller binds each pod's containers to the secrets in its spec, and the data plane refuses to rewrite a placeholder for any other pod: the placeholder goes out unchanged and the controller logs a secret binding violation. Apps that read a shadow secret through the Kubernetes API instead of mounting it are refused too. Set controller.ebpf.secretBinding: audit to let such rewrites through while logging each one, to find these workloads before enforcing.
3. Automatic In-Kernel Rewrite
When the application makes an outbound HTTPS request, the eBPF uprobe reads the TLS write, verifies the destination through the DNS trust chain, and stages a patch. After the TLS library encrypts the record, a tc program on the host side of the pod's veth XOR-patches the ciphertext so that it decrypts to the real secret, and recomputes the AES-GCM tag. The real value never exists in the pod's memory.
# What the application writes (and what is in its memory):
Authorization: Bearer kl::q7XmB2rTa9
# What api.stripe.com decrypts (after the in-kernel ciphertext patch):
Authorization: Bearer sk-live-xyz123
Development
make build # Build the kloak binary
make test # Run all tests
make docker-build # Build Docker image
eBPF development requires Linux. On macOS, Kloak uses Lima VMs:
make lima-start # Start Lima VM
make generate-ebpf # Generate eBPF Go bindings
make test-linux # Run tests in Linux VM
make lima-shell # Shell into VM
License
AGPL-3.0