kntrl is an eBPF-based runtime security agent that monitors and controls network, process, DNS, TLS, and file activity on CI/CD runners and build pipelines. It hooks into kernel-level syscalls to enforce policies in real time — blocking supply-chain attacks, unauthorized network access, and anomalous process behaviour before they cause damage.
Refer to this presentation for a deeper look at the architecture.
- Network monitoring & enforcement — Intercepts IPv4/IPv6 TCP and UDP connections via eBPF kprobes. Allows or blocks based on destination IP, domain, CIDR range, and process identity.
- DNS monitoring — Captures DNS queries and responses at the kernel level. Tracks which domains each process resolves and restricts DNS server usage.
- TLS SNI inspection — Extracts Server Name Indication from TLS ClientHello packets via eBPF TC hooks for accurate domain-based blocking even before the connection completes.
- Process ancestry tracking — Monitors fork/exec events to build an in-memory process tree. Blocks connections when specific process chains are detected (e.g., block
curlspawned bynpm). - File access monitoring — Tracks file open events on sensitive paths (e.g.,
/etc/shadow,/root/.ssh/). - Per-process network profiles — Assign different allowed hosts per process (e.g.,
npmcan only reachregistry.npmjs.org). - OPA policy engine — All policy decisions use Open Policy Agent with embedded Rego rules. Extend or override with custom
.regofiles. - Two operating modes —
monitor(log only) andtrace(enforce and block). - Webhook alerting — Send block/pass events to external endpoints in real time.
- Established connection preloading — Reads
/proc/net/tcp{,6}at startup so existing SSH and database connections survive agent start. - SIGHUP live reload — Reload YAML rules and flush policy caches without restarting.
- Daemon mode — Run in the background with PID file management.
- Process identity validation — Overrides the BPF
commfield (spoofable viaprctl) with the real executable name from/proc/[pid]/exe. - Dot-boundary host matching —
"github.com"matchessub.github.combut NOTevil-github.com. - Raw socket monitoring — Detects creation of
AF_PACKET,IPPROTO_RAW, andIPPROTO_ICMPsockets. - No DNS auto-whitelisting — DNS-resolved IPs require explicit policy approval before they enter the BPF allowlist.
- LRU BPF maps — Auto-evicting hash maps prevent map overflow under high load.
- Policy result caching — Per-IP+task+ancestry TTL cache avoids redundant OPA evaluations (30s TTL).
- Async DNS resolution — Non-blocking worker goroutine for forward DNS cache population.
- Buffered report I/O — 64KB buffered writer reduces syscall overhead for report file writes.
kntrl is available as a downloadable binary from the releases page. Download the pre-compiled binary and copy it to the desired location.
docker pull kondukto/kntrl:latest
To pull a specific version:
docker pull kondukto/kntrl:0.1.4
make generate # compile eBPF programs (requires clang, llvm, libelf-dev)
make build # build the Go binary to build/kntrlStart the agent in monitor mode during your CI/CD job:
- name: start kntrl agent
run: sudo ./kntrl start --mode=monitor --allowed-hosts=download.kondukto.io,${{ env.GITHUB_ACTIONS_URL }} --allowed-ips=10.0.2.3 --daemonizeStop and print the report:
- name: stop kntrl agent
run: sudo ./kntrl stopOr with Docker:
- name: kntrl agent
run: sudo docker run --privileged \
--pid=host \
--network=host \
--cgroupns=host \
--volume=/sys/kernel/debug:/sys/kernel/debug:ro \
--volume /tmp:/tmp \
--rm docker.io/kondukto/kntrl:0.1.4 \
start --mode=trace --allowed-hosts=kondukto.io,download.kondukto.iokntrl [command]
Commands:
start Start kntrl agent
stop Stop kntrl daemon and print reports
status Print kntrl daemon status
completion Generate shell autocompletion script
help Help about any command
Global flags:
-v, --verbose Enable verbose logging
--version Show version
| Flag | Default | Description |
|---|---|---|
--mode |
monitor |
Operating mode: monitor (log only) or trace (enforce) |
--allowed-hosts |
Comma-separated allowed hostnames (e.g., example.com,.github.com) |
|
--allowed-ips |
Comma-separated allowed IP addresses | |
--allow-local-ranges |
true |
Allow local IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) |
--allow-github-meta |
false |
Allow GitHub Actions IP ranges from api.github.com/meta |
--allow-metadata |
false |
Allow cloud metadata endpoints (169.254.169.254, 168.63.129.16) |
--monitor-processes |
true |
Enable process fork/exec monitoring |
--rules-file |
Path to a YAML policy file | |
--rules-dir |
Path to a directory containing .yaml and/or .rego rule files |
|
-o, --output-file-name |
/tmp/kntrl.out |
Report output file |
--pretty |
false |
Pretty-print process events as a tree |
--daemonize |
false |
Run in the background |
Use --rules-file to load a comprehensive YAML policy. This gives fine-grained control over network access, process monitoring, DNS servers, file access, process ancestry chains, per-process profiles, and webhook alerting.
version: "1"
mode: trace
rules:
network:
allowed_hosts:
- "github.com"
- ".npmjs.org"
- ".amazonaws.com"
allowed_ips:
- "10.0.0.0/8"
- "172.16.0.0/12"
allow_local_ranges: true
allow_github_meta: true
allow_metadata: false
allowed_processes:
- "curl"
- "git"
- "wget"
- "docker"
profiles:
- process: "npm"
allowed_hosts:
- "registry.npmjs.org"
- process: "pip"
allowed_hosts:
- "pypi.org"
- "files.pythonhosted.org"
process:
enabled: true
blocked_chains:
- process: "curl"
ancestors: ["npm"]
- process: "wget"
ancestors: ["pip"]
dns:
allowed_servers:
- "8.8.8.8"
- "8.8.4.4"
file:
enabled: false
monitored_paths:
- "/etc/shadow"
- "/root/.ssh/"
- "/proc/self/environ"
webhooks:
- url: "https://your-siem.example.com/events"
headers:
Authorization: "Bearer <token>"
filter: "block"See examples/policy-v2.yaml for a full example.
Send SIGHUP to the running agent to reload the YAML rules file and flush policy caches without restarting:
kill -HUP $(cat /tmp/kntrl.pid)kntrl tracks process fork/exec events to build an in-memory process tree. When a network connection is made, kntrl walks the tree to determine the full ancestry chain of the connecting process. This ancestry is then evaluated against blocked_chains rules.
Each blocked_chains entry specifies a process name and a list of ancestors. If the process making the network connection matches process and every name in ancestors appears somewhere in its ancestry chain, the connection is denied.
This lets you write rules like "block curl if it was spawned (directly or indirectly) by npm":
rules:
process:
enabled: true
blocked_chains:
- process: "curl"
ancestors: ["npm"]With this rule:
npm installspawningsh -> curlto exfiltrate data is blocked- A user running
curl github.comdirectly from a shell is allowed
You can require multiple ancestors to be present:
blocked_chains:
- process: "sh"
ancestors: ["npm", "node"]The ancestry chain is also passed into OPA policy evaluation as input.ancestors, so you can write custom Rego rules against it.
Restrict which hosts a specific process can reach. For example, allow npm to reach only its registry:
rules:
network:
profiles:
- process: "npm"
allowed_hosts:
- "registry.npmjs.org"
- process: "pip"
allowed_hosts:
- "pypi.org"
- "files.pythonhosted.org"If the connecting process matches a profile, only the hosts listed in that profile are allowed — the global allowed list does not apply.
kntrl captures DNS queries and responses at the kernel level via eBPF kprobes. This provides:
- Correlation between resolved domains and destination IPs for accurate policy evaluation
- DNS server restriction — only allow queries to approved DNS servers
- Visibility into which processes resolve which domains
rules:
dns:
allowed_servers:
- "8.8.8.8"
- "8.8.4.4"DNS events are displayed in the report as a separate table:
Domain | DNS Server
------------------------------------------
registry.npmjs.org | 8.8.8.8
github.com | 8.8.8.8
kntrl uses eBPF TC (traffic control) hooks to inspect TLS ClientHello packets and extract the Server Name Indication (SNI) field. This allows domain-based policy enforcement even for encrypted connections, correlating the SNI with the network connection event.
Track access to sensitive files:
rules:
file:
enabled: true
monitored_paths:
- "/etc/shadow"
- "/root/.ssh/"
- "/proc/self/environ"File events appear in a separate report table:
Pid | Comm | Filename | Flags
---------------------------------------------------
1234 | python | /etc/shadow | 0
Send events to external systems (SIEM, Slack, etc.) in real time:
webhooks:
- url: "https://your-siem.example.com/events"
headers:
Authorization: "Bearer <token>"
filter: "block" # "block", "pass", or "all"Events are delivered asynchronously via buffered channels with a 5-second HTTP timeout per webhook.
kntrl uses an embedded OPA policy engine. All policy rules live under bundle/kntrl/ and are compiled into the binary. The policy evaluation flow:
- Network event arrives from eBPF
- Event is enriched with DNS domains, SNI, and process ancestry
- OPA evaluates against all rules (allowed hosts, allowed IPs, local ranges, GitHub meta, process profiles, ancestry chains, custom rules)
- Result:
passorblock
| Policy | Description |
|---|---|
is_allowed_hosts |
Match domains against allowed host list with dot-boundary safety |
is_allowed_ip |
Match IPs against explicit allowlist |
is_allowed_cidr |
Match IPs against CIDR ranges |
is_local_ip_addr |
Allow RFC 1918 private ranges |
is_github_range |
Allow GitHub Actions infrastructure IPs |
is_metadata |
Control cloud metadata endpoint access |
is_process_profile |
Per-process allowed hosts |
ancestry |
Block based on process ancestry chains |
custom |
Placeholder for user-defined rules |
Add custom .rego files via --rules-dir:
package kntrl.network["my_custom_rule"]
import rego.v1
policy if {
input.task_name == "curl"
some ancestor in input.ancestors
ancestor == "npm"
}make test-rego-local # requires opa CLI
make test-rego # runs in DockerEvents are logged to the output file (default /tmp/kntrl.out) in JSON format, one event per line:
{
"pid": 2806,
"task_name": "curl",
"proto": "tcp",
"daddr": "140.82.114.22",
"dport": 443,
"domains": ["lb-140-82-114-22-iad.github.com"],
"policy": "pass",
"ancestors": ["bash"]
}{
"pid": 3201,
"task_name": "curl",
"proto": "tcp",
"daddr": "evil.example.com",
"dport": 443,
"domains": ["evil.example.com"],
"policy": "block",
"ancestors": ["sh", "npm", "node", "bash"]
}When the agent stops, summary tables are printed:
Pid | Comm | Proto | Domain | Destination Addr | Policy
---------------------------------------------------------------------------------------
2806 | curl | tcp | lb-140-82-114-22-iad.github.com | 140.82.114.22:443 | pass
---------------------------------------------------------------------------------------
3201 | curl | tcp | evil.example.com | 93.184.216.34:443 | block
Use --pretty to display process events as a tree:
Process Tree
[1234] /bin/bash
├── [1235] npm install
│ └── [1236] sh -c curl http://evil.com
│ └── [1237] curl http://evil.com
└── [1240] git clone https://github.com/org/repo
kntrl attaches the following eBPF programs to kernel hooks:
| Hook | Type | Purpose |
|---|---|---|
tcp_v4_connect |
kprobe | IPv4 TCP connection attempts |
ip4_datagram_connect |
kprobe | IPv4 UDP connection attempts |
tcp_v6_connect |
kprobe | IPv6 TCP connection attempts |
udpv6_sendmsg |
kprobe | IPv6 UDP sends |
skb_consume_udp |
kprobe | DNS packet capture |
inet_sock_set_state |
tracepoint | TCP state change tracking |
sched_process_exec |
tracepoint | Process execution events |
sched_process_fork |
tracepoint | Process fork events |
security_socket_create |
kprobe | Raw socket creation detection |
| TC classifier | tc | TLS SNI extraction from packets |
All event data flows through eBPF ring buffers for efficient kernel-to-userspace communication. BPF maps use LRU eviction to prevent overflow under high load.
make test-unit # unit tests (Docker)
make test-rego # OPA policy tests (Docker)
make test-ebpf # eBPF tests (requires Linux kernel)
make test-integration # integration tests (requires Linux kernel)
make test-all # all tests (Docker)
make test-unit-local # unit tests (local, works on macOS)
make test-rego-local # OPA policy tests (local, requires opa CLI)Contributions to kntrl are welcome. Feel free to join our Slack channel https://kntrl.slack.com
Except for the eBPF code, all components are distributed under the Apache License (version 2.0).
kntrl is an open source project maintained by Kondukto.