From b53d7a1401c0a9fc12b42836eaff7a83884d4c6a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:07:09 +0300 Subject: [PATCH 1/7] nix: get nftables from correct parent attr --- nix/module.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/module.nix b/nix/module.nix index 50f6fb0..f8a8984 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -216,7 +216,7 @@ in { }; config = mkIf cfg.enable { - services.nftables = { + networking.nftables = { enable = mkIf cfg.nftablesIntegration cfg.nftablesIntegration; ruleset = mkIf cfg.nftablesIntegration '' table inet filter { @@ -248,8 +248,8 @@ in { 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"; + after = ["network.target"] ++ (optionals cfg.nftablesIntegration ["nftables.service"]); + requires = optionals cfg.nftablesIntegration ["nftables.service"]; serviceConfig = { # User and Group configuration From d0edbdf5bbbbe65607a10a43f43ab2a14a4d4cbd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:14:45 +0300 Subject: [PATCH 2/7] nix: what do you mean `baseNameOf` is not in lib --- nix/module.nix | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/nix/module.nix b/nix/module.nix index f8a8984..45fb250 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -5,6 +5,7 @@ self: { pkgs, ... }: let + inherit (builtins) toJSON baseNameOf toString; inherit (lib.modules) mkIf; inherit (lib.options) mkOption mkEnableOption literalExpression; inherit (lib.types) package str port int listOf enum bool attrsOf path; @@ -13,7 +14,7 @@ self: { cfg = config.services.eris; # Generate the config.json content - erisConfigFile = pkgs.writeText "eris-config.json" (builtins.toJSON { + erisConfigFile = pkgs.writeText "eris-config.json" (toJSON { listen_addr = cfg.listenAddress; metrics_port = cfg.metricsPort; backend_addr = cfg.backendAddress; @@ -326,8 +327,8 @@ in { 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" + StateDirectory = baseNameOf cfg.stateDir; # e.g., "eris" + CacheDirectory = baseNameOf cfg.cacheDir; # e.g., "eris" StateDirectoryMode = "0750"; CacheDirectoryMode = "0750"; @@ -362,15 +363,15 @@ in { 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} \ + ${chownCmd} /var/lib/${baseNameOf cfg.stateDir} \ + /var/cache/${baseNameOf cfg.cacheDir} \ ${cfg.stateDir}/conf \ ${cfg.dataDir} \ ${corporaDir} \ ${scriptsDir} # Copy declarative files - ${lib.toString copyCorporaCmds} - ${lib.toString copyLuaScriptCmds} + ${toString copyCorporaCmds} + ${toString copyLuaScriptCmds} ''; }; }; From fb6a800e604b8073f8a20eed0905fc3bf69d2fff Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:19:02 +0300 Subject: [PATCH 3/7] nix: `pkgs.system` -> `pkgs.hostPlatform.system` --- nix/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/module.nix b/nix/module.nix index 45fb250..1428e0d 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -38,7 +38,7 @@ in { package = mkOption { type = package; - default = self.packages.${pkgs.system}.eris; + default = self.packages.${pkgs.stdenv.system}.eris; defaultText = literalExpression "pkgs.eris"; description = "The Eris package to use."; }; From 73f6f76135c55fb57232c68a37aefd2b79725f92 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:23:38 +0300 Subject: [PATCH 4/7] nix: include contrib in pacakge source --- nix/package.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index b63886d..b227e1a 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -18,6 +18,7 @@ in root = s; fileset = fs.unions [ (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) + (s + /contrib) lockfile cargoToml ]; @@ -25,8 +26,8 @@ in postInstall = '' mkdir -p $out/share/contrib - cp -r ${../contrib}/corpus $out/share/contrib/ - cp -r ${../contrib}/lua $out/share/contrib/ + cp -rv $src/contrib/corpus $out/share/contrib + cp -rv $src/contrib/lua $out/share/contrib ''; cargoLock.lockFile = lockfile; From f1b4764270411ee1924d43db391eee025d8fdbba Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:29:39 +0300 Subject: [PATCH 5/7] meta: add contrib --- contrib/corpus/php_exploit.txt | 29 ++++++ contrib/corpus/wordpress.txt | 22 +++++ contrib/lua/better_response.lua | 161 ++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 contrib/corpus/php_exploit.txt create mode 100644 contrib/corpus/wordpress.txt create mode 100644 contrib/lua/better_response.lua diff --git a/contrib/corpus/php_exploit.txt b/contrib/corpus/php_exploit.txt new file mode 100644 index 0000000..1e16e7d --- /dev/null +++ b/contrib/corpus/php_exploit.txt @@ -0,0 +1,29 @@ +PHP Fatal error: Uncaught Error: Call to undefined function mysql_connect() in /var/www/html/index.php:27 +Stack trace: +#0 /var/www/html/functions.php(15): connect_db() +#1 /var/www/html/admin/index.php(5): include('/var/www/html/f...') +#2 {main} +thrown in /var/www/html/index.php on line 27 + +PHP Warning: include(config.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 3 +PHP Warning: include(): Failed opening 'config.php' for inclusion (include_path='.:/usr/share/php') in /var/www/html/index.php on line 3 + +PHP Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed in /var/www/html/lib/Request.php on line 42 + +PHP Fatal error: Uncaught PDOException: SQLSTATE[HY000] [2002] Connection refused in /var/www/html/database.php:14 +Stack trace: +#0 /var/www/html/database.php(14): PDO->__construct('mysql:host=127....', 'dbuser', '********') +#1 /var/www/html/index.php(7): require_once('/var/www/html/d...') +#2 {main} +thrown in /var/www/html/database.php on line 14 + +PHP Warning: require_once(vendor/autoload.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 3 + +PHP Notice: Undefined index: username in /var/www/html/admin/login.php on line 15 +PHP Notice: Undefined index: password in /var/www/html/admin/login.php on line 16 + +PHP Warning: session_start(): Cannot start session when headers already sent in /var/www/html/lib/Session.php on line 5 + +PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 16384 bytes) in /var/www/vendor/doctrine/orm/lib/Doctrine/ORM/QueryBuilder.php on line 312 + +PHP Fatal error: Uncaught Error: Class 'PDO' not found in /var/www/html/includes/database.php:10 diff --git a/contrib/corpus/wordpress.txt b/contrib/corpus/wordpress.txt new file mode 100644 index 0000000..5765213 --- /dev/null +++ b/contrib/corpus/wordpress.txt @@ -0,0 +1,22 @@ +WordPress database error: [Table 'wp_options' doesn't exist] +SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes' + +Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/wp-content/themes/twentytwentyone/functions.php:258) in /var/www/html/wp-includes/functions.php on line 6198 + +Fatal error: Allowed memory size of 41943040 bytes exhausted (tried to allocate 32768 bytes) in /var/www/html/wp-includes/class-wp-query.php on line 3324 + +WordPress database error Table 'wp_users' doesn't exist for query SELECT * FROM wp_users WHERE user_login = 'admin' made by include('wp-blog-header.php'), require_once('wp-load.php'), require_once('wp-config.php') + +Notice: WP_Scripts::localize was called incorrectly. The $l10n parameter must be an array. To pass arbitrary data to scripts, use the wp_add_inline_script() function instead. Please see Debugging in WordPress for more information. + +WordPress database error: [Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'wp_posts.ID' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by] +SELECT ID FROM wp_posts WHERE post_type = 'post' GROUP BY YEAR(post_date), MONTH(post_date) ORDER BY post_date DESC + +WordPress database error: [Disk full (/var/tmp/#sql_b72_0.MAI); waiting for someone to free some space... (errno: 28 "No space left on device")] +SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'category' ORDER BY t.name ASC + +Warning: session_start() [function.session-start]: open(/var/lib/php/sessions/sess_05kqdnq9gfhj7v3c3q93fedlv2, O_RDWR) failed: Permission denied (13) in /var/www/html/wp-content/plugins/my-calendar/my-calendar.php on line 95 + +Notice: Undefined index: HTTP_REFERER in /var/www/html/wp-content/themes/twentytwentytwo/template-parts/header.php on line 73 + +Warning: Declaration of Walker_Nav_Menu_Dropdown::start_lvl(&$output, $depth) should be compatible with Walker_Nav_Menu::start_lvl(&$output, $depth = 0, $args = Array) in /var/www/html/wp-content/themes/responsive/core/includes/functions-extras.php on line 57 diff --git a/contrib/lua/better_response.lua b/contrib/lua/better_response.lua new file mode 100644 index 0000000..91ce98f --- /dev/null +++ b/contrib/lua/better_response.lua @@ -0,0 +1,161 @@ +-- better_response.lua +-- +-- Adds realistic-looking, but fake content to responses based on the type of the request being +-- made by the bots. This is a demo implementation to demonstrate how scripting works for Eris. + +-- Random honeytoken generation +function generate_honeytoken(token) + local token_types = { + "API_KEY", "AUTH_TOKEN", "SESSION_ID", "SECRET_KEY", + "DB_PASSWORD", "ADMIN_TOKEN", "SSH_KEY" + } + + local prefix = token_types[math.random(#token_types)] + local suffix = string.format("%08x%08x", math.random(0xffffffff), math.random(0xffffffff)) + + return prefix .. "_" .. token .. "_" .. suffix +end + +-- Generates a "believable" (but fake) error stack trace +function generate_stack_trace() + local files = { + "/var/www/html/index.php", + "/var/www/html/wp-content/plugins/contact-form-7/includes/submission.php", + "/var/www/vendor/symfony/http-kernel/HttpKernel.php", + "/var/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", + "/var/www/html/app/Controllers/AdminController.php" + } + + local functions = { + "execute", "handle", "processRequest", "loadModel", + "authenticate", "validateInput", "renderTemplate" + } + + local trace = {} + local depth = math.random(3, 8) + + for i=1,depth do + local file = files[math.random(#files)] + local func = functions[math.random(#functions)] + local line = math.random(10, 400) + + table.insert(trace, string.format("#%d %s(%d): %s()", i, file, line, func)) + end + + return table.concat(trace, "\n") +end + +-- Table of response enhancements by type +local enhancers = {} + +enhancers.php_exploit = function(text, path, token) + local honeytoken = generate_honeytoken(token) + local stack = generate_stack_trace() + + -- HTML comments that look like sensitive data + local enhancements = { + string.format("", honeytoken), + string.format("", honeytoken), + string.format("", stack), + string.format("", + string.sub(token, 1, 8), string.sub(token, -6)), + "" + } + + -- Insert appealing content at random positions in the text + local result = text + for _, enhancement in ipairs(enhancements) do + local pos = math.random(1, #result) + result = string.sub(result, 1, pos) .. enhancement .. string.sub(result, pos+1) + } + + return result +end + +enhancers.wordpress = function(text, path, token) + local honeytoken = generate_honeytoken(token) + + local enhancements = { + string.format("", token), + string.format("", honeytoken), + string.format("", honeytoken), + "", + string.format("", string.sub(token, 1, 8)) + } + + local result = text + for _, enhancement in ipairs(enhancements) do + local pos = math.random(1, #result) + result = string.sub(result, 1, pos) .. enhancement .. string.sub(result, pos+1) + } + + return result +end + +enhancers.api = function(text, path, token) + -- Create fake API responses that look like they contain tokens or keys + local api_response = { + status = "error", + error = "Authentication required", + request_id = string.format("req_%s", token), + debug_token = generate_honeytoken(token), + _links = { + documentation = "https://api.example.com/docs", + support = "https://example.com/api-support" + } + } + + -- Convert to JSON-like string + local function to_json(obj, indent) + indent = indent or "" + local json_str = "{\n" + + for k, v in pairs(obj) do + json_str = json_str .. indent .. " \"" .. k .. "\": " + + if type(v) == "table" then + json_str = json_str .. to_json(v, indent .. " ") + elseif type(v) == "string" then + json_str = json_str .. "\"" .. v .. "\"" + elseif type(v) == "number" then + json_str = json_str .. tostring(v) + elseif type(v) == "boolean" then + json_str = json_str .. tostring(v) + else + json_str = json_str .. "null" + end + + json_str = json_str .. ",\n" + end + + -- Remove trailing comma + json_str = string.sub(json_str, 1, -3) .. "\n" .. indent .. "}" + return json_str + end + + return text .. "\n
" .. to_json(api_response) .. "
" +end + +enhancers.generic = function(text, path, token) + -- General honeypot enhancements for any path + local server_info = { + "Server running Apache/2.4.41", + "PHP version: 7.4.3", + string.format("Generated for client: %s", token), + string.format("Server ID: srv-%s", string.sub(token, 1, 12)), + string.format("Request processed by worker-%s", string.sub(token, -8)) + } + + local result = text + result = result .. "\n" + result = result .. string.format("\n", token) + + return result +end + +-- Main entry point function that Rust will call +function enhance_response(text, response_type, path, token) + -- Default to generic if type not found + local enhancer = enhancers[response_type] or enhancers.generic + return enhancer(text, path, token) +end From bd10705a29646276806708d96a8071bc5ce1893e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:31:30 +0300 Subject: [PATCH 6/7] docs: add project readme --- README.md | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8209063 --- /dev/null +++ b/README.md @@ -0,0 +1,300 @@ +# Eris + +This is a sophisticated HTTP tarpit and honeypot stream designed to protect, +delay, block and dare I say _troll_ malicious scanners and vulnerability probes +while protecting **legitimate** web traffic. + +## How Does It Work? + +Eris works by sitting in front of your web server, intercepting all incoming +HTTP requests and: + +- **Allowing** legitimate requests to pass through to your actual web server +- **Trapping and delaying** malicious requests using a tarpit technique +- **Blocking** repeat offenders at the firewall level (goodbye fail2ban) + +### Process Flow + +1. Eris receives all incoming HTTP requests +2. It analyzes each request against its list of trap patterns +3. Legitimate requests are proxied directly to your backend server +4. Suspicious requests are tarpitted: + - Response is sent painfully slowly, byte-by-byte with random delays + - Deceptive content is generated to waste attacker time + - Connections are held open for extended periods to consume attacker + resources +5. Repeat offenders are automatically blocked at the firewall level + +## Why Use Eris? + +Traditional web application firewalls simply block malicious requests, which +immediately alerts attackers they've been detected. Eris takes a deceptive +approach: + +- Wastes attacker time and resources on fake responses +- Collects data about attack patterns and techniques +- Reduces server load by handling malicious traffic efficiently +- Provides valuable metrics and insights about attack traffic + +## Key Features + +The design I had in mind was minimal, but more features crept in while I was +fixing things on to go. More features to come as I see fit. + +- **Low-overhead tarpit**: Delays responses to suspicious requests byte-by-byte + with random delays +- **Markov chain response generation**: Creates realistic-looking fake responses + to deceive attackers (success my vary depending on how sophisticated the + attackers are) +- **Firewall integration**: Automatically blocks repeat offenders using nftables + (iptables is legacy, and not supported) +- **Customizable response scripts**: Using Lua scripting to generate deceptive + responses +- **Prometheus metrics**: Tracks and exposes honeypot activity + +## Installation + +### From Source + +```bash +# Clone the repository +git clone https://github.com/notashelf/eris +cd eris + +# Build the binary +cargo build --release + +# Create necessary directories +mkdir -p ~/.local/share/eris/data +mkdir -p ~/.local/share/eris/corpora +mkdir -p ~/.local/share/eris/scripts +``` + +### Pre-built Binaries + +Pre-built binaries are not yet available. + +## Deployment Architecture + +### Static Sites + +For static sites served by Nginx, the proper setup is to place Eris in front of +Nginx. Here is a graph of how it's meant to be configured: + +``` +Internet → [Eris (port 80)] → [Nginx (local port)] +``` + +You will want to configure Eris to listen on port 80 (or 443 for SSL) and +forward legitimate traffic to Nginx running on a local port: + +```bash +# Run Eris on port 80, forwarding legitimate requests to Nginx on port 8080 +eris --listen-addr 0.0.0.0:80 --backend-addr 127.0.0.1:8080 +``` + +Then, configure Nginx to serve your static site on the local port: + +```nginx +server { + # XXX: Nginx listens on local port only and it is + # not exposed to internet + listen 127.0.0.1:8080; + server_name example.com; + + root /var/www/example.com; # your site's source + index index.html; + + location / { + try_files $uri $uri/ =404; + } +} +``` + +With the setup above, several things will happen (in this order): + +1. All traffic hits Eris first (on port 80) +2. Eris inspects each request against its trap patterns +3. Malicious requests get tarpitted by Eris +4. Legitimate requests get proxied to Nginx on port 8080 +5. Nginx serves your static content as usual + +## HTTPS Support + +For HTTPS, you have two options: + +### Option 1: Terminate SSL at Eris + +```bash +# SSL termination at Eris (requires cert files) +eris --listen-addr 0.0.0.0:443 --backend-addr 127.0.0.1:8080 --ssl-cert /path/to/cert.pem --ssl-key /path/to/key.pem +``` + +### Option 2: Use a separate SSL terminator + +``` +Internet → [SSL Terminator (port 443)] → [Eris (local port)] → [Nginx (local port)] +``` + +You can use Nginx, HAProxy, or Caddy as the SSL terminator, forwarding decrypted +traffic to Eris. + +## Configuration + +### Command Line Options + +``` +USAGE: + eris [OPTIONS] + +OPTIONS: + --listen-addr Address to listen on [default: 0.0.0.0:8888] + --metrics-port Port for Prometheus metrics [default: 9100] + --backend-addr Address of backend server [default: 127.0.0.1:80] + --min-delay Minimum delay between tarpit bytes in ms [default: 1000] + --max-delay Maximum delay between tarpit bytes in ms [default: 15000] + --max-tarpit-time Maximum time to hold a tarpit connection in seconds [default: 600] + --block-threshold Block IPs after this many hits [default: 3] + --base-dir Base directory for all files (optional) + --config-file JSON configuration file (optional) + --log-level Log level (debug, info, warn, error) [default: info] +``` + +### JSON Configuration File + +For more complex configurations, use a JSON configuration file: + +```json +{ + "listen_addr": "0.0.0.0:8888", + "metrics_port": 9100, + "backend_addr": "127.0.0.1:80", + "min_delay": 1000, + "max_delay": 15000, + "max_tarpit_time": 600, + "block_threshold": 3, + "trap_patterns": [ + "/vendor/phpunit", + "eval-stdin.php", + "/wp-admin", + "/wp-login.php", + "/xmlrpc.php", + "/phpMyAdmin", + "/solr/", + "/.env", + "/config", + "/api/", + "/actuator/" + ], + "whitelist_networks": [ + "192.168.0.0/16", + "10.0.0.0/8", + "172.16.0.0/12", + "127.0.0.0/8" + ], + "markov_corpora_dir": "./corpora", + "lua_scripts_dir": "./scripts", + "data_dir": "./data" +} +``` + +## Extending and Customizing + +### Adding Trap Patterns + +Edit your configuration file to add additional trap patterns: + +```json +"trap_patterns": [ + "/vendor/phpunit", + "eval-stdin.php", + "/wp-admin", + "/wp-login.php", + "/xmlrpc.php", + "/phpMyAdmin", + "/solr/", + "/.env", + "/config", + "/api/", + "/actuator/", + "/wp-content/debug.log", + "/cgi-bin/", + "/admin/" +] +``` + +### Custom Response Generation with Lua + +> [!WARNING] +> The Lua API is experimental, and might be subject to change. Breaking changes +> will be + +Create Lua scripts in your `lua_scripts_dir` to customize response generation: + +```lua +-- honeypot.lua +function generate_honeytoken(token) + local token_types = {"API_KEY", "AUTH_TOKEN", "SESSION_ID", "SECRET_KEY"} + local prefix = token_types[math.random(#token_types)] + local suffix = string.format("%08x", math.random(0xffffff)) + return prefix .. "_" .. token .. "_" .. suffix +end + +function enhance_response(text, response_type, path, token) + local result = text + local honeytoken = generate_honeytoken(token) + + if response_type == "php_exploit" then + result = result .. "\n" + result = result .. "\n" + elseif response_type == "wordpress" then + result = result .. "\n" + elseif response_type == "api" then + result = '{"error":"Unauthorized","message":"Invalid credentials",' + result = result .. '"debug_token":"' .. honeytoken .. '"}' + else + result = result .. "\n" + result = result .. "\n" + end + + return result +end +``` + +### Custom Markov Response Corpus + +Create text files in your `markov_corpora_dir` to provide custom text for +response generation: + +- `generic.txt` - Default responses +- `php_exploit.txt` - PHP-specific error messages +- `wordpress.txt` - WordPress-style responses +- `api.txt` - API error messages + +## Metrics and Monitoring + +Eris exposes Prometheus metrics on the configured metrics port (default: 9100): + +- `eris_hits_total` - Total number of hits to honeypot paths +- `eris_blocked_ips` - Number of IPs permanently blocked +- `eris_active_connections` - Number of currently active connections in tarpit +- `eris_path_hits_total` - Hits by path +- `eris_ua_hits_total` - Hits by user agent + +View current status at: `http://your-server:9100/status` + +## Additional Security Considerations + +Internally, Eris is designed with security and maximum performance in mind. +Additionally you might want to consider: + +- Running Eris as a non-privileged user +- Placing Eris behind a CDN or DDoS protection service for high-traffic sites +- Regularly reviewing logs to identify new attack patterns (and keeping verbose + NGINX logs) +- Hiding the metrics endpoint from public access + +## License + +This project is licensed under the MIT License - see the LICENSE file for +details. From d89fe8ddd501da03b3f43406fa0be933259e6d35 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 1 May 2025 06:55:11 +0300 Subject: [PATCH 7/7] nix: fix types --- nix/module.nix | 56 ++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/nix/module.nix b/nix/module.nix index 1428e0d..49e2c2d 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -5,7 +5,7 @@ self: { pkgs, ... }: let - inherit (builtins) toJSON baseNameOf toString; + 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; @@ -30,6 +30,13 @@ self: { config_dir = "${cfg.stateDir}/conf"; cache_dir = cfg.cacheDir; }); + + # Check if we need privileged port capability + portMatch = builtins.match ".*:([0-9]+)" cfg.listenAddress; + needsPrivilegedPort = + portMatch + != null + && builtins.fromJSON (builtins.head portMatch) < 1024; in { ###### interface options = { @@ -39,7 +46,7 @@ in { package = mkOption { type = package; default = self.packages.${pkgs.stdenv.system}.eris; - defaultText = literalExpression "pkgs.eris"; + defaultText = literalExpression "self.packages.\${pkgs.stdenv.system}.eris"; description = "The Eris package to use."; }; @@ -154,7 +161,7 @@ in { dataDir = mkOption { # This derives from stateDir by default to keep persistent data together type = path; - default = "${cfg.stateDir}/data"; + default = "/var/lib/eris/data"; description = "Directory containing `corpora` and `scripts` subdirectories."; }; @@ -180,7 +187,7 @@ in { 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 '' + example = literalExpression '' { "custom_tokens.lua" = ./custom_tokens.lua; } @@ -225,7 +232,7 @@ in { type ipv4_addr; flags interval; comment "Managed by Eris NixOS module"; } - chain input { + chain INPUT { ${lib.optionalString cfg.nftablesDropRule '' ip saddr @eris_blacklist counter drop comment "Drop traffic from Eris blacklist"; ''} @@ -281,20 +288,20 @@ in { # Explicitly allow writes to state/cache/data dirs ReadWritePaths = [ - cfg.stateDir - cfg.cacheDir - cfg.dataDir + "${cfg.stateDir}" + "${cfg.cacheDir}" + "${cfg.dataDir}" ]; # Allow reads from config file path - ReadOnlyPaths = [erisConfigFile]; + ReadOnlyPaths = ["${erisConfigFile}"]; # Explicitly deny access to sensitive paths InaccessiblePaths = [ "/boot" "/root" "/home" - "/srv" # Add others as needed + "/srv" ]; PrivateTmp = true; # Use private /tmp and /var/tmp @@ -307,17 +314,14 @@ in { 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 + RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; + + CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"]; 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 @@ -327,8 +331,8 @@ in { RestrictSUIDSGID = true; # Ignore SUID/SGID bits on execution # Directories managed by systemd - StateDirectory = baseNameOf cfg.stateDir; # e.g., "eris" - CacheDirectory = baseNameOf cfg.cacheDir; # e.g., "eris" + StateDirectory = "eris"; + CacheDirectory = "eris"; StateDirectoryMode = "0750"; CacheDirectoryMode = "0750"; @@ -341,6 +345,7 @@ in { preStart = let corporaDir = "${cfg.dataDir}/corpora"; scriptsDir = "${cfg.dataDir}/scripts"; + confDir = "${cfg.stateDir}/conf"; chownCmd = "${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group}"; # Create commands to copy corpora files @@ -359,19 +364,12 @@ in { '') cfg.luaScripts; in '' - # Systemd creates StateDirectory and CacheDirectory, but we need subdirs - mkdir -p ${cfg.stateDir}/conf ${cfg.dataDir} ${corporaDir} ${scriptsDir} + # Create subdirectories only - base directories are created by systemd + mkdir -p ${confDir} ${corporaDir} ${scriptsDir} - # Ensure ownership is correct for all relevant dirs managed by systemd or created here - ${chownCmd} /var/lib/${baseNameOf cfg.stateDir} \ - /var/cache/${baseNameOf cfg.cacheDir} \ - ${cfg.stateDir}/conf \ - ${cfg.dataDir} \ - ${corporaDir} \ - ${scriptsDir} # Copy declarative files - ${toString copyCorporaCmds} - ${toString copyLuaScriptCmds} + ${lib.optionalString (cfg.corpora != {}) (toString copyCorporaCmds)} + ${lib.optionalString (cfg.luaScripts != {}) (toString copyLuaScriptCmds)} ''; }; };