{ config, pkgs, lib, ... }: let inherit (builtins) elem filter isBool ; inherit (lib) concatMapStrings concatStringsSep filterAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkOption ; inherit (lib.types) attrsOf bool either enum int lines nullOr package path str submodule ; cfg = config.nixsap.apps.nginx; explicit = filterAttrs (n: v: n != "_module" && v != null); attrs = opts: submodule { options = opts; }; default = d: t: mkOption { type = t; default = d; }; optional = t: mkOption { type = nullOr t; default = null; }; show = v: if isBool v then (if v then "on" else "off") else toString v; format = indent: set: let mkEntry = k: v: "${indent}${k} ${show v};"; in concatStringsSep "\n" (mapAttrsToList mkEntry (explicit set)); mkServer = name: text: pkgs.writeText "nginx-${name}.conf" '' # ${name}: server { ${text} } ''; # Hardcode defaults that could be overriden in server context. # Add options for http-only directives. nginx-conf = pkgs.writeText "nginx.conf" '' daemon off; user ${cfg.user} ${cfg.user}; pid ${cfg.runDir}/nginx.pid; ${format "" (filterAttrs (n: _: ! elem n ["events" "http"]) cfg.conf)} events { ${format " " cfg.conf.events} } http { ${cfg.conf.http.context} ${concatMapStrings (s: "include ${s};\n") (mapAttrsToList mkServer cfg.conf.http.servers)} } ''; exec = "${cfg.package}/bin/nginx -c ${nginx-conf} -p ${cfg.stateDir}"; enabled = {} != explicit cfg.conf.http.servers; in { options.nixsap.apps.nginx = { package = mkOption { description = "Nginx package"; type = package; default = pkgs.nginx; }; user = mkOption { description = "User to run as"; type = str; default = "nginx"; }; stateDir = mkOption { description = "Directory holding all state for nginx to run"; type = path; default = "/nginx"; }; logDir = mkOption { description = '' Nginx directory for logs. This is read-only. Use it in configuration files of nginx itself or logrotate. ''; type = path; readOnly = true; default = "${cfg.stateDir}/logs"; }; runDir = mkOption { description = '' Directory for sockets and PID-file. UNIX-sockets created by nginx are world-writable. So if you want some privacy, put sockets in this directory. It is owned by nginx user and group, and has mode 0640. ''; type = path; readOnly = true; default = "/run/nginx"; }; conf = default {} (attrs { pcre_jit = optional bool; timer_resolution = optional int; worker_cpu_affinity = optional str; worker_priority = optional int; worker_processes = default "auto" (either int (enum ["auto"])); worker_rlimit_core = optional int; worker_rlimit_nofile = optional int; events = default {} (attrs { accept_mutex = optional bool; accept_mutex_delay = optional int; multi_accept = optional bool; worker_aio_requests = optional int; worker_connections = optional int; }); http = default {} (attrs { servers = default {} (attrsOf lines); context = mkOption { description = '' Default directives in the http context. You normally don't need to change it, because most of directives can be overriden in server or location contexts. This parameter has a reasonale default value which you should append in nixos modules, i. e. by adding geoip directives or maps. Use `lib.mkForce` to completely omit default directives. ''; type = lines; }; }); }); }; config = { nixsap.apps.nginx.conf.http.context = '' include ${cfg.package}/conf/mime.types; default_type application/octet-stream; # This is `combined` format with $remote_user replaced by $http_from. # $http_from is provided by Sproxy: https://hackage.haskell.org/package/sproxy2 log_format sproxy '$remote_addr - $http_from [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent"'; access_log off; error_log stderr info; gzip on; keepalive_timeout 65; sendfile on; ssl_prefer_server_ciphers on; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; tcp_nodelay on; tcp_nopush on; types_hash_max_size 2048; # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ fastcgi_param HTTP_PROXY ""; proxy_set_header Proxy ""; ''; nixsap.system.users.daemons = mkIf enabled [ cfg.user ]; nixsap.apps.logrotate.conf.nginx = mkIf enabled { files = [ "${cfg.logDir}/*.log" ]; directives = { su = "${cfg.user} ${cfg.user}"; delaycompress = mkDefault true; missingok = mkDefault true; notifempty = mkDefault true; rotate = mkDefault 14; sharedscripts = true; daily = mkDefault true; create = mkDefault "0640 ${cfg.user} ${cfg.user}"; postrotate = pkgs.writeBashScript "logrotate-nginx-postrotate" "systemctl kill -s SIGUSR1 --kill-who=main nginx.service"; }; }; systemd.services.nginx = mkIf enabled { description = "web/proxy server"; wants = [ "keys.target" ]; after = [ "keys.target" "local-fs.target" "network.target" ]; wantedBy = [ "multi-user.target" ]; preStart = '' rm -rf '${cfg.runDir}' mkdir -p '${cfg.stateDir}/logs' '${cfg.runDir}' chown -Rc '${cfg.user}:${cfg.user}' '${cfg.stateDir}' '${cfg.runDir}' chmod -Rc u=rwX,g=rX,o= '${cfg.stateDir}' '${cfg.runDir}' ''; serviceConfig = { ExecStart = exec; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; RestartSec = "10s"; StartLimitInterval = "1min"; Restart = "always"; }; }; }; }