Skip to content

Integrations Tetragon

LoopGlitch26 edited this page Oct 20, 2026 · 1 revision

Tetragon

Experimental: this works and is tested, but may change and gets less hardening than the stable core. Linux only. Tested: event lines shaped as Tetragon's source writes them, imported into a real signer; not yet run on a live Tetragon (#312).

Tetragon is an eBPF sensor: it sees, in the kernel, the processes a workload starts, the connections it opens and the files it writes, and exports them as JSON lines. tracekit sensor import reads that export and sends each event of an agent's process to the v2 signer, which signs it into the agent's run as an effect.observed record. It works whatever sandbox the agent runs in, or none.

Tetragon Tracekit
What it does Sees processes, connections and file writes in the kernel, and exports them Signs each one into the run of the agent that caused it, joined to the tool call when the process names one, and signs a gap for what Tetragon says it dropped
Where A DaemonSet (or a host service) with eBPF programs in the kernel The import next to Tetragon, the signer outside the agent
What a third party can check later Nothing signed: an export file, rotated away A signed, hash-chained bundle, verified offline

Tracekit doesn't check or change what Tetragon sees: a record says what Tetragon exported, under the import's own signer identity. The same import reads Falco's alerts.

Setup

1. Tetragon. Export to a file, keep the agents' events and the throttle events, and, for Codex, keep the one environment variable that names the tool call (Helm values):

tetragon:
  exportAllowList: |-
    {"namespace":["agents"],"event_set":["PROCESS_EXEC","PROCESS_EXIT","PROCESS_KPROBE"]}
    {"event_set":["PROCESS_THROTTLE"]}
  extraArgs:
    enable-process-environment-variables: "true"
    filter-environment-variables: CODEX_TOOL_CALL_ID

The chart writes the export to /var/log/tetragon/tetragon.log on each node. Outside Kubernetes, the same flags are --export-filename, --export-allowlist, --enable-process-environment-variables and --filter-environment-variables. Field filters (--field-filters) must keep process with its pod, environment_variables, and the kprobe args.

2. A TracingPolicy for connections and for writes outside the workspace. Exec and exit events need none: Tetragon always sends them.

apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
  name: tracekit-agent-effects
  namespace: agents
spec:
  kprobes:
  - call: security_socket_connect     # every connect, after the kernel copied the address
    syscall: false
    args:
    - index: 1
      type: sockaddr
    selectors:
    - matchArgs:
      - index: 1
        operator: Family
        values: [AF_INET, AF_INET6]
  - call: security_file_permission    # every read and write path (write, sendfile, io_uring, ...)
    syscall: false
    args:
    - index: 0
      type: file
    - index: 1
      type: int
    selectors:
    - matchArgs:
      - index: 0
        operator: FileType
        values: [reg]                 # regular files: no pipes or sockets
      - index: 0
        operator: NotPrefix
        values: [/workspace/, /tmp/]  # the agent's own directories
      - index: 1
        operator: Mask
        values: ["2"]                 # MAY_WRITE
      matchActions:
      - action: Post
        rateLimit: 1m                 # one event a minute per process and file: every write() call would flood
        rateLimitScope: process       # the export, and a repeat names no file the first didn't

Three filters on one selector need a 5.3 or later kernel. Put your own workspace path in NotPrefix. The rate limit drops a process's repeat writes to one file within the minute, so tracekit analyze --v2 may find a second write call to that file in the minute with no effect (TK-X207): read it against the first.

3. The signer. Give each node's import its own identity and the observe_effect grant, never a default, and keep them out of every agent's hands:

authorize: {"mtls:spiffe://acme.org/tracekit-sensor/node-1": [observe_effect],
            "mtls:spiffe://acme.org/tracekit-sensor/node-2": [observe_effect]}
tenants: {"mtls:spiffe://acme.org/tracekit-sensor/node-1": acme,   # the tenant whose runs it covers
          "mtls:spiffe://acme.org/tracekit-sensor/node-2": acme}

One identity is one process: when a second process of it calls, the signer takes it for a restart of the first and refuses the first from then on (conflict), so imports on two nodes under one identity stop each other. Run an import per node, each with its own identity, or one import for the whole cluster reading every node's export.

4. The import, on each node, next to Tetragon's export (sensor.yaml):

sensor: tetragon
signer: https://signer.internal:8443   # its identity: TRACEKIT_SIGNER_TOKEN_FILE or _CERT/_KEY
runs:
  - match: {namespace: agents, pod: "coder-*"}            # globs over namespace, pod, container (id), uid
    caller: k8s_sa:system:serviceaccount:agents:coder      # the agent's signer identity
tracekit sensor import --config sensor.yaml --follow /var/log/tetragon/tetragon.log
kubectl logs -n kube-system <the node's tetragon pod> -c export-stdout -f | tracekit sensor import --config sensor.yaml

kubectl logs ds/tetragon reads one pod of the DaemonSet, so one node's events: name each node's pod, as above.

--follow reads from the file's end on and keeps reading across Tetragon's rotation. Without --follow it reads the file once and exits; without a file, stdin.

Attribution

