Compare commits

...

3 commits

Author SHA1 Message Date
18be5ba041
docs: add project readme 2025-05-01 18:00:19 +03:00
b2cee02dd4
meta: add contrib 2025-05-01 18:00:18 +03:00
bc21bfc506
nix: get nftables from correct parent attr
nix: what do you mean `baseNameOf` is not in lib

nix: `pkgs.system` -> `pkgs.hostPlatform.system`

nix: include contrib in pacakge source

nix: fix types
2025-05-01 17:56:08 +03:00
6 changed files with 558 additions and 57 deletions

300
README.md Normal file
View file

@ -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 <ADDR> Address to listen on [default: 0.0.0.0:8888]
--metrics-port <PORT> Port for Prometheus metrics [default: 9100]
--backend-addr <ADDR> Address of backend server [default: 127.0.0.1:80]
--min-delay <MS> Minimum delay between tarpit bytes in ms [default: 1000]
--max-delay <MS> Maximum delay between tarpit bytes in ms [default: 15000]
--max-tarpit-time <SEC> Maximum time to hold a tarpit connection in seconds [default: 600]
--block-threshold <COUNT> Block IPs after this many hits [default: 3]
--base-dir <PATH> Base directory for all files (optional)
--config-file <FILE> JSON configuration file (optional)
--log-level <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<?php /* Debug mode enabled */ ?>"
result = result .. "\n<!-- DEBUG TOKEN: " .. honeytoken .. " -->"
elseif response_type == "wordpress" then
result = result .. "\n<!-- WordPress debug: DB_PASSWORD='" .. honeytoken .. "' -->"
elseif response_type == "api" then
result = '{"error":"Unauthorized","message":"Invalid credentials",'
result = result .. '"debug_token":"' .. honeytoken .. '"}'
else
result = result .. "\n<!-- Server: Apache/2.4.41 PHP/7.4.3 -->"
result = result .. "\n<!-- Debug: " .. honeytoken .. " -->"
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.

View file

@ -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

View file

@ -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

View file

