FAQ

Here's a FAQ for reaction:

What is JSONnet? Why should I use it over YAML?

Answer

JSONnet already has a good tutorial to start with.

It permits to define functions, variables, etc, and thus to avoid duplication.

Other configuration languages exist, like Nix, Dhall, etc. You should feel at home if you already used one of those.

JSONnet semantics also borrow from Javascript and Python.

Why start, stop, stream and action commands are arrays?

Answer

This is not always common knowledge, but this is how UNIX actually works.

Shells, like Bash, will perform a few actions on your command-line before creating a new process with the execve syscall (man 2 execve). Those actions include:

  • Expanding globs, most commonly *
  • Splitting parameters on white space (but not within single quotes and double quotes)
  • ...

Here's a list of Bash commands and what process are actually created:

command lineexecve syscallstdout
echo hello world['echo', 'hello', 'world']prints hello world
echo "hello world"['echo', 'hello world']prints hello world
echo   hello   world['echo', 'hello', 'world']prints hello world
echo "hello   world"['echo', 'hello   world']prints hello   world
echo /b*['echo', '/bin', '/boot']prints /bin /boot

What is important to understand here is that quotes, globs, etc, are not seen by echo. echo is executed only after the shell have performed those substitutions.

So commands written in reaction will be executed as is. The only substitution performed by reaction is to replace a Pattern by its corresponding Match in Actions.

See Security for reasons why you should be careful when executing actions with shell scripts. TL;DR: ['sh', '-c', 'inline script'] can lead to code injection by attackers.

How do I write defaults at one place and reuse them elsewhere?

Answer

While we don't recommend having defaults and prefer explicit configuration, it is possible with JSONnet.

A defaults set must first be defined.

Here's an example for default options for a filter:

local filter_default = {
  retry: 3,
  retryperiod: '3h',
  actions: banFor('24h'),
};

Then it can be used in filters:

{
  streams: {
    ssh: {
      filters: {
        failedlogin: filter_default + {
          regex: ['...'],
        },
      },
    },
  },
}

Defaults can be overriden:

{
  streams: {
    ssh: {
      filters: {
        failedlogin: filter_default + {
          regex: ['...'],
          // retry is overriden here
          retry: 1,
        },
      },
    },
  },
}

And the + is optional.

{
  streams: {
    ssh: {
      filters: {
        failedlogin: filter_default {
          regex: ['...'],
        },
      },
    },
  },
}

How do I add multiple actions defined by JSONnet functions on the same filter?

Answer

Let's take this example: we have two functions defining actions:

The first is made to ban the IP using linux's iptables firewall:

local banFor(time) = {
  ban: {
    cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
  unban: {
    after: time,
    cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
};

The second sends a mail:

local sendmail(text) = {
  mail: {
    cmd: ['sh', '-c', '/root/scripts/mailreaction.sh', text],
  },
};

Both create a set of actions. We want to merge the two sets.

To merge two sets with JSONnet, it's as easy as set1 + set2.

Let's see what it looks like with a real example.

local banFor(time) = {
  ban: {
    cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
  unban: {
    after: time,
    cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
};

local sendmail(text) = {
  mail: {
    cmd: ['sh', '-c', '/root/scripts/mailreaction.sh', text],
  },
};

{
  streams: {
    ssh: {
      filters: {
        failedlogin: {
          regex: [
            // skipping
          ],
          retry: 3,
          retryperiod: '3h',
          actions: banFor('720h') + sendmail('banned <ip> from service ssh'),
        },
      },
    },
  },
}

This will generate this configuration:

{
  "streams": {
    "ssh": {
      "filters": {
        "failedlogin": {
          "regex": [ ],
          "retry": 3,
          "retryperiod": "3h",
          "actions": {
            "ban": {
              "cmd": [ "ip46tables", "-w", "-A", "reaction", "-s", "<ip>", "-j", "DROP" ]
            },
            "unban": {
              "after": "720h",
              "cmd": [ "ip46tables", "-w", "-D", "reaction", "-s", "<ip>", "-j", "DROP" ]
            },
            "mail": {
              "cmd": [ "sh", "-c", "/root/scripts/mailreaction.sh", "banned <ip> from service ssh" ]
            }
          }
        }
      }
    }
  }
}

How do I separate my configuration in multiple files?

Answer

Starting with reaction v2.0.0, you can specify a folder containing multiple configuration files. reaction will read and merge all files that:

  • Do not start with . or _.
  • And that ends with .json, .jsonnet, .yml or .yaml.

JSONnet files starting with _ or . will not be directly read. You can manually import them in other files, for example to store definitions used accross files:

You can use the import JSONnet keyword to import files, relative to the file calling them. (Remember, the JSONnet tutorial is a good place to understand its basics).

Here's an example of how you could do this:

/etc/reaction/main.jsonnet

{
  streams: {
    ssh: {
      cmd: [ "tail", "-F", /* skip */ ],
    },
    // ...
  },
}

/etc/reaction/ssh.jsonnet

local lib = import '_lib.jsonnet';
{
  streams: {
    ssh: {
        filters: {
          failedlogin: lib.filter_default + {
          regex: ['...'],
          retry: 3,
          retryperiod: '2h',
          actions: lib.banFor('30d'),
        },
      },
    },
  },
}

/etc/reaction/_lib.jsonnet (definitions you can reuse in all other files)

local banFor(time) = {
  ban: {
    cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
  unban: {
    after: time,
    cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'DROP'],
  },
};

local filter_default = {
  retry: 3,
  retryperiod: '3h',
  actions: banFor('24h'),
};

{
  banFor: banFor,
  filter_default: filter_default,
}

You can split different patterns, streams and filters accross files.

However, you can't spread the definition of one pattern or filter accross multiple files.

How do I use environment variables in actions?

Answer

Environment variables available to the reaction daemon are also available to stream's and actions's commands.

Specifying environment variables

You may specify Environment or EnvironmentFile attributes to the systemd file:

/etc/systemd/system/reaction.service or /etc/systemd/system/reaction.service.d/env.conf:

[Service]
Environment=MY_ENV_VAR=...
EnvironmentFile=/path/to/secrets/file

See man systemd.exec for more details. These options can be specified multiple times.

Using environment variables in scripts

Let's say you run a command with a custom shell script:

cmd: ['/usr/local/bin/my_script.sh', '<ip>']

You may use environment variables inside, for example: $MY_ENV_VAR.

echo "$MY_ENV_VAR"

For a Python script, variables are accessible with os.environ.

import os
print(os.environ['MY_ENV_VAR'])

Directly substituting environment variables in reaction actions

Note that this is not substituted, because this is a shell feature and reaction doesn't currently have any shell-like features:

# Doesn't work!
cmd: ['curl', '$HOOK_URL', '--json', '{ ... }']

See issue #127 for discussion.