The first runs entry whose match fits the event's process (its pod's namespace and name, its container id, its uid) attributes it: the signer gives the event to the latest open run of that entry's caller registered by the event's time (Tetragon's time; attribution: identity in the record), so an event read late goes to the run it happened in, and one older than every open run of the caller (an export read again) to none. With no older run open, an event up to 5 s before a run registered (the node's clock behind the signer's) goes to that run. An event of a process no entry matches is not an agent's: the import skips it. An event of a matched process whose caller has no open run is a signed capture.gap{effect_outside_run} in the tenant's signer run. The import covers the runs of its entries' callers only (it names them in every call), so a run of any other identity never says its processes were observed, and the import's losses are no gaps in it.

What is recorded

One effect.observed per event, in the run (source: signer, format 1.4, format v2):

Tetragon event kind Fields
process_exec exec binary, pid, exec_id, parent_exec_id (the process that started it), args_commitment (the arguments, committed to, never stored)
process_exit exit status, signal
process_kprobe with a sockaddr_arg (security_socket_connect) or sock_arg (tcp_connect) connect host (the address), port
process_kprobe with a file_arg or path_arg file path

Every record also has observer (the import's identity), sensor: tetragon, ts (Tetragon's time) and attribution. Other events (other kprobes, tracepoints, LSM hooks) aren't recorded.

run.final's coverage matrix says processes observed (tetragon) when an import covered the run throughout: the run's identity is a caller of its runs, and the import was alive when the run registered, sent a heartbeat at least every 30 s, reported no loss, and recorded at least one of the run's events. An import per node can't tell which runs are its node's: only the one that recorded a run's events says it observed it.

Tool calls

Codex puts the id of the tool call that started a command into its environment as CODEX_TOOL_CALL_ID, the id Tracekit records as the call's tool_call_id (coding agents). With Tetragon keeping that variable (step 1), each record of the command's processes carries it as tool_call_id. The id is the agent's claim: the process can change, remove or reuse it, so the signer records it as it came and never takes it as explaining the record. tracekit analyze --v2 checks it (TK-X206, below): the call ran, the record falls in its window, and its tool causes that kind of effect.

Claude Code puts no such id into the commands it runs, so its runs' records carry none.

Checking effects against calls

tracekit analyze --v2 compares the records with the run's calls (findings):

  • TK-X206, effect with no action (high): a process, connection or file write no call explains. A process is a call's when it names the call (tool_call_id, re-checked: the call ran, the effect is in its window, and its tool causes that kind of effect), when the process that started it is the call's (parent_exec_id: a server a call left running, and what it starts later), or when it started while a call that runs processes was running. A process naming a Read call, or a call long finished, is a finding.
  • TK-X207, action with no effect (medium): a call that returned ok without what it should have caused: a Bash command's programs (and curl's or wget's connection), a fetch tool's connection, a write tool's file outside the directories your TracingPolicy leaves out (pass them: --sensor-skip /workspace/ --sensor-skip /tmp/ for the sample above). Checked only when run.final says the import covered the run throughout.

The agent's harness runs from before the run, so its own connections (its model calls) and files aren't checked, except a file a write call named. Shell and Python processes started outside any call, as Tracekit's own hooks are, aren't either. The checks need the sensor's clock and the signer's to agree within --clock-skew (0.5 s): keep both on NTP; a finding says when they don't, and by how much.

Losses

Tetragon numbers no events, so a missing one can't be seen from the stream. What it reports in the stream becomes a signed gap:

  • rate_limit_info: its export rate limit (--export-rate-limit) dropped that many events.
  • process_throttle (THROTTLE_START): its cgroup rate limit (--cgroup-rate) stopped sending a cgroup's exec and exit events.
  • A process_exec flagged procFS: Tetragon (re)started and reported the processes already running, read from /proc; what happened while it was down isn't in its export.

The import adds lines it can't read and an agent's events it can't record. It skips, and counts on stderr when it exits, events it never records: a connect to no IP address (a unix socket: any family but AF_INET and AF_INET6), other kinds of event, and events of no agent; none of these is a loss. It sends the losses to the signer as lost, which writes a capture.gap{observer_lost} in each run the import covers (in the tenant's signer run when it covers none), and their matrix then says processes not observed; they stay watched, so the import's silence after it is a gap too. An import that stops sending for 90 s, or restarts, is the same gap, as for the egress observer. With an import per node, each of these is a gap in the open runs of every node: an import can't tell which runs are its node's.

Tetragon counts some drops only in its metrics (--metrics-server): tetragon_observer_ringbuf_events_lost_total, tetragon_observer_ringbuf_queue_events_lost_total, tetragon_events_export_failed_total. Alert on them in your monitoring: the import doesn't read them.

What Tracekit sees and doesn't

  • Sees: what Tetragon exported for the agents' processes, as Tetragon reported it, and the losses Tetragon reports in its export.
  • Doesn't see: what Tetragon didn't export: events its policies didn't select, drops it counts only in metrics, events of a container that started before Tetragon applied a namespaced policy to it (without Tetragon's OCI runtime hooks, policies apply best effort), and anything before the import's first heartbeat.
  • Doesn't check: that Tetragon's events are true. The records are the import's assertion of what Tetragon said; a root user on the node can change Tetragon, its export or the import.

Limits lists these with what to do. A bundle with these records needs verifier 1.4.0 or later; an older one reports it UNVERIFIABLE (needs tracekit >= 1.4.0). Verify it with uvx --from "tracekit-ai>=1.4.0" tracekit verify run.tkb --trust trust.json, npx -y @cygnuxlabs/tracekit@latest verify run.tkb --trust trust.json, or the browser page (verdicts).

Home

Get started

How it works

Integrations

Policy and approvals

Deploy and operate

Verify the evidence

Security and privacy

Compliance

Evaluation

Demos

Help

Clone this wiki locally