Implement start/stop commands

fix #41
update README and configuration files accordingly
This commit is contained in:
ppom 2023-10-18 12:00:00 +02:00
parent d35167b878
commit 345dd94b17
8 changed files with 127 additions and 77 deletions

View File

@ -4,7 +4,7 @@ a program that scans program outputs, such as logs,
for repeated patterns, such as failed login attempts, for repeated patterns, such as failed login attempts,
and takes action, such as banning ips. and takes action, such as banning ips.
(adapted from [fail2ban](http://fail2ban.org)'s presentation 😄) (adapted from [ fail2ban ](http://fail2ban.org)'s presentation 😄)
🚧 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 🚧
@ -22,16 +22,27 @@ and an always-running daemon should be implemented in a fast language.
this configuration file is all that should be needed to prevent brute force attacks on an ssh server. this configuration file is all that should be needed to prevent brute force attacks on an ssh server.
see [reaction.service](./config/reaction.service) and [reaction.yml](./app/reaction.yml) for the fully explained examples. see [ reaction.service ](./config/reaction.service) and [reaction.yml](./app/reaction.yml) for the fully explained examples.
`/etc/reaction.yml` `/etc/reaction.yml`
```yaml ```yaml
definitions: definitions:
- &iptablesban [ "iptables" "-w" "-I" "reaction" "1" "-s" "<ip>" "-j" "block" ] - &iptablesban [ "ip46tables" "-w" "-I" "reaction" "1" "-s" "<ip>" "-j" "block" ]
- &iptablesunban [ "iptables" "-w" "-D" "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:
- [ "ip46tables", "-w", "-N", "reaction" ]
- [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ]
stop:
- [ "ip46tables", "-w," "-D," "INPUT", "-p", "all", "-j", "reaction" ]
- [ "ip46tables", "-w," "-F," "reaction" ]
- [ "ip46tables", "-w," "-X," "reaction" ]
streams: streams:
ssh: ssh:
@ -54,17 +65,29 @@ jsonnet is also supported:
`/etc/reaction.jsonnet` `/etc/reaction.jsonnet`
```jsonnet ```jsonnet
local iptablesban = ['iptables', '-w', '-A', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptables(args) = [ 'ip46tables', '-w' ] + args;
local iptablesunban = ['iptables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptablesban(ip) = iptables([ '-A', 'reaction', '1', '-s', ip, '-j', 'DROP' ]);
local iptablesunban(ip) = iptables([ '-D', 'reaction', '1', '-s', ip, '-j', 'DROP' ]);
{ {
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: {
iptables([ "-N", "reaction" ]),
iptables([ "-A", "reaction", "-j", "ACCEPT" ]),
iptables([ "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]),
iptables([ "-I", "INPUT", "-p", "all", "-j", "reaction" ]),
},
stop: {
iptables([ "-D,", "INPUT", "-p", "all", "-j", "reaction" ]),
iptables([ "-F,", "reaction" ]),
iptables([ "-X,", "reaction" ]),
},
streams: { streams: {
ssh: { ssh: {
cmd: ['journalctl', '-fu', 'sshd.service'], cmd: [ 'journalctl', '-fu', 'sshd.service' ],
filters: { filters: {
failedlogin: { failedlogin: {
regex: [ @'authentication failure;.*rhost=<ip>' ], regex: [ @'authentication failure;.*rhost=<ip>' ],
@ -72,10 +95,10 @@ local iptablesunban = ['iptables', '-w', '-D', 'reaction', '1', '-s', '<ip>', '-
retryperiod: '6h', retryperiod: '6h',
actions: { actions: {
ban: { ban: {
cmd: iptablesban, cmd: iptablesban('<ip>'),
}, },
unban: { unban: {
cmd: iptablesunban, cmd: iptablesunban('<ip>'),
after: '48h', after: '48h',
onexit: true, onexit: true,
}, },
@ -91,21 +114,12 @@ note that both yaml and jsonnet are extensions of json, so json is also inherent
`/etc/systemd/system/reaction.service` `/etc/systemd/system/reaction.service`
```systemd ```systemd
[Unit] [ Unit ]
WantedBy=multi-user.target WantedBy=multi-user.target
[Service] [ Service ]
ExecStart=/path/to/reaction -c /etc/reaction.yml ExecStart=/path/to/reaction -c /etc/reaction.yml
ExecStartPre=/path/to/iptables -w -N reaction
ExecStartPre=/path/to/iptables -w -A reaction -j ACCEPT
ExecStartPre=/path/to/iptables -w -I reaction 1 -s 127.0.0.1 -j ACCEPT
ExecStartPre=/path/to/iptables -w -I INPUT -p all -j reaction
ExecStopPost=/path/to/iptables -w -D INPUT -p all -j reaction
ExecStopPost=/path/to/iptables -w -F reaction
ExecStopPost=/path/to/iptables -w -X reaction
StateDirectory=reaction StateDirectory=reaction
RuntimeDirectory=reaction RuntimeDirectory=reaction
WorkingDirectory=/var/lib/reaction WorkingDirectory=/var/lib/reaction
@ -142,11 +156,11 @@ $ 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) in addition to the [ package ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/pkgs/reaction/default.nix)
and [module](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/modules/common/reaction.nix) and [ module ](https://framagit.org/ppom/nixos/-/blob/cf5448b21ae3386265485308a6cd077e8068ad77/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, 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), 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), [ 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), 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), [ 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. 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.

View File

@ -39,6 +39,15 @@ func cmdStdout(commandline []string) chan *string {
return lines return lines
} }
func runCommands(commands [][]string, moment string) {
for _, command := range commands {
cmd := exec.Command(command[0], command[1:]...)
if err := cmd.Start(); err != nil {
logger.Printf(logger.ERROR, "couldn't execute %v command: %v", moment, err)
}
}
}
func (p *Pattern) notAnIgnore(match *string) bool { func (p *Pattern) notAnIgnore(match *string) bool {
for _, ignore := range p.Ignore { for _, ignore := range p.Ignore {
if ignore == *match { if ignore == *match {
@ -323,6 +332,8 @@ func Daemon(confFilename string) {
actions = make(ActionsMap) actions = make(ActionsMap)
matches = make(MatchesMap) matches = make(MatchesMap)
runCommands(conf.Start, "start")
go DatabaseManager(conf) go DatabaseManager(conf)
go MatchesManager() go MatchesManager()
go ActionsManager() go ActionsManager()
@ -348,16 +359,16 @@ func Daemon(confFilename string) {
logger.Printf(logger.ERROR, "%s stream finished", finishedStream.name) logger.Printf(logger.ERROR, "%s stream finished", finishedStream.name)
nbStreamsInExecution-- nbStreamsInExecution--
if nbStreamsInExecution == 0 { if nbStreamsInExecution == 0 {
quit() quit(conf)
} }
case <-sigs: case <-sigs:
logger.Printf(logger.INFO, "Received SIGINT/SIGTERM, exiting") logger.Printf(logger.INFO, "Received SIGINT/SIGTERM, exiting")
quit() quit(conf)
} }
} }
} }
func quit() { func quit(conf *Conf) {
// send stop to StreamManager·s // send stop to StreamManager·s
close(stopStreams) close(stopStreams)
logger.Println(logger.INFO, "Waiting for Streams to finish...") logger.Println(logger.INFO, "Waiting for Streams to finish...")
@ -369,6 +380,8 @@ func quit() {
// stop all actions // stop all actions
logger.Println(logger.INFO, "Waiting for Actions to finish...") logger.Println(logger.INFO, "Waiting for Actions to finish...")
wgActions.Wait() wgActions.Wait()
// run stop commands
runCommands(conf.Stop, "stop")
// delete pipe // delete pipe
err := os.Remove(*SocketPath) err := os.Remove(*SocketPath)
if err != nil { if err != nil {

View File

@ -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
@ -18,6 +18,20 @@ patterns:
- 127.0.0.1 - 127.0.0.1
- ::1 - ::1
# Those commands will be executed in order at start, before everything else
start:
- [ "ip46tables", "-w", "-N", "reaction" ]
- [ "ip46tables", "-w", "-A", "reaction", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "reaction", "1", "-s", "127.0.0.1", "-j", "ACCEPT" ]
- [ "ip46tables", "-w", "-I", "INPUT", "-p", "all", "-j", "reaction" ]
# Those commands will be executed in order at stop, after everything else
stop:
- [ "ip46tables", "-w,", "-D", "INPUT", "-p", "all", "-j", "reaction" ]
- [ "ip46tables", "-w", "-F", "reaction" ]
- [ "ip46tables", "-w", "-X", "reaction" ]
# streams are commands # streams are commands
# they're run and their ouptut is captured # they're run and their ouptut is captured
# *example:* `tail -f /var/log/nginx/access.log` # *example:* `tail -f /var/log/nginx/access.log`
@ -27,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

View File

@ -10,6 +10,8 @@ import (
type Conf struct { type Conf struct {
Patterns map[string]*Pattern `json:"patterns"` Patterns map[string]*Pattern `json:"patterns"`
Streams map[string]*Stream `json:"streams"` Streams map[string]*Stream `json:"streams"`
Start [][]string `json:"start"`
Stop [][]string `json:"stop"`
} }
type Pattern struct { type Pattern struct {

View File

@ -1,8 +1,6 @@
local directory = '~/.local/share/watch';
// Those strings will be substitued in each shell() call // Those strings will be substitued in each shell() call
local substitutions = [ local substitutions = [
['OUTFILE', directory + '/logs-$(date %+F)'], ['OUTFILE', '"$HOME/.local/share/watch/logs-$(date +%F)"'],
['TMUXFILE', directory + '/tmux'],
['DATE', '"$(date "+%F %T")"'], ['DATE', '"$(date "+%F %T")"'],
]; ];
@ -18,19 +16,17 @@ local shell(prg) = [
sub(prg), sub(prg),
]; ];
{ local log(line) = shell('echo DATE ' + std.strReplace(line, '\n', ' ') + '>> OUTFILE');
// Startup is currently not implemented
startup: shell(|||
mkdir -p "$(dirname OUTFILE)"
echo DATE start >> OUTFILE
# tmux set-hook -g pane-focus-in[50] new-session -d 'echo tmux >> TMUXFILE'
|||),
// Stop is currently not implemented {
stop: shell(||| start: [
tmux set-hook -ug pane-focus-in[50] shell('mkdir -p "$(dirname OUTFILE)"'),
echo DATE stop >> OUTFILE log('start'),
|||), ],
stop: [
log('stop'),
],
patterns: { patterns: {
all: { regex: '.*' }, all: { regex: '.*' },
@ -47,7 +43,7 @@ local shell(prg) = [
send: { send: {
regex: ['^<all>$'], regex: ['^<all>$'],
actions: { actions: {
send: { cmd: shell('echo DATE focus <all> >> OUTFILE') }, send: { cmd: log('focus <all>') },
}, },
}, },
}, },
@ -55,12 +51,13 @@ local shell(prg) = [
// Be notified when user is away // Be notified when user is away
swayidle: { swayidle: {
cmd: ['swayidle', 'timeout', '60', 'echo sleep', 'resume', 'echo resume'], // FIXME echo stop and start instead?
cmd: ['swayidle', 'timeout', '30', 'echo sleep', 'resume', 'echo resume'],
filters: { filters: {
send: { send: {
regex: ['^<all>$'], regex: ['^<all>$'],
actions: { actions: {
send: { cmd: shell('echo DATE <all> >> OUTFILE') }, send: { cmd: log('<all>') },
}, },
}, },
}, },
@ -92,7 +89,7 @@ local shell(prg) = [
// send: { // send: {
// regex: ['^tmux <all>$'], // regex: ['^tmux <all>$'],
// actions: { // actions: {
// send: { cmd: shell('echo DATE tmux <all> >> OUTFILE') }, // send: { cmd: log('tmux <all>') },
// }, // },
// }, // },
// }, // },

View File

@ -1,11 +1,13 @@
// This file is using JSONNET, a complete configuration language based on JSON // This file is using JSONNET, a complete configuration language based on JSON
// See https://jsonnet.org // See https://jsonnet.org
// 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. // Note that YAML is also supported, see ./example.yml
// A JSONNET function
local iptables(args) = ['ip46tables', '-w'] + args;
// variables defined for later use. // variables defined for later use.
local iptablesban = ['ip46tables', '-w', '-A', 'reaction', '1', '-s', '<ip>', '-j', 'DROP']; local iptablesban = iptables(['-A', 'reaction', '1', '-s', '<ip>', '-j', 'drop']);
local iptablesunban = ['ip46tables', '-w', '-D', '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
@ -21,6 +23,30 @@ local iptablesunban = ['ip46tables', '-w', '-D', 'reaction', '1', '-s', '<ip>',
}, },
}, },
// Those commands will be executed in order at start, before everything else
start: [
// Create an iptables chain for reaction
iptables(['-N', 'reaction']),
// Set its default to ACCEPT
iptables(['-A', 'reaction', '-j', 'ACCEPT']),
// Always accept 127.0.0.1
iptables(['-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT']),
// Always accept ::1
iptables(['-I', 'reaction', '1', '-s', '::1', '-j', 'ACCEPT']),
// Insert this chain as the first item of the INPUT chain (for incoming connections)
iptables(['-I', 'INPUT', '-p', 'all', '-j', 'reaction']),
],
// Those commands will be executed in order at stop, after everything else
stop: [
// Remove the chain from the INPUT chain
iptables(['-D,', 'INPUT', '-p', 'all', '-j', 'reaction']),
// Empty the chain
iptables(['-F,', 'reaction']),
// Delete the chain
iptables(['-X,', 'reaction']),
],
// streams are commands // streams are commands
// they're run and their ouptut is captured // they're run and their ouptut is captured
// *example:* `tail -f /var/log/nginx/access.log` // *example:* `tail -f /var/log/nginx/access.log`

View File

@ -6,24 +6,6 @@ WantedBy=multi-user.target
[Service] [Service]
ExecStart=/path/to/reaction -c /etc/reaction.yml ExecStart=/path/to/reaction -c /etc/reaction.yml
# Create an iptables chain for reaction
ExecStartPre=/path/to/ip46tables -w -N reaction
# Set its default to ACCEPT
ExecStartPre=/path/to/ip46tables -w -A reaction -j ACCEPT
# Always accept 127.0.0.1
ExecStartPre=/path/to/ip46tables -w -I reaction 1 -s 127.0.0.1 -j ACCEPT
# Always accept ::1
ExecStartPre=/path/to/ip46tables -w -I reaction 1 -s ::1 -j ACCEPT
# Insert this chain as the first item of the INPUT chain (for incoming connections)
ExecStartPre=/path/to/ip46tables -w -I INPUT -p all -j reaction
# Remove the chain from the INPUT chain
ExecStopPost=/path/to/ip46tables -w -D INPUT -p all -j reaction
# Empty the chain
ExecStopPost=/path/to/ip46tables -w -F reaction
# Delete the chain
ExecStopPost=/path/to/ip46tables -w -X reaction
# 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
# Ask systemd to create /run/reaction at runtime (/run/ is implicit) # Ask systemd to create /run/reaction at runtime (/run/ is implicit)

View File

@ -69,8 +69,10 @@ func Printf(level Level, format string, args ...any) {
} }
func Fatalln(args ...any) { func Fatalln(args ...any) {
level := FATAL newargs := make([]any, 0)
log.Fatalln(level.String(), args) newargs = append(newargs, FATAL)
newargs = append(newargs, args...)
log.Fatalln(newargs...)
} }
func Fatalf(format string, args ...any) { func Fatalf(format string, args ...any) {