diff mbox series

[bug#56608,v2,1/2] gnu: security: Add fail2ban-service-type.

Message ID 20220822172607.31515-2-mail@muradm.net
State Accepted
Headers show
Series Add fail2ban-service-type. | expand

Checks

Context Check Description
cbaines/comparison success View comparision
cbaines/git-branch success View Git branch
cbaines/applying patch success View Laminar job
cbaines/issue success View issue

Commit Message

muradm Aug. 22, 2022, 5:26 p.m. UTC
* gnu/services/security.scm: New module.
* gnu/local.mk: Add new security module.
* doc/guix.text: Add fail2ban-service-type documentation.
---
 doc/guix.texi             | 249 ++++++++++++++++++++++++
 gnu/local.mk              |   2 +
 gnu/services/security.scm | 385 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 636 insertions(+)
 create mode 100644 gnu/services/security.scm
diff mbox series

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index 023b48ae35..5467f47412 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -36283,6 +36283,255 @@  Extra command line options for @code{nix-service-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 malicious
+signs -- too many password failures, seeking for exploits, etc.
+
+@code{fail2ban} service is provided in @code{(gnu services security)} module.
+
+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 omitted here
+  (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{<service-type>} 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 virtually
+   ;; any service type with any fail2ban jail.
+   ;; This way we don't have to explicitly extend services
+   ;; with fail2ban-service-type.
+   (fail2ban-jail-service
+    openssh-service-type
+    (fail2ban-jail-configuration
+     (name "sshd")
+     (enabled #t)))
+   (openssh-configuration ...))))
+@end lisp
+@end deffn
+
+@deftp {Data Type} fail2ban-configuration
+Available @code{fail2ban-configuration} fields are:
+
+@table @asis
+@item @code{fail2ban} (default: @code{fail2ban}) (type: package)
+The @code{fail2ban} package to use.  It used for both binaries and as
+base default configuration that will be extended with
+@code{<fail2ban-jail-configuration>}s.
+
+@item @code{run-directory} (default: @code{"/var/run/fail2ban"}) (type: string)
+State directory for @code{fail2ban} daemon.
+
+@item @code{jails} (default: @code{()}) (type: list-of-fail2ban-jail-configurations)
+Instances of @code{<fail2ban-jail-configuration>} collected from
+extensions.
+
+@item @code{extra-jails} (default: @code{()}) (type: list-of-fail2ban-jail-configurations)
+Instances of @code{<fail2ban-jail-configuration>} provided by user
+explicitly.
+
+@item @code{extra-content} (type: maybe-string)
+Extra raw content to add at the end of @file{jail.local}.
+
+@end table
+
+@end deftp
+
+
+@deftp {Data Type} fail2ban-jail-configuration
+Available @code{fail2ban-jail-configuration} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+Required name of this jail configuration.
+
+@item @code{enabled} (type: maybe-boolean)
+Either @code{#t} or @code{#f} for @samp{true} and @samp{false}
+respectively.
+
+@item @code{backend} (type: maybe-symbol)
+Backend to be used to detect changes in the @code{ogpath}.
+
+@item @code{maxretry} (type: maybe-integer)
+Is the number of failures before a host get banned (e.g.  @code{(maxretry
+5)}).
+
+@item @code{maxmatches} (type: maybe-integer)
+Is the number of matches stored in ticket (resolvable via tag
+@code{<matches>}) in action.
+
+@item @code{findtime} (type: maybe-string)
+A host is banned if it has generated @code{maxretry} during the last
+@code{findtime} seconds (e.g.  @code{(findtime "10m")}).
+
+@item @code{bantime} (type: maybe-string)
+Is the number of seconds that a host is banned (e.g.  @code{(bantime
+"10m")}).
+
+@item @code{bantime.increment} (type: maybe-boolean)
+Allows to use database for searching of previously banned ip's to
+increase a default ban time using special formula.
+
+@item @code{bantime.factor} (type: maybe-string)
+Is a coefficient to calculate exponent growing of the formula or common
+multiplier.
+
+@item @code{bantime.formula} (type: maybe-string)
+Used by default to calculate next value of ban time.
+
+@item @code{bantime.multipliers} (type: maybe-string)
+Used to calculate next value of ban time instead of formula.
+
+@item @code{bantime.maxtime} (type: maybe-string)
+Is the max number of seconds using the ban time can reach (doesn't grow
+further).
+
+@item @code{bantime.rndtime} (type: maybe-string)
+Is the max number of seconds using for mixing with random time to
+prevent ``clever'' botnets calculate exact time IP can be unbanned
+again.
+
+@item @code{bantime.overalljails} (type: maybe-boolean)
+Either @code{#t} or @code{#f} for @samp{true} and @samp{false}
+respectively.
+@itemize @bullet
+@item @code{true} - specifies the search of IP in the database will be executed cross over all jails
+@item @code{false} - only current jail of the ban IP will be searched
+@end itemize
+
+@item @code{ignorecommand} (type: maybe-string)
+External command that will take an tagged arguments to ignore.  Note:
+while provided, currently unimplemented in the context of @code{guix}.
+
+@item @code{ignoreself} (type: maybe-boolean)
+Specifies whether the local resp.  own IP addresses should be ignored.
+
+@item @code{ignoreip} (default: @code{()}) (type: list-of-strings)
+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{ignorecache} (type: maybe-fail2ban-ignorecache-configuration)
+Provide cache parameters for ignore failure check.
+
+@item @code{filter} (type: maybe-fail2ban-jail-filter-configuration)
+Defines the filter to use by the jail, using
+@code{<fail2ban-jail-filter-configuration>}.  By default jails have
+names matching their filter name.
+
+@item @code{logtimezone} (type: maybe-string)
+Force the time zone for log lines that don't have one.
+
+@item @code{logencoding} (type: maybe-symbol)
+Specifies the encoding of the log files handled by the jail.  Possible
+values: @code{'ascii}, @code{'utf-8}, @code{'auto}.
+
+@item @code{logpath} (default: @code{()}) (type: list-of-strings)
+Filename(s) of the log files to be monitored.
+
+@item @code{action} (default: @code{()}) (type: list-of-fail2ban-jail-actions)
+List of @code{<fail2ban-jail-action-configuration>}.
+
+@item @code{extra-content} (type: maybe-string)
+Extra content for the jail configuration.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} fail2ban-ignorecache-configuration
+Available @code{fail2ban-ignorecache-configuration} fields are:
+
+@table @asis
+@item @code{key} (type: string)
+Cache key.
+
+@item @code{max-count} (type: integer)
+Cache size.
+
+@item @code{max-time} (type: integer)
+Cache time.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} fail2ban-jail-action-configuration
+Available @code{fail2ban-jail-action-configuration} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+Action name.
+
+@item @code{arguments} (default: @code{()}) (type: list-of-arguments)
+Action arguments.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} fail2ban-jail-filter-configuration
+Available @code{fail2ban-jail-filter-configuration} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+Filter to use.
+
+@item @code{mode} (type: maybe-string)
+Mode for filter.
+
+@end table
+
+@end deftp
+
 @node Setuid Programs
 @section Setuid Programs
 
diff --git a/gnu/local.mk b/gnu/local.mk
index 26dfb6afe2..acd41797b9 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -51,6 +51,7 @@ 
 # Copyright © 2022 Remco van 't Veer <remco@remworks.net>
 # Copyright © 2022 Artyom V. Poptsov <poptsov.artyom@gmail.com>
 # Copyright © 2022 John Kehayias <john.kehayias@protonmail.com>
+# Copyright © 2022 muradm <mail@muradm.net>
 #
 # This file is part of GNU Guix.
 #
@@ -672,6 +673,7 @@  GNU_SYSTEM_MODULES =				\
   %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..79e202565c
--- /dev/null
+++ b/gnu/services/security.scm
@@ -0,0 +1,385 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2022 muradm <mail@muradm.net>
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(define-module (gnu services security)
+  #:use-module (gnu packages admin)
+  #:use-module (gnu services)
+  #:use-module (gnu services configuration)
+  #:use-module (gnu services shepherd)
+  #:use-module (guix gexp)
+  #:use-module (guix packages)
+  #:use-module (guix records)
+  #:use-module (guix ui)
+  #:use-module (ice-9 format)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:export (fail2ban-configuration
+            fail2ban-configuration-fields
+            fail2ban-jail-configuration
+            fail2ban-jail-configuration-fields
+
+            fail2ban-ignorecache-configuration
+            fail2ban-ignorecache-configuration-fields
+            fail2ban-jail-action-configuration
+            fail2ban-jail-action-configuration-fields
+            fail2ban-jail-filter-configuration
+            fail2ban-jail-filter-configuration-fields
+
+            fail2ban-service-type
+            fail2ban-jail-service))
+
+(define-configuration/no-serialization fail2ban-ignorecache-configuration
+  (key (string) "Cache key.")
+  (max-count (integer) "Cache size.")
+  (max-time (integer) "Cache time."))
+
+(define serialize-fail2ban-ignorecache-configuration
+  (match-lambda
+    (($ <fail2ban-ignorecache-configuration> _ key max-count max-time)
+     (format #f "key=\"~a\", max-count=~d, max-time=~d"
+             key max-count max-time))))
+
+(define-maybe/no-serialization string)
+
+(define-configuration/no-serialization fail2ban-jail-filter-configuration
+  (name (string) "Filter to use.")
+  (mode maybe-string "Mode for filter."))
+
+(define serialize-fail2ban-jail-filter-configuration
+  (match-lambda
+    (($ <fail2ban-jail-filter-configuration> _ name mode)
+     (format #f "~a~a"
+             name (if (eq? 'unset mode) "" (format #f "[mode=~a]" mode))))))
+
+(define (list-of-arguments? lst)
+  (every
+   (lambda (e) (and (pair? e)
+                    (string? (car e))
+                    (or (string? (cdr e))
+                        (list-of-strings? (cdr e)))))
+   lst))
+
+(define-configuration/no-serialization fail2ban-jail-action-configuration
+  (name (string) "Action name.")
+  (arguments (list-of-arguments '()) "Action arguments."))
+
+(define list-of-fail2ban-jail-actions?
+  (list-of fail2ban-jail-action-configuration?))
+
+(define (serialize-fail2ban-jail-action-configuration-arguments args)
+  (let* ((multi-value
+          (lambda (v)
+            (format #f "~a" (string-join v ","))))
+         (any-value
+          (lambda (v)
+            (if (list? v) (string-append "\"" (multi-value v) "\"") v)))
+         (key-value
+          (lambda (e)
+            (format #f "~a=~a" (car e) (any-value (cdr e))))))
+    (format #f "~a" (string-join (map key-value args) ","))))
+
+(define serialize-fail2ban-jail-action-configuration
+  (match-lambda
+    (($ <fail2ban-jail-action-configuration> _ name arguments)
+     (format
+      #f "~a~a"
+      name
+      (if (null? arguments) ""
+          (format
+           #f "[~a]"
+           (serialize-fail2ban-jail-action-configuration-arguments
+            arguments)))))))
+
+(define fail2ban-backend->string
+  (match-lambda
+    ('auto "auto")
+    ('pyinotify "pyinotify")
+    ('gamin "gamin")
+    ('polling "polling")
+    ('systemd "systemd")
+    (unknown
+     (leave
+      (G_ "fail2ban: '~a' is not a supported backend~%") unknown))))
+
+(define fail2ban-logencoding->string
+  (match-lambda
+    ('auto "auto")
+    ('utf-8 "utf-8")
+    ('ascii "ascii")
+    (unknown
+     (leave
+      (G_ "fail2ban: '~a' is not a supported log encoding~%") unknown))))
+
+(define (fail2ban-jail-configuration-serialize-string field-name value)
+  #~(string-append #$(symbol->string field-name) " = " #$value "\n"))
+
+(define (fail2ban-jail-configuration-serialize-integer field-name value)
+  (fail2ban-jail-configuration-serialize-string
+   field-name (number->string value)))
+
+(define (fail2ban-jail-configuration-serialize-boolean field-name value)
+  (fail2ban-jail-configuration-serialize-string
+   field-name (if value "true" "false")))
+
+(define (fail2ban-jail-configuration-serialize-backend field-name value)
+  (if (eq? 'unset value) ""
+      (fail2ban-jail-configuration-serialize-string
+       field-name (fail2ban-backend->string value))))
+
+(define (fail2ban-jail-configuration-serialize-fail2ban-ignorecache-configuration field-name value)
+  (fail2ban-jail-configuration-serialize-string
+   field-name (serialize-fail2ban-ignorecache-configuration value)))
+
+(define (fail2ban-jail-configuration-serialize-fail2ban-jail-filter-configuration field-name value)
+  (fail2ban-jail-configuration-serialize-string
+   field-name (serialize-fail2ban-jail-filter-configuration value)))
+
+(define (fail2ban-jail-configuration-serialize-logencoding field-name value)
+  (if (eq? 'unset value) ""
+      (fail2ban-jail-configuration-serialize-string
+       field-name (fail2ban-logencoding->string value))))
+
+(define (fail2ban-jail-configuration-serialize-list-of-strings field-name value)
+  (if (null? value) ""
+      (fail2ban-jail-configuration-serialize-string
+       field-name (string-join value " "))))
+
+(define (fail2ban-jail-configuration-serialize-list-of-fail2ban-jail-actions field-name value)
+  (if (null? value) ""
+      (fail2ban-jail-configuration-serialize-string
+       field-name (string-join
+                   (map serialize-fail2ban-jail-action-configuration value) "\n"))))
+
+(define (fail2ban-jail-configuration-serialize-symbol field-name value)
+  (fail2ban-jail-configuration-serialize-string field-name (symbol->string value)))
+
+(define (fail2ban-jail-configuration-serialize-extra-content field-name value)
+  (if (eq? 'unset value) ""  (string-append "\n" value "\n")))
+
+(define-maybe integer (prefix fail2ban-jail-configuration-))
+(define-maybe string (prefix fail2ban-jail-configuration-))
+(define-maybe boolean (prefix fail2ban-jail-configuration-))
+(define-maybe symbol (prefix fail2ban-jail-configuration-))
+(define-maybe fail2ban-ignorecache-configuration (prefix fail2ban-jail-configuration-))
+(define-maybe fail2ban-jail-filter-configuration (prefix fail2ban-jail-configuration-))
+
+(define-configuration fail2ban-jail-configuration
+  (name
+   (string)
+   "Required name of this jail configuration.")
+  (enabled
+   maybe-boolean
+   "Either @code{#t} or @code{#f} for @samp{true} and
+@samp{false} respectively.")
+  (backend
+   maybe-symbol
+   "Backend to be used to detect changes in the @code{ogpath}."
+   fail2ban-jail-configuration-serialize-backend)
+  (maxretry
+   maybe-integer
+   "Is the number of failures before a host get banned
+(e.g. @code{(maxretry 5)}).")
+  (maxmatches
+   maybe-integer
+   "Is the number of matches stored in ticket (resolvable via
+tag @code{<matches>}) in action.")
+  (findtime
+   maybe-string
+   "A host is banned if it has generated @code{maxretry} during the last
+@code{findtime} seconds (e.g. @code{(findtime \"10m\")}).")
+  (bantime
+   maybe-string
+   "Is the number of seconds that a host is banned
+(e.g. @code{(bantime \"10m\")}).")
+  (bantime.increment
+   maybe-boolean
+   "Allows to use database for searching of previously banned
+ip's to increase a default ban time using special formula.")
+  (bantime.factor
+   maybe-string
+   "Is a coefficient to calculate exponent growing of the
+formula or common multiplier.")
+  (bantime.formula
+   maybe-string
+   "Used by default to calculate next value of ban time.")
+  (bantime.multipliers
+   maybe-string
+   "Used to calculate next value of ban time instead of formula.")
+  (bantime.maxtime
+   maybe-string
+   "Is the max number of seconds using the ban time can reach
+(doesn't grow further).")
+  (bantime.rndtime
+   maybe-string
+   "Is the max number of seconds using for mixing with random time
+to prevent ``clever'' botnets calculate exact time IP can be unbanned again.")
+  (bantime.overalljails
+   maybe-boolean
+   "Either @code{#t} or @code{#f} for @samp{true} and @samp{false} respectively.
+@itemize
+@item @code{true} - specifies the search of IP in the database will be executed cross over all jails
+@item @code{false} - only current jail of the ban IP will be searched
+@end itemize")
+  (ignorecommand
+   maybe-string
+   "External command that will take an tagged arguments to ignore.
+Note: while provided, currently unimplemented in the context of @code{guix}.")
+  (ignoreself
+   maybe-boolean
+   "Specifies whether the local resp. own IP addresses should be ignored.")
+  (ignoreip
+   (list-of-strings '())
+   "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.")
+  (ignorecache
+   maybe-fail2ban-ignorecache-configuration
+   "Provide cache parameters for ignore failure check.")
+  (filter
+   maybe-fail2ban-jail-filter-configuration
+   "Defines the filter to use by the jail, using
+@code{<fail2ban-jail-filter-configuration>}.
+By default jails have names matching their filter name.")
+  (logtimezone
+   maybe-string
+   "Force the time zone for log lines that don't have one.")
+  (logencoding
+   maybe-symbol
+   "Specifies the encoding of the log files handled by the jail.
+Possible values: @code{'ascii}, @code{'utf-8}, @code{'auto}."
+   fail2ban-jail-configuration-serialize-logencoding)
+  (logpath
+   (list-of-strings '())
+   "Filename(s) of the log files to be monitored.")
+  (action
+   (list-of-fail2ban-jail-actions '())
+   "List of @code{<fail2ban-jail-action-configuration>}.")
+  (extra-content
+   maybe-string
+   "Extra content for the jail configuration."
+   fail2ban-jail-configuration-serialize-extra-content)
+  (prefix fail2ban-jail-configuration-))
+
+(define list-of-fail2ban-jail-configurations?
+  (list-of fail2ban-jail-configuration?))
+
+(define (serialize-fail2ban-jail-configuration config)
+  #~(string-append
+     #$(format #f "[~a]\n" (fail2ban-jail-configuration-name config))
+     #$(serialize-configuration
+      config fail2ban-jail-configuration-fields)))
+
+(define-configuration/no-serialization fail2ban-configuration
+  (fail2ban
+   (package fail2ban)
+   "The @code{fail2ban} package to use. It used for both binaries and
+as base default configuration that will be extended with
+@code{<fail2ban-jail-configuration>}s.")
+  (run-directory
+   (string "/var/run/fail2ban")
+   "State directory for @code{fail2ban} daemon.")
+  (jails
+   (list-of-fail2ban-jail-configurations '())
+   "Instances of @code{<fail2ban-jail-configuration>} collected from
+extensions.")
+  (extra-jails
+   (list-of-fail2ban-jail-configurations '())
+   "Instances of @code{<fail2ban-jail-configuration>} provided by user
+explicitly.")
+  (extra-content
+   maybe-string
+   "Extra raw content to add at the end of @file{jail.local}."))
+
+(define (serialize-fail2ban-configuration config)
+  (let* ((jails (fail2ban-configuration-jails config))
+         (extra-jails (fail2ban-configuration-extra-jails config))
+         (extra-content (fail2ban-configuration-extra-content config)))
+    (interpose
+     (append (map serialize-fail2ban-jail-configuration
+                  (append jails extra-jails))
+             (list (if (eq? 'unset extra-content) "" extra-content))))))
+
+(define (make-fail2ban-configuration-package config)
+  (let* ((fail2ban (fail2ban-configuration-fail2ban config))
+         (jail-local (apply mixed-text-file "jail.local"
+                            (serialize-fail2ban-configuration 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-configuration>
+    (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))))))))