316 lines
9.4 KiB
Nix
316 lines
9.4 KiB
Nix
# SearXNG Service Configuration for Home Lab
|
|
#
|
|
# This module provides SearXNG configuration for private metasearch engine
|
|
# SearXNG aggregates results from various search engines while preserving privacy
|
|
#
|
|
# Features:
|
|
# - Uses reverse proxy for outbound traffic to search engines
|
|
# - Web UI only accessible from Tailscale network
|
|
# - No logging of user queries for privacy
|
|
# - Nginx reverse proxy with security headers
|
|
# - Rate limiting and security hardening
|
|
#
|
|
# Usage:
|
|
# In your machine's configuration.nix, add:
|
|
# imports = [ ../../modules/services/SearXNG.nix ];
|
|
# services.searxng-lab = {
|
|
# enable = true;
|
|
# hostName = "searxng.your-domain.com";
|
|
# reverseProxyHost = "reverse-proxy";
|
|
# };
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}: let
|
|
cfg = config.services.searxng-lab;
|
|
|
|
# Generate a unique secret key for this instance
|
|
secretKeyFile = pkgs.writeText "searxng-secret" (builtins.hashString "sha256" cfg.hostName);
|
|
|
|
# SearXNG settings configuration
|
|
searxngSettings = {
|
|
use_default_settings = true;
|
|
|
|
server = {
|
|
port = cfg.port;
|
|
bind_address = "127.0.0.1";
|
|
secret_key = "@SECRET_KEY@"; # Will be replaced at runtime
|
|
base_url = "http://${cfg.hostName}";
|
|
image_proxy = true;
|
|
public_instance = false;
|
|
limiter = true;
|
|
method = "POST";
|
|
|
|
default_http_headers = {
|
|
X-Content-Type-Options = "nosniff";
|
|
X-Download-Options = "noopen";
|
|
X-Robots-Tag = "noindex, nofollow";
|
|
Referrer-Policy = "no-referrer";
|
|
X-Frame-Options = "DENY";
|
|
X-XSS-Protection = "1; mode=block";
|
|
};
|
|
};
|
|
|
|
general = {
|
|
debug = false;
|
|
instance_name = "Private Search - ${cfg.hostName}";
|
|
privacypolicy_url = false;
|
|
donation_url = false;
|
|
contact_url = false;
|
|
enable_metrics = false;
|
|
};
|
|
|
|
search = {
|
|
safe_search = 0;
|
|
autocomplete = "";
|
|
default_lang = "en";
|
|
ban_time_on_fail = 5;
|
|
max_ban_time_on_fail = 120;
|
|
formats = ["html" "json"];
|
|
};
|
|
|
|
ui = {
|
|
static_use_hash = true;
|
|
default_locale = "en";
|
|
query_in_title = false;
|
|
infinite_scroll = false;
|
|
center_alignment = false;
|
|
default_theme = "simple";
|
|
hotkeys = "default";
|
|
search_on_category_select = true;
|
|
};
|
|
|
|
# Configure outbound requests through reverse proxy
|
|
outgoing = {
|
|
request_timeout = 3.0;
|
|
useragent_suffix = "";
|
|
pool_connections = 100;
|
|
pool_maxsize = 10;
|
|
enable_http2 = true;
|
|
using_tor_proxy = false;
|
|
|
|
# Use reverse proxy for all outbound requests
|
|
proxies = {
|
|
http = "http://${cfg.reverseProxyHost}:${toString cfg.reverseProxyPort}";
|
|
https = "http://${cfg.reverseProxyHost}:${toString cfg.reverseProxyPort}";
|
|
};
|
|
};
|
|
};
|
|
|
|
# Convert settings to YAML format
|
|
settingsFile = pkgs.writeText "searxng-settings.yml" (lib.generators.toYAML {} searxngSettings);
|
|
in {
|
|
options.services.searxng-lab = {
|
|
enable = lib.mkEnableOption "SearXNG metasearch engine for home lab";
|
|
|
|
hostName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "searxng.local";
|
|
example = "search.example.com";
|
|
description = "Hostname for the SearXNG instance";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8888;
|
|
description = "Port for SearXNG to listen on";
|
|
};
|
|
|
|
reverseProxyHost = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "reverse-proxy";
|
|
example = "proxy.internal.lan";
|
|
description = "Hostname of the reverse proxy for outbound traffic";
|
|
};
|
|
|
|
reverseProxyPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 3128;
|
|
description = "Port of the reverse proxy for outbound traffic";
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to open firewall for HTTP access";
|
|
};
|
|
|
|
tailscaleOnly = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to restrict access to Tailscale network only";
|
|
};
|
|
|
|
nginxVhost = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to create Nginx virtual host";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# Create SearXNG user and group
|
|
users.users.searxng = {
|
|
isSystemUser = true;
|
|
group = "searxng";
|
|
home = "/var/lib/searxng";
|
|
createHome = true;
|
|
description = "SearXNG metasearch engine user";
|
|
};
|
|
|
|
users.groups.searxng = {};
|
|
|
|
# Install SearXNG package
|
|
environment.systemPackages = with pkgs; [
|
|
searxng
|
|
];
|
|
|
|
# Create configuration directory and files
|
|
environment.etc."searxng/settings.yml" = {
|
|
source = settingsFile;
|
|
mode = "0644";
|
|
};
|
|
|
|
# SearXNG systemd service
|
|
systemd.services.searxng = {
|
|
description = "SearXNG metasearch engine";
|
|
after = ["network-online.target"];
|
|
wants = ["network-online.target"];
|
|
wantedBy = ["multi-user.target"];
|
|
|
|
environment = {
|
|
SEARXNG_SETTINGS_PATH = "/etc/searxng/settings.yml";
|
|
PYTHONPATH = "${pkgs.searxng.pythonPath}";
|
|
};
|
|
|
|
serviceConfig = {
|
|
Type = "exec";
|
|
User = "searxng";
|
|
Group = "searxng";
|
|
ExecStart = "${pkgs.searxng}/bin/searxng-run";
|
|
Restart = "always";
|
|
RestartSec = "10s";
|
|
|
|
# Security hardening
|
|
NoNewPrivileges = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
ReadWritePaths = ["/var/lib/searxng" "/tmp"];
|
|
RestrictAddressFamilies = ["AF_INET" "AF_INET6"];
|
|
|
|
# Process limits
|
|
LimitNOFILE = 4096;
|
|
MemoryMax = "512M";
|
|
|
|
# Network restrictions for Tailscale-only access
|
|
IPAddressDeny = lib.mkIf cfg.tailscaleOnly "any";
|
|
IPAddressAllow = lib.mkIf cfg.tailscaleOnly [
|
|
"100.0.0.0/8" # Tailscale network
|
|
"127.0.0.0/8" # Localhost
|
|
"::1/128" # IPv6 localhost
|
|
];
|
|
};
|
|
|
|
preStart = ''
|
|
# Generate secret key and update settings
|
|
SECRET_KEY=$(${pkgs.openssl}/bin/openssl rand -hex 32)
|
|
${pkgs.gnused}/bin/sed -i "s/@SECRET_KEY@/$SECRET_KEY/g" /etc/searxng/settings.yml
|
|
|
|
# Ensure proper permissions
|
|
mkdir -p /var/lib/searxng/cache
|
|
chown -R searxng:searxng /var/lib/searxng
|
|
'';
|
|
};
|
|
|
|
# Nginx reverse proxy configuration
|
|
services.nginx = lib.mkIf cfg.nginxVhost {
|
|
enable = true;
|
|
recommendedGzipSettings = true;
|
|
recommendedOptimisation = true;
|
|
recommendedProxySettings = true;
|
|
recommendedTlsSettings = true;
|
|
virtualHosts."${cfg.hostName}" = {
|
|
listen = [
|
|
{
|
|
addr = "0.0.0.0";
|
|
port = 80;
|
|
}
|
|
];
|
|
|
|
locations."/" = {
|
|
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
|
|
|
extraConfig = ''
|
|
# Proxy headers
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Hide server information
|
|
proxy_hide_header X-Powered-By;
|
|
proxy_hide_header Server;
|
|
|
|
${lib.optionalString cfg.tailscaleOnly ''
|
|
# Restrict access to Tailscale network only
|
|
allow 100.0.0.0/8; # Tailscale network
|
|
allow 127.0.0.1; # Localhost
|
|
allow ::1; # IPv6 localhost
|
|
deny all; # Deny all other access
|
|
''}
|
|
'';
|
|
};
|
|
|
|
# Security headers for all responses
|
|
extraConfig = ''
|
|
# Security headers
|
|
add_header X-Frame-Options DENY always;
|
|
add_header X-Content-Type-Options nosniff always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "no-referrer" always;
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
|
|
# Content Security Policy for SearXNG
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; form-action 'self';" always;
|
|
|
|
# Hide server information
|
|
server_tokens off;
|
|
more_clear_headers Server;
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Firewall configuration
|
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
allowedTCPPorts = [80];
|
|
|
|
# Tailscale-specific firewall rules
|
|
extraCommands = lib.optionalString cfg.tailscaleOnly ''
|
|
# Allow Tailscale network access to HTTP
|
|
iptables -I nixos-fw -i tailscale0 -p tcp --dport 80 -j ACCEPT
|
|
iptables -I nixos-fw -s 100.0.0.0/8 -p tcp --dport 80 -j ACCEPT
|
|
'';
|
|
|
|
extraStopCommands = lib.optionalString cfg.tailscaleOnly ''
|
|
# Clean up Tailscale rules
|
|
iptables -D nixos-fw -i tailscale0 -p tcp --dport 80 -j ACCEPT 2>/dev/null || true
|
|
iptables -D nixos-fw -s 100.0.0.0/8 -p tcp --dport 80 -j ACCEPT 2>/dev/null || true
|
|
'';
|
|
};
|
|
|
|
# Ensure required directories exist
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/searxng 0755 searxng searxng -"
|
|
"d /var/lib/searxng/cache 0755 searxng searxng -"
|
|
];
|
|
|
|
# Add any additional packages needed
|
|
environment.systemPackages = with pkgs; [
|
|
curl # For health checks
|
|
jq # For JSON processing if needed
|
|
];
|
|
};
|
|
}
|