Design considerations for new boefjes runner

The new boefjes runner will run boefjes in a containerized environment. This ensures isolation of code and dependencies, and allows for easy distribution of boefjes outside the KAT repository and release cycle.

A boefje can be written in any language, as long as it follows the I/O contract (see below) and it is properly packaged in an OCI image.

Images

OCI images will be used to package the boefjes’ code, its dependencies (libraries) and required tools such as Nmap.

Distribution

OCI images can be distributed in any OCI registry, such as Docker Hub or GitHub Container Registry. Several open source projects are available to create a self-hosted OCI registry, such as Harbor.

In the future, boefjes can be imported into the KATalogus using its OCI image URL. It can be pinned to a version-specific tag, SHA256 identifier, or simply use the latest tag. A JSON list of recommended boefjes will be included with each OpenKAT release, or published to https://openkat.nl.

Metadata

To import a boefje into the KATalogus, we need some of its metadata. We can distribute this metadata together with the image by leveraging the OCI image manifest. For example, we could add an annotation to the manifest with a well-known name and predefined format, such as JSON, or add multiple annotations for each metadata attribute.

The essential metadata includes:

  • Name

  • Version

  • Description

  • Image URL

  • Settings schema

  • List of OOI types that the boefje works on

  • Boefjes runner HTTP API version

  • Minimum compatible KAT version (for OOI schema compatibility)

I/O

Because stdin and stdout in container orchestrators are relatively complicated and work on a best-effort basis, this is not reliable enough for boefje input and output. Also see the OpenAPI docs, where you can also find the full OpenAPI specification. Kubernetes will for example redirect stdout and stderr to log files and will by default rotate the log file when it gets larger than 10 MB. See the Kubernetes logging documentation for more information about this. Tools like filebeat also work by mounting the host /var/log/containers in the filebeat container. This is something that can be done with a cluster component like filebeat that is supposed to have access to the log files of all containers. This should not be done with an application like OpenKAT, because OpenKAT should not have access to the log files of other applications that are running on the Kubernetes cluster.

Copying files from the container is also not an option, because for example the kubectl cp command to copy files from a container actually executes tar in the container using kubectl exec. There is also no guarantee that the container will be around when it’s done, because after the container exits it is usually removed right away.

Because of this we designed a simple HTTP API for input and output. This HTTP API will be part of new boefjes runner and will communicate with existing parts of KAT such as bytes and mula (the scheduler) to get the boefje input and save its output.

The HTTP API will be versioned, so that the API can evolve while staying compatible with existing boefjes.

Input

The container will get a URL of an API endpoint that will provide its input as one of its command line arguments. The container will then make a GET request to this URL to get the input.

The input is a JSON object, specified by the following JSON schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openkat.nl/boefje_input.schema.json",
  "title": "Boefje input",
  "properties": {
    "task_id": {
      "type": "string"
    },
    "output_url": {
      "type": "string"
    },
    "boefje_meta": {
      "type": "object",
      "properties": {
        "boefje": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string"
            },
            "version": {
              "type": "string"
            }
          }
        },
        "input_ooi": {
          "type": "string"
        },
        "arguments": {
          "type": "object"
        },
        "organization": {
          "type": "string"
        },
        "environment": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      }
    },
    "required": [
      "boefje",
      "input_ooi",
      "arguments",
      "organization",
      "environment"
    ]
  },
  "required": ["task_id", "output_url", "boefje_meta"]
}

Output

When the container is finished, it can POST its output to the URL specified in the input JSON object. The output is a JSON object, specified by the following JSON schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openkat.nl/boefje_output.schema.json",
  "title": "Boefje output",
  "properties": {
    "status": {
      "type": "string",
      "enum": ["COMPLETED", "FAILED"]
    },
    "files": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "content": {
            "type": "string",
            "contentEncoding": "base64"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["content"]
      }
    }
  },
  "required": ["status"]
}

The tags for each file can include a MIME type.

Logging

Logging will be captured through the container’s orchestrator/runtime API and stored in Bytes. Alternatively, the boefje can output its own logging in a separate file as part of its output, which will be stored in Bytes as well.

Runtimes

Docker

Docker containers can be run as one-off jobs by creating a container, polling its status on a regular interval, and removing it when it is finished.

An official, well-maintained Python API is available:

  • https://pypi.org/project/docker/

  • https://docker-py.readthedocs.io/en/stable/

  • https://github.com/docker/docker-py

Logging can be captured through the API, but the specifics of available on the logging driver: https://docs.docker.com/config/containers/logging/json-file/

Kubernetes

Kubernetes has a specific object type for one-off workloads: Jobs.

These Jobs can be created through the Kubernetes API, and their status can be polled (pull-based) or watched (push-based) through the API as well.

An official, well-maintained Python API is available:

  • https://pypi.org/project/kubernetes/

  • https://github.com/kubernetes-client/python

Logging can be captured through the API, but logs get rotated so a large volume of logs may not be fully available. See https://kubernetes.io/docs/concepts/cluster-administration/logging/ for more details.

Nomad

Nomad can run one-off jobs by setting the job type to ‘batch’:

  • https://developer.hashicorp.com/nomad/docs/job-specification/job#type

  • https://developer.hashicorp.com/nomad/docs/schedulers#batch

An unofficial Python API is available:

  • https://pypi.org/project/python-nomad/

  • https://github.com/jrxfive/python-nomad

Logging can be captured through the API, but Nomad does not retain logs for long periods of time. From https://developer.hashicorp.com/nomad/tutorials/manage-jobs/jobs-accessing-logs:

While the logs command works well for quickly accessing application logs, it generally does not scale to large systems or systems that produce a lot of log output, especially for the long-term storage of logs. Nomad’s retention of log files is best effort, so chatty applications should use a better log retention strategy.

Building images with this spec from the current boefjes

The approach to building OCI images from the boefjes we currently have in our system has been discussed in this ticket, with the first versions having been implemented in these PRs:

  • https://github.com/minvws/nl-kat-coordination/pull/2709

  • https://github.com/minvws/nl-kat-coordination/pull/2832

Summary of decisions

We decided not to focus on the following:

  • We are not going to provide plain zip archives in the near future.

  • Discoverability of images from external repositories (potentially containing multiple boefjes) will be pushed to later versions of OpenKAT.

In terms of how we are going to build images, we decided to:

  • Just leverage Docker as this has to be available for OpenKAT devs anyway.

  • Aim to keep the build scripts flexible but simple, e.g. for kat_dnssec we have:

docker build -f ./boefjes/plugins/kat_dnssec/boefje.Dockerfile -t openkat/dns-sec --build-arg BOEFJE_PATH=./boefjes/plugins/kat_dnssec .
  • Use, as shown above, the naming convention for Dockerfiles since we may want to add normaliser Dockerfiles in the same directory.

  • Use a Python base image for all our boefjes, so we can use shared Python code to communicate with the boefjes API. Since there is no one tool available across Docker base images that can perform HTTP communication, we might as well use Python for this. Other possible tools to perform HTTP communication are curl, wget and/or other HTTP clients. Later, we can consider creating platform-specific, pre-built binaries using languages such as Go or Rust.

  • In particular, build the images using a python:3.11-slim base image. A basic check shows the following sizes per base image, but Alpine does not support standard PyPI wheels:

python:3.11

python:3.11-slim

python:3.11-alpine

1.01 GB

157 MB

57 MB

In terms of when to build images, we decided to:

  • Make the builds part of the installation script through make -C boefjes images.

  • Put the responsibility to (re)build new images while developing boefjes on developers.

Limitations

In this design the boefjes runner will create a new container for each task, which has a non-negligible overhead. This overhead can be reduced by batching multiple tasks in a single container run. This design does not currently consider that to ensure the implementation is as simple as possible. It can be added to the runner in the future, but will also require changes to the KAT scheduler to support scheduling batched tasks. Also see the following issues and discussions to see the progress on this (performance) feature:

  • https://github.com/minvws/nl-kat-coordination/issues/2613

  • https://github.com/minvws/nl-kat-coordination/issues/2857

  • https://github.com/minvws/nl-kat-coordination/issues/2811