365 lines
11 KiB
Nix
365 lines
11 KiB
Nix
# NixOS Module for Eris Tarpit
|
|
self: {
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}: let
|
|
inherit (builtins) toJSON toString;
|
|
inherit (lib.modules) mkIf;
|
|
inherit (lib.options) mkOption mkEnableOption literalExpression;
|
|
inherit (lib.types) package str port int listOf enum bool attrsOf path;
|
|
inherit (lib.lists) optionals;
|
|
|
|
cfg = config.services.eris;
|
|
|
|
# Generate the config.json content
|
|
erisConfigFile = pkgs.writeText "eris-config.json" (toJSON {
|
|
listen_addr = cfg.listenAddress;
|
|
metrics_addr = cfg.metricsAddress;
|
|
backend_addr = cfg.backendAddress;
|
|
min_delay = cfg.minDelay;
|
|
max_delay = cfg.maxDelay;
|
|
max_tarpit_time = cfg.maxTarpitTime;
|
|
block_threshold = cfg.blockThreshold;
|
|
trap_patterns = cfg.trapPatterns;
|
|
whitelist_networks = cfg.whitelistNetworks;
|
|
markov_corpora_dir = "${cfg.dataDir}/corpora";
|
|
lua_scripts_dir = "${cfg.dataDir}/scripts";
|
|
data_dir = cfg.dataDir;
|
|
config_dir = "${cfg.stateDir}/conf";
|
|
cache_dir = cfg.cacheDir;
|
|
});
|
|
in {
|
|
###### interface
|
|
options = {
|
|
services.eris = {
|
|
enable = mkEnableOption "Eris Tarpit Service";
|
|
|
|
package = mkOption {
|
|
type = package;
|
|
default = self.packages.${pkgs.stdenv.system}.eris;
|
|
defaultText = literalExpression "self.packages.\${pkgs.stdenv.system}.eris";
|
|
description = "The Eris package to use.";
|
|
};
|
|
|
|
listenAddress = mkOption {
|
|
type = str;
|
|
default = "0.0.0.0:8888";
|
|
description = "The IP address and port for the main tarpit service to listen on.";
|
|
example = "127.0.0.1:9999";
|
|
};
|
|
|
|
metricsAddress = mkOption {
|
|
type = port;
|
|
default = "0.0.0.0:9100";
|
|
example = "127.0.0.1:9100";
|
|
description = "The IP address and port for the Prometheus metrics endpoint.";
|
|
};
|
|
|
|
backendAddress = mkOption {
|
|
type = str;
|
|
default = "127.0.0.1:80";
|
|
description = "The address of the backend service to proxy legitimate requests to.";
|
|
example = "10.0.0.5:8080";
|
|
};
|
|
|
|
minDelay = mkOption {
|
|
type = int;
|
|
default = 1000;
|
|
description = "Minimum delay (in ms) between sending characters in tarpit mode.";
|
|
};
|
|
|
|
maxDelay = mkOption {
|
|
type = int;
|
|
default = 15000;
|
|
description = "Maximum delay (in ms) between sending characters in tarpit mode.";
|
|
};
|
|
|
|
maxTarpitTime = mkOption {
|
|
type = int;
|
|
default = 600;
|
|
description = "Maximum time (in seconds) a single connection can be held in the tarpit.";
|
|
};
|
|
|
|
blockThreshold = mkOption {
|
|
type = int;
|
|
default = 3;
|
|
description = "Number of hits from an IP before it gets permanently blocked.";
|
|
};
|
|
|
|
trapPatterns = mkOption {
|
|
type = listOf str;
|
|
default = [
|
|
"/vendor/phpunit"
|
|
"eval-stdin.php"
|
|
"/wp-admin"
|
|
"/wp-login.php"
|
|
"/xmlrpc.php"
|
|
"/phpMyAdmin"
|
|
"/solr/"
|
|
"/.env"
|
|
"/config"
|
|
"/api/"
|
|
"/actuator/"
|
|
];
|
|
description = "List of URL path substrings that trigger the tarpit.";
|
|
example = literalExpression ''
|
|
[ "/.git/config", "/admin" ]
|
|
'';
|
|
};
|
|
|
|
whitelistNetworks = mkOption {
|
|
type = listOf str;
|
|
default = [
|
|
"192.168.0.0/16"
|
|
"10.0.0.0/8"
|
|
"172.16.0.0/12"
|
|
"127.0.0.0/8"
|
|
];
|
|
example = literalExpression "[ \"192.168.1.0/24\" ]";
|
|
description = "List of CIDR networks whose IPs should never be tarpitted.";
|
|
};
|
|
|
|
logLevel = mkOption {
|
|
type = enum ["error" "warn" "info" "debug" "trace"];
|
|
default = "info";
|
|
description = "Logging level for the Eris service.";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = str;
|
|
default = "eris";
|
|
description = "User account under which Eris runs.";
|
|
};
|
|
group = mkOption {
|
|
type = str;
|
|
default = "eris";
|
|
description = "Group under which Eris runs.";
|
|
};
|
|
|
|
stateDir = mkOption {
|
|
type = path;
|
|
default = "/var/lib/eris";
|
|
description = ''
|
|
Directory to store persistent state like blocked IPs. Subdirectories `conf` and `data` will be used.
|
|
'';
|
|
};
|
|
|
|
cacheDir = mkOption {
|
|
type = path;
|
|
default = "/var/cache/eris";
|
|
description = "Directory to store cache data like hit counters.";
|
|
};
|
|
|
|
dataDir = mkOption {
|
|
# This derives from stateDir by default to keep persistent data together
|
|
type = path;
|
|
default = "/var/lib/eris/data";
|
|
description = "Directory containing `corpora` and `scripts` subdirectories.";
|
|
};
|
|
|
|
corpora = mkOption {
|
|
type = attrsOf path;
|
|
default = {};
|
|
description = ''
|
|
An attribute set where keys are corpus filenames (e.g., "generic", "wordpress")
|
|
and values are paths to the corpus files. These will be placed in {path}`''${cfg.dataDir}/corpora`.
|
|
'';
|
|
example = literalExpression ''
|
|
{
|
|
generic = ./my-generic-corpus.txt;
|
|
wordpress = ./wp-corpus.txt;
|
|
}
|
|
'';
|
|
};
|
|
|
|
luaScripts = mkOption {
|
|
type = attrsOf path;
|
|
default = {};
|
|
description = ''
|
|
An attribute set where keys are Lua script filenames (e.g., "`my_script.lua`")
|
|
and values are paths to the script files. These will be placed in {path}`''${cfg.dataDir}/scripts`.
|
|
'';
|
|
example = literalExpression ''
|
|
{
|
|
"custom_tokens.lua" = ./custom_tokens.lua;
|
|
}
|
|
'';
|
|
};
|
|
|
|
# Firewall integration options
|
|
nftablesIntegration = mkOption {
|
|
type = bool;
|
|
default = true;
|
|
description = ''
|
|
Whether to enable nftables, and create the `eris_blacklist` and potentially a basic drop rule.
|
|
|
|
:::{.note}
|
|
This does *not* grant the Eris service permission to modify the set using the `nft` command.
|
|
Such permissions must be configured separately (e.g., via Polkit or sudo rules for the
|
|
service user).
|
|
|
|
Set this to false if you manage nftables entirely separately.
|
|
:::
|
|
'';
|
|
};
|
|
|
|
nftablesDropRule = mkOption {
|
|
type = bool;
|
|
default = true;
|
|
description = ''
|
|
If {option}`nftablesIntegration` is `true`, also add a rule to the 'input' chain to drop traffic
|
|
matching the 'eris_blacklist' set. Set to `false` if you want to manage the drop rule placement
|
|
yourself.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
networking.nftables = {
|
|
enable = mkIf cfg.nftablesIntegration cfg.nftablesIntegration;
|
|
ruleset = mkIf cfg.nftablesIntegration ''
|
|
table inet filter {
|
|
set eris_blacklist {
|
|
type ipv4_addr; flags interval; comment "Managed by Eris NixOS module";
|
|
}
|
|
|
|
chain INPUT {
|
|
${lib.optionalString cfg.nftablesDropRule ''
|
|
ip saddr @eris_blacklist counter drop comment "Drop traffic from Eris blacklist";
|
|
''}
|
|
}
|
|
}
|
|
'';
|
|
};
|
|
|
|
users.users = mkIf (cfg.user == "eris") {
|
|
eris = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
home = cfg.stateDir;
|
|
};
|
|
};
|
|
|
|
users.groups = mkIf (cfg.group == "eris") {
|
|
eris = {};
|
|
};
|
|
|
|
systemd.services.eris = {
|
|
description = "Eris Tarpit Service";
|
|
wantedBy = ["multi-user.target"];
|
|
after = ["network.target"] ++ (optionals cfg.nftablesIntegration ["nftables.service"]);
|
|
requires = optionals cfg.nftablesIntegration ["nftables.service"];
|
|
|
|
serviceConfig = {
|
|
# User and Group configuration
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
|
|
# Process management
|
|
ExecStart = ''
|
|
${lib.getExe cfg.package} \
|
|
--config-file ${erisConfigFile} \
|
|
--log-level ${cfg.logLevel}
|
|
'';
|
|
Restart = "on-failure";
|
|
RestartSec = "5s";
|
|
WorkingDirectory = cfg.stateDir; # state/data is accessed via config paths
|
|
|
|
# Resource limits
|
|
LimitNOFILE = 65536; # can handle many connections
|
|
LimitNPROC = 1024; # limit number of processes/threads
|
|
|
|
# Sandboxing and Security Hardening
|
|
# Deny privilege escalation
|
|
NoNewPrivileges = true;
|
|
|
|
# FIXME: this breaks everything.
|
|
# Filesystem access control
|
|
# ProtectSystem = "strict"; # Mount /usr, /boot, /etc read-only
|
|
# ProtectHome = true; # Make /home, /root inaccessible
|
|
#
|
|
# # Explicitly allow writes to state/cache/data dirs
|
|
# ReadWritePaths = [
|
|
# "/var/lib/eris"
|
|
# "${cfg.stateDir}"
|
|
# "${cfg.cacheDir}"
|
|
# "${cfg.dataDir}"
|
|
# ];
|
|
#
|
|
# # Allow reads from config file path
|
|
# ReadOnlyPaths = ["${erisConfigFile}"];
|
|
#
|
|
# # Explicitly deny access to sensitive paths
|
|
# InaccessiblePaths = [
|
|
# "/boot"
|
|
# "/root"
|
|
# "/home"
|
|
# "/srv"
|
|
# ];
|
|
|
|
PrivateTmp = true; # Use private /tmp and /var/tmp
|
|
PrivateDevices = true; # Restrict device access (/dev)
|
|
ProtectHostname = true; # Prevent changing hostname
|
|
ProtectClock = true; # Prevent changing system clock
|
|
ProtectKernelTunables = true; # Prevent changing kernel variables (/proc, /sys)
|
|
ProtectKernelModules = true; # Prevent loading/unloading kernel modules
|
|
ProtectKernelLogs = true; # Prevent reading kernel logs via dmesg
|
|
ProtectControlGroups = true; # Make Control Group hierarchies read-only
|
|
|
|
# Network access control
|
|
RestrictAddressFamilies = ["AF_INET" "AF_INET6"];
|
|
|
|
CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
|
|
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
|
|
|
# Start with a reasonable baseline for network services
|
|
SystemCallFilter = ["@system-service" "@network-io" "@file-system"];
|
|
|
|
# Other hardening
|
|
RestrictNamespaces = true; # Prevent creation of new namespaces
|
|
LockPersonality = true; # Lock down legacy personality settings
|
|
MemoryDenyWriteExecute = true; # Prevent RWX memory mappings
|
|
RemoveIPC = true; # Remove access to System V IPC objects
|
|
RestrictRealtime = true; # Prevent use of realtime scheduling policies
|
|
RestrictSUIDSGID = true; # Ignore SUID/SGID bits on execution
|
|
|
|
# Directories managed by systemd
|
|
StateDirectory = "eris";
|
|
CacheDirectory = "eris";
|
|
StateDirectoryMode = "0750";
|
|
CacheDirectoryMode = "0750";
|
|
|
|
# Logging
|
|
StandardOutput = "journal";
|
|
StandardError = "journal";
|
|
};
|
|
|
|
# Pre-start script to ensure directories and copy declarative files
|
|
preStart = let
|
|
corporaDir = "${cfg.dataDir}/corpora";
|
|
scriptsDir = "${cfg.dataDir}/scripts";
|
|
|
|
# Create commands to copy corpora files
|
|
copyCorporaCmds =
|
|
lib.mapAttrsToList (name: path: ''
|
|
cp -vf ${path} ${corporaDir}/${name}
|
|
'')
|
|
cfg.corpora;
|
|
|
|
# Create commands to copy Lua script files
|
|
copyLuaScriptCmds =
|
|
lib.mapAttrsToList (name: path: ''
|
|
cp -vf ${path} ${scriptsDir}/${name}
|
|
'')
|
|
cfg.luaScripts;
|
|
in ''
|
|
# Copy declarative files
|
|
${lib.optionalString (cfg.corpora != {}) (toString copyCorporaCmds)}
|
|
${lib.optionalString (cfg.luaScripts != {}) (toString copyLuaScriptCmds)}
|
|
'';
|
|
};
|
|
};
|
|
}
|