crowdsec: switch to Docker container with native firewall bouncer

Replaces the incomplete nixpkgs NixOS module with the official
CrowdSec Docker image for the LAPI, while keeping the firewall
bouncer as a native systemd service. API key is read from
/var/lib/secrets/crowdsec-bouncer-key at start time so it
never enters the Nix store.

https://claude.ai/code/session_01PwAXuaoJx7qD5FhVLsn7Sn
This commit is contained in:
Claude 2026-04-06 07:05:59 +00:00
parent f493d09c50
commit f5bb08d7dd
No known key found for this signature in database

View file

@ -1,43 +1,112 @@
{ config, lib, pkgs, ... }:
let
# Acquisition config is written to the host config dir before the container
# starts, so it persists across container restarts and reflects Nix config.
acquisYaml = ''
- source: journalctl
journalctl_filter:
- "-u"
- "sshd"
labels:
type: syslog
'';
# Generates /run/crowdsec-bouncer/config.yaml at service start, injecting the
# API key from /var/lib/secrets/crowdsec-bouncer-key without it ever entering
# the Nix store. See services/crowdsec.md for key setup instructions.
bouncerPreStart = pkgs.writeShellScript "crowdsec-bouncer-prestart" ''
set -euo pipefail
KEY_FILE=/var/lib/secrets/crowdsec-bouncer-key
if [ ! -f "$KEY_FILE" ]; then
echo "ERROR: $KEY_FILE not found. See services/crowdsec.md for setup steps." >&2
exit 1
fi
API_KEY=$(cat "$KEY_FILE")
cat > /run/crowdsec-bouncer/config.yaml << EOF
mode: nftables
pid_dir: /run/crowdsec-bouncer/
update_frequency: 10s
log_mode: stdout
log_level: info
api_url: http://127.0.0.1:8080
api_key: $API_KEY
disable_ipv6: false
deny_action: DROP
deny_log: false
nftables:
ipv4:
enabled: true
set-only: false
table: crowdsec
chain: crowdsec-chain
ipv6:
enabled: true
set-only: false
table: crowdsec6
chain: crowdsec-chain6
EOF
'';
in
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
services.crowdsec = {
enable = true;
autoUpdateService = true;
# Install detection collections on first boot
hub.collections = [ "crowdsecurity/linux" "crowdsecurity/sshd" ];
virtualisation.docker.enable = true;
virtualisation.oci-containers.backend = "docker";
settings = {
# Enable the Local API server (required for bouncer registration)
general.api.server.enable = true;
# Where the LAPI client credentials will be written on first boot
lapi.credentialsFile = "/var/lib/crowdsec/state/lapi-credentials.yaml";
};
localConfig.acquisitions = [
# SSH brute-force detection
{
source = "journalctl";
journalctl_filter = [ "-u" "sshd" ];
labels.type = "syslog";
}
# CrowdSec LAPI runs as a Docker container.
# Collections are installed on first boot via the COLLECTIONS env var.
# Journals are mounted read-only so CrowdSec can run journalctl inside the container.
virtualisation.oci-containers.containers.crowdsec = {
image = "crowdsecurity/crowdsec:latest";
ports = [ "127.0.0.1:8080:8080" ];
volumes = [
"/var/lib/crowdsec/data:/var/lib/crowdsec/data"
"/var/lib/crowdsec/config:/etc/crowdsec"
"/var/log/journal:/var/log/journal:ro"
"/run/log/journal:/run/log/journal:ro"
"/etc/machine-id:/etc/machine-id:ro"
];
environment = {
COLLECTIONS = "crowdsecurity/linux crowdsecurity/sshd";
};
};
# The bouncer-register service uses raw cscli (no -c flag), so it looks for
# config at /etc/crowdsec/config.yaml. Symlink the Nix-generated config there.
# Write acquisition config into the host config dir before the container starts.
systemd.services.docker-crowdsec.preStart = ''
mkdir -p /var/lib/crowdsec/config/acquis.d
cat > /var/lib/crowdsec/config/acquis.d/nixos.yaml << 'ACQUIS'
${acquisYaml}
ACQUIS
'';
systemd.tmpfiles.rules = [
"L+ /etc/crowdsec/config.yaml - - - - ${(pkgs.formats.yaml { }).generate "crowdsec.yaml" config.services.crowdsec.settings.general}"
"d /var/lib/crowdsec/data 0750 root root -"
"d /var/lib/crowdsec/config 0750 root root -"
"d /var/lib/secrets 0700 root root -"
];
# Ensure /var/lib/crowdsec exists before crowdsec starts (race with tmpfiles-resetup)
systemd.services.crowdsec.after = [ "systemd-tmpfiles-resetup.service" ];
# Firewall bouncer runs natively. API key is injected at start time from
# /var/lib/secrets/crowdsec-bouncer-key — see services/crowdsec.md.
systemd.services.crowdsec-firewall-bouncer = {
description = "CrowdSec nftables firewall bouncer";
after = [ "network.target" "docker-crowdsec.service" ];
wants = [ "docker-crowdsec.service" ];
wantedBy = [ "multi-user.target" ];
# Firewall bouncer — auto-registers to local CrowdSec LAPI
services.crowdsec-firewall-bouncer = {
enable = true;
settings.api_url = "http://127.0.0.1:8080";
serviceConfig = {
Type = "simple";
RuntimeDirectory = "crowdsec-bouncer";
ExecStartPre = bouncerPreStart;
ExecStart = "${pkgs.crowdsec-firewall-bouncer}/bin/cs-firewall-bouncer -c /run/crowdsec-bouncer/config.yaml";
Restart = "on-failure";
RestartSec = "5s";
AmbientCapabilities = [ "CAP_NET_ADMIN" "CAP_NET_RAW" ];
CapabilityBoundingSet = [ "CAP_NET_ADMIN" "CAP_NET_RAW" ];
};
};
};
}