Compare commits
7 commits
18be5ba041
...
d89fe8ddd5
| Author | SHA1 | Date | |
|---|---|---|---|
|
d89fe8ddd5 |
|||
|
bd10705a29 |
|||
|
f1b4764270 |
|||
|
73f6f76135 |
|||
|
fb6a800e60 |
|||
|
d0edbdf5bb |
|||
|
b53d7a1401 |
6 changed files with 547 additions and 35 deletions
300
README.md
Normal file
300
README.md
Normal 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.
|
||||
29
contrib/corpus/php_exploit.txt
Normal file
29
contrib/corpus/php_exploit.txt
Normal 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
|
||||
22
contrib/corpus/wordpress.txt
Normal file
22
contrib/corpus/wordpress.txt
Normal 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
|
||||
161
contrib/lua/better_response.lua
Normal file
161
contrib/lua/better_response.lua
Normal 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
|
||||
|
|
@ -5,6 +5,7 @@ self: {
|
|||
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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -29,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 = {
|
||||
|
|
@ -37,8 +45,8 @@ in {
|
|||
|
||||
package = mkOption {
|
||||
type = package;
|
||||
default = self.packages.${pkgs.system}.eris;
|
||||
defaultText = literalExpression "pkgs.eris";
|
||||
default = self.packages.${pkgs.stdenv.system}.eris;
|
||||
defaultText = literalExpression "self.packages.\${pkgs.stdenv.system}.eris";
|
||||
description = "The Eris package to use.";
|
||||
};
|
||||
|
||||
|
|
@ -153,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.";
|
||||
};
|
||||
|
||||
|
|
@ -179,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;
|
||||
}
|
||||
|
|
@ -216,7 +224,7 @@ in {
|
|||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.nftables = {
|
||||
networking.nftables = {
|
||||
enable = mkIf cfg.nftablesIntegration cfg.nftablesIntegration;
|
||||
ruleset = mkIf cfg.nftablesIntegration ''
|
||||
table inet filter {
|
||||
|
|
@ -224,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";
|
||||
''}
|
||||
|
|
@ -248,8 +256,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
|
||||
|
|
@ -280,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
|
||||
|
|
@ -306,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
|
||||
|
|
@ -326,8 +331,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 = "eris";
|
||||
CacheDirectory = "eris";
|
||||
StateDirectoryMode = "0750";
|
||||
CacheDirectoryMode = "0750";
|
||||
|
||||
|
|
@ -340,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
|
||||
|
|
@ -358,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/${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}
|
||||
${lib.optionalString (cfg.corpora != {}) (toString copyCorporaCmds)}
|
||||
${lib.optionalString (cfg.luaScripts != {}) (toString copyLuaScriptCmds)}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue