new doc, new examples, support -help
This commit is contained in:
parent
b7eeb51e82
commit
8c50f8412a
162
README.md
162
README.md
@ -1,52 +1,55 @@
|
|||||||
# reaction
|
# reaction
|
||||||
|
|
||||||
a program that scans program outputs, such as logs,
|
A daemon that scans program outputs for repeated patterns, and takes action.
|
||||||
for repeated patterns, such as failed login attempts,
|
|
||||||
and takes action, such as banning ips.
|
|
||||||
|
|
||||||
(adapted from [ fail2ban ](http://fail2ban.org)'s presentation 😄)
|
A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors.
|
||||||
|
|
||||||
🚧 this program hasn't received external audit. however, it already works well on my servers 🚧
|
🚧 This program hasn't received external audit. however, it already works well on my servers 🚧
|
||||||
|
|
||||||
## rationale
|
## Rationale
|
||||||
|
|
||||||
i was using fail2ban since quite a long time, but i was a bit frustrated by its cpu consumption
|
I was using the honorable fail2ban since quite a long time, but i was a bit frustrated by its cpu consumption
|
||||||
and all its heavy default configuration.
|
and all its heavy default configuration.
|
||||||
|
|
||||||
in my view, a security-oriented program should be simple to configure (`sudo` is a very bad example!)
|
In my view, a security-oriented program should be simple to configure
|
||||||
and an always-running daemon should be implemented in a fast language.
|
and an always-running daemon should be implemented in a fast*er* language.
|
||||||
|
|
||||||
<a href="https://u.ppom.me/reaction.webm">📽️ french example</a>
|
reaction does not have all the features of the honorable fail2ban, but it's ~10x faster and has more manageable configuration.
|
||||||
|
|
||||||
## configuration
|
<a href="https://u.ppom.me/reaction.webm">📽️ french quick explanation 😉</a>
|
||||||
|
|
||||||
this configuration file is all that should be needed to prevent brute force attacks on an ssh server.
|
## Configuration
|
||||||
|
|
||||||
see [ reaction.service ](./config/reaction.service) and [reaction.yml](./app/reaction.yml) for the fully explained examples.
|
YAML and [JSONnet](https://jsonnet.org/) (more powerful) are supported.
|
||||||
|
both are extensions of JSON, so JSON is transitively supported.
|
||||||
|
|
||||||
|
- See [reaction.yml](./app/example.yml) or [reaction.jsonnet](./config/example.jsonnet) for a fully explained reference
|
||||||
|
- See [server.jsonnet](./config/server.jsonnet) for a real-world configuration
|
||||||
|
- See [reaction.service](./config/reaction.service) for a systemd service file
|
||||||
|
- This quick example shows what's needed to prevent brute force attacks on an ssh server:
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
|
||||||
|
<summary><code>/etc/reaction.yml</code></summary>
|
||||||
|
|
||||||
`/etc/reaction.yml`
|
|
||||||
```yaml
|
```yaml
|
||||||
definitions:
|
|
||||||
- &iptablesban [ "ip46tables" "-w" "-I" "reaction" "1" "-s" "<ip>" "-j" "block" ]
|
|
||||||
- &iptablesunban [ "ip46tables" "-w" "-D" "reaction" "1" "-s" "<ip>" "-j" "block" ]
|
|
||||||
|
|
||||||
patterns:
|
patterns:
|
||||||
ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})'
|
ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})'
|
||||||
|
|
||||||
start:
|
start:
|
||||||
- [ "ip46tables", "-w", "-N", "reaction" ]
|
- [ 'ip46tables', '-w', '-N', 'reaction' ]
|
||||||
- [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ]
|
- [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ]
|
||||||
- [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]
|
- [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]
|
||||||
- [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ]
|
- [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
- [ "ip46tables", "-w," "-D," "INPUT", "-p", "all", "-j", "reaction" ]
|
- [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]
|
||||||
- [ "ip46tables", "-w," "-F," "reaction" ]
|
- [ 'ip46tables', '-w', '-F', 'reaction' ]
|
||||||
- [ "ip46tables", "-w," "-X," "reaction" ]
|
- [ 'ip46tables', '-w', '-X', 'reaction' ]
|
||||||
|
|
||||||
streams:
|
streams:
|
||||||
ssh:
|
ssh:
|
||||||
cmd: [ "journalctl" "-fu" "sshd.service" ]
|
cmd: [ 'journalctl', '-fu', 'sshd.service' ]
|
||||||
filters:
|
filters:
|
||||||
failedlogin:
|
failedlogin:
|
||||||
regex:
|
regex:
|
||||||
@ -55,36 +58,46 @@ streams:
|
|||||||
retryperiod: '6h'
|
retryperiod: '6h'
|
||||||
actions:
|
actions:
|
||||||
ban:
|
ban:
|
||||||
cmd: *iptablesban
|
cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '<ip>', '-j', 'block' ]
|
||||||
unban:
|
unban:
|
||||||
cmd: *iptablesunban
|
cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'block' ]
|
||||||
after: '48h'
|
after: '48h'
|
||||||
```
|
```
|
||||||
|
|
||||||
jsonnet is also supported:
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary><code>/etc/reaction.jsonnet</code></summary>
|
||||||
|
|
||||||
`/etc/reaction.jsonnet`
|
|
||||||
```jsonnet
|
```jsonnet
|
||||||
local iptables(args) = [ 'ip46tables', '-w' ] + args;
|
local iptables(args) = [ 'ip46tables', '-w' ] + args;
|
||||||
local iptablesban(ip) = iptables([ '-A', 'reaction', '1', '-s', ip, '-j', 'DROP' ]);
|
local banFor(time) = {
|
||||||
local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DROP' ]);
|
ban: {
|
||||||
|
cmd: iptables(['-A', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
|
||||||
|
},
|
||||||
|
unban: {
|
||||||
|
after: time,
|
||||||
|
cmd: iptables(['-D', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
|
||||||
|
},
|
||||||
|
};
|
||||||
{
|
{
|
||||||
patterns: {
|
patterns: {
|
||||||
ip: {
|
ip: {
|
||||||
regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})',
|
regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
start: {
|
start: [
|
||||||
iptables([ "-N", "reaction" ]),
|
iptables([ '-N', 'reaction' ]),
|
||||||
iptables([ "-A", "reaction", "-j", "ACCEPT" ]),
|
iptables([ '-A', 'reaction', '-j', 'ACCEPT' ]),
|
||||||
iptables([ "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]),
|
iptables([ '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]),
|
||||||
iptables([ "-I", "INPUT", "-p", "all", "-j", "reaction" ]),
|
iptables([ '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
|
||||||
},
|
],
|
||||||
stop: {
|
stop: [
|
||||||
iptables([ "-D,", "INPUT", "-p", "all", "-j", "reaction" ]),
|
iptables([ '-D,', 'INPUT', '-p', 'all', '-j', 'reaction' ]),
|
||||||
iptables([ "-F,", "reaction" ]),
|
iptables([ '-F,', 'reaction' ]),
|
||||||
iptables([ "-X,", "reaction" ]),
|
iptables([ '-X,', 'reaction' ]),
|
||||||
},
|
],
|
||||||
streams: {
|
streams: {
|
||||||
ssh: {
|
ssh: {
|
||||||
cmd: [ 'journalctl', '-fu', 'sshd.service' ],
|
cmd: [ 'journalctl', '-fu', 'sshd.service' ],
|
||||||
@ -93,16 +106,7 @@ local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DRO
|
|||||||
regex: [ @'authentication failure;.*rhost=<ip>' ],
|
regex: [ @'authentication failure;.*rhost=<ip>' ],
|
||||||
retry: 3,
|
retry: 3,
|
||||||
retryperiod: '6h',
|
retryperiod: '6h',
|
||||||
actions: {
|
actions: banFor('48h'),
|
||||||
ban: {
|
|
||||||
cmd: iptablesban('<ip>'),
|
|
||||||
},
|
|
||||||
unban: {
|
|
||||||
cmd: iptablesunban('<ip>'),
|
|
||||||
after: '48h',
|
|
||||||
onexit: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -110,57 +114,45 @@ local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DRO
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
note that both yaml and jsonnet are extensions of json, so json is also inherently supported.
|
</details>
|
||||||
|
|
||||||
`/etc/systemd/system/reaction.service`
|
|
||||||
```systemd
|
|
||||||
[ Unit ]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
[ Service ]
|
### Database
|
||||||
ExecStart=/path/to/reaction -c /etc/reaction.yml
|
|
||||||
|
|
||||||
StateDirectory=reaction
|
The embedded database is stored in the working directory.
|
||||||
RuntimeDirectory=reaction
|
If you don't know where to start reaction, `/var/lib/reaction` should be a sane choice.
|
||||||
WorkingDirectory=/var/lib/reaction
|
|
||||||
```
|
|
||||||
|
|
||||||
### database
|
### CLI
|
||||||
|
|
||||||
the working directory of `reaction` will be used to create and read from the embedded database.
|
- `reaction start` runs the server
|
||||||
if you don't know where to start it, `/var/lib/reaction` should be a sane choice.
|
- `reaction show` show pending actions (ie. bans)
|
||||||
|
- `reaction flush` permits to run pending actions (ie. clear bans)
|
||||||
### socket
|
- `reaction test-regex` permits to test regexes
|
||||||
|
- `reaction help` for full usage.
|
||||||
the socket allowing communication between the cli and server will be created at `/run/reaction/reaction.socket`.
|
|
||||||
|
|
||||||
### `ip46tables`
|
### `ip46tables`
|
||||||
|
|
||||||
`ip46tables` is a minimal c program present in its own subdirectory with only standard posix dependencies.
|
`ip46tables` is a minimal c program present in its own subdirectory with only standard posix dependencies.
|
||||||
|
|
||||||
it permits to configure `iptables` and `ip6tables` at the same time.
|
It permits to configure `iptables` and `ip6tables` at the same time.
|
||||||
it will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6 and both if no ip address is present on the command line.
|
It will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6 and both if no ip address is present on the command line.
|
||||||
|
|
||||||
### compilation
|
### Compilation
|
||||||
|
|
||||||
you'll need the go toolchain for reaction and a c compiler for ip46tables.
|
You'll need the go toolchain for reaction and a c compiler for ip46tables.
|
||||||
```shell
|
```shell
|
||||||
$ make
|
$ make
|
||||||
```
|
```
|
||||||
|
|
||||||
alternatively,
|
Alternatively,
|
||||||
```shell
|
```shell
|
||||||
|
# creates ./reaction
|
||||||
$ go build .
|
$ go build .
|
||||||
|
# creates ./ip46tables
|
||||||
$ gcc ip46tables.d/ip46tables.c -o ip46tables
|
$ gcc ip46tables.d/ip46tables.c -o ip46tables
|
||||||
```
|
```
|
||||||
|
|
||||||
### nixos
|
### NixOS
|
||||||
|
|
||||||
in addition to the [ package ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/pkgs/reaction/default.nix)
|
- [ package ](https://framagit.org/ppom/nixos/-/blob/main/pkgs/reaction/default.nix)
|
||||||
and [ module ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction.nix)
|
- [ module ](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix)
|
||||||
that i didn't try to upstream to nixpkgs yet (although they are ready), i use extensively reaction on my servers. if you're using nixos,
|
|
||||||
consider reading and building upon [ my own building blocks ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction-variables.nix),
|
|
||||||
[ my own non-root reaction conf, including conf for SSH, port scanning & Nginx common attack URLS ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction-custom.nix),
|
|
||||||
and the configuration for [ nextcloud ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/file.ppom.me.nix#L53),
|
|
||||||
[ vaultwarden ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/vaultwarden.nix#L45),
|
|
||||||
and [ maddy ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/mail.nix#L74). see also an [example](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/musi/mail.nix#L85) where it does something else than banning IPs.
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
# using YAML anchors `&name` and pointers `*name`
|
# using YAML anchors `&name` and pointers `*name`
|
||||||
# definitions are not readed by reaction
|
# definitions are not readed by reaction
|
||||||
definitions:
|
definitions:
|
||||||
- &iptablesban [ "ip46tables", "-w", "-A", "reaction", "1", "-s", "<ip>", "-j", "DROP" ]
|
- &iptablesban [ 'ip46tables', '-w', '-A', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
|
||||||
- &iptablesunban [ "ip46tables", "-w", "-D", "reaction", "1", "-s", "<ip>", "-j", "DROP" ]
|
- &iptablesunban [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP' ]
|
||||||
# ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory.
|
# ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory.
|
||||||
# it permits to handle both ipv4/iptables and ipv6/ip6tables commands
|
# it permits to handle both ipv4/iptables and ipv6/ip6tables commands
|
||||||
|
|
||||||
@ -20,20 +20,20 @@ patterns:
|
|||||||
|
|
||||||
# Those commands will be executed in order at start, before everything else
|
# Those commands will be executed in order at start, before everything else
|
||||||
start:
|
start:
|
||||||
- [ "ip46tables", "-w", "-N", "reaction" ]
|
- [ 'ip46tables', '-w', '-N', 'reaction' ]
|
||||||
- [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ]
|
- [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ]
|
||||||
- [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]
|
- [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]
|
||||||
- [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ]
|
- [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]
|
||||||
|
|
||||||
# Those commands will be executed in order at stop, after everything else
|
# Those commands will be executed in order at stop, after everything else
|
||||||
stop:
|
stop:
|
||||||
- [ "ip46tables", "-w,", "-D", "INPUT", "-p", "all", "-j", "reaction" ]
|
- [ 'ip46tables', '-w,', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]
|
||||||
- [ "ip46tables", "-w", "-F", "reaction" ]
|
- [ 'ip46tables', '-w', '-F', 'reaction' ]
|
||||||
- [ "ip46tables", "-w", "-X", "reaction" ]
|
- [ 'ip46tables', '-w', '-X', 'reaction' ]
|
||||||
|
|
||||||
|
|
||||||
# streams are commands
|
# streams are commands
|
||||||
# they're run and their ouptut is captured
|
# they are run and their ouptut is captured
|
||||||
# *example:* `tail -f /var/log/nginx/access.log`
|
# *example:* `tail -f /var/log/nginx/access.log`
|
||||||
# their output will be used by one or more filters
|
# their output will be used by one or more filters
|
||||||
streams:
|
streams:
|
||||||
@ -41,7 +41,7 @@ streams:
|
|||||||
ssh:
|
ssh:
|
||||||
# note that if the command is not in environment's `PATH`
|
# note that if the command is not in environment's `PATH`
|
||||||
# its full path must be given.
|
# its full path must be given.
|
||||||
cmd: [ "journalctl", "-n0", "-fu", "sshd.service" ]
|
cmd: [ 'journalctl', '-n0', '-fu', 'sshd.service' ]
|
||||||
# filters run actions when they match regexes on a stream
|
# filters run actions when they match regexes on a stream
|
||||||
filters:
|
filters:
|
||||||
# filters have a user-defined name
|
# filters have a user-defined name
|
||||||
@ -73,7 +73,7 @@ streams:
|
|||||||
onexit: true
|
onexit: true
|
||||||
# (defaults to false)
|
# (defaults to false)
|
||||||
# here it is not useful because we will flush the chain containing the bans anyway
|
# here it is not useful because we will flush the chain containing the bans anyway
|
||||||
# (see /conf/reaction.service)
|
# (with the stop commands)
|
||||||
|
|
||||||
# persistence
|
# persistence
|
||||||
# tldr; when an `after` action is set in a filter, such filter acts as a 'jail',
|
# tldr; when an `after` action is set in a filter, such filter acts as a 'jail',
|
||||||
|
@ -118,13 +118,10 @@ func Main() {
|
|||||||
logger.Fatalln("No argument provided. Try `reaction help`")
|
logger.Fatalln("No argument provided. Try `reaction help`")
|
||||||
basicUsage()
|
basicUsage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else if os.Args[1] == "-h" || os.Args[1] == "--help" {
|
|
||||||
basicUsage()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
f := flag.NewFlagSet(os.Args[1], flag.ExitOnError)
|
f := flag.NewFlagSet(os.Args[1], flag.ExitOnError)
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "-help", "--help":
|
||||||
basicUsage()
|
basicUsage()
|
||||||
|
|
||||||
case "example-conf":
|
case "example-conf":
|
||||||
@ -224,7 +221,7 @@ func Main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.Fatalln("subcommand not recognized")
|
logger.Fatalf("subcommand %v not recognized. Try `reaction help`", os.Args[1])
|
||||||
basicUsage()
|
basicUsage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,8 @@
|
|||||||
// JSONNET is a superset of JSON, so one can write plain JSON files if wanted.
|
// JSONNET is a superset of JSON, so one can write plain JSON files if wanted.
|
||||||
// Note that YAML is also supported, see ./example.yml
|
// Note that YAML is also supported, see ./example.yml
|
||||||
|
|
||||||
// A JSONNET function
|
// JSONNET functions
|
||||||
local iptables(args) = ['ip46tables', '-w'] + args;
|
local iptables(args) = ['ip46tables', '-w'] + args;
|
||||||
// variables defined for later use.
|
|
||||||
local iptablesban = iptables(['-A', 'reaction', '1', '-s', '<ip>', '-j', 'drop']);
|
|
||||||
local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '<ip>', '-j', 'drop']);
|
|
||||||
// ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory.
|
// ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory.
|
||||||
// it permits to handle both ipv4/iptables and ipv6/ip6tables commands
|
// it permits to handle both ipv4/iptables and ipv6/ip6tables commands
|
||||||
|
|
||||||
@ -48,7 +45,7 @@ local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '<ip>', '-j', 'drop
|
|||||||
],
|
],
|
||||||
|
|
||||||
// streams are commands
|
// streams are commands
|
||||||
// they're run and their ouptut is captured
|
// they are run and their ouptut is captured
|
||||||
// *example:* `tail -f /var/log/nginx/access.log`
|
// *example:* `tail -f /var/log/nginx/access.log`
|
||||||
// their output will be used by one or more filters
|
// their output will be used by one or more filters
|
||||||
streams: {
|
streams: {
|
||||||
@ -77,11 +74,10 @@ local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '<ip>', '-j', 'drop
|
|||||||
actions: {
|
actions: {
|
||||||
// actions have a user-defined name
|
// actions have a user-defined name
|
||||||
ban: {
|
ban: {
|
||||||
// JSONNET substitutes the variable (defined at the beginning of the file)
|
cmd: iptables(['-A', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
|
||||||
cmd: iptablesban,
|
|
||||||
},
|
},
|
||||||
unban: {
|
unban: {
|
||||||
cmd: iptablesunban,
|
cmd: iptables(['-D', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse']),
|
||||||
// if after is defined, the action will not take place immediately, but after a specified duration
|
// if after is defined, the action will not take place immediately, but after a specified duration
|
||||||
// same format as retryperiod
|
// same format as retryperiod
|
||||||
after: '48h',
|
after: '48h',
|
||||||
@ -90,7 +86,7 @@ local iptablesunban = iptables(['-D', 'reaction', '1', '-s', '<ip>', '-j', 'drop
|
|||||||
onexit: true,
|
onexit: true,
|
||||||
// (defaults to false)
|
// (defaults to false)
|
||||||
// here it is not useful because we will flush the chain containing the bans anyway
|
// here it is not useful because we will flush the chain containing the bans anyway
|
||||||
// (see /conf/reaction.service)
|
// (with the stop commands)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@ patterns:
|
|||||||
|
|
||||||
streams:
|
streams:
|
||||||
tailDown1:
|
tailDown1:
|
||||||
cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo found $(($i % 100)); done" ]
|
cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo found $(($i % 100)); done' ]
|
||||||
filters:
|
filters:
|
||||||
findIP:
|
findIP:
|
||||||
regex:
|
regex:
|
||||||
@ -18,13 +18,13 @@ streams:
|
|||||||
retryperiod: 1m
|
retryperiod: 1m
|
||||||
actions:
|
actions:
|
||||||
damn:
|
damn:
|
||||||
cmd: [ "echo", "<num>" ]
|
cmd: [ 'echo', '<num>' ]
|
||||||
undamn:
|
undamn:
|
||||||
cmd: [ "echo", "undamn", "<num>" ]
|
cmd: [ 'echo', 'undamn', '<num>' ]
|
||||||
after: 1m
|
after: 1m
|
||||||
onexit: false
|
onexit: false
|
||||||
tailDown2:
|
tailDown2:
|
||||||
cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo prout $(($i % 100)); done" ]
|
cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo prout $(($i % 100)); done' ]
|
||||||
filters:
|
filters:
|
||||||
findIP:
|
findIP:
|
||||||
regex:
|
regex:
|
||||||
@ -33,13 +33,13 @@ streams:
|
|||||||
retryperiod: 1m
|
retryperiod: 1m
|
||||||
actions:
|
actions:
|
||||||
damn:
|
damn:
|
||||||
cmd: [ "echo", "<num>" ]
|
cmd: [ 'echo', '<num>' ]
|
||||||
undamn:
|
undamn:
|
||||||
cmd: [ "echo", "undamn", "<num>" ]
|
cmd: [ 'echo', 'undamn', '<num>' ]
|
||||||
after: 1m
|
after: 1m
|
||||||
onexit: false
|
onexit: false
|
||||||
tailDown3:
|
tailDown3:
|
||||||
cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done" ]
|
cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done' ]
|
||||||
filters:
|
filters:
|
||||||
findIP:
|
findIP:
|
||||||
regex:
|
regex:
|
||||||
@ -48,13 +48,13 @@ streams:
|
|||||||
retryperiod: 2m
|
retryperiod: 2m
|
||||||
actions:
|
actions:
|
||||||
damn:
|
damn:
|
||||||
cmd: [ "true" ]
|
cmd: [ 'true' ]
|
||||||
undamn:
|
undamn:
|
||||||
cmd: [ "true" ]
|
cmd: [ 'true' ]
|
||||||
after: 1m
|
after: 1m
|
||||||
onexit: false
|
onexit: false
|
||||||
tailDown4:
|
tailDown4:
|
||||||
cmd: [ "sh", "-c", "sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done" ]
|
cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done' ]
|
||||||
filters:
|
filters:
|
||||||
findIP:
|
findIP:
|
||||||
regex:
|
regex:
|
||||||
@ -63,8 +63,8 @@ streams:
|
|||||||
retryperiod: 2m
|
retryperiod: 2m
|
||||||
actions:
|
actions:
|
||||||
damn:
|
damn:
|
||||||
cmd: [ "echo", "<num>" ]
|
cmd: [ 'echo', '<num>' ]
|
||||||
undamn:
|
undamn:
|
||||||
cmd: [ "echo", "undamn", "<num>" ]
|
cmd: [ 'echo', 'undamn', '<num>' ]
|
||||||
after: 1m
|
after: 1m
|
||||||
onexit: false
|
onexit: false
|
||||||
|
@ -4,7 +4,7 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
# See `man systemd.exec` and `man systemd.service` for most options below
|
# See `man systemd.exec` and `man systemd.service` for most options below
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/path/to/reaction -c /etc/reaction.yml
|
ExecStart=/path/to/reaction start -c /etc/reaction.yml
|
||||||
|
|
||||||
# Ask systemd to create /var/lib/reaction (/var/lib/ is implicit)
|
# Ask systemd to create /var/lib/reaction (/var/lib/ is implicit)
|
||||||
StateDirectory=reaction
|
StateDirectory=reaction
|
||||||
|
147
config/server.jsonnet
Normal file
147
config/server.jsonnet
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// This is the extensive configuration used on a **real** server!
|
||||||
|
|
||||||
|
local banFor(time) = {
|
||||||
|
ban: {
|
||||||
|
cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse'],
|
||||||
|
},
|
||||||
|
unban: {
|
||||||
|
after: time,
|
||||||
|
cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '<ip>', '-j', 'reaction-log-refuse'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
patterns: {
|
||||||
|
// IPs can be IPv4 or IPv6
|
||||||
|
// ip46tables (C program also in this repo) handles running the good commands
|
||||||
|
ip: {
|
||||||
|
regex: @'(([0-9]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
streams: {
|
||||||
|
// Ban hosts failing to connect via ssh
|
||||||
|
ssh: {
|
||||||
|
cmd: [' journalctl', '-fn0', '-u', 'sshd.service'],
|
||||||
|
filters: {
|
||||||
|
failedlogin: {
|
||||||
|
regex: [
|
||||||
|
@'authentication failure;.*rhost=<ip>',
|
||||||
|
@'Connection reset by authenticating user .* <ip>',
|
||||||
|
],
|
||||||
|
retry: 3,
|
||||||
|
retryperiod: '6h',
|
||||||
|
actions: banFor('48h'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ban hosts which knock on closed ports.
|
||||||
|
// It needs this iptables chain to be used to drop packets:
|
||||||
|
// ip46tables -N log-refuse
|
||||||
|
// ip46tables -A log-refuse -p tcp --syn -j LOG --log-level info --log-prefix 'refused connection: '
|
||||||
|
// ip46tables -A log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
|
||||||
|
// ip46tables -A log-refuse -j DROP
|
||||||
|
kernel: {
|
||||||
|
cmd: ['journalctl', '-fn0', '-k'],
|
||||||
|
filters: {
|
||||||
|
portscan: {
|
||||||
|
regex: ['refused connection: .*SRC=<ip>'],
|
||||||
|
retry: 4,
|
||||||
|
retryperiod: '1h',
|
||||||
|
actions: banFor('720h'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note: nextcloud and vaultwarden could also be filters on the nginx stream
|
||||||
|
// I did use their own logs instead because it's less logs to parse than the front webserver
|
||||||
|
|
||||||
|
// Ban hosts failing to connect to Nextcloud
|
||||||
|
nextcloud: {
|
||||||
|
cmd: ['journalctl', '-fn0', '-u', 'phpfpm-nextcloud.service'],
|
||||||
|
filters: {
|
||||||
|
failedLogin: {
|
||||||
|
regex: [
|
||||||
|
@'"remoteAddr":"<ip>".*"message":"Login failed:',
|
||||||
|
@'"remoteAddr":"<ip>".*"message":"Trusted domain error.',
|
||||||
|
],
|
||||||
|
retry: 3,
|
||||||
|
retryperiod: '1h',
|
||||||
|
actions: banFor('1h'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ban hosts failing to connect to vaultwarden
|
||||||
|
vaultwarden: {
|
||||||
|
cmd: ['journalctl', '-fn0', '-u', 'vaultwarden.service'],
|
||||||
|
filters: {
|
||||||
|
failedlogin: {
|
||||||
|
actions: banFor('2h'),
|
||||||
|
regex: [@'Username or password is incorrect\. Try again\. IP: <ip>\. Username:'],
|
||||||
|
retry: 3,
|
||||||
|
retryperiod: '1h',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Used with this nginx log configuration:
|
||||||
|
// log_format withhost '$remote_addr - $remote_user [$time_local] $host "$request" $status $bytes_sent "$http_referer" "$http_user_agent"';
|
||||||
|
// access_log /var/log/nginx/access.log withhost;
|
||||||
|
nginx: {
|
||||||
|
cmd: ['tail', '-n0', '-f', '/var/log/nginx/access.log'],
|
||||||
|
filters: {
|
||||||
|
// Ban hosts failing to connect to Directus
|
||||||
|
directus: {
|
||||||
|
regex: [
|
||||||
|
@'^<ip> .* "POST /auth/login HTTP/..." 401 [0-9]+ .https://directusdomain',
|
||||||
|
],
|
||||||
|
retry: 6,
|
||||||
|
retryperiod: '4h',
|
||||||
|
actions: banFor('4h'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ban hosts presenting themselves as bots of ChatGPT
|
||||||
|
gptbot: {
|
||||||
|
regex: [@'^<ip>.*GPTBot/1.0'],
|
||||||
|
action: banFor('720h'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ban hosts failing to connect to slskd
|
||||||
|
slskd: {
|
||||||
|
regex: [
|
||||||
|
@'^<ip> .* "POST /api/v0/session HTTP/..." 401 [0-9]+ .https://slskddomain',
|
||||||
|
],
|
||||||
|
retry: 3,
|
||||||
|
retryperiod: '1h',
|
||||||
|
actions: banFor('6h'),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ban suspect HTTP requests
|
||||||
|
// Those are frequent malicious requests I got from bots
|
||||||
|
// Make sure you don't have honnest use cases for those requests, or your clients may be banned for 2 weeks!
|
||||||
|
suspectRequests: {
|
||||||
|
regex: [
|
||||||
|
// (?:[^/" ]*/)* is a "non-capturing group" regex that allow for subpath(s)
|
||||||
|
// example: /code/.env should be matched as well as /.env
|
||||||
|
// ^^^^^
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*\.env ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*info\.php ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*owa/auth/logon.aspx ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*auth.html ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*auth1.html ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*password.txt ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*passwords.txt ',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*dns-query ',
|
||||||
|
// Do not include this if you have a Wordpress website ;)
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*wp-login\.php',
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*wp-includes',
|
||||||
|
// Do not include this if a client must retrieve a config.json file ;)
|
||||||
|
@'^<ip>.*"GET /(?:[^/" ]*/)*config\.json ',
|
||||||
|
],
|
||||||
|
action: banFor('720h'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -39,7 +39,6 @@
|
|||||||
},
|
},
|
||||||
tailDown2: {
|
tailDown2: {
|
||||||
cmd: ['sh', '-c', 'echo coucou; sleep 2m'],
|
cmd: ['sh', '-c', 'echo coucou; sleep 2m'],
|
||||||
// cmd: ['sh', '-c', "echo 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 6 7 8 9 | tr ' ' '\n' | while read i; do sleep 3; echo found $(($i % 60)); done"],
|
|
||||||
filters: {
|
filters: {
|
||||||
findIP: {
|
findIP: {
|
||||||
regex: ['^found <num>$'],
|
regex: ['^found <num>$'],
|
||||||
|
Loading…
Reference in New Issue
Block a user