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 <noreply@anthropic.com>
This commit is contained in:
parent
5dcb85e56d
commit
24d01ac630
7 changed files with 469 additions and 1 deletions
|
|
@ -14,7 +14,7 @@
|
||||||
# === Firewall ===
|
# === Firewall ===
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
enable = true;
|
enable = true;
|
||||||
allowedTCPPorts = [ 22 ]; # SSH
|
allowedTCPPorts = [ 22 ]; # SSH (80/443 added by nginx module)
|
||||||
allowPing = false;
|
allowPing = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -67,4 +67,23 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules = [
|
modules = [
|
||||||
./configuration.nix
|
./configuration.nix
|
||||||
|
./modules
|
||||||
sops-nix.nixosModules.sops
|
sops-nix.nixosModules.sops
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
9
modules/default.nix
Normal file
9
modules/default.nix
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Module exports
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./system.nix
|
||||||
|
./podman.nix
|
||||||
|
./nginx.nix
|
||||||
|
./searxng.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
93
modules/nginx.nix
Normal file
93
modules/nginx.nix
Normal file
|
|
@ -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" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
32
modules/podman.nix
Normal file
32
modules/podman.nix
Normal file
|
|
@ -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" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
289
modules/searxng.nix
Normal file
289
modules/searxng.nix
Normal file
|
|
@ -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"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
25
modules/system.nix
Normal file
25
modules/system.nix
Normal file
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue