From 7fa64f845a3c9b07b04cae53136c06c274cf3f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Barnouin?= Date: Thu, 3 Apr 2025 13:59:37 +0200 Subject: [PATCH] Add Crowdsec custom module (cimer Pablo) --- modules/crowdsec.nix | 923 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 923 insertions(+) create mode 100644 modules/crowdsec.nix diff --git a/modules/crowdsec.nix b/modules/crowdsec.nix new file mode 100644 index 0000000..fd9f702 --- /dev/null +++ b/modules/crowdsec.nix @@ -0,0 +1,923 @@ +{ + config, + pkgs, + lib, + ... +}: +let + + format = pkgs.formats.yaml { }; + + rootDir = "/var/lib/crowdsec"; + stateDir = "${rootDir}/state"; + confDir = "/etc/crowdsec/"; + hubDir = "${stateDir}/hub/"; + notificationsDir = "${confDir}/notifications/"; + pluginDir = "${confDir}/plugins/"; + parsersDir = "${confDir}/parsers/"; + localPostOverflowsDir = "${confDir}/postoverflows/"; + localPostOverflowsS01WhitelistDir = "${localPostOverflowsDir}/s01-whitelist/"; + localScenariosDir = "${confDir}/scenarios/"; + localParsersS00RawDir = "${parsersDir}/s00-raw/"; + localParsersS01ParseDir = "${parsersDir}/s01-parse/"; + localParsersS02EnrichDir = "${parsersDir}/s02-enrich/"; + localContextsDir = "${confDir}/contexts/"; + +in +{ + + options.services.crowdsec = with lib; { + enable = mkEnableOption "CrowdSec Security Engine"; + + package = mkPackageOption pkgs "crowdsec" { }; + + autoUpdateService = mkEnableOption "Auto Hub Update"; + + openFirewall = mkEnableOption "opening the ports in the firewall"; + + user = mkOption { + type = types.str; + description = "The user to run crowdsec as"; + default = "crowdsec"; + }; + + group = mkOption { + type = types.str; + description = "The group to run crowdsec as"; + default = "crowdsec"; + }; + + name = mkOption { + type = types.str; + description = '' + Name of the machine when registering it at the central or local api. + ''; + default = config.networking.hostName; + defaultText = lib.literalExpression "config.networking.hostName"; + }; + + localConfig = mkOption { + type = types.submodule { + options = { + acquisitions = mkOption { + type = types.listOf format.type; + default = [ ]; + description = '' + A list of acquisition specifications, which define the data sources you want to be parsed. + See for details. + ''; + example = [ + { + source = "journalctl"; + journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ]; + labels = { + type = "syslog"; + }; + } + ]; + }; + scenarios = mkOption { + type = types.listOf format.type; + default = [ ]; + description = '' + A list of scenarios specifications. + See for details. + ''; + example = [ + { + type = "leaky"; + name = "crowdsecurity/myservice-bf"; + description = "Detect myservice bruteforce"; + filter = "evt.Meta.log_type == 'myservice_failed_auth'"; + leakspeed = "10s"; + capacity = 5; + groupby = "evt.Meta.source_ip"; + } + ]; + }; + parsers = mkOption { + type = types.submodule { + options = { + s00Raw = mkOption { + type = types.listOf format.type; + default = [ ]; + description = '' + A list of stage s00-raw specifications. Most of the time, those are already included in the hub, but are presented here anyway. + See for details. + ''; + }; + s01Parse = mkOption { + type = types.listOf format.type; + default = [ ]; + description = '' + A list of stage s01-parse specifications. + See for details. + ''; + example = [ + { + filter = "1=1"; + debug = true; + onsuccess = "next_stage"; + name = "example/custom-service-logs"; + description = "Parsing custom service logs"; + grok = { + pattern = "^%{DATA:some_data}$"; + apply_on = "message"; + }; + statics = [ + { + parsed = "is_my_custom_service"; + value = "yes"; + } + ]; + } + ]; + }; + s02Enrich = mkOption { + type = types.listOf format.type; + default = [ ]; + description = '' + A list of stage s02-enrich specifications. Inside this list, you can specify Parser Whitelists. + See for details. + ''; + example = [ + { + name = "myips/whitelist"; + description = "Whitelist parse events from my IPs"; + whitelist = { + reason = "My IP ranges"; + ip = [ + "1.2.3.4" + ]; + cidr = [ + "1.2.3.0/24" + ]; + }; + } + ]; + }; + }; + }; + default = { }; + }; + postOverflows = mkOption { + type = types.submodule { + options = { + s01Whitelist = mkOption { + type = types.listOf format.type; + default = [ ]; + description = '' + A list of stage s01-whitelist specifications. Inside this list, you can specify Postoverflows Whitelists. + See for details. + ''; + example = [ + { + name = "postoverflows/whitelist_my_dns_domain"; + description = "Whitelist my reverse DNS"; + whitelist = { + reason = "Don't ban me"; + expression = [ + "evt.Enriched.reverse_dns endsWith '.local.'" + ]; + }; + } + ]; + }; + }; + }; + default = { }; + }; + contexts = mkOption { + type = types.listOf format.type; + description = '' + A list of additional contexts to specify. + See for details. + ''; + example = [ + { + context = { + target_uri = [ "evt.Meta.http_path" ]; + user_agent = [ "evt.Meta.http_user_agent" ]; + method = [ "evt.Meta.http_verb" ]; + status = [ "evt.Meta.http_status" ]; + }; + } + ]; + default = [ ]; + }; + notifications = mkOption { + type = types.listOf format.type; + description = '' + A list of notifications to enable and use in your profiles. Note that for now, only the plugins shipped by default with CrowdSec are supported. + See for details. + ''; + example = [ + { + type = "http"; + name = "default_http_notification"; + log_level = "info"; + format = '' + {{.|toJson}} + ''; + url = "https://example.com/hook"; + method = "POST"; + } + ]; + default = [ ]; + }; + profiles = mkOption { + type = types.listOf format.type; + description = '' + A list of profiles to enable. + See for more details. + ''; + example = [ + { + name = "default_ip_remediation"; + filters = [ + "Alert.Remediation == true && Alert.GetScope() == 'Ip'" + ]; + decisions = [ + { + type = "ban"; + duration = "4h"; + } + ]; + on_success = "break"; + } + { + name = "default_range_remediation"; + filters = [ + "Alert.Remediation == true && Alert.GetScope() == 'Range'" + ]; + decisions = [ + { + type = "ban"; + duration = "4h"; + } + ]; + on_success = "break"; + } + ]; + default = [ + { + name = "default_ip_remediation"; + filters = [ + "Alert.Remediation == true && Alert.GetScope() == 'Ip'" + ]; + decisions = [ + { + type = "ban"; + duration = "4h"; + } + ]; + on_success = "break"; + } + { + name = "default_range_remediation"; + filters = [ + "Alert.Remediation == true && Alert.GetScope() == 'Range'" + ]; + decisions = [ + { + type = "ban"; + duration = "4h"; + } + ]; + on_success = "break"; + } + ]; + }; + patterns = mkOption { + type = types.listOf types.package; + default = [ ]; + example = lib.literalExpression '' + [ (pkgs.writeTextDir "custom_service_logs" (builtins.readFile ./custom_service_logs)) ] + ''; + }; + }; + }; + default = { }; + }; + + hub = mkOption { + type = types.submodule { + options = { + collections = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hub collections to install"; + example = [ "crowdsecurity/linux" ]; + }; + + scenarios = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hub scenarios to install"; + example = [ "crowdsecurity/ssh-bf" ]; + }; + + parsers = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hub parsers to install"; + example = [ "crowdsecurity/sshd-logs" ]; + }; + + postOverflows = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hub postoverflows to install"; + example = [ "crowdsecurity/auditd-nix-wrappers-whitelist-process" ]; + }; + + appSecConfigs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hub appsec configurations to install"; + example = [ "crowdsecurity/appsec-default" ]; + }; + + appSecRules = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "List of hub appsec rules to install"; + example = [ "crowdsecurity/base-config" ]; + }; + }; + }; + default = { }; + description = '' + Hub collections, parsers, AppSec rules, etc. + ''; + }; + + settings = mkOption { + type = types.submodule { + options = { + general = mkOption { + description = '' + Settings for the main CrowdSec configuration file. + Refer to the defaults at . + ''; + type = format.type; + default = { }; + }; + simulation = mkOption { + type = format.type; + default = { + simulation = false; + }; + description = '' + Attributes inside the simulation.yaml file. + ''; + }; + + lapi = mkOption { + type = types.submodule { + options = { + credentialsFile = mkOption { + type = types.nullOr types.path; + example = "/run/crowdsec/lapi.yaml"; + description = '' + The LAPI credential file to use. + ''; + default = null; + }; + }; + }; + description = '' + LAPI Configuration attributes + ''; + default = { }; + }; + capi = mkOption { + type = types.submodule { + options = { + credentialsFile = mkOption { + type = types.nullOr types.path; + example = "/run/crowdsec/capi.yaml"; + description = '' + The CAPI credential file to use. + ''; + default = null; + }; + }; + }; + description = '' + CAPI Configuration attributes + ''; + default = { }; + }; + console = mkOption { + type = types.submodule { + options = { + tokenFile = mkOption { + type = types.nullOr types.path; + example = "/run/crowdsec/console_token.yaml"; + description = '' + The Console Token file to use. + ''; + default = null; + }; + configuration = mkOption { + type = format.type; + default = { + share_manual_decisions = false; + share_custom = false; + share_tainted = false; + share_context = false; + }; + description = '' + Attributes inside the console.yaml file. + ''; + }; + }; + }; + description = '' + Console Configuration attributes + ''; + default = { }; + }; + }; + }; + }; + }; + config = + let + cfg = config.services.crowdsec; + configFile = format.generate "crowdsec.yaml" cfg.settings.general; + simulationFile = format.generate "simulation.yaml" cfg.settings.simulation; + consoleFile = format.generate "console.yaml" cfg.settings.console.configuration; + patternsDir = pkgs.buildPackages.symlinkJoin { + name = "crowdsec-patterns"; + paths = [ + cfg.localConfig.patterns + "${lib.attrsets.getOutput "out" cfg.package}/share/crowdsec/config/patterns/" + ]; + }; + + cscli = pkgs.writeShellScriptBin "cscli" '' + set -euo pipefail + # cscli needs crowdsec on it's path in order to be able to run `cscli explain` + export PATH="$PATH:${lib.makeBinPath [ cfg.package ]}" + sudo=exec + if [ "$USER" != "${cfg.user}" ]; then + sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}' + fi + $sudo ${lib.getExe' cfg.package "cscli"} -c=${configFile} "$@" + ''; + + localScenariosMap = (map (format.generate "scenario.yaml") cfg.localConfig.scenarios); + localParsersS00RawMap = ( + map (format.generate "parsers-s00-raw.yaml") cfg.localConfig.parsers.s00Raw + ); + localParsersS01ParseMap = ( + map (format.generate "parsers-s01-parse.yaml") cfg.localConfig.parsers.s01Parse + ); + localParsersS02EnrichMap = ( + map (format.generate "parsers-s02-enrich.yaml") cfg.localConfig.parsers.s02Enrich + ); + localPostOverflowsS01WhitelistMap = ( + map (format.generate "postoverflows-s01-whitelist.yaml") cfg.localConfig.postOverflows.s01Whitelist + ); + localContextsMap = (map (format.generate "context.yaml") cfg.localConfig.contexts); + localNotificationsMap = (map (format.generate "notification.yaml") cfg.localConfig.notifications); + localProfilesFile = pkgs.writeText "local_profiles.yaml" '' + --- + ${lib.strings.concatMapStringsSep "\n---\n" builtins.toJSON cfg.localConfig.profiles} + --- + ''; + localAcquisisionFile = pkgs.writeText "local_acquisisions.yaml" '' + --- + ${lib.strings.concatMapStringsSep "\n---\n" builtins.toJSON cfg.localConfig.acquisitions} + --- + ''; + + scriptArray = + [ + "set -euo pipefail" + "${lib.getExe cscli} hub update" + ] + ++ lib.optionals (cfg.hub.collections != [ ]) [ + "${lib.getExe cscli} collections install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections + }" + ] + ++ lib.optionals (cfg.hub.scenarios != [ ]) [ + "${lib.getExe cscli} scenarios install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios + }" + ] + ++ lib.optionals (cfg.hub.parsers != [ ]) [ + "${lib.getExe cscli} parsers install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers + }" + ] + ++ lib.optionals (cfg.hub.postOverflows != [ ]) [ + "${lib.getExe cscli} postoverflows install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows + }" + ] + ++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [ + "${lib.getExe cscli} appsec-configs install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs + }" + ] + ++ lib.optionals (cfg.hub.appSecRules != [ ]) [ + "${lib.getExe cscli} appsec-rules install ${ + lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecRules + }" + ] + ++ lib.optionals (cfg.settings.general.api.server.enable) [ + '' + if [ ! -s "${cfg.settings.general.api.client.credentials_path}" ]; then + ${lib.getExe cscli} machine add "${cfg.name}" --auto + fi + '' + ] + ++ lib.optionals (cfg.settings.capi.credentialsFile != null) [ + '' + if ! grep -q password "${cfg.settings.capi.credentialsFile}" ]; then + ${lib.getExe cscli} capi register + fi + '' + ] + ++ lib.optionals (cfg.settings.console.tokenFile != null) [ + '' + if [ ! -e "${cfg.settings.console.tokenFile}" ]; then + ${lib.getExe cscli} console enroll "$(cat ${cfg.settings.console.tokenFile})" --name ${cfg.name} + fi + '' + ]; + + setupScript = pkgs.writeShellScriptBin "crowdsec-setup" ( + lib.strings.concatStringsSep "\n" scriptArray + ); + + in + lib.mkIf (cfg.enable) { + + warnings = + [ ] + ++ lib.optionals (cfg.localConfig.profiles == [ ]) [ + "By not specifying profiles in services.crowdsec.localConfig.profiles, CrowdSec will not react to any alert by default." + ] + ++ lib.optionals (cfg.localConfig.acquisitions == [ ]) [ + "By not specifying acquisitions in services.crowdsec.localConfig.acquisitions, CrowdSec will not look for any data source." + ]; + + services.crowdsec.settings.general = with lib; { + common = { + daemonize = false; + log_media = "stdout"; + }; + config_paths = { + config_dir = confDir; + data_dir = stateDir; + simulation_path = simulationFile; + hub_dir = hubDir; + index_path = lib.strings.normalizePath "${stateDir}/hub/.index.json"; + notification_dir = notificationsDir; + plugin_dir = pluginDir; + pattern_dir = patternsDir; + }; + db_config = { + type = mkDefault "sqlite"; + db_path = mkDefault (lib.strings.normalizePath "${stateDir}/crowdsec.db"); + use_wal = mkDefault true; + }; + crowdsec_service = { + enable = mkDefault true; + acquisition_path = mkDefault localAcquisisionFile; + }; + api = { + client = { + credentials_path = cfg.settings.lapi.credentialsFile; + }; + server = { + enable = mkDefault false; + listen_uri = mkDefault "127.0.0.1:8080"; + + console_path = mkDefault consoleFile; + profiles_path = mkDefault localProfilesFile; + + online_client = mkDefault { + sharing = mkDefault true; + pull = mkDefault { + community = mkDefault true; + blocklists = mkDefault true; + }; + credentials_path = cfg.settings.capi.credentialsFile; + }; + }; + }; + prometheus = { + enabled = mkDefault true; + level = mkDefault "full"; + listen_addr = mkDefault "127.0.0.1"; + listen_port = mkDefault 6060; + }; + cscli = { + hub_branch = "v${cfg.package.version}"; + }; + }; + + environment = { + systemPackages = [ cscli ]; + }; + + systemd.packages = [ cfg.package ]; + + systemd.timers.crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { + description = "Update the crowdsec hub index"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = "yes"; + Unit = "crowdsec-update-hub.service"; + }; + }; + systemd.services = { + crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) { + description = "Update the crowdsec hub index"; + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + LimitNOFILE = 65536; + NoNewPrivileges = true; + LockPersonality = true; + RemoveIPC = true; + ReadWritePaths = [ + rootDir + confDir + ]; + ProtectSystem = "strict"; + PrivateUsers = true; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + UMask = "0077"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + SystemCallFilter = [ + " " # This is needed to clear the SystemCallFilter existing definitions + "~@reboot" + "~@swap" + "~@obsolete" + "~@mount" + "~@module" + "~@debug" + "~@cpu-emulation" + "~@clock" + "~@raw-io" + "~@privileged" + "~@resources" + ]; + CapabilityBoundingSet = [ + " " # Reset all capabilities to an empty set + ]; + RestrictAddressFamilies = [ + " " # This is needed to clear the RestrictAddressFamilies existing definitions + "none" # Remove all addresses families + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + DevicePolicy = "closed"; + ProtectKernelLogs = true; + SystemCallArchitectures = "native"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + ExecStart = "${lib.getExe cscli} --error hub update"; + ExecStartPost = "systemctl reload crowdsec.service"; + }; + }; + + crowdsec = { + description = "CrowdSec is a free, modern & collaborative behavior detection engine, coupled with a global IP reputation network."; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + path = lib.mkForce [ ]; + environment = { + LC_ALL = "C"; + LANG = "C"; + }; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + Type = "simple"; + RestartSec = 60; + LimitNOFILE = 65536; + NoNewPrivileges = true; + LockPersonality = true; + RemoveIPC = true; + ReadWritePaths = [ + rootDir + confDir + ]; + ProtectSystem = "strict"; + PrivateUsers = true; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + UMask = "0077"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + SystemCallFilter = [ + " " # This is needed to clear the SystemCallFilter existing definitions + "~@reboot" + "~@swap" + "~@obsolete" + "~@mount" + "~@module" + "~@debug" + "~@cpu-emulation" + "~@clock" + "~@raw-io" + "~@privileged" + "~@resources" + ]; + CapabilityBoundingSet = [ + " " # Reset all capabilities to an empty set + "CAP_SYSLOG" # Add capability to read syslog + ]; + RestrictAddressFamilies = [ + " " # This is needed to clear the RestrictAddressFamilies existing definitions + "none" # Remove all addresses families + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + DevicePolicy = "closed"; + ProtectKernelLogs = true; + SystemCallArchitectures = "native"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + ExecReload = [ + " " # This is needed to clear the ExecReload definitions from upstream + ]; + ExecStart = [ + " " # This is needed to clear the ExecStart definitions from upstream + "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -info" + ]; + ExecStartPre = [ + " " # This is needed to clear the ExecStartPre definitions from upstream + "${lib.getExe setupScript}" + "${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error" + ]; + }; + }; + }; + + systemd.tmpfiles.settings = { + "10-crowdsec" = + builtins.listToAttrs ( + map + (dirName: { + inherit cfg; + name = lib.strings.normalizePath dirName; + value = { + d = { + user = cfg.user; + group = cfg.group; + mode = "0750"; + }; + }; + }) + [ + stateDir + hubDir + confDir + localScenariosDir + localPostOverflowsDir + localPostOverflowsS01WhitelistDir + parsersDir + localParsersS00RawDir + localParsersS01ParseDir + localParsersS02EnrichDir + localContextsDir + notificationsDir + pluginDir + ] + ) + // builtins.listToAttrs ( + map (scenarioFile: { + inherit cfg; + name = lib.strings.normalizePath "${localScenariosDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf scenarioFile)}"; + value = { + link = { + type = "L+"; + argument = "${scenarioFile}"; + }; + }; + }) localScenariosMap + ) + // builtins.listToAttrs ( + map (parser: { + inherit cfg; + name = lib.strings.normalizePath "${localParsersS00RawDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}"; + value = { + link = { + type = "L+"; + argument = "${parser}"; + }; + }; + }) localParsersS00RawMap + ) + // builtins.listToAttrs ( + map (parser: { + inherit cfg; + name = lib.strings.normalizePath "${localParsersS01ParseDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}"; + value = { + link = { + type = "L+"; + argument = "${parser}"; + }; + }; + }) localParsersS01ParseMap + ) + // builtins.listToAttrs ( + map (parser: { + inherit cfg; + name = lib.strings.normalizePath "${localParsersS02EnrichDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}"; + value = { + link = { + type = "L+"; + argument = "${parser}"; + }; + }; + }) localParsersS02EnrichMap + ) + // builtins.listToAttrs ( + map (postoverflow: { + inherit cfg; + name = lib.strings.normalizePath "${localPostOverflowsS01WhitelistDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf postoverflow)}"; + value = { + link = { + type = "L+"; + argument = "${postoverflow}"; + }; + }; + }) localPostOverflowsS01WhitelistMap + ) + // builtins.listToAttrs ( + map (context: { + inherit cfg; + name = lib.strings.normalizePath "${localContextsDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf context)}"; + value = { + link = { + type = "L+"; + argument = "${context}"; + }; + }; + }) localContextsMap + ) + // builtins.listToAttrs ( + map (notification: { + inherit cfg; + name = lib.strings.normalizePath "${notificationsDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf notification)}"; + value = { + link = { + type = "L+"; + argument = "${notification}"; + }; + }; + }) localNotificationsMap + ); + }; + + users.users.${cfg.user} = { + name = cfg.user; + description = lib.mkDefault "CrowdSec service user"; + isSystemUser = true; + group = cfg.group; + extraGroups = [ "systemd-journal" ]; + }; + + users.groups.${cfg.group} = lib.mapAttrs (name: lib.mkDefault) { }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ + 6060 + 8080 + ]; + }; + + meta = { + maintainers = with lib.maintainers; [ + m0ustach3 + jk + ]; + }; +}