diff --git a/nixos/lib/attrsets.nix b/nixos/lib/attrsets.nix new file mode 100644 index 000000000..d8af08882 --- /dev/null +++ b/nixos/lib/attrsets.nix @@ -0,0 +1,19 @@ +{ lib, ...}: + +with lib; + +rec { + # Returns a list of attribute names that appear in more than one attrset. + # Useful for checking if merging the attrsets would overwrite a previous value. + # + # duplicateAttrNames [ { a = 1; } { b = 2; } { a = 3; } ] + # => [ "a" ] + duplicateAttrNames = listOfAttrs: + attrNames + (filterAttrs + (n: a: a > 1) + (foldAttrs + (n: acc: acc + 1) + 0 + listOfAttrs)); +} diff --git a/nixos/lib/default.nix b/nixos/lib/default.nix index 1c47ce03d..46cc15512 100644 --- a/nixos/lib/default.nix +++ b/nixos/lib/default.nix @@ -1,7 +1,8 @@ { config, pkgs, lib, ... }: let - files = import ./files.nix { inherit pkgs lib; }; + attrsets = import ./attrsets.nix { inherit config lib; }; + files = import ./files.nix { inherit config pkgs lib; }; math = import ./math.nix { inherit pkgs lib; }; network = import ./network.nix { inherit config pkgs lib; }; system = import ./system.nix { inherit config pkgs lib; }; @@ -19,7 +20,7 @@ in config = { fclib = - { inherit files math network system utils; } - // files // math // network // system // utils; + { inherit attrsets files math network system utils; } + // attrsets // files // math // network // system // utils; }; } diff --git a/nixos/lib/files.nix b/nixos/lib/files.nix index 26422d0e4..b38f8cde5 100644 --- a/nixos/lib/files.nix +++ b/nixos/lib/files.nix @@ -1,4 +1,8 @@ -{ lib, ... }: +{ config, lib, ... }: + +let + fclib = config.fclib; +in with lib; @@ -26,4 +30,31 @@ rec { jsonFromFile = file: default: builtins.fromJSON (configFromFile file default); + # Reads JSON config snippets from a directory and merges them into one object. + # Each snippet must contain a single top-level object. + # The keys in the top-level objects must be unique for all snippets. + # Throws an error if duplicate keys are found. + jsonFromDir = path: let + objects = + map + (filename: builtins.fromJSON (readFile filename)) + (filter + (filename: hasSuffix "json" filename) + (files path)); + + mergedObject = + fold + (obj: acc: acc // obj) + {} + objects; + + duplicates = fclib.duplicateAttrNames objects; + in + if duplicates == [] + then mergedObject + else throw '' + Top-level JSON config keys are not unique in ${path}! + Duplicate keys: ${concatStringsSep ", " duplicates} + '' + ; } diff --git a/nixos/services/nginx/README.txt b/nixos/services/nginx/README.txt index 63a013987..343b8329c 100644 --- a/nixos/services/nginx/README.txt +++ b/nixos/services/nginx/README.txt @@ -1,38 +1,30 @@ Nginx is enabled on this machine. -Manual configuration --------------------- - -Put your site configuration into this directory as `*.conf`. You may -add other files, like SSL keys, as well. - -If you want to authenticate against the Flying Circus users with login permission, -use the following snippet, and *USE SSL*: - -auth_basic "FCIO user"; -auth_basic_user_file "/etc/local/nginx/htpasswd_fcio_users"; - -There is also an `example-configuration` here. Copy to some file ending with -*.conf and adapt. - -Changes to *.conf will cause nginx only to reload, not to restart, on the next -nixos-rebuild run. +We provide basic config. You can use two ways to add custom configuration. +The combined nginx config file can be shown with: `nginx-show-config` Structured configuration ------------------------ -Alternatively, you can place virtual host definition in -`/etc/local/nginx/vhosts.json` which resembles NixOS nginx virtualHosts options +You can place virtual host definitions in `/etc/local/nginx/*.json` +that are managed by NixOS. They are built into the combined config file by fc-manage. + +Changes to these files will cause nginx to reload without downtime on the next +fc-manage run if the config is valid. It will display a warning if invalid +settings are found in the nginx config. -Example vhosts.json containing a virtual with static root directory, a bit of -custom configuration, and Let's encrypt SSL: +The config snippets must contain a single JSON object that defines one or +more virtual hosts. Multiple JSON config files will be merged into one object. +It's a good idea to use one config file per virtual host. + +example.json containing a virtual with static root directory, a bit of +custom configuration, and SSL with a default certificate from Let's Encrypt: ``` { "www.example.org": { "serverAliases": ["example.org"], - "acmeEmail" "forceSSL": true, "root": "/srv/webroot", "extraConfig": "add_header Strict-Transport-Security max-age=31536000; rewrite ^/old_url /new_url redirect;", @@ -45,9 +37,34 @@ custom configuration, and Let's encrypt SSL: } ``` -All options are documented in -https://nixos.org/nixos/options.html#services.nginx.virtualhosts. Note that an -non-standard attribute "acmeEmail" must be set to a contact mail address -in order to activate Let's encrypt. +All options are documented at +https://nixos.org/nixos/options.html#services.nginx.virtualhosts.%3Cname%3E + +For SSL support with redirection from HTTP to HTTPS, use `forceSSL`. +Let's Encrypt (`enableACME`) is activated automatically if one of `forceSSL`, `onlySSL` or `addSSL` +is set to true. +To use a custom certificate, set the certificate options and set `"enableACME" = false`. + +We support the custom option `emailACME` to set the contact address for Let's Encrypt. + +Manual configuration +-------------------- + +You can also use plain nginx config files /etc/local/nginx/*.conf for configuration. +These config files are not managed by NixOS. You are on your own. + +You may add other files to the directory, like SSL keys, as well. + +If you want to authenticate against the Flying Circus users with login permission, +use the following snippet, and *USE SSL*: + +auth_basic "FCIO user"; +auth_basic_user_file "/etc/local/nginx/htpasswd_fcio_users"; + +There is also an `example-configuration` here. Copy to some file ending with +*.conf and adapt. + +You can check if the config is valid with: `nginx-check-config` -Changes to *.json will cause nginx to restart on the next nixos-rebuild run. +Changes to *.conf will be picked up after a service reload: `systemctl reload nginx` +Nginx will not reload if config errors are present. diff --git a/nixos/services/nginx/default.nix b/nixos/services/nginx/default.nix index 37e31e149..b6f586a9f 100644 --- a/nixos/services/nginx/default.nix +++ b/nixos/services/nginx/default.nix @@ -9,20 +9,21 @@ let package = config.services.nginx.package; currentConf = "/etc/current-config/nginx.conf"; - vhostsJSON = fclib.jsonFromFile "/etc/local/nginx/vhosts.json" "{}"; + vhostsJSON = fclib.jsonFromDir "/etc/local/nginx"; + virtualHosts = lib.mapAttrs ( _: val: (lib.optionalAttrs - ((val ? addSSL || val ? onlySSL || val ? forceSSL) && val ? acmeEmail) - { enableACME = true; }) // removeAttrs val [ "acmeEmail" ]) + ((val ? addSSL || val ? onlySSL || val ? forceSSL)) + { enableACME = true; }) // removeAttrs val [ "emailACME" ]) vhostsJSON; - acmeEmails = - lib.mapAttrs (name: val: { email = val.acmeEmail; }) - (lib.filterAttrs (_: val: val ? acmeEmail ) vhostsJSON); + # only email setting supported at the moment + acmeSettings = + lib.mapAttrs (name: val: { email = val.emailACME; }) + (lib.filterAttrs (_: val: val ? emailACME ) vhostsJSON); - localConfig = - lib.optionalString (pathExists "/etc/local/nginx") "${/etc/local/nginx}"; + acmeVhosts = (lib.filterAttrs (_: val: val ? enableACME ) vhostsJSON); baseConfig = '' worker_processes ${toString (fclib.current_cores config 1)}; @@ -169,11 +170,11 @@ in notification = "HTTPS cert for ${name} (Let's encrypt)"; command = "check_http -H ${name} -p 443 -S -C 5"; interval = 600; - })) acmeEmails); + })) acmeVhosts); networking.firewall.allowedTCPPorts = [ 80 443 ]; - security.acme.certs = acmeEmails; + security.acme.certs = acmeSettings; services.nginx = { enable = true; @@ -209,35 +210,21 @@ in "d /etc/local/nginx/modsecurity 2775 nginx service" ]; - flyingcircus.activationScripts = { - - nginx-reload = lib.stringAfter [ "etc" ] '' - if [[ ! -e ${stateDir}/reload.conf ]]; then - install -D /dev/null ${stateDir}/reload.conf - fi - # reload not possible when structured vhosts have changed -> skip - if [[ -n "$(find -L /etc/local/nginx -type f -name '*.json' \ - -newer ${stateDir}/reload.conf)" ]]; then - echo -n "${localConfig}" > ${stateDir}/reload.conf - # check if local config has changed - elif [[ "$(< ${stateDir}/reload.conf )" != "${localConfig}" ]]; then - echo "nginx: config change detected, reloading" - if ${package}/bin/nginx -t -c ${currentConf} -p ${stateDir}; then - ${pkgs.systemd}/bin/systemctl reload nginx.service || true - echo -n "${localConfig}" > ${stateDir}/reload.conf - else - echo "nginx: not reloading due to error" - false - fi - fi - ''; - }; - flyingcircus.localConfigDirs.nginx = { dir = "/etc/local/nginx"; user = "nginx"; }; - + + environment.systemPackages = [ + (pkgs.writeScriptBin "nginx-check-config" '' + $(systemctl cat nginx | grep "X-CheckConfigCmd" | cut -d= -f2) + '') + + (pkgs.writeScriptBin "nginx-show-config" '' + cat $(systemctl cat nginx | grep "X-ConfigFile" | cut -d= -f2) + '') + ]; + }) { diff --git a/tests/default.nix b/tests/default.nix index c6bd29737..48afa614f 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -30,6 +30,7 @@ in { mongodb34 = callTest ./mongodb.nix { rolename = "mongodb34"; }; network = callSubTests ./network {}; nginx = callTest ./nginx.nix {}; + nginx_reload = callTest (nixpkgs + /nixos/tests/nginx.nix) {}; openvpn = callTest ./openvpn.nix {}; postgresql95 = callTest ./postgresql.nix { rolename = "postgresql95"; }; postgresql96 = callTest ./postgresql.nix { rolename = "postgresql96"; }; diff --git a/versions.json b/versions.json index 2734fad91..e41687e24 100644 --- a/versions.json +++ b/versions.json @@ -2,8 +2,8 @@ "nixpkgs": { "owner": "flyingcircusio", "repo": "nixpkgs", - "rev": "fd0aff0181edef0bad830af8e70f664e85838f19", - "sha256": "0j25wbr00hzqvv3pnji1qif3w6f8wqmp7lw8a9ih5xxrcwzf1sgw" + "rev": "9bc51a466db15db1d791a066981538d556f135b8", + "sha256": "0hz5bpihckghjdq30bcwxwb5x7i41gpaywi8gbhq17v7xv7h2cjh" }, "nixos-18.09": { "owner": "nixos",