Kubernetes

Unlock the Power of Tracee: Real-time Kubernetes Security with eBPF

Tracee is an open source runtime agent provided by Aqua Security.

Giulia Di Pietro

Nov 19, 2024


As part of our Kubernetes security series, we recently explained OPA Gatekeeper, Falco, Tetragon, and KubeArmor, covering the solution, the type of policies provided by the agent, and the observability data it exposes.

This article will focus on another runtime agent, Tracee, which also detects suspicious security behavior within our environment using eBPF. We will start with an introduction to Tracee, then move on to Tracee’s policies, and finally, its observability.

Introduction to Tracee

Tracee is an open source runtime agent provided by Aqua Security. Over the years, Aqua Security has built many valuable solutions to secure our K8S environment, like the Trivy Operator, that identifies vulnerabilities in the containers deployed in our cluster. More than exposing the vulnerabilities, it also controls that your deployment respects security best practices.

However, as mentioned in the episode about Falco, detecting vulnerabilities is excellent but not enough. That is why we need runtime agents that detect suspicious events. Aqua Security has built Tracee for that.

Tracee architecture

When you deploy Tracee, the daemonset deploys the runtime agents using an eBPF probe to capture kernel events. It also adds a Kubernetes deployment with the Tracee operator, adding a CRD to our cluster called Policy to configure your agents by building policies.

The eBPF probe generates events that are, by default, structured in JSON format. This allows you to see the detected events by looking directly at the logs produced by the Tracee agent. By default, Tracee only reports events related to the default policy deployed by Tracee after the installation.

The policy is a rule defining what we would like to track as Tracee events. If you were wondering if Tracee could apply enforcement rules by killing or blocking a specific event, the answer is no. Compared to the other agents, Tracee is more similar to Falco in detecting events.

Tracee events

As a runtime agent, Tracee will produce the time of the event, the details about the process, the user ID, the process ID, and information on the container, Kubernetes metadata, etc. Here is an example of an event generated by the Tracee agent:

            

{"timestamp":1881288794674,"threadStartTime":-265248579340728,"processorId":1,"processId":1,"cgroupId":7245,"threadId":1,"parentProcessId":0,"hostProcessId":4909,"hostThreadId":4909,"hostParentProcessId":4679,"userId":1000,"mountNamespace":4026532479,"pidNamespace":4026532480,"processName":"proxy-agent","executable":{"path":""},"hostName":"konnectivity-ag","containerId":"e28b5b597fcc8de506d7ff668911aab87a35958263efb44dd858299b97cbac31","container":{"id":"e28b5b597fcc8de506d7ff668911aab87a35958263efb44dd858299b97cbac31","name":"konnectivity-agent","image":"gke.gcr.io/proxy-agent@sha256:d0346df5dceadc5bd9fa6a00415353bcc85b18c48a40bee5aa0df698c13c39f4","imageDigest":"sha256:d0346df5dceadc5bd9fa6a00415353bcc85b18c48a40bee5aa0df698c13c39f4"},"kubernetes":{"podName":"konnectivity-agent-74f48b4b57-5sbbq","podNamespace":"kube-system","podUID":"be60f7cb-2ede-4798-b977-2faebf719ab7"},"eventId":"1","eventName":"write","matchedPolicies":["uid-higher-than-or-equal-to-zero"],"argsNum":3,"returnValue":1,"syscall":"write","stackAddresses":null,"contextFlags":{"containerStarted":true,"isCompat":false},"threadEntityId":950139473,"processEntityId":950139473,"parentEntityId":2204267369,"args":[{"name":"fd","type":"int","value":6},{"name":"buf","type":"void*","value":"0x7ffd0ec9715b"},{"name":"count","type":"size_t","value":1}]}

As you can see, we have all the process details, the actual reference to the host process, the container details, and the k8S metadata. In addition, it includes details about the actual policy: event ID, event name, and policy name. If there were a syscall, it would include the syscall name and the actual args of the syscall. It’s like Tetragon but without complex kernel function mapping.

The event structure is almost similar to all the events that Tracee supports. Of course, the args would be different depending on the actual function or event.

You can configure the level of detail in your event. Tracee in Kubernetes has a config map that allows you to enable or disable details on the event. The configuration config map is tracee-config and will be in the same namespace as Tracee.

There are many options in Tracee's configuration. You can view them all in the Tracee docs.

It’s interesting that you can generate the logs in JSON, send the event directly using the fluent-forward-protocol, and that there’s a webhook option where we can post the events directly to an endpoint.

The section that defines the details that we would like to add or remove in our event is:

            

options:

# none: false

# stack-addresses: true

# exec-env: false

# exec-hash: dev-inode

# parse-arguments: true

# sort-events: false

The default settings are good enough for us; enabling the details on the environment variable is too much.

Another aspect is enabling or disabling the Prometheus exporter, and we can configure the port used for the metrics.

Tracee allows you to configure the capabilities of the Tracee agent and the size of the cache used to receive the various eBPF events. It receives many events and checks if they match your policy before displaying them. So, the cache size would be very important if you don’t want to lose any events.

Tracee Policy

At first glance, I thought the Tracee policy was a bit light and couldn’t be extended from an observability perspective. On the contrary, the Tracee policy configuration is much easier than KubeArmor or Tetragon.

            

kind: Policy

metadata:

