NAME

Ereshkigal - Handle firewall or similar bans.

VERSION

Version 0.0.1

SYNOPSIS

use Ereshkigal;

my $ereshkigal = Ereshkigal->new( config => '/usr/local/etc/ereshkigal.toml' );

$ereshkigal->start_server;

Ereshkigal is a ban manager for firewalls. It wrangles various Ereshkigal::Kur instances, spawned via the kur bin, each of which runs as its own process and uses Net::Firewall::BlockerHelper for talking to the firewall.

The manager listens on a unix socket, by default /var/run/ereshkigal/socket, speaking the newline delimited JSON protocol of POE::Component::Server::JSONUnix, and proxies per instance work to the kur sockets under /var/run/ereshkigal/kur/.

CONFIG FILE

The config file is TOML. Hashes under kur define instances. The instance name is the hash name, so the hash at kur.sshd is the kur instance sshd. Keys inside are what kur/Net::Firewall::BlockerHelper take... backend, ports, protocols, prefix, self_heal, and the backend specific options table.

Values in the options table must be plain scalars, with one exception... interfaces, which backends such as xdp want as an array, may be given as one. Any other array or table valued option is refused at config load rather than being handed to the backend as a stringified ref.

Top level keys are manager settings.

- socket_group :: Group ownership of the manager socket.
    Default :: the default group of the root user

- socket_mode :: Perms for the manager socket. Processed via oct, so
      should be specified as a string such as "0660". Kur sockets are
      always 0600 and not configurable.
    Default :: 0660

- run_base_dir :: Base dir for run files.
    Default :: /var/run/ereshkigal

- cache_base_dir :: Base dir for cache files, passed to kur.
    Default :: /var/cache/ereshkigal

- kur_bin :: The kur bin to spawn instances with.
    Default :: kur

- timeout :: Timeout in seconds used when talking to kur sockets. For
      commands touching multiple kurs this bounds the whole fan out
      rather than each kur individually.
    Default :: 30

- ban_time :: How long bans should last in seconds. 0 means bans never
      time out. May be overridden per kur via ban_time in its hash and
      per ban request.
    Default :: 600

- checkpoint :: Seconds between periodic rewrites of each kur's ban
      state CSV. 0 disables the periodic rewrite... ban/unban, stop,
      and on demand checkpoints still happen. May be overridden per kur
      via checkpoint in its hash.
    Default :: 60

- enable_cidr :: Whether CIDR banning is enabled. May be overridden per
      kur via enable_cidr in its hash. Even when set, the cidr_ban and
      cidr_unban commands only work on a kur whose backend supports CIDR
      bans.
    Default :: false

- cidr_silent_drop :: How a kur handles a CIDR command when CIDR banning
      is not available for it, either because enable_cidr is off or the
      backend cannot do CIDR. When set such a kur silently drops the
      command rather than erroring, which keeps a fan out across a mix of
      CIDR capable and incapable kurs from being spoiled by the incapable
      ones. May be overridden per kur via cidr_silent_drop in its hash.
    Default :: false

- enable_auth :: Enables the L<POE::Component::Server::JSONUnix>
      auth_required cookie file ownership challenge on the manager
      socket, along with authorization via authed_users/authed_groups.
    Default :: 0

- authed_users :: An array of users with global access.
    Default :: []

- authed_groups :: An array of groups with global access.
    Default :: []

- auth_temp_dir :: Dir used for the ownership challenge cookie files,
      passed through to L<POE::Component::Server::JSONUnix>.
    Default :: undef

A kur hash may instead carry fan_out, an array of other kur names, in place of backend. Such a kur is manager side only... no process and no socket of its own. Commands targeted at it (ban and cidr_ban with args.kur, checkpoint, re_init, and clear_retries with args.kur, and status_kur) fan out to its members instead, making it usable as a single point of contact for driving a whole set of kurs. With enable_auth on, authorization for a command targeted at a fan_out kur is checked against the fan_out kur's own lists rather than its members', so an integration may be granted just the gate without being listed on any member. Members must be defined non fan_out kurs... fan_out kurs may not nest. Untargeted commands (ban and cidr_ban without args.kur, unban, cidr_unban, banned, and checkpoint, re_init, and clear_retries without args.kur) touch only real kurs, never fan_out ones.

[kur.baphomet]
fan_out      = [ "sshd", "smtp" ]
authed_users = [ "baphomet" ]

Each kur hash may also carry its own authed_users/authed_groups, which expand upon the global ones for that kur... the effective lists for a kur are the global ones plus its own. A command must be authorized for every kur it touches, with untargeted fan-out commands touching every kur, while commands about the manager itself (stop, add_kur, remove_kur, and the whole manager views status/status_all) require the global lists. UID 0 is always authorized. The kur backends do no checking at all... their sockets are 0600 and only ereshkigal is expected to be able to write to them, so enforcement is entirely the manager's responsibility.

A refused command comes back as a normal JSONUnix error response carrying a machine-readable code, permission_denied for an authorization refusal, matching the code field convention of POE::Component::Server::JSONUnix's own permission and auth errors, so a consumer may branch on the code rather than matching the message text.

