summary refs log tree commit diff
path: root/nixos/modules/services
diff options
context:
space:
mode:
authorGuillaume Girol <symphorien+git@xlumurb.eu>2022-12-10 12:00:00 +0000
committerGuillaume Girol <symphorien+git@xlumurb.eu>2022-12-10 23:04:57 +0100
commita768871934dd263423fca2979cedd7f5e8c7379d (patch)
treeeb79d96d321e27d4456366c8d58f655f76fe6ca9 /nixos/modules/services
parent2787fc7d1e51404678614bf0fe92fc296746eec0 (diff)
downloadnixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.tar
nixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.tar.gz
nixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.tar.bz2
nixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.tar.lz
nixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.tar.xz
nixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.tar.zst
nixpkgs-a768871934dd263423fca2979cedd7f5e8c7379d.zip
nixos/nginx: validate syntax of config file at build time
Shamelessly stolen from nixcloud-webservices:
https://github.com/nixcloud/nixcloud-webservices/blob/master/modules/web/webserver/lib/nginx_check_config.nix

The nixos test testing the behavior of nginx in case of faulty config
would not build with this change (on purpose), so I modified it so that
the failure is not syntactic.
Diffstat (limited to 'nixos/modules/services')
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix47
1 files changed, 44 insertions, 3 deletions
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 85c76ed59d6..a7cde85a588 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);