nix: initial nixos module implementation
This commit is contained in:
parent
7b6c010ef5
commit
8a736599fd
1 changed files with 377 additions and 0 deletions
377
nix/module.nix
Normal file
377
nix/module.nix
Normal file
|
@ -0,0 +1,377 @@
|
|||
# NixOS Module for Eris Tarpit
|
||||
self: {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
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" (builtins.toJSON {
|
||||
listen_addr = cfg.listenAddress;
|
||||
metrics_port = cfg.metricsPort;
|
||||
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.system}.eris;
|
||||
defaultText = literalExpression "pkgs.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";
|
||||
};
|
||||
|
||||
metricsPort = mkOption {
|
||||
type = port;
|
||||
default = 9100;
|
||||
description = "The 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 milliseconds) between sending characters in tarpit mode.";
|
||||
};
|
||||
|
||||
maxDelay = mkOption {
|
||||
type = int;
|
||||
default = 15000;
|
||||
description = "Maximum delay (in milliseconds) 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 = "${cfg.stateDir}/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 = lib.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 {
|
||||
services.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 = ''
|
||||
${cfg.package}/bin/eris \
|
||||
--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;
|
||||
|
||||
# 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 = [
|
||||
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" # Add others as needed
|
||||
];
|
||||
|
||||
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"]; # Allow only standard IP protocols
|
||||
CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"]; # Allow binding to ports < 1024 if needed
|
||||
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
|
||||
|
||||
# System call filtering (adjust based on Eris's needs)
|
||||
# Start with a reasonable baseline for network services
|
||||
SystemCallFilter = ["@system-service" "@network-io" "@file-system"];
|
||||
|
||||
# TODO: Consider adding more specific filters or removing groups if issues arise
|
||||
# e.g., SystemCallArchitectures=native. This probably will not be enough
|
||||
|
||||
# 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 = lib.baseNameOf cfg.stateDir; # e.g., "eris"
|
||||
CacheDirectory = lib.baseNameOf cfg.cacheDir; # e.g., "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";
|
||||
chownCmd = "${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group}";
|
||||
|
||||
# Create commands to copy corpora files
|
||||
copyCorporaCmds =
|
||||
lib.mapAttrsToList (name: path: ''
|
||||
cp -vf ${path} ${corporaDir}/${name}
|
||||
${chownCmd} ${corporaDir}/${name}
|
||||
'')
|
||||
cfg.corpora;
|
||||
|
||||
# Create commands to copy Lua script files
|
||||
copyLuaScriptCmds =
|
||||
lib.mapAttrsToList (name: path: ''
|
||||
cp -vf ${path} ${scriptsDir}/${name}
|
||||
${chownCmd} ${scriptsDir}/${name}
|
||||
'')
|
||||
cfg.luaScripts;
|
||||
in ''
|
||||
# Systemd creates StateDirectory and CacheDirectory, but we need subdirs
|
||||
mkdir -p ${cfg.stateDir}/conf ${cfg.dataDir} ${corporaDir} ${scriptsDir}
|
||||
|
||||
# Ensure ownership is correct for all relevant dirs managed by systemd or created here
|
||||
${chownCmd} /var/lib/${lib.baseNameOf cfg.stateDir} \
|
||||
/var/cache/${lib.baseNameOf cfg.cacheDir} \
|
||||
${cfg.stateDir}/conf \
|
||||
${cfg.dataDir} \
|
||||
${corporaDir} \
|
||||
${scriptsDir}
|
||||
# Copy declarative files
|
||||
${lib.toString copyCorporaCmds}
|
||||
${lib.toString copyLuaScriptCmds}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue