From 24d01ac63067070b755be6f910d2422366b081af Mon Sep 17 00:00:00 2001 From: ashisgreat22 Date: Tue, 17 Mar 2026 19:47:43 +0100 Subject: [PATCH] Add modular service configuration with SearXNG and Nginx - Create modules/ directory with reusable NixOS modules - Add system module for main user configuration - Add podman module for rootless container support - Add nginx module with automatic Let's Encrypt SSL - Add searxng module with Anubis AI firewall protection - Configure SearXNG at search.ashisgreat.xyz - Enable nginx reverse proxy with HTTPS Co-Authored-By: Claude Opus 4.6 --- configuration.nix | 21 +++- flake.nix | 1 + modules/default.nix | 9 ++ modules/nginx.nix | 93 ++++++++++++++ modules/podman.nix | 32 +++++ modules/searxng.nix | 289 ++++++++++++++++++++++++++++++++++++++++++++ modules/system.nix | 25 ++++ 7 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 modules/default.nix create mode 100644 modules/nginx.nix create mode 100644 modules/podman.nix create mode 100644 modules/searxng.nix create mode 100644 modules/system.nix diff --git a/configuration.nix b/configuration.nix index dfb97c8..02770c1 100644 --- a/configuration.nix +++ b/configuration.nix @@ -14,7 +14,7 @@ # === Firewall === networking.firewall = { enable = true; - allowedTCPPorts = [ 22 ]; # SSH + allowedTCPPorts = [ 22 ]; # SSH (80/443 added by nginx module) allowPing = false; }; @@ -67,4 +67,23 @@ ]; nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + # === SearXNG === + myModules.searxng = { + enable = true; + port = 8888; + domain = "search.ashisgreat.xyz"; # Change to your domain + instanceName = "Ashie Search"; + }; + + # === Nginx Reverse Proxy === + myModules.nginx = { + enable = true; + email = "info@ashisgreat.xyz"; + domains = { + "search.ashisgreat.xyz" = { + port = 8888; + }; + }; + }; } diff --git a/flake.nix b/flake.nix index b75008f..62ba5f2 100644 --- a/flake.nix +++ b/flake.nix @@ -12,6 +12,7 @@ system = "x86_64-linux"; modules = [ ./configuration.nix + ./modules sops-nix.nixosModules.sops ]; }; diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..e743ff5 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,9 @@ +# Module exports +{ + imports = [ + ./system.nix + ./podman.nix + ./nginx.nix + ./searxng.nix + ]; +} diff --git a/modules/nginx.nix b/modules/nginx.nix new file mode 100644 index 0000000..400528a --- /dev/null +++ b/modules/nginx.nix @@ -0,0 +1,93 @@ +# Nginx Reverse Proxy Module +# Provides: Nginx with automatic Let's Encrypt certificates +# +# Usage: +# myModules.nginx = { +# enable = true; +# email = "your@email.com"; +# domains = { +# "search.example.com" = { +# port = 8888; +# }; +# }; +# }; + +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.myModules.nginx; +in +{ + options.myModules.nginx = { + enable = lib.mkEnableOption "Nginx reverse proxy with Let's Encrypt"; + + email = lib.mkOption { + type = lib.types.str; + example = "admin@example.com"; + description = "Email address for Let's Encrypt registration"; + }; + + domains = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + port = lib.mkOption { + type = lib.types.port; + description = "Local port to proxy to"; + }; + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Extra Nginx config for this location"; + }; + }; + }); + default = { }; + description = "Domains to configure with their proxy targets"; + }; + }; + + config = lib.mkIf cfg.enable { + # Open HTTP/HTTPS ports + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + # ACME (Let's Encrypt) configuration + security.acme = { + acceptTerms = true; + defaults.email = cfg.email; + certs = lib.mapAttrs' (domain: opts: { + name = domain; + value = { }; + }) cfg.domains; + }; + + # Nginx configuration + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + + virtualHosts = lib.mapAttrs' (domain: opts: { + name = domain; + value = { + enableACME = true; + forceSSL = true; + + locations."/" = { + proxyPass = "http://127.0.0.1:${toString opts.port}"; + extraConfig = opts.extraConfig; + }; + }; + }) cfg.domains; + }; + + # Ensure nginx user can access ACME certs + users.users.nginx.extraGroups = [ "acme" ]; + }; +} diff --git a/modules/podman.nix b/modules/podman.nix new file mode 100644 index 0000000..971a21d --- /dev/null +++ b/modules/podman.nix @@ -0,0 +1,32 @@ +# Podman Module +# Provides: Rootless container runtime configuration +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.myModules.podman; + mainUser = config.myModules.system.mainUser; +in +{ + options.myModules.podman = { + enable = lib.mkEnableOption "Podman container runtime"; + }; + + config = lib.mkIf cfg.enable { + virtualisation.podman = { + enable = true; + dockerCompat = true; + defaultNetwork.settings.dns_enabled = true; + }; + + # Enable OCI containers (quadlet/podman containers) + virtualisation.oci-containers.backend = "podman"; + + # Give main user access to podman + users.users.${mainUser}.extraGroups = [ "podman" ]; + }; +} diff --git a/modules/searxng.nix b/modules/searxng.nix new file mode 100644 index 0000000..e42c036 --- /dev/null +++ b/modules/searxng.nix @@ -0,0 +1,289 @@ +# SearXNG Module (Rootless Podman) +# Provides: Private meta-search engine running in a rootless container +# +# Usage: +# myModules.searxng = { +# enable = true; +# port = 8888; +# domain = "search.example.com"; +# }; + +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.myModules.searxng; + mainUser = config.myModules.system.mainUser; + mainUserUid = toString config.users.users.${mainUser}.uid; + anubisPolicy = pkgs.writeText "anubis-policy.yml" '' + bots: + - name: "Allow OpenSearch" + action: ALLOW + path_regex: ".*opensearch\\.xml.*" + - name: "Catch-All" + user_agent_regex: ".*" + action: CHALLENGE + ''; + + faviconsConfig = pkgs.writeText "favicons.toml" '' + [favicons] + cfg_schema = 1 + + [favicons.cache] + db_url = "/var/cache/searxng/faviconcache.db" + LIMIT_TOTAL_BYTES = 2147483648 + + [favicons.proxy.resolver_map] + google = "searx.favicons.resolver.google_resolver" + duckduckgo = "searx.favicons.resolver.duckduckgo_resolver" + ''; +in +{ + options.myModules.searxng = { + enable = lib.mkEnableOption "SearXNG meta-search engine"; + + port = lib.mkOption { + type = lib.types.port; + default = 8888; + description = "Port to expose SearXNG on localhost"; + }; + + domain = lib.mkOption { + type = lib.types.str; + example = "search.example.com"; + description = "Public domain name for SearXNG"; + }; + + instanceName = lib.mkOption { + type = lib.types.str; + default = "SearXNG"; + description = "Name displayed in the search interface"; + }; + + donations = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Map of donation platform names to URLs (e.g. { patreon = '...'; })"; + }; + }; + + config = lib.mkIf cfg.enable { + # Ensure Podman is enabled + myModules.podman.enable = true; + + # 1. Create Bridge Network + systemd.services."create-searxng-network" = { + serviceConfig.Type = "oneshot"; + serviceConfig.User = mainUser; + serviceConfig.RemainAfterExit = true; + after = [ "user-runtime-dir@${mainUserUid}.service" ]; + requires = [ "user-runtime-dir@${mainUserUid}.service" ]; + path = [ + pkgs.podman + pkgs.shadow + ]; + script = '' + export PATH=/run/wrappers/bin:$PATH + export XDG_RUNTIME_DIR="/run/user/${mainUserUid}" + export HOME="/home/${mainUser}" + + if ! podman network exists searxng-net; then + echo "Creating searxng-net..." + podman network create searxng-net --subnet 10.89.2.0/24 + else + echo "searxng-net already exists." + fi + ''; + }; + + # 2. Valkey Container (Cache/Limiter) + virtualisation.oci-containers.containers."searxng-valkey" = { + image = "docker.io/valkey/valkey:alpine"; + labels = { "io.containers.autoupdate" = "registry"; }; + cmd = [ + "valkey-server" + "--save" + "" + "--appendonly" + "no" + ]; + extraOptions = [ + "--network=searxng-net" + "--network-alias=valkey" + ]; + }; + + # 3. SearXNG Container + virtualisation.oci-containers.containers."searxng" = { + image = "docker.io/searxng/searxng:latest"; + environment = { + "SEARXNG_BASE_URL" = "https://${cfg.domain}"; + "SEARXNG_REDIS_URL" = "valkey://valkey:6379"; + "SEARXNG_URL_BASE" = "https://${cfg.domain}"; + "GRANIAN_HOST" = "0.0.0.0"; + }; + environmentFiles = [ + config.sops.templates."searxng.env".path + ]; + extraOptions = [ + "--network=searxng-net" + "--network-alias=searxng" + "--cap-drop=ALL" + "--cap-add=CHOWN" + "--cap-add=SETGID" + "--cap-add=SETUID" + "--cap-add=DAC_OVERRIDE" + "--dns=9.9.9.9" + "--dns=1.1.1.1" + ]; + volumes = [ + "${config.sops.templates."searxng_settings.yml".path}:/etc/searxng/settings.yml:ro" + "${faviconsConfig}:/etc/searxng/favicons.toml:ro" + "searxng-cache:/var/cache/searxng" + ]; + dependsOn = [ "searxng-valkey" ]; + }; + + # 4. Anubis Container (AI Firewall) + virtualisation.oci-containers.containers."searxng-anubis" = { + image = "ghcr.io/techarohq/anubis:latest"; + labels = { "io.containers.autoupdate" = "registry"; }; + ports = [ "127.0.0.1:${toString cfg.port}:8080" ]; + environment = { + "TARGET" = "http://searxng:8080"; + "BIND" = ":8080"; + "POLICY_FNAME" = "/etc/anubis/policy.yml"; + }; + extraOptions = [ + "--network=searxng-net" + "--network-alias=searxng-anubis" + ]; + volumes = [ + "${anubisPolicy}:/etc/anubis/policy.yml:ro" + ]; + dependsOn = [ "searxng" ]; + }; + + # SOPS templates + sops.templates."searxng.env" = { + owner = mainUser; + content = '' + SEARXNG_SECRET_KEY=${config.sops.placeholder.searxng_secret_key} + ''; + }; + + sops.templates."searxng_settings.yml" = { + owner = mainUser; + content = '' + use_default_settings: true + + general: + debug: false + instance_name: "${cfg.instanceName}" + contact_url: false + issue_url: false + donation_url: ${if cfg.donations ? "Monero" then "\"${cfg.donations.Monero}\"" else "false"} + donations: + ${lib.concatStringsSep "\n " ( + lib.mapAttrsToList (name: url: "${name}: \"${url}\"") cfg.donations + )} + + outgoing: + request_timeout: 10.0 + connect_timeout: 6.0 + max_retry_count: 3 + enable_ipv6: false + + search: + safe_search: 0 + favicon_resolver: "google" + autocomplete: "google" + default_lang: "en-US" + formats: + - html + - json + + server: + port: 8080 + bind_address: "0.0.0.0" + secret_key: "${config.sops.placeholder.searxng_secret_key}" + limiter: true + image_proxy: true + public_instance: true + + default_http_headers: + Content-Security-Policy: "upgrade-insecure-requests; default-src 'none'; script-src 'self'; style-src 'self' 'sha256-/ldGxQqxNIMRftg3AGsPF+F281wiBPECUDcL2RJkxdU='; form-action 'self' https://github.com/searxng/searxng/issues/new; font-src 'self'; frame-ancestors 'self'; img-src 'self' data:; connect-src 'self' https://overpass-api.de; manifest-src 'self'" + + ui: + default_theme: simple + default_theme_style: dark + static_use_hash: true + + redis: + url: valkey://valkey:6379/0 + ''; + }; + + # Secret definitions + sops.secrets.searxng_secret_key = { }; + + # Rootless Overrides + systemd.services."podman-searxng".serviceConfig.User = lib.mkForce mainUser; + systemd.services."podman-searxng".environment = { + HOME = "/home/${mainUser}"; + XDG_RUNTIME_DIR = "/run/user/${mainUserUid}"; + }; + systemd.services."podman-searxng".serviceConfig.Type = lib.mkForce "simple"; + systemd.services."podman-searxng".serviceConfig.Delegate = true; + systemd.services."podman-searxng".after = [ + "create-searxng-network.service" + "user-runtime-dir@${mainUserUid}.service" + "network-online.target" + ]; + systemd.services."podman-searxng".requires = [ + "create-searxng-network.service" + "user-runtime-dir@${mainUserUid}.service" + "network-online.target" + ]; + + systemd.services."podman-searxng-valkey".serviceConfig.User = lib.mkForce mainUser; + systemd.services."podman-searxng-valkey".environment = { + HOME = "/home/${mainUser}"; + XDG_RUNTIME_DIR = "/run/user/${mainUserUid}"; + }; + systemd.services."podman-searxng-valkey".serviceConfig.Type = lib.mkForce "simple"; + systemd.services."podman-searxng-valkey".serviceConfig.Delegate = true; + systemd.services."podman-searxng-valkey".after = [ + "create-searxng-network.service" + "user-runtime-dir@${mainUserUid}.service" + "network-online.target" + ]; + systemd.services."podman-searxng-valkey".requires = [ + "create-searxng-network.service" + "user-runtime-dir@${mainUserUid}.service" + "network-online.target" + ]; + + systemd.services."podman-searxng-anubis".serviceConfig.User = lib.mkForce mainUser; + systemd.services."podman-searxng-anubis".environment = { + HOME = "/home/${mainUser}"; + XDG_RUNTIME_DIR = "/run/user/${mainUserUid}"; + }; + systemd.services."podman-searxng-anubis".serviceConfig.Type = lib.mkForce "simple"; + systemd.services."podman-searxng-anubis".serviceConfig.Delegate = true; + systemd.services."podman-searxng-anubis".after = [ + "create-searxng-network.service" + "user-runtime-dir@${mainUserUid}.service" + "network-online.target" + ]; + systemd.services."podman-searxng-anubis".requires = [ + "create-searxng-network.service" + "user-runtime-dir@${mainUserUid}.service" + "network-online.target" + ]; + }; +} diff --git a/modules/system.nix b/modules/system.nix new file mode 100644 index 0000000..ea302af --- /dev/null +++ b/modules/system.nix @@ -0,0 +1,25 @@ +# System Module +# Provides: Common system configuration options used by other modules +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.myModules.system; +in +{ + options.myModules.system = { + mainUser = lib.mkOption { + type = lib.types.str; + default = "ashie"; + description = "Main user account for running services"; + }; + }; + + config = { + # Nothing here by default - just provides the option + }; +}