{ config, pkgs, lib, ... }: let # Read multiple API keys from the secrets file at build time # Note: This embeds the secrets in the Nix store, which is a trade-off # Alternative: Keep secrets file and read via njs module or external auth service secretsFile = "/srv/nginx/secrets"; # Read API keys from file (one key per line, will be evaluated at build time) # If the file doesn't exist yet, this will fail - create it first apiKeysRaw = builtins.readFile secretsFile; apiKeys = lib.filter (k: k != "") (lib.splitString "\n" apiKeysRaw); # Generate map entries for each key mapEntries = lib.concatMapStringsSep "\n " (key: ''"Bearer ${key}" "authorized";'') apiKeys; in { services.nginx = { enable = true; # Recommended settings recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; # Increase bucket size for long Bearer tokens mapHashBucketSize = 128; # Map directive to check Authorization header against multiple keys appendHttpConfig = '' # Check if the Authorization header matches any expected value map $http_authorization $auth_status { default "unauthorized"; "" "no_auth"; ${mapEntries} } ''; # Virtual hosts configuration virtualHosts = { # Main website - Static HTML/CSS "www.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; root = "/srv/www/binning.net"; locations."/" = { index = "index.html"; tryFiles = "$uri $uri/ =404"; extraConfig = '' # Enable Server Side Includes for navbar/footer includes ssi on; ''; }; # Private blog articles with HTTP basic authentication locations."/blog/private/" = { extraConfig = '' auth_basic "Private Articles"; auth_basic_user_file /srv/nginx/.htpasswd; # Enable Server Side Includes ssi on; ''; }; # Optional: Custom 404 page extraConfig = '' error_page 404 /404.html; ''; }; # Ollama with Bearer token authentication "ollama.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { extraConfig = '' # Check auth status if ($auth_status = "no_auth") { return 401 "Unauthorized: Bearer token required\n"; } if ($auth_status = "unauthorized") { return 403 "Forbidden: Invalid API key\n"; } # Proxy to Ollama (only if authorized) proxy_pass http://localhost:11434; 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; # Timeouts for long-running requests proxy_read_timeout 300s; proxy_connect_timeout 300s; proxy_send_timeout 300s; # Allow large request bodies client_max_body_size 100M; # Logging access_log /var/log/nginx/ollama_access.log; error_log /var/log/nginx/ollama_error.log; ''; }; }; # Forgejo "forgejo.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:3000"; # No extraConfig needed - recommendedProxySettings handles headers }; }; # Radicale "radicale.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:5232"; # recommendedProxySettings handles most headers extraConfig = '' proxy_set_header X-Script-Name ""; ''; }; }; # DocuSeal "docuseal.binning.net" = { forceSSL = true; #enableACME = true; sslCertificate = "/srv/nginx/binning.net.pem"; sslCertificateKey = "/srv/nginx/binning.net.key.pem"; locations."/" = { proxyPass = "http://127.0.0.1:3030"; proxyWebsockets = true; }; }; }; }; # Firewall networking.firewall.allowedTCPPorts = [ 80 443 ]; # ACME/Let's Encrypt security.acme = { acceptTerms = true; defaults.email = "hvanb@pm.me"; }; }