about summary refs log tree commit diff
path: root/nixpkgs/nixos/tests/common
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2019-01-07 02:18:36 +0000
committerAlyssa Ross <hi@alyssa.is>2019-01-07 02:18:47 +0000
commit36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2 (patch)
treeb3faaf573407b32aa645237a4d16b82778a39a92 /nixpkgs/nixos/tests/common
parent4e31070265257dc67d120c27e0f75c2344fdfa9a (diff)
parentabf060725d7614bd3b9f96764262dfbc2f9c2199 (diff)
downloadnixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.tar
nixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.tar.gz
nixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.tar.bz2
nixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.tar.lz
nixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.tar.xz
nixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.tar.zst
nixlib-36f56d99fa0a0765c9f1de4a5f17a9b05830c3f2.zip
Add 'nixpkgs/' from commit 'abf060725d7614bd3b9f96764262dfbc2f9c2199'
git-subtree-dir: nixpkgs
git-subtree-mainline: 4e31070265257dc67d120c27e0f75c2344fdfa9a
git-subtree-split: abf060725d7614bd3b9f96764262dfbc2f9c2199
Diffstat (limited to 'nixpkgs/nixos/tests/common')
-rw-r--r--nixpkgs/nixos/tests/common/letsencrypt/common.nix27
-rw-r--r--nixpkgs/nixos/tests/common/letsencrypt/default.nix445
-rw-r--r--nixpkgs/nixos/tests/common/letsencrypt/mkcerts.nix69
-rwxr-xr-xnixpkgs/nixos/tests/common/letsencrypt/mkcerts.sh6
-rw-r--r--nixpkgs/nixos/tests/common/letsencrypt/snakeoil-certs.nix253
-rw-r--r--nixpkgs/nixos/tests/common/resolver.nix141
-rw-r--r--nixpkgs/nixos/tests/common/user-account.nix14
-rw-r--r--nixpkgs/nixos/tests/common/webroot/news-rss.xml15
-rw-r--r--nixpkgs/nixos/tests/common/x11.nix12
9 files changed, 982 insertions, 0 deletions
diff --git a/nixpkgs/nixos/tests/common/letsencrypt/common.nix b/nixpkgs/nixos/tests/common/letsencrypt/common.nix
new file mode 100644
index 000000000000..798a749f7f9b
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/letsencrypt/common.nix
@@ -0,0 +1,27 @@
+{ lib, nodes, ... }: {
+  networking.nameservers = [
+    nodes.letsencrypt.config.networking.primaryIPAddress
+  ];
+
+  nixpkgs.overlays = lib.singleton (self: super: {
+    cacert = super.cacert.overrideDerivation (drv: {
+      installPhase = (drv.installPhase or "") + ''
+        cat "${nodes.letsencrypt.config.test-support.letsencrypt.caCert}" \
+          >> "$out/etc/ssl/certs/ca-bundle.crt"
+      '';
+    });
+
+    # Override certifi so that it accepts fake certificate for Let's Encrypt
+    # Need to override the attribute used by simp_le, which is python3Packages
+    python3Packages = (super.python3.override {
+      packageOverrides = lib.const (pysuper: {
+        certifi = pysuper.certifi.overridePythonAttrs (attrs: {
+          postPatch = (attrs.postPatch or "") + ''
+            cat "${self.cacert}/etc/ssl/certs/ca-bundle.crt" \
+              > certifi/cacert.pem
+          '';
+        });
+      });
+    }).pkgs;
+  });
+}
diff --git a/nixpkgs/nixos/tests/common/letsencrypt/default.nix b/nixpkgs/nixos/tests/common/letsencrypt/default.nix
new file mode 100644
index 000000000000..73aac51a0126
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/letsencrypt/default.nix
@@ -0,0 +1,445 @@
+# Fully pluggable module to have Letsencrypt's Boulder ACME service running in
+# a test environment.
+#
+# The certificate for the ACME service is exported as:
+#
+#   config.test-support.letsencrypt.caCert
+#
+# This value can be used inside the configuration of other test nodes to inject
+# the snakeoil certificate into security.pki.certificateFiles or into package
+# overlays.
+#
+# Another value that's needed if you don't use a custom resolver (see below for
+# notes on that) is to add the letsencrypt node as a nameserver to every node
+# that needs to acquire certificates using ACME, because otherwise the API host
+# for letsencrypt.org can't be resolved.
+#
+# A configuration example of a full node setup using this would be this:
+#
+# {
+#   letsencrypt = import ./common/letsencrypt;
+#
+#   example = { nodes, ... }: {
+#     networking.nameservers = [
+#       nodes.letsencrypt.config.networking.primaryIPAddress
+#     ];
+#     security.pki.certificateFiles = [
+#       nodes.letsencrypt.config.test-support.letsencrypt.caCert
+#     ];
+#   };
+# }
+#
+# By default, this module runs a local resolver, generated using resolver.nix
+# from the parent directory to automatically discover all zones in the network.
+#
+# If you do not want this and want to use your own resolver, you can just
+# override networking.nameservers like this:
+#
+# {
+#   letsencrypt = { nodes, ... }: {
+#     imports = [ ./common/letsencrypt ];
+#     networking.nameservers = [
+#       nodes.myresolver.config.networking.primaryIPAddress
+#     ];
+#   };
+#
+#   myresolver = ...;
+# }
+#
+# Keep in mind, that currently only _one_ resolver is supported, if you have
+# more than one resolver in networking.nameservers only the first one will be
+# used.
+#
+# Also make sure that whenever you use a resolver from a different test node
+# that it has to be started _before_ the ACME service.
+{ config, pkgs, lib, ... }:
+
+let
+  softhsm = pkgs.stdenv.mkDerivation rec {
+    name = "softhsm-${version}";
+    version = "1.3.8";
+
+    src = pkgs.fetchurl {
+      url = "https://dist.opendnssec.org/source/${name}.tar.gz";
+      sha256 = "0flmnpkgp65ym7w3qyg78d3fbmvq3aznmi66rgd420n33shf7aif";
+    };
+
+    configureFlags = [ "--with-botan=${pkgs.botan}" ];
+    buildInputs = [ pkgs.sqlite ];
+  };
+
+  pkcs11-proxy = pkgs.stdenv.mkDerivation {
+    name = "pkcs11-proxy";
+
+    src = pkgs.fetchFromGitHub {
+      owner = "SUNET";
+      repo = "pkcs11-proxy";
+      rev = "944684f78bca0c8da6cabe3fa273fed3db44a890";
+      sha256 = "1nxgd29y9wmifm11pjcdpd2y293p0dgi0x5ycis55miy97n0f5zy";
+    };
+
+    postPatch = "patchShebangs mksyscalls.sh";
+
+    nativeBuildInputs = [ pkgs.cmake ];
+    buildInputs = [ pkgs.openssl pkgs.libseccomp ];
+  };
+
+  mkGoDep = { goPackagePath, url ? "https://${goPackagePath}", rev, sha256 }: {
+    inherit goPackagePath;
+    src = pkgs.fetchgit { inherit url rev sha256; };
+  };
+
+  goose = let
+    owner = "liamstask";
+    repo = "goose";
+    rev = "8488cc47d90c8a502b1c41a462a6d9cc8ee0a895";
+    version = "20150116";
+
+  in pkgs.buildGoPackage rec {
+    name = "${repo}-${version}";
+
+    src = pkgs.fetchFromBitbucket {
+      name = "${name}-src";
+      inherit rev owner repo;
+      sha256 = "1jy0pscxjnxjdg3hj111w21g8079rq9ah2ix5ycxxhbbi3f0wdhs";
+    };
+
+    goPackagePath = "bitbucket.org/${owner}/${repo}";
+    subPackages = [ "cmd/goose" ];
+    extraSrcs = map mkGoDep [
+      { goPackagePath = "github.com/go-sql-driver/mysql";
+        rev = "2e00b5cd70399450106cec6431c2e2ce3cae5034";
+        sha256 = "085g48jq9hzmlcxg122n0c4pi41sc1nn2qpx1vrl2jfa8crsppa5";
+      }
+      { goPackagePath = "github.com/kylelemons/go-gypsy";
+        rev = "08cad365cd28a7fba23bb1e57aa43c5e18ad8bb8";
+        sha256 = "1djv7nii3hy451n5jlslk0dblqzb1hia1cbqpdwhnps1g8hqjy8q";
+      }
+      { goPackagePath = "github.com/lib/pq";
+        rev = "ba5d4f7a35561e22fbdf7a39aa0070f4d460cfc0";
+        sha256 = "1mfbqw9g00bk24bfmf53wri5c2wqmgl0qh4sh1qv2da13a7cwwg3";
+      }
+      { goPackagePath = "github.com/mattn/go-sqlite3";
+        rev = "2acfafad5870400156f6fceb12852c281cbba4d5";
+        sha256 = "1rpgil3w4hh1cibidskv1js898hwz83ps06gh0hm3mym7ki8d5h7";
+      }
+      { goPackagePath = "github.com/ziutek/mymysql";
+        rev = "0582bcf675f52c0c2045c027fd135bd726048f45";
+        sha256 = "0bkc9x8sgqbzgdimsmsnhb0qrzlzfv33fgajmmjxl4hcb21qz3rf";
+      }
+      { goPackagePath = "golang.org/x/net";
+        url = "https://go.googlesource.com/net";
+        rev = "10c134ea0df15f7e34d789338c7a2d76cc7a3ab9";
+        sha256 = "14cbr2shl08gyg85n5gj7nbjhrhhgrd52h073qd14j97qcxsakcz";
+      }
+    ];
+  };
+
+  boulder = let
+    owner = "letsencrypt";
+    repo = "boulder";
+    rev = "9c6a1f2adc4c26d925588f5ae366cfd4efb7813a";
+    version = "20180129";
+
+  in pkgs.buildGoPackage rec {
+    name = "${repo}-${version}";
+
+    src = pkgs.fetchFromGitHub {
+      name = "${name}-src";
+      inherit rev owner repo;
+      sha256 = "09kszswrifm9rc6idfaq0p1mz5w21as2qbc8gd5pphrq9cf9pn55";
+    };
+
+    postPatch = ''
+      # compat for go < 1.8
+      sed -i -e 's/time\.Until(\([^)]\+\))/\1.Sub(time.Now())/' \
+        test/ocsp/helper/helper.go
+
+      find test -type f -exec sed -i -e '/libpkcs11-proxy.so/ {
+        s,/usr/local,${pkcs11-proxy},
+      }' {} +
+
+      sed -i -r \
+        -e '/^def +install/a \    return True' \
+        -e 's,exec \./bin/,,' \
+        test/startservers.py
+
+      cat ${lib.escapeShellArg snakeOilCerts.ca.key} > test/test-ca.key
+      cat ${lib.escapeShellArg snakeOilCerts.ca.cert} > test/test-ca.pem
+    '';
+
+    # Until vendored pkcs11 is go 1.9 compatible
+    preBuild = ''
+      rm -r go/src/github.com/letsencrypt/boulder/vendor/github.com/miekg/pkcs11
+    '';
+
+    # XXX: Temporarily brought back putting the source code in the output,
+    # since e95f17e2720e67e2eabd59d7754c814d3e27a0b2 was removing that from
+    # buildGoPackage.
+    preInstall = ''
+      mkdir -p $out
+      pushd "$NIX_BUILD_TOP/go"
+      while read f; do
+        echo "$f" | grep -q '^./\(src\|pkg/[^/]*\)/${goPackagePath}' \
+          || continue
+        mkdir -p "$(dirname "$out/share/go/$f")"
+        cp "$NIX_BUILD_TOP/go/$f" "$out/share/go/$f"
+      done < <(find . -type f)
+      popd
+    '';
+
+    extraSrcs = map mkGoDep [
+      { goPackagePath = "github.com/miekg/pkcs11";
+        rev           = "6dbd569b952ec150d1425722dbbe80f2c6193f83";
+        sha256        = "1m8g6fx7df6hf6q6zsbyw1icjmm52dmsx28rgb0h930wagvngfwb";
+      }
+    ];
+
+    goPackagePath = "github.com/${owner}/${repo}";
+    buildInputs = [ pkgs.libtool ];
+  };
+
+  boulderSource = "${boulder.out}/share/go/src/${boulder.goPackagePath}";
+
+  softHsmConf = pkgs.writeText "softhsm.conf" ''
+    0:/var/lib/softhsm/slot0.db
+    1:/var/lib/softhsm/slot1.db
+  '';
+
+  snakeOilCerts = import ./snakeoil-certs.nix;
+
+  wfeDomain = "acme-v01.api.letsencrypt.org";
+  wfeCertFile = snakeOilCerts.${wfeDomain}.cert;
+  wfeKeyFile = snakeOilCerts.${wfeDomain}.key;
+
+  siteDomain = "letsencrypt.org";
+  siteCertFile = snakeOilCerts.${siteDomain}.cert;
+  siteKeyFile = snakeOilCerts.${siteDomain}.key;
+
+  # Retrieved via:
+  # curl -s -I https://acme-v01.api.letsencrypt.org/terms \
+  #   | sed -ne 's/^[Ll]ocation: *//p'
+  tosUrl = "https://letsencrypt.org/documents/2017.11.15-LE-SA-v1.2.pdf";
+  tosPath = builtins.head (builtins.match "https?://[^/]+(.*)" tosUrl);
+
+  tosFile = pkgs.fetchurl {
+    url = tosUrl;
+    sha256 = "0yvyckqzj0b1xi61sypcha82nanizzlm8yqy828h2jbza7cxi26c";
+  };
+
+  resolver = let
+    message = "You need to define a resolver for the letsencrypt test module.";
+    firstNS = lib.head config.networking.nameservers;
+  in if config.networking.nameservers == [] then throw message else firstNS;
+
+  cfgDir = pkgs.stdenv.mkDerivation {
+    name = "boulder-config";
+    src = "${boulderSource}/test/config";
+    nativeBuildInputs = [ pkgs.jq ];
+    phases = [ "unpackPhase" "patchPhase" "installPhase" ];
+    postPatch = ''
+      sed -i -e 's/5002/80/' -e 's/5002/443/' va.json
+      sed -i -e '/listenAddress/s/:4000/:80/' wfe.json
+      sed -i -r \
+        -e ${lib.escapeShellArg "s,http://boulder:4000/terms/v1,${tosUrl},g"} \
+        -e 's,http://(boulder|127\.0\.0\.1):4000,https://${wfeDomain},g' \
+        -e '/dnsResolver/s/127\.0\.0\.1:8053/${resolver}:53/' \
+        *.json
+      if grep 4000 *.json; then exit 1; fi
+
+      # Change all ports from 1909X to 909X, because the 1909X range of ports is
+      # allocated by startservers.py in order to intercept gRPC communication.
+      sed -i -e 's/\<1\(909[0-9]\)\>/\1/' *.json
+
+      # Patch out all additional issuer certs
+      jq '. + {ca: (.ca + {Issuers:
+        [.ca.Issuers[] | select(.CertFile == "test/test-ca.pem")]
+      })}' ca.json > tmp
+      mv tmp ca.json
+    '';
+    installPhase = "cp -r . \"$out\"";
+  };
+
+  components = {
+    gsb-test-srv.args = "-apikey my-voice-is-my-passport";
+    gsb-test-srv.waitForPort = 6000;
+    gsb-test-srv.first = true;
+    boulder-sa.args = "--config ${cfgDir}/sa.json";
+    boulder-wfe.args = "--config ${cfgDir}/wfe.json";
+    boulder-ra.args = "--config ${cfgDir}/ra.json";
+    boulder-ca.args = "--config ${cfgDir}/ca.json";
+    boulder-va.args = "--config ${cfgDir}/va.json";
+    boulder-publisher.args = "--config ${cfgDir}/publisher.json";
+    boulder-publisher.waitForPort = 9091;
+    ocsp-updater.args = "--config ${cfgDir}/ocsp-updater.json";
+    ocsp-updater.after = [ "boulder-publisher" ];
+    ocsp-responder.args = "--config ${cfgDir}/ocsp-responder.json";
+    ct-test-srv = {};
+    mail-test-srv.args = let
+      key = "${boulderSource}/test/mail-test-srv/minica-key.pem";
+      crt = "${boulderSource}/test/mail-test-srv/minica.pem";
+     in
+      "--closeFirst 5 --cert ${crt} --key ${key}";
+  };
+
+  commonPath = [ softhsm pkgs.mariadb goose boulder ];
+
+  mkServices = a: b: with lib; listToAttrs (concatLists (mapAttrsToList a b));
+
+  componentServices = mkServices (name: attrs: let
+    mkSrvName = n: "boulder-${n}.service";
+    firsts = lib.filterAttrs (lib.const (c: c.first or false)) components;
+    firstServices = map mkSrvName (lib.attrNames firsts);
+    firstServicesNoSelf = lib.remove "boulder-${name}.service" firstServices;
+    additionalAfter = firstServicesNoSelf ++ map mkSrvName (attrs.after or []);
+    needsPort = attrs ? waitForPort;
+    inits = map (n: "boulder-init-${n}.service") [ "mysql" "softhsm" ];
+    portWaiter = {
+      name = "boulder-${name}";
+      value = {
+        description = "Wait For Port ${toString attrs.waitForPort} (${name})";
+        after = [ "boulder-real-${name}.service" "bind.service" ];
+        requires = [ "boulder-real-${name}.service" ];
+        requiredBy = [ "boulder.service" ];
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        script = let
+          netcat = "${pkgs.libressl.nc}/bin/nc";
+          portCheck = "${netcat} -z 127.0.0.1 ${toString attrs.waitForPort}";
+        in "while ! ${portCheck}; do :; done";
+      };
+    };
+  in lib.optional needsPort portWaiter ++ lib.singleton {
+    name = if needsPort then "boulder-real-${name}" else "boulder-${name}";
+    value = {
+      description = "Boulder ACME Component (${name})";
+      after = inits ++ additionalAfter;
+      requires = inits;
+      requiredBy = [ "boulder.service" ];
+      path = commonPath;
+      environment.GORACE = "halt_on_error=1";
+      environment.SOFTHSM_CONF = softHsmConf;
+      environment.PKCS11_PROXY_SOCKET = "tcp://127.0.0.1:5657";
+      serviceConfig.WorkingDirectory = boulderSource;
+      serviceConfig.ExecStart = "${boulder}/bin/${name} ${attrs.args or ""}";
+      serviceConfig.Restart = "on-failure";
+    };
+  }) components;
+
+in {
+  imports = [ ../resolver.nix ];
+
+  options.test-support.letsencrypt.caCert = lib.mkOption {
+    type = lib.types.path;
+    description = ''
+      A certificate file to use with the <literal>nodes</literal> attribute to
+      inject the snakeoil CA certificate used in the ACME server into
+      <option>security.pki.certificateFiles</option>.
+    '';
+  };
+
+  config = {
+    test-support = {
+      resolver.enable = let
+        isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ];
+      in lib.mkOverride 900 isLocalResolver;
+      letsencrypt.caCert = snakeOilCerts.ca.cert;
+    };
+
+    # This has priority 140, because modules/testing/test-instrumentation.nix
+    # already overrides this with priority 150.
+    networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ];
+    networking.firewall.enable = false;
+
+    networking.extraHosts = ''
+      127.0.0.1 ${toString [
+        "sa.boulder" "ra.boulder" "wfe.boulder" "ca.boulder" "va.boulder"
+        "publisher.boulder" "ocsp-updater.boulder" "admin-revoker.boulder"
+        "boulder" "boulder-mysql" wfeDomain
+      ]}
+      ${config.networking.primaryIPAddress} ${wfeDomain} ${siteDomain}
+    '';
+
+    services.mysql.enable = true;
+    services.mysql.package = pkgs.mariadb;
+
+    services.nginx.enable = true;
+    services.nginx.recommendedProxySettings = true;
+    # This fixes the test on i686
+    services.nginx.commonHttpConfig = ''
+      server_names_hash_bucket_size 64;
+    '';
+    services.nginx.virtualHosts.${wfeDomain} = {
+      onlySSL = true;
+      enableACME = false;
+      sslCertificate = wfeCertFile;
+      sslCertificateKey = wfeKeyFile;
+      locations."/".proxyPass = "http://127.0.0.1:80";
+    };
+    services.nginx.virtualHosts.${siteDomain} = {
+      onlySSL = true;
+      enableACME = false;
+      sslCertificate = siteCertFile;
+      sslCertificateKey = siteKeyFile;
+      locations.${tosPath}.extraConfig = "alias ${tosFile};";
+    };
+
+    systemd.services = {
+      pkcs11-daemon = {
+        description = "PKCS11 Daemon";
+        after = [ "boulder-init-softhsm.service" ];
+        before = map (n: "${n}.service") (lib.attrNames componentServices);
+        wantedBy = [ "multi-user.target" ];
+        environment.SOFTHSM_CONF = softHsmConf;
+        environment.PKCS11_DAEMON_SOCKET = "tcp://127.0.0.1:5657";
+        serviceConfig.ExecStart = let
+          softhsmLib = "${softhsm}/lib/softhsm/libsofthsm.so";
+        in "${pkcs11-proxy}/bin/pkcs11-daemon ${softhsmLib}";
+      };
+
+      boulder-init-mysql = {
+        description = "Boulder ACME Init (MySQL)";
+        after = [ "mysql.service" ];
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        serviceConfig.WorkingDirectory = boulderSource;
+        path = commonPath;
+        script = "${pkgs.bash}/bin/sh test/create_db.sh";
+      };
+
+      boulder-init-softhsm = {
+        description = "Boulder ACME Init (SoftHSM)";
+        environment.SOFTHSM_CONF = softHsmConf;
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        serviceConfig.WorkingDirectory = boulderSource;
+        preStart = "mkdir -p /var/lib/softhsm";
+        path = commonPath;
+        script = ''
+          softhsm --slot 0 --init-token \
+            --label intermediate --pin 5678 --so-pin 1234
+          softhsm --slot 0 --import test/test-ca.key \
+            --label intermediate_key --pin 5678 --id FB
+          softhsm --slot 1 --init-token \
+            --label root --pin 5678 --so-pin 1234
+          softhsm --slot 1 --import test/test-root.key \
+            --label root_key --pin 5678 --id FA
+        '';
+      };
+
+      boulder = {
+        description = "Boulder ACME Server";
+        after = map (n: "${n}.service") (lib.attrNames componentServices);
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        script = let
+          ports = lib.range 8000 8005 ++ lib.singleton 80;
+          netcat = "${pkgs.libressl.nc}/bin/nc";
+          mkPortCheck = port: "${netcat} -z 127.0.0.1 ${toString port}";
+          checks = "(${lib.concatMapStringsSep " && " mkPortCheck ports})";
+        in "while ! ${checks}; do :; done";
+      };
+    } // componentServices;
+  };
+}
diff --git a/nixpkgs/nixos/tests/common/letsencrypt/mkcerts.nix b/nixpkgs/nixos/tests/common/letsencrypt/mkcerts.nix
new file mode 100644
index 000000000000..3b4a589e4142
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/letsencrypt/mkcerts.nix
@@ -0,0 +1,69 @@
+{ pkgs ? import <nixpkgs> {}
+, lib ? pkgs.lib
+
+, domains ? [ "acme-v01.api.letsencrypt.org" "letsencrypt.org" ]
+}:
+
+pkgs.runCommand "letsencrypt-snakeoil-ca" {
+  nativeBuildInputs = [ pkgs.openssl ];
+} ''
+  addpem() {
+    local file="$1"; shift
+    local storeFileName="$(IFS=.; echo "$*")"
+
+    echo -n "  " >> "$out"
+
+    # Every following argument is an attribute, so let's recurse and check
+    # every attribute whether it must be quoted and write it into $out.
+    while [ -n "$1" ]; do
+      if expr match "$1" '^[a-zA-Z][a-zA-Z0-9]*$' > /dev/null; then
+        echo -n "$1" >> "$out"
+      else
+        echo -n '"' >> "$out"
+        echo -n "$1" | sed -e 's/["$]/\\&/g' >> "$out"
+        echo -n '"' >> "$out"
+      fi
+      shift
+      [ -z "$1" ] || echo -n . >> "$out"
+    done
+
+    echo " = builtins.toFile \"$storeFileName\" '''" >> "$out"
+    sed -e 's/^/    /' "$file" >> "$out"
+
+    echo "  ''';" >> "$out"
+  }
+
+  echo '# Generated via mkcert.sh in the same directory.' > "$out"
+  echo '{' >> "$out"
+
+  openssl req -newkey rsa:4096 -x509 -sha256 -days 36500 \
+    -subj '/CN=Snakeoil CA' -nodes -out ca.pem -keyout ca.key
+
+  addpem ca.key ca key
+  addpem ca.pem ca cert
+
+  ${lib.concatMapStrings (fqdn: let
+    opensslConfig = pkgs.writeText "snakeoil.cnf" ''
+      [req]
+      default_bits = 4096
+      prompt = no
+      default_md = sha256
+      req_extensions = req_ext
+      distinguished_name = dn
+      [dn]
+      CN = ${fqdn}
+      [req_ext]
+      subjectAltName = DNS:${fqdn}
+    '';
+  in ''
+    export OPENSSL_CONF=${lib.escapeShellArg opensslConfig}
+    openssl genrsa -out snakeoil.key 4096
+    openssl req -new -key snakeoil.key -out snakeoil.csr
+    openssl x509 -req -in snakeoil.csr -sha256 -set_serial 666 \
+      -CA ca.pem -CAkey ca.key -out snakeoil.pem -days 36500
+    addpem snakeoil.key ${lib.escapeShellArg fqdn} key
+    addpem snakeoil.pem ${lib.escapeShellArg fqdn} cert
+  '') domains}
+
+  echo '}' >> "$out"
+''
diff --git a/nixpkgs/nixos/tests/common/letsencrypt/mkcerts.sh b/nixpkgs/nixos/tests/common/letsencrypt/mkcerts.sh
new file mode 100755
index 000000000000..cc7f8ca650dd
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/letsencrypt/mkcerts.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -p nix bash -i bash
+set -e
+cd "$(dirname "$0")"
+storepath="$(nix-build --no-out-link mkcerts.nix)"
+cat "$storepath" > snakeoil-certs.nix
diff --git a/nixpkgs/nixos/tests/common/letsencrypt/snakeoil-certs.nix b/nixpkgs/nixos/tests/common/letsencrypt/snakeoil-certs.nix
new file mode 100644
index 000000000000..c3d29ab8f163
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/letsencrypt/snakeoil-certs.nix
@@ -0,0 +1,253 @@
+# Generated via mkcert.sh in the same directory.
+{
+  ca.key = builtins.toFile "ca.key" ''
+    -----BEGIN PRIVATE KEY-----
+    MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDfdVxC/4HwhuzD
+    9or9CDDu3TBQE5lirJI5KYmfMZtfgdzEjgOzmR9AVSkn2rQeCqzM5m+YCzPO+2y7
+    0Fdk7vDORi1OdhYfUQIW6/TZ27xEjx4t82j9i705yUqTJZKjMbD830geXImJ6VGj
+    Nv/WisTHmwBspWKefYQPN68ZvYNCn0d5rYJg9uROZPJHSI0MYj9iERWIPN+xhZoS
+    xN74ILJ0rEOQfx2GHDhTr99vZYAFqbAIfh35fYulRWarUSekI+rDxa83FD8q9cMg
+    OP84KkLep2dRXXTbUWErGUOpHP55M9M7ws0RVNdl9PUSbDgChl7yYlHCde3261q/
+    zGp5dMV/t/jXXNUgRurvXc4gUKKjS4Sffvg0XVnPs3sMlZ4JNmycK9klgISVmbTK
+    VcjRRJv8Bva2NQVsJ9TIryV0QEk94DucgsC3LbhQfQdmnWVcEdzwrZHNpk9az5mn
+    w42RuvZW9L19T7xpIrdLSHaOis4VEquZjkWIhfIz0DVMeXtYEQmwqFG23Ww0utcp
+    mCW4FPvpyYs5GAPmGWfrlMxsLD/7eteot3AheC+56ZBoVBnI8FFvIX2qci+gfVDu
+    CjvDmbyS/0NvxLGqvSC1GUPmWP3TR5Fb1H8Rp+39zJHRmH+qYWlhcv6p7FlY2/6d
+    9Rkw8WKRTSCB7yeUdNNPiPopk6N4NwIDAQABAoICAQCzV0ei5dntpvwjEp3eElLj
+    glYiDnjOPt5kTjgLsg6XCmyau7ewzrXMNgz/1YE1ky+4i0EI8AS2nAdafQ2HDlXp
+    11zJWfDLVYKtztYGe1qQU6TPEEo1I4/M7waRLliP7XO0n6cL5wzjyIQi0CNolprz
+    8CzZBasutGHmrLQ1nmnYcGk2+NBo7f2yBUaFe27of3mLRVbYrrKBkU5kveiNkABp
+    r0/SipKxbbivQbm7d+TVpqiHSGDaOa54CEksOcfs7n6efOvw8qj326KtG9GJzDE6
+    7XP4U19UHe40XuR0t7Zso/FmRyO6QzNUutJt5LjXHezZ75razTcdMyr0QCU8MUHH
+    jXZxQCsbt+9AmdxUMBm1SMNVBdHYM8oiNHynlgsEj9eM6jxDEss/Uc3FeKoHl+XL
+    L6m28guIB8NivqjVzZcwhxvdiQCzYxjyqMC+/eX7aaK4NIlX2QRMoDL6mJ58Bz/8
+    V2Qxp2UNVwKJFWAmpgXC+sq6XV/TP3HkOvd0OK82Nid2QxEvfE/EmOhU63qAjgUR
+    QnteLEcJ3MkGGurs05pYBDE7ejKVz6uu2tHahFMOv+yanGP2gfivnT9a323/nTqH
+    oR5ffMEI1u/ufpWU7sWXZfL/mH1L47x87k+9wwXHCPeSigcy+hFI7t1+rYsdCmz9
+    V6QtmxZHMLanwzh5R0ipcQKCAQEA8kuZIz9JyYP6L+5qmIUxiWESihVlRCSKIqLB
+    fJ5sQ06aDBV2sqS4XnoWsHuJWUd39rulks8cg8WIQu8oJwVkFI9EpARt/+a1fRP0
+    Ncc9qiBdP6VctQGgKfe5KyOfMzIBUl3zj2cAmU6q+CW1OgdhnEl4QhgBe5XQGquZ
+    Alrd2P2jhJbMO3sNFgzTy7xPEr3KqUy+L4gtRnGOegKIh8EllmsyMRO4eIrZV2z3
+    XI+S2ZLyUn3WHYkaJqvUFrbfekgBBmbk5Ead6ImlsLsBla6MolKrVYV1kN6KT+Y+
+    plcxNpWY8bnWfw5058OWPLPa9LPfReu9rxAeGT2ZLmAhSkjGxQKCAQEA7BkBzT3m
+    SIzop9RKl5VzYbVysCYDjFU9KYMW5kBIw5ghSMnRmU7kXIZUkc6C1L/v9cTNFFLw
+    ZSF4vCHLdYLmDysW2d4DU8fS4qdlDlco5A00g8T1FS7nD9CzdkVN/oix6ujw7RuI
+    7pE1K3JELUYFBc8AZ7mIGGbddeCwnM+NdPIlhWzk5s4x4/r31cdk0gzor0kE4e+d
+    5m0s1T4O/Iak6rc0MGDeTejZQg04p1eAJFYQ6OY23tJhH/kO8CMYnQ4fidfCkf8v
+    85v4EC1MCorFR7J65uSj8MiaL7LTXPvLAkgFls1c3ijQ2tJ8qXvqmfo0by33T1OF
+    ZGyaOP9/1WQSywKCAQB47m6CfyYO5EZNAgxGD8SHsuGT9dXTSwF/BAjacB/NAEA2
+    48eYpko3LWyBrUcCPn+LsGCVg7XRtxepgMBjqXcoI9G4o1VbsgTHZtwus0D91qV0
+    DM7WsPcFu1S6SU8+OCkcuTPFUT2lRvRiYj+vtNttK+ZP5rdmvYFermLyH/Q2R3ID
+    zVgmH+aKKODVASneSsgJ8/nAs5EVZbwc/YKzbx2Zk+s7P4KE95g+4G4dzrMW0RcN
+    QS1LFJDu2DhFFgU4fRO15Ek9/lj2JS2DpfLGiJY8tlI5nyDsq4YRFvQSBdbUTZpG
+    m+CJDegffSlRJtuT4ur/dQf5hmvfYTVBRk2XS/eZAoIBAB143a22PWnvFRfmO02C
+    3X1j/iYZCLZa6aCl+ZTSj4LDGdyRPPXrUDxwlFwDMHfIYfcHEyanV9T4Aa9SdKh9
+    p6RbF6YovbeWqS+b/9RzcupM77JHQuTbDwL9ZXmtGxhcDgGqBHFEz6ogPEfpIrOY
+    GwZnmcBY+7E4HgsZ+lII4rqng6GNP2HEeZvg91Eba+2AqQdAkTh3Bfn+xOr1rT8+
+    u5WFOyGS5g1JtN0280yIcrmWeNPp8Q2Nq4wnNgMqDmeEnNFDOsmo1l6NqMC0NtrW
+    CdxyXj82aXSkRgMQSqw/zk7BmNkDV8VvyOqX/fHWQynnfuYmEco4Pd2UZQgadOW5
+    cVMCggEBANGz1fC+QQaangUzsVNOJwg2+CsUFYlAKYA3pRKZPIyMob2CBXk3Oln/
+    YqOq6j373kG2AX74EZT07JFn28F27JF3r+zpyS/TYrfZyO1lz/5ZejPtDTmqBiVd
+    qa2coaPKwCOz64s77A9KSPyvpvyuTfRVa8UoArHcrQsPXMHgEhnFRsbxgmdP582A
+    kfYfoJBSse6dQtS9ZnREJtyWJlBNIBvsuKwzicuIgtE3oCBcIUZpEa6rBSN7Om2d
+    ex8ejCcS7qpHeULYspXbm5ZcwE4glKlQbJDTKaJ9mjiMdvuNFUZnv1BdMQ3Tb8zf
+    Gvfq54FbDuB10XP8JdLrsy9Z6GEsmoE=
+    -----END PRIVATE KEY-----
+  '';
+  ca.cert = builtins.toFile "ca.cert" ''
+    -----BEGIN CERTIFICATE-----
+    MIIFATCCAumgAwIBAgIJANydi4uFZr0LMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
+    BAMMC1NuYWtlb2lsIENBMCAXDTE4MDcxMjAwMjIxNloYDzIxMTgwNjE4MDAyMjE2
+    WjAWMRQwEgYDVQQDDAtTbmFrZW9pbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+    ADCCAgoCggIBAN91XEL/gfCG7MP2iv0IMO7dMFATmWKskjkpiZ8xm1+B3MSOA7OZ
+    H0BVKSfatB4KrMzmb5gLM877bLvQV2Tu8M5GLU52Fh9RAhbr9NnbvESPHi3zaP2L
+    vTnJSpMlkqMxsPzfSB5ciYnpUaM2/9aKxMebAGylYp59hA83rxm9g0KfR3mtgmD2
+    5E5k8kdIjQxiP2IRFYg837GFmhLE3vggsnSsQ5B/HYYcOFOv329lgAWpsAh+Hfl9
+    i6VFZqtRJ6Qj6sPFrzcUPyr1wyA4/zgqQt6nZ1FddNtRYSsZQ6kc/nkz0zvCzRFU
+    12X09RJsOAKGXvJiUcJ17fbrWr/Manl0xX+3+Ndc1SBG6u9dziBQoqNLhJ9++DRd
+    Wc+zewyVngk2bJwr2SWAhJWZtMpVyNFEm/wG9rY1BWwn1MivJXRAST3gO5yCwLct
+    uFB9B2adZVwR3PCtkc2mT1rPmafDjZG69lb0vX1PvGkit0tIdo6KzhUSq5mORYiF
+    8jPQNUx5e1gRCbCoUbbdbDS61ymYJbgU++nJizkYA+YZZ+uUzGwsP/t616i3cCF4
+    L7npkGhUGcjwUW8hfapyL6B9UO4KO8OZvJL/Q2/Esaq9ILUZQ+ZY/dNHkVvUfxGn
+    7f3MkdGYf6phaWFy/qnsWVjb/p31GTDxYpFNIIHvJ5R000+I+imTo3g3AgMBAAGj
+    UDBOMB0GA1UdDgQWBBQ3vPWzjLmu5krbSpfhBAht9KL3czAfBgNVHSMEGDAWgBQ3
+    vPWzjLmu5krbSpfhBAht9KL3czAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
+    A4ICAQDF9HyC1ZFN3Ob+JA9Dj5+Rcobi7JIA5F8uW3Q92LfPoVaUGEkBrwJSiTFX
+    47zvP/ySBJIpZ9rzHMbJ+1L+eJgczF1uQ91inthCKo1THTPo5TgBrpJj0YAIunsj
+    9eH1tBnfWFYdVIDZoTSiwPtgIvglpyuK/eJXEe+FRzubhtdc9w1Hlzox1sd0TQuy
+    Pl9KFHg7BlFZfCPig1mkB8pfwjBDgVhv5DKJ9cJXh3R5zSoiyuS2b+qYSvw8YTHq
+    0WNKWUthb7BVAYE3OmcbOHgUAUjtJ6EIGIB9z/SoLe90CofXLXFR5dppuVLKCMBA
+    kgL4luBIu7t8mcnN2yzobvcGHy8RVY6F5abCCy6gackLzjOzvH1SYOxP8yN74aKB
+    ANgcqdWspb8JYoU8lEbA8dhBVrsgBf7XeJlrZvMdcUENlJ2PI0JWr9WvlRAM9rYY
+    EY1alJqBCp6530Ggd6/f0V64cEqptejUdmN9L0zboxKjQf4LjpUNraGvg8tw/xkY
+    4dT1U2HlVnhOyBVkx/tE6zIK/RU16oMqwpjCdfbK/TuWCNc/emJz5PMlp81zm83+
+    dExpWwuV4rt6OQbZ/GSatNLJXOw+pkLjaEhnHgrsgI+HqAUXg3ByKol+1e76wN51
+    k1ZKpB6mk4kejySGPYBHiJwED0IyXu9gUfalSczXFO4ySAvhCg==
+    -----END CERTIFICATE-----
+  '';
+  "acme-v01.api.letsencrypt.org".key = builtins.toFile "acme-v01.api.letsencrypt.org.key" ''
+    -----BEGIN RSA PRIVATE KEY-----
+    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
+    -----END RSA PRIVATE KEY-----
+  '';
+  "acme-v01.api.letsencrypt.org".cert = builtins.toFile "acme-v01.api.letsencrypt.org.cert" ''
+    -----BEGIN CERTIFICATE-----
+    MIIEtDCCApwCAgKaMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNVBAMMC1NuYWtlb2ls
+    IENBMCAXDTE4MDcxMjAwMjIxN1oYDzIxMTgwNjE4MDAyMjE3WjAnMSUwIwYDVQQD
+    DBxhY21lLXYwMS5hcGkubGV0c2VuY3J5cHQub3JnMIICIjANBgkqhkiG9w0BAQEF
+    AAOCAg8AMIICCgKCAgEAvG+sL4q0VkgSClBTn4NkPiUrtXx5oLyZ+CCM1jrQx/xo
+    tUt5X2S4/7vMnAK/yRLsR7R2PhXO8CZPqJ7B6OfAgaDTgvipJkZYPZQSMP3KOinM
+    3WJLssqKh7/HOxZIf0iyUXewrnX5eTAo/CLsUnhBjBD7E99nmQz/leLWSl82sSYD
+    kO3nUk3/1qJZA8iddb4uH0IEQWcNKev3WoQQzwiVrXBiftlRQOJy5JJXm5m8229M
+    CpMA1AUWmpdu6sl3/gFFdsDhUFq/a7LFrVyaUCMRIHg9szAB7ZFkixr9umQs8jKw
+    uo983JHB11h2SirwgfIzHHmyhaWhCt22ucTwEXGhq63LtrzZvLsfP8Ql5S+AuqGT
+    H0v8meuc784leAjulBZjkpuIFwDnVv9+YeUEbqJeo1hSHrILddora3nkH4E2dJWm
+    LpqpiPr++GRi+BNgYKW/BQLTJ7C6v+vUs+kdPgYJH5z7oP6f0YZkT0Wkubp/UEz7
+    UV2dfjz57d77DYx5rFWGYzJriWR/xltgL1zDpjwjwG1FDpRqwlyYbBFpjQhxI+X0
+    aT98m6fCzBDQHDb/+JgvsjTHh6OZatahFAwzFIEfrceDv1BG8sBWIaZGhLzYiWQx
+    afl8oXbWv1T6I1jpsTlCdCSkWzaJb4ZjxI9Ga1ynVu8F16+GR2a71wKWu7UbZQsC
+    AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAzeGlFMz1Bo+bbpZDQ60HLdw7qDp3SPJi
+    x5LYG860yzbh9ghvyc59MIm5E6vB140LRJAs+Xo6VdVSTC4jUA2kI9k1BQsbZKds
+    XT0RqA7HkqcLS3t3JWFkkKbCshMGZTSZ//hpbaUG1qEAfUfmZw1lAxqSa0kqavbP
+    awf7k8qHbqcj7WORCdH7fjKAjntEQwIpl1GEkAdCSghOJz2/o9aWmiGZt27OM/sG
+    MLSrcmL3QBElCjOxg14P8rnsmZ+VEp6MO93otoJ4dJL7fN7vTIh5ThbS384at/4l
+    4KK/y7XctUzAtWzhnodjk/NSgrrGX2kseOGOWEM1sZc9xtinHH2tpOMqtLVOkgHD
+    Lul+TArqgqeoOdEM/9OL64kgOrO/JzxBq+egLUi4wgAul2wmtecKZK1dkwYZHeqW
+    74i55yeBp+TTomnPr0ZBns6xKFYldJVzC34OB+2YVDxe8y9XtWtuQOxFw0LQHhNb
+    zy5aBverWzZFwiIIjJoVHTQq848uKBJec0YILfMinS1Wjif4xqW/IMfi+GFS0oka
+    sKCGNE/8ur9u/Jm6cbto3f2dtV8/vkhiITQgwzM2jalyuVJ9jyPxG7EvbTvZORgw
+    pRvBRTd4/eE7I1L+UDe6x8EjR/MrqfF9FWVGOZo4vPTyNbrSWYBh6s9kYy56ds1l
+    IRxst1BXEfI=
+    -----END CERTIFICATE-----
+  '';
+  "letsencrypt.org".key = builtins.toFile "letsencrypt.org.key" ''
+    -----BEGIN RSA PRIVATE KEY-----
+    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
+    -----END RSA PRIVATE KEY-----
+  '';
+  "letsencrypt.org".cert = builtins.toFile "letsencrypt.org.cert" ''
+    -----BEGIN CERTIFICATE-----
+    MIIEpzCCAo8CAgKaMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNVBAMMC1NuYWtlb2ls
+    IENBMCAXDTE4MDcxMjAwMjIxOVoYDzIxMTgwNjE4MDAyMjE5WjAaMRgwFgYDVQQD
+    DA9sZXRzZW5jcnlwdC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+    AQDA++GXB6aA+Lr3X2xIPs/PqXoJF9TUb98NzQC+ww3+dOaIY0Omqf15/UID5G0u
+    0649zUsvISi9x+7vn9X7opKA7iTX0TYKsUIXCMQ5YMYXgOByVPfvnVYaYUmDM8R9
+    l+Fs6uZB9Bq7zorEC01lWn0XFwpu4vJD+w03F80wH7sgdtWHC/NVyd0elhkQ/qR2
+    fC80s/bZui5fmrdq/FZK2WfTW5nQF9SbhfDFpQ0hTu3QO/noUr0L2Fb0Blp7R2jQ
+    EkFZEXvFLAt4Mmqf25nP6xfRf+0hppJBIG5nJKPQd7lkrN1Q7S2nHbFNLYGuXdjY
+    E4lo4T3we5ta90qiyUu3hfatWszEZrzpKYXNzIMvVFKjBw/+u7LLnnmXul6kvmzh
+    yYyBEEFePm1eGpRNU6flY74kKUuT6u2+0BPiuiAmYyoQqYzDa3ss3LVb2uL/fvxA
+    b+LcAZosgv/saw9u5E7nVJ22LhAxV7bM0XN7vabM2aPVJeiRbLv5m+NQynnmgSNF
+    GxM9Rl004QclbLo6zbOe6ovtmkgcLEEbwUqeQJWJdEmoADxvNwSFH6ktqYsg4eW/
+    iR3MnqmEeRK8rryq9tFF5SY6MmBK6lUEj2OEr58drL9RZHjN8lB5330fxk6iJeWs
+    sJWz5U8PKdRQqvMXWdjKiU3OL81Lh7gBud9aDILkkpGmNQIDAQABMA0GCSqGSIb3
+    DQEBCwUAA4ICAQAkx3jcryukAuYP7PQxMy3LElOl65ZFVqxDtTDlr7DvAkWJzVCb
+    g08L6Tu+K0rKh2RbG/PqS0+8/jBgc4IwSOPfDDAX+sinfj0kwXG34WMzB0G3fQzU
+    2BMplJDOaBcNqHG8pLP1BG+9HAtR/RHe9p2Jw8LG2qmZs6uemPT/nCTNoyIL4oxh
+    UncjETV4ayCHDKD1XA7/icgddYsnfLQHWuIMuCrmQCHo0uQAd7qVHfUWZ+gcsZx0
+    jTNCcaI8OTS2S65Bjaq2HaM7GMcUYNUD2vSyNQeQbha4ZeyZ9bPyFzznPMmrPXQe
+    MJdkbJ009RQIG9As79En4m+l+/6zrdx4DNdROqaL6YNiSebWMnuFHpMW/rCnhrT/
+    HYadijHOiJJGj9tWSdC4XJs7fvZW3crMPUYxpOvl01xW2ZlgaekILi1FAjSMQVoV
+    NhWstdGCKJdthJqLL5MtNdfgihKcmgkJqKFXTkPv7sgAQCopu6X+S+srCgn856Lv
+    21haRWZa8Ml+E0L/ticT8Fd8Luysc6K9TJ4mT8ENC5ywvgDlEkwBD3yvINXm5lg1
+    xOIxv/Ye5gFk1knuM7OzpUFBrXUHdVVxflCUqNAhFPbcXwjgEQ+A+S5B0vI6Ohue
+    ZnR/wuiou6Y+Yzh8XfqL/3H18mGDdjyMXI1B6l4Judk000UVyr46cnI7mw==
+    -----END CERTIFICATE-----
+  '';
+}
diff --git a/nixpkgs/nixos/tests/common/resolver.nix b/nixpkgs/nixos/tests/common/resolver.nix
new file mode 100644
index 000000000000..6be8d1d18e62
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/resolver.nix
@@ -0,0 +1,141 @@
+# This module automatically discovers zones in BIND and NSD NixOS
+# configurations and creates zones for all definitions of networking.extraHosts
+# (except those that point to 127.0.0.1 or ::1) within the current test network
+# and delegates these zones using a fake root zone served by a BIND recursive
+# name server.
+{ config, nodes, pkgs, lib, ... }:
+
+{
+  options.test-support.resolver.enable = lib.mkOption {
+    type = lib.types.bool;
+    default = true;
+    internal = true;
+    description = ''
+      Whether to enable the resolver that automatically discovers zone in the
+      test network.
+
+      This option is <literal>true</literal> by default, because the module
+      defining this option needs to be explicitly imported.
+
+      The reason this option exists is for the
+      <filename>nixos/tests/common/letsencrypt</filename> module, which
+      needs that option to disable the resolver once the user has set its own
+      resolver.
+    '';
+  };
+
+  config = lib.mkIf config.test-support.resolver.enable {
+    networking.firewall.enable = false;
+    services.bind.enable = true;
+    services.bind.cacheNetworks = lib.mkForce [ "any" ];
+    services.bind.forwarders = lib.mkForce [];
+    services.bind.zones = lib.singleton {
+      name = ".";
+      file = let
+        addDot = zone: zone + lib.optionalString (!lib.hasSuffix "." zone) ".";
+        mkNsdZoneNames = zones: map addDot (lib.attrNames zones);
+        mkBindZoneNames = zones: map (zone: addDot zone.name) zones;
+        getZones = cfg: mkNsdZoneNames cfg.services.nsd.zones
+                     ++ mkBindZoneNames cfg.services.bind.zones;
+
+        getZonesForNode = attrs: {
+          ip = attrs.config.networking.primaryIPAddress;
+          zones = lib.filter (zone: zone != ".") (getZones attrs.config);
+        };
+
+        zoneInfo = lib.mapAttrsToList (lib.const getZonesForNode) nodes;
+
+        # A and AAAA resource records for all the definitions of
+        # networking.extraHosts except those for 127.0.0.1 or ::1.
+        #
+        # The result is an attribute set with keys being the host name and the
+        # values are either { ipv4 = ADDR; } or { ipv6 = ADDR; } where ADDR is
+        # the IP address for the corresponding key.
+        recordsFromExtraHosts = let
+          getHostsForNode = lib.const (n: n.config.networking.extraHosts);
+          allHostsList = lib.mapAttrsToList getHostsForNode nodes;
+          allHosts = lib.concatStringsSep "\n" allHostsList;
+
+          reIp = "[a-fA-F0-9.:]+";
+          reHost = "[a-zA-Z0-9.-]+";
+
+          matchAliases = str: let
+            matched = builtins.match "[ \t]+(${reHost})(.*)" str;
+            continue = lib.singleton (lib.head matched)
+                    ++ matchAliases (lib.last matched);
+          in if matched == null then [] else continue;
+
+          matchLine = str: let
+            result = builtins.match "[ \t]*(${reIp})[ \t]+(${reHost})(.*)" str;
+          in if result == null then null else {
+            ipAddr = lib.head result;
+            hosts = lib.singleton (lib.elemAt result 1)
+                 ++ matchAliases (lib.last result);
+          };
+
+          skipLine = str: let
+            rest = builtins.match "[^\n]*\n(.*)" str;
+          in if rest == null then "" else lib.head rest;
+
+          getEntries = str: acc: let
+            result = matchLine str;
+            next = getEntries (skipLine str);
+            newEntry = acc ++ lib.singleton result;
+            continue = if result == null then next acc else next newEntry;
+          in if str == "" then acc else continue;
+
+          isIPv6 = str: builtins.match ".*:.*" str != null;
+          loopbackIps = [ "127.0.0.1" "::1" ];
+          filterLoopback = lib.filter (e: !lib.elem e.ipAddr loopbackIps);
+
+          allEntries = lib.concatMap (entry: map (host: {
+            inherit host;
+            ${if isIPv6 entry.ipAddr then "ipv6" else "ipv4"} = entry.ipAddr;
+          }) entry.hosts) (filterLoopback (getEntries (allHosts + "\n") []));
+
+          mkRecords = entry: let
+            records = lib.optional (entry ? ipv6) "AAAA ${entry.ipv6}"
+                   ++ lib.optional (entry ? ipv4) "A ${entry.ipv4}";
+            mkRecord = typeAndData: "${entry.host}. IN ${typeAndData}";
+          in lib.concatMapStringsSep "\n" mkRecord records;
+
+        in lib.concatMapStringsSep "\n" mkRecords allEntries;
+
+        # All of the zones that are subdomains of existing zones.
+        # For example if there is only "example.com" the following zones would
+        # be 'subZones':
+        #
+        #  * foo.example.com.
+        #  * bar.example.com.
+        #
+        # While the following would *not* be 'subZones':
+        #
+        #  * example.com.
+        #  * com.
+        #
+        subZones = let
+          allZones = lib.concatMap (zi: zi.zones) zoneInfo;
+          isSubZoneOf = z1: z2: lib.hasSuffix z2 z1 && z1 != z2;
+        in lib.filter (z: lib.any (isSubZoneOf z) allZones) allZones;
+
+        # All the zones without 'subZones'.
+        filteredZoneInfo = map (zi: zi // {
+          zones = lib.filter (x: !lib.elem x subZones) zi.zones;
+        }) zoneInfo;
+
+      in pkgs.writeText "fake-root.zone" ''
+        $TTL 3600
+        . IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d )
+        ns.fakedns. IN A ${config.networking.primaryIPAddress}
+        . IN NS ns.fakedns.
+        ${lib.concatImapStrings (num: { ip, zones }: ''
+          ns${toString num}.fakedns. IN A ${ip}
+          ${lib.concatMapStrings (zone: ''
+          ${zone} IN NS ns${toString num}.fakedns.
+          '') zones}
+        '') (lib.filter (zi: zi.zones != []) filteredZoneInfo)}
+        ${recordsFromExtraHosts}
+      '';
+    };
+  };
+}
diff --git a/nixpkgs/nixos/tests/common/user-account.nix b/nixpkgs/nixos/tests/common/user-account.nix
new file mode 100644
index 000000000000..9cd531a1f96c
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/user-account.nix
@@ -0,0 +1,14 @@
+{ ... }:
+
+{ users.users.alice =
+    { isNormalUser = true;
+      description = "Alice Foobar";
+      password = "foobar";
+    };
+
+  users.users.bob =
+    { isNormalUser = true;
+      description = "Bob Foobar";
+      password = "foobar";
+    };
+}
diff --git a/nixpkgs/nixos/tests/common/webroot/news-rss.xml b/nixpkgs/nixos/tests/common/webroot/news-rss.xml
new file mode 100644
index 000000000000..28e6fa7da1f3
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/webroot/news-rss.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:blogChannel="http://backend.userland.com/blogChannelModule" version="2.0"><channel><title>NixOS News</title><link>https://nixos.org</link><description>News for NixOS, the purely functional Linux distribution.</description><image><title>NixOS</title><url>https://nixos.org/logo/nixos-logo-only-hires.png</url><link>https://nixos.org/</link></image><item><title>
+      NixOS 18.09 released
+    </title><link>https://nixos.org/news.html</link><description>
+      <a href="https://github.com/NixOS/nixos-artwork/blob/master/releases/18.09-jellyfish/jellyfish.png">
+        <img class="inline" src="logo/nixos-logo-18.09-jellyfish-lores.png" alt="18.09 Jellyfish logo" with="100" height="87"/>
+      </a>
+      NixOS 18.09 “Jellyfish” has been released, the tenth stable release branch.
+      See the <a href="/nixos/manual/release-notes.html#sec-release-18.09">release notes</a>
+      for details. You can get NixOS 18.09 ISOs and VirtualBox appliances
+      from the <a href="nixos/download.html">download page</a>.
+      For information on how to upgrade from older release branches
+      to 18.09, check out the
+      <a href="/nixos/manual/index.html#sec-upgrading">manual section on upgrading</a>.
+    </description><pubDate>Sat Oct 06 2018 00:00:00 GMT</pubDate></item></channel></rss>
diff --git a/nixpkgs/nixos/tests/common/x11.nix b/nixpkgs/nixos/tests/common/x11.nix
new file mode 100644
index 000000000000..c5a7c165d126
--- /dev/null
+++ b/nixpkgs/nixos/tests/common/x11.nix
@@ -0,0 +1,12 @@
+{ services.xserver.enable = true;
+
+  # Automatically log in.
+  services.xserver.displayManager.auto.enable = true;
+
+  # Use IceWM as the window manager.
+  services.xserver.windowManager.default = "icewm";
+  services.xserver.windowManager.icewm.enable = true;
+
+  # Don't use a desktop manager.
+  services.xserver.desktopManager.default = "none";
+}