name: sample-data-filter

annotations:

description: sample data filter

spec:

scope:

- global

rules:

- event: vfs_read

filters:

The policy has a scope and rules. Let’s look at what you can configure.

Policy Scope

Scope defines where the event will be coming from:

  • Global: the entire host

  • Container: event coming from a container

  • Not container: everything except containers

  • You can also build a scope that tracks everything from a specific process, pID, user uID, or executable.

Once the scope is defined, you’ll create your rules made of one or several events.

Policy Rules

In the rules, you can define which type of event to filter for. In Tracee, there are 6 types of events:

  • Syscalls

  • Network

  • Security

  • Others

  • Containers

You won’t have to define the type. You simply need to assign the right name corresponding to one of those categories.

The security category is interesting because it provides signatures that are predefined code filters that combine various syscall or network interactions to capture known security vulnerabilities.

When deploying Tracee, the default policy comprises a couple of those security signatures.

But Tracee is providing many other security signatures.

Let’s have a look at the default policy:

            

apiVersion: tracee.aquasec.com/v1beta1

kind: Policy

metadata:

annotations:

description: traces default events

meta.helm.sh/release-name: tracee

generation: 1

labels:

app.kubernetes.io/managed-by: Helm

name: default-policy

spec:

rules:

- event: stdio_over_socket

- event: k8s_api_connection

- event: aslr_inspection

- event: proc_mem_code_injection

- event: docker_abuse

- event: scheduled_task_mod

- event: ld_preload

- event: cgroup_notify_on_release

- event: default_loader_mod

- event: sudoers_modification

- event: sched_debug_recon

- event: system_request_key_mod

- event: cgroup_release_agent

- event: rcd_modification

- event: core_pattern_modification

- event: proc_kcore_read

- event: proc_mem_access

- event: hidden_file_created

- event: anti_debugging

- event: ptrace_code_injection

- event: process_vm_write_inject

- event: disk_mount

- event: dynamic_code_loading

- event: fileless_execution

- event: illegitimate_shell

- event: kernel_module_loading

- event: k8s_cert_theft

- event: proc_fops_hooking

- event: syscall_hooking

- event: dropped_executable

- event: container_create

- event: container_remove

scope:

- global

Out of this list, only container_create and container_remove are not in the security signature.

Of course, I won’t describe all of them because it would take forever, but I strongly recommend looking at those various signatures to enable the ones you need. Moreover, you should be able to react to dangerous security events when they occur.

Besides the security signature, you have the network events (including any network traffic; net_packt_http_request, or net_packet_http_response or specific DNS request), the syscall events, and extra events (like vfs_read or vfs_write).

Once this is done, you can apply various filters. First, we’re using a networking event, for example, in the actual data of the event. We want to drop the network traffic sent to localhost.

We can take advantage of the data exposed by the event. For example, in the case of HTTP, the event args field contains a data field and metadata. The data holds the details of the network communication, such as the src or destination. We can filter by data.dest=127.0.0.1 and data.src=127.0.0.1

Most network types will have similar signatures. Of course, you’ll find different details, such as the URL in the request event, the HTTP code, and the size of the response in the HTTP response. Instead of filtering the event data, we can filter on the event's global data, such as the process path, the pID, user ID, the pod namespace, etc. Building a rule is very easy.

Let’s say I want to produce events that report all the HTTP communication of the OpenTelemetry demo. I’ll simply create the following rule:

            

apiVersion: tracee.aquasec.com/v1beta1

kind: Policy

metadata:

name: otel-http-traffic

annotations:

description: this policy capture all the http traffic of the otel namespace

spec:

scope:

- container

rules:

- event: net_packet_http_request

filters:

- podNamespace=otel-demo

- event: net_packet_http_response

filters:

- podNamespace=otel-demo

The simplicity of the configuration is mainly related to the predefined signatures. But one very exciting thing is that Tracee has an SDK allowing us to build your signature. You can define what to track and extract from the event. I think that is very smart.

Observability with Tracee

On the observability side, Tracee will extend your visibility by nature. Collecting Tracee events is quite simple: either collect the logs from Tracee with the log agent of your choice or enable fluent_forward to send the events directly to Fluent Bit or the collector with fluent forward support.

It would be great if Tracee could also offer the default option to send the events in OpenTelemetry log format using OTLP, HTTP, or grpcs. But again, getting the details we need with the current feature is very easy.

The other great thing is that most events have a standard structure. Parsing the events is relatively straightforward. The content of the event and the args fields that hold the data collected from the event would differ depending on the signature used. But for the rest, a common parsing rule would do the job.

Tracee exposes Prometheus metrics by default, but the documentation doesn’t describe the metrics produced and focuses on the events raised. Tracee uses a cache, so I would like to understand how it behaves. Unfortunately, we don’t have KPIs reporting Tracee’s or the operator's health, which is disappointing.

The only metrics that could help us in that direction are:

  • Tracee_ebpf_erorr to see if there is any error

  • Tracee_ebpf_lostevent to know if we’re losing events, then our cache should be full

  • Tracee_ebpf network_capture_lost events and tracee_ebpf_write_lostvent to see if we’re also losing networking events or write events

A view of how the cache behaves would help us tune Tracee to fit our needs.


Watch Episode

Let's watch the whole episode on our YouTube channel.

Go Deeper


Related Articles