Aqua Blog

Tracee Runtime Security Series: Writing Custom Tracee Rules

Tracee Runtime Security Series: Writing Custom Tracee Rules

As an open source runtime security tool, Tracee provides a base rule set that can detect a variety of attacks. However, there’s often the need to add new rules either to contribute to the project or to provide specific rules for your environment. Because Tracee allows for new rules to be written in Rego and Golang, you have a variety of options for creating a rule set that works for your environment.

This blog will walk through the process of creating and testing a new Tracee rule, designed to fire whenever the Docker socket file is accessed from a container. This might indicate that an attacker has managed to mount it inside a container and is seeking to escalate their privileges on the host or attack other containers.

Designing a rule

The first thing we need to do when creating a Tracee rule is to understand what pattern of access we’re looking to model and work out how to test for that access. In this case, we’re looking at access to a Unix socket. So, the first step is to check whether Tracee can detect that type of event. Tracee can provide a list of possible events using the `-l` switch:

docker run aquasec/tracee:latest trace -l

From this list we can see a good candidate for our rule:

security_socket_connect [default lsm_hooks net net_sock]
(int sockfd, struct sockaddr* remote_addr)

Once we’ve got our possible event, a good way to ensure that it will work is to use Tracee’s raw trace mode functionality (assuming you’re using an up-to-date kernel that supports BTF) to show us instances of that event happening without any security rules attached. This command will use Tracee to display instances of the security_socket_connect event:

docker run --rm --pid=host --privileged -v /tmp/tracee:/tmp/tracee -it aquasec/tracee:latest trace --trace event=security_socket_connect

Once Tracee is running, we can use a command like docker info to generate some events. It should look like this:

Events generated by running Tracee

What we can see from the output is that while we’ve got instances of access to the Docker socket, there’s also other socket access that we probably want to avoid turning up as alerts.

Importantly, we’re also given the arguments that go with the event. From that we can see that the remote_addr argument contains the socket name, which will let us match only where the docker.sock file is accessed.

Now that we’ve got the basic idea of what we want to include in our rule, we can get to writing it.

Writing a rule

Tracee supports two formats for rules: Golang and Rego. Rego is simpler to get started with, and it should work well for this kind of rule. Each rule is created as a text file with the information needed, then placed in the Tracee rules directory and read at program start-up.

There are a couple of sections we need to populate when writing a Tracee rule. First, we need to name our package and import the helper functions that we’ll use later. In this case, I’ve just named my rule in a way to avoid clashing with any existing ones.

package tracee.RMM_1

import data.tracee.helpers

Next, we need to populate the rule metadata. This includes setting a unique ID for the rule and providing some descriptive text, which will be sent when the rule is triggered. Here, we’re describing Docker socket access and aligning it with the MITRE Container ATT&CK framework.

__rego_metadoc__ := {
     "id": "RMM-1",
     "version": "0.0.1",
     "name": "Docker Socket Access Detected",
     "description": "A container accessed the Docker socket, allowing for control of the container daemon and possible privilege escalation",
     "tags": ["container"],
     "properties": {
          "Severity": 0,
          "MITRE ATT&CK": "Container Administration Command"
     }
}

After that, we need to specify the source of the events that we want to evaluate with our rule. The key parts here are the name of the event, which is the one we identified earlier, and the origin of the event. Setting this to container means that we’ll only see events that were generated inside containers. That’s handy for avoiding false positives, if we’re not interested in events generated from users that aren’t operating inside containers.

eventSelectors := [
     {
          "source": "tracee",
          "name": "security_socket_connect",
          "origin": "container"
     }
]

Once we’ve got our events identified in the rule, we need to create the logic of the rule, which is done in the tracee_match section. These matches are essentially Rego assertions, so anyone familiar with writing rules for OPA or other tools that use Rego should find the process familiar. In this case, it’s pretty straightforward. First, we specify our input event name, which is the security_socket_connect. Then we set a variable called addr, which gets the remote_addr argument for the event.

Lastly, we specify that the alert should be raised where that remote_addr variable contains the string docker.sock so that we don’t get false alarms from other socket access inside a container.

tracee_selected_events[eventSelector] {
     eventSelector := eventSelectors[_]
}

tracee_match {
     input.eventName == "security_socket_connect"
     addr := helpers.get_tracee_argument("remote_addr")
     contains(addr,"docker.sock")
}

Testing your rule

Now that we’ve created the rule, we need to test it to make sure it works as intended. To do that, we can launch Tracee and specify a custom rules directory, so it’ll pick up the newly authored rule. In the command below, the directory /home/rorym/docker_socket_rule is mapped in as the Tracee rules directory.

docker run --name tracee --rm --pid=host --privileged -v /tmp/tracee:/tmp/tracee -v /home/rorym/docker_socket_rule/:/tracee/rules -it aquasec/tracee:latest

Then, in another shell, launch a container with the Docker socket mapped in and try some Docker commands and watch for alerts.

This command will run an image that has the Docker CLI embedded and mounted in the Docker socket:

docker run -it -v /var/run/docker.sock:/var/run/docker.sock raesene/alpine-containertools /bin/bash

Then, running the command docker info in that shell should result in the alert below.

Tracee alert: Docker socket access detection

Conclusion

Solutions like Tracee can provide a great addition to organizations’ security tooling just with their in-built rule sets. However, the ability to customize and extend these rules provides opportunities to really optimize and extend their benefits.

You can use Tracee’s flexible extension capability to write new rules in Rego or Golang to tailor its capabilities for your organization’s specific threats and environments. Also, the project welcomes contributions of new signatures for container security alerts, so you can contribute back to the community and improve Tracee detection for everyone.

Rory McCune
Rory was a Cloud Native Security Advocate at Aqua. He has worked in the Information and IT Security arena for the last 20 years in a variety of roles. He is an active member of the container security community having delivered presentations at a variety of IT and Information security conferences. He has also presented at major containerization conferences and is an author of the CIS Benchmarks for Docker and Kubernetes and main author of the Mastering Container Security training course which has been delivered at numerous industry conferences including Blackhat USA.