Files
nix-config/modules/nixos/services/crowdsec/default.nix
2026-03-23 14:07:48 -05:00

208 lines
7.5 KiB
Nix
Executable File

{
config,
lib,
pkgs,
namespace,
...
}:
let
inherit (lib.${namespace}) mkOpt;
name = "crowdsec";
cfg = config.${namespace}.services.${name};
crowdsecConfig = lib.${namespace}.mkModule {
inherit config name;
description = "crowdsec";
options = with lib; {
apiKey = mkOpt types.str "" "API key for crowdsec bouncer";
};
moduleConfig = {
services = {
crowdsec = {
enable = true;
openFirewall = cfg.openFirewall;
hub = {
appSecConfigs = [
"crowdsecurity/appsec-default"
];
appSecRules = [
"crowdsecurity/base-config"
];
collections = [
"crowdsecurity/http-cve"
"crowdsecurity/http-dos"
"crowdsecurity/linux"
"crowdsecurity/nextcloud"
"crowdsecurity/pgsql"
"crowdsecurity/smb"
"crowdsecurity/sshd"
"crowdsecurity/traefik"
"firix/authentik"
];
parsers = [
"crowdsecurity/actual-budget-whitelist"
"crowdsecurity/jellyfin-whitelist"
"crowdsecurity/jellyseerr-whitelist"
"crowdsecurity/nextcloud-logs"
"crowdsecurity/nextcloud-whitelist"
"crowdsecurity/pgsql-logs"
"crowdsecurity/smb-logs"
"crowdsecurity/sshd-logs"
"crowdsecurity/sshd-success-logs"
"crowdsecurity/syslog-logs"
];
postOverflows = [
"crowdsecurity/auditd-nix-wrappers-whitelist-process"
];
scenarios = [
"crowdsecurity/ssh-bf"
];
};
localConfig = {
acquisitions = [
{
journalctl_filter = [
"_SYSTEMD_UNIT=authentik.service"
];
labels = {
type = "syslog";
};
source = "journalctl";
}
{
journalctl_filter = [
"_SYSTEMD_UNIT=postgresql.service"
];
labels = {
type = "syslog";
};
source = "journalctl";
}
{
journalctl_filter = [
"_SYSTEMD_UNIT=smbd.service"
];
labels = {
type = "syslog";
};
source = "journalctl";
}
{
journalctl_filter = [
"_SYSTEMD_UNIT=sshd.service"
];
labels = {
type = "syslog";
};
source = "journalctl";
}
{
journalctl_filter = [
"_SYSTEMD_UNIT=traefik.service"
];
labels = {
type = "syslog";
};
source = "journalctl";
}
];
};
settings = {
general.api = {
server = {
enable = true;
listen_uri = "${cfg.listenAddress}:${toString cfg.port}";
};
client = {
credentials_path = lib.mkForce "${cfg.configDir}/crowdsec/client.yaml";
};
};
capi.credentialsFile = lib.mkDefault "${cfg.configDir}/crowdsec/capi.yaml";
};
};
crowdsec-firewall-bouncer = {
enable = true;
registerBouncer = {
enable = true;
bouncerName = "nas-bouncer";
};
# secrets.apiKeyPath = config.sops.secrets."jallen-nas/crowdsec-firewall-bouncer-api-key".path;
};
};
# The upstream crowdsec module uses ReadWritePaths (not StateDirectory) on
# crowdsec.service, meaning it expects /var/lib/crowdsec to exist as a real
# directory (created by tmpfiles). However, crowdsec-firewall-bouncer-register
# declares StateDirectory=crowdsec with DynamicUser=true, which conflicts: it
# tries to create /var/lib/private/crowdsec and symlink /var/lib/crowdsec → it,
# but /var/lib/crowdsec already exists as a real dir. Disabling DynamicUser on
# those two services lets them use the real crowdsec user/group instead, which is
# consistent with how crowdsec.service itself runs.
systemd.services.crowdsec.serviceConfig.DynamicUser = lib.mkForce false;
systemd.services.crowdsec-firewall-bouncer.serviceConfig.DynamicUser = lib.mkForce false;
systemd.services.crowdsec-firewall-bouncer-register.serviceConfig.DynamicUser = lib.mkForce false;
# The upstream unit has Requires= but no After= for the register service, so
# the bouncer starts in parallel and hits LoadCredential before the key file
# exists. Adding After= enforces that the register service completes first.
systemd.services.crowdsec-firewall-bouncer.after = [ "crowdsec-firewall-bouncer-register.service" ];
# The upstream register script exits with an error when the bouncer is already
# registered in the LAPI but the local api-key.cred file is missing (e.g. after
# a system wipe or impermanence rotation). Override the script so that when the
# key file is absent it deletes the stale registration and re-registers, producing
# a fresh key file.
systemd.services.crowdsec-firewall-bouncer-register.script =
let
apiKeyFile = "/var/lib/crowdsec-firewall-bouncer-register/api-key.cred";
bouncerName = "nas-bouncer";
cscli = lib.getExe' config.services.crowdsec.package "cscli";
jq = lib.getExe pkgs.jq;
in
lib.mkForce ''
if ${cscli} bouncers list --output json | ${jq} -e -- 'any(.[]; .name == "${bouncerName}")' >/dev/null; then
# Bouncer already registered. Verify the API key is still present.
if [ ! -f ${apiKeyFile} ]; then
echo "Bouncer registered but API key file missing deleting stale registration and re-registering"
${cscli} bouncers delete -- ${bouncerName}
rm -f '${apiKeyFile}'
if ! ${cscli} bouncers add --output raw -- ${bouncerName} >${apiKeyFile}; then
rm -f '${apiKeyFile}'
exit 1
fi
fi
else
# Bouncer not registered fresh registration.
rm -f '${apiKeyFile}'
if ! ${cscli} bouncers add --output raw -- ${bouncerName} >${apiKeyFile}; then
rm -f '${apiKeyFile}'
exit 1
fi
fi
'';
# crowdsec-firewall-bouncer-register calls cscli without -c, so cscli
# looks for /etc/crowdsec/config.yaml. The upstream crowdsec.service uses
# a nix store path via -c and never creates that file. Expose the config
# at /etc/crowdsec/config.yaml by extracting the store path from the
# crowdsec service's ExecStart list at NixOS eval time.
environment.etc."crowdsec/config.yaml" =
let
# ExecStart is [ " " "<store>/crowdsec -c <config-file> -info" ]
execStart = builtins.elemAt config.systemd.services.crowdsec.serviceConfig.ExecStart 1;
configPath = builtins.head (builtins.match ".* -c ([^ ]+) .*" execStart);
in
{
source = configPath;
mode = "0440";
user = "crowdsec";
group = "crowdsec";
};
};
};
in
{
imports = [ crowdsecConfig ];
}