Initial commit

This commit is contained in:
Christian Kampka
2024-03-10 15:42:56 +01:00
commit a700a08c5b
8 changed files with 630 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
result

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

132
README.md Normal file
View File

@@ -0,0 +1,132 @@
# Crowdsec for NixOS
This repository contains a [Nix flake](https://nixos.wiki/wiki/Flakes) for running [Crowdsec](https://www.crowdsec.net/) on NixOS.
CrowdSec is a security tool designed to protect servers, services, and applications by analyzing user behavior and network traffic to detect and block potential attacks. It operates similarly to Fail2Ban but with a few key differences:
CrowdSec leverages the power of its community by sharing information about attacks among users. When one user detects a new threat, the details are shared across the network, allowing others to protect themselves against this threat, effectively creating a collective intelligence about emerging threats.
In simple terms, think of CrowdSec as a neighborhood watch program for the internet, where everyone contributes to and benefits from a shared pool of intelligence about potential threats.
## Usage
### Crowdsec engine
To setup the [security engine](https://docs.crowdsec.net/docs/getting_started/security_engine_intro/), import the module and activate the service.
```nix
{
inputs = {
crowdsec = {
url = "github:kampka/nix-flake-crowdsec";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = flakes @ {
self,
nixpkgs,
crowdsec,
...
}: {
nixosConfiguration.<your-hostname> = nixpkgs.lib.nixosSystem {
# ...
modules = [
# ...
crowdsec.nixosModules.crowdsec
({ pkgs, lib, ... }: {
services.crowdsec = {
enable = true;
enrollKeyFile = "/path/to/enroll-key";
settings = {
api.server = {
listen_uri = "127.0.0.1:8080";
};
};
};
})
];
};
};
}
```
In case you are setting up a central security engine, adjust the `listen_uri` to be reachable by your bouncers.
To enroll your crowdsec engine into the central API, you need to obtain an enrollment key from the central [app dashboard](https://app.crowdsec.net/).
Enrolling your engine will give it access to community or commercial blocklist and decisions, depending on your plan.
Enrollment is optional, if you do not want to enroll your engine and just at on your own logs / events, simply omit the `enrollKeyFile` from the settings.
For additional configuration options, please consult the (Crowdsec documentation)[https://docs.crowdsec.net/docs/configuration/crowdsec_configuration/].
### Crowdsec firewall bouncer
This flake ships the Crowdsec [firewall bouncer](https://docs.crowdsec.net/docs/getting_started/security_engine_intro/).
It will block traffic from blacklisted IPs on the firewall level.
At the time of writing, only `iptables` support has proper defaults and testing.
If you are using `nftables` (`networking.nftables.enable = true`), you need to supply bouncer configuration yourself (PRs welcome).
Please consult the [bouncer documentation](https://docs.crowdsec.net/u/bouncers/firewall/#nftables-specific-directives) for directions.
```nix
{
inputs = {
crowdsec = {
url = "github:kampka/nix-flake-crowdsec";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = flakes @ {
self,
nixpkgs,
crowdsec,
...
}: {
nixosConfiguration.<your-hostname> = nixpkgs.lib.nixosSystem {
# ...
modules = [
# ...
crowdsec.nixosModules.crowdsec-firewall-bouncer;
({ pkgs, lib, ... }: {
nixpkgs.overlays = [crowdsec.overlays.default];
services.crowdsec-firewall-bouncer = {
enable = true;
settings = {
api_key = "<api-key>";
api_url = "http://localhost:8080";
};
};
})
];
};
};
}
```
In order to connect to your security engine, you need to [add your bouncer](https://docs.crowdsec.net/docs/cscli/cscli_bouncers_add/) to the security engine.
You can either use a pre-generated key or have the security engine generate one for you.
Depending on your security requirements and secrets management, this process is scriptable through an `ExecStartPre` script of the engine, eg.
```nix
{
services.crowdsec = {
ExecStartPre = let
script = pkgs.writeScriptBin "register-bouncer" ''
#!${pkgs.runtimeShell}
set -eu
set -o pipefail
if ! cscli bouncers list | grep -q "my-bouncer"; then
cscli bouncers add "my-bouncer" --key "<api-key>"
fi
'';
in ["${script}/bin/register-bouncer"];
};
}
```

58
flake.lock generated Normal file
View File

@@ -0,0 +1,58 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1697915759,
"narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

27
flake.nix Normal file
View File

@@ -0,0 +1,27 @@
{
description = "A Aggregate prometheus exporters into a single endpoint";
outputs = {
self,
nixpkgs,
flake-utils,
}: let
systems = flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
bouncer-firewall = pkgs.callPackage ./packages/bouncer-firewall {};
in {
formatter = pkgs.alejandra;
packages."crowdsec-firewall-bouncer" = bouncer-firewall;
});
in (systems
// {
nixosModules = {
crowdsec = import ./modules/crowdsec;
crowdsec-firewall-bouncer = import ./modules/crowdsec-firewall-bouncer;
};
overlays.default = final: prev: {
crowdsec-firewall-bouncer = systems.packages.${final.system}.crowdsec-firewall-bouncer;
};
});
}

View File

@@ -0,0 +1,101 @@
{
config,
pkgs,
lib,
...
}: let
cfg = config.services.crowdsec-firewall-bouncer;
format = pkgs.formats.yaml {};
configFile = format.generate "crowdsec.yaml" cfg.settings;
pkg = cfg.package;
backend =
if config.networking.nftables.enable
then "nftables"
else "iptables";
defaultSettings = with lib; {
log_mode = "stdout";
mode = mkDefault backend;
ipset_type = mkDefault "nethash";
update_frequency = mkDefault "10s";
deny_action = mkDefault "DROP";
blacklists_ipv4 = mkDefault "crowdsec-blacklists";
blacklists_ipv6 = mkDefault "crowdsec6-blacklists";
iptables_chains = mkDefault ["INPUT"];
};
in {
options.services.crowdsec-firewall-bouncer = with lib; {
enable = mkEnableOption "CrowSec Firewall Bouncer";
package = mkPackageOption pkgs "crowdsec-firewall-bouncer" {};
settings = mkOption {
description = mdDoc ''
Settings for CrowdSec Firewall Bouncer. Refer to <https://docs.crowdsec.net/u/bouncers/firewall/#configuration-directives> for details.
'';
type = format.type;
default = {};
};
};
config = lib.mkIf (cfg.enable) {
services.crowdsec-firewall-bouncer.settings = defaultSettings;
systemd.packages = [pkg];
systemd.services = {
crowdsec-firewall-bouncer = {
description = "Crowdsec Firewall Bouncer";
path = [pkg pkgs.ipset pkgs.iptables pkgs.nftables];
wantedBy = ["multi-user.target"];
partOf = ["firewall.service"];
serviceConfig = with lib; {
Type = "notify";
Restart = "on-failure";
RestartSec = 10;
LimitNOFILE = mkDefault 65536;
MemoryDenyWriteExecute = mkDefault true;
CapabilityBoundingSet = mkDefault ["CAP_NET_ADMIN" "CAP_NET_RAW"];
NoNewPrivileges = mkDefault true;
LockPersonality = mkDefault true;
RemoveIPC = mkDefault true;
ProtectSystem = mkDefault "strict";
ProtectHome = mkDefault true;
PrivateTmp = mkDefault true;
PrivateDevices = mkDefault true;
ProtectHostname = mkDefault true;
ProtectKernelTunables = mkDefault true;
ProtectKernelModules = mkDefault true;
ProtectControlGroups = mkDefault true;
ProtectProc = mkDefault "invisible";
ProcSubset = mkDefault "pid";
RestrictNamespaces = mkDefault true;
RestrictRealtime = mkDefault true;
RestrictSUIDSGID = mkDefault true;
SystemCallFilter = mkDefault ["@system-service" "@network-io"];
SystemCallArchitectures = ["native"];
SystemCallErrorNumber = mkDefault "EPERM";
ExecPaths = ["/nix/store"];
NoExecPaths = ["/"];
ExecStartPost = "${pkgs.coreutils}/bin/sleep 0.2";
ExecStart = "${pkg}/bin/cs-firewall-bouncer -c ${configFile}";
ExecStartPre = ["${pkg}/bin/cs-firewall-bouncer -t -c ${configFile}"];
};
};
};
};
}

View File

@@ -0,0 +1,264 @@
{
config,
pkgs,
lib,
...
}: let
cfg = config.services.crowdsec;
format = pkgs.formats.yaml {};
configFile = format.generate "crowdsec.yaml" cfg.settings;
pkg = cfg.package.overrideAttrs (old: {
ldflags =
(old.ldflags or [])
++ [
"-X github.com/crowdsecurity/go-cs-lib/version.Version=v${old.version}"
];
patches =
(old.patches or [])
++ [
(
pkgs.fetchpatch
{
url = "https://patch-diff.githubusercontent.com/raw/crowdsecurity/crowdsec/pull/2868.patch";
hash = "sha256-RSfLhNZ3JVvHoW/BNca9Hs4lpjcDtE1vsBDjJeaHqvc=";
}
)
];
});
defaultPatterns = lib.mapAttrs (name: value: lib.mkDefault "${pkg}/share/crowdsec/config/patterns/${name}") (builtins.readDir "${pkg}/share/crowdsec/config/patterns");
patternsDir = pkgs.runCommandNoCC "crowdsec-patterns" {} ''
mkdir -p $out
${lib.concatStringsSep "\n" (lib.attrValues (lib.mapAttrs (
k: v: ''
ln -sf ${v} $out/${k}
''
)
cfg.patterns))}
'';
consoleSettings = {
share_manual_decisions = false;
share_custom = true;
share_tainted = true;
share_context = false;
};
defaultSettings = with lib; {
common = {
daemonize = mkForce false;
log_media = mkForce "stdout";
};
config_paths = {
config_dir = mkDefault "/var/lib/crowdsec/config";
data_dir = mkDefault dataDir;
hub_dir = mkDefault hubDir;
index_path = mkDefault "${hubDir}/.index.json";
simulation_path = mkDefault "${pkg}/share/crowdsec/config/simulation.yaml";
pattern_dir = mkDefault patternsDir;
};
db_config = {
type = mkDefault "sqlite";
db_path = mkDefault "${dataDir}/crowdsec.db";
use_wal = true;
};
crowdsec_service = {
enable = mkDefault true;
};
api = {
client = {
credentials_path = mkDefault "${stateDir}/local_api_credentials.yaml";
};
server = {
enable = mkDefault (cfg.enrollKeyFile != null);
listen_uri = mkDefault "127.0.0.1:8080";
console_path = mkDefault "${stateDir}/console.yaml";
profiles_path = mkDefault "${pkg}/share/crowdsec/config/profiles.yaml";
online_client.credentials_path = mkDefault "${stateDir}/online_api_credentials.yaml";
};
};
};
user = "crowdsec";
group = "crowdsec";
stateDir = "/var/lib/crowdsec";
dataDir = "${stateDir}/data";
hubDir = "${stateDir}/hub";
in {
options.services.crowdsec = with lib; {
enable = mkEnableOption "CrowSec Security Engine";
package = mkPackageOption pkgs "crowdsec" {};
name = mkOption {
type = types.str;
description = mdDoc ''
Name of the machine when registering it at the central or loal api.
'';
default = config.networking.hostName;
};
enrollKeyFile = mkOption {
description = mdDoc ''
The file containing the enrollment key used to enroll the engine at the central api console.
See <https://docs.crowdsec.net/docs/next/console/enrollment/#where-can-i-find-my-enrollment-key> for details.
'';
type = types.nullOr types.path;
default = null;
};
patterns = mkOption {
description = mdDoc ''
A set of pattern files for parsing logs, in the form "type" to file containing the corresponding GROK patterns.
All default patterns are automatically included.
See <https://github.com/crowdsecurity/crowdsec/tree/master/config/patterns>.
'';
type = types.attrsOf types.pathInStore;
default = {};
example = lib.literalExpression ''
{ ssh = ./patterns/ssh;}
'';
};
settings = mkOption {
description = mdDoc ''
Settings for MediaMTX. Refer to the defaults at
<https://github.com/bluenviron/mediamtx/blob/main/mediamtx.yml>.
'';
type = format.type;
default = {};
};
};
config = let
cscli = pkgs.writeScriptBin "cscli" ''
#!${pkgs.runtimeShell}
set -eu
set -o pipefail
exec ${pkg}/bin/cscli -c=${configFile} "''${@}"
'';
in
lib.mkIf (cfg.enable) {
services.crowdsec.settings = defaultSettings;
services.crowdsec.patterns = defaultPatterns;
environment = {
systemPackages = [cscli];
};
systemd.packages = [pkg];
systemd.timers.crowdsec-update-hub = {
description = "Update the crowdsec hub index";
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = "daily";
Persistent = "yes";
Unit = "crowdsec-update-hub.service";
};
};
systemd.services = let
sudo_doas =
if config.security.doas.enable == true
then "${pkgs.doas}/bin/doas"
else "${pkgs.sudo}/bin/sudo";
in {
crowdsec-update-hub = {
description = "Update the crowdsec hub index";
path = [cscli];
serviceConfig = {
Type = "oneshot";
ExecStart = "${sudo_doas} -u crowdsec ${cscli}/bin/cscli --error hub upgrade";
ExecStartPost = " systemctl restart crowdsec.service";
};
};
crowdsec = {
description = "CrowdSec is a free, modern & collaborative behavior detection engine, coupled with a global IP reputation network.";
path = [cscli];
wantedBy = ["multi-user.target"];
serviceConfig = with lib; {
User = "crowdsec";
Group = "crowdsec";
Restart = "on-failure";
LimitNOFILE = mkDefault 65536;
CapabilityBoundingSet = mkDefault [];
NoNewPrivileges = mkDefault true;
LockPersonality = mkDefault true;
RemoveIPC = mkDefault true;
ReadWritePaths = [stateDir];
ProtectSystem = mkDefault "strict";
PrivateUsers = mkDefault true;
ProtectHome = mkDefault true;
PrivateTmp = mkDefault true;
PrivateDevices = mkDefault true;
ProtectHostname = mkDefault true;
ProtectKernelTunables = mkDefault true;
ProtectKernelModules = mkDefault true;
ProtectControlGroups = mkDefault true;
ProtectProc = mkDefault "invisible";
ProcSubset = mkDefault "pid";
RestrictNamespaces = mkDefault true;
RestrictRealtime = mkDefault true;
RestrictSUIDSGID = mkDefault true;
SystemCallFilter = mkDefault ["@system-service" "@network-io"];
SystemCallArchitectures = ["native"];
SystemCallErrorNumber = mkDefault "EPERM";
ExecPaths = ["/nix/store"];
NoExecPaths = ["/"];
ExecStart = "${pkg}/bin/crowdsec -c ${configFile}";
ExecStartPre = let
script = pkgs.writeScriptBin "crowdsec-setup" ''
#!${pkgs.runtimeShell}
set -eu
set -o pipefail
if [ ! -s "${cfg.settings.api.client.credentials_path}" ]; then
cscli machine add "${cfg.name}" --auto
fi
${lib.optionalString cfg.settings.api.server.enable ''
if ! grep -q password "${cfg.settings.api.server.online_client.credentials_path}" ]; then
cscli capi register
fi
cscli hub update
${lib.optionalString (cfg.enrollKeyFile != null) ''
if [ ! -e "${cfg.settings.api.server.console_path}" ]; then
cscli console enroll "$(cat ${cfg.enrollKeyFile})" --name ${cfg.name}
fi
''}
''}
'';
in ["${script}/bin/crowdsec-setup"];
};
};
};
systemd.tmpfiles.rules = [
"d '${stateDir}' 0750 ${user} ${group} - -"
"d '${dataDir}' 0750 ${user} ${group} - -"
"d '${hubDir}' 0750 ${user} ${group} - -"
"f '${cfg.settings.api.server.online_client.credentials_path}' 0750 ${user} ${group} - -"
"f '${cfg.settings.config_paths.index_path}' 0750 ${user} ${group} - -"
];
users.users.${user} = lib.mapAttrs (name: lib.mkDefault) {
description = "Cowdsec service user";
isSystemUser = true;
inherit group;
};
users.groups.${group} = lib.mapAttrs (name: lib.mkDefault) {};
};
}

View File

@@ -0,0 +1,28 @@
{
lib,
buildGoModule,
fetchFromGitHub,
}:
buildGoModule rec {
pname = "cs-firewall-bouncer";
version = "0.0.28";
src = fetchFromGitHub {
owner = "crowdsecurity";
repo = pname;
rev = "v${version}";
hash = "sha256-Y1pCupCtYkOD6vKpcmM8nPlsGbO0kYhc3PC9YjJHeMw=";
};
vendorHash = "sha256-BA7OHvqIRck3LVgtx7z8qhgueaJ6DOMU8clvWKUCdqE=";
meta = with lib; {
homepage = "https://crowdsec.net/";
changelog = "https://github.com/crowdsecurity/${pname}/releases/tag/v${version}";
description = "Crowdsec bouncer for firewalls.";
longDescription = ''
crowdsec-firewall-bouncer will fetch new and old decisions from a CrowdSec API to add them in a blocklist used by supported firewalls.
'';
license = licenses.mit;
};
}