@ -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("<!-- DEBUG MODE ENABLED - %s -->", honeytoken),
string.format("<input type='hidden' name='csrf_token' value='%s'>", honeytoken),
string.format("<div class='debug-output' style='display:none'>%s</div>", stack),
string.format("<!-- DB_HOST=localhost DB_USER=webapp_%s DB_PASS=Secret_%s! -->",
string.sub(token, 1, 8), string.sub(token, -6)),
"<script>console.log('Failed to load security module. Contact administrator.')</script>"
}
-- 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("<meta name='generator' content='WordPress 5.9.6; host=%s'>", token),
string.format("<!-- WP DEBUG: [Error] Failed to verify nonce '%s' -->", honeytoken),
string.format("<script>var ajaxurl = 'https://example.com/wp-admin/admin-ajax.php'; var wpApiSettings = {nonce: '%s'}</script>", honeytoken),
"<!-- Debugging: Failed to load plugin 'security-master' -->",
string.format("<!-- wpdb error: Table 'wp_%s.wp_users' doesn't exist -->", 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<pre class='api-response'>" .. to_json(api_response) .. "</pre>"
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<!-- " .. table.concat(server_info, ", ") .. " -->"
result = result .. string.format("\n<script>console.log('Session tracking enabled: %s')</script>", 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

View file

@ -5,6 +5,7 @@ self: {
pkgs, pkgs,
... ...
}: let }: let
inherit (builtins) toJSON toString;
inherit (lib.modules) mkIf; inherit (lib.modules) mkIf;
inherit (lib.options) mkOption mkEnableOption literalExpression; inherit (lib.options) mkOption mkEnableOption literalExpression;
inherit (lib.types) package str port int listOf enum bool attrsOf path; inherit (lib.types) package str port int listOf enum bool attrsOf path;
@ -13,7 +14,7 @@ self: {
cfg = config.services.eris; cfg = config.services.eris;
# Generate the config.json content # Generate the config.json content
erisConfigFile = pkgs.writeText "eris-config.json" (builtins.toJSON { erisConfigFile = pkgs.writeText "eris-config.json" (toJSON {
listen_addr = cfg.listenAddress; listen_addr = cfg.listenAddress;
metrics_port = cfg.metricsPort; metrics_port = cfg.metricsPort;
backend_addr = cfg.backendAddress; backend_addr = cfg.backendAddress;
@ -37,8 +38,8 @@ in {
package = mkOption { package = mkOption {
type = package; type = package;
default = self.packages.${pkgs.system}.eris; 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."; description = "The Eris package to use.";
}; };
@ -52,6 +53,7 @@ in {
metricsPort = mkOption { metricsPort = mkOption {
type = port; type = port;
default = 9100; default = 9100;
example = 9110;
description = "The port for the Prometheus metrics endpoint."; description = "The port for the Prometheus metrics endpoint.";
}; };
@ -65,13 +67,13 @@ in {
minDelay = mkOption { minDelay = mkOption {
type = int; type = int;
default = 1000; default = 1000;
description = "Minimum delay (in milliseconds) between sending characters in tarpit mode."; description = "Minimum delay (in ms) between sending characters in tarpit mode.";
}; };
maxDelay = mkOption { maxDelay = mkOption {
type = int; type = int;
default = 15000; default = 15000;
description = "Maximum delay (in milliseconds) between sending characters in tarpit mode."; description = "Maximum delay (in ms) between sending characters in tarpit mode.";
}; };
maxTarpitTime = mkOption { maxTarpitTime = mkOption {
@ -153,7 +155,7 @@ in {
dataDir = mkOption { dataDir = mkOption {
# This derives from stateDir by default to keep persistent data together # This derives from stateDir by default to keep persistent data together
type = path; type = path;
default = "${cfg.stateDir}/data"; default = "/var/lib/eris/data";
description = "Directory containing `corpora` and `scripts` subdirectories."; description = "Directory containing `corpora` and `scripts` subdirectories.";
}; };
@ -179,7 +181,7 @@ in {
An attribute set where keys are Lua script filenames (e.g., "`my_script.lua`") 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`. 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; "custom_tokens.lua" = ./custom_tokens.lua;
} }
@ -216,7 +218,7 @@ in {
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
services.nftables = { networking.nftables = {
enable = mkIf cfg.nftablesIntegration cfg.nftablesIntegration; enable = mkIf cfg.nftablesIntegration cfg.nftablesIntegration;
ruleset = mkIf cfg.nftablesIntegration '' ruleset = mkIf cfg.nftablesIntegration ''
table inet filter { table inet filter {
@ -224,7 +226,7 @@ in {
type ipv4_addr; flags interval; comment "Managed by Eris NixOS module"; type ipv4_addr; flags interval; comment "Managed by Eris NixOS module";
} }
chain input { chain INPUT {
${lib.optionalString cfg.nftablesDropRule '' ${lib.optionalString cfg.nftablesDropRule ''
ip saddr @eris_blacklist counter drop comment "Drop traffic from Eris blacklist"; ip saddr @eris_blacklist counter drop comment "Drop traffic from Eris blacklist";
''} ''}
@ -248,8 +250,8 @@ in {
systemd.services.eris = { systemd.services.eris = {
description = "Eris Tarpit Service"; description = "Eris Tarpit Service";
wantedBy = ["multi-user.target"]; wantedBy = ["multi-user.target"];
after = ["network.target"] ++ optionals cfg.nftablesIntegration "nftables.service"; after = ["network.target"] ++ (optionals cfg.nftablesIntegration ["nftables.service"]);
requires = optionals cfg.nftablesIntegration "nftables.service"; requires = optionals cfg.nftablesIntegration ["nftables.service"];
serviceConfig = { serviceConfig = {
# User and Group configuration # User and Group configuration
@ -258,7 +260,7 @@ in {
# Process management # Process management
ExecStart = '' ExecStart = ''
${cfg.package}/bin/eris \ ${lib.getExe cfg.package} \
--config-file ${erisConfigFile} \ --config-file ${erisConfigFile} \
--log-level ${cfg.logLevel} --log-level ${cfg.logLevel}
''; '';
@ -274,27 +276,29 @@ in {
# Deny privilege escalation # Deny privilege escalation
NoNewPrivileges = true; NoNewPrivileges = true;
# FIXME: this breaks everything.
# Filesystem access control # Filesystem access control
ProtectSystem = "strict"; # Mount /usr, /boot, /etc read-only # ProtectSystem = "strict"; # Mount /usr, /boot, /etc read-only
ProtectHome = true; # Make /home, /root inaccessible # ProtectHome = true; # Make /home, /root inaccessible
#
# Explicitly allow writes to state/cache/data dirs # # Explicitly allow writes to state/cache/data dirs
ReadWritePaths = [ # ReadWritePaths = [
cfg.stateDir # "/var/lib/eris"
cfg.cacheDir # "${cfg.stateDir}"
cfg.dataDir # "${cfg.cacheDir}"
]; # "${cfg.dataDir}"
# ];
# Allow reads from config file path #
ReadOnlyPaths = [erisConfigFile]; # # Allow reads from config file path
# ReadOnlyPaths = ["${erisConfigFile}"];
# Explicitly deny access to sensitive paths #
InaccessiblePaths = [ # # Explicitly deny access to sensitive paths
"/boot" # InaccessiblePaths = [
"/root" # "/boot"
"/home" # "/root"
"/srv" # Add others as needed # "/home"
]; # "/srv"
# ];
PrivateTmp = true; # Use private /tmp and /var/tmp PrivateTmp = true; # Use private /tmp and /var/tmp
PrivateDevices = true; # Restrict device access (/dev) PrivateDevices = true; # Restrict device access (/dev)
@ -306,17 +310,14 @@ in {
ProtectControlGroups = true; # Make Control Group hierarchies read-only ProtectControlGroups = true; # Make Control Group hierarchies read-only
# Network access control # Network access control
RestrictAddressFamilies = ["AF_INET" "AF_INET6"]; # Allow only standard IP protocols RestrictAddressFamilies = ["AF_INET" "AF_INET6"];
CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"]; # Allow binding to ports < 1024 if needed
CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
AmbientCapabilities = ["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 # Start with a reasonable baseline for network services
SystemCallFilter = ["@system-service" "@network-io" "@file-system"]; 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 # Other hardening
RestrictNamespaces = true; # Prevent creation of new namespaces RestrictNamespaces = true; # Prevent creation of new namespaces
LockPersonality = true; # Lock down legacy personality settings LockPersonality = true; # Lock down legacy personality settings
@ -326,8 +327,8 @@ in {
RestrictSUIDSGID = true; # Ignore SUID/SGID bits on execution RestrictSUIDSGID = true; # Ignore SUID/SGID bits on execution
# Directories managed by systemd # Directories managed by systemd
StateDirectory = lib.baseNameOf cfg.stateDir; # e.g., "eris" StateDirectory = "eris";
CacheDirectory = lib.baseNameOf cfg.cacheDir; # e.g., "eris" CacheDirectory = "eris";
StateDirectoryMode = "0750"; StateDirectoryMode = "0750";
CacheDirectoryMode = "0750"; CacheDirectoryMode = "0750";
@ -340,13 +341,11 @@ in {
preStart = let preStart = let
corporaDir = "${cfg.dataDir}/corpora"; corporaDir = "${cfg.dataDir}/corpora";
scriptsDir = "${cfg.dataDir}/scripts"; scriptsDir = "${cfg.dataDir}/scripts";
chownCmd = "${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group}";
# Create commands to copy corpora files # Create commands to copy corpora files
copyCorporaCmds = copyCorporaCmds =
lib.mapAttrsToList (name: path: '' lib.mapAttrsToList (name: path: ''
cp -vf ${path} ${corporaDir}/${name} cp -vf ${path} ${corporaDir}/${name}
${chownCmd} ${corporaDir}/${name}
'') '')
cfg.corpora; cfg.corpora;
@ -354,23 +353,12 @@ in {
copyLuaScriptCmds = copyLuaScriptCmds =
lib.mapAttrsToList (name: path: '' lib.mapAttrsToList (name: path: ''
cp -vf ${path} ${scriptsDir}/${name} cp -vf ${path} ${scriptsDir}/${name}
${chownCmd} ${scriptsDir}/${name}
'') '')
cfg.luaScripts; cfg.luaScripts;
in '' 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 # Copy declarative files
${lib.toString copyCorporaCmds} ${lib.optionalString (cfg.corpora != {}) (toString copyCorporaCmds)}
${lib.toString copyLuaScriptCmds} ${lib.optionalString (cfg.luaScripts != {}) (toString copyLuaScriptCmds)}
''; '';
}; };
}; };

View file

@ -18,6 +18,7 @@ in
root = s; root = s;
fileset = fs.unions [ fileset = fs.unions [
(fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src))
(s + /contrib)
lockfile lockfile
cargoToml cargoToml
]; ];
@ -25,8 +26,8 @@ in
postInstall = '' postInstall = ''
mkdir -p $out/share/contrib mkdir -p $out/share/contrib
cp -r ${../contrib}/corpus $out/share/contrib/ cp -rv $src/contrib/corpus $out/share/contrib
cp -r ${../contrib}/lua $out/share/contrib/ cp -rv $src/contrib/lua $out/share/contrib
''; '';
cargoLock.lockFile = lockfile; cargoLock.lockFile = lockfile;