diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 85c76ed59d66..a7cde85a5883 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -241,7 +241,7 @@ let configPath = if cfg.enableReload then "/etc/nginx/nginx.conf" - else configFile; + else finalConfigFile; execCommand = "${cfg.package}/bin/nginx -c '${configPath}'"; @@ -391,6 +391,38 @@ let ); mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; + + snakeOilCert = pkgs.runCommand "nginx-config-validate-cert" { nativeBuildInputs = [ pkgs.openssl.bin ]; } '' + mkdir $out + openssl genrsa -des3 -passout pass:xxxxx -out server.pass.key 2048 + openssl rsa -passin pass:xxxxx -in server.pass.key -out $out/server.key + openssl req -new -key $out/server.key -out server.csr \ + -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com" + openssl x509 -req -days 1 -in server.csr -signkey $out/server.key -out $out/server.crt + ''; + validatedConfigFile = pkgs.runCommand "validated-nginx.conf" { nativeBuildInputs = [ cfg.package ]; } '' + # nginx absolutely wants to read the certificates even when told to only validate config, so let's provide fake certs + sed ${configFile} \ + -e "s|ssl_certificate .*;|ssl_certificate ${snakeOilCert}/server.crt;|g" \ + -e "s|ssl_trusted_certificate .*;|ssl_trusted_certificate ${snakeOilCert}/server.crt;|g" \ + -e "s|ssl_certificate_key .*;|ssl_certificate_key ${snakeOilCert}/server.key;|g" \ + > conf + + LD_PRELOAD=${pkgs.libredirect}/lib/libredirect.so \ + NIX_REDIRECTS="/etc/resolv.conf=/dev/null" \ + nginx -t -c $(readlink -f ./conf) > out 2>&1 || true + if ! grep -q "syntax is ok" out; then + echo nginx config validation failed. + echo config was ${configFile}. + echo 'in case of false positive, set `services.nginx.validateConfig` to false.' + echo nginx output: + cat out + exit 1 + fi + cp ${configFile} $out + ''; + + finalConfigFile = if cfg.validateConfig then validatedConfigFile else configFile; in { @@ -489,6 +521,15 @@ in ''; }; + validateConfig = mkOption { + default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform; + defaultText = literalExpression "pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform"; + type = types.bool; + description = lib.mdDoc '' + Validate the generated nginx config at build time. The check is not very robust and can be disabled in case of false positives. This is notably the case when cross-compiling or when using `include` with files outside of the store. + ''; + }; + additionalModules = mkOption { default = []; type = types.listOf (types.attrsOf types.anything); @@ -1027,7 +1068,7 @@ in }; environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload { - source = configFile; + source = finalConfigFile; }; # This service waits for all certificates to be available @@ -1046,7 +1087,7 @@ in # certs are updated _after_ config has been reloaded. before = sslTargets; after = sslServices; - restartTriggers = optionals (cfg.enableReload) [ configFile ]; + restartTriggers = optionals (cfg.enableReload) [ finalConfigFile ]; # Block reloading if not all certs exist yet. # Happens when config changes add new vhosts/certs. unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames); diff --git a/nixos/tests/nginx.nix b/nixos/tests/nginx.nix index d9d073822a14..73f1133bd6ca 100644 --- a/nixos/tests/nginx.nix +++ b/nixos/tests/nginx.nix @@ -61,7 +61,7 @@ import ./make-test-python.nix ({ pkgs, ... }: { specialisation.reloadWithErrorsSystem.configuration = { services.nginx.package = pkgs.nginxMainline; - services.nginx.virtualHosts."!@$$(#*%".locations."~@#*$*!)".proxyPass = ";;;"; + services.nginx.virtualHosts."hello".extraConfig = "access_log /does/not/exist.log;"; }; }; };