- crowdsec.nix: drop the ntfy notifications (one push per ban was constant noise on the WAN-exposed box); bans still happen silently - service-health.nix: OnFailure=notify-failure@%n on 16 core units sends an ntfy 'down' push when a unit truly fails (after exhausting Restart=), then a 'recovered' push when it comes back. Shares /var/secrets/ntfy-url. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
145 lines
5.6 KiB
Nix
145 lines
5.6 KiB
Nix
# services/crowdsec.nix — Vendors the crowdsec module rewrite from
|
|
# https://github.com/NixOS/nixpkgs/pull/446307 (TornaxO7's branch) until
|
|
# it lands upstream. The upstream module in nixpkgs at the pinned revision
|
|
# is broken for first-time bootstrap (no auto cscli machines add, DynamicUser
|
|
# state ownership wedges).
|
|
#
|
|
# When PR #446307 merges to nixpkgs unstable:
|
|
# 1. Bump flake.lock past the merge commit
|
|
# 2. Delete ../modules/crowdsec/ and the disabledModules + imports lines below
|
|
# 3. The settings/option API is the same as the PR's, so config below is forward-compatible
|
|
#
|
|
# CrowdSec bans silently — no ntfy pushes (they were constant noise).
|
|
# The /var/secrets/ntfy-url topic is used by services/service-health.nix instead.
|
|
{ config, lib, pkgs, ... }:
|
|
let
|
|
# nixpkgs only builds the agent + cscli; the new module also expects
|
|
# notification plugins at $out/libexec/crowdsec/plugins/. Compile them
|
|
# from the same source tree (cmd/notification-*) and move them there.
|
|
pluginNames = [ "dummy" "email" "file" "http" "sentinel" "slack" "splunk" ];
|
|
crowdsecWithPlugins = pkgs.crowdsec.overrideAttrs (old: {
|
|
subPackages = (old.subPackages or [ ]) ++ map (p: "cmd/notification-${p}") pluginNames;
|
|
postInstall = (old.postInstall or "") + ''
|
|
mkdir -p $out/libexec/crowdsec/plugins
|
|
for p in ${lib.concatStringsSep " " pluginNames}; do
|
|
if [ -f $out/bin/notification-$p ]; then
|
|
mv $out/bin/notification-$p $out/libexec/crowdsec/plugins/notification-$p
|
|
fi
|
|
done
|
|
'';
|
|
});
|
|
in
|
|
{
|
|
disabledModules = [
|
|
"services/security/crowdsec.nix"
|
|
"services/security/crowdsec-firewall-bouncer.nix"
|
|
];
|
|
|
|
imports = [
|
|
../modules/crowdsec/crowdsec.nix
|
|
../modules/crowdsec/crowdsec-firewall-bouncer.nix
|
|
];
|
|
|
|
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
|
|
|
|
# Static user/group for crowdsec. The vendored module relies on
|
|
# DynamicUser=true plus a chown hack in crowdsec-setup's ExecStartPre,
|
|
# which broke on stable's systemd because the dynamic user isn't
|
|
# visible to NSS at chown time. Declaring the user statically makes
|
|
# systemd use it (DynamicUser becomes a no-op) and chown succeeds.
|
|
users.users.crowdsec = {
|
|
isSystemUser = true;
|
|
group = "crowdsec";
|
|
home = "/var/lib/crowdsec";
|
|
description = "CrowdSec security agent";
|
|
};
|
|
users.groups.crowdsec = { };
|
|
|
|
services.crowdsec = {
|
|
enable = true;
|
|
name = "fredos-mediaserver";
|
|
package = crowdsecWithPlugins;
|
|
|
|
hub.collections = [
|
|
"crowdsecurity/linux" # sshd + linux LPE
|
|
"crowdsecurity/nginx" # nginx parser
|
|
"crowdsecurity/base-http-scenarios" # generic HTTP attacks
|
|
"crowdsecurity/http-cve" # known-CVE fingerprints
|
|
"crowdsecurity/whitelist-good-actors" # don't ban legit crawlers
|
|
];
|
|
|
|
# Allow the agent (DynamicUser) to read its acquisition sources:
|
|
# - nginx group → /var/log/nginx/access.log (nginx:nginx 640)
|
|
# - systemd-journal → journald entries from sshd + authelia
|
|
# (without it, journalctl returns "insufficient permissions" and
|
|
# the entire ssh-bf / authelia-bf detection chain runs blind)
|
|
readOnlyPaths = [ "/var/log/nginx" ];
|
|
extraGroups = [ "nginx" "systemd-journal" ];
|
|
|
|
settings = {
|
|
# config.yaml — main agent + LAPI configuration
|
|
config.api.server.listen_uri = "127.0.0.1:8081"; # 8080 is qBit
|
|
|
|
# Drop alerts originating from LAN clients before they're scored.
|
|
# Without this, repeated Authelia 401s from inside the house (e.g.
|
|
# a stale browser session on the gaming desktop) trip ssh-bf /
|
|
# http-bf scenarios and the firewall bouncer self-bans 10.0.0.x.
|
|
parsers.s02Enrich = [
|
|
{
|
|
name = "nordhammer/lan-whitelist";
|
|
description = "Whitelist LAN + loopback to prevent self-bans";
|
|
whitelist = {
|
|
reason = "Local LAN";
|
|
ip = [ "127.0.0.1" "::1" ];
|
|
cidr = [ "10.0.0.0/24" ];
|
|
};
|
|
}
|
|
];
|
|
|
|
# Log sources to ingest
|
|
acquisitions = [
|
|
{
|
|
source = "file";
|
|
filenames = [ "/var/log/nginx/access.log" ];
|
|
labels.type = "nginx";
|
|
}
|
|
{
|
|
source = "journalctl";
|
|
journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ];
|
|
labels.type = "syslog";
|
|
}
|
|
{
|
|
source = "journalctl";
|
|
journalctl_filter = [ "_SYSTEMD_UNIT=authelia-main.service" ];
|
|
labels.type = "syslog";
|
|
}
|
|
];
|
|
|
|
# Profiles set ban duration to 4h. No ntfy notifications: a push per
|
|
# ban was constant noise on a WAN-exposed box. ntfy is now reserved
|
|
# for service-down alerts (see services/service-health.nix); CrowdSec
|
|
# still bans silently.
|
|
profiles = [
|
|
{
|
|
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";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
# Firewall bouncer enforces decisions via nftables; auto-registers with LAPI
|
|
services.crowdsec-firewall-bouncer = {
|
|
enable = true;
|
|
registerBouncer.enable = true;
|
|
};
|
|
};
|
|
}
|