Example...

socket_group = "wheel"
socket_mode  = "0660"

[kur.sshd]
backend   = "ipfw"
ports     = [ "22" ]
protocols = [ "tcp" ]

METHODS

new

Initiates the object. All errors are considered fatal, meaning if new fails it will die.

- config :: Path to the TOML config file.
    Default :: /usr/local/etc/ereshkigal.toml

socket_path

Returns the path of the manager unix socket.

my $socket_path = $ereshkigal->socket_path;

pid_path

Returns the path of the manager PID file.

my $pid_path = $ereshkigal->pid_path;

kur_socket_path

Returns the path of the unix socket for the specified kur instance.

my $kur_socket_path = $ereshkigal->kur_socket_path($name);

start_server

Starts the manager. Spawns all configured kur instances, each supervised and restarted with a backoff should it die, and brings up the POE::Component::Server::JSONUnix server on the manager socket, then calls $poe_kernel->run.

This should not be expected to return till the manager is told to stop.

After binding, the manager socket is chowned to the configured group and chmoded to the configured mode.

The JSON commands handled are as below.

- status :: Manager status... uptime and kur list with up/down state.

- status_all :: The above plus each kur's full status block.

- status_kur :: Full status of the kur instance args.name. For a
      fan_out kur this is its member list plus each member's status.

- banned :: Banned IPs, grouped per kur, along with when each expires.

- ban :: Ban the IPs args.ips on the kur args.kur, or on all kurs if
      args.kur is not specified. If args.kur is a fan_out kur it expands
      to its members. args.ban_time, if defined, is forwarded
      to the kurs, overriding their default for how long the bans should
      last in seconds, with 0 meaning never time out. IPs are validated
      and normalized to their canonical form before being fanned out,
      with anything failing to validate reported back per IP under
      rejected rather than being sent to the kurs. If nothing validates
      the request as a whole errors.

- unban :: If args.all is true, flush every kur. Otherwise validate and
      normalize args.ip, erroring if it fails to validate, then check
      each kur for it and unban it from each kur it is present on.

- cidr_ban :: Ban the CIDR ranges args.cidrs, otherwise behaving like
      ban including the args.kur targeting and args.ban_time forwarding.
      CIDRs are validated and reduced to their canonical network form
      before being fanned out. A targeted kur, or an untargeted fan out,
      for which CIDR is not available answers per kur with either a drop
      or an error depending on its cidr_silent_drop.

- cidr_unban :: Validate and normalize args.cidr, erroring if it fails to
      validate, then check each kur for it and unban it from each kur it
      is present on. There is no all form... unban with args.all already
      flushes CIDR bans alongside single IP bans.

- add_kur :: Define and start a new kur instance, args.name and
      args.opts. Does not rewrite the config file.

- remove_kur :: Stop the kur instance args.name and deregister it. Does
      not rewrite the config file.

- checkpoint :: Force the kur args.kur, or all kurs if args.kur is not
      specified, to write their ban state CSVs out now. If args.kur is a
      fan_out kur it expands to its members.

- re_init :: Have the kur args.kur, or all kurs if args.kur is not
      specified, tear their firewall setup down and rebuild it, re-banning
      everything their ban book carries. Expands a fan_out kur the same
      way checkpoint does. Bans are briefly not enforced while the setup
      is being rebuilt.

- clear_retries :: Have the kur args.kur, or all kurs if args.kur is not
      specified, forget unbans still owed to the firewall. args.ip or
      args.cidr, at most one of them, names a single owed unban to
      forget rather than the lot. Expands a fan_out kur the same way
      checkpoint does. Nothing is asked of the firewall, so anything
      genuinely still banished there stays that way.

- stop :: Stop all kur instances and then the manager.

ERROR CODES / ERROR FLAGS

Error handling is provided by Error::Helper. All errors are considered fatal.

1, configReadFailed

Failed to read the config file.

2, configParseFailed

Failed to parse the config file as TOML.

3, invalidKurDef

A kur def in the config is invalid... bad name, not a hash, lacking a backend or a fan_out, having both, an invalid fan_out (not an array of kur names, an unknown member, or a nested fan_out kur), or an options table carrying a non-scalar value for anything other than interfaces.

4, runBaseDirError

The run base dir or the kur dir under it could not be created or is not read/writable.

5, badSocketGroup

Failed to resolve the socket group to a GID.

6, invalidBanTime

ban_time is not a non-negative int of seconds.

7, invalidCheckpoint

checkpoint is not a non-negative int of seconds.

8, invalidAuthedList

authed_users or authed_groups is not an array of strings.

AUTHOR

Zane C. Bowers-Hadley, <vvelox at vvelox.net>

BUGS

Please report any bugs or feature requests via GitHub at https://github.com/LilithSec/Ereshkigal/issues, or to bug-ereshkigal at rt.cpan.org.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Ereshkigal

You can also look for information at:

LICENSE AND COPYRIGHT

This software is Copyright (c) 2023 by Zane C. Bowers-Hadley.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)