From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp12.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms5.migadu.com with LMTPS id 73ucBfSd6mKyhgAAbAwnHQ (envelope-from ) for ; Wed, 03 Aug 2022 18:10:28 +0200 Received: from aspmx1.migadu.com ([2001:41d0:8:6d80::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp12.migadu.com with LMTPS id qCitBPSd6mICIwEAauVa8A (envelope-from ) for ; Wed, 03 Aug 2022 18:10:28 +0200 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id B17F62E200 for ; Wed, 3 Aug 2022 18:10:27 +0200 (CEST) Received: from localhost ([::1]:36896 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1oJGx0-0003TQ-II for larch@yhetil.org; Wed, 03 Aug 2022 12:10:26 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:36848) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1oJGwe-0003TE-7v for guix-patches@gnu.org; Wed, 03 Aug 2022 12:10:04 -0400 Received: from debbugs.gnu.org ([209.51.188.43]:60075) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1oJGwc-0005pm-9D for guix-patches@gnu.org; Wed, 03 Aug 2022 12:10:03 -0400 Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1oJGwc-0006lR-0B for guix-patches@gnu.org; Wed, 03 Aug 2022 12:10:02 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#56608] [PATCH] gnu: security: Add fail2ban-service-type. Resent-From: Maxim Cournoyer Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Wed, 03 Aug 2022 16:10:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 56608 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: muradm Cc: 56608@debbugs.gnu.org Received: via spool by 56608-submit@debbugs.gnu.org id=B56608.165954297225959 (code B ref 56608); Wed, 03 Aug 2022 16:10:01 +0000 Received: (at 56608) by debbugs.gnu.org; 3 Aug 2022 16:09:32 +0000 Received: from localhost ([127.0.0.1]:49824 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oJGw5-0006kZ-8b for submit@debbugs.gnu.org; Wed, 03 Aug 2022 12:09:32 -0400 Received: from mail-qt1-f170.google.com ([209.85.160.170]:43944) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oJGw1-0006kJ-E6 for 56608@debbugs.gnu.org; Wed, 03 Aug 2022 12:09:27 -0400 Received: by mail-qt1-f170.google.com with SMTP id a15so3158741qto.10 for <56608@debbugs.gnu.org>; Wed, 03 Aug 2022 09:09:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:references:date:in-reply-to:message-id :user-agent:mime-version:content-transfer-encoding; bh=adhENkVpdYhJ0qWaOOZopCo6D+tw/Fb5vLfNC9pHSoQ=; b=gqDu+3FZCxmJDEtPfP92b6B62BOpBo17MBXRMDr66YtlCAAJaGMHrAJDKtOPlE4pJ2 gxhK1/Y2TbnaW03mTrsV5l43fXDqNPlpuZHhD/3RSaqDWosF9CTnH8ynAx/9XVLmd4nI fjZJPFCfRHxpTEeBenYHtjPT1nXBFmh00T7n6cyjLsiV121q6hHfswzw4DMcvqdL1L47 iIAgb0/BpOhx0zC8WyebJ6NCRcGzWu2lh5FJ/J9q/UN8jeWTXQdTUY74xb35sB9VP6Ug DJ4oVQCQizPOF0ZL9bIo+oBjQ3h2oXMY0pbS7EQcbm7+6W2wI+adAbB6GnyfJggkCSgI i9qg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:references:date:in-reply-to :message-id:user-agent:mime-version:content-transfer-encoding; bh=adhENkVpdYhJ0qWaOOZopCo6D+tw/Fb5vLfNC9pHSoQ=; b=AXY2yEmgGkPJzycxrYEmsxeDk/0TanUmyyC9aYds/T+UdFzvN5rC+xCUrPFTF8F+UX JZl8j78rvT4961aWjH9QhWw8Hr8wwYy2h7dfun3nd0EvvgWZau87OstjDAJobO9Hea4H LK2DyTCXpTdvtKyFXCpFOl9HW+EQUmFOi+ed6+bJinDa4uWbhwd3bDgKiommS2UdRr9S Bnamb/cp+7euLe0vhxLN7UyYs06e8oTyPzt0lIqetVK94qG/nfXc25apOGC1uyzgldGu CJWem8tbYbPmWuBYxXF5dp7VxklToAY6M49xifoZv+Kg79lh3K6t5ebnPXqT7Jn1er1h D/IA== X-Gm-Message-State: AJIora/dwD3BuxH9/F9SSIrWsdQuSJwv1sNSunbUYH2DvoVPSUHDcbQv jLbUH/cVc5mLtFJHzgpqJrNuvOqE3Zo= X-Google-Smtp-Source: AGRyM1srUSrgG2KN2NrfqqUM7NyfDWbmzLas7nuzPA5d+EZUjwhmGtHY6vxQfMYhWjy1EspdnlsTEw== X-Received: by 2002:a05:622a:609:b0:31e:f083:7dff with SMTP id z9-20020a05622a060900b0031ef0837dffmr22480538qta.38.1659542957689; Wed, 03 Aug 2022 09:09:17 -0700 (PDT) Received: from hurd (dsl-151-182.b2b2c.ca. [66.158.151.182]) by smtp.gmail.com with ESMTPSA id cd4-20020a05622a418400b0031f27b82268sm11357340qtb.71.2022.08.03.09.09.16 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 03 Aug 2022 09:09:17 -0700 (PDT) From: Maxim Cournoyer References: <20220717023223.440-1-mail@muradm.net> Date: Wed, 03 Aug 2022 12:09:16 -0400 In-Reply-To: <20220717023223.440-1-mail@muradm.net> (muradm's message of "Sun, 17 Jul 2022 05:32:23 +0300") Message-ID: <87edxxqpg3.fsf@gmail.com> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/28.1 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: guix-patches@gnu.org List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: guix-patches-bounces+larch=yhetil.org@gnu.org Sender: "Guix-patches" X-Migadu-Flow: FLOW_IN X-Migadu-To: larch@yhetil.org X-Migadu-Country: US ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=yhetil.org; s=key1; t=1659543027; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:cc:mime-version:mime-version: content-type:content-type: content-transfer-encoding:content-transfer-encoding:resent-cc: resent-from:resent-sender:resent-message-id:in-reply-to:in-reply-to: references:references:list-id:list-help:list-unsubscribe: list-subscribe:list-post:dkim-signature; bh=adhENkVpdYhJ0qWaOOZopCo6D+tw/Fb5vLfNC9pHSoQ=; b=fzBWa52exAqshg9+6gZlKSUXUGCe5tWjqVbnTYFXgJ6QlQ79FRmBScKr/YX8FCf2BT3Gxz ho5iHT9NT95w7whBkY8ro66atw7EBKTVzhWBP1dgIbR3v1zZ6f8DFwKuGS8DHKAP5Gmyfl PDxo/1rZIkVZ7TbpqDzOK4sP+2wGRgg4AvNfD3Ne33ZKiIyk+uYKaG5kcR7VIi9nkQdmPl rskpJ18ykrMfJI3FbHSs1UeCRSGdTNhCtPoiuQaaxqKwV7dx3hQ6RNvUHMLGt2xjyEUUJ8 kEdmX8K2dnrnpCQ9u/YAffzTV1klnVAaHPn7fr8bsHogB/+vRuOU1dx80KljJw== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1659543027; a=rsa-sha256; cv=none; b=QXB5+rWB/W21UUFE+j6J4sMMGO5u4JZfXGJDUFXuZaYhct2BuT3hMG1V//a5g4fZ2UNUdY 1RbF5u5grF9EKhByXYfTpmRfhcqDWL/HrGhmCXcWphmDBXFrxKFBJ4KQnLOeqdMLtaeS6C kWoSSzBFNalDQRHomMd87Zfj/Yyf5om48S2SOGbFEmJxhWd1Zk/GI9/8lConOccH8iVfGH Sf7dGx+xFLjuQZGZfF6dFK6AEd7jfsZXChgV/zSS+x5XErC2ODYoDQdgBzHc4lOlGk8CJG LSCXzcAfW1AY01jlvkWjHScyjo/Ufbk0l5RAYk3NZtU6Mn5bgtcLEKqTPTo8/Q== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=gqDu+3FZ; dmarc=fail reason="SPF not aligned (relaxed)" header.from=gmail.com (policy=none); spf=pass (aspmx1.migadu.com: domain of "guix-patches-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="guix-patches-bounces+larch=yhetil.org@gnu.org" X-Migadu-Spam-Score: 6.99 Authentication-Results: aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=gqDu+3FZ; dmarc=fail reason="SPF not aligned (relaxed)" header.from=gmail.com (policy=none); spf=pass (aspmx1.migadu.com: domain of "guix-patches-bounces+larch=yhetil.org@gnu.org" designates 209.51.188.17 as permitted sender) smtp.mailfrom="guix-patches-bounces+larch=yhetil.org@gnu.org" X-Migadu-Queue-Id: B17F62E200 X-Spam-Score: 6.99 X-Migadu-Scanner: scn0.migadu.com X-TUID: zOIrOKi3Cd0J Hi muradm, muradm writes: > * gnu/services/security.scm: New module. > * gnu/local.mk: Add new security module. > * doc/guix.text: Add fail2ban-service-type documentation. > --- > doc/guix.texi | 215 +++++++++++++++++++++++++ > gnu/local.mk | 2 + > gnu/services/security.scm | 328 ++++++++++++++++++++++++++++++++++++++ > 3 files changed, 545 insertions(+) > create mode 100644 gnu/services/security.scm > > diff --git a/doc/guix.texi b/doc/guix.texi > index 8b09bcd4eb..1fc327c4bc 100644 > --- a/doc/guix.texi > +++ b/doc/guix.texi > @@ -36285,6 +36285,221 @@ Extra command line options for @code{nix-servic= e-type}. > @end table > @end deftp > > +@cindex Fail2ban > +@subsubheading Fail2ban service > + > +@uref{http://www.fail2ban.org/, @code{fail2ban}} scans log files > +(e.g. @code{/var/log/apache/error_log}) and bans IPs that show the malic= ious > +signs -- too many password failures, seeking for exploits, etc. > + > +@code{fail2ban} service is provided in @code{(gnu services security)} mo= dule. > + > +This is the type of the service that runs @code{fail2ban} daemon. It can= be > +used in various ways, which are: > + > +@itemize > + > +@item Explicit configuration > +users are free to enable @code{fail2ban} configuration without strong > +dependency. > + > +@item On-demand extending configuration > +convenience @code{fail2ban-jail-service} function is provided, in order > +to extend existing services on-demand. > + > +@item Permanent extending configuration > +service developers may not @code{fail2ban-service-type} in service-type's > +extensions. > + > +@end itemize > + > +@defvr {Scheme Variable} fail2ban-service-type > + > +This is the type of the service that runs @code{fail2ban} daemon. It can= be > +configured explicitly as following: > + > +@lisp > +(append > + (list > + ;; excplicit configuration, this way fail2ban daemon > + ;; will start and do its thing for sshd jail > + (service fail2ban-service-type > + (fail2ban-configuration > + (extra-jails > + (list > + (fail2ban-jail-configuration > + (name "sshd") > + (enabled #t)))))) > + ;; there is no direct dependency with actual openssh > + ;; server configuration, it could even be omited here ^omitted > + (service openssh-service-type)) > + %base-services) > +@end lisp > +@end defvr > + > +@deffn {Scheme Procedure} fail2ban-jail-service @var{svc-type} @var{jail} > +Return extended @var{svc-type} of @code{} with added > +@var{jail} of type @code{fail2ban-jail-configuration} extension > +for @code{fail2ban-service-type}. > + > +For example: > + > +@lisp > +(append > + (list > + (service > + ;; using convenience function we can extend virutally Please use full sentences for standalone (not inline) comments. Also, typo: virutally -> virtually. > + ;; any service type with any fail2ban jail > + ;; this way we don't have to explicitly add > + ;; (service fail2ban-service-type) to our configuration > + (fail2ban-jail-service > + openssh-service-type > + (fail2ban-jail-configuration > + (name "sshd") > + (enabled #t))) > + (openssh-configuration ...)))) > +@end lisp > +@end deffn > + > +@deftp {Data Type} fail2ban-configuration > +Configuration record for the @code{fail2ban-service-type}. > +@table @asis > + > +@item @code{fail2ban} (default: @code{fail2ban}) > +The @code{fail2ban} package to use. It used for both binaries and > +as base default configuration that will be extended with > +@code{}s. > + > +@item @code{run-directory} (default: @file{"/var/run/fail2ban"}) > +State directory for @code{fail2ban} daemon. > + > +@item @code{jails} (default: @code{'()}) > +Instances of @code{} collected from > +extensions. > + > +@item @code{extra-jails} (default: @code{'()}) > +Instances of @code{} provided by user > +explicitly. > + > +@item @code{extra-content} (default: @code{""}) > +Extra raw content to add at the end of @file{jail.local}. > + > +@end table > +@end deftp > + > +@deftp {Data Type} fail2ban-jail-configuration > +@code{fail2ban} jail configuration to be added to @file{jail.local}. > +@table @asis > + > +Fields with default value of @code{*unspecified*} will not be serialized > +to configuration, thus default values of @code{fail2ban} will apply. Perhaps this configuration should use the 'define-configuration' mechanism, which allows to declare which fields can be left unspecified, without *unspecified* being a visible part of the API (which is not great, in my opinion). > +For details of field meanings, please refer to @code{fail2ban} documenta= tion. > + > +@item @code{name} > +Required name of this jail configuration. > + > +@item @code{enabled} (default: @code{*unspecified*}) > +Either @code{#t} or @code{#f} for @samp{true} and @samp{false} respectiv= ely. > + > +@item @code{max-retry} (default: @code{*unspecified*}) > +Is the number of failures before a host get banned (e.g. @code{(max-retr= y 5)}). > + > +@item @code{max-matches} (default: @code{*unspecified*}) > +Is the number of matches stored in ticket (resolvable via > +tag @code{}) in action. > + > +@item @code{find-time} (default: @code{*unspecified*}) > +A host is banned if it has geneerated @code{max-retry} during the last > +@code{find-time} seconds (e.g. @code{(find-time "10m")}). > + > +@item @code{ban-time} (default: @code{*unspecified*}) > +Is the number of seconds that a host is banned (e.g. @code{(ban-time "10= m")}). > + > +@item @code{ban-time-increment} (default: @code{*unspecified*}) > +Allows to use database for searching of previously banned ip's to increa= se a > +default ban time using special formula. > + > +@item @code{ban-time-factor} (default: @code{*unspecified*}) > +Is a coefficient to calculate exponent growing of the formula or common = multiplier. > + > +@item @code{ban-time-formula} (default: @code{*unspecified*}) > +Used by default to calculate next value of ban time. > + > +@item @code{ban-time-multipliers} (default: @code{*unspecified*}) > +Used to calculate next value of ban time instead of formula. > + > +@item @code{ban-time-maxtime} (default: @code{*unspecified*}) > +Is the max number of seconds using the ban time can reach (doesn't grow = further). > + > +@item @code{ban-time-rndtime} (default: @code{*unspecified*}) > +Is the max number of seconds using for mixing with random time > +to prevent ``clever'' botnets calculate exact time IP can be unbanned ag= ain. > + > +@item @code{ban-time-overalljails} (default: @code{*unspecified*}) > +Either @code{#t} or @code{#f} for @samp{true} and @samp{false} respectiv= ely. > +@itemize > +@item @code{true} - specifies the search of IP in the database will be e= xecuted cross over all jails > +@item @code{false} - only current jail of the ban IP will be searched > +@end itemize > + > +@item @code{ignore-command} (default: @code{*unspecified*}) > +External command that will take an tagged arguments to ignore. > +Note: while provided, currently unimplemented in the context of @code{gu= ix}. > + > +@item @code{ignore-self} (default: @code{*unspecified*}) > +Specifies whether the local resp. own IP addresses should be ignored. > + > +@item @code{ignore-ip} (default: @code{'()}) > +Can be a list of IP addresses, CIDR masks or DNS hosts. @code{fail2ban} > +will not ban a host which matches an address in this list > + > +@item @code{ignore-cache} (default: @code{*unspecified*}) > + > +@item @code{filter} (default: @code{*unspecified*}) > +Defines the filter to use by the jail, using > +@code{}. > +By default jails have names matching their filter name. > + > +@item @code{log-time-zone} (default: @code{*unspecified*}) > + > +@item @code{log-encoding} (default: @code{*unspecified*}) > +Specifies the encoding of the log files handled by the jail. > +Possible values: @code{'ascii}, @code{'utf-8}, @code{'auto}. > + > +@item @code{log-path} (default: @code{*unspecified*}) > + > +@item @code{action} (default: @code{'()}) > +List of @code{}. > + > +@end table > +@end deftp See above comment about *unspecified*. > +@deftp {Data Type} fail2ban-jail-filter-configuration > +@code{fail2ban} jail filter configuration. > +@table @asis > + > +@item @code{name} > +Name part required. > + > +@item @code{mode} (default: @code{*unspecified*}) > + > +@end table > +@end deftp > + > +@deftp {Data Type} fail2ban-jail-action-configuration > +@code{fail2ban} jail action configuration. > +@table @asis > + > +@item @code{name} > +Name part required. > + > +@item @code{arguments} (default: @code{'()}) > +Association list of key value pairs. > + > +@end table > +@end deftp > + > @node Setuid Programs > @section Setuid Programs > > diff --git a/gnu/local.mk b/gnu/local.mk > index 07e3497d10..eef7d09fd4 100644 > --- a/gnu/local.mk > +++ b/gnu/local.mk > @@ -50,6 +50,7 @@ > # Copyright =C2=A9 2022 Daniel Mei=C3=9Fner > # Copyright =C2=A9 2022 Remco van 't Veer > # Copyright =C2=A9 2022 Artyom V. Poptsov > +# Copyright =C2=A9 2022 muradm > # > # This file is part of GNU Guix. > # > @@ -670,6 +671,7 @@ GNU_SYSTEM_MODULES =3D \ > %D%/services/nfs.scm \ > %D%/services/pam-mount.scm \ > %D%/services/science.scm \ > + %D%/services/security.scm \ > %D%/services/security-token.scm \ > %D%/services/shepherd.scm \ > %D%/services/sound.scm \ > diff --git a/gnu/services/security.scm b/gnu/services/security.scm > new file mode 100644 > index 0000000000..db95d68035 > --- /dev/null > +++ b/gnu/services/security.scm > @@ -0,0 +1,328 @@ > +;;; GNU Guix --- Functional package management for GNU > +;;; Copyright =C2=A9 2022 muradm > +;;; > +;;; This file is part of GNU Guix. > +;;; > +;;; GNU Guix is free software; you can redistribute it and/or modify it > +;;; under the terms of the GNU General Public License as published by > +;;; the Free Software Foundation; either version 3 of the License, or (at > +;;; your option) any later version. > +;;; > +;;; GNU Guix is distributed in the hope that it will be useful, but > +;;; WITHOUT ANY WARRANTY; without even the implied warranty of > +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the > +;;; GNU General Public License for more details. > +;;; > +;;; You should have received a copy of the GNU General Public License > +;;; along with GNU Guix. If not, see . > + > +(define-module (gnu services security) > + #:use-module (guix gexp) > + #:use-module (guix records) > + #:use-module (gnu packages admin) > + #:use-module (ice-9 format) > + #:use-module (ice-9 match) > + #:use-module (srfi srfi-1) > + #:use-module (gnu services) > + #:use-module (gnu services shepherd) Please re-order lexicographically. > + #:export (fail2ban-ignore-cache-configuration > + fail2ban-jail-filter-configuration > + fail2ban-jail-action-configuration > + fail2ban-jail-configuration > + fail2ban-configuration > + fail2ban-service-type > + fail2ban-jail-service)) Configuration and services are often separated by a blank line to ease the reading. I'd order them here as well. > +(define (fail2ban-section->string name) > + (format #f "[~a]" name)) > + > +(define fail2ban-backend->string > + (match-lambda > + ('auto "auto") > + ('pyinotify "pyinotify") > + ('gamin "gamin") > + ('polling "polling") > + ('systemd "systemd") > + (unknown (error (format #f "Unknown fail2ban backend: ~a" unknown)))= )) This should use the (guix diagnostics), e.g. report-error module to produce standardized errors, and possibly hints. Errors and warnings should not be full sentences, while hints should be, by convention. > +(define fail2ban-log-encoding->string > + (match-lambda > + ('auto "auto") > + ('utf-8 "utf-8") > + ('ascii "ascii") > + (unknown (error (format #f "Unknown fail2ban log-encoding: ~a" unkno= wn))))) > + > +(define-record-type* > + fail2ban-ignore-cache-configuration make-fail2ban-ignore-cache-configu= ration > + fail2ban-ignore-cache-configuration? > + (key fail2ban-ignore-cache-configuration-key) > + (max-count fail2ban-ignore-cache-configuration-max-count) > + (max-time fail2ban-ignore-cache-configuration-max-time)) > + > +(define fail2ban-ignore-cache-configuration->string > + (match-lambda > + (($ key max-count max-time) > + (format #f "key=3D\"~a\", max-count=3D~d, max-time=3D~d" key max-co= unt max-time)))) > + > +(define-record-type* > + fail2ban-jail-filter-configuration make-fail2ban-jail-filter-configura= tion > + fail2ban-jail-filter-configuration? > + (name fail2ban-jail-filter-configuration-name) > + (mode fail2ban-jail-filter-configuration-node > + (default *unspecified*))) > + > +(define fail2ban-jail-filter-configuration->string > + (match-lambda > + (($ name mode) > + (format #f "~a~a" > + name (if (unspecified? mode) "" (format #f "[mode=3D~a]" mo= de)))))) > + > +(define-record-type* > + fail2ban-jail-action-configuration make-fail2ban-jail-action-configura= tion > + fail2ban-jail-action-configuration? > + (name fail2ban-jail-action-configuration-name) > + (arguments fail2ban-jail-action-configuration-arguments > + (default '()))) > + > +(define (fail2ban-arguments->string args) > + (let* ((multi-value > + (lambda (v) > + (format #f "\"~a\"" (string-join (map object->string v) ",")= ))) > + (any-value > + (lambda (v) > + (if (list? v) (multi-value v) v))) > + (key-value > + (lambda (e) > + (format #f "~a=3D~a" (car e) (any-value (cdr e)))))) > + (format #f "~a" (string-join (map key-value args) ",")))) > + > +(define fail2ban-jail-action-configuration->string > + (match-lambda > + (($ name arguments) > + (format #f "~a~a" > + name (if (null? arguments) "" > + (format #f "[~a]" > + (fail2ban-arguments->string arguments)))))= )) > + > +(define-record-type* > + fail2ban-jail-configuration make-fail2ban-jail-configuration > + fail2ban-jail-configuration? > + (name fail2ban-jail-configuration-name) > + (enabled fail2ban-jail-configuration-enabled > + (default *unspecified*)) > + (backend fail2ban-jail-configuration-backend > + (default *unspecified*)) > + (max-retry fail2ban-jail-configuration-max-retry > + (default *unspecified*)) > + (max-matches fail2ban-jail-configuration-max-matches > + (default *unspecified*)) > + (find-time fail2ban-jail-configuration-find-time > + (default *unspecified*)) > + (ban-time fail2ban-jail-configuration-ban-time > + (default *unspecified*)) > + (ban-time-increment fail2ban-jail-configuration-ban-time-increment > + (default *unspecified*)) > + (ban-time-factor fail2ban-jail-configuration-ban-time-factor > + (default *unspecified*)) > + (ban-time-formula fail2ban-jail-configuration-ban-time-formula > + (default *unspecified*)) > + (ban-time-multipliers fail2ban-jail-configuration-ban-time-multipliers > + (default *unspecified*)) > + (ban-time-maxtime fail2ban-jail-configuration-ban-time-maxtime > + (default *unspecified*)) > + (ban-time-rndtime fail2ban-jail-configuration-ban-time-rndtime > + (default *unspecified*)) > + (ban-time-overalljails fail2ban-jail-configuration-ban-time-overalljai= ls > + (default *unspecified*)) > + (ignore-command fail2ban-jail-configuration-ignore-command > + (default *unspecified*)) > + (ignore-self fail2ban-jail-configuration-ignore-self > + (default *unspecified*)) > + (ignore-ip fail2ban-jail-configuration-ignore-ip > + (default '())) > + (ignore-cache fail2ban-jail-configuration-ignore-cache > + (default *unspecified*)) > + (filter fail2ban-jail-configuration-filter > + (default *unspecified*)) > + (log-time-zone fail2ban-jail-configuration-log-time-zone > + (default *unspecified*)) > + (log-encoding fail2ban-jail-configuration-log-encoding > + (default *unspecified*)) > + (log-path fail2ban-jail-configuration-log-path > + (default *unspecified*)) > + (action fail2ban-jail-configuration-action > + (default '()))) > + > +(define fail2ban-jail-configuration->string > + (match-lambda > + (($ name enabled backend > + max-retry max-matches > + find-time ban-time > + ban-time-increment ban-time-factor > + ban-time-formula ban-time-multipli= ers > + ban-time-maxtime ban-time-rndtime > + ban-time-overalljails > + ignore-command ignore-self > + ignore-ip ignore-cache > + fltr > + log-time-zone log-encoding log-path > + action) > + (string-join > + (filter > + (lambda (s) (not (unspecified? s))) > + (list > + (fail2ban-section->string name) > + (unless (unspecified? enabled) > + (format #f "enabled =3D ~a" > + (if enabled "true" "false"))) > + (unless (unspecified? backend) > + (format #f "backend =3D ~a" > + (fail2ban-backend->string backend))) > + (unless (unspecified? max-retry) > + (format #f "maxretry =3D ~d" max-retry)) > + (unless (unspecified? max-matches) > + (format #f "maxmatches =3D ~d" max-matches)) > + (unless (unspecified? find-time) > + (format #f "findtime =3D ~a" find-time)) > + (unless (unspecified? ban-time) > + (format #f "bantime =3D ~a" ban-time)) > + (unless (unspecified? ban-time-increment) > + (format #f "bantime.increment =3D ~a" > + (if ban-time-increment "true" "false"))) > + (unless (unspecified? ban-time-factor) > + (format #f "bantime.factor =3D ~a" ban-time-factor)) > + (unless (unspecified? ban-time-formula) > + (format #f "bantime.formula =3D ~a" ban-time-formula)) > + (unless (unspecified? ban-time-multipliers) > + (format #f "bantime.multipliers =3D ~a" ban-time-multipliers)) > + (unless (unspecified? ban-time-maxtime) > + (format #f "bantime.maxtime =3D ~a" ban-time-maxtime)) > + (unless (unspecified? ban-time-rndtime) > + (format #f "bantime.rndtime =3D ~a" ban-time-rndtime)) > + (unless (unspecified? ban-time-overalljails) > + (format #f "bantime.overalljails =3D ~a" > + (if ban-time-overalljails "true" "false"))) > + (unless (unspecified? ignore-command) > + (format #f "ignorecommand =3D ~a" ignore-command)) > + (unless (unspecified? ignore-self) > + (format #f "ignoreself =3D ~a" > + (if ignore-self "true" "false"))) > + (unless (null? ignore-ip) > + (format #f "ignoreip =3D ~a" (string-join ignore-ip " "))) > + (unless (unspecified? ignore-cache) > + (format #f "ignorecache =3D ~a" > + (fail2ban-ignore-cache-configuration->string > + ignore-cache))) > + (unless (unspecified? fltr) > + (format #f "filter =3D ~a" > + (fail2ban-jail-filter-configuration->string fltr))) > + (unless (unspecified? log-time-zone) > + (format #f "logtimezone =3D ~a" log-time-zone)) > + (unless (unspecified? log-encoding) > + (format #f "logencoding =3D ~a" > + (fail2ban-log-encoding->string log-encoding))) > + (unless (unspecified? log-path) > + (format #f "logpath =3D ~a" log-path)) > + (unless (null? action) > + (format #f "action =3D ~a" > + (string-join > + (map fail2ban-jail-action-configuration->string actio= n) > + "\n"))))) > + "\n")))) > + > +(define-record-type* > + fail2ban-configuration make-fail2ban-configuration > + fail2ban-configuration? > + (fail2ban fail2ban-configuration-fail2ban > + (default fail2ban)) > + (run-directory fail2ban-configuration-run-directory > + (default "/var/run/fail2ban")) > + (jails fail2ban-configuration-jails > + (default '())) > + (extra-jails fail2ban-configuration-extra-jails > + (default '())) > + (extra-content fail2ban-configuration-extra-content > + (default ""))) > + > +(define (fail2ban-configuration->string config) > + (let* ((jails (fail2ban-configuration-jails config)) > + (extra-jails (fail2ban-configuration-extra-jails config)) > + (extra-content (fail2ban-configuration-extra-content config))) > + (string-append > + (string-join > + (map fail2ban-jail-configuration->string > + (append jails extra-jails)) > + "\n") > + "\n" extra-content "\n"))) > + > +(define (make-fail2ban-configuration-package config) > + (let* ((fail2ban (fail2ban-configuration-fail2ban config)) > + (jail-local > + (plain-file "jail.local" > + (fail2ban-configuration->string config)))) > + (computed-file > + "fail2ban-configuration" > + (with-imported-modules '((guix build utils)) > + #~(begin > + (use-modules (guix build utils)) > + (let* ((out (ungexp output))) > + (mkdir-p (string-append out "/etc/fail2ban")) > + (copy-recursively > + (string-append #$fail2ban "/etc/fail2ban") > + (string-append out "/etc/fail2ban")) > + (symlink > + #$jail-local > + (string-append out "/etc/fail2ban/jail.local")))))))) > + > +(define (fail2ban-shepherd-service config) > + (match-record config > + (fail2ban run-directory) > + (let* ((fail2ban-server (file-append fail2ban "/bin/fail2ban-server"= )) > + (pid-file (in-vicinity run-directory "fail2ban.pid")) > + (socket-file (in-vicinity run-directory "fail2ban.sock")) > + (config-dir (make-fail2ban-configuration-package config)) > + (config-dir (file-append config-dir "/etc/fail2ban")) > + (fail2ban-action > + (lambda args > + #~(lambda _ > + (invoke #$fail2ban-server > + "-c" #$config-dir > + "-p" #$pid-file > + "-s" #$socket-file > + "-b" > + #$@args))))) > + > + ;; TODO: Add 'reload' action. > + (list (shepherd-service > + (provision '(fail2ban)) > + (documentation "Run the fail2ban daemon.") > + (requirement '(user-processes)) > + (modules `((ice-9 match) > + ,@%default-modules)) > + (start (fail2ban-action "start")) > + (stop (fail2ban-action "stop"))))))) > + > +(define fail2ban-service-type > + (service-type (name 'fail2ban) > + (extensions > + (list (service-extension shepherd-root-service-type > + fail2ban-shepherd-service))) > + (compose concatenate) > + (extend (lambda (config jails) > + (fail2ban-configuration > + (inherit config) > + (jails > + (append > + (fail2ban-configuration-jails config) > + jails))))) > + (default-value (fail2ban-configuration)) > + (description "Run the fail2ban server."))) > + > +(define (fail2ban-jail-service svc-type jail) > + (service-type > + (inherit svc-type) > + (extensions > + (append > + (service-type-extensions svc-type) > + (list (service-extension fail2ban-service-type > + (lambda _ (list jail)))))))) Overall this looks very good to me. A system test attempting to brute force an SSH port and being blocked would be neat, to provide some insurance that it actually works, and that it continues working in the future. An OS template with an SSH service, the fail2ban service, and then localhost attempting a few erroneous login attempts to self, and a check to see if localhost was blocked in iptables, or something similar. Do you think it'd be feasible? Thanks, Maxim