README
¶
TraceGuard
TraceGuard is a Go 1.26 Linux security utility that uses the kernel eBPF subsystem to:
- observe outbound DNS queries on UDP and TCP port 53
- report the process that issued the DNS request
- enrich events with process path, argv, UID, PPID, and parent process metadata from
/proc - detect outbound DNS-over-TLS resolver connections
- detect configured DNS-over-HTTPS resolver connections
- trace
execveandexecveatactivity so newly spawned programs are visible - optionally block DNS lookups for domains loaded from a local or remote blocklist
- apply exact-match allow rules that take precedence over exact-match block rules
- support suffix policies such as
*.example.comandsuffix:example.com - expose health and Prometheus-style metrics over HTTP
- optionally archive JSON events locally and export them to an HTTPS collector
- support batched authenticated HTTPS export with durable retry spooling
- support optional gzip and mTLS for HTTPS event export
- optionally enrich pod-scoped events with Kubernetes namespace, pod, node, workload, service account, container, and image metadata
- run a built-in environment doctor check before deployment
In block mode, TraceGuard caches the remote blocklist for six hours by default and refreshes it on the same cadence.
Design
TraceGuard uses two eBPF programs:
cgroup_skb/egressparses outbound UDP and TCP DNS packets on port 53, emits DNS telemetry, and drops matching queries when blocking is enabledcgroup/connect4andcgroup/connect6observe resolver endpoint connections for DoT and configured DoH endpoints and block matching endpoints when blocking is enabledtracepoint/syscalls/sys_enter_execve*emits process execution events
The user-space service:
- normalizes blocklist input before loading it into a BPF hash map
- supports
block:andallow:policy entries in local and remote rule sources - supports exact and suffix domain rules in the policy engine
- caches the remote blocklist on disk with atomic file replacement
- enriches event records from
/procusing a bounded metadata cache - can archive structured events to a local JSONL file with rotation
- can export structured events to an HTTPS endpoint in batches
- can persist failed export batches to disk for later replay
- can enrich pod-scoped events from the Kubernetes API using the existing
pod_uidsignal - can emit either text logs or newline-delimited JSON records
- exports
/healthand/metricswhen a metrics address is configured - uses bounded parsing and fixed-size buffers throughout the BPF program
- avoids shelling out or executing fetched content
Requirements
- Linux with cgroup v2 mounted at
/sys/fs/cgroup - eBPF support for cgroup egress and tracepoints
- privileges equivalent to
CAP_BPF,CAP_NET_ADMIN,CAP_PERFMON, andCAP_SYS_RESOURCE - Go 1.26
clangforgo generate
Notes:
- blocking is exact-match on normalized DNS QNAMEs for classic UDP/TCP DNS on port 53
- allow rules are exact-match and take precedence over exact-match block rules
- suffix rules match a domain and any subdomain, for example
*.example.comorsuffix:example.com - DNS QNAME matching is ASCII case-insensitive
- DoT support is endpoint-based: TraceGuard can detect outbound connections to port 853 and block configured DoT resolver endpoints
- DoH support is endpoint-based: TraceGuard can detect and block configured HTTPS resolver endpoints, but it cannot recover the encrypted inner DNS query name
- DoT and DoH endpoint rules are configured with
dot://resolver.exampleorhttps://resolver.example/dns-query - logs are written to
/var/log/traceguard/traceguard.logby default, rotate at 1 GiB, and retain the last 5 rotated files - process metadata is cached from
/procfor 10 minutes by default to reduce lookup overhead - Kubernetes enrichment is optional, API-driven, and keyed by the observed pod UID
- common IPv6 extension headers are parsed before DNS inspection
- in block mode, segmented TCP DNS queries and fragmented IPv6 DNS packets that cannot be safely inspected are denied instead of allowed
- exact domain policies are enforceable in kernel block mode; suffix and wildcard domain policies are available for observe and dry-run workflows but are rejected in enforced block mode on this kernel path
- event archive and export use the same structured event records as the logger
- event export can use custom trust roots, client certificates, and gzip-compressed batches
Build
go generate ./internal/ebpf
go test ./...
go build ./cmd/traceguard
Common targets:
make generate
make test
make build
make snapshot
Usage
Observe only:
sudo ./traceguard
Block exact domains:
sudo ./traceguard -block \
-block-domain example.com \
-block-domain bad.example.org
Allow a resolver hostname even if it appears in a remote blocklist:
sudo ./traceguard -block \
-blocklist-url https://security.example/blocklist.txt \
-allow-domain resolver.corp.example
Dry-run the policy without enforcing drops:
sudo ./traceguard -dry-run \
-block-domain '*.example.com'
Manually reload policy sources:
sudo kill -HUP $(pidof traceguard)
Block a DoH resolver endpoint and a DoT resolver endpoint:
sudo ./traceguard -block \
-block-domain https://dns.google/dns-query \
-block-domain dot://one.one.one.one
Block from a remote list with six-hour refresh:
sudo ./traceguard -block \
-blocklist-url https://security.example/blocklist.txt \
-cache-path /var/lib/traceguard/blocklist.txt \
-refresh-interval 6h
Print the program version:
./traceguard -v
Run diagnostics:
./traceguard -doctor
Enable Kubernetes enrichment on a node:
sudo ./traceguard \
-kubernetes-enrich \
-kubernetes-api-url https://kubernetes.default.svc:443 \
-kubernetes-node-name "$(hostname)"
Enable JSON output and metrics:
sudo ./traceguard \
-log-format json \
-metrics-addr :9090
Archive events locally and export them to a collector:
sudo ./traceguard \
-log-format json \
-event-archive-path /var/lib/traceguard/events.jsonl \
-event-export-url https://siem.example/api/traceguard \
-event-export-auth-token 'Bearer secret-token' \
-event-export-gzip \
-event-export-spool-path /var/lib/traceguard/export-spool
Use mTLS for the HTTPS event collector:
sudo ./traceguard \
-event-export-url https://siem.example/api/traceguard \
-event-export-ca-path /etc/traceguard/siem-ca.crt \
-event-export-client-cert /etc/traceguard/siem-client.crt \
-event-export-client-key /etc/traceguard/siem-client.key
Environment variables can be used instead of flags:
TRACEGUARD_BLOCKTRACEGUARD_DRY_RUNTRACEGUARD_BLOCKLIST_URLTRACEGUARD_BLOCK_DOMAINSTRACEGUARD_ALLOW_DOMAINSTRACEGUARD_CACHE_PATHTRACEGUARD_REFRESH_INTERVALTRACEGUARD_CGROUP_PATHTRACEGUARD_LOG_PATHTRACEGUARD_LOG_FORMATTRACEGUARD_METRICS_ADDRTRACEGUARD_EVENT_ARCHIVE_PATHTRACEGUARD_EVENT_EXPORT_URLTRACEGUARD_EVENT_EXPORT_AUTH_HEADERTRACEGUARD_EVENT_EXPORT_AUTH_TOKENTRACEGUARD_EVENT_EXPORT_BATCH_SIZETRACEGUARD_EVENT_EXPORT_FLUSH_INTERVALTRACEGUARD_EVENT_EXPORT_SPOOL_PATHTRACEGUARD_EVENT_EXPORT_CA_PATHTRACEGUARD_EVENT_EXPORT_CLIENT_CERTTRACEGUARD_EVENT_EXPORT_CLIENT_KEYTRACEGUARD_EVENT_EXPORT_GZIPTRACEGUARD_PROCESS_CACHE_TTLTRACEGUARD_KUBERNETES_ENRICHTRACEGUARD_KUBERNETES_API_URLTRACEGUARD_KUBERNETES_TOKEN_PATHTRACEGUARD_KUBERNETES_CA_PATHTRACEGUARD_KUBERNETES_NODE_NAMETRACEGUARD_KUBERNETES_POLL_INTERVAL
Example output:
2026/03/16 08:17:20 dns level="info" cgroup="/kubepods.slice/kubepods-burstable.slice/pod12345678_1234_1234_1234_123456789abc.slice/cri-containerd-0123.scope" cmdline=["/usr/bin/dig","example.com"] domain="example.com" event="dns" exe="/usr/bin/dig" k8s_app="dns-client" k8s_containers=["app","sidecar"] k8s_images=["ghcr.io/example/app:v1","ghcr.io/example/sidecar:v2"] k8s_namespace="default" k8s_node="worker-1" k8s_owner="dns-client-7f4b6d" k8s_owner_kind="ReplicaSet" k8s_pod="dns-client" k8s_pod_ip="10.0.0.12" k8s_service_account="dns-client" parent_program="bash" pid=31742 pod_uid="12345678-1234-1234-1234-123456789abc" ppid=31680 program="dig" runtime="containerd" service="cri-containerd-0123.scope" transport="udp" uid=1000
2026/03/16 08:17:21 blocked-doh level="info" address="8.8.8.8" container_id="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" endpoint="dns.google" event="resolver_blocked" exe="/usr/bin/curl" parent_program="python3" pid=31811 policy="block" port=443 program="curl" transport="doh" uid=1000
2026/03/16 08:17:22 would-block level="info" cmdline=["/usr/bin/dig","api.example.com"] domain="api.example.com" event="dns" exe="/usr/bin/dig" mode="dry-run" pid=31742 pod_uid="12345678-1234-1234-1234-123456789abc" policy="block" program="dig" runtime="containerd" transport="udp" uid=1000
Example JSON output:
{"timestamp":"2026-03-16T08:17:20.123456Z","level":"info","message":"dns","event":"dns","program":"dig","pid":31742,"exe":"/usr/bin/dig","cmdline":["/usr/bin/dig","example.com"],"uid":1000,"ppid":31680,"parent_program":"bash","cgroup":"/kubepods.slice/kubepods-burstable.slice/pod12345678_1234_1234_1234_123456789abc.slice/cri-containerd-0123.scope","service":"cri-containerd-0123.scope","pod_uid":"12345678-1234-1234-1234-123456789abc","runtime":"containerd","k8s_namespace":"default","k8s_pod":"dns-client","k8s_node":"worker-1","k8s_pod_ip":"10.0.0.12","k8s_service_account":"dns-client","k8s_owner_kind":"ReplicaSet","k8s_owner":"dns-client-7f4b6d","k8s_app":"dns-client","k8s_containers":["app","sidecar"],"k8s_images":["ghcr.io/example/app:v1","ghcr.io/example/sidecar:v2"],"domain":"example.com","transport":"udp"}
Packaging
GoReleaser is configured to build Linux archives plus:
.deb.rpmarchlinux
Run a local snapshot release with:
goreleaser release --snapshot --clean
The generated packages install:
/usr/bin/traceguard/etc/traceguard/traceguard.env/var/log/traceguard/traceguard.logat runtime via the packaged service defaults- a systemd unit at the distro-appropriate system path
- optional metrics on the configured listen address
Secure Development Notes
- Dependencies are managed through Go modules and suitable for GoReleaser verifiable builds.
- Remote blocklist fetches use HTTPS only, bounded response sizes, and network timeouts.
- Oversized remote blocklists are rejected instead of being silently truncated.
- Remote blocklist redirects are limited and must remain on HTTPS.
- Log file creation rejects symlink targets and non-regular files to reduce log-path attacks.
- Cache reads reject symlinks, cache writes are atomic, and cached blocklists are written with restricted permissions.
- The BPF parser uses explicit bounds checks and fixed maximum sizes to satisfy the verifier and reduce parser risk.
- Block mode fails closed if blocked-event telemetry cannot be emitted or if TCP/IPv6 DNS traffic cannot be safely inspected.
- Process enrichment is performed from
/procin userspace; if a process exits before enrichment, TraceGuard falls back to kernel-provided task metadata. - Process enrichment also extracts cgroup path, likely service unit, and container ID heuristics from
/proc/<pid>/cgroup. - Process enrichment now also extracts pod UID and runtime hints from common Kubernetes/container cgroup layouts when present.
- Optional Kubernetes API enrichment can add namespace, pod name, pod IP, node name, service account, controller workload, app label, container names, and image names keyed by the observed pod UID.
dry-runuses the same policy engine as enforcement mode but logswould-blockdecisions instead of enabling kernel drops.SIGHUPtriggers an immediate policy reload from local and remote sources.- Metrics and health endpoints are served only when explicitly enabled with
-metrics-addr. - Event export requires an HTTPS endpoint and uses bounded in-memory queuing to avoid blocking the main event loop.
- Event export batches records as JSON arrays, supports a configurable auth header, optional gzip compression, optional mTLS, and can spool failed batches to disk for replay.
- Kubernetes enrichment uses HTTPS, a bearer token, bounded response sizes, and a periodic cache refresh instead of live per-event API calls.
- Encrypted DoH and DoT traffic is handled at the resolver-endpoint level; the implementation does not attempt TLS interception or decryption.