From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp10.migadu.com ([2001:41d0:2:bcc0::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms5.migadu.com with LMTPS id YFkJBv3RA2MspQAAbAwnHQ (envelope-from ) for ; Mon, 22 Aug 2022 20:59:09 +0200 Received: from aspmx1.migadu.com ([2001:41d0:2:bcc0::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp10.migadu.com with LMTPS id ZTgvBf3RA2MEGQEAG6o9tA (envelope-from ) for ; Mon, 22 Aug 2022 20:59:09 +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 AD0DF380FC for ; Mon, 22 Aug 2022 20:59:08 +0200 (CEST) Received: from localhost ([::1]:40902 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1oQCde-0007Zj-M6 for larch@yhetil.org; Mon, 22 Aug 2022 14:59:06 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:37132) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1oQCZi-0004bD-GH for guix-patches@gnu.org; Mon, 22 Aug 2022 14:55:02 -0400 Received: from debbugs.gnu.org ([209.51.188.43]:51999) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1oQCZi-00060A-6d for guix-patches@gnu.org; Mon, 22 Aug 2022 14:55:02 -0400 Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1oQCZi-00037K-0F for guix-patches@gnu.org; Mon, 22 Aug 2022 14:55:02 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#56608] [PATCH v2 1/2] gnu: security: Add fail2ban-service-type. Resent-From: Maxim Cournoyer Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Mon, 22 Aug 2022 18:55: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.166119444911917 (code B ref 56608); Mon, 22 Aug 2022 18:55:01 +0000 Received: (at 56608) by debbugs.gnu.org; 22 Aug 2022 18:54:09 +0000 Received: from localhost ([127.0.0.1]:41748 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oQCYq-000368-Gl for submit@debbugs.gnu.org; Mon, 22 Aug 2022 14:54:09 -0400 Received: from mail-qk1-f181.google.com ([209.85.222.181]:42882) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1oQCYm-00035S-RU for 56608@debbugs.gnu.org; Mon, 22 Aug 2022 14:54:06 -0400 Received: by mail-qk1-f181.google.com with SMTP id h27so8596470qkk.9 for <56608@debbugs.gnu.org>; Mon, 22 Aug 2022 11:54:04 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=content-transfer-encoding:mime-version:user-agent:message-id :in-reply-to:date:references:subject:cc:to:from:from:to:cc; bh=tcSoAvgDjCzIIl+dHyrvCo+mwK63u727eiP2Nr00zbY=; b=D1uMx8+a4iTsK6PLr38IdvaONVArW5K3owW3XgF/1xIoTfd/IFFP7pJHotSjpX/Kbo 9weg/IY0m3QmWayAQWB91R2bKU7ESeiXcjPXWs33XmoK2NsbUSZXuTZLHNU3mV+D+4R+ +5DRNagEIGe7w9G8TWZcpBw+vft9MWeqrBdXdV0YUaIjJJJsgUPLf76kLIq4MsGTWdtV PL2dZL1lvyxzf2g8h7M7W5stUA6W1LHO5I2BVDhKfQuhrnIAGXTLFbXpkLWSEytb2gRL qsLVT8iql16rLl1kMcw2nni5VzvcgsTnkmA3Ja+ivlqTyerLo27K8pU2PA57cXUPS4rY 76Hw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=content-transfer-encoding:mime-version:user-agent:message-id :in-reply-to:date:references:subject:cc:to:from:x-gm-message-state :from:to:cc; bh=tcSoAvgDjCzIIl+dHyrvCo+mwK63u727eiP2Nr00zbY=; b=bvZiEk3Tpgdl2TZpA1GqZtjSdA2JHu1ebyxYUVTEhk6GgWRqMAP01pykCtlJSs1r4o tcpT7Cg1jaR+z1nZ0vPGZT78WTA6yoRS2y2uD/qQ4ztxsMSXKfv7n4TjHeROr1b4iKHB OJxp8sJs/R90Ze+7Y3CCOWYAKoiWoZuQ21CNJDZpFGMfGTgnEdZZ/VndUv8DeY48D6iO xEcLtPH6nv04IqudqAkPoKoAyN8/lug826xeO3P1kuaj1T3wlt5MJBCmoomCKweZ95Q9 HeqQzDCzbba+zz60nhChcpK/jju6EaHYjB3DtI0dMxRiALv7OQNYMLt9QEkcTVzVCcD2 dG4g== X-Gm-Message-State: ACgBeo1qqUqP4w8pHcyUh3cGJMSFg4FTtMlGx8DoYeWu/l506cp/Su+A 5aFu4IyJNgCaquK0G6gbAtLja7N7skY= X-Google-Smtp-Source: AA6agR5xtAhNu6D0WKJ4oUzN7eDTnohAKziV7wCHj9g398ToDH/frxQI440wo83xd1ie5ZazsXzvlA== X-Received: by 2002:ae9:f40b:0:b0:6ba:c027:8d2d with SMTP id y11-20020ae9f40b000000b006bac0278d2dmr13372377qkl.642.1661194438754; Mon, 22 Aug 2022 11:53:58 -0700 (PDT) Received: from hurd (dsl-150-55.b2b2c.ca. [66.158.150.55]) by smtp.gmail.com with ESMTPSA id l16-20020a37f910000000b006bbe6e89bdcsm7908551qkj.31.2022.08.22.11.53.58 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 22 Aug 2022 11:53:58 -0700 (PDT) From: Maxim Cournoyer References: <87edxxqpg3.fsf@gmail.com> <20220822172607.31515-1-mail@muradm.net> <20220822172607.31515-2-mail@muradm.net> Date: Mon, 22 Aug 2022 14:53:57 -0400 In-Reply-To: <20220822172607.31515-2-mail@muradm.net> (muradm's message of "Mon, 22 Aug 2022 20:26:06 +0300") Message-ID: <87o7wcgldm.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=1661194748; 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=tcSoAvgDjCzIIl+dHyrvCo+mwK63u727eiP2Nr00zbY=; b=rgJ7LhE74eKd5y3J4vN4UFK3dhZTNy2rM3wp4JDtijFUV1g0sAhXYzTLMaXjkMySsjFeie e68ZW79XEc77t1QeYrZX39mQRdl6F0n2NiWUgbhl0ticSKWWF3XkdzKGGNz7BWzcHei8x+ EQExSWfMDHI7GtqJRbajR18BXkcwhoTVbwcqeCwRn/C7WaNCSLA76p2lAgRu+fcX5SKGP4 ZpvpZ1RRYGjnOabYYu1xADj+YYR2bMTpp4VF1LyrOua3mXqZzPwtMZiJHLt1Bks1V2rOaM lU2sJCg2U+nbGAaMGfNUCweyqWTIjdJD4L9Y3/epYZko9RiOkHaDztIb+JL3Qg== ARC-Seal: i=1; s=key1; d=yhetil.org; t=1661194748; a=rsa-sha256; cv=none; b=hd95vszpqlOPnx24q+oU75pr+NAMnK0njYMTs1/29txFQOdFOVPjO3Wh8AJmZSE/oviNCp Z1WoaVMNUyq48dVLM1yrcvwgSjwt58scItGipTUF/gfnCKDu0BAL4RzuO/pEpykoej121F xFNdHvLTaKNVD5Wubql3SLEITLNWDa1utxA292BPlnshIpcYZOYd8LReE94GkMHVXdrkzA zOB5SVFDO1w0KmP6enPw6OEXEKKkXUbyaQ1Q/5le7hL84Yn04guMXCDu0vAfqq9zpvX0Zd VFgRjbHlhjf74yuO4gycTOFQ2iX7LIj34Jxqf1LFUbZFp4P+zaypjPfCxk5NHA== ARC-Authentication-Results: i=1; aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=D1uMx8+a; 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.09 Authentication-Results: aspmx1.migadu.com; dkim=fail ("headers rsa verify failed") header.d=gmail.com header.s=20210112 header.b=D1uMx8+a; 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: AD0DF380FC X-Spam-Score: 6.09 X-Migadu-Scanner: scn0.migadu.com X-TUID: Z91lb/BE4qgh Hi, Well done implementing the configuration records using define-configuration! I believe it'll help its users avoiding mistakes. Here are some comments for the guix.texi bits which are not automatically generated: 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 | 249 ++++++++++++++++++++++++ > gnu/local.mk | 2 + > gnu/services/security.scm | 385 ++++++++++++++++++++++++++++++++++++++ > 3 files changed, 636 insertions(+) > create mode 100644 gnu/services/security.scm > > 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-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 ^ two spa= ces > +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 ^ missing verb > +extensions. > + > +@end itemize > + > +@defvr {Scheme Variable} fail2ban-service-type > + > +This is the type of the service that runs @code{fail2ban} daemon. It can= be ^ two spa= ces > +configured explicitly as following: > + > +@lisp > +(append > + (list > + ;; excplicit configuration, this way fail2ban daemon > + ;; will start and do its thing for sshd jail Typo (excplicit). Also, please use full sentences for stand-alone comments (with proper punctuation). > + (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 Likewise. [...] > 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 @@ [...] > +(define-configuration/no-serialization fail2ban-ignorecache-configuration > + (key (string) "Cache key.") > + (max-count (integer) "Cache size.") > + (max-time (integer) "Cache time.")) Note that when you do not use a default value, you can leave out the parenthesizes. > + > +(define serialize-fail2ban-ignorecache-configuration > + (match-lambda > + (($ _ key max-count max-time) > + (format #f "key=3D\"~a\", max-count=3D~d, max-time=3D~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 > + (($ _ name mode) > + (format #f "~a~a" > + name (if (eq? 'unset mode) "" (format #f "[mode=3D~a]" mode= )))))) You could use (ice-9 format) with a conditional formatting here. The example from info '(guile) Formatted Output' is --8<---------------cut here---------------start------------->8--- (format #f "~:[false~;not false~]" 'abc) =E2=87=92 "not false" --8<---------------cut here---------------end--------------->8--- > +(define (list-of-arguments? lst) > + (every > + (lambda (e) (and (pair? e) > + (string? (car e)) > + (or (string? (cdr e)) > + (list-of-strings? (cdr e))))) > + lst)) It could be better to define a argument? predicate, use the 'list-of' procedure from (gnu services configuration) on it. [...] > +(define (fail2ban-jail-configuration-serialize-fail2ban-ignorecache-conf= iguration field-name value) > + (fail2ban-jail-configuration-serialize-string > + field-name (serialize-fail2ban-ignorecache-configuration value))) > + > +(define (fail2ban-jail-configuration-serialize-fail2ban-jail-filter-conf= iguration field-name value) > + (fail2ban-jail-configuration-serialize-string > + field-name (serialize-fail2ban-jail-filter-configuration value))) > + > +(define (fail2ban-jail-configuration-serialize-logencoding field-name va= lue) > + (if (eq? 'unset value) "" > + (fail2ban-jail-configuration-serialize-string > + field-name (fail2ban-logencoding->string value)))) > + > +(define (fail2ban-jail-configuration-serialize-list-of-strings field-nam= e value) > + (if (null? value) "" > + (fail2ban-jail-configuration-serialize-string > + field-name (string-join value " ")))) > + > +(define (fail2ban-jail-configuration-serialize-list-of-fail2ban-jail-act= ions field-name value) > + (if (null? value) "" > + (fail2ban-jail-configuration-serialize-string > + field-name (string-join > + (map serialize-fail2ban-jail-action-configuration val= ue) "\n")))) > + > +(define (fail2ban-jail-configuration-serialize-symbol field-name value) > + (fail2ban-jail-configuration-serialize-string field-name (symbol->stri= ng 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-c= onfiguration-)) > +(define-maybe fail2ban-jail-filter-configuration (prefix fail2ban-jail-c= onfiguration-)) Is using the prefix absolutely necessary? It makes things awkwardly long. Since fail2ban-service-type it's the first citizen of (gnu services security), it could enjoy the luxury of not using a prefix, in my opinion. Later services may need to define their own prefix. > +(define-configuration fail2ban-jail-configuration > + (name > + (string) > + "Required name of this jail configuration.") > + (enabled I'd use enabled? and employ a name normalizer (e.g., stripping any trailing '?') in the boolean serializer. > + 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 I think we could use hyphen in the field names, and use a normalizer to strip them at serialization time (assuming hyphens are never allowed in a key name). > + 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{}) 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 ag= ain.") > + (bantime.overalljails We could have the normalization "bantime-" -> "bantime." done in the serializers, to use more Schemey names. > + maybe-boolean > + "Either @code{#t} or @code{#f} for @samp{true} and @samp{false} respe= ctively. > +@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") > + (ignorecommand > + maybe-string > + "External command that will take an tagged arguments to ignore. > +Note: while provided, currently unimplemented in the context of @code{gu= ix}.") Then I'd remove it, as I don't see in which other context it would be useful. > + (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{fail2b= an} ^ Please use two spaces after period. > +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{}. > +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.") The convention in GNU is to use "file name" rather than "filename". > + (action > + (list-of-fail2ban-jail-actions '()) > + "List of @code{}.") > + (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{}s.") > + (run-directory > + (string "/var/run/fail2ban") > + "State directory for @code{fail2ban} daemon.") > + (jails > + (list-of-fail2ban-jail-configurations '()) > + "Instances of @code{} collected from > +extensions.") > + (extra-jails > + (list-of-fail2ban-jail-configurations '()) > + "Instances of @code{} provided by user > +explicitly.") > + (extra-content > + maybe-string > + "Extra raw content to add at the end of @file{jail.local}.")) ^the @file{jail.local} file. > + > +(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))) ^ use just let here > + (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)))))))) ^ nitpick, but (compose list jail) is more common That looks very good. I haven't checked the tests yet, but I think with the extra polishing suggested above, it should be in a good place to be merged soon! Thanks, Maxim