summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/administration/running.xml2
-rw-r--r--nixos/doc/manual/configuration/configuration.xml4
-rw-r--r--nixos/doc/manual/configuration/kubernetes.xml127
-rw-r--r--nixos/doc/manual/configuration/matrix.xml197
-rw-r--r--nixos/doc/manual/configuration/wireless.xml26
-rw-r--r--nixos/doc/manual/default.nix7
-rw-r--r--nixos/doc/manual/development/development.xml2
-rw-r--r--nixos/doc/manual/development/option-types.xml4
-rwxr-xr-xnixos/doc/manual/development/releases.xml7
-rw-r--r--nixos/doc/manual/installation/installation.xml2
-rw-r--r--nixos/doc/manual/installation/installing-usb.xml2
-rw-r--r--nixos/doc/manual/installation/installing.xml4
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml14
-rw-r--r--nixos/doc/manual/release-notes/release-notes.xml1
-rw-r--r--nixos/doc/manual/release-notes/rl-1903.xml284
-rw-r--r--nixos/doc/manual/release-notes/rl-1909.xml68
-rw-r--r--nixos/lib/eval-config.nix4
-rw-r--r--nixos/lib/testing.nix2
-rw-r--r--nixos/maintainers/scripts/cloudstack/cloudstack-image.nix23
-rw-r--r--nixos/maintainers/scripts/ec2/amazon-image.nix1
-rw-r--r--nixos/maintainers/scripts/openstack/nova-image.nix26
-rw-r--r--nixos/maintainers/scripts/openstack/openstack-image.nix26
-rw-r--r--nixos/modules/config/fonts/fontconfig-penultimate.nix4
-rw-r--r--nixos/modules/config/fonts/fontconfig-ultimate.nix2
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix2
-rw-r--r--nixos/modules/config/fonts/fontdir.nix2
-rw-r--r--nixos/modules/config/ldap.nix56
-rw-r--r--nixos/modules/config/no-x-libs.nix2
-rw-r--r--nixos/modules/config/nsswitch.nix13
-rw-r--r--nixos/modules/config/pulseaudio.nix5
-rw-r--r--nixos/modules/config/zram.nix6
-rw-r--r--nixos/modules/hardware/acpilight.nix24
-rw-r--r--nixos/modules/hardware/ledger.nix14
-rw-r--r--nixos/modules/hardware/video/nvidia.nix5
-rw-r--r--nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix1
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix4
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix6
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl8
-rw-r--r--nixos/modules/installer/tools/nixos-install.sh13
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh21
-rw-r--r--nixos/modules/installer/virtualbox-demo.nix2
-rw-r--r--nixos/modules/misc/documentation.nix18
-rw-r--r--nixos/modules/misc/ids.nix6
-rw-r--r--nixos/modules/misc/version.nix4
-rw-r--r--nixos/modules/module-list.nix37
-rw-r--r--nixos/modules/profiles/graphical.nix4
-rw-r--r--nixos/modules/profiles/minimal.nix2
-rw-r--r--nixos/modules/programs/autojump.nix33
-rw-r--r--nixos/modules/programs/bash/bash.nix2
-rw-r--r--nixos/modules/programs/fish.nix53
-rw-r--r--nixos/modules/programs/fish_completion-generator.patch11
-rw-r--r--nixos/modules/programs/gnupg.nix4
-rw-r--r--nixos/modules/programs/iotop.nix17
-rw-r--r--nixos/modules/programs/less.nix2
-rw-r--r--nixos/modules/programs/singularity.nix21
-rw-r--r--nixos/modules/programs/ssh.nix10
-rw-r--r--nixos/modules/programs/sway-beta.nix90
-rw-r--r--nixos/modules/programs/sway.nix57
-rw-r--r--nixos/modules/programs/waybar.nix20
-rw-r--r--nixos/modules/programs/zsh/zsh-syntax-highlighting.nix22
-rw-r--r--nixos/modules/programs/zsh/zsh.nix36
-rw-r--r--nixos/modules/rename.nix17
-rw-r--r--nixos/modules/security/ca.nix1
-rw-r--r--nixos/modules/security/duosec.nix19
-rw-r--r--nixos/modules/security/pam.nix132
-rw-r--r--nixos/modules/security/sudo.nix5
-rw-r--r--nixos/modules/services/audio/snapserver.nix217
-rw-r--r--nixos/modules/services/audio/squeezelite.nix27
-rw-r--r--nixos/modules/services/backup/duplicity.nix141
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix167
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dashboard.nix (renamed from nixos/modules/services/cluster/kubernetes/dashboard.nix)15
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix (renamed from nixos/modules/services/cluster/kubernetes/dns.nix)54
-rw-r--r--nixos/modules/services/cluster/kubernetes/apiserver.nix428
-rw-r--r--nixos/modules/services/cluster/kubernetes/controller-manager.nix162
-rw-r--r--nixos/modules/services/cluster/kubernetes/default.nix1133
-rw-r--r--nixos/modules/services/cluster/kubernetes/flannel.nix134
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix358
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix388
-rw-r--r--nixos/modules/services/cluster/kubernetes/proxy.nix82
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix94
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agent.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix1
-rw-r--r--nixos/modules/services/databases/hbase.nix2
-rw-r--r--nixos/modules/services/databases/influxdb.nix1
-rw-r--r--nixos/modules/services/databases/mysql.nix1
-rw-r--r--nixos/modules/services/databases/openldap.nix2
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix45
-rw-r--r--nixos/modules/services/desktops/pantheon/contractor.nix39
-rw-r--r--nixos/modules/services/desktops/pantheon/files.nix36
-rw-r--r--nixos/modules/services/desktops/tumbler.nix50
-rw-r--r--nixos/modules/services/games/minecraft-server.nix201
-rw-r--r--nixos/modules/services/hardware/acpid.nix2
-rw-r--r--nixos/modules/services/hardware/bolt.nix34
-rw-r--r--nixos/modules/services/hardware/fwupd.nix26
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/dsseries.nix26
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix20
-rw-r--r--nixos/modules/services/hardware/tlp.nix1
-rw-r--r--nixos/modules/services/logging/logcheck.nix2
-rw-r--r--nixos/modules/services/logging/logstash.nix5
-rw-r--r--nixos/modules/services/mail/davmail.nix91
-rw-r--r--nixos/modules/services/mail/opensmtpd.nix2
-rw-r--r--nixos/modules/services/mail/rmilter.nix4
-rw-r--r--nixos/modules/services/mail/roundcube.nix20
-rw-r--r--nixos/modules/services/misc/airsonic.nix17
-rw-r--r--nixos/modules/services/misc/beanstalkd.nix52
-rw-r--r--nixos/modules/services/misc/bepasty.nix6
-rw-r--r--nixos/modules/services/misc/docker-registry.nix2
-rw-r--r--nixos/modules/services/misc/gitlab.nix24
-rw-r--r--nixos/modules/services/misc/gitolite.nix2
-rw-r--r--nixos/modules/services/misc/headphones.nix87
-rw-r--r--nixos/modules/services/misc/home-assistant.nix99
-rw-r--r--nixos/modules/services/misc/jackett.nix59
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix1
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix2
-rw-r--r--nixos/modules/services/misc/nzbget.nix40
-rw-r--r--nixos/modules/services/misc/plex.nix1
-rw-r--r--nixos/modules/services/misc/radarr.nix59
-rw-r--r--nixos/modules/services/misc/redmine.nix110
-rw-r--r--nixos/modules/services/misc/rippled.nix346
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix2
-rw-r--r--nixos/modules/services/misc/tautulli.nix (renamed from nixos/modules/services/misc/plexpy.nix)40
-rw-r--r--nixos/modules/services/misc/weechat.xml4
-rw-r--r--nixos/modules/services/misc/zoneminder.nix28
-rw-r--r--nixos/modules/services/monitoring/apcupsd.nix2
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix8
-rw-r--r--nixos/modules/services/monitoring/grafana.nix184
-rw-r--r--nixos/modules/services/monitoring/graphite.nix6
-rw-r--r--nixos/modules/services/monitoring/hdaps.nix1
-rw-r--r--nixos/modules/services/monitoring/munin.nix209
-rw-r--r--nixos/modules/services/monitoring/nagios.nix6
-rw-r--r--nixos/modules/services/monitoring/netdata.nix27
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix3
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix2
-rw-r--r--nixos/modules/services/monitoring/scollector.nix2
-rw-r--r--nixos/modules/services/monitoring/telegraf.nix1
-rw-r--r--nixos/modules/services/monitoring/uptime.nix3
-rw-r--r--nixos/modules/services/network-filesystems/beegfs.nix5
-rw-r--r--nixos/modules/services/network-filesystems/diod.nix1
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix2
-rw-r--r--nixos/modules/services/network-filesystems/openafs/client.nix6
-rw-r--r--nixos/modules/services/networking/coredns.nix50
-rw-r--r--nixos/modules/services/networking/dnscache.nix2
-rw-r--r--nixos/modules/services/networking/firefox/sync-server.nix26
-rw-r--r--nixos/modules/services/networking/flannel.nix55
-rw-r--r--nixos/modules/services/networking/gnunet.nix4
-rw-r--r--nixos/modules/services/networking/hylafax/systemd.nix2
-rw-r--r--nixos/modules/services/networking/knot.nix95
-rw-r--r--nixos/modules/services/networking/mosquitto.nix9
-rw-r--r--nixos/modules/services/networking/ndppd.nix170
-rw-r--r--nixos/modules/services/networking/nix-serve.nix9
-rw-r--r--nixos/modules/services/networking/nylon.nix1
-rw-r--r--nixos/modules/services/networking/prayer.nix2
-rw-r--r--nixos/modules/services/networking/quassel.nix28
-rw-r--r--nixos/modules/services/networking/shout.nix2
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix9
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/module.nix9
-rw-r--r--nixos/modules/services/networking/syncthing.nix2
-rw-r--r--nixos/modules/services/networking/teamspeak3.nix24
-rw-r--r--nixos/modules/services/networking/unifi.nix4
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix25
-rw-r--r--nixos/modules/services/networking/xrdp.nix2
-rw-r--r--nixos/modules/services/printing/cupsd.nix9
-rw-r--r--nixos/modules/services/security/certmgr.nix11
-rw-r--r--nixos/modules/services/security/munge.nix2
-rw-r--r--nixos/modules/services/security/nginx-sso.nix58
-rw-r--r--nixos/modules/services/security/sks.nix28
-rw-r--r--nixos/modules/services/security/sshguard.nix99
-rw-r--r--nixos/modules/services/torrent/transmission.nix3
-rw-r--r--nixos/modules/services/ttys/kmscon.nix2
-rw-r--r--nixos/modules/services/web-apps/atlassian/confluence.nix9
-rw-r--r--nixos/modules/services/web-apps/atlassian/crowd.nix9
-rw-r--r--nixos/modules/services/web-apps/atlassian/jira.nix9
-rw-r--r--nixos/modules/services/web-apps/codimd.nix2
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix626
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix157
-rw-r--r--nixos/modules/services/web-apps/matomo-doc.xml32
-rw-r--r--nixos/modules/services/web-apps/matomo.nix99
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix69
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix53
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix87
-rw-r--r--nixos/modules/services/web-apps/youtrack.nix1
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix24
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/mediawiki.nix7
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix21
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix1
-rw-r--r--nixos/modules/services/web-servers/phpfpm/default.nix10
-rw-r--r--nixos/modules/services/web-servers/phpfpm/pool-options.nix9
-rw-r--r--nixos/modules/services/web-servers/traefik.nix1
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix125
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix5
-rw-r--r--nixos/modules/services/x11/desktop-managers/mate.nix29
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix196
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix30
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix7
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix46
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix47
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix1
-rw-r--r--nixos/modules/services/x11/display-managers/slim.nix2
-rw-r--r--nixos/modules/services/x11/gdk-pixbuf.nix2
-rw-r--r--nixos/modules/services/x11/xautolock.nix2
-rw-r--r--nixos/modules/services/x11/xserver.nix10
-rw-r--r--nixos/modules/system/activation/top-level.nix4
-rw-r--r--nixos/modules/system/boot/kernel_config.nix137
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix2
-rw-r--r--nixos/modules/system/boot/stage-1.nix36
-rw-r--r--nixos/modules/system/boot/systemd-unit-options.nix9
-rw-r--r--nixos/modules/system/boot/systemd.nix31
-rw-r--r--nixos/modules/tasks/auto-upgrade.nix6
-rw-r--r--nixos/modules/tasks/encrypted-devices.nix6
-rw-r--r--nixos/modules/tasks/filesystems.nix2
-rw-r--r--nixos/modules/tasks/filesystems/vboxsf.nix2
-rw-r--r--nixos/modules/tasks/kbd.nix1
-rw-r--r--nixos/modules/tasks/network-interfaces-scripted.nix20
-rw-r--r--nixos/modules/tasks/network-interfaces.nix2
-rw-r--r--nixos/modules/testing/service-runner.nix25
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix30
-rw-r--r--nixos/modules/virtualisation/cloudstack-config.nix40
-rw-r--r--nixos/modules/virtualisation/containers.nix28
-rw-r--r--nixos/modules/virtualisation/docker-preloader.nix5
-rw-r--r--nixos/modules/virtualisation/docker.nix25
-rw-r--r--nixos/modules/virtualisation/ec2-metadata-fetcher.nix23
-rw-r--r--nixos/modules/virtualisation/nova-config.nix60
-rw-r--r--nixos/modules/virtualisation/openstack-config.nix57
-rw-r--r--nixos/modules/virtualisation/virtualbox-host.nix2
-rw-r--r--nixos/modules/virtualisation/vmware-guest.nix18
-rw-r--r--nixos/release-combined.nix1
-rw-r--r--nixos/tests/all-tests.nix16
-rw-r--r--nixos/tests/avahi.nix2
-rw-r--r--nixos/tests/beanstalkd.nix45
-rw-r--r--nixos/tests/bittorrent.nix2
-rw-r--r--nixos/tests/colord.nix18
-rw-r--r--nixos/tests/common/ec2.nix49
-rw-r--r--nixos/tests/containers-bridge.nix19
-rw-r--r--nixos/tests/containers-imperative.nix2
-rw-r--r--nixos/tests/containers-ipv4.nix2
-rw-r--r--nixos/tests/containers-ipv6.nix2
-rw-r--r--nixos/tests/containers-portforward.nix2
-rw-r--r--nixos/tests/docker-tools.nix4
-rw-r--r--nixos/tests/ec2.nix62
-rw-r--r--nixos/tests/elk.nix2
-rw-r--r--nixos/tests/firefox.nix2
-rw-r--r--nixos/tests/firewall.nix2
-rw-r--r--nixos/tests/fish.nix21
-rw-r--r--nixos/tests/flannel.nix5
-rw-r--r--nixos/tests/fwupd.nix2
-rw-r--r--nixos/tests/gitea.nix2
-rw-r--r--nixos/tests/gitlab.nix36
-rw-r--r--nixos/tests/gnome3.nix2
-rw-r--r--nixos/tests/home-assistant.nix22
-rw-r--r--nixos/tests/hydra/default.nix160
-rw-r--r--nixos/tests/influxdb.nix2
-rw-r--r--nixos/tests/installer.nix72
-rw-r--r--nixos/tests/ipv6.nix2
-rw-r--r--nixos/tests/jenkins.nix2
-rw-r--r--nixos/tests/kexec.nix2
-rw-r--r--nixos/tests/knot.nix197
-rw-r--r--nixos/tests/kubernetes/base.nix39
-rw-r--r--nixos/tests/kubernetes/certs.nix219
-rw-r--r--nixos/tests/kubernetes/dns.nix15
-rw-r--r--nixos/tests/kubernetes/kubernetes-common.nix57
-rw-r--r--nixos/tests/kubernetes/rbac.nix13
-rw-r--r--nixos/tests/ldap.nix383
-rw-r--r--nixos/tests/login.nix2
-rw-r--r--nixos/tests/matrix-synapse.nix50
-rw-r--r--nixos/tests/misc.nix2
-rw-r--r--nixos/tests/mongodb.nix2
-rw-r--r--nixos/tests/mumble.nix2
-rw-r--r--nixos/tests/munin.nix6
-rw-r--r--nixos/tests/mysql-replication.nix2
-rw-r--r--nixos/tests/mysql.nix2
-rw-r--r--nixos/tests/nat.nix2
-rw-r--r--nixos/tests/ndppd.nix60
-rw-r--r--nixos/tests/neo4j.nix20
-rw-r--r--nixos/tests/networking.nix3
-rw-r--r--nixos/tests/nfs.nix2
-rw-r--r--nixos/tests/nginx-sso.nix44
-rw-r--r--nixos/tests/openssh.nix25
-rw-r--r--nixos/tests/openstack-image.nix88
-rw-r--r--nixos/tests/osrm-backend.nix53
-rw-r--r--nixos/tests/overlayfs.nix57
-rw-r--r--nixos/tests/pam-u2f.nix23
-rw-r--r--nixos/tests/pantheon.nix55
-rw-r--r--nixos/tests/phabricator.nix2
-rw-r--r--nixos/tests/postgis.nix5
-rw-r--r--nixos/tests/postgresql.nix17
-rw-r--r--nixos/tests/printing.nix4
-rw-r--r--nixos/tests/proxy.nix2
-rw-r--r--nixos/tests/quake3.nix2
-rw-r--r--nixos/tests/rabbitmq.nix2
-rw-r--r--nixos/tests/redmine.nix84
-rw-r--r--nixos/tests/roundcube.nix4
-rw-r--r--nixos/tests/rspamd.nix16
-rw-r--r--nixos/tests/subversion.nix2
-rw-r--r--nixos/tests/switch-test.nix13
-rw-r--r--nixos/tests/tomcat.nix2
-rw-r--r--nixos/tests/trac.nix2
-rw-r--r--nixos/tests/udisks2.nix2
-rw-r--r--nixos/tests/virtualbox.nix2
-rw-r--r--nixos/tests/xfce.nix2
303 files changed, 9482 insertions, 2823 deletions
diff --git a/nixos/doc/manual/administration/running.xml b/nixos/doc/manual/administration/running.xml
index 786dd5e2390..19bec1f7794 100644
--- a/nixos/doc/manual/administration/running.xml
+++ b/nixos/doc/manual/administration/running.xml
@@ -4,7 +4,7 @@
       version="5.0"
       xml:id="ch-running">
  <title>Administration</title>
- <partintro>
+ <partintro xml:id="ch-running-intro">
   <para>
    This chapter describes various aspects of managing a running NixOS system,
    such as how to use the <command>systemd</command> service manager.
diff --git a/nixos/doc/manual/configuration/configuration.xml b/nixos/doc/manual/configuration/configuration.xml
index cebc4122c6c..5961209bc13 100644
--- a/nixos/doc/manual/configuration/configuration.xml
+++ b/nixos/doc/manual/configuration/configuration.xml
@@ -4,7 +4,7 @@
       version="5.0"
       xml:id="ch-configuration">
  <title>Configuration</title>
- <partintro>
+ <partintro xml:id="ch-configuration-intro">
   <para>
    This chapter describes how to configure various aspects of a NixOS machine
    through the configuration file
@@ -21,7 +21,9 @@
  <xi:include href="xfce.xml" />
  <xi:include href="networking.xml" />
  <xi:include href="linux-kernel.xml" />
+ <xi:include href="matrix.xml" />
  <xi:include href="../generated/modules.xml" xpointer="xpointer(//section[@id='modules']/*)" />
  <xi:include href="profiles.xml" />
+ <xi:include href="kubernetes.xml" />
 <!-- Apache; libvirtd virtualisation -->
 </part>
diff --git a/nixos/doc/manual/configuration/kubernetes.xml b/nixos/doc/manual/configuration/kubernetes.xml
new file mode 100644
index 00000000000..ddc026c0c01
--- /dev/null
+++ b/nixos/doc/manual/configuration/kubernetes.xml
@@ -0,0 +1,127 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="sec-kubernetes">
+ <title>Kubernetes</title>
+
+ <para>
+   The NixOS Kubernetes module is a collective term for a handful of
+   individual submodules implementing the Kubernetes cluster components.
+ </para>
+
+ <para>
+   There are generally two ways of enabling Kubernetes on NixOS.
+   One way is to enable and configure cluster components appropriately by hand:
+<programlisting>
+services.kubernetes = {
+  apiserver.enable = true;
+  controllerManager.enable = true;
+  scheduler.enable = true;
+  addonManager.enable = true;
+  proxy.enable = true;
+  flannel.enable = true;
+};
+</programlisting>
+  Another way is to assign cluster roles ("master" and/or "node") to the host.
+  This enables apiserver, controllerManager, scheduler, addonManager,
+  kube-proxy and etcd:
+<programlisting>
+<xref linkend="opt-services.kubernetes.roles"/> = [ "master" ];
+</programlisting>
+  While this will enable the kubelet and kube-proxy only:
+<programlisting>
+<xref linkend="opt-services.kubernetes.roles"/> = [ "node" ];
+</programlisting>
+  Assigning both the master and node roles is usable if you want a single
+  node Kubernetes cluster for dev or testing purposes:
+<programlisting>
+<xref linkend="opt-services.kubernetes.roles"/> = [ "master" "node" ];
+</programlisting>
+  Note: Assigning either role will also default both
+  <xref linkend="opt-services.kubernetes.flannel.enable"/> and
+  <xref linkend="opt-services.kubernetes.easyCerts"/> to true.
+  This sets up flannel as CNI and activates automatic PKI bootstrapping.
+ </para>
+
+ <para>
+   As of kubernetes 1.10.X it has been deprecated to open
+   non-tls-enabled ports on kubernetes components. Thus, from NixOS 19.03 all
+   plain HTTP ports have been disabled by default.
+   While opening insecure ports is still possible, it is recommended not to
+   bind these to other interfaces than loopback.
+
+   To re-enable the insecure port on the apiserver, see options:
+   <xref linkend="opt-services.kubernetes.apiserver.insecurePort"/>
+   and
+   <xref linkend="opt-services.kubernetes.apiserver.insecureBindAddress"/>
+ </para>
+
+ <note>
+  <para>
+   As of NixOS 19.03, it is mandatory to configure:
+   <xref linkend="opt-services.kubernetes.masterAddress"/>.
+   The masterAddress must be resolveable and routeable by all cluster nodes.
+   In single node clusters, this can be set to <literal>localhost</literal>.
+  </para>
+ </note>
+
+ <para>
+   Role-based access control (RBAC) authorization mode is enabled by default.
+   This means that anonymous requests to the apiserver secure port will
+   expectedly cause a permission denied error. All cluster components must
+   therefore be configured with x509 certificates for two-way tls communication.
+   The x509 certificate subject section determines the roles and permissions
+   granted by the apiserver to perform clusterwide or namespaced operations.
+   See also:
+   <link
+     xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">
+     Using RBAC Authorization</link>.
+ </para>
+
+  <para>
+   The NixOS kubernetes module provides an option for automatic certificate
+   bootstrapping and configuration,
+    <xref linkend="opt-services.kubernetes.easyCerts"/>.
+   The PKI bootstrapping process involves setting up a certificate authority
+   (CA) daemon (cfssl) on the kubernetes master node. cfssl generates a CA-cert
+   for the cluster, and uses the CA-cert for signing subordinate certs issued to
+   each of the cluster components. Subsequently, the certmgr daemon monitors
+   active certificates and renews them when needed. For single node Kubernetes
+   clusters, setting <xref linkend="opt-services.kubernetes.easyCerts"/> = true
+   is sufficient and no further action is required. For joining extra node
+   machines to an existing cluster on the other hand, establishing initial trust
+   is mandatory.
+ </para>
+
+ <para>
+   To add new nodes to the cluster:
+   On any (non-master) cluster node where
+   <xref linkend="opt-services.kubernetes.easyCerts"/> is enabled, the helper
+   script <literal>nixos-kubernetes-node-join</literal> is available on PATH.
+   Given a token on stdin, it will copy the token to the kubernetes
+   secrets directory and restart the certmgr service. As requested
+   certificates are issued, the script will restart kubernetes cluster
+   components as needed for them to pick up new keypairs.
+ </para>
+
+ <note>
+  <para>
+   Multi-master (HA) clusters are not supported by the easyCerts module.
+  </para>
+ </note>
+
+ <para>
+   In order to interact with an RBAC-enabled cluster as an administrator, one
+   needs to have cluster-admin privileges. By default, when easyCerts is
+   enabled, a cluster-admin kubeconfig file is generated and linked into
+   <literal>/etc/kubernetes/cluster-admin.kubeconfig</literal> as determined by
+   <xref linkend="opt-services.kubernetes.pki.etcClusterAdminKubeconfig"/>.
+   <literal>export KUBECONFIG=/etc/kubernetes/cluster-admin.kubeconfig</literal>
+   will make kubectl use this kubeconfig to access and authenticate the cluster.
+   The cluster-admin kubeconfig references an auto-generated keypair owned by
+   root. Thus, only root on the kubernetes master may obtain cluster-admin
+   rights by means of this file.
+ </para>
+
+</chapter>
diff --git a/nixos/doc/manual/configuration/matrix.xml b/nixos/doc/manual/configuration/matrix.xml
new file mode 100644
index 00000000000..a9a5d6de1f1
--- /dev/null
+++ b/nixos/doc/manual/configuration/matrix.xml
@@ -0,0 +1,197 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="module-services-matrix">
+ <title>Matrix</title>
+ <para>
+  <link xlink:href="https://matrix.org/">Matrix</link>
+  is an open standard for interoperable, decentralised, real-time communication over IP.
+  It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication -
+  or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history.
+ </para>
+ <para>
+  This chapter will show you how to set up your own, self-hosted Matrix homeserver using the Synapse reference homeserver,
+  and how to serve your own copy of the Riot web client.
+  See the <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try Matrix Now!</link>
+  overview page for links to Riot Apps for Android and iOS, desktop clients,
+  as well as bridges to other networks and other projects around Matrix.
+ </para>
+
+ <section xml:id="module-services-matrix-synapse">
+  <title>Synapse Homeserver</title>
+  <para>
+   <link xlink:href="https://github.com/matrix-org/synapse">Synapse</link>
+   is the reference homeserver implementation of Matrix from the core development team at matrix.org.
+   The following configuration example will set up a synapse server for the <literal>example.org</literal>
+   domain, served from the host <literal>myhostname.example.org</literal>.
+   For more information, please refer to the
+   <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
+    installation instructions of Synapse
+   </link>.
+   <programlisting>
+    let
+      fqdn =
+        let
+          join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+        in join config.networking.hostName config.networking.domain;
+    in {
+      networking = {
+        hostName = "myhostname";
+        domain = "example.org";
+      };
+      networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+      services.nginx = {
+        enable = true;
+        # only recommendedProxySettings and recommendedGzipSettings are strictly required,
+        # but the rest make sense as well
+        recommendedTlsSettings = true;
+        recommendedOptimisation = true;
+        recommendedGzipSettings = true;
+        recommendedProxySettings = true;
+
+        virtualHosts = {
+          # This host section can be placed on a different host than the rest,
+          # i.e. to delegate from the host being accessible as ${config.networking.domain}
+          # to another host actually running the Matrix homeserver.
+          "${config.networking.domain}" = {
+            locations."= /.well-known/matrix/server".extraConfig =
+              let
+                # use 443 instead of the default 8448 port to unite
+                # the client-server and server-server port for simplicity
+                server = { "m.server" = "${fqdn}:443"; };
+              in ''
+                add_header Content-Type application/json;
+                return 200 '${builtins.toJSON server}';
+              '';
+            locations."= /.well-known/matrix/client".extraConfig =
+              let
+                client = {
+                  "m.homeserver" =  { "base_url" = "https://${fqdn}"; };
+                  "m.identity_server" =  { "base_url" = "https://vector.im"; };
+                };
+              # ACAO required to allow riot-web on any URL to request this json file
+              in ''
+                add_header Content-Type application/json;
+                add_header Access-Control-Allow-Origin *;
+                return 200 '${builtins.toJSON client}';
+              '';
+          };
+
+          # Reverse proxy for Matrix client-server and server-server communication
+          ${fqdn} = {
+            enableACME = true;
+            forceSSL = true;
+
+            # Or do a redirect instead of the 404, or whatever is appropriate for you.
+            # But do not put a Matrix Web client here! See the Riot Web section below.
+            locations."/".extraConfig = ''
+              return 404;
+            '';
+
+            # forward all Matrix API calls to the synapse Matrix homeserver
+            locations."/_matrix" = {
+              proxyPass = "http://[::1]:8008";
+            };
+          };
+        };
+      };
+      services.matrix-synapse = {
+        enable = true;
+        server_name = config.networking.domain;
+        listeners = [
+          {
+            port = 8008;
+            bind_address = "::1";
+            type = "http";
+            tls = false;
+            x_forwarded = true;
+            resources = [
+              { names = [ "client" "federation" ]; compress = false; }
+            ];
+          }
+        ];
+      };
+    };
+   </programlisting>
+  </para>
+  <para>
+   If the <code>A</code> and <code>AAAA</code> DNS records on <literal>example.org</literal>
+   do not point on the same host as the records for <code>myhostname.example.org</code>,
+   you can easily move the <code>/.well-known</code> virtualHost section of the code
+   to the host that is serving <literal>example.org</literal>,
+   while the rest stays on <literal>myhostname.example.org</literal>
+   with no other changes required.
+   This pattern also allows to seamlessly move the homeserver from <literal>myhostname.example.org</literal>
+   to <literal>myotherhost.example.org</literal> by only changing the <code>/.well-known</code> redirection target.
+  </para>
+
+  <para>
+   If you want to run a server with public registration by anybody,
+   you can then enable
+   <option>services.matrix-synapse.enable_registration = true;</option>.
+   Otherwise, or you can generate a registration secret with <command>pwgen -s 64 1</command>
+   and set it with
+   <option>services.matrix-synapse.registration_shared_secret</option>.
+   To create a new user or admin,
+   run the following after you have set the secret and have rebuilt NixOS:
+
+   <programlisting>
+    $ nix run nixpkgs.matrix-synapse
+    $ register_new_matrix_user -k &lt;your-registration-shared-secret&gt; http://localhost:8008
+    New user localpart: &lt;your-username&gt;
+    Password:
+    Confirm password:
+    Make admin [no]:
+    Success!
+   </programlisting>
+   In the example, this would create a user with the Matrix Identifier
+   <literal>@your-username:example.org</literal>.
+   Note that the registration secret ends up in the nix store and therefore is world-readable
+   by any user on your machine, so it makes sense to only temporarily activate the
+   <option>registration_shared_secret</option> option until a better solution for NixOS is in place.
+  </para>
+ </section>
+
+ <section xml:id="module-services-matrix-riot-web">
+  <title>Riot Web Client</title>
+  <para>
+   <link xlink:href="https://github.com/vector-im/riot-web/">Riot Web</link>
+   is the reference web client for Matrix and developed by the core team at matrix.org.
+   The following snippet can be optionally added to the code before to complete the synapse
+   installation with a web client served at
+   <code>https://riot.myhostname.example.org</code> and <code>https://riot.example.org</code>.
+   Alternatively, you can use the hosted copy at
+   <link xlink:href="https://riot.im/app">https://riot.im/app</link>,
+   or use other web clients or native client applications.
+   Due to the <literal>/.well-known</literal> urls set up done above,
+   many clients should fill in the required connection details automatically
+   when you enter your Matrix Identifier.
+   See <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try Matrix Now!</link>
+   for a list of existing clients and their supported featureset.
+
+   <programlisting>
+     services.nginx.virtualHosts."riot.${fqdn}" = {
+       enableACME = true;
+       forceSSL = true;
+       serverAliases = [
+        "riot.${config.networking.domain}"
+       ];
+
+       root = pkgs.riot-web;
+     };
+   </programlisting>
+  </para>
+  <para>
+   Note that the Riot developers do not recommend running Riot and your Matrix homeserver
+   on the same fully-qualified domain name for security reasons.
+   In the example, this means that you should not reuse the <literal>myhostname.example.org</literal>
+   virtualHost to also serve Riot, but instead serve it on a different subdomain,
+   like <literal>riot.example.org</literal> in the example.
+   See the
+   <link xlink:href="https://github.com/vector-im/riot-web#important-security-note">Riot Important Security Notes</link>
+   for more information on this subject.
+  </para>
+ </section>
+</chapter>
diff --git a/nixos/doc/manual/configuration/wireless.xml b/nixos/doc/manual/configuration/wireless.xml
index 999447234ad..dda2193dd93 100644
--- a/nixos/doc/manual/configuration/wireless.xml
+++ b/nixos/doc/manual/configuration/wireless.xml
@@ -29,12 +29,32 @@
   networks are set, it will default to using a configuration file at
   <literal>/etc/wpa_supplicant.conf</literal>. You should edit this file
   yourself to define wireless networks, WPA keys and so on (see
-  wpa_supplicant.conf(5)).
+  <citerefentry>
+    <refentrytitle>wpa_supplicant.conf</refentrytitle>
+    <manvolnum>5</manvolnum>
+  </citerefentry>).
  </para>
 
  <para>
-  If you are using WPA2 the <command>wpa_passphrase</command> tool might be
-  useful to generate the <literal>wpa_supplicant.conf</literal>.
+  If you are using WPA2 you can generate pskRaw key using
+  <command>wpa_passphrase</command>:
+<screen>
+$ wpa_passphrase ESSID PSK
+network={
+        ssid="echelon"
+        #psk="abcdefgh"
+        psk=dca6d6ed41f4ab5a984c9f55f6f66d4efdc720ebf66959810f4329bb391c5435
+}
+</screen>
+<programlisting>
+<xref linkend="opt-networking.wireless.networks"/> = {
+  echelon = {
+    pskRaw = "dca6d6ed41f4ab5a984c9f55f6f66d4efdc720ebf66959810f4329bb391c5435";
+  };
+}
+</programlisting>
+  or you can use it to directly generate the
+  <literal>wpa_supplicant.conf</literal>:
 <screen>
 # wpa_passphrase ESSID PSK > /etc/wpa_supplicant.conf</screen>
   After you have edited the <literal>wpa_supplicant.conf</literal>, you need to
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
index faae4f20544..7fc0ad702f8 100644
--- a/nixos/doc/manual/default.nix
+++ b/nixos/doc/manual/default.nix
@@ -265,9 +265,13 @@ in rec {
       xsltproc \
         ${manualXsltprocOptions} \
         --stringparam target.database.document "${olinkDB}/olinkdb.xml" \
+        --stringparam id.warnings "1" \
         --nonet --output $dst/ \
         ${docbook_xsl_ns}/xml/xsl/docbook/xhtml/chunktoc.xsl \
-        ${manual-combined}/manual-combined.xml
+        ${manual-combined}/manual-combined.xml \
+        |& tee xsltproc.out
+      grep "^ID recommended on" xsltproc.out &>/dev/null && echo "error: some IDs are missing" && false
+      rm xsltproc.out
 
       mkdir -p $dst/images/callouts
       cp ${docbook_xsl_ns}/xml/xsl/docbook/images/callouts/*.svg $dst/images/callouts/
@@ -326,6 +330,7 @@ in rec {
       # Generate manpages.
       mkdir -p $out/share/man
       xsltproc --nonet \
+        --maxdepth 6000 \
         --param man.output.in.separate.dir 1 \
         --param man.output.base.dir "'$out/share/man/'" \
         --param man.endnotes.are.numbered 0 \
diff --git a/nixos/doc/manual/development/development.xml b/nixos/doc/manual/development/development.xml
index 03dee6ff09b..43f511b3e96 100644
--- a/nixos/doc/manual/development/development.xml
+++ b/nixos/doc/manual/development/development.xml
@@ -4,7 +4,7 @@
         version="5.0"
         xml:id="ch-development">
  <title>Development</title>
- <partintro>
+ <partintro xml:id="ch-development-intro">
   <para>
    This chapter describes how you can modify and extend NixOS.
   </para>
diff --git a/nixos/doc/manual/development/option-types.xml b/nixos/doc/manual/development/option-types.xml
index d993e47bc91..069cc36573d 100644
--- a/nixos/doc/manual/development/option-types.xml
+++ b/nixos/doc/manual/development/option-types.xml
@@ -512,7 +512,7 @@ config.mod.two = { foo = 2; bar = "two"; };</screen>
       The function to type check the value. Takes a value as parameter and
       return a boolean. It is possible to extend a type check with the
       <literal>addCheck</literal> function
-      (<xref 
+      (<xref
           linkend='ex-extending-type-check-1' />), or to fully
       override the check function
       (<xref linkend='ex-extending-type-check-2' />).
@@ -522,7 +522,7 @@ config.mod.two = { foo = 2; bar = "two"; };</screen>
 <screen>
 byte = mkOption {
   description = "An integer between 0 and 255.";
-  type = addCheck types.int (x: x &gt;= 0 &amp;&amp; x &lt;= 255);
+  type = types.addCheck types.int (x: x &gt;= 0 &amp;&amp; x &lt;= 255);
 };</screen>
      </example>
      <example xml:id='ex-extending-type-check-2'>
diff --git a/nixos/doc/manual/development/releases.xml b/nixos/doc/manual/development/releases.xml
index d4e5ff3f431..dcedad540e1 100755
--- a/nixos/doc/manual/development/releases.xml
+++ b/nixos/doc/manual/development/releases.xml
@@ -62,13 +62,6 @@
     </listitem>
     <listitem>
      <para>
-      <link xlink:href="https://github.com/NixOS/nixpkgs/settings/branches">
-      Let a GitHub nixpkgs admin lock the branch on github for you. (so
-      developers can’t force push) </link>
-     </para>
-    </listitem>
-    <listitem>
-     <para>
       <link xlink:href="https://github.com/NixOS/nixpkgs/compare/bdf161ed8d21...6b63c4616790">
       Bump the <literal>system.defaultChannel</literal> attribute in
       <literal>nixos/modules/misc/version.nix</literal> </link>
diff --git a/nixos/doc/manual/installation/installation.xml b/nixos/doc/manual/installation/installation.xml
index d4276be95d6..2901f462dee 100644
--- a/nixos/doc/manual/installation/installation.xml
+++ b/nixos/doc/manual/installation/installation.xml
@@ -4,7 +4,7 @@
       version="5.0"
       xml:id="ch-installation">
  <title>Installation</title>
- <partintro>
+ <partintro xml:id="ch-installation-intro">
   <para>
    This section describes how to obtain, install, and configure NixOS for
    first-time use.
diff --git a/nixos/doc/manual/installation/installing-usb.xml b/nixos/doc/manual/installation/installing-usb.xml
index 3a81b3a2040..c0372e8ebd9 100644
--- a/nixos/doc/manual/installation/installing-usb.xml
+++ b/nixos/doc/manual/installation/installing-usb.xml
@@ -23,7 +23,7 @@ $ diskutil list
 [..]
 $ diskutil unmountDisk diskN
 Unmount of all volumes on diskN was successful
-$ sudo dd bs=1000000 if=nix.iso of=/dev/rdiskN
+$ sudo dd if=nix.iso of=/dev/rdiskN
 </programlisting>
     Using the 'raw' <command>rdiskN</command> device instead of
     <command>diskN</command> completes in minutes instead of hours. After
diff --git a/nixos/doc/manual/installation/installing.xml b/nixos/doc/manual/installation/installing.xml
index 8e94f946c5e..f4f8d470f80 100644
--- a/nixos/doc/manual/installation/installing.xml
+++ b/nixos/doc/manual/installation/installing.xml
@@ -378,6 +378,10 @@
      the grub menu.
     </para>
     <para>
+     If you need to configure networking for your machine the configuration
+     options are described in <xref linkend="sec-networking"/>.
+    </para>
+    <para>
      Another critical option is <option>fileSystems</option>, specifying the
      file systems that need to be mounted by NixOS. However, you typically
      don’t need to set it yourself, because
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index b6a247286d4..654b5f4b284 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -39,6 +39,10 @@
    </arg>
 
    <arg choice='plain'>
+    <option>edit</option>
+   </arg>
+
+   <arg choice='plain'>
     <option>build-vm</option>
    </arg>
 
@@ -190,6 +194,16 @@ $ nix-build /path/to/nixpkgs/nixos -A system
     </varlistentry>
     <varlistentry>
      <term>
+      <option>edit</option>
+     </term>
+     <listitem>
+      <para>
+        Opens <filename>configuration.nix</filename> in the default editor.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>
       <option>build-vm</option>
      </term>
      <listitem>
diff --git a/nixos/doc/manual/release-notes/release-notes.xml b/nixos/doc/manual/release-notes/release-notes.xml
index a222bfa29d5..02b59147721 100644
--- a/nixos/doc/manual/release-notes/release-notes.xml
+++ b/nixos/doc/manual/release-notes/release-notes.xml
@@ -8,6 +8,7 @@
   This section lists the release notes for each stable version of NixOS and
   current unstable revision.
  </para>
+ <xi:include href="rl-1909.xml" />
  <xi:include href="rl-1903.xml" />
  <xi:include href="rl-1809.xml" />
  <xi:include href="rl-1803.xml" />
diff --git a/nixos/doc/manual/release-notes/rl-1903.xml b/nixos/doc/manual/release-notes/rl-1903.xml
index da3b75cf614..3871d54c59c 100644
--- a/nixos/doc/manual/release-notes/rl-1903.xml
+++ b/nixos/doc/manual/release-notes/rl-1903.xml
@@ -23,6 +23,47 @@
     The default Python 3 interpreter is now CPython 3.7 instead of CPython 3.6.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     Added the Pantheon desktop environment.
+     It can be enabled through <varname>services.xserver.desktopManager.pantheon.enable</varname>.
+    </para>
+    <note>
+     <para>
+      <varname>services.xserver.desktopManager.pantheon</varname> default enables lightdm
+      as a display manager and using Pantheon's greeter.
+     </para>
+     <para>
+      This is because of limitations with the screenlocking implementation, whereas the
+      screenlocker would be non-functional without it.
+     </para>
+     <para>
+      Because of that it is recommended to retain this precaution, however if you'd like to change this set:
+     </para>
+     <itemizedlist>
+      <listitem>
+       <para>
+        <option>services.xserver.displayManager.lightdm.enable</option>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+        <option>services.xserver.displayManager.lightdm.greeters.pantheon.enable</option>
+       </para>
+      </listitem>
+     </itemizedlist>
+     <para>to <literal>false</literal> and enable your preferred display manager.</para>
+    </note>
+   </listitem>
+   <listitem>
+     <para>
+       A major refactoring of the Kubernetes module has been completed.
+       Refactorings primarily focus on decoupling components and enhancing
+       security. Two-way TLS and RBAC has been enabled by default for all
+       components, which slightly changes the way the module is configured.
+       See: <xref linkend="sec-kubernetes"/> for details.
+     </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -52,6 +93,35 @@
      in <literal>nixos/modules/virtualisation/google-compute-config.nix</literal>.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     <literal>./services/misc/beanstalkd.nix</literal>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     There is a new <varname>services.cockroachdb</varname> module for running
+     CockroachDB databases. NixOS now ships with CockroachDB 2.1.x as well, available
+     on <literal>x86_64-linux</literal> and <literal>aarch64-linux</literal>.
+    </para>
+   </listitem>
+  </itemizedlist>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+      <literal>./security/duosec.nix</literal>
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <link xlink:href="https://duo.com/docs/duounix">PAM module for Duo
+     Security</link> has been enabled for use.  One can configure it using
+     the <option>security.duosec</option> options along with the
+     corresponding PAM option in
+     <option>security.pam.services.&lt;name?&gt;.duoSecurity.enable</option>.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -114,6 +184,20 @@
    </listitem>
    <listitem>
     <para>
+      The <varname>buildPythonPackage</varname> function now sets <varname>strictDeps = true</varname>
+      to help distinguish between native and non-native dependencies in order to
+      improve cross-compilation compatibility. Note however that this may break
+      user expressions.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+      The <varname>buildPythonPackage</varname> function now sets <varname>LANG = C.UTF-8</varname>
+      to enable Unicode support. The <varname>glibcLocales</varname> package is no longer needed as a build input.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
       The Syncthing state and configuration data has been moved from
       <varname>services.syncthing.dataDir</varname> to the newly defined
       <varname>services.syncthing.configDir</varname>, which default to
@@ -331,18 +415,30 @@
    <para>
      The <literal>pam_unix</literal> account module is now loaded with its
      control field set to <literal>required</literal> instead of
-     <literal>sufficient</literal>, so that later pam account modules that
+     <literal>sufficient</literal>, so that later PAM account modules that
      might do more extensive checks are being executed.
      Previously, the whole account module verification was exited prematurely
      in case a nss module provided the account name to
      <literal>pam_unix</literal>.
      The LDAP and SSSD NixOS modules already add their NSS modules when
-     enabled. In case your setup breaks due to some later pam account module
+     enabled. In case your setup breaks due to some later PAM account module
      previosuly shadowed, or failing NSS lookups, please file a bug. You can
      get back the old behaviour by manually setting
      <literal><![CDATA[security.pam.services.<name?>.text]]></literal>.
    </para>
   </listitem>
+  <listitem>
+   <para>
+     The <literal>pam_unix</literal> password module is now loaded with its
+     control field set to <literal>sufficient</literal> instead of
+     <literal>required</literal>, so that password managed only
+     by later PAM password modules are being executed.
+     Previously, for example, changing an LDAP account's password through PAM
+     was not possible: the whole password module verification
+     was exited prematurely by <literal>pam_unix</literal>,
+     preventing <literal>pam_ldap</literal> to manage the password as it should.
+   </para>
+  </listitem>
    <listitem>
     <para>
      <literal>fish</literal> has been upgraded to 3.0.
@@ -350,6 +446,46 @@
      See the <literal>fish</literal> <link xlink:href="https://github.com/fish-shell/fish-shell/releases/tag/3.0.0">release notes</link> for more information.
     </para>
    </listitem>
+  <listitem>
+    <para>
+      The ibus-table input method has had a change in config format, which
+      causes all previous settings to be lost. See
+      <link xlink:href="https://github.com/mike-fabian/ibus-table/commit/f9195f877c5212fef0dfa446acb328c45ba5852b">this commit message</link>
+      for details.
+    </para>
+  </listitem>
+  <listitem>
+   <para>
+    NixOS module system type <literal>types.optionSet</literal> and
+    <literal>lib.mkOption</literal> argument <literal>options</literal> are deprecated.
+    Use <literal>types.submodule</literal> instead.
+    (<link xlink:href="https://github.com/NixOS/nixpkgs/pull/54637">#54637</link>)
+   </para>
+  </listitem>
+  <listitem>
+   <para>
+    <literal>matrix-synapse</literal> has been updated to version 0.99. It will
+    <link xlink:href="https://github.com/matrix-org/synapse/pull/4509">no longer generate a self-signed certificate on first launch</link>
+    and will be <link xlink:href="https://matrix.org/blog/2019/02/05/synapse-0-99-0/">the last version to accept self-signed certificates</link>.
+    As such, it is now recommended to use a proper certificate verified by a
+    root CA (for example Let's Encrypt).
+    The new <link linkend="module-services-matrix">manual chapter on Matrix</link> contains a working example of using nginx as a reverse proxy
+    in front of <literal>matrix-synapse</literal>, using Let's Encrypt certificates.
+   </para>
+  </listitem>
+   <listitem>
+    <para>
+     <literal>mailutils</literal> now works by default when
+     <literal>sendmail</literal> is not in a setuid wrapper. As a consequence,
+     the <literal>sendmailPath</literal> argument, having lost its main use, has
+     been removed.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     <literal>graylog</literal> has been upgraded from version 2.* to 3.*. Some setups making use of extraConfig (especially those exposing Graylog via reverse proxies) need to be updated as upstream removed/replaced some settings. See <link xlink:href="http://docs.graylog.org/en/3.0/pages/upgrade/graylog-3.0.html#simplified-http-interface-configuration">Upgrading Graylog</link> for details.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -367,6 +503,23 @@
      <option>services.matomo.package</option> which determines the used
      Matomo version.
     </para>
+    <para>
+     The Matomo module now also comes with the systemd service <literal>matomo-archive-processing.service</literal>
+     and a timer that automatically triggers archive processing every hour.
+     This means that you can safely
+     <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
+      disable browser triggers for Matomo archiving
+     </link> at <literal>Administration > System > General Settings</literal>.
+    </para>
+    <para>
+     Additionally, you can enable to
+     <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
+      delete old visitor logs
+     </link> at <literal>Administration > System > Privacy</literal>,
+     but make sure that you run <literal>systemctl start matomo-archive-processing.service</literal>
+     at least once without errors if you have already collected data before,
+     so that the reports get archived before the source data gets deleted.
+    </para>
    </listitem>
    <listitem>
     <para>
@@ -403,6 +556,26 @@
      </para>
    </listitem>
    <listitem>
+    <para>
+     The manual gained a
+     <link linkend="module-services-matrix">
+      new chapter on self-hosting <literal>matrix-synapse</literal> and <literal>riot-web</literal>
+     </link>, the most prevalent server and client implementations for the
+     <link xlink:href="https://matrix.org/">Matrix</link> federated communication network.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       The astah-community package was removed from nixpkgs due to it being discontinued and the downloads not being available anymore.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       The httpd service now saves log files with a .log file extension by default for
+       easier integration with the logrotate service.
+     </para>
+   </listitem>
+   <listitem>
      <para>
        The owncloud server packages and httpd subservice module were removed
        from nixpkgs due to the lack of maintainers.
@@ -415,7 +588,7 @@
        but is still possible by setting <literal>zramSwap.swapDevices</literal> explicitly.
      </para>
      <para>
-      Default algorithm for ZRAM swap was changed to <literal>zstd</literal>.
+      ZRAM algorithm can be changed now.
      </para>
      <para>
       Changes to ZRAM algorithm are applied during <literal>nixos-rebuild switch</literal>,
@@ -423,6 +596,111 @@
       use <literal>nixos-rebuild boot; reboot</literal>.
      </para>
    </listitem>
+   <listitem>
+    <para>
+      Flat volumes are now disabled by default in <literal>hardware.pulseaudio</literal>.
+      This has been done to prevent applications, which are unaware of this feature, setting
+      their volumes to 100% on startup causing harm to your audio hardware and potentially your ears.
+    </para>
+    <note>
+     <para>
+      With this change application specific volumes are relative to the master volume which can be
+      adjusted independently, whereas before they were absolute; meaning that in effect, it scaled the
+      device-volume with the volume of the loudest application.
+     </para>
+    </note>
+   </listitem>
+   <listitem>
+    <para>
+     The <link xlink:href="https://github.com/DanielAdolfsson/ndppd"><literal>ndppd</literal></link> module
+     now supports <link linkend="opt-services.ndppd.enable">all config options</link> provided by the current
+     upstream version as service options. Additionally the <literal>ndppd</literal> package doesn't contain
+     the systemd unit configuration from upstream anymore, the unit is completely configured by the NixOS module now.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     New installs of NixOS will default to the Redmine 4.x series unless otherwise specified in
+     <literal>services.redmine.package</literal> while existing installs of NixOS will default to
+     the Redmine 3.x series.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     The <link linkend="opt-services.grafana.enable">Grafana module</link> now supports declarative
+     <link xlink:href="http://docs.grafana.org/administration/provisioning/">datasource and dashboard</link>
+     provisioning.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       The use of insecure ports on kubernetes has been deprecated.
+       Thus options:
+       <varname>services.kubernetes.apiserver.port</varname> and
+       <varname>services.kubernetes.controllerManager.port</varname>
+       has been renamed to <varname>.insecurePort</varname>,
+       and default of both options has changed to 0 (disabled).
+     </para>
+    </listitem>
+    <listitem>
+      <para>
+        Note that the default value of
+        <varname>services.kubernetes.apiserver.bindAddress</varname>
+        has changed from 127.0.0.1 to 0.0.0.0, allowing the apiserver to be
+        accessible from outside the master node itself.
+        If the apiserver insecurePort is enabled,
+        it is strongly recommended to only bind on the loopback interface. See:
+        <varname>services.kubernetes.apiserver.insecurebindAddress</varname>.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        The option <varname>services.kubernetes.apiserver.allowPrivileged</varname>
+        and <varname>services.kubernetes.kubelet.allowPrivileged</varname> now
+        defaults to false. Disallowing privileged containers on the cluster.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        The kubernetes module does no longer add the kubernetes package to
+        <varname>environment.systemPackages</varname> implicitly.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+        The <literal>intel</literal> driver has been removed from the default list of
+        <link linkend="opt-services.xserver.videoDrivers">X.org video drivers</link>.
+        The <literal>modesetting</literal> driver should take over automatically,
+        it is better maintained upstream and has less problems with advanced X11 features.
+        This can lead to a change in the output names used by <literal>xrandr</literal>.
+        Some performance regressions on some GPU models might happen.
+        Some OpenCL and VA-API applications might also break
+        (Beignet seems to provide OpenCL support with
+        <literal>modesetting</literal> driver, too).
+        Kernel mode setting API does not support backlight control,
+        so <literal>xbacklight</literal> tool will not work;
+        backlight level can be controlled directly via <literal>/sys/</literal>
+        or with <literal>brightnessctl</literal>.
+        Users who need this functionality more than multi-output XRandR are advised
+        to add `intel` to `videoDrivers` and report an issue (or provide additional
+        details in an existing one)
+      </para>
+   </listitem>
+   <listitem>
+     <para>
+       Openmpi has been updated to version 4.0.0, which removes some deprecated MPI-1 symbols.
+       This may break some older applications that still rely on those symbols.
+       An upgrade guide can be found <link xlink:href="https://www.open-mpi.org/faq/?category=mpi-removed">here</link>.
+     </para>
+    <para>
+     The nginx package now relies on OpenSSL 1.1 and supports TLS 1.3 by default. You can set the protocols used by the nginx service using <xref linkend="opt-services.nginx.sslProtocols"/>.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       A new subcommand <command>nixos-rebuild edit</command> was added.
+     </para>
+   </listitem>
   </itemizedlist>
  </section>
 </section>
diff --git a/nixos/doc/manual/release-notes/rl-1909.xml b/nixos/doc/manual/release-notes/rl-1909.xml
new file mode 100644
index 00000000000..f54592b6bf6
--- /dev/null
+++ b/nixos/doc/manual/release-notes/rl-1909.xml
@@ -0,0 +1,68 @@
+<section xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="sec-release-19.09">
+ <title>Release 19.09 (“Loris”, 2019/09/??)</title>
+
+ <section xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="sec-release-19.09-highlights">
+  <title>Highlights</title>
+
+  <para>
+   In addition to numerous new and upgraded packages, this release has the
+   following highlights:
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para />
+   </listitem>
+  </itemizedlist>
+ </section>
+
+ <section xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="sec-release-19.09-new-services">
+  <title>New Services</title>
+
+  <para>
+   The following new services were added since the last release:
+  </para>
+
+  <itemizedlist>
+   <listitem>
+    <para />
+   </listitem>
+  </itemizedlist>
+ </section>
+
+ <section xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         version="5.0"
+         xml:id="sec-release-19.09-notable-changes">
+  <title>Other Notable Changes</title>
+
+  <itemizedlist>
+   <listitem>
+    <para>
+     The <option>documentation</option> module gained an option named
+     <option>documentation.nixos.includeAllModules</option> which makes the generated
+     <citerefentry><refentrytitle>configuration.nix</refentrytitle>
+     <manvolnum>5</manvolnum></citerefentry> manual page include all options from all NixOS modules
+     included in a given <literal>configuration.nix</literal> configuration file. Currently, it is
+     set to <literal>false</literal> by default as enabling it frequently prevents evaluation. But
+     the plan is to eventually have it set to <literal>true</literal> by default. Please set it to
+     <literal>true</literal> now in your <literal>configuration.nix</literal> and fix all the bugs
+     it uncovers.
+    </para>
+   </listitem>
+  </itemizedlist>
+ </section>
+</section>
diff --git a/nixos/lib/eval-config.nix b/nixos/lib/eval-config.nix
index 5f05b037bdd..77490ca3762 100644
--- a/nixos/lib/eval-config.nix
+++ b/nixos/lib/eval-config.nix
@@ -51,7 +51,7 @@ in rec {
   # system configuration.
   inherit (lib.evalModules {
     inherit prefix check;
-    modules = modules ++ extraModules ++ baseModules ++ [ pkgsModule ];
+    modules = baseModules ++ extraModules ++ [ pkgsModule ] ++ modules;
     args = extraArgs;
     specialArgs =
       { modulesPath = builtins.toString ../modules; } // specialArgs;
@@ -60,7 +60,7 @@ in rec {
   # These are the extra arguments passed to every module.  In
   # particular, Nixpkgs is passed through the "pkgs" argument.
   extraArgs = extraArgs_ // {
-    inherit modules baseModules;
+    inherit baseModules extraModules modules;
   };
 
   inherit (config._module.args) pkgs;
diff --git a/nixos/lib/testing.nix b/nixos/lib/testing.nix
index e68563ef48d..a13e76a6956 100644
--- a/nixos/lib/testing.nix
+++ b/nixos/lib/testing.nix
@@ -3,7 +3,7 @@
   # Use a minimal kernel?
 , minimal ? false
   # Ignored
-, config ? null
+, config ? {}
   # Modules to add to each VM
 , extraConfigurations ? [] }:
 
diff --git a/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix b/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
new file mode 100644
index 00000000000..37b46db059c
--- /dev/null
+++ b/nixos/maintainers/scripts/cloudstack/cloudstack-image.nix
@@ -0,0 +1,23 @@
+# nix-build '<nixpkgs/nixos>' -A config.system.build.cloudstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/cloudstack/cloudstack-image.nix ]; }"
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ../../../modules/virtualisation/cloudstack-config.nix ];
+
+  system.build.cloudstackImage = import ../../../lib/make-disk-image.nix {
+    inherit lib config pkgs;
+    diskSize = 8192;
+    format = "qcow2";
+    configFile = pkgs.writeText "configuration.nix"
+      ''
+        {
+          imports = [ <nixpkgs/nixos/modules/virtualisation/cloudstack-config.nix> ];
+        }
+      '';
+  };
+
+}
diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix
index eeae27ede0f..88d95e67544 100644
--- a/nixos/maintainers/scripts/ec2/amazon-image.nix
+++ b/nixos/maintainers/scripts/ec2/amazon-image.nix
@@ -53,6 +53,7 @@ in {
     pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
     partitionTableType = if config.ec2.hvm then "legacy" else "none";
     diskSize = cfg.sizeMB;
+    fsType = "ext4";
     configFile = pkgs.writeText "configuration.nix"
       ''
         {
diff --git a/nixos/maintainers/scripts/openstack/nova-image.nix b/nixos/maintainers/scripts/openstack/nova-image.nix
deleted file mode 100644
index b6f3a5b1520..00000000000
--- a/nixos/maintainers/scripts/openstack/nova-image.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-# nix-build '<nixpkgs/nixos>' -A config.system.build.novaImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/openstack/nova-image.nix ]; }"
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-{
-  imports =
-    [ ../../../modules/installer/cd-dvd/channel.nix
-      ../../../modules/virtualisation/nova-config.nix
-    ];
-
-  system.build.novaImage = import ../../../lib/make-disk-image.nix {
-    inherit lib config;
-    pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
-    diskSize = 8192;
-    format = "qcow2";
-    configFile = pkgs.writeText "configuration.nix"
-      ''
-        {
-          imports = [ <nixpkgs/nixos/modules/virtualisation/nova-config.nix> ];
-        }
-      '';
-  };
-
-}
diff --git a/nixos/maintainers/scripts/openstack/openstack-image.nix b/nixos/maintainers/scripts/openstack/openstack-image.nix
new file mode 100644
index 00000000000..4c464f43f61
--- /dev/null
+++ b/nixos/maintainers/scripts/openstack/openstack-image.nix
@@ -0,0 +1,26 @@
+# nix-build '<nixpkgs/nixos>' -A config.system.build.openstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/openstack/openstack-image.nix ]; }"
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports =
+    [ ../../../modules/installer/cd-dvd/channel.nix
+      ../../../modules/virtualisation/openstack-config.nix
+    ];
+
+  system.build.openstackImage = import ../../../lib/make-disk-image.nix {
+    inherit lib config;
+    pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package
+    diskSize = 8192;
+    format = "qcow2";
+    configFile = pkgs.writeText "configuration.nix"
+      ''
+        {
+          imports = [ <nixpkgs/nixos/modules/virtualisation/openstack-config.nix> ];
+        }
+      '';
+  };
+
+}
diff --git a/nixos/modules/config/fonts/fontconfig-penultimate.nix b/nixos/modules/config/fonts/fontconfig-penultimate.nix
index 7e05e77d967..2c18244621a 100644
--- a/nixos/modules/config/fonts/fontconfig-penultimate.nix
+++ b/nixos/modules/config/fonts/fontconfig-penultimate.nix
@@ -55,7 +55,9 @@ let
   localConf = pkgs.writeText "fc-local.conf" cfg.localConf;
 
   # The configuration to be included in /etc/font/
-  penultimateConf = pkgs.runCommand "font-penultimate-conf" {} ''
+  penultimateConf = pkgs.runCommand "font-penultimate-conf" {
+    preferLocalBuild = true;
+    } ''
     support_folder=$out/etc/fonts/conf.d
     latest_folder=$out/etc/fonts/${latestVersion}/conf.d
 
diff --git a/nixos/modules/config/fonts/fontconfig-ultimate.nix b/nixos/modules/config/fonts/fontconfig-ultimate.nix
index 7549dc6c065..45328f3eaf1 100644
--- a/nixos/modules/config/fonts/fontconfig-ultimate.nix
+++ b/nixos/modules/config/fonts/fontconfig-ultimate.nix
@@ -7,7 +7,7 @@ let cfg = config.fonts.fontconfig.ultimate;
     latestVersion  = pkgs.fontconfig.configVersion;
 
     # The configuration to be included in /etc/font/
-    confPkg = pkgs.runCommand "font-ultimate-conf" {} ''
+    confPkg = pkgs.runCommand "font-ultimate-conf" { preferLocalBuild = true; } ''
       support_folder=$out/etc/fonts/conf.d
       latest_folder=$out/etc/fonts/${latestVersion}/conf.d
 
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
index 12f5ca2e799..d79c43c0b5b 100644
--- a/nixos/modules/config/fonts/fontconfig.nix
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -190,7 +190,7 @@ let cfg = config.fonts.fontconfig;
     '';
 
     # fontconfig configuration package
-    confPkg = pkgs.runCommand "fontconfig-conf" {} ''
+    confPkg = pkgs.runCommand "fontconfig-conf" { preferLocalBuild = true; } ''
       support_folder=$out/etc/fonts
       latest_folder=$out/etc/fonts/${latestVersion}
 
diff --git a/nixos/modules/config/fonts/fontdir.nix b/nixos/modules/config/fonts/fontdir.nix
index 180e38f81f4..cc70fbf8744 100644
--- a/nixos/modules/config/fonts/fontdir.nix
+++ b/nixos/modules/config/fonts/fontdir.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
 
-  x11Fonts = pkgs.runCommand "X11-fonts" { } ''
+  x11Fonts = pkgs.runCommand "X11-fonts" { preferLocalBuild = true; } ''
     mkdir -p "$out/share/X11-fonts"
     find ${toString config.fonts.fonts} \
       \( -name fonts.dir -o -name '*.ttf' -o -name '*.otf' \) \
diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix
index 0693e896f71..f65a3fc50d5 100644
--- a/nixos/modules/config/ldap.nix
+++ b/nixos/modules/config/ldap.nix
@@ -38,6 +38,8 @@ let
       bind_timelimit ${toString cfg.bind.timeLimit}
       ${optionalString (cfg.bind.distinguishedName != "")
         "binddn ${cfg.bind.distinguishedName}" }
+      ${optionalString (cfg.daemon.rootpwmoddn != "")
+        "rootpwmoddn ${cfg.daemon.rootpwmoddn}" }
       ${optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig }
     '';
   };
@@ -126,6 +128,26 @@ in
             the end of the nslcd configuration file (nslcd.conf).
           '' ;
         } ;
+
+        rootpwmoddn = mkOption {
+          default = "";
+          example = "cn=admin,dc=example,dc=com";
+          type = types.str;
+          description = ''
+            The distinguished name to use to bind to the LDAP server
+            when the root user tries to modify a user's password.
+          '';
+        };
+
+        rootpwmodpw = mkOption {
+          default = "";
+          example = "/run/keys/nslcd.rootpwmodpw";
+          type = types.str;
+          description = ''
+            The path to a file containing the credentials with which
+            to bind to the LDAP server if the root user tries to change a user's password
+          '';
+        };
       };
 
       bind = {
@@ -203,9 +225,11 @@ in
     system.activationScripts = mkIf insertLdapPassword {
       ldap = stringAfter [ "etc" "groups" "users" ] ''
         if test -f "${cfg.bind.password}" ; then
-          echo "bindpw "$(cat ${cfg.bind.password})"" | cat ${ldapConfig.source} - > /etc/ldap.conf.bindpw
-          mv -fT /etc/ldap.conf.bindpw /etc/ldap.conf
-          chmod 600 /etc/ldap.conf
+          umask 0077
+          conf="$(mktemp)"
+          printf 'bindpw %s\n' "$(cat ${cfg.bind.password})" |
+          cat ${ldapConfig.source} - >"$conf"
+          mv -fT "$conf" /etc/ldap.conf
         fi
       '';
     };
@@ -232,21 +256,31 @@ in
         wantedBy = [ "multi-user.target" ];
 
         preStart = ''
-          mkdir -p /run/nslcd
-          rm -f /run/nslcd/nslcd.pid;
-          chown nslcd.nslcd /run/nslcd
-          ${optionalString (cfg.bind.distinguishedName != "") ''
-            if test -s "${cfg.bind.password}" ; then
-              ln -sfT "${cfg.bind.password}" /run/nslcd/bindpw
-            fi
-          ''}
+          umask 0077
+          conf="$(mktemp)"
+          {
+            cat ${nslcdConfig.source}
+            test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.password}' ||
+            printf 'bindpw %s\n' "$(cat '${cfg.bind.password}')"
+            test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpw}' ||
+            printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpw}')"
+          } >"$conf"
+          mv -fT "$conf" /etc/nslcd.conf
         '';
 
+        # NOTE: because one cannot pass a custom config path to `nslcd`
+        # (which is only able to use `/etc/nslcd.conf`)
+        # changes in `nslcdConfig` won't change `serviceConfig`,
+        # and thus won't restart `nslcd`.
+        # Therefore `restartTriggers` is used on `/etc/nslcd.conf`.
+        restartTriggers = [ nslcdConfig.source ];
+
         serviceConfig = {
           ExecStart = "${nss_pam_ldapd}/sbin/nslcd";
           Type = "forking";
           PIDFile = "/run/nslcd/nslcd.pid";
           Restart = "always";
+          RuntimeDirectory = [ "nslcd" ];
         };
       };
 
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
index 37e66c64542..9d202347702 100644
--- a/nixos/modules/config/no-x-libs.nix
+++ b/nixos/modules/config/no-x-libs.nix
@@ -34,7 +34,7 @@ with lib;
       networkmanager-openvpn = super.networkmanager-openvpn.override { withGnome = false; };
       networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; };
       networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; };
-      pinentry = super.pinentry_ncurses;
+      pinentry = super.pinentry.override { gtk2 = null; qt = null; };
       gobject-introspection = super.gobject-introspection.override { x11Support = false; };
     }));
   };
diff --git a/nixos/modules/config/nsswitch.nix b/nixos/modules/config/nsswitch.nix
index b601e908e49..13277fe56e4 100644
--- a/nixos/modules/config/nsswitch.nix
+++ b/nixos/modules/config/nsswitch.nix
@@ -61,6 +61,15 @@ in {
         };
     };
 
+    system.nssHosts = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "mdns" ];
+      description = ''
+        List of host entries to configure in <filename>/etc/nsswitch.conf</filename>.
+      '';
+    };
+
   };
 
   config = {
@@ -85,7 +94,7 @@ in {
       group:     ${concatStringsSep " " passwdArray}
       shadow:    ${concatStringsSep " " shadowArray}
 
-      hosts:     ${concatStringsSep " " hostArray}
+      hosts:     ${concatStringsSep " " config.system.nssHosts}
       networks:  files
 
       ethers:    files
@@ -94,6 +103,8 @@ in {
       rpc:       files
     '';
 
+    system.nssHosts = hostArray;
+
     # Systemd provides nss-myhostname to ensure that our hostname
     # always resolves to a valid IP address.  It returns all locally
     # configured IP addresses, or ::1 and 127.0.0.2 as
diff --git a/nixos/modules/config/pulseaudio.nix b/nixos/modules/config/pulseaudio.nix
index 67f7105fe2f..e61a3a73120 100644
--- a/nixos/modules/config/pulseaudio.nix
+++ b/nixos/modules/config/pulseaudio.nix
@@ -180,7 +180,7 @@ in {
           type = types.attrsOf types.unspecified;
           default = {};
           description = ''Config of the pulse daemon. See <literal>man pulse-daemon.conf</literal>.'';
-          example = literalExample ''{ flat-volumes = "no"; }'';
+          example = literalExample ''{ realtime-scheduling = "yes"; }'';
         };
       };
 
@@ -242,6 +242,9 @@ in {
           source = writeText "libao.conf" "default_driver=pulse"; }
       ];
 
+      # Disable flat volumes to enable relative ones
+      hardware.pulseaudio.daemon.config.flat-volumes = mkDefault "no";
+
       # Allow PulseAudio to get realtime priority using rtkit.
       security.rtkit.enable = true;
 
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 925d945c081..5d411c73a56 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -91,13 +91,13 @@ in
       };
 
       algorithm = mkOption {
-        default = "zstd";
-        example = "lzo";
+        default = "lzo";
+        example = "lz4";
         type = with types; either (enum [ "lzo" "lz4" "zstd" ]) str;
         description = ''
           Compression algorithm. <literal>lzo</literal> has good compression,
           but is slow. <literal>lz4</literal> has bad compression, but is fast.
-          <literal>zstd</literal> is both good compression and fast.
+          <literal>zstd</literal> is both good compression and fast, but requires newer kernel.
           You can check what other algorithms are supported by your zram device with
           <programlisting>cat /sys/class/block/zram*/comp_algorithm</programlisting>
         '';
diff --git a/nixos/modules/hardware/acpilight.nix b/nixos/modules/hardware/acpilight.nix
new file mode 100644
index 00000000000..34e8a222096
--- /dev/null
+++ b/nixos/modules/hardware/acpilight.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.hardware.acpilight;
+in
+{
+  options = {
+    hardware.acpilight = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enable acpilight.
+          This will allow brightness control via xbacklight from users in the video group
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = with pkgs; [ acpilight ];
+  };
+}
diff --git a/nixos/modules/hardware/ledger.nix b/nixos/modules/hardware/ledger.nix
new file mode 100644
index 00000000000..41abe74315a
--- /dev/null
+++ b/nixos/modules/hardware/ledger.nix
@@ -0,0 +1,14 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.ledger;
+
+in {
+  options.hardware.ledger.enable = mkEnableOption "udev rules for Ledger devices";
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.ledger-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index 6ba8130af71..80ea7bc5d5c 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -172,6 +172,11 @@ in
     environment.systemPackages = [ nvidia_x11.bin nvidia_x11.settings ]
       ++ lib.filter (p: p != null) [ nvidia_x11.persistenced ];
 
+    systemd.tmpfiles.rules = optional config.virtualisation.docker.enableNvidia
+        "L+ /run/nvidia-docker/bin - - - - ${nvidia_x11.bin}/origBin"
+      ++ optional (nvidia_x11.persistenced != null && config.virtualisation.docker.enableNvidia)
+        "L+ /run/nvidia-docker/extras/bin/nvidia-persistenced - - - - ${nvidia_x11.persistenced}/origBin/nvidia-persistenced";
+
     boot.extraModulePackages = [ nvidia_x11.bin ];
 
     # nvidia-uvm is required by CUDA applications.
diff --git a/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix b/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix
index 2cf5f13bc15..a808429c999 100644
--- a/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix
+++ b/nixos/modules/hardware/video/uvcvideo/uvcdynctrl-udev-rules.nix
@@ -29,6 +29,7 @@ runCommand "uvcdynctrl-udev-rules-${version}"
   ];
   dontPatchELF = true;
   dontStrip = true;
+  preferLocalBuild = true;
 }
 ''
   mkdir -p "$out/lib/udev"
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index e946c4abc57..ab5e7c0645f 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -13,7 +13,7 @@ let
   # user, as expected by nixos-rebuild/nixos-install. FIXME: merge
   # with make-channel.nix.
   channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}"
-    { }
+    { preferLocalBuild = true; }
     ''
       mkdir -p $out
       cp -prd ${nixpkgs.outPath} $out/nixos
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
index 228ef371d25..917b3758d38 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
@@ -31,6 +31,10 @@ with lib;
   # there is no power management backend such as upower).
   powerManagement.enable = true;
 
+  # Enable sound in graphical iso's.
+  hardware.pulseaudio.enable = true;
+  hardware.pulseaudio.systemWide = true; # Needed since we run plasma as root.
+
   environment.systemPackages = [
     # Include gparted for partitioning disks.
     pkgs.gparted
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 2d41370e8ae..d71e06202e3 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -341,11 +341,11 @@ let
     #   dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i)
     ''
       mkdir ./contents && cd ./contents
-      cp -rp "${efiDir}"/* .
+      cp -rp "${efiDir}"/EFI .
       mkdir ./boot
       cp -p "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}" \
         "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}" ./boot/
-      touch --date=@0 ./*
+      touch --date=@0 ./EFI ./boot
 
       usage_size=$(du -sb --apparent-size . | tr -cd '[:digit:]')
       # Make the image 110% as big as the files need to make up for FAT overhead
@@ -357,7 +357,7 @@ let
       echo "Image size: $image_size"
       truncate --size=$image_size "$out"
       ${pkgs.libfaketime}/bin/faketime "2000-01-01 00:00:00" ${pkgs.dosfstools}/sbin/mkfs.vfat -i 12345678 -n EFIBOOT "$out"
-      mcopy -psvm -i "$out" ./* ::
+      mcopy -psvm -i "$out" ./EFI ./boot ::
       # Verify the FAT partition.
       ${pkgs.dosfstools}/sbin/fsck.vfat -vn "$out"
     ''; # */
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index bad9356ab5a..686204ee034 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -340,6 +340,8 @@ foreach my $fs (read_file("/proc/self/mountinfo")) {
     chomp $fs;
     my @fields = split / /, $fs;
     my $mountPoint = $fields[4];
+    $mountPoint =~ s/\\040/ /g; # account for mount points with spaces in the name (\040 is the escape character)
+    $mountPoint =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
     next unless -d $mountPoint;
     my @mountOptions = split /,/, $fields[5];
 
@@ -355,6 +357,8 @@ foreach my $fs (read_file("/proc/self/mountinfo")) {
     my $fsType = $fields[$n];
     my $device = $fields[$n + 1];
     my @superOptions = split /,/, $fields[$n + 2];
+    $device =~ s/\\040/ /g; # account for devices with spaces in the name (\040 is the escape character)
+    $device =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
 
     # Skip the read-only bind-mount on /nix/store.
     next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions) && (grep { $_ eq "ro" } @mountOptions);
@@ -635,9 +639,9 @@ $bootLoaderConfig
   # services.xserver.desktopManager.plasma5.enable = true;
 
   # Define a user account. Don't forget to set a password with ‘passwd’.
-  # users.users.guest = {
+  # users.users.jane = {
   #   isNormalUser = true;
-  #   uid = 1000;
+  #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
   # };
 
   # This value determines the NixOS release with which your system is to be
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index defc46ad2a7..8685cb345e1 100644
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -138,7 +138,18 @@ fi
 # Ask the user to set a root password, but only if the passwd command
 # exists (i.e. when mutable user accounts are enabled).
 if [[ -z $noRootPasswd ]] && [ -t 0 ]; then
-    nixos-enter --root "$mountPoint" -c '[[ -e /nix/var/nix/profiles/system/sw/bin/passwd ]] && echo "setting root password..." && /nix/var/nix/profiles/system/sw/bin/passwd'
+    if nixos-enter --root "$mountPoint" -c 'test -e /nix/var/nix/profiles/system/sw/bin/passwd'; then
+        set +e
+        nixos-enter --root "$mountPoint" -c 'echo "setting root password..." && /nix/var/nix/profiles/system/sw/bin/passwd'
+        exit_code=$?
+        set -e
+
+        if [[ $exit_code != 0 ]]; then
+            echo "Setting a root password failed with the above printed error."
+            echo "You can set the root password manually by executing \`nixos-enter --root ${mountPoint@Q}\` and then running \`passwd\` in the shell of the new system."
+            exit $exit_code
+        fi
+    fi
 fi
 
 echo "installation finished!"
diff --git a/nixos/modules/installer/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nixos-rebuild.sh
index 361c2e49e05..6a08c9b4c6c 100644
--- a/nixos/modules/installer/tools/nixos-rebuild.sh
+++ b/nixos/modules/installer/tools/nixos-rebuild.sh
@@ -29,7 +29,7 @@ while [ "$#" -gt 0 ]; do
       --help)
         showSyntax
         ;;
-      switch|boot|test|build|dry-build|dry-run|dry-activate|build-vm|build-vm-with-bootloader)
+      switch|boot|test|build|edit|dry-build|dry-run|dry-activate|build-vm|build-vm-with-bootloader)
         if [ "$i" = dry-run ]; then i=dry-build; fi
         action="$i"
         ;;
@@ -227,6 +227,13 @@ if [ -z "$_NIXOS_REBUILD_REEXEC" -a -n "$canRun" -a -z "$fast" ]; then
     fi
 fi
 
+# Find configuration.nix and open editor instead of building.
+if [ "$action" = edit ]; then
+    NIXOS_CONFIG=${NIXOS_CONFIG:-$(nix-instantiate --find-file nixos-config)}
+    exec "${EDITOR:-nano}" "$NIXOS_CONFIG"
+    exit 1
+fi
+
 
 tmpDir=$(mktemp -t -d nixos-rebuild.XXXXXX)
 SSHOPTS="$NIX_SSHOPTS -o ControlMaster=auto -o ControlPath=$tmpDir/ssh-%n -o ControlPersist=60"
@@ -260,6 +267,14 @@ if [ -n "$rollback" -o "$action" = dry-build ]; then
     buildNix=
 fi
 
+nixSystem() {
+    machine="$(uname -m)"
+    if [[ "$machine" =~ i.86 ]]; then
+        machine=i686
+    fi
+    echo $machine-linux
+}
+
 prebuiltNix() {
     machine="$1"
     if [ "$machine" = x86_64 ]; then
@@ -279,7 +294,9 @@ if [ -n "$buildNix" ]; then
     nixDrv=
     if ! nixDrv="$(nix-instantiate '<nixpkgs/nixos>' --add-root $tmpDir/nix.drv --indirect -A config.nix.package.out "${extraBuildFlags[@]}")"; then
         if ! nixDrv="$(nix-instantiate '<nixpkgs>' --add-root $tmpDir/nix.drv --indirect -A nix "${extraBuildFlags[@]}")"; then
-            nixStorePath="$(prebuiltNix "$(uname -m)")"
+            if ! nixStorePath="$(nix-instantiate --eval '<nixpkgs/nixos/modules/installer/tools/nix-fallback-paths.nix>' -A $(nixSystem) | sed -e 's/^"//' -e 's/"$//')"; then
+                nixStorePath="$(prebuiltNix "$(uname -m)")"
+            fi
             if ! nix-store -r $nixStorePath --add-root $tmpDir/nix --indirect \
                 --option extra-binary-caches https://cache.nixos.org/; then
                 echo "warning: don't know how to get latest Nix" >&2
diff --git a/nixos/modules/installer/virtualbox-demo.nix b/nixos/modules/installer/virtualbox-demo.nix
index 2e1b4b3998b..af3e1aecca7 100644
--- a/nixos/modules/installer/virtualbox-demo.nix
+++ b/nixos/modules/installer/virtualbox-demo.nix
@@ -57,7 +57,5 @@ with lib;
 
   # Enable the OpenSSH daemon.
   # services.openssh.enable = true;
-
-  system.stateVersion = mkDefault "18.03";
   '';
 }
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 09d53c322fb..834ac0de912 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, baseModules, ... }:
+{ config, lib, pkgs, baseModules, extraModules, modules, ... }:
 
 with lib;
 
@@ -6,6 +6,8 @@ let
 
   cfg = config.documentation;
 
+  manualModules = baseModules ++ optionals cfg.nixos.includeAllModules (extraModules ++ modules);
+
   /* For the purpose of generating docs, evaluate options with each derivation
     in `pkgs` (recursively) replaced by a fake with path "\${pkgs.attribute.path}".
     It isn't perfect, but it seems to cover a vast majority of use cases.
@@ -18,7 +20,7 @@ let
     options =
       let
         scrubbedEval = evalModules {
-          modules = [ { nixpkgs.localSystem = config.nixpkgs.localSystem; } ] ++ baseModules;
+          modules = [ { nixpkgs.localSystem = config.nixpkgs.localSystem; } ] ++ manualModules;
           args = (config._module.args) // { modules = [ ]; };
           specialArgs = { pkgs = scrubDerivations "pkgs" pkgs; };
         };
@@ -146,6 +148,17 @@ in
         '';
       };
 
+      nixos.includeAllModules = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether the generated NixOS's documentation should include documentation for all
+          the options from all the NixOS modules included in the current
+          <literal>configuration.nix</literal>. Disabling this will make the manual
+          generator to ignore options defined outside of <literal>baseModules</literal>.
+        '';
+      };
+
     };
 
   };
@@ -156,6 +169,7 @@ in
       environment.systemPackages = [ pkgs.man-db ];
       environment.pathsToLink = [ "/share/man" ];
       environment.extraOutputsToInstall = [ "man" ] ++ optional cfg.dev.enable "devman";
+      environment.etc."man.conf".source = "${pkgs.man-db}/etc/man_db.conf";
     })
 
     (mkIf cfg.info.enable {
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index d6e6ccaecd2..e78673514e3 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -272,7 +272,7 @@
       nzbget = 245;
       mosquitto = 246;
       toxvpn = 247;
-      squeezelite = 248;
+      # squeezelite = 248; # DynamicUser = true
       turnserver = 249;
       smokeping = 250;
       gocd-agent = 251;
@@ -290,7 +290,7 @@
       riak-cs = 263;
       infinoted = 264;
       sickbeard = 265;
-      # glance = 266; # unused, removed 2017-12-13
+      headphones = 266;
       couchpotato = 267;
       gogs = 268;
       pdns-recursor = 269;
@@ -590,7 +590,7 @@
       riak-cs = 263;
       infinoted = 264;
       sickbeard = 265;
-      # glance = 266; # unused, removed 2017-12-13
+      headphones = 266;
       couchpotato = 267;
       gogs = 268;
       kresd = 270;
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index 001505320c0..c576cf4cb92 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -36,14 +36,14 @@ in
     nixos.revision = mkOption {
       internal = true;
       type = types.str;
-      default = lib.trivial.revisionWithDefault "master";
+      default = trivial.revisionWithDefault "master";
       description = "The Git revision from which this NixOS configuration was built.";
     };
 
     nixos.codeName = mkOption {
       readOnly = true;
       type = types.str;
-      default = lib.trivial.codeName;
+      default = trivial.codeName;
       description = "The NixOS release code name (e.g. <literal>Emu</literal>).";
     };
 
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1a8bd9cccb1..91645a57b49 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -35,6 +35,7 @@
   ./config/users-groups.nix
   ./config/vpnc.nix
   ./config/zram.nix
+  ./hardware/acpilight.nix
   ./hardware/all-firmware.nix
   ./hardware/bladeRF.nix
   ./hardware/brightnessctl.nix
@@ -44,6 +45,7 @@
   ./hardware/digitalbitbox.nix
   ./hardware/sensor/iio.nix
   ./hardware/ksm.nix
+  ./hardware/ledger.nix
   ./hardware/mcelog.nix
   ./hardware/network/b43.nix
   ./hardware/nitrokey.nix
@@ -82,6 +84,7 @@
   ./misc/version.nix
   ./programs/adb.nix
   ./programs/atop.nix
+  ./programs/autojump.nix
   ./programs/bash/bash.nix
   ./programs/bcc.nix
   ./programs/blcr.nix
@@ -101,6 +104,7 @@
   ./programs/gnupg.nix
   ./programs/gphoto2.nix
   ./programs/iftop.nix
+  ./programs/iotop.nix
   ./programs/java.nix
   ./programs/kbdlight.nix
   ./programs/less.nix
@@ -126,7 +130,6 @@
   ./programs/sysdig.nix
   ./programs/systemtap.nix
   ./programs/sway.nix
-  ./programs/sway-beta.nix
   ./programs/thefuck.nix
   ./programs/tmux.nix
   ./programs/udevil.nix
@@ -134,6 +137,7 @@
   ./programs/vim.nix
   ./programs/wavemon.nix
   ./programs/way-cooler.nix
+  ./programs/waybar.nix
   ./programs/wireshark.nix
   ./programs/xfs_quota.nix
   ./programs/xonsh.nix
@@ -179,6 +183,7 @@
   ./services/audio/mpd.nix
   ./services/audio/mopidy.nix
   ./services/audio/slimserver.nix
+  ./services/audio/snapserver.nix
   ./services/audio/squeezelite.nix
   ./services/audio/ympd.nix
   ./services/backup/bacula.nix
@@ -186,6 +191,7 @@
   ./services/backup/duplicati.nix
   ./services/backup/crashplan.nix
   ./services/backup/crashplan-small-business.nix
+  ./services/backup/duplicity.nix
   ./services/backup/mysql-backup.nix
   ./services/backup/postgresql-backup.nix
   ./services/backup/restic.nix
@@ -194,9 +200,17 @@
   ./services/backup/tarsnap.nix
   ./services/backup/znapzend.nix
   ./services/cluster/hadoop/default.nix
+  ./services/cluster/kubernetes/addons/dns.nix
+  ./services/cluster/kubernetes/addons/dashboard.nix
+  ./services/cluster/kubernetes/addon-manager.nix
+  ./services/cluster/kubernetes/apiserver.nix
+  ./services/cluster/kubernetes/controller-manager.nix
   ./services/cluster/kubernetes/default.nix
-  ./services/cluster/kubernetes/dns.nix
-  ./services/cluster/kubernetes/dashboard.nix
+  ./services/cluster/kubernetes/flannel.nix
+  ./services/cluster/kubernetes/kubelet.nix
+  ./services/cluster/kubernetes/pki.nix
+  ./services/cluster/kubernetes/proxy.nix
+  ./services/cluster/kubernetes/scheduler.nix
   ./services/computing/boinc/client.nix
   ./services/computing/torque/server.nix
   ./services/computing/torque/mom.nix
@@ -241,6 +255,8 @@
   ./services/desktops/bamf.nix
   ./services/desktops/dleyna-renderer.nix
   ./services/desktops/dleyna-server.nix
+  ./services/desktops/pantheon/contractor.nix
+  ./services/desktops/pantheon/files.nix
   ./services/desktops/flatpak.nix
   ./services/desktops/geoclue2.nix
   ./services/desktops/gsignond.nix
@@ -255,6 +271,7 @@
   ./services/desktops/gnome3/gnome-online-accounts.nix
   ./services/desktops/gnome3/gnome-remote-desktop.nix
   ./services/desktops/gnome3/gnome-online-miners.nix
+  ./services/desktops/gnome3/gnome-settings-daemon.nix
   ./services/desktops/gnome3/gnome-terminal-server.nix
   ./services/desktops/gnome3/gnome-user-share.nix
   ./services/desktops/gnome3/gpaste.nix
@@ -266,6 +283,7 @@
   ./services/desktops/gnome3/tracker-miners.nix
   ./services/desktops/profile-sync-daemon.nix
   ./services/desktops/telepathy.nix
+  ./services/desktops/tumbler.nix
   ./services/desktops/zeitgeist.nix
   ./services/development/bloop.nix
   ./services/development/hoogle.nix
@@ -279,6 +297,7 @@
   ./services/hardware/acpid.nix
   ./services/hardware/actkbd.nix
   ./services/hardware/bluetooth.nix
+  ./services/hardware/bolt.nix
   ./services/hardware/brltty.nix
   ./services/hardware/freefall.nix
   ./services/hardware/fwupd.nix
@@ -293,6 +312,7 @@
   ./services/hardware/ratbagd.nix
   ./services/hardware/sane.nix
   ./services/hardware/sane_extra_backends/brscan4.nix
+  ./services/hardware/sane_extra_backends/dsseries.nix
   ./services/hardware/tcsd.nix
   ./services/hardware/tlp.nix
   ./services/hardware/thinkfan.nix
@@ -322,6 +342,7 @@
   ./services/logging/syslog-ng.nix
   ./services/logging/syslogd.nix
   ./services/mail/clamsmtp.nix
+  ./services/mail/davmail.nix
   ./services/mail/dkimproxy-out.nix
   ./services/mail/dovecot.nix
   ./services/mail/dspam.nix
@@ -347,6 +368,7 @@
   ./services/misc/apache-kafka.nix
   ./services/misc/autofs.nix
   ./services/misc/autorandr.nix
+  ./services/misc/beanstalkd.nix
   ./services/misc/bees.nix
   ./services/misc/bepasty.nix
   ./services/misc/canto-daemon.nix
@@ -379,6 +401,7 @@
   ./services/misc/gogs.nix
   ./services/misc/gollum.nix
   ./services/misc/gpsd.nix
+  ./services/misc/headphones.nix
   ./services/misc/home-assistant.nix
   ./services/misc/ihaskell.nix
   ./services/misc/irkerd.nix
@@ -407,7 +430,7 @@
   ./services/misc/parsoid.nix
   ./services/misc/phd.nix
   ./services/misc/plex.nix
-  ./services/misc/plexpy.nix
+  ./services/misc/tautulli.nix
   ./services/misc/pykms.nix
   ./services/misc/radarr.nix
   ./services/misc/redmine.nix
@@ -511,6 +534,7 @@
   ./services/networking/cntlm.nix
   ./services/networking/connman.nix
   ./services/networking/consul.nix
+  ./services/networking/coredns.nix
   ./services/networking/coturn.nix
   ./services/networking/dante.nix
   ./services/networking/ddclient.nix
@@ -556,6 +580,7 @@
   ./services/networking/keepalived/default.nix
   ./services/networking/keybase.nix
   ./services/networking/kippo.nix
+  ./services/networking/knot.nix
   ./services/networking/kresd.nix
   ./services/networking/lambdabot.nix
   ./services/networking/libreswan.nix
@@ -681,6 +706,7 @@
   ./services/security/hologram-server.nix
   ./services/security/hologram-agent.nix
   ./services/security/munge.nix
+  ./services/security/nginx-sso.nix
   ./services/security/oauth2_proxy.nix
   ./services/security/oauth2_proxy_nginx.nix
   ./services/security/physlock.nix
@@ -714,6 +740,8 @@
   ./services/web-apps/atlassian/jira.nix
   ./services/web-apps/codimd.nix
   ./services/web-apps/frab.nix
+  ./services/web-apps/icingaweb2/icingaweb2.nix
+  ./services/web-apps/icingaweb2/module-monitoring.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/nextcloud.nix
   ./services/web-apps/nexus.nix
@@ -740,6 +768,7 @@
   ./services/web-servers/nginx/default.nix
   ./services/web-servers/nginx/gitweb.nix
   ./services/web-servers/phpfpm/default.nix
+  ./services/web-servers/unit/default.nix
   ./services/web-servers/shellinabox.nix
   ./services/web-servers/tomcat.nix
   ./services/web-servers/traefik.nix
diff --git a/nixos/modules/profiles/graphical.nix b/nixos/modules/profiles/graphical.nix
index fba756391b1..649f5564ac6 100644
--- a/nixos/modules/profiles/graphical.nix
+++ b/nixos/modules/profiles/graphical.nix
@@ -14,5 +14,9 @@
     libinput.enable = true; # for touchpad support on many laptops
   };
 
+  # Enable sound in virtualbox appliances.
+  hardware.pulseaudio.enable = true;
+  hardware.pulseaudio.systemWide = true; # Needed since we run plasma as root.
+
   environment.systemPackages = [ pkgs.glxinfo pkgs.firefox ];
 }
diff --git a/nixos/modules/profiles/minimal.nix b/nixos/modules/profiles/minimal.nix
index 809bedc588f..f044e6f39ea 100644
--- a/nixos/modules/profiles/minimal.nix
+++ b/nixos/modules/profiles/minimal.nix
@@ -13,5 +13,5 @@ with lib;
 
   documentation.enable = mkDefault false;
 
-  services.nixosManual.enable = mkDefault false;
+  documentation.nixos.enable = mkDefault false;
 }
diff --git a/nixos/modules/programs/autojump.nix b/nixos/modules/programs/autojump.nix
new file mode 100644
index 00000000000..3a8feec4bb4
--- /dev/null
+++ b/nixos/modules/programs/autojump.nix
@@ -0,0 +1,33 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.autojump;
+  prg = config.programs;
+in
+{
+  options = {
+    programs.autojump = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable autojump.
+        '';
+      };
+    };
+  }; 
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    environment.pathsToLink = [ "/share/autojump" ];
+    environment.systemPackages = [ pkgs.autojump ];
+
+    programs.bash.interactiveShellInit = "source ${pkgs.autojump}/share/autojump/autojump.bash"; 
+    programs.zsh.interactiveShellInit = mkIf prg.zsh.enable "source ${pkgs.autojump}/share/autojump/autojump.zsh";
+    programs.fish.interactiveShellInit = mkIf prg.fish.enable "source ${pkgs.autojump}/share/autojump/autojump.fish";
+  };
+}
diff --git a/nixos/modules/programs/bash/bash.nix b/nixos/modules/programs/bash/bash.nix
index d22f9dfa319..d53c6b318f1 100644
--- a/nixos/modules/programs/bash/bash.nix
+++ b/nixos/modules/programs/bash/bash.nix
@@ -102,7 +102,7 @@ in
               # Emacs term mode doesn't support xterm title escape sequence (\e]0;)
               PS1="\n\[\033[$PROMPT_COLOR\][\u@\h:\w]\\$\[\033[0m\] "
             else
-              PS1="\n\[\033[$PROMPT_COLOR\][\[\e]0;\u@\h: \w\a\]\u@\h:\w]\$\[\033[0m\] "
+              PS1="\n\[\033[$PROMPT_COLOR\][\[\e]0;\u@\h: \w\a\]\u@\h:\w]\\$\[\033[0m\] "
             fi
             if test "$TERM" = "xterm"; then
               PS1="\[\033]2;\h:\u:\w\007\]$PS1"
diff --git a/nixos/modules/programs/fish.nix b/nixos/modules/programs/fish.nix
index b38af07b92c..bcb5a3f341b 100644
--- a/nixos/modules/programs/fish.nix
+++ b/nixos/modules/programs/fish.nix
@@ -169,6 +169,59 @@ in
       end
     '';
 
+    programs.fish.interactiveShellInit = ''
+      # add completions generated by NixOS to $fish_complete_path
+      begin
+        # joins with null byte to acommodate all characters in paths, then respectively gets all paths before (exclusive) / after (inclusive) the first one including "generated_completions",
+        # splits by null byte, and then removes all empty lines produced by using 'string'
+        set -l prev (string join0 $fish_complete_path | string match --regex "^.*?(?=\x00[^\x00]*generated_completions.*)" | string split0 | string match -er ".")
+        set -l post (string join0 $fish_complete_path | string match --regex "[^\x00]*generated_completions.*" | string split0 | string match -er ".")
+        set fish_complete_path $prev "/etc/fish/generated_completions" $post
+      end
+    '';
+
+    environment.etc."fish/generated_completions".source =
+      let
+        patchedGenerator = pkgs.stdenv.mkDerivation {
+          name = "fish_patched-completion-generator";
+          srcs = [
+            "${pkgs.fish}/share/fish/tools/create_manpage_completions.py"
+            "${pkgs.fish}/share/fish/tools/deroff.py"
+          ];
+          unpackCmd = "cp $curSrc $(basename $curSrc)";
+          sourceRoot = ".";
+          patches = [ ./fish_completion-generator.patch ]; # to prevent collisions of identical completion files
+          dontBuild = true;
+          installPhase = ''
+            mkdir -p $out
+            cp * $out/
+          '';
+          preferLocalBuild = true;
+          allowSubstitutes = false;
+        };
+        generateCompletions = package: pkgs.runCommand
+          "${package.name}_fish-completions"
+          (
+            {
+              inherit package;
+              preferLocalBuild = true;
+              allowSubstitutes = false;
+            }
+            // optionalAttrs (package ? meta.priority) { meta.priority = package.meta.priority; }
+          )
+          ''
+            mkdir -p $out
+            if [ -d $package/share/man ]; then
+              find $package/share/man -type f | xargs ${pkgs.python3.interpreter} ${patchedGenerator}/create_manpage_completions.py --directory $out >/dev/null
+            fi
+          '';
+      in
+        pkgs.buildEnv {
+          name = "system_fish-completions";
+          ignoreCollisions = true;
+          paths = map generateCompletions config.environment.systemPackages;
+        };
+
     # include programs that bring their own completions
     environment.pathsToLink = []
       ++ optional cfg.vendor.config.enable "/share/fish/vendor_conf.d"
diff --git a/nixos/modules/programs/fish_completion-generator.patch b/nixos/modules/programs/fish_completion-generator.patch
new file mode 100644
index 00000000000..a8c797d185a
--- /dev/null
+++ b/nixos/modules/programs/fish_completion-generator.patch
@@ -0,0 +1,11 @@
+--- a/create_manpage_completions.py
++++ b/create_manpage_completions.py
+@@ -776,8 +776,6 @@ def parse_manpage_at_path(manpage_path, output_directory):
+
+             built_command_output.insert(0, "# " + CMDNAME)
+
+-            # Output the magic word Autogenerated so we can tell if we can overwrite this
+-            built_command_output.insert(1, "# Autogenerated from man page " + manpage_path)
+             # built_command_output.insert(2, "# using " + parser.__class__.__name__) # XXX MISATTRIBUTES THE CULPABILE PARSER! Was really using Type2 but reporting TypeDeroffManParser
+
+             for line in built_command_output:
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index b01de9efaa5..22521280e93 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -85,11 +85,13 @@ in
       # SSH agent protocol doesn't support changing TTYs, so bind the agent
       # to every new TTY.
       ${pkgs.gnupg}/bin/gpg-connect-agent --quiet updatestartuptty /bye > /dev/null
+    '');
 
+    environment.extraInit = mkIf cfg.agent.enableSSHSupport ''
       if [ -z "$SSH_AUTH_SOCK" ]; then
         export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
       fi
-    '');
+    '';
 
     assertions = [
       { assertion = cfg.agent.enableSSHSupport -> !config.programs.ssh.startAgent;
diff --git a/nixos/modules/programs/iotop.nix b/nixos/modules/programs/iotop.nix
new file mode 100644
index 00000000000..5512dbc62f7
--- /dev/null
+++ b/nixos/modules/programs/iotop.nix
@@ -0,0 +1,17 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.iotop;
+in {
+  options = {
+    programs.iotop.enable = mkEnableOption "iotop + setcap wrapper";
+  };
+  config = mkIf cfg.enable {
+    security.wrappers.iotop = {
+      source = "${pkgs.iotop}/bin/iotop";
+      capabilities = "cap_net_admin+p";
+    };
+  };
+}
diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix
index d39103a5805..9fdf99e9c69 100644
--- a/nixos/modules/programs/less.nix
+++ b/nixos/modules/programs/less.nix
@@ -25,7 +25,7 @@ let
   '';
 
   lessKey = pkgs.runCommand "lesskey"
-            { src = pkgs.writeText "lessconfig" configText; }
+            { src = pkgs.writeText "lessconfig" configText; preferLocalBuild = true; }
             "${pkgs.less}/bin/lesskey -o $out $src";
 
 in
diff --git a/nixos/modules/programs/singularity.nix b/nixos/modules/programs/singularity.nix
index 86153d93385..b27e122bd1d 100644
--- a/nixos/modules/programs/singularity.nix
+++ b/nixos/modules/programs/singularity.nix
@@ -3,18 +3,27 @@
 with lib;
 let
   cfg = config.programs.singularity;
+  singularity = pkgs.singularity.overrideAttrs (attrs : {
+    installPhase = attrs.installPhase + ''
+      mv $bin/libexec/singularity/bin/starter-suid $bin/libexec/singularity/bin/starter-suid.orig
+      ln -s /run/wrappers/bin/singularity-suid $bin/libexec/singularity/bin/starter-suid
+    '';
+  });
 in {
   options.programs.singularity = {
     enable = mkEnableOption "Singularity";
   };
 
   config = mkIf cfg.enable {
-      environment.systemPackages = [ pkgs.singularity ];
-      systemd.tmpfiles.rules = [ "d /var/singularity/mnt/session 0770 root root -"
-                                 "d /var/singularity/mnt/final 0770 root root -"
-                                 "d /var/singularity/mnt/overlay 0770 root root -"
-                                 "d /var/singularity/mnt/container 0770 root root -"
-                                 "d /var/singularity/mnt/source 0770 root root -"];
+      environment.systemPackages = [ singularity ];
+      security.wrappers.singularity-suid.source = "${singularity}/libexec/singularity/bin/starter-suid.orig";
+      systemd.tmpfiles.rules = [
+        "d /var/singularity/mnt/session 0770 root root -"
+        "d /var/singularity/mnt/final 0770 root root -"
+        "d /var/singularity/mnt/overlay 0770 root root -"
+        "d /var/singularity/mnt/container 0770 root root -"
+        "d /var/singularity/mnt/source 0770 root root -"
+      ];
   };
 
 }
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 4640c1d78d2..46965dd35b7 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -88,7 +88,8 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Extra configuration text appended to <filename>ssh_config</filename>.
+          Extra configuration text prepended to <filename>ssh_config</filename>. Other generated
+          options will be added after a <code>Host *</code> pattern.
           See <citerefentry><refentrytitle>ssh_config</refentrytitle><manvolnum>5</manvolnum></citerefentry>
           for help.
         '';
@@ -203,6 +204,11 @@ in
     # generation in the sshd service.
     environment.etc."ssh/ssh_config".text =
       ''
+        # Custom options from `extraConfig`, to override generated options
+        ${cfg.extraConfig}
+
+        # Generated options from other settings
+        Host *
         AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
 
         ${optionalString cfg.setXAuthLocation ''
@@ -213,8 +219,6 @@ in
 
         ${optionalString (cfg.pubkeyAcceptedKeyTypes != []) "PubkeyAcceptedKeyTypes ${concatStringsSep "," cfg.pubkeyAcceptedKeyTypes}"}
         ${optionalString (cfg.hostKeyAlgorithms != []) "HostKeyAlgorithms ${concatStringsSep "," cfg.hostKeyAlgorithms}"}
-
-        ${cfg.extraConfig}
       '';
 
     environment.etc."ssh/ssh_known_hosts".text = knownHostsText;
diff --git a/nixos/modules/programs/sway-beta.nix b/nixos/modules/programs/sway-beta.nix
deleted file mode 100644
index 7fc5979a38a..00000000000
--- a/nixos/modules/programs/sway-beta.nix
+++ /dev/null
@@ -1,90 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let
-  cfg = config.programs.sway-beta;
-  swayPackage = cfg.package;
-
-  swayWrapped = pkgs.writeShellScriptBin "sway" ''
-    set -o errexit
-
-    if [ ! "$_SWAY_WRAPPER_ALREADY_EXECUTED" ]; then
-      export _SWAY_WRAPPER_ALREADY_EXECUTED=1
-      ${cfg.extraSessionCommands}
-    fi
-
-    if [ "$DBUS_SESSION_BUS_ADDRESS" ]; then
-      export DBUS_SESSION_BUS_ADDRESS
-      exec ${swayPackage}/bin/sway "$@"
-    else
-      exec ${pkgs.dbus}/bin/dbus-run-session ${swayPackage}/bin/sway "$@"
-    fi
-  '';
-  swayJoined = pkgs.symlinkJoin {
-    name = "sway-joined";
-    paths = [ swayWrapped swayPackage ];
-  };
-in {
-  options.programs.sway-beta = {
-    enable = mkEnableOption ''
-      Sway, the i3-compatible tiling Wayland compositor. This module will be removed after the final release of Sway 1.0
-    '';
-
-    package = mkOption {
-      type = types.package;
-      default = pkgs.sway-beta;
-      defaultText = "pkgs.sway-beta";
-      description = ''
-        The package to be used for `sway`.
-      '';
-    };
-
-    extraSessionCommands = mkOption {
-      type = types.lines;
-      default = "";
-      example = ''
-        export SDL_VIDEODRIVER=wayland
-        # needs qt5.qtwayland in systemPackages
-        export QT_QPA_PLATFORM=wayland
-        export QT_WAYLAND_DISABLE_WINDOWDECORATION="1"
-        # Fix for some Java AWT applications (e.g. Android Studio),
-        # use this if they aren't displayed properly:
-        export _JAVA_AWT_WM_NONREPARENTING=1
-      '';
-      description = ''
-        Shell commands executed just before Sway is started.
-      '';
-    };
-
-    extraPackages = mkOption {
-      type = with types; listOf package;
-      default = with pkgs; [
-        xwayland rxvt_unicode dmenu
-      ];
-      defaultText = literalExample ''
-        with pkgs; [ xwayland rxvt_unicode dmenu ];
-      '';
-      example = literalExample ''
-        with pkgs; [
-          xwayland
-          i3status i3status-rust
-          termite rofi light
-        ]
-      '';
-      description = ''
-        Extra packages to be installed system wide.
-      '';
-    };
-  };
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ swayJoined ] ++ cfg.extraPackages;
-    security.pam.services.swaylock = {};
-    hardware.opengl.enable = mkDefault true;
-    fonts.enableDefaultFonts = mkDefault true;
-    programs.dconf.enable = mkDefault true;
-  };
-
-  meta.maintainers = with lib.maintainers; [ gnidorah primeos colemickens ];
-}
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index b3847db8cd9..457faaa3c10 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -16,9 +16,9 @@ let
 
     if [ "$DBUS_SESSION_BUS_ADDRESS" ]; then
       export DBUS_SESSION_BUS_ADDRESS
-      exec sway-setcap "$@"
+      exec ${swayPackage}/bin/sway "$@"
     else
-      exec ${pkgs.dbus}/bin/dbus-run-session sway-setcap "$@"
+      exec ${pkgs.dbus}/bin/dbus-run-session ${swayPackage}/bin/sway "$@"
     fi
   '';
   swayJoined = pkgs.symlinkJoin {
@@ -28,22 +28,24 @@ let
 in {
   options.programs.sway = {
     enable = mkEnableOption ''
-      the tiling Wayland compositor Sway. After adding yourself to the "sway"
-      group you can manually launch Sway by executing "sway" from a terminal.
-      If you call "sway" with any parameters the extraSessionCommands won't be
-      executed and Sway won't be launched with dbus-launch'';
+      Sway, the i3-compatible tiling Wayland compositor. You can manually launch
+      Sway by executing "exec sway" on a TTY. Copy /etc/sway/config to
+      ~/.config/sway/config to modify the default configuration. See
+      https://github.com/swaywm/sway/wiki and "man 5 sway" for more information.
+      Please have a look at the "extraSessionCommands" example for running
+      programs natively under Wayland'';
 
     extraSessionCommands = mkOption {
       type = types.lines;
       default = "";
       example = ''
-        # Define a keymap (US QWERTY is the default)
-        export XKB_DEFAULT_LAYOUT=de,us
-        export XKB_DEFAULT_VARIANT=nodeadkeys
-        export XKB_DEFAULT_OPTIONS=grp:alt_shift_toggle,caps:escape
-        # Change the Keyboard repeat delay and rate
-        export WLC_REPEAT_DELAY=660
-        export WLC_REPEAT_RATE=25
+        export SDL_VIDEODRIVER=wayland
+        # needs qt5.qtwayland in systemPackages
+        export QT_QPA_PLATFORM=wayland
+        export QT_WAYLAND_DISABLE_WINDOWDECORATION="1"
+        # Fix for some Java AWT applications (e.g. Android Studio),
+        # use this if they aren't displayed properly:
+        export _JAVA_AWT_WM_NONREPARENTING=1
       '';
       description = ''
         Shell commands executed just before Sway is started.
@@ -53,14 +55,17 @@ in {
     extraPackages = mkOption {
       type = with types; listOf package;
       default = with pkgs; [
-        i3status xwayland rxvt_unicode dmenu
+        swaylock swayidle
+        xwayland rxvt_unicode dmenu
       ];
       defaultText = literalExample ''
-        with pkgs; [ i3status xwayland rxvt_unicode dmenu ];
+        with pkgs; [ swaylock swayidle xwayland rxvt_unicode dmenu ];
       '';
       example = literalExample ''
         with pkgs; [
-          i3lock light termite
+          xwayland
+          i3status i3status-rust
+          termite rofi light
         ]
       '';
       description = ''
@@ -70,23 +75,19 @@ in {
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ swayJoined ] ++ cfg.extraPackages;
-    security.wrappers.sway = {
-      program = "sway-setcap";
-      source = "${swayPackage}/bin/sway";
-      capabilities = "cap_sys_ptrace,cap_sys_tty_config=eip";
-      owner = "root";
-      group = "sway";
-      permissions = "u+rx,g+rx";
+    environment = {
+      systemPackages = [ swayJoined ] ++ cfg.extraPackages;
+      etc = {
+        "sway/config".source = "${swayPackage}/etc/sway/config";
+        #"sway/security.d".source = "${swayPackage}/etc/sway/security.d/";
+        #"sway/config.d".source = "${swayPackage}/etc/sway/config.d/";
+      };
     };
-
-    users.groups.sway = {};
     security.pam.services.swaylock = {};
-
     hardware.opengl.enable = mkDefault true;
     fonts.enableDefaultFonts = mkDefault true;
     programs.dconf.enable = mkDefault true;
   };
 
-  meta.maintainers = with lib.maintainers; [ gnidorah primeos ];
+  meta.maintainers = with lib.maintainers; [ gnidorah primeos colemickens ];
 }
diff --git a/nixos/modules/programs/waybar.nix b/nixos/modules/programs/waybar.nix
new file mode 100644
index 00000000000..22530e6c7d4
--- /dev/null
+++ b/nixos/modules/programs/waybar.nix
@@ -0,0 +1,20 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+{
+  options.programs.waybar = {
+    enable = mkEnableOption "waybar";
+  };
+
+  config = mkIf config.programs.waybar.enable {
+    systemd.user.services.waybar = {
+      description = "Waybar as systemd service";
+      wantedBy = [ "graphical-session.target" ];
+      partOf = [ "graphical-session.target" ];
+      script = "${pkgs.waybar}/bin/waybar";
+    };
+  };
+
+  meta.maintainers = [ maintainers.FlorianFranzen ];
+}
diff --git a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
index e7cf17c2c00..89087a229eb 100644
--- a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
+++ b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
@@ -48,6 +48,23 @@ in
           https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters/pattern.md
         '';
       };
+      styles = mkOption {
+        default = {};
+        type = types.attrsOf types.string;
+
+        example = literalExample ''
+          {
+            "alias" = "fg=magenta,bold";
+          }
+        '';
+
+        description = ''
+          Specifies custom styles to be highlighted by zsh-syntax-highlighting.
+
+          Please refer to the docs for more information about the usage:
+          https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters/main.md
+        '';
+      };
     };
   };
 
@@ -73,6 +90,11 @@ in
             pattern: design:
             "ZSH_HIGHLIGHT_PATTERNS+=('${pattern}' '${design}')"
           ) cfg.patterns)
+        ++ optionals (length(attrNames cfg.styles) > 0)
+          (mapAttrsToList (
+            styles: design:
+            "ZSH_HIGHLIGHT_STYLES[${styles}]='${design}'"
+          ) cfg.styles)
       );
   };
 }
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 164d8db5859..4b995b66a26 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -79,6 +79,33 @@ in
         type = types.lines;
       };
 
+      histSize = mkOption {
+        default = 2000;
+        description = ''
+          Change history size.
+        '';
+        type = types.int;
+      };
+
+      histFile = mkOption {
+        default = "$HOME/.zsh_history";
+        description = ''
+          Change history file.
+        '';
+        type = types.str;
+      };
+
+      setOptions = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "HIST_IGNORE_DUPS" "SHARE_HISTORY" "HIST_FCNTL_LOCK"
+        ];
+        example = [ "EXTENDED_HISTORY" "RM_STAR_WAIT" ];
+        description = ''
+          Configure zsh options.
+        '';
+      };
+
       enableCompletion = mkOption {
         default = true;
         description = ''
@@ -162,12 +189,11 @@ in
 
         . /etc/zinputrc
 
-        # history defaults
-        SAVEHIST=2000
-        HISTSIZE=2000
-        HISTFILE=$HOME/.zsh_history
+        export SAVEHIST=${toString cfg.histSize}
+        export HISTSIZE=${toString cfg.histSize}
+        export HISTFILE=${cfg.histFile}
 
-        setopt HIST_IGNORE_DUPS SHARE_HISTORY HIST_FCNTL_LOCK
+        ${optionalString (cfg.setOptions != []) "setopt ${concatStringsSep " " cfg.setOptions}"}
 
         HELPDIR="${pkgs.zsh}/share/zsh/$ZSH_VERSION/help"
 
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index dc0a175d5bb..7fb58a2b800 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -40,9 +40,19 @@ with lib;
     (mkRenamedOptionModule [ "services" "kibana" "host" ] [ "services" "kibana" "listenAddress" ])
     (mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "admissionControl" ] [ "services" "kubernetes" "apiserver" "enableAdmissionPlugins" ])
     (mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "address" ] ["services" "kubernetes" "apiserver" "bindAddress"])
+    (mkRenamedOptionModule [ "services" "kubernetes" "apiserver" "port" ] ["services" "kubernetes" "apiserver" "insecurePort"])
     (mkRemovedOptionModule [ "services" "kubernetes" "apiserver" "publicAddress" ] "")
     (mkRenamedOptionModule [ "services" "kubernetes" "addons" "dashboard" "enableRBAC" ] [ "services" "kubernetes" "addons" "dashboard" "rbac" "enable" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "address" ] ["services" "kubernetes" "controllerManager" "bindAddress"])
+    (mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "port" ] ["services" "kubernetes" "controllerManager" "insecurePort"])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "servers" ] [ "services" "kubernetes" "apiserver" "etcd" "servers" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "keyFile" ] [ "services" "kubernetes" "apiserver" "etcd" "keyFile" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "certFile" ] [ "services" "kubernetes" "apiserver" "etcd" "certFile" ])
+    (mkRenamedOptionModule [ "services" "kubernetes" "etcd" "caFile" ] [ "services" "kubernetes" "apiserver" "etcd" "caFile" ])
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "applyManifests" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
+    (mkRenamedOptionModule [ "services" "kubernetes" "proxy" "address" ] ["services" "kubernetes" "proxy" "bindAddress"])
+    (mkRemovedOptionModule [ "services" "kubernetes" "verbose" ] "")
     (mkRenamedOptionModule [ "services" "logstash" "address" ] [ "services" "logstash" "listenAddress" ])
     (mkRenamedOptionModule [ "services" "mpd" "network" "host" ] [ "services" "mpd" "network" "listenAddress" ])
     (mkRenamedOptionModule [ "services" "neo4j" "host" ] [ "services" "neo4j" "defaultListenAddress" ])
@@ -59,6 +69,7 @@ with lib;
     (mkRenamedOptionModule [ "services" "statsd" "host" ] [ "services" "statsd" "listenAddress" ])
     (mkRenamedOptionModule [ "services" "subsonic" "host" ] [ "services" "subsonic" "listenAddress" ])
     (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "relay" "port" ])
+    (mkRenamedOptionModule [ "services" "vmwareGuest" ] [ "virtualisation" "vmware" "guest" ])
     (mkRenamedOptionModule [ "jobs" ] [ "systemd" "services" ])
 
     (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
@@ -69,6 +80,9 @@ with lib;
     (mkRemovedOptionModule [ "security" "setuidOwners" ] "Use security.wrappers instead")
     (mkRemovedOptionModule [ "security" "setuidPrograms" ] "Use security.wrappers instead")
 
+    # PAM
+    (mkRenamedOptionModule [ "security" "pam" "enableU2F" ] [ "security" "pam" "u2f" "enable" ])
+
     (mkRemovedOptionModule [ "services" "rmilter" "bindInetSockets" ] "Use services.rmilter.bindSocket.* instead")
     (mkRemovedOptionModule [ "services" "rmilter" "bindUnixSockets" ] "Use services.rmilter.bindSocket.* instead")
 
@@ -172,6 +186,9 @@ with lib;
     # parsoid
     (mkRemovedOptionModule [ "services" "parsoid" "interwikis" ] [ "services" "parsoid" "wikis" ])
 
+    # plexpy / tautulli
+    (mkRenamedOptionModule [ "services" "plexpy" ] [ "services" "tautulli" ])
+
     # piwik was renamed to matomo
     (mkRenamedOptionModule [ "services" "piwik" "enable" ] [ "services" "matomo" "enable" ])
     (mkRenamedOptionModule [ "services" "piwik" "webServerUser" ] [ "services" "matomo" "webServerUser" ])
diff --git a/nixos/modules/security/ca.nix b/nixos/modules/security/ca.nix
index 67469be18b4..1c4ee421fc5 100644
--- a/nixos/modules/security/ca.nix
+++ b/nixos/modules/security/ca.nix
@@ -14,6 +14,7 @@ let
     { files =
         cfg.certificateFiles ++
         [ (builtins.toFile "extra.crt" (concatStringsSep "\n" cfg.certificates)) ];
+      preferLocalBuild = true;
      }
     ''
       cat $files > $out
diff --git a/nixos/modules/security/duosec.nix b/nixos/modules/security/duosec.nix
index df6108dede7..14bf118f2d8 100644
--- a/nixos/modules/security/duosec.nix
+++ b/nixos/modules/security/duosec.nix
@@ -7,7 +7,7 @@ let
 
   boolToStr = b: if b then "yes" else "no";
 
-  configFile = ''
+  configFilePam = ''
     [duo]
     ikey=${cfg.ikey}
     skey=${cfg.skey}
@@ -16,21 +16,24 @@ let
     failmode=${cfg.failmode}
     pushinfo=${boolToStr cfg.pushinfo}
     autopush=${boolToStr cfg.autopush}
-    motd=${boolToStr cfg.motd}
     prompts=${toString cfg.prompts}
-    accept_env_factor=${boolToStr cfg.acceptEnvFactor}
     fallback_local_ip=${boolToStr cfg.fallbackLocalIP}
   '';
 
+  configFileLogin = configFilePam + ''
+    motd=${boolToStr cfg.motd}
+    accept_env_factor=${boolToStr cfg.acceptEnvFactor}
+  '';
+
   loginCfgFile = optional cfg.ssh.enable
-    { source = pkgs.writeText "login_duo.conf" configFile;
+    { source = pkgs.writeText "login_duo.conf" configFileLogin;
       mode   = "0600";
       user   = "sshd";
       target = "duo/login_duo.conf";
     };
 
   pamCfgFile = optional cfg.pam.enable
-    { source = pkgs.writeText "pam_duo.conf" configFile;
+    { source = pkgs.writeText "pam_duo.conf" configFilePam;
       mode   = "0600";
       user   = "sshd";
       target = "duo/pam_duo.conf";
@@ -180,12 +183,6 @@ in
   };
 
   config = mkIf (cfg.ssh.enable || cfg.pam.enable) {
-    assertions =
-      [ { assertion = !cfg.pam.enable;
-          message   = "PAM support is currently not implemented.";
-        }
-      ];
-
      environment.systemPackages = [ pkgs.duo-unix ];
 
      security.wrappers.login_duo.source = "${pkgs.duo-unix.out}/bin/login_duo";
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index b1a0eff98c2..03d2f899f2a 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -37,12 +37,14 @@ let
       };
 
       u2fAuth = mkOption {
-        default = config.security.pam.enableU2F;
+        default = config.security.pam.u2f.enable;
         type = types.bool;
         description = ''
           If set, users listed in
-          <filename>~/.config/Yubico/u2f_keys</filename> are able to log in
-          with the associated U2F key.
+          <filename>$XDG_CONFIG_HOME/Yubico/u2f_keys</filename> (or
+          <filename>$HOME/.config/Yubico/u2f_keys</filename> if XDG variable is
+          not set) are able to log in with the associated U2F key. Path can be
+          changed using <option>security.pam.u2f.authFile</option> option.
         '';
       };
 
@@ -129,6 +131,18 @@ let
         '';
       };
 
+      duoSecurity = {
+        enable = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            If set, use the Duo Security pam module
+            <literal>pam_duo</literal> for authentication.  Requires
+            configuration of <option>security.duosec</option> options.
+          '';
+        };
+      };
+
       startSession = mkOption {
         default = false;
         type = types.bool;
@@ -320,8 +334,8 @@ let
               "auth sufficient ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so file=~/.ssh/authorized_keys:~/.ssh/authorized_keys2:/etc/ssh/authorized_keys.d/%u"}
           ${optionalString cfg.fprintAuth
               "auth sufficient ${pkgs.fprintd}/lib/security/pam_fprintd.so"}
-          ${optionalString cfg.u2fAuth
-              "auth sufficient ${pkgs.pam_u2f}/lib/security/pam_u2f.so"}
+          ${let u2f = config.security.pam.u2f; in optionalString cfg.u2fAuth
+              "auth ${u2f.control} ${pkgs.pam_u2f}/lib/security/pam_u2f.so ${optionalString u2f.debug "debug"} ${optionalString (u2f.authFile != null) "authfile=${u2f.authFile}"} ${optionalString u2f.interactive "interactive"} ${optionalString u2f.cue "cue"}"}
           ${optionalString cfg.usbAuth
               "auth sufficient ${pkgs.pam_usb}/lib/security/pam_usb.so"}
           ${let oath = config.security.pam.oath; in optionalString cfg.oathAuth
@@ -338,7 +352,8 @@ let
             || cfg.pamMount
             || cfg.enableKwallet
             || cfg.enableGnomeKeyring
-            || cfg.googleAuthenticator.enable)) ''
+            || cfg.googleAuthenticator.enable
+            || cfg.duoSecurity.enable)) ''
               auth required pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} likeauth
               ${optionalString config.security.pam.enableEcryptfs
                 "auth optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so unwrap"}
@@ -348,9 +363,11 @@ let
                 ("auth optional ${pkgs.plasma5.kwallet-pam}/lib/security/pam_kwallet5.so" +
                  " kwalletd=${pkgs.libsForQt5.kwallet.bin}/bin/kwalletd5")}
               ${optionalString cfg.enableGnomeKeyring
-                ("auth optional ${pkgs.gnome3.gnome-keyring}/lib/security/pam_gnome_keyring.so")}
+                "auth optional ${pkgs.gnome3.gnome-keyring}/lib/security/pam_gnome_keyring.so"}
               ${optionalString cfg.googleAuthenticator.enable
-                  "auth required ${pkgs.googleAuthenticator}/lib/security/pam_google_authenticator.so no_increment_hotp"}
+                "auth required ${pkgs.googleAuthenticator}/lib/security/pam_google_authenticator.so no_increment_hotp"}
+              ${optionalString cfg.duoSecurity.enable
+                "auth required ${pkgs.duo-unix}/lib/security/pam_duo.so"}
             '') + ''
           ${optionalString cfg.unixAuth
               "auth sufficient pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} likeauth try_first_pass"}
@@ -368,7 +385,7 @@ let
           auth required pam_deny.so
 
           # Password management.
-          password requisite pam_unix.so nullok sha512
+          password sufficient pam_unix.so nullok sha512
           ${optionalString config.security.pam.enableEcryptfs
               "password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"}
           ${optionalString cfg.pamMount
@@ -527,11 +544,96 @@ in
       '';
     };
 
-    security.pam.enableU2F = mkOption {
-      default = false;
-      description = ''
-        Enable the U2F PAM module.
-      '';
+    security.pam.u2f = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Enables U2F PAM (<literal>pam-u2f</literal>) module.
+
+          If set, users listed in
+          <filename>$XDG_CONFIG_HOME/Yubico/u2f_keys</filename> (or
+          <filename>$HOME/.config/Yubico/u2f_keys</filename> if XDG variable is
+          not set) are able to log in with the associated U2F key. The path can
+          be changed using <option>security.pam.u2f.authFile</option> option.
+
+          File format is:
+          <literal>username:first_keyHandle,first_public_key: second_keyHandle,second_public_key</literal>
+          This file can be generated using <command>pamu2fcfg</command> command.
+
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/pam-u2f/">here</link>.
+        '';
+      };
+
+      authFile = mkOption {
+        default = null;
+        type = with types; nullOr path;
+        description = ''
+          By default <literal>pam-u2f</literal> module reads the keys from
+          <filename>$XDG_CONFIG_HOME/Yubico/u2f_keys</filename> (or
+          <filename>$HOME/.config/Yubico/u2f_keys</filename> if XDG variable is
+          not set).
+
+          If you want to change auth file locations or centralize database (for
+          example use <filename>/etc/u2f-mappings</filename>) you can set this
+          option.
+
+          File format is:
+          <literal>username:first_keyHandle,first_public_key: second_keyHandle,second_public_key</literal>
+          This file can be generated using <command>pamu2fcfg</command> command.
+
+          More information can be found <link
+          xlink:href="https://developers.yubico.com/pam-u2f/">here</link>.
+        '';
+      };
+
+      control = mkOption {
+        default = "sufficient";
+        type = types.enum [ "required" "requisite" "sufficient" "optional" ];
+        description = ''
+          This option sets pam "control".
+          If you want to have multi factor authentication, use "required".
+          If you want to use U2F device instead of regular password, use "sufficient".
+
+          Read
+          <citerefentry>
+            <refentrytitle>pam.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for better understanding of this option.
+        '';
+      };
+
+      debug = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Debug output to stderr.
+        '';
+      };
+
+      interactive = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Set to prompt a message and wait before testing the presence of a U2F device.
+          Recommended if your device doesn’t have a tactile trigger.
+        '';
+      };
+
+      cue = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          By default <literal>pam-u2f</literal> module does not inform user
+          that he needs to use the u2f device, it just waits without a prompt.
+
+          If you set this option to <literal>true</literal>,
+          <literal>cue</literal> option is added to <literal>pam-u2f</literal>
+          module and reminder message will be displayed.
+        '';
+      };
     };
 
     security.pam.enableEcryptfs = mkOption {
@@ -563,7 +665,7 @@ in
       ++ optionals config.krb5.enable [pam_krb5 pam_ccreds]
       ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
       ++ optionals config.security.pam.oath.enable [ pkgs.oathToolkit ]
-      ++ optionals config.security.pam.enableU2F [ pkgs.pam_u2f ];
+      ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ];
 
     boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ];
 
diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix
index 69a2a4f8f9a..573588aaeec 100644
--- a/nixos/modules/security/sudo.nix
+++ b/nixos/modules/security/sudo.nix
@@ -215,7 +215,10 @@ in
     environment.etc = singleton
       { source =
           pkgs.runCommand "sudoers"
-          { src = pkgs.writeText "sudoers-in" cfg.configFile; }
+          {
+            src = pkgs.writeText "sudoers-in" cfg.configFile;
+            preferLocalBuild = true;
+          }
           # Make sure that the sudoers file is syntactically valid.
           # (currently disabled - NIXOS-66)
           "${pkgs.buildPackages.sudo}/sbin/visudo -f $src -c && cp $src $out";
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
new file mode 100644
index 00000000000..f709dd7fe16
--- /dev/null
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -0,0 +1,217 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  package = "snapcast";
+  name = "snapserver";
+
+  cfg = config.services.snapserver;
+
+  # Using types.nullOr to inherit upstream defaults.
+  sampleFormat = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = ''
+      Default sample format.
+    '';
+    example = "48000:16:2";
+  };
+
+  codec = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = ''
+      Default audio compression method.
+    '';
+    example = "flac";
+  };
+
+  streamToOption = name: opt:
+    let
+      os = val:
+        optionalString (val != null) "${val}";
+      os' = prefixx: val:
+        optionalString (val != null) (prefixx + "${val}");
+      flatten = key: value:
+        "&${key}=${value}";
+    in
+      "-s ${opt.type}://" + os opt.location + "?" + os' "name=" name
+        + concatStrings (mapAttrsToList flatten opt.query);
+
+  optionalNull = val: ret:
+    optional (val != null) ret;
+
+  optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams
+             ++ ["-p ${toString cfg.port}"]
+             ++ ["--controlPort ${toString cfg.controlPort}"]
+             ++ optionalNull cfg.sampleFormat "--sampleFormat ${cfg.sampleFormat}"
+             ++ optionalNull cfg.codec "-c ${cfg.codec}"
+             ++ optionalNull cfg.streamBuffer "--streamBuffer ${cfg.streamBuffer}"
+             ++ optionalNull cfg.buffer "-b ${cfg.buffer}"
+             ++ optional cfg.sendToMuted "--sendToMuted");
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.snapserver = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable snapserver.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 1704;
+        description = ''
+          The port that snapclients can connect to.
+        '';
+      };
+
+      controlPort = mkOption {
+        type = types.port;
+        default = 1705;
+        description = ''
+          The port for control connections (JSON-RPC).
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to automatically open the specified ports in the firewall.
+        '';
+      };
+
+      inherit sampleFormat;
+      inherit codec;
+
+      streams = mkOption {
+        type = with types; attrsOf (submodule {
+          options = {
+            location = mkOption {
+              type = types.path;
+              description = ''
+                The location of the pipe.
+              '';
+            };
+            type = mkOption {
+              type = types.enum [ "pipe" "file" "process" "spotify" "airplay" ];
+              default = "pipe";
+              description = ''
+                The type of input stream.
+              '';
+            };
+            query = mkOption {
+              type = attrsOf str;
+              default = {};
+              description = ''
+                Key-value pairs that convey additional parameters about a stream.
+              '';
+              example = literalExample ''
+                # for type == "pipe":
+                {
+                  mode = "listen";
+                };
+                # for type == "process":
+                {
+                  params = "--param1 --param2";
+                  logStderr = "true";
+                };
+              '';
+            };
+            inherit sampleFormat;
+            inherit codec;
+          };
+        });
+        default = { default = {}; };
+        description = ''
+          The definition for an input source.
+        '';
+        example = literalExample ''
+          {
+            mpd = {
+              type = "pipe";
+              location = "/run/snapserver/mpd";
+              sampleFormat = "48000:16:2";
+              codec = "pcm";
+            };
+          };
+        '';
+      };
+
+      streamBuffer = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          Stream read (input) buffer in ms.
+        '';
+        example = 20;
+      };
+
+      buffer = mkOption {
+        type = with types; nullOr int;
+        default = null;
+        description = ''
+          Network buffer in ms.
+        '';
+        example = 1000;
+      };
+
+      sendToMuted = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Send audio to muted clients.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.snapserver = {
+      after = [ "network.target" ];
+      description = "Snapserver";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "mpd.service" "mopidy.service" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}";
+        Type = "forking";
+        LimitRTPRIO = 50;
+        LimitRTTIME = "infinity";
+        NoNewPrivileges = true;
+        PIDFile = "/run/${name}/pid";
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+        RestrictNamespaces = true;
+        RuntimeDirectory = name;
+        StateDirectory = name;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = optionals cfg.openFirewall [ cfg.port cfg.controlPort ];
+  };
+
+  meta = {
+    maintainers = with maintainers; [ tobim ];
+  };
+
+}
diff --git a/nixos/modules/services/audio/squeezelite.nix b/nixos/modules/services/audio/squeezelite.nix
index 57ae3855993..05506f5bcc7 100644
--- a/nixos/modules/services/audio/squeezelite.nix
+++ b/nixos/modules/services/audio/squeezelite.nix
@@ -3,8 +3,7 @@
 with lib;
 
 let
-
-  uid = config.ids.uids.squeezelite;
+  dataDir = "/var/lib/squeezelite";
   cfg = config.services.squeezelite;
 
 in {
@@ -17,14 +16,6 @@ in {
 
       enable = mkEnableOption "Squeezelite, a software Squeezebox emulator";
 
-      dataDir = mkOption {
-        default = "/var/lib/squeezelite";
-        type = types.str;
-        description = ''
-          The directory where Squeezelite stores its name file.
-        '';
-      };
-
       extraArguments = mkOption {
         default = "";
         type = types.str;
@@ -46,22 +37,14 @@ in {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" "sound.target" ];
       description = "Software Squeezebox emulator";
-      preStart = "mkdir -p ${cfg.dataDir} && chown -R squeezelite ${cfg.dataDir}";
       serviceConfig = {
-        ExecStart = "${pkgs.squeezelite}/bin/squeezelite -N ${cfg.dataDir}/player-name ${cfg.extraArguments}";
-        User = "squeezelite";
-        PermissionsStartOnly = true;
+        DynamicUser = true;
+        ExecStart = "${pkgs.squeezelite}/bin/squeezelite -N ${dataDir}/player-name ${cfg.extraArguments}";
+        StateDirectory = builtins.baseNameOf dataDir;
+        SupplementaryGroups = "audio";
       };
     };
 
-    users.users.squeezelite= {
-      inherit uid;
-      group = "nogroup";
-      extraGroups = [ "audio" ];
-      description = "Squeezelite user";
-      home = "${cfg.dataDir}";
-    };
-
   };
 
 }
diff --git a/nixos/modules/services/backup/duplicity.nix b/nixos/modules/services/backup/duplicity.nix
new file mode 100644
index 00000000000..a8d56424862
--- /dev/null
+++ b/nixos/modules/services/backup/duplicity.nix
@@ -0,0 +1,141 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfg = config.services.duplicity;
+
+  stateDirectory = "/var/lib/duplicity";
+
+  localTarget = if hasPrefix "file://" cfg.targetUrl
+    then removePrefix "file://" cfg.targetUrl else null;
+
+in {
+  options.services.duplicity = {
+    enable = mkEnableOption "backups with duplicity";
+
+    root = mkOption {
+      type = types.path;
+      default = "/";
+      description = ''
+        Root directory to backup.
+      '';
+    };
+
+    include = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "/home" ];
+      description = ''
+        List of paths to include into the backups. See the FILE SELECTION
+        section in <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry> for details on the syntax.
+      '';
+    };
+
+    exclude = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        List of paths to exclude from backups. See the FILE SELECTION section in
+        <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry> for details on the syntax.
+      '';
+    };
+
+    targetUrl = mkOption {
+      type = types.str;
+      example = "s3://host:port/prefix";
+      description = ''
+        Target url to backup to. See the URL FORMAT section in
+        <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry> for supported urls.
+      '';
+    };
+
+    secretFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path of a file containing secrets (gpg passphrase, access key...) in
+        the format of EnvironmentFile as described by
+        <citerefentry><refentrytitle>systemd.exec</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry>. For example:
+        <programlisting>
+        PASSPHRASE=<replaceable>...</replaceable>
+        AWS_ACCESS_KEY_ID=<replaceable>...</replaceable>
+        AWS_SECRET_ACCESS_KEY=<replaceable>...</replaceable>
+        </programlisting>
+      '';
+    };
+
+    frequency = mkOption {
+      type = types.nullOr types.str;
+      default = "daily";
+      description = ''
+        Run duplicity with the given frequency (see
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry> for the format).
+        If null, do not run automatically.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--full-if-older-than" "1M" ];
+      description = ''
+        Extra command-line flags passed to duplicity. See
+        <citerefentry><refentrytitle>duplicity</refentrytitle>
+        <manvolnum>1</manvolnum></citerefentry>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      services.duplicity = {
+        description = "backup files with duplicity";
+
+        environment.HOME = stateDirectory;
+
+        serviceConfig = {
+          ExecStart = ''
+            ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs (
+              [
+                cfg.root
+                cfg.targetUrl
+                "--archive-dir" stateDirectory
+              ]
+              ++ concatMap (p: [ "--include" p ]) cfg.include
+              ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
+              ++ cfg.extraFlags)}
+          '';
+          PrivateTmp = true;
+          ProtectSystem = "strict";
+          ProtectHome = "read-only";
+          StateDirectory = baseNameOf stateDirectory;
+        } // optionalAttrs (localTarget != null) {
+          ReadWritePaths = localTarget;
+        } // optionalAttrs (cfg.secretFile != null) {
+          EnvironmentFile = cfg.secretFile;
+        };
+      } // optionalAttrs (cfg.frequency != null) {
+        startAt = cfg.frequency;
+      };
+
+      tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
+    };
+
+    assertions = singleton {
+      # Duplicity will fail if the last file selection option is an include. It
+      # is not always possible to detect but this simple case can be caught.
+      assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != [];
+      message = ''
+        Duplicity will fail if you only specify included paths ("Because the
+        default is to include all files, the expression is redundant. Exiting
+        because this probably isn't what you meant.")
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
new file mode 100644
index 00000000000..17f2dde31a7
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -0,0 +1,167 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.addonManager;
+
+  isRBACEnabled = elem "RBAC" top.apiserver.authorizationMode;
+
+  addons = pkgs.runCommand "kubernetes-addons" { } ''
+    mkdir -p $out
+    # since we are mounting the addons to the addon manager, they need to be copied
+    ${concatMapStringsSep ";" (a: "cp -v ${a}/* $out/") (mapAttrsToList (name: addon:
+      pkgs.writeTextDir "${name}.json" (builtins.toJSON addon)
+    ) (cfg.addons))}
+  '';
+in
+{
+  ###### interface
+  options.services.kubernetes.addonManager = with lib.types; {
+
+    bootstrapAddons = mkOption {
+      description = ''
+        Bootstrap addons are like regular addons, but they are applied with cluster-admin rigths.
+        They are applied at addon-manager startup only.
+      '';
+      default = { };
+      type = attrsOf attrs;
+      example = literalExample ''
+        {
+          "my-service" = {
+            "apiVersion" = "v1";
+            "kind" = "Service";
+            "metadata" = {
+              "name" = "my-service";
+              "namespace" = "default";
+            };
+            "spec" = { ... };
+          };
+        }
+      '';
+    };
+
+    addons = mkOption {
+      description = "Kubernetes addons (any kind of Kubernetes resource can be an addon).";
+      default = { };
+      type = attrsOf (either attrs (listOf attrs));
+      example = literalExample ''
+        {
+          "my-service" = {
+            "apiVersion" = "v1";
+            "kind" = "Service";
+            "metadata" = {
+              "name" = "my-service";
+              "namespace" = "default";
+            };
+            "spec" = { ... };
+          };
+        }
+        // import <nixpkgs/nixos/modules/services/cluster/kubernetes/dashboard.nix> { cfg = config.services.kubernetes; };
+      '';
+    };
+
+    enable = mkEnableOption "Whether to enable Kubernetes addon manager.";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.etc."kubernetes/addons".source = "${addons}/";
+
+    systemd.services.kube-addon-manager = {
+      description = "Kubernetes addon manager";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      environment.ADDON_PATH = "/etc/kubernetes/addons/";
+      path = [ pkgs.gawk ];
+      serviceConfig = {
+        Slice = "kubernetes.slice";
+        ExecStart = "${top.package}/bin/kube-addons";
+        WorkingDirectory = top.dataDir;
+        User = "kubernetes";
+        Group = "kubernetes";
+        Restart = "on-failure";
+        RestartSec = 10;
+      };
+    };
+
+    services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled
+    (let
+      name = system:kube-addon-manager;
+      namespace = "kube-system";
+    in
+    {
+
+      kube-addon-manager-r = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "Role";
+        metadata = {
+          inherit name namespace;
+        };
+        rules = [{
+          apiGroups = ["*"];
+          resources = ["*"];
+          verbs = ["*"];
+        }];
+      };
+
+      kube-addon-manager-rb = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "RoleBinding";
+        metadata = {
+          inherit name namespace;
+        };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "Role";
+          inherit name;
+        };
+        subjects = [{
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "User";
+          inherit name;
+        }];
+      };
+
+      kube-addon-manager-cluster-lister-cr = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRole";
+        metadata = {
+          name = "${name}:cluster-lister";
+        };
+        rules = [{
+          apiGroups = ["*"];
+          resources = ["*"];
+          verbs = ["list"];
+        }];
+      };
+
+      kube-addon-manager-cluster-lister-crb = {
+        apiVersion = "rbac.authorization.k8s.io/v1";
+        kind = "ClusterRoleBinding";
+        metadata = {
+          name = "${name}:cluster-lister";
+        };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "ClusterRole";
+          name = "${name}:cluster-lister";
+        };
+        subjects = [{
+          kind = "User";
+          inherit name;
+        }];
+      };
+    });
+
+    services.kubernetes.pki.certs = {
+      addonManager = top.lib.mkCert {
+        name = "kube-addon-manager";
+        CN = "system:kube-addon-manager";
+        action = "systemctl restart kube-addon-manager.service";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/cluster/kubernetes/dashboard.nix b/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
index cbd6e8f7bf7..454e7d35bc0 100644
--- a/nixos/modules/services/cluster/kubernetes/dashboard.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
@@ -8,6 +8,13 @@ in {
   options.services.kubernetes.addons.dashboard = {
     enable = mkEnableOption "kubernetes dashboard addon";
 
+    extraArgs = mkOption {
+      description = "Extra arguments to append to the dashboard cmdline";
+      type = types.listOf types.str;
+      default = [];
+      example = ["--enable-skip-login"];
+    };
+
     rbac = mkOption {
       description = "Role-based access control (RBAC) options";
       default = {};
@@ -31,7 +38,7 @@ in {
     version = mkOption {
       description = "Which version of the kubernetes dashboard to deploy";
       type = types.str;
-      default = "v1.8.3";
+      default = "v1.10.1";
     };
 
     image = mkOption {
@@ -39,9 +46,9 @@ in {
       type = types.attrs;
       default = {
         imageName = "k8s.gcr.io/kubernetes-dashboard-amd64";
-        imageDigest = "sha256:dc4026c1b595435ef5527ca598e1e9c4343076926d7d62b365c44831395adbd0";
+        imageDigest = "sha256:0ae6b69432e78069c5ce2bcde0fe409c5c4d6f0f4d9cd50a17974fea38898747";
         finalImageTag = cfg.version;
-        sha256 = "18ajcg0q1vignfjk2sm4xj4wzphfz8wah69ps8dklqfvv0164mc8";
+        sha256 = "01xrr4pwgr2hcjrjsi3d14ifpzdfbxzqpzxbk2fkbjb9zkv38zxy";
       };
     };
   };
@@ -99,7 +106,7 @@ in {
                     memory = "100Mi";
                   };
                 };
-                args = ["--auto-generate-certificates"];
+                args = ["--auto-generate-certificates"] ++ cfg.extraArgs;
                 volumeMounts = [{
                   name = "tmp-volume";
                   mountPath = "/tmp";
diff --git a/nixos/modules/services/cluster/kubernetes/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index 5a3e281ea69..4368159ea6e 100644
--- a/nixos/modules/services/cluster/kubernetes/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  version = "1.2.5";
+  version = "1.3.1";
   cfg = config.services.kubernetes.addons.dns;
   ports = {
     dns = 10053;
@@ -38,14 +38,26 @@ in {
       type = types.int;
     };
 
+    reconcileMode = mkOption {
+      description = ''
+        Controls the addon manager reconciliation mode for the DNS addon.
+
+        Setting reconcile mode to EnsureExists makes it possible to tailor DNS behavior by editing the coredns ConfigMap.
+
+        See: <link xlink:href="https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/addon-manager/README.md"/>.
+      '';
+      default = "Reconcile";
+      type = types.enum [ "Reconcile" "EnsureExists" ];
+    };
+
     coredns = mkOption {
       description = "Docker image to seed for the CoreDNS container.";
       type = types.attrs;
       default = {
         imageName = "coredns/coredns";
-        imageDigest = "sha256:33c8da20b887ae12433ec5c40bfddefbbfa233d5ce11fb067122e68af30291d6";
+        imageDigest = "sha256:02382353821b12c21b062c59184e227e001079bb13ebd01f9d3270ba0fcbf1e4";
         finalImageTag = version;
-        sha256 = "13q19rgwapv27xcs664dw502254yw4zw63insf6g2danidv2mg6i";
+        sha256 = "0vbylgyxv2jm2mnzk6f28jbsj305zsxmx3jr6ngjq461czcl5fi5";
       };
     };
   };
@@ -54,21 +66,7 @@ in {
     services.kubernetes.kubelet.seedDockerImages =
       singleton (pkgs.dockerTools.pullImage cfg.coredns);
 
-    services.kubernetes.addonManager.addons = {
-      coredns-sa = {
-        apiVersion = "v1";
-        kind = "ServiceAccount";
-        metadata = {
-          labels = {
-            "addonmanager.kubernetes.io/mode" = "Reconcile";
-            "k8s-app" = "kube-dns";
-            "kubernetes.io/cluster-service" = "true";
-          };
-          name = "coredns";
-          namespace = "kube-system";
-        };
-      };
-
+    services.kubernetes.addonManager.bootstrapAddons = {
       coredns-cr = {
         apiVersion = "rbac.authorization.k8s.io/v1beta1";
         kind = "ClusterRole";
@@ -123,13 +121,29 @@ in {
           }
         ];
       };
+    };
+
+    services.kubernetes.addonManager.addons = {
+      coredns-sa = {
+        apiVersion = "v1";
+        kind = "ServiceAccount";
+        metadata = {
+          labels = {
+            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            "k8s-app" = "kube-dns";
+            "kubernetes.io/cluster-service" = "true";
+          };
+          name = "coredns";
+          namespace = "kube-system";
+        };
+      };
 
       coredns-cm = {
         apiVersion = "v1";
         kind = "ConfigMap";
         metadata = {
           labels = {
-            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            "addonmanager.kubernetes.io/mode" = cfg.reconcileMode;
             "k8s-app" = "kube-dns";
             "kubernetes.io/cluster-service" = "true";
           };
@@ -160,7 +174,7 @@ in {
         kind = "Deployment";
         metadata = {
           labels = {
-            "addonmanager.kubernetes.io/mode" = "Reconcile";
+            "addonmanager.kubernetes.io/mode" = cfg.reconcileMode;
             "k8s-app" = "kube-dns";
             "kubernetes.io/cluster-service" = "true";
             "kubernetes.io/name" = "CoreDNS";
diff --git a/nixos/modules/services/cluster/kubernetes/apiserver.nix b/nixos/modules/services/cluster/kubernetes/apiserver.nix
new file mode 100644
index 00000000000..455d0239604
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/apiserver.nix
@@ -0,0 +1,428 @@
+  { config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.apiserver;
+
+  isRBACEnabled = elem "RBAC" cfg.authorizationMode;
+
+  apiserverServiceIP = (concatStringsSep "." (
+    take 3 (splitString "." cfg.serviceClusterIpRange
+  )) + ".1");
+in
+{
+  ###### interface
+  options.services.kubernetes.apiserver = with lib.types; {
+
+    advertiseAddress = mkOption {
+      description = ''
+        Kubernetes apiserver IP address on which to advertise the apiserver
+        to members of the cluster. This address must be reachable by the rest
+        of the cluster.
+      '';
+      default = null;
+      type = nullOr str;
+    };
+
+    allowPrivileged = mkOption {
+      description = "Whether to allow privileged containers on Kubernetes.";
+      default = false;
+      type = bool;
+    };
+
+    authorizationMode = mkOption {
+      description = ''
+        Kubernetes apiserver authorization mode (AlwaysAllow/AlwaysDeny/ABAC/Webhook/RBAC/Node). See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
+      '';
+      default = ["RBAC" "Node"]; # Enabling RBAC by default, although kubernetes default is AllowAllow
+      type = listOf (enum ["AlwaysAllow" "AlwaysDeny" "ABAC" "Webhook" "RBAC" "Node"]);
+    };
+
+    authorizationPolicy = mkOption {
+      description = ''
+        Kubernetes apiserver authorization policy file. See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
+      '';
+      default = [];
+      type = listOf attrs;
+    };
+
+    basicAuthFile = mkOption {
+      description = ''
+        Kubernetes apiserver basic authentication file. See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    bindAddress = mkOption {
+      description = ''
+        The IP address on which to listen for the --secure-port port.
+        The associated interface(s) must be reachable by the rest
+        of the cluster, and by CLI/web clients.
+      '';
+      default = "0.0.0.0";
+      type = str;
+    };
+
+    clientCaFile = mkOption {
+      description = "Kubernetes apiserver CA file for client auth.";
+      default = top.caFile;
+      type = nullOr path;
+    };
+
+    disableAdmissionPlugins = mkOption {
+      description = ''
+        Kubernetes admission control plugins to disable. See
+        <link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
+      '';
+      default = [];
+      type = listOf str;
+    };
+
+    enable = mkEnableOption "Kubernetes apiserver";
+
+    enableAdmissionPlugins = mkOption {
+      description = ''
+        Kubernetes admission control plugins to enable. See
+        <link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
+      '';
+      default = [
+        "NamespaceLifecycle" "LimitRanger" "ServiceAccount"
+        "ResourceQuota" "DefaultStorageClass" "DefaultTolerationSeconds"
+        "NodeRestriction"
+      ];
+      example = [
+        "NamespaceLifecycle" "NamespaceExists" "LimitRanger"
+        "SecurityContextDeny" "ServiceAccount" "ResourceQuota"
+        "PodSecurityPolicy" "NodeRestriction" "DefaultStorageClass"
+      ];
+      type = listOf str;
+    };
+
+    etcd = {
+      servers = mkOption {
+        description = "List of etcd servers.";
+        default = ["http://127.0.0.1:2379"];
+        type = types.listOf types.str;
+      };
+
+      keyFile = mkOption {
+        description = "Etcd key file.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      certFile = mkOption {
+        description = "Etcd cert file.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      caFile = mkOption {
+        description = "Etcd ca file.";
+        default = top.caFile;
+        type = types.nullOr types.path;
+      };
+    };
+
+    extraOpts = mkOption {
+      description = "Kubernetes apiserver extra command line options.";
+      default = "";
+      type = str;
+    };
+
+    extraSANs = mkOption {
+      description = "Extra x509 Subject Alternative Names to be added to the kubernetes apiserver tls cert.";
+      default = [];
+      type = listOf str;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      type = listOf str;
+    };
+
+    insecureBindAddress = mkOption {
+      description = "The IP address on which to serve the --insecure-port.";
+      default = "127.0.0.1";
+      type = str;
+    };
+
+    insecurePort = mkOption {
+      description = "Kubernetes apiserver insecure listening port. (0 = disabled)";
+      default = 0;
+      type = int;
+    };
+
+    kubeletClientCaFile = mkOption {
+      description = "Path to a cert file for connecting to kubelet.";
+      default = top.caFile;
+      type = nullOr path;
+    };
+
+    kubeletClientCertFile = mkOption {
+      description = "Client certificate to use for connections to kubelet.";
+      default = null;
+      type = nullOr path;
+    };
+
+    kubeletClientKeyFile = mkOption {
+      description = "Key to use for connections to kubelet.";
+      default = null;
+      type = nullOr path;
+    };
+
+    kubeletHttps = mkOption {
+      description = "Whether to use https for connections to kubelet.";
+      default = true;
+      type = bool;
+    };
+
+    runtimeConfig = mkOption {
+      description = ''
+        Api runtime configuration. See
+        <link xlink:href="https://kubernetes.io/docs/tasks/administer-cluster/cluster-management/"/>
+      '';
+      default = "authentication.k8s.io/v1beta1=true";
+      example = "api/all=false,api/v1=true";
+      type = str;
+    };
+
+    storageBackend = mkOption {
+      description = ''
+        Kubernetes apiserver storage backend.
+      '';
+      default = "etcd3";
+      type = enum ["etcd2" "etcd3"];
+    };
+
+    securePort = mkOption {
+      description = "Kubernetes apiserver secure port.";
+      default = 6443;
+      type = int;
+    };
+
+    serviceAccountKeyFile = mkOption {
+      description = ''
+        Kubernetes apiserver PEM-encoded x509 RSA private or public key file,
+        used to verify ServiceAccount tokens. By default tls private key file
+        is used.
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    serviceClusterIpRange = mkOption {
+      description = ''
+        A CIDR notation IP range from which to assign service cluster IPs.
+        This must not overlap with any IP ranges assigned to nodes for pods.
+      '';
+      default = "10.0.0.0/24";
+      type = str;
+    };
+
+    tlsCertFile = mkOption {
+      description = "Kubernetes apiserver certificate file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsKeyFile = mkOption {
+      description = "Kubernetes apiserver private key file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tokenAuthFile = mkOption {
+      description = ''
+        Kubernetes apiserver token authentication file. See
+        <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+    webhookConfig = mkOption {
+      description = ''
+        Kubernetes apiserver Webhook config file. It uses the kubeconfig file format.
+        See <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/webhook/"/>
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+  };
+
+
+  ###### implementation
+  config = mkMerge [
+
+    (mkIf cfg.enable {
+        systemd.services.kube-apiserver = {
+          description = "Kubernetes APIServer Service";
+          wantedBy = [ "kubernetes.target" ];
+          after = [ "network.target" ];
+          serviceConfig = {
+            Slice = "kubernetes.slice";
+            ExecStart = ''${top.package}/bin/kube-apiserver \
+              --allow-privileged=${boolToString cfg.allowPrivileged} \
+              --authorization-mode=${concatStringsSep "," cfg.authorizationMode} \
+                ${optionalString (elem "ABAC" cfg.authorizationMode)
+                  "--authorization-policy-file=${
+                    pkgs.writeText "kube-auth-policy.jsonl"
+                    (concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.authorizationPolicy)
+                  }"
+                } \
+                ${optionalString (elem "Webhook" cfg.authorizationMode)
+                  "--authorization-webhook-config-file=${cfg.webhookConfig}"
+                } \
+              --bind-address=${cfg.bindAddress} \
+              ${optionalString (cfg.advertiseAddress != null)
+                "--advertise-address=${cfg.advertiseAddress}"} \
+              ${optionalString (cfg.clientCaFile != null)
+                "--client-ca-file=${cfg.clientCaFile}"} \
+              --disable-admission-plugins=${concatStringsSep "," cfg.disableAdmissionPlugins} \
+              --enable-admission-plugins=${concatStringsSep "," cfg.enableAdmissionPlugins} \
+              --etcd-servers=${concatStringsSep "," cfg.etcd.servers} \
+              ${optionalString (cfg.etcd.caFile != null)
+                "--etcd-cafile=${cfg.etcd.caFile}"} \
+              ${optionalString (cfg.etcd.certFile != null)
+                "--etcd-certfile=${cfg.etcd.certFile}"} \
+              ${optionalString (cfg.etcd.keyFile != null)
+                "--etcd-keyfile=${cfg.etcd.keyFile}"} \
+              ${optionalString (cfg.featureGates != [])
+                "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+              ${optionalString (cfg.basicAuthFile != null)
+                "--basic-auth-file=${cfg.basicAuthFile}"} \
+              --kubelet-https=${boolToString cfg.kubeletHttps} \
+              ${optionalString (cfg.kubeletClientCaFile != null)
+                "--kubelet-certificate-authority=${cfg.kubeletClientCaFile}"} \
+              ${optionalString (cfg.kubeletClientCertFile != null)
+                "--kubelet-client-certificate=${cfg.kubeletClientCertFile}"} \
+              ${optionalString (cfg.kubeletClientKeyFile != null)
+                "--kubelet-client-key=${cfg.kubeletClientKeyFile}"} \
+              --insecure-bind-address=${cfg.insecureBindAddress} \
+              --insecure-port=${toString cfg.insecurePort} \
+              ${optionalString (cfg.runtimeConfig != "")
+                "--runtime-config=${cfg.runtimeConfig}"} \
+              --secure-port=${toString cfg.securePort} \
+              ${optionalString (cfg.serviceAccountKeyFile!=null)
+                "--service-account-key-file=${cfg.serviceAccountKeyFile}"} \
+              --service-cluster-ip-range=${cfg.serviceClusterIpRange} \
+              --storage-backend=${cfg.storageBackend} \
+              ${optionalString (cfg.tlsCertFile != null)
+                "--tls-cert-file=${cfg.tlsCertFile}"} \
+              ${optionalString (cfg.tlsKeyFile != null)
+                "--tls-private-key-file=${cfg.tlsKeyFile}"} \
+              ${optionalString (cfg.tokenAuthFile != null)
+                "--token-auth-file=${cfg.tokenAuthFile}"} \
+              ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+              ${cfg.extraOpts}
+            '';
+            WorkingDirectory = top.dataDir;
+            User = "kubernetes";
+            Group = "kubernetes";
+            AmbientCapabilities = "cap_net_bind_service";
+            Restart = "on-failure";
+            RestartSec = 5;
+          };
+        };
+
+        services.etcd = {
+          clientCertAuth = mkDefault true;
+          peerClientCertAuth = mkDefault true;
+          listenClientUrls = mkDefault ["https://0.0.0.0:2379"];
+          listenPeerUrls = mkDefault ["https://0.0.0.0:2380"];
+          advertiseClientUrls = mkDefault ["https://${top.masterAddress}:2379"];
+          initialCluster = mkDefault ["${top.masterAddress}=https://${top.masterAddress}:2380"];
+          name = mkDefault top.masterAddress;
+          initialAdvertisePeerUrls = mkDefault ["https://${top.masterAddress}:2380"];
+        };
+
+        services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled {
+
+          apiserver-kubelet-api-admin-crb = {
+            apiVersion = "rbac.authorization.k8s.io/v1";
+            kind = "ClusterRoleBinding";
+            metadata = {
+              name = "system:kube-apiserver:kubelet-api-admin";
+            };
+            roleRef = {
+              apiGroup = "rbac.authorization.k8s.io";
+              kind = "ClusterRole";
+              name = "system:kubelet-api-admin";
+            };
+            subjects = [{
+              kind = "User";
+              name = "system:kube-apiserver";
+            }];
+          };
+
+        };
+
+      services.kubernetes.pki.certs = with top.lib; {
+        apiServer = mkCert {
+          name = "kube-apiserver";
+          CN = "kubernetes";
+          hosts = [
+                    "kubernetes.default.svc"
+                    "kubernetes.default.svc.${top.addons.dns.clusterDomain}"
+                    cfg.advertiseAddress
+                    top.masterAddress
+                    apiserverServiceIP
+                    "127.0.0.1"
+                  ] ++ cfg.extraSANs;
+          action = "systemctl restart kube-apiserver.service";
+        };
+        apiserverKubeletClient = mkCert {
+          name = "kube-apiserver-kubelet-client";
+          CN = "system:kube-apiserver";
+          action = "systemctl restart kube-apiserver.service";
+        };
+        apiserverEtcdClient = mkCert {
+          name = "kube-apiserver-etcd-client";
+          CN = "etcd-client";
+          action = "systemctl restart kube-apiserver.service";
+        };
+        clusterAdmin = mkCert {
+          name = "cluster-admin";
+          CN = "cluster-admin";
+          fields = {
+            O = "system:masters";
+          };
+          privateKeyOwner = "root";
+        };
+        etcd = mkCert {
+          name = "etcd";
+          CN = top.masterAddress;
+          hosts = [
+                    "etcd.local"
+                    "etcd.${top.addons.dns.clusterDomain}"
+                    top.masterAddress
+                    cfg.advertiseAddress
+                  ];
+          privateKeyOwner = "etcd";
+          action = "systemctl restart etcd.service";
+        };
+      };
+
+    })
+
+  ];
+
+}
diff --git a/nixos/modules/services/cluster/kubernetes/controller-manager.nix b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
new file mode 100644
index 00000000000..060fd9b78db
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
@@ -0,0 +1,162 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.controllerManager;
+in
+{
+  ###### interface
+  options.services.kubernetes.controllerManager = with lib.types; {
+
+    allocateNodeCIDRs = mkOption {
+      description = "Whether to automatically allocate CIDR ranges for cluster nodes.";
+      default = true;
+      type = bool;
+    };
+
+    bindAddress = mkOption {
+      description = "Kubernetes controller manager listening address.";
+      default = "127.0.0.1";
+      type = str;
+    };
+
+    clusterCidr = mkOption {
+      description = "Kubernetes CIDR Range for Pods in cluster.";
+      default = top.clusterCidr;
+      type = str;
+    };
+
+    enable = mkEnableOption "Kubernetes controller manager.";
+
+    extraOpts = mkOption {
+      description = "Kubernetes controller manager extra command line options.";
+      default = "";
+      type = str;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      type = listOf str;
+    };
+
+    insecurePort = mkOption {
+      description = "Kubernetes controller manager insecure listening port.";
+      default = 0;
+      type = int;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes controller manager";
+
+    leaderElect = mkOption {
+      description = "Whether to start leader election before executing main loop.";
+      type = bool;
+      default = true;
+    };
+
+    rootCaFile = mkOption {
+      description = ''
+        Kubernetes controller manager certificate authority file included in
+        service account's token secret.
+      '';
+      default = top.caFile;
+      type = nullOr path;
+    };
+
+    securePort = mkOption {
+      description = "Kubernetes controller manager secure listening port.";
+      default = 10252;
+      type = int;
+    };
+
+    serviceAccountKeyFile = mkOption {
+      description = ''
+        Kubernetes controller manager PEM-encoded private RSA key file used to
+        sign service account tokens
+      '';
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsCertFile = mkOption {
+      description = "Kubernetes controller-manager certificate file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsKeyFile = mkOption {
+      description = "Kubernetes controller-manager private key file.";
+      default = null;
+      type = nullOr path;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.kube-controller-manager = {
+      description = "Kubernetes Controller Manager Service";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      serviceConfig = {
+        RestartSec = "30s";
+        Restart = "on-failure";
+        Slice = "kubernetes.slice";
+        ExecStart = ''${top.package}/bin/kube-controller-manager \
+          --allocate-node-cidrs=${boolToString cfg.allocateNodeCIDRs} \
+          --bind-address=${cfg.bindAddress} \
+          ${optionalString (cfg.clusterCidr!=null)
+            "--cluster-cidr=${cfg.clusterCidr}"} \
+          ${optionalString (cfg.featureGates != [])
+            "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+          --kubeconfig=${top.lib.mkKubeConfig "kube-controller-manager" cfg.kubeconfig} \
+          --leader-elect=${boolToString cfg.leaderElect} \
+          ${optionalString (cfg.rootCaFile!=null)
+            "--root-ca-file=${cfg.rootCaFile}"} \
+          --port=${toString cfg.insecurePort} \
+          --secure-port=${toString cfg.securePort} \
+          ${optionalString (cfg.serviceAccountKeyFile!=null)
+            "--service-account-private-key-file=${cfg.serviceAccountKeyFile}"} \
+          ${optionalString (cfg.tlsCertFile!=null)
+            "--tls-cert-file=${cfg.tlsCertFile}"} \
+          ${optionalString (cfg.tlsKeyFile!=null)
+            "--tls-private-key-file=${cfg.tlsKeyFile}"} \
+          ${optionalString (elem "RBAC" top.apiserver.authorizationMode)
+            "--use-service-account-credentials"} \
+          ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+          ${cfg.extraOpts}
+        '';
+        WorkingDirectory = top.dataDir;
+        User = "kubernetes";
+        Group = "kubernetes";
+      };
+      path = top.path;
+    };
+
+    services.kubernetes.pki.certs = with top.lib; {
+      controllerManager = mkCert {
+        name = "kube-controller-manager";
+        CN = "kube-controller-manager";
+        action = "systemctl restart kube-controller-manager.service";
+      };
+      controllerManagerClient = mkCert {
+        name = "kube-controller-manager-client";
+        CN = "system:kube-controller-manager";
+        action = "systemctl restart kube-controller-manager.service";
+      };
+    };
+
+    services.kubernetes.controllerManager.kubeconfig.server = mkDefault top.apiserverAddress;
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix
index 6f3c45b29bf..3e53d18f8bb 100644
--- a/nixos/modules/services/cluster/kubernetes/default.nix
+++ b/nixos/modules/services/cluster/kubernetes/default.nix
@@ -5,74 +5,52 @@ with lib;
 let
   cfg = config.services.kubernetes;
 
-  # YAML config; see:
-  #   https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/
-  #   https://github.com/kubernetes/kubernetes/blob/release-1.10/pkg/kubelet/apis/kubeletconfig/v1beta1/types.go
-  #
-  # TODO: migrate the following flags to this config file
-  #
-  #   --pod-manifest-path
-  #   --address
-  #   --port
-  #   --tls-cert-file
-  #   --tls-private-key-file
-  #   --client-ca-file
-  #   --authentication-token-webhook
-  #   --authentication-token-webhook-cache-ttl
-  #   --authorization-mode
-  #   --healthz-bind-address
-  #   --healthz-port
-  #   --allow-privileged
-  #   --cluster-dns
-  #   --cluster-domain
-  #   --hairpin-mode
-  #   --feature-gates
-  kubeletConfig = pkgs.runCommand "kubelet-config.yaml" { } ''
-    echo > $out ${pkgs.lib.escapeShellArg (builtins.toJSON {
-      kind = "KubeletConfiguration";
-      apiVersion = "kubelet.config.k8s.io/v1beta1";
-      ${if cfg.kubelet.applyManifests then "staticPodPath" else null} =
-        manifests;
-    })}
-  '';
-
-  infraContainer = pkgs.dockerTools.buildImage {
-    name = "pause";
-    tag = "latest";
-    contents = cfg.package.pause;
-    config.Cmd = "/bin/pause";
-  };
-
-  mkKubeConfig = name: cfg: pkgs.writeText "${name}-kubeconfig" (builtins.toJSON {
+  mkKubeConfig = name: conf: pkgs.writeText "${name}-kubeconfig" (builtins.toJSON {
     apiVersion = "v1";
     kind = "Config";
     clusters = [{
       name = "local";
-      cluster.certificate-authority = cfg.caFile;
-      cluster.server = cfg.server;
+      cluster.certificate-authority = conf.caFile or cfg.caFile;
+      cluster.server = conf.server;
     }];
     users = [{
-      name = "kubelet";
+      inherit name;
       user = {
-        client-certificate = cfg.certFile;
-        client-key = cfg.keyFile;
+        client-certificate = conf.certFile;
+        client-key = conf.keyFile;
       };
     }];
     contexts = [{
       context = {
         cluster = "local";
-        user = "kubelet";
+        user = name;
       };
-      current-context = "kubelet-context";
+      current-context = "local";
     }];
   });
 
+  caCert = secret "ca";
+
+  etcdEndpoints = ["https://${cfg.masterAddress}:2379"];
+
+  mkCert = { name, CN, hosts ? [], fields ? {}, action ? "",
+             privateKeyOwner ? "kubernetes" }: rec {
+    inherit name caCert CN hosts fields action;
+    cert = secret name;
+    key = secret "${name}-key";
+    privateKeyOptions = {
+      owner = privateKeyOwner;
+      group = "nogroup";
+      mode = "0600";
+      path = key;
+    };
+  };
+
+  secret = name: "${cfg.secretsPath}/${name}.pem";
+
   mkKubeConfigOptions = prefix: {
     server = mkOption {
       description = "${prefix} kube-apiserver server address.";
-      default = "http://${if cfg.apiserver.advertiseAddress != null
-                          then cfg.apiserver.advertiseAddress
-                          else "127.0.0.1"}:${toString cfg.apiserver.port}";
       type = types.str;
     };
 
@@ -101,66 +79,6 @@ let
     certFile = mkDefault cfg.kubeconfig.certFile;
     keyFile = mkDefault cfg.kubeconfig.keyFile;
   };
-
-  cniConfig =
-    if cfg.kubelet.cni.config != [] && !(isNull cfg.kubelet.cni.configDir) then
-      throw "Verbatim CNI-config and CNI configDir cannot both be set."
-    else if !(isNull cfg.kubelet.cni.configDir) then
-      cfg.kubelet.cni.configDir
-    else
-      (pkgs.buildEnv {
-        name = "kubernetes-cni-config";
-        paths = imap (i: entry:
-          pkgs.writeTextDir "${toString (10+i)}-${entry.type}.conf" (builtins.toJSON entry)
-        ) cfg.kubelet.cni.config;
-      });
-
-  manifests = pkgs.buildEnv {
-    name = "kubernetes-manifests";
-    paths = mapAttrsToList (name: manifest:
-      pkgs.writeTextDir "${name}.json" (builtins.toJSON manifest)
-    ) cfg.kubelet.manifests;
-  };
-
-  addons = pkgs.runCommand "kubernetes-addons" { } ''
-    mkdir -p $out
-    # since we are mounting the addons to the addon manager, they need to be copied
-    ${concatMapStringsSep ";" (a: "cp -v ${a}/* $out/") (mapAttrsToList (name: addon:
-      pkgs.writeTextDir "${name}.json" (builtins.toJSON addon)
-    ) (cfg.addonManager.addons))}
-  '';
-
-  taintOptions = { name, ... }: {
-    options = {
-      key = mkOption {
-        description = "Key of taint.";
-        default = name;
-        type = types.str;
-      };
-      value = mkOption {
-        description = "Value of taint.";
-        type = types.str;
-      };
-      effect = mkOption {
-        description = "Effect of taint.";
-        example = "NoSchedule";
-        type = types.enum ["NoSchedule" "PreferNoSchedule" "NoExecute"];
-      };
-    };
-  };
-
-  taints = concatMapStringsSep "," (v: "${v.key}=${v.value}:${v.effect}") (mapAttrsToList (n: v: v) cfg.kubelet.taints);
-
-  # needed for flannel to pass options to docker
-  mkDockerOpts = pkgs.runCommand "mk-docker-opts" {
-    buildInputs = [ pkgs.makeWrapper ];
-  } ''
-    mkdir -p $out
-    cp ${pkgs.kubernetes.src}/cluster/centos/node/bin/mk-docker-opts.sh $out/mk-docker-opts.sh
-
-    # bashInteractive needed for `compgen`
-    makeWrapper ${pkgs.bashInteractive}/bin/bash $out/mk-docker-opts --add-flags "$out/mk-docker-opts.sh"
-  '';
 in {
 
   ###### interface
@@ -170,8 +88,9 @@ in {
       description = ''
         Kubernetes role that this machine should take.
 
-        Master role will enable etcd, apiserver, scheduler and controller manager
-        services. Node role will enable etcd, docker, kubelet and proxy services.
+        Master role will enable etcd, apiserver, scheduler, controller manager
+        addon manager, flannel and proxy services.
+        Node role will enable flannel, docker, kubelet and proxy services.
       '';
       default = [];
       type = types.listOf (types.enum ["master" "node"]);
@@ -184,40 +103,17 @@ in {
       defaultText = "pkgs.kubernetes";
     };
 
-    verbose = mkOption {
-      description = "Kubernetes enable verbose mode for debugging.";
-      default = false;
-      type = types.bool;
-    };
-
-    etcd = {
-      servers = mkOption {
-        description = "List of etcd servers. By default etcd is started, except if this option is changed.";
-        default = ["http://127.0.0.1:2379"];
-        type = types.listOf types.str;
-      };
-
-      keyFile = mkOption {
-        description = "Etcd key file.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      certFile = mkOption {
-        description = "Etcd cert file.";
-        default = null;
-        type = types.nullOr types.path;
-      };
+    kubeconfig = mkKubeConfigOptions "Default kubeconfig";
 
-      caFile = mkOption {
-        description = "Etcd ca file.";
-        default = cfg.caFile;
-        type = types.nullOr types.path;
-      };
+    apiserverAddress = mkOption {
+      description = ''
+        Clusterwide accessible address for the kubernetes apiserver,
+        including protocol and optional port.
+      '';
+      example = "https://kubernetes-apiserver.example.com:6443";
+      type = types.str;
     };
 
-    kubeconfig = mkKubeConfigOptions "Default kubeconfig";
-
     caFile = mkOption {
       description = "Default kubernetes certificate authority";
       type = types.nullOr types.path;
@@ -230,549 +126,22 @@ in {
       type = types.path;
     };
 
+    easyCerts = mkOption {
+      description = "Automatically setup x509 certificates and keys for the entire cluster.";
+      default = false;
+      type = types.bool;
+    };
+
     featureGates = mkOption {
-      description = "List set of feature gates";
+      description = "List set of feature gates.";
       default = [];
       type = types.listOf types.str;
     };
 
-    apiserver = {
-      enable = mkOption {
-        description = "Whether to enable Kubernetes apiserver.";
-        default = false;
-        type = types.bool;
-      };
-
-      featureGates = mkOption {
-        description = "List set of feature gates";
-        default = cfg.featureGates;
-        type = types.listOf types.str;
-      };
-
-      bindAddress = mkOption {
-        description = ''
-          The IP address on which to listen for the --secure-port port.
-          The associated interface(s) must be reachable by the rest
-          of the cluster, and by CLI/web clients.
-        '';
-        default = "0.0.0.0";
-        type = types.str;
-      };
-
-      advertiseAddress = mkOption {
-        description = ''
-          Kubernetes apiserver IP address on which to advertise the apiserver
-          to members of the cluster. This address must be reachable by the rest
-          of the cluster.
-        '';
-        default = null;
-        type = types.nullOr types.str;
-      };
-
-      storageBackend = mkOption {
-        description = ''
-          Kubernetes apiserver storage backend.
-        '';
-        default = "etcd3";
-        type = types.enum ["etcd2" "etcd3"];
-      };
-
-      port = mkOption {
-        description = "Kubernetes apiserver listening port.";
-        default = 8080;
-        type = types.int;
-      };
-
-      securePort = mkOption {
-        description = "Kubernetes apiserver secure port.";
-        default = 443;
-        type = types.int;
-      };
-
-      tlsCertFile = mkOption {
-        description = "Kubernetes apiserver certificate file.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      tlsKeyFile = mkOption {
-        description = "Kubernetes apiserver private key file.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      clientCaFile = mkOption {
-        description = "Kubernetes apiserver CA file for client auth.";
-        default = cfg.caFile;
-        type = types.nullOr types.path;
-      };
-
-      tokenAuthFile = mkOption {
-        description = ''
-          Kubernetes apiserver token authentication file. See
-          <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
-        '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      basicAuthFile = mkOption {
-        description = ''
-          Kubernetes apiserver basic authentication file. See
-          <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authentication"/>
-        '';
-        default = pkgs.writeText "users" ''
-          kubernetes,admin,0
-        '';
-        type = types.nullOr types.path;
-      };
-
-      authorizationMode = mkOption {
-        description = ''
-          Kubernetes apiserver authorization mode (AlwaysAllow/AlwaysDeny/ABAC/Webhook/RBAC/Node). See
-          <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
-        '';
-        default = ["RBAC" "Node"];
-        type = types.listOf (types.enum ["AlwaysAllow" "AlwaysDeny" "ABAC" "Webhook" "RBAC" "Node"]);
-      };
-
-      authorizationPolicy = mkOption {
-        description = ''
-          Kubernetes apiserver authorization policy file. See
-          <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/authorization/"/>
-        '';
-        default = [];
-        type = types.listOf types.attrs;
-      };
-
-      webhookConfig = mkOption {
-        description = ''
-          Kubernetes apiserver Webhook config file. It uses the kubeconfig file format.
-          See <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/webhook/"/>
-        '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      allowPrivileged = mkOption {
-        description = "Whether to allow privileged containers on Kubernetes.";
-        default = true;
-        type = types.bool;
-      };
-
-      serviceClusterIpRange = mkOption {
-        description = ''
-          A CIDR notation IP range from which to assign service cluster IPs.
-          This must not overlap with any IP ranges assigned to nodes for pods.
-        '';
-        default = "10.0.0.0/24";
-        type = types.str;
-      };
-
-      runtimeConfig = mkOption {
-        description = ''
-          Api runtime configuration. See
-          <link xlink:href="https://kubernetes.io/docs/tasks/administer-cluster/cluster-management/"/>
-        '';
-        default = "authentication.k8s.io/v1beta1=true";
-        example = "api/all=false,api/v1=true";
-        type = types.str;
-      };
-
-      enableAdmissionPlugins = mkOption {
-        description = ''
-          Kubernetes admission control plugins to enable. See
-          <link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
-        '';
-        default = ["NamespaceLifecycle" "LimitRanger" "ServiceAccount" "ResourceQuota" "DefaultStorageClass" "DefaultTolerationSeconds" "NodeRestriction"];
-        example = [
-          "NamespaceLifecycle" "NamespaceExists" "LimitRanger"
-          "SecurityContextDeny" "ServiceAccount" "ResourceQuota"
-          "PodSecurityPolicy" "NodeRestriction" "DefaultStorageClass"
-        ];
-        type = types.listOf types.str;
-      };
-
-      disableAdmissionPlugins = mkOption {
-        description = ''
-          Kubernetes admission control plugins to disable. See
-          <link xlink:href="https://kubernetes.io/docs/admin/admission-controllers/"/>
-        '';
-        default = [];
-        type = types.listOf types.str;
-      };
-
-      serviceAccountKeyFile = mkOption {
-        description = ''
-          Kubernetes apiserver PEM-encoded x509 RSA private or public key file,
-          used to verify ServiceAccount tokens. By default tls private key file
-          is used.
-        '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      kubeletClientCaFile = mkOption {
-        description = "Path to a cert file for connecting to kubelet.";
-        default = cfg.caFile;
-        type = types.nullOr types.path;
-      };
-
-      kubeletClientCertFile = mkOption {
-        description = "Client certificate to use for connections to kubelet.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      kubeletClientKeyFile = mkOption {
-        description = "Key to use for connections to kubelet.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      kubeletHttps = mkOption {
-        description = "Whether to use https for connections to kubelet.";
-        default = true;
-        type = types.bool;
-      };
-
-      extraOpts = mkOption {
-        description = "Kubernetes apiserver extra command line options.";
-        default = "";
-        type = types.str;
-      };
-    };
-
-    scheduler = {
-      enable = mkOption {
-        description = "Whether to enable Kubernetes scheduler.";
-        default = false;
-        type = types.bool;
-      };
-
-      featureGates = mkOption {
-        description = "List set of feature gates";
-        default = cfg.featureGates;
-        type = types.listOf types.str;
-      };
-
-      address = mkOption {
-        description = "Kubernetes scheduler listening address.";
-        default = "127.0.0.1";
-        type = types.str;
-      };
-
-      port = mkOption {
-        description = "Kubernetes scheduler listening port.";
-        default = 10251;
-        type = types.int;
-      };
-
-      leaderElect = mkOption {
-        description = "Whether to start leader election before executing main loop.";
-        type = types.bool;
-        default = true;
-      };
-
-      kubeconfig = mkKubeConfigOptions "Kubernetes scheduler";
-
-      extraOpts = mkOption {
-        description = "Kubernetes scheduler extra command line options.";
-        default = "";
-        type = types.str;
-      };
-    };
-
-    controllerManager = {
-      enable = mkOption {
-        description = "Whether to enable Kubernetes controller manager.";
-        default = false;
-        type = types.bool;
-      };
-
-      featureGates = mkOption {
-        description = "List set of feature gates";
-        default = cfg.featureGates;
-        type = types.listOf types.str;
-      };
-
-      address = mkOption {
-        description = "Kubernetes controller manager listening address.";
-        default = "127.0.0.1";
-        type = types.str;
-      };
-
-      port = mkOption {
-        description = "Kubernetes controller manager listening port.";
-        default = 10252;
-        type = types.int;
-      };
-
-      leaderElect = mkOption {
-        description = "Whether to start leader election before executing main loop.";
-        type = types.bool;
-        default = true;
-      };
-
-      serviceAccountKeyFile = mkOption {
-        description = ''
-          Kubernetes controller manager PEM-encoded private RSA key file used to
-          sign service account tokens
-        '';
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      rootCaFile = mkOption {
-        description = ''
-          Kubernetes controller manager certificate authority file included in
-          service account's token secret.
-        '';
-        default = cfg.caFile;
-        type = types.nullOr types.path;
-      };
-
-      kubeconfig = mkKubeConfigOptions "Kubernetes controller manager";
-
-      extraOpts = mkOption {
-        description = "Kubernetes controller manager extra command line options.";
-        default = "";
-        type = types.str;
-      };
-    };
-
-    kubelet = {
-      enable = mkOption {
-        description = "Whether to enable Kubernetes kubelet.";
-        default = false;
-        type = types.bool;
-      };
-
-      featureGates = mkOption {
-        description = "List set of feature gates";
-        default = cfg.featureGates;
-        type = types.listOf types.str;
-      };
-
-      seedDockerImages = mkOption {
-        description = "List of docker images to preload on system";
-        default = [];
-        type = types.listOf types.package;
-      };
-
-      registerNode = mkOption {
-        description = "Whether to auto register kubelet with API server.";
-        default = true;
-        type = types.bool;
-      };
-
-      address = mkOption {
-        description = "Kubernetes kubelet info server listening address.";
-        default = "0.0.0.0";
-        type = types.str;
-      };
-
-      port = mkOption {
-        description = "Kubernetes kubelet info server listening port.";
-        default = 10250;
-        type = types.int;
-      };
-
-      tlsCertFile = mkOption {
-        description = "File containing x509 Certificate for HTTPS.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      tlsKeyFile = mkOption {
-        description = "File containing x509 private key matching tlsCertFile.";
-        default = null;
-        type = types.nullOr types.path;
-      };
-
-      clientCaFile = mkOption {
-        description = "Kubernetes apiserver CA file for client authentication.";
-        default = cfg.caFile;
-        type = types.nullOr types.path;
-      };
-
-      healthz = {
-        bind = mkOption {
-          description = "Kubernetes kubelet healthz listening address.";
-          default = "127.0.0.1";
-          type = types.str;
-        };
-
-        port = mkOption {
-          description = "Kubernetes kubelet healthz port.";
-          default = 10248;
-          type = types.int;
-        };
-      };
-
-      hostname = mkOption {
-        description = "Kubernetes kubelet hostname override.";
-        default = config.networking.hostName;
-        type = types.str;
-      };
-
-      allowPrivileged = mkOption {
-        description = "Whether to allow Kubernetes containers to request privileged mode.";
-        default = true;
-        type = types.bool;
-      };
-
-      clusterDns = mkOption {
-        description = "Use alternative DNS.";
-        default = "10.1.0.1";
-        type = types.str;
-      };
-
-      clusterDomain = mkOption {
-        description = "Use alternative domain.";
-        default = config.services.kubernetes.addons.dns.clusterDomain;
-        type = types.str;
-      };
-
-      networkPlugin = mkOption {
-        description = "Network plugin to use by Kubernetes.";
-        type = types.nullOr (types.enum ["cni" "kubenet"]);
-        default = "kubenet";
-      };
-
-      cni = {
-        packages = mkOption {
-          description = "List of network plugin packages to install.";
-          type = types.listOf types.package;
-          default = [];
-        };
-
-        config = mkOption {
-          description = "Kubernetes CNI configuration.";
-          type = types.listOf types.attrs;
-          default = [];
-          example = literalExample ''
-            [{
-              "cniVersion": "0.2.0",
-              "name": "mynet",
-              "type": "bridge",
-              "bridge": "cni0",
-              "isGateway": true,
-              "ipMasq": true,
-              "ipam": {
-                  "type": "host-local",
-                  "subnet": "10.22.0.0/16",
-                  "routes": [
-                      { "dst": "0.0.0.0/0" }
-                  ]
-              }
-            } {
-              "cniVersion": "0.2.0",
-              "type": "loopback"
-            }]
-          '';
-        };
-
-        configDir = mkOption {
-          description = "Path to Kubernetes CNI configuration directory.";
-          type = types.nullOr types.path;
-          default = null;
-        };
-      };
-
-      manifests = mkOption {
-        description = "List of manifests to bootstrap with kubelet (only pods can be created as manifest entry)";
-        type = types.attrsOf types.attrs;
-        default = {};
-      };
-
-      applyManifests = mkOption {
-        description = "Whether to apply manifests (this is true for master node).";
-        default = false;
-        type = types.bool;
-      };
-
-      unschedulable = mkOption {
-        description = "Whether to set node taint to unschedulable=true as it is the case of node that has only master role.";
-        default = false;
-        type = types.bool;
-      };
-
-      taints = mkOption {
-        description = "Node taints (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/).";
-        default = {};
-        type = types.attrsOf (types.submodule [ taintOptions ]);
-      };
-
-      nodeIp = mkOption {
-        description = "IP address of the node. If set, kubelet will use this IP address for the node.";
-        default = null;
-        type = types.nullOr types.str;
-      };
-
-      kubeconfig = mkKubeConfigOptions "Kubelet";
-
-      extraOpts = mkOption {
-        description = "Kubernetes kubelet extra command line options.";
-        default = "";
-        type = types.str;
-      };
-    };
-
-    proxy = {
-      enable = mkOption {
-        description = "Whether to enable Kubernetes proxy.";
-        default = false;
-        type = types.bool;
-      };
-
-      featureGates = mkOption {
-        description = "List set of feature gates";
-        default = cfg.featureGates;
-        type = types.listOf types.str;
-      };
-
-      address = mkOption {
-        description = "Kubernetes proxy listening address.";
-        default = "0.0.0.0";
-        type = types.str;
-      };
-
-      kubeconfig = mkKubeConfigOptions "Kubernetes proxy";
-
-      extraOpts = mkOption {
-        description = "Kubernetes proxy extra command line options.";
-        default = "";
-        type = types.str;
-      };
-    };
-
-    addonManager = {
-      enable = mkOption {
-        description = "Whether to enable Kubernetes addon manager.";
-        default = false;
-        type = types.bool;
-      };
-
-      addons = mkOption {
-        description = "Kubernetes addons (any kind of Kubernetes resource can be an addon).";
-        default = { };
-        type = types.attrsOf (types.either types.attrs (types.listOf types.attrs));
-        example = literalExample ''
-          {
-            "my-service" = {
-              "apiVersion" = "v1";
-              "kind" = "Service";
-              "metadata" = {
-                "name" = "my-service";
-                "namespace" = "default";
-              };
-              "spec" = { ... };
-            };
-          }
-          // import <nixpkgs/nixos/modules/services/cluster/kubernetes/dashboard.nix> { cfg = config.services.kubernetes; };
-        '';
-      };
+    masterAddress = mkOption {
+      description = "Clusterwide available network address or hostname for the kubernetes master server.";
+      example = "master.example.com";
+      type = types.str;
     };
 
     path = mkOption {
@@ -787,304 +156,75 @@ in {
       type = types.nullOr types.str;
     };
 
-    flannel.enable = mkOption {
-      description = "Whether to enable flannel networking";
-      default = false;
-      type = types.bool;
+    lib = mkOption {
+      description = "Common functions for the kubernetes modules.";
+      default = {
+        inherit mkCert;
+        inherit mkKubeConfig;
+        inherit mkKubeConfigOptions;
+      };
+      type = types.attrs;
     };
 
+    secretsPath = mkOption {
+      description = "Default location for kubernetes secrets. Not a store location.";
+      type = types.path;
+      default = cfg.dataDir + "/secrets";
+    };
   };
 
   ###### implementation
 
   config = mkMerge [
-    (mkIf cfg.kubelet.enable {
-      services.kubernetes.kubelet.seedDockerImages = [infraContainer];
-
-      systemd.services.kubelet-bootstrap = {
-        description = "Boostrap Kubelet";
-        wantedBy = ["kubernetes.target"];
-        after = ["docker.service" "network.target"];
-        path = with pkgs; [ docker ];
-        script = ''
-          ${concatMapStrings (img: ''
-            echo "Seeding docker image: ${img}"
-            docker load <${img}
-          '') cfg.kubelet.seedDockerImages}
-
-          rm /opt/cni/bin/* || true
-          ${concatMapStrings (package: ''
-            echo "Linking cni package: ${package}"
-            ln -fs ${package}/bin/* /opt/cni/bin
-          '') cfg.kubelet.cni.packages}
-        '';
-        serviceConfig = {
-          Slice = "kubernetes.slice";
-          Type = "oneshot";
-        };
-      };
 
-      systemd.services.kubelet = {
-        description = "Kubernetes Kubelet Service";
-        wantedBy = [ "kubernetes.target" ];
-        after = [ "network.target" "docker.service" "kube-apiserver.service" "kubelet-bootstrap.service" ];
-        path = with pkgs; [ gitMinimal openssh docker utillinux iproute ethtool thin-provisioning-tools iptables socat ] ++ cfg.path;
-        serviceConfig = {
-          Slice = "kubernetes.slice";
-          CPUAccounting = true;
-          MemoryAccounting = true;
-          ExecStart = ''${cfg.package}/bin/kubelet \
-            ${optionalString (taints != "")
-              "--register-with-taints=${taints}"} \
-            --kubeconfig=${mkKubeConfig "kubelet" cfg.kubelet.kubeconfig} \
-            --config=${kubeletConfig} \
-            --address=${cfg.kubelet.address} \
-            --port=${toString cfg.kubelet.port} \
-            --register-node=${boolToString cfg.kubelet.registerNode} \
-            ${optionalString (cfg.kubelet.tlsCertFile != null)
-              "--tls-cert-file=${cfg.kubelet.tlsCertFile}"} \
-            ${optionalString (cfg.kubelet.tlsKeyFile != null)
-              "--tls-private-key-file=${cfg.kubelet.tlsKeyFile}"} \
-            ${optionalString (cfg.kubelet.clientCaFile != null)
-              "--client-ca-file=${cfg.kubelet.clientCaFile}"} \
-            --authentication-token-webhook \
-            --authentication-token-webhook-cache-ttl="10s" \
-            --authorization-mode=Webhook \
-            --healthz-bind-address=${cfg.kubelet.healthz.bind} \
-            --healthz-port=${toString cfg.kubelet.healthz.port} \
-            --hostname-override=${cfg.kubelet.hostname} \
-            --allow-privileged=${boolToString cfg.kubelet.allowPrivileged} \
-            --root-dir=${cfg.dataDir} \
-            ${optionalString (cfg.kubelet.clusterDns != "")
-              "--cluster-dns=${cfg.kubelet.clusterDns}"} \
-            ${optionalString (cfg.kubelet.clusterDomain != "")
-              "--cluster-domain=${cfg.kubelet.clusterDomain}"} \
-            --pod-infra-container-image=pause \
-            ${optionalString (cfg.kubelet.networkPlugin != null)
-              "--network-plugin=${cfg.kubelet.networkPlugin}"} \
-            --cni-conf-dir=${cniConfig} \
-            --hairpin-mode=hairpin-veth \
-            ${optionalString (cfg.kubelet.nodeIp != null)
-              "--node-ip=${cfg.kubelet.nodeIp}"} \
-            ${optionalString (cfg.kubelet.featureGates != [])
-              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.kubelet.featureGates}"} \
-            ${optionalString cfg.verbose "--v=6 --log_flush_frequency=1s"} \
-            ${cfg.kubelet.extraOpts}
-          '';
-          WorkingDirectory = cfg.dataDir;
-        };
-      };
-
-      # Allways include cni plugins
-      services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins];
-
-      boot.kernelModules = ["br_netfilter"];
-
-      services.kubernetes.kubelet.kubeconfig = kubeConfigDefaults;
-    })
-
-    (mkIf (cfg.kubelet.applyManifests && cfg.kubelet.enable) {
-      environment.etc = mapAttrs' (name: manifest:
-        nameValuePair "kubernetes/manifests/${name}.json" {
-          text = builtins.toJSON manifest;
-          mode = "0755";
-        }
-      ) cfg.kubelet.manifests;
-    })
-
-    (mkIf (cfg.kubelet.unschedulable && cfg.kubelet.enable) {
-      services.kubernetes.kubelet.taints.unschedulable = {
-        value = "true";
-        effect = "NoSchedule";
-      };
-    })
-
-    (mkIf cfg.apiserver.enable {
-      systemd.services.kube-apiserver = {
-        description = "Kubernetes APIServer Service";
-        wantedBy = [ "kubernetes.target" ];
-        after = [ "network.target" "docker.service" ];
-        serviceConfig = {
-          Slice = "kubernetes.slice";
-          ExecStart = ''${cfg.package}/bin/kube-apiserver \
-            --etcd-servers=${concatStringsSep "," cfg.etcd.servers} \
-            ${optionalString (cfg.etcd.caFile != null)
-              "--etcd-cafile=${cfg.etcd.caFile}"} \
-            ${optionalString (cfg.etcd.certFile != null)
-              "--etcd-certfile=${cfg.etcd.certFile}"} \
-            ${optionalString (cfg.etcd.keyFile != null)
-              "--etcd-keyfile=${cfg.etcd.keyFile}"} \
-            --insecure-port=${toString cfg.apiserver.port} \
-            --bind-address=${cfg.apiserver.bindAddress} \
-            ${optionalString (cfg.apiserver.advertiseAddress != null)
-              "--advertise-address=${cfg.apiserver.advertiseAddress}"} \
-            --allow-privileged=${boolToString cfg.apiserver.allowPrivileged}\
-            ${optionalString (cfg.apiserver.tlsCertFile != null)
-              "--tls-cert-file=${cfg.apiserver.tlsCertFile}"} \
-            ${optionalString (cfg.apiserver.tlsKeyFile != null)
-              "--tls-private-key-file=${cfg.apiserver.tlsKeyFile}"} \
-            ${optionalString (cfg.apiserver.tokenAuthFile != null)
-              "--token-auth-file=${cfg.apiserver.tokenAuthFile}"} \
-            ${optionalString (cfg.apiserver.basicAuthFile != null)
-              "--basic-auth-file=${cfg.apiserver.basicAuthFile}"} \
-            --kubelet-https=${if cfg.apiserver.kubeletHttps then "true" else "false"} \
-            ${optionalString (cfg.apiserver.kubeletClientCaFile != null)
-              "--kubelet-certificate-authority=${cfg.apiserver.kubeletClientCaFile}"} \
-            ${optionalString (cfg.apiserver.kubeletClientCertFile != null)
-              "--kubelet-client-certificate=${cfg.apiserver.kubeletClientCertFile}"} \
-            ${optionalString (cfg.apiserver.kubeletClientKeyFile != null)
-              "--kubelet-client-key=${cfg.apiserver.kubeletClientKeyFile}"} \
-            ${optionalString (cfg.apiserver.clientCaFile != null)
-              "--client-ca-file=${cfg.apiserver.clientCaFile}"} \
-            --authorization-mode=${concatStringsSep "," cfg.apiserver.authorizationMode} \
-            ${optionalString (elem "ABAC" cfg.apiserver.authorizationMode)
-              "--authorization-policy-file=${
-                pkgs.writeText "kube-auth-policy.jsonl"
-                (concatMapStringsSep "\n" (l: builtins.toJSON l) cfg.apiserver.authorizationPolicy)
-              }"
-            } \
-            ${optionalString (elem "Webhook" cfg.apiserver.authorizationMode)
-              "--authorization-webhook-config-file=${cfg.apiserver.webhookConfig}"
-            } \
-            --secure-port=${toString cfg.apiserver.securePort} \
-            --service-cluster-ip-range=${cfg.apiserver.serviceClusterIpRange} \
-            ${optionalString (cfg.apiserver.runtimeConfig != "")
-              "--runtime-config=${cfg.apiserver.runtimeConfig}"} \
-            --enable-admission-plugins=${concatStringsSep "," cfg.apiserver.enableAdmissionPlugins} \
-            --disable-admission-plugins=${concatStringsSep "," cfg.apiserver.disableAdmissionPlugins} \
-            ${optionalString (cfg.apiserver.serviceAccountKeyFile!=null)
-              "--service-account-key-file=${cfg.apiserver.serviceAccountKeyFile}"} \
-            ${optionalString cfg.verbose "--v=6"} \
-            ${optionalString cfg.verbose "--log-flush-frequency=1s"} \
-            --storage-backend=${cfg.apiserver.storageBackend} \
-            ${optionalString (cfg.kubelet.featureGates != [])
-              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.kubelet.featureGates}"} \
-            ${cfg.apiserver.extraOpts}
-          '';
-          WorkingDirectory = cfg.dataDir;
-          User = "kubernetes";
-          Group = "kubernetes";
-          AmbientCapabilities = "cap_net_bind_service";
-          Restart = "on-failure";
-          RestartSec = 5;
-        };
-      };
+    (mkIf cfg.easyCerts {
+      services.kubernetes.pki.enable = mkDefault true;
+      services.kubernetes.caFile = caCert;
     })
 
-    (mkIf cfg.scheduler.enable {
-      systemd.services.kube-scheduler = {
-        description = "Kubernetes Scheduler Service";
-        wantedBy = [ "kubernetes.target" ];
-        after = [ "kube-apiserver.service" ];
-        serviceConfig = {
-          Slice = "kubernetes.slice";
-          ExecStart = ''${cfg.package}/bin/kube-scheduler \
-            --address=${cfg.scheduler.address} \
-            --port=${toString cfg.scheduler.port} \
-            --leader-elect=${boolToString cfg.scheduler.leaderElect} \
-            --kubeconfig=${mkKubeConfig "kube-scheduler" cfg.scheduler.kubeconfig} \
-            ${optionalString cfg.verbose "--v=6"} \
-            ${optionalString cfg.verbose "--log-flush-frequency=1s"} \
-            ${optionalString (cfg.scheduler.featureGates != [])
-              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.scheduler.featureGates}"} \
-            ${cfg.scheduler.extraOpts}
-          '';
-          WorkingDirectory = cfg.dataDir;
-          User = "kubernetes";
-          Group = "kubernetes";
-        };
-      };
-
-      services.kubernetes.scheduler.kubeconfig = kubeConfigDefaults;
-    })
-
-    (mkIf cfg.controllerManager.enable {
-      systemd.services.kube-controller-manager = {
-        description = "Kubernetes Controller Manager Service";
-        wantedBy = [ "kubernetes.target" ];
-        after = [ "kube-apiserver.service" ];
-        serviceConfig = {
-          RestartSec = "30s";
-          Restart = "on-failure";
-          Slice = "kubernetes.slice";
-          ExecStart = ''${cfg.package}/bin/kube-controller-manager \
-            --address=${cfg.controllerManager.address} \
-            --port=${toString cfg.controllerManager.port} \
-            --kubeconfig=${mkKubeConfig "kube-controller-manager" cfg.controllerManager.kubeconfig} \
-            --leader-elect=${boolToString cfg.controllerManager.leaderElect} \
-            ${if (cfg.controllerManager.serviceAccountKeyFile!=null)
-              then "--service-account-private-key-file=${cfg.controllerManager.serviceAccountKeyFile}"
-              else "--service-account-private-key-file=/var/run/kubernetes/apiserver.key"} \
-            ${if (cfg.controllerManager.rootCaFile!=null)
-              then "--root-ca-file=${cfg.controllerManager.rootCaFile}"
-              else "--root-ca-file=/var/run/kubernetes/apiserver.crt"} \
-            ${if (cfg.clusterCidr!=null)
-              then "--cluster-cidr=${cfg.clusterCidr} --allocate-node-cidrs=true"
-              else "--allocate-node-cidrs=false"} \
-            ${optionalString (cfg.controllerManager.featureGates != [])
-              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.controllerManager.featureGates}"} \
-            ${optionalString cfg.verbose "--v=6"} \
-            ${optionalString cfg.verbose "--log-flush-frequency=1s"} \
-            ${cfg.controllerManager.extraOpts}
-          '';
-          WorkingDirectory = cfg.dataDir;
-          User = "kubernetes";
-          Group = "kubernetes";
+    (mkIf (elem "master" cfg.roles) {
+      services.kubernetes.apiserver.enable = mkDefault true;
+      services.kubernetes.scheduler.enable = mkDefault true;
+      services.kubernetes.controllerManager.enable = mkDefault true;
+      services.kubernetes.addonManager.enable = mkDefault true;
+      services.kubernetes.proxy.enable = mkDefault true;
+      services.etcd.enable = true; # Cannot mkDefault because of flannel default options
+      services.kubernetes.kubelet = {
+        enable = mkDefault true;
+        taints = mkIf (!(elem "node" cfg.roles)) {
+          master = {
+            key = "node-role.kubernetes.io/master";
+            value = "true";
+            effect = "NoSchedule";
+          };
         };
-        path = cfg.path;
       };
-
-      services.kubernetes.controllerManager.kubeconfig = kubeConfigDefaults;
     })
 
-    (mkIf cfg.proxy.enable {
-      systemd.services.kube-proxy = {
-        description = "Kubernetes Proxy Service";
-        wantedBy = [ "kubernetes.target" ];
-        after = [ "kube-apiserver.service" ];
-        path = [pkgs.iptables pkgs.conntrack_tools];
-        serviceConfig = {
-          Slice = "kubernetes.slice";
-          ExecStart = ''${cfg.package}/bin/kube-proxy \
-            --kubeconfig=${mkKubeConfig "kube-proxy" cfg.proxy.kubeconfig} \
-            --bind-address=${cfg.proxy.address} \
-            ${optionalString (cfg.proxy.featureGates != [])
-              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.proxy.featureGates}"} \
-            ${optionalString cfg.verbose "--v=6"} \
-            ${optionalString cfg.verbose "--log-flush-frequency=1s"} \
-            ${optionalString (cfg.clusterCidr!=null)
-              "--cluster-cidr=${cfg.clusterCidr}"} \
-            ${cfg.proxy.extraOpts}
-          '';
-          WorkingDirectory = cfg.dataDir;
-        };
-      };
-
-      # kube-proxy needs iptables
-      networking.firewall.enable = mkDefault true;
 
-      services.kubernetes.proxy.kubeconfig = kubeConfigDefaults;
+    (mkIf (all (el: el == "master") cfg.roles) {
+      # if this node is only a master make it unschedulable by default
+      services.kubernetes.kubelet.unschedulable = mkDefault true;
     })
 
-    (mkIf (any (el: el == "master") cfg.roles) {
-      virtualisation.docker.enable = mkDefault true;
+    (mkIf (elem "node" cfg.roles) {
       services.kubernetes.kubelet.enable = mkDefault true;
-      services.kubernetes.kubelet.allowPrivileged = mkDefault true;
-      services.kubernetes.kubelet.applyManifests = mkDefault true;
-      services.kubernetes.apiserver.enable = mkDefault true;
-      services.kubernetes.scheduler.enable = mkDefault true;
-      services.kubernetes.controllerManager.enable = mkDefault true;
-      services.etcd.enable = mkDefault (cfg.etcd.servers == ["http://127.0.0.1:2379"]);
-      services.kubernetes.addonManager.enable = mkDefault true;
       services.kubernetes.proxy.enable = mkDefault true;
     })
 
-    # if this node is only a master make it unschedulable by default
-    (mkIf (all (el: el == "master") cfg.roles) {
-      services.kubernetes.kubelet.unschedulable = mkDefault true;
+    # Using "services.kubernetes.roles" will automatically enable easyCerts and flannel
+    (mkIf (cfg.roles != []) {
+      services.kubernetes.flannel.enable = mkDefault true;
+      services.flannel.etcd.endpoints = mkDefault etcdEndpoints;
+      services.kubernetes.easyCerts = mkDefault true;
+    })
+
+    (mkIf cfg.apiserver.enable {
+      services.kubernetes.pki.etcClusterAdminKubeconfig = mkDefault "kubernetes/cluster-admin.kubeconfig";
+      services.kubernetes.apiserver.etcd.servers = mkDefault etcdEndpoints;
     })
 
-    (mkIf (any (el: el == "node") cfg.roles) {
+    (mkIf cfg.kubelet.enable {
       virtualisation.docker = {
         enable = mkDefault true;
 
@@ -1094,26 +234,18 @@ in {
         # iptables must be disabled for kubernetes
         extraOptions = "--iptables=false --ip-masq=false";
       };
-
-      services.kubernetes.kubelet.enable = mkDefault true;
-      services.kubernetes.proxy.enable = mkDefault true;
     })
 
-    (mkIf cfg.addonManager.enable {
-      environment.etc."kubernetes/addons".source = "${addons}/";
-
-      systemd.services.kube-addon-manager = {
-        description = "Kubernetes addon manager";
-        wantedBy = [ "kubernetes.target" ];
-        after = [ "kube-apiserver.service" ];
-        environment.ADDON_PATH = "/etc/kubernetes/addons/";
-        path = [ pkgs.gawk ];
-        serviceConfig = {
-          Slice = "kubernetes.slice";
-          ExecStart = "${cfg.package}/bin/kube-addons";
-          WorkingDirectory = cfg.dataDir;
-          User = "kubernetes";
-          Group = "kubernetes";
+    (mkIf (cfg.apiserver.enable || cfg.controllerManager.enable) {
+      services.kubernetes.pki.certs = {
+        serviceAccount = mkCert {
+          name = "service-account";
+          CN = "system:service-account-signer";
+          action = ''
+            systemctl reload \
+              kube-apiserver.service \
+              kube-controller-manager.service
+          '';
         };
       };
     })
@@ -1123,7 +255,8 @@ in {
         cfg.scheduler.enable ||
         cfg.controllerManager.enable ||
         cfg.kubelet.enable ||
-        cfg.proxy.enable
+        cfg.proxy.enable ||
+        cfg.addonManager.enable
     ) {
       systemd.targets.kubernetes = {
         description = "Kubernetes";
@@ -1132,11 +265,10 @@ in {
 
       systemd.tmpfiles.rules = [
         "d /opt/cni/bin 0755 root root -"
-        "d /var/run/kubernetes 0755 kubernetes kubernetes -"
+        "d /run/kubernetes 0755 kubernetes kubernetes -"
         "d /var/lib/kubernetes 0755 kubernetes kubernetes -"
       ];
 
-      environment.systemPackages = [ cfg.package ];
       users.users = singleton {
         name = "kubernetes";
         uid = config.ids.uids.kubernetes;
@@ -1148,53 +280,12 @@ in {
       };
       users.groups.kubernetes.gid = config.ids.gids.kubernetes;
 
-			# dns addon is enabled by default
+      # dns addon is enabled by default
       services.kubernetes.addons.dns.enable = mkDefault true;
-    })
 
-    (mkIf cfg.flannel.enable {
-      services.flannel = {
-        enable = mkDefault true;
-        network = mkDefault cfg.clusterCidr;
-        etcd = mkDefault {
-          endpoints = cfg.etcd.servers;
-          inherit (cfg.etcd) caFile certFile keyFile;
-        };
-      };
-
-      services.kubernetes.kubelet = {
-        networkPlugin = mkDefault "cni";
-        cni.config = mkDefault [{
-          name = "mynet";
-          type = "flannel";
-          delegate = {
-            isDefaultGateway = true;
-            bridge = "docker0";
-          };
-        }];
-      };
-
-      systemd.services."mk-docker-opts" = {
-        description = "Pre-Docker Actions";
-        wantedBy = [ "flannel.service" ];
-        before = [ "docker.service" ];
-        after = [ "flannel.service" ];
-        path = [ pkgs.gawk pkgs.gnugrep ];
-        script = ''
-          mkdir -p /run/flannel
-          ${mkDockerOpts}/mk-docker-opts -d /run/flannel/docker
-        '';
-        serviceConfig.Type = "oneshot";
-      };
-      systemd.services.docker.serviceConfig.EnvironmentFile = "/run/flannel/docker";
-
-      # read environment variables generated by mk-docker-opts
-      virtualisation.docker.extraOptions = "$DOCKER_OPTS";
-
-      networking.firewall.allowedUDPPorts = [
-        8285  # flannel udp
-        8472  # flannel vxlan
-      ];
+      services.kubernetes.apiserverAddress = mkDefault ("https://${if cfg.apiserver.advertiseAddress != null
+                          then cfg.apiserver.advertiseAddress
+                          else "${cfg.masterAddress}:${toString cfg.apiserver.securePort}"}");
     })
   ];
 }
diff --git a/nixos/modules/services/cluster/kubernetes/flannel.nix b/nixos/modules/services/cluster/kubernetes/flannel.nix
new file mode 100644
index 00000000000..93ee2fd65ee
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/flannel.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.flannel;
+
+  # we want flannel to use kubernetes itself as configuration backend, not direct etcd
+  storageBackend = "kubernetes";
+
+  # needed for flannel to pass options to docker
+  mkDockerOpts = pkgs.runCommand "mk-docker-opts" {
+    buildInputs = [ pkgs.makeWrapper ];
+  } ''
+    mkdir -p $out
+    cp ${pkgs.kubernetes.src}/cluster/centos/node/bin/mk-docker-opts.sh $out/mk-docker-opts.sh
+
+    # bashInteractive needed for `compgen`
+    makeWrapper ${pkgs.bashInteractive}/bin/bash $out/mk-docker-opts --add-flags "$out/mk-docker-opts.sh"
+  '';
+in
+{
+  ###### interface
+  options.services.kubernetes.flannel = {
+    enable = mkEnableOption "enable flannel networking";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.flannel = {
+
+      enable = mkDefault true;
+      network = mkDefault top.clusterCidr;
+      inherit storageBackend;
+      nodeName = config.services.kubernetes.kubelet.hostname;
+    };
+
+    services.kubernetes.kubelet = {
+      networkPlugin = mkDefault "cni";
+      cni.config = mkDefault [{
+        name = "mynet";
+        type = "flannel";
+        delegate = {
+          isDefaultGateway = true;
+          bridge = "docker0";
+        };
+      }];
+    };
+
+    systemd.services."mk-docker-opts" = {
+      description = "Pre-Docker Actions";
+      path = with pkgs; [ gawk gnugrep ];
+      script = ''
+        ${mkDockerOpts}/mk-docker-opts -d /run/flannel/docker
+        systemctl restart docker
+      '';
+      serviceConfig.Type = "oneshot";
+    };
+
+    systemd.paths."flannel-subnet-env" = {
+      wantedBy = [ "flannel.service" ];
+      pathConfig = {
+        PathModified = "/run/flannel/subnet.env";
+        Unit = "mk-docker-opts.service";
+      };
+    };
+
+    systemd.services.docker = {
+      environment.DOCKER_OPTS = "-b none";
+      serviceConfig.EnvironmentFile = "-/run/flannel/docker";
+    };
+
+    # read environment variables generated by mk-docker-opts
+    virtualisation.docker.extraOptions = "$DOCKER_OPTS";
+
+    networking = {
+      firewall.allowedUDPPorts = [
+        8285  # flannel udp
+        8472  # flannel vxlan
+      ];
+      dhcpcd.denyInterfaces = [ "docker*" "flannel*" ];
+    };
+
+    services.kubernetes.pki.certs = {
+      flannelClient = top.lib.mkCert {
+        name = "flannel-client";
+        CN = "flannel-client";
+        action = "systemctl restart flannel.service";
+      };
+    };
+
+    # give flannel som kubernetes rbac permissions if applicable
+    services.kubernetes.addonManager.bootstrapAddons = mkIf ((storageBackend == "kubernetes") && (elem "RBAC" top.apiserver.authorizationMode)) {
+
+      flannel-cr = {
+        apiVersion = "rbac.authorization.k8s.io/v1beta1";
+        kind = "ClusterRole";
+        metadata = { name = "flannel"; };
+        rules = [{
+          apiGroups = [ "" ];
+          resources = [ "pods" ];
+          verbs = [ "get" ];
+        }
+        {
+          apiGroups = [ "" ];
+          resources = [ "nodes" ];
+          verbs = [ "list" "watch" ];
+        }
+        {
+          apiGroups = [ "" ];
+          resources = [ "nodes/status" ];
+          verbs = [ "patch" ];
+        }];
+      };
+
+      flannel-crb = {
+        apiVersion = "rbac.authorization.k8s.io/v1beta1";
+        kind = "ClusterRoleBinding";
+        metadata = { name = "flannel"; };
+        roleRef = {
+          apiGroup = "rbac.authorization.k8s.io";
+          kind = "ClusterRole";
+          name = "flannel";
+        };
+        subjects = [{
+          kind = "User";
+          name = "flannel-client";
+        }];
+      };
+
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
new file mode 100644
index 00000000000..c94bb28bf7f
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -0,0 +1,358 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.kubelet;
+
+  cniConfig =
+    if cfg.cni.config != [] && !(isNull cfg.cni.configDir) then
+      throw "Verbatim CNI-config and CNI configDir cannot both be set."
+    else if !(isNull cfg.cni.configDir) then
+      cfg.cni.configDir
+    else
+      (pkgs.buildEnv {
+        name = "kubernetes-cni-config";
+        paths = imap (i: entry:
+          pkgs.writeTextDir "${toString (10+i)}-${entry.type}.conf" (builtins.toJSON entry)
+        ) cfg.cni.config;
+      });
+
+  infraContainer = pkgs.dockerTools.buildImage {
+    name = "pause";
+    tag = "latest";
+    contents = top.package.pause;
+    config.Cmd = "/bin/pause";
+  };
+
+  kubeconfig = top.lib.mkKubeConfig "kubelet" cfg.kubeconfig;
+
+  manifests = pkgs.buildEnv {
+    name = "kubernetes-manifests";
+    paths = mapAttrsToList (name: manifest:
+      pkgs.writeTextDir "${name}.json" (builtins.toJSON manifest)
+    ) cfg.manifests;
+  };
+
+  manifestPath = "kubernetes/manifests";
+
+  taintOptions = with lib.types; { name, ... }: {
+    options = {
+      key = mkOption {
+        description = "Key of taint.";
+        default = name;
+        type = str;
+      };
+      value = mkOption {
+        description = "Value of taint.";
+        type = str;
+      };
+      effect = mkOption {
+        description = "Effect of taint.";
+        example = "NoSchedule";
+        type = enum ["NoSchedule" "PreferNoSchedule" "NoExecute"];
+      };
+    };
+  };
+
+  taints = concatMapStringsSep "," (v: "${v.key}=${v.value}:${v.effect}") (mapAttrsToList (n: v: v) cfg.taints);
+in
+{
+  ###### interface
+  options.services.kubernetes.kubelet = with lib.types; {
+
+    address = mkOption {
+      description = "Kubernetes kubelet info server listening address.";
+      default = "0.0.0.0";
+      type = str;
+    };
+
+    allowPrivileged = mkOption {
+      description = "Whether to allow Kubernetes containers to request privileged mode.";
+      default = false;
+      type = bool;
+    };
+
+    clusterDns = mkOption {
+      description = "Use alternative DNS.";
+      default = "10.1.0.1";
+      type = str;
+    };
+
+    clusterDomain = mkOption {
+      description = "Use alternative domain.";
+      default = config.services.kubernetes.addons.dns.clusterDomain;
+      type = str;
+    };
+
+    clientCaFile = mkOption {
+      description = "Kubernetes apiserver CA file for client authentication.";
+      default = top.caFile;
+      type = nullOr path;
+    };
+
+    cni = {
+      packages = mkOption {
+        description = "List of network plugin packages to install.";
+        type = listOf package;
+        default = [];
+      };
+
+      config = mkOption {
+        description = "Kubernetes CNI configuration.";
+        type = listOf attrs;
+        default = [];
+        example = literalExample ''
+          [{
+            "cniVersion": "0.2.0",
+            "name": "mynet",
+            "type": "bridge",
+            "bridge": "cni0",
+            "isGateway": true,
+            "ipMasq": true,
+            "ipam": {
+                "type": "host-local",
+                "subnet": "10.22.0.0/16",
+                "routes": [
+                    { "dst": "0.0.0.0/0" }
+                ]
+            }
+          } {
+            "cniVersion": "0.2.0",
+            "type": "loopback"
+          }]
+        '';
+      };
+
+      configDir = mkOption {
+        description = "Path to Kubernetes CNI configuration directory.";
+        type = nullOr path;
+        default = null;
+      };
+    };
+
+    enable = mkEnableOption "Kubernetes kubelet.";
+
+    extraOpts = mkOption {
+      description = "Kubernetes kubelet extra command line options.";
+      default = "";
+      type = str;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      type = listOf str;
+    };
+
+    healthz = {
+      bind = mkOption {
+        description = "Kubernetes kubelet healthz listening address.";
+        default = "127.0.0.1";
+        type = str;
+      };
+
+      port = mkOption {
+        description = "Kubernetes kubelet healthz port.";
+        default = 10248;
+        type = int;
+      };
+    };
+
+    hostname = mkOption {
+      description = "Kubernetes kubelet hostname override.";
+      default = config.networking.hostName;
+      type = str;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubelet";
+
+    manifests = mkOption {
+      description = "List of manifests to bootstrap with kubelet (only pods can be created as manifest entry)";
+      type = attrsOf attrs;
+      default = {};
+    };
+
+    networkPlugin = mkOption {
+      description = "Network plugin to use by Kubernetes.";
+      type = nullOr (enum ["cni" "kubenet"]);
+      default = "kubenet";
+    };
+
+    nodeIp = mkOption {
+      description = "IP address of the node. If set, kubelet will use this IP address for the node.";
+      default = null;
+      type = nullOr str;
+    };
+
+    registerNode = mkOption {
+      description = "Whether to auto register kubelet with API server.";
+      default = true;
+      type = bool;
+    };
+
+    port = mkOption {
+      description = "Kubernetes kubelet info server listening port.";
+      default = 10250;
+      type = int;
+    };
+
+    seedDockerImages = mkOption {
+      description = "List of docker images to preload on system";
+      default = [];
+      type = listOf package;
+    };
+
+    taints = mkOption {
+      description = "Node taints (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/).";
+      default = {};
+      type = attrsOf (submodule [ taintOptions ]);
+    };
+
+    tlsCertFile = mkOption {
+      description = "File containing x509 Certificate for HTTPS.";
+      default = null;
+      type = nullOr path;
+    };
+
+    tlsKeyFile = mkOption {
+      description = "File containing x509 private key matching tlsCertFile.";
+      default = null;
+      type = nullOr path;
+    };
+
+    unschedulable = mkOption {
+      description = "Whether to set node taint to unschedulable=true as it is the case of node that has only master role.";
+      default = false;
+      type = bool;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkMerge [
+    (mkIf cfg.enable {
+      services.kubernetes.kubelet.seedDockerImages = [infraContainer];
+
+      systemd.services.kubelet = {
+        description = "Kubernetes Kubelet Service";
+        wantedBy = [ "kubernetes.target" ];
+        after = [ "network.target" "docker.service" "kube-apiserver.service" ];
+        path = with pkgs; [ gitMinimal openssh docker utillinux iproute ethtool thin-provisioning-tools iptables socat ] ++ top.path;
+        preStart = ''
+          ${concatMapStrings (img: ''
+            echo "Seeding docker image: ${img}"
+            docker load <${img}
+          '') cfg.seedDockerImages}
+
+          rm /opt/cni/bin/* || true
+          ${concatMapStrings (package: ''
+            echo "Linking cni package: ${package}"
+            ln -fs ${package}/bin/* /opt/cni/bin
+          '') cfg.cni.packages}
+        '';
+        serviceConfig = {
+          Slice = "kubernetes.slice";
+          CPUAccounting = true;
+          MemoryAccounting = true;
+          Restart = "on-failure";
+          RestartSec = "1000ms";
+          ExecStart = ''${top.package}/bin/kubelet \
+            --address=${cfg.address} \
+            --allow-privileged=${boolToString cfg.allowPrivileged} \
+            --authentication-token-webhook \
+            --authentication-token-webhook-cache-ttl="10s" \
+            --authorization-mode=Webhook \
+            ${optionalString (cfg.clientCaFile != null)
+              "--client-ca-file=${cfg.clientCaFile}"} \
+            ${optionalString (cfg.clusterDns != "")
+              "--cluster-dns=${cfg.clusterDns}"} \
+            ${optionalString (cfg.clusterDomain != "")
+              "--cluster-domain=${cfg.clusterDomain}"} \
+            --cni-conf-dir=${cniConfig} \
+            ${optionalString (cfg.featureGates != [])
+              "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+            --hairpin-mode=hairpin-veth \
+            --healthz-bind-address=${cfg.healthz.bind} \
+            --healthz-port=${toString cfg.healthz.port} \
+            --hostname-override=${cfg.hostname} \
+            --kubeconfig=${kubeconfig} \
+            ${optionalString (cfg.networkPlugin != null)
+              "--network-plugin=${cfg.networkPlugin}"} \
+            ${optionalString (cfg.nodeIp != null)
+              "--node-ip=${cfg.nodeIp}"} \
+            --pod-infra-container-image=pause \
+            ${optionalString (cfg.manifests != {})
+              "--pod-manifest-path=/etc/${manifestPath}"} \
+            --port=${toString cfg.port} \
+            --register-node=${boolToString cfg.registerNode} \
+            ${optionalString (taints != "")
+              "--register-with-taints=${taints}"} \
+            --root-dir=${top.dataDir} \
+            ${optionalString (cfg.tlsCertFile != null)
+              "--tls-cert-file=${cfg.tlsCertFile}"} \
+            ${optionalString (cfg.tlsKeyFile != null)
+              "--tls-private-key-file=${cfg.tlsKeyFile}"} \
+            ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+            ${cfg.extraOpts}
+          '';
+          WorkingDirectory = top.dataDir;
+        };
+      };
+
+      # Allways include cni plugins
+      services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins];
+
+      boot.kernelModules = ["br_netfilter"];
+
+      services.kubernetes.kubelet.hostname = with config.networking;
+        mkDefault (hostName + optionalString (!isNull domain) ".${domain}");
+
+      services.kubernetes.pki.certs = with top.lib; {
+        kubelet = mkCert {
+          name = "kubelet";
+          CN = top.kubelet.hostname;
+          action = "systemctl restart kubelet.service";
+
+        };
+        kubeletClient = mkCert {
+          name = "kubelet-client";
+          CN = "system:node:${top.kubelet.hostname}";
+          fields = {
+            O = "system:nodes";
+          };
+          action = "systemctl restart kubelet.service";
+        };
+      };
+
+      services.kubernetes.kubelet.kubeconfig.server = mkDefault top.apiserverAddress;
+    })
+
+    (mkIf (cfg.enable && cfg.manifests != {}) {
+      environment.etc = mapAttrs' (name: manifest:
+        nameValuePair "${manifestPath}/${name}.json" {
+          text = builtins.toJSON manifest;
+          mode = "0755";
+        }
+      ) cfg.manifests;
+    })
+
+    (mkIf (cfg.unschedulable && cfg.enable) {
+      services.kubernetes.kubelet.taints.unschedulable = {
+        value = "true";
+        effect = "NoSchedule";
+      };
+    })
+
+  ];
+}
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
new file mode 100644
index 00000000000..38deca23a99
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -0,0 +1,388 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.pki;
+
+  csrCA = pkgs.writeText "kube-pki-cacert-csr.json" (builtins.toJSON {
+    key = {
+        algo = "rsa";
+        size = 2048;
+    };
+    names = singleton cfg.caSpec;
+  });
+
+  csrCfssl = pkgs.writeText "kube-pki-cfssl-csr.json" (builtins.toJSON {
+    key = {
+        algo = "rsa";
+        size = 2048;
+    };
+    CN = top.masterAddress;
+  });
+
+  cfsslAPITokenBaseName = "apitoken.secret";
+  cfsslAPITokenPath = "${config.services.cfssl.dataDir}/${cfsslAPITokenBaseName}";
+  certmgrAPITokenPath = "${top.secretsPath}/${cfsslAPITokenBaseName}";
+  cfsslAPITokenLength = 32;
+
+  clusterAdminKubeconfig = with cfg.certs.clusterAdmin;
+    top.lib.mkKubeConfig "cluster-admin" {
+        server = top.apiserverAddress;
+        certFile = cert;
+        keyFile = key;
+    };
+
+  remote = with config.services; "https://${kubernetes.masterAddress}:${toString cfssl.port}";
+in
+{
+  ###### interface
+  options.services.kubernetes.pki = with lib.types; {
+
+    enable = mkEnableOption "Whether to enable easyCert issuer service.";
+
+    certs = mkOption {
+      description = "List of certificate specs to feed to cert generator.";
+      default = {};
+      type = attrs;
+    };
+
+    genCfsslCACert = mkOption {
+      description = ''
+        Whether to automatically generate cfssl CA certificate and key,
+        if they don't exist.
+      '';
+      default = true;
+      type = bool;
+    };
+
+    genCfsslAPICerts = mkOption {
+      description = ''
+        Whether to automatically generate cfssl API webserver TLS cert and key,
+        if they don't exist.
+      '';
+      default = true;
+      type = bool;
+    };
+
+    genCfsslAPIToken = mkOption {
+      description = ''
+        Whether to automatically generate cfssl API-token secret,
+        if they doesn't exist.
+      '';
+      default = true;
+      type = bool;
+    };
+
+    pkiTrustOnBootstrap = mkOption {
+      description = "Whether to always trust remote cfssl server upon initial PKI bootstrap.";
+      default = true;
+      type = bool;
+    };
+
+    caCertPathPrefix = mkOption {
+      description = ''
+        Path-prefrix for the CA-certificate to be used for cfssl signing.
+        Suffixes ".pem" and "-key.pem" will be automatically appended for
+        the public and private keys respectively.
+      '';
+      default = "${config.services.cfssl.dataDir}/ca";
+      type = str;
+    };
+
+    caSpec = mkOption {
+      description = "Certificate specification for the auto-generated CAcert.";
+      default = {
+        CN = "kubernetes-cluster-ca";
+        O = "NixOS";
+        OU = "services.kubernetes.pki.caSpec";
+        L = "auto-generated";
+      };
+      type = attrs;
+    };
+
+    etcClusterAdminKubeconfig = mkOption {
+      description = ''
+        Symlink a kubeconfig with cluster-admin privileges to environment path
+        (/etc/&lt;path&gt;).
+      '';
+      default = null;
+      type = nullOr str;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable
+  (let
+    cfsslCertPathPrefix = "${config.services.cfssl.dataDir}/cfssl";
+    cfsslCert = "${cfsslCertPathPrefix}.pem";
+    cfsslKey = "${cfsslCertPathPrefix}-key.pem";
+  in
+  {
+
+    services.cfssl = mkIf (top.apiserver.enable) {
+      enable = true;
+      address = "0.0.0.0";
+      tlsCert = cfsslCert;
+      tlsKey = cfsslKey;
+      configFile = toString (pkgs.writeText "cfssl-config.json" (builtins.toJSON {
+        signing = {
+          profiles = {
+            default = {
+              usages = ["digital signature"];
+              auth_key = "default";
+              expiry = "720h";
+            };
+          };
+        };
+        auth_keys = {
+          default = {
+            type = "standard";
+            key = "file:${cfsslAPITokenPath}";
+          };
+        };
+      }));
+    };
+
+    systemd.services.cfssl.preStart = with pkgs; with config.services.cfssl; mkIf (top.apiserver.enable)
+    (concatStringsSep "\n" [
+      "set -e"
+      (optionalString cfg.genCfsslCACert ''
+        if [ ! -f "${cfg.caCertPathPrefix}.pem" ]; then
+          ${cfssl}/bin/cfssl genkey -initca ${csrCA} | \
+            ${cfssl}/bin/cfssljson -bare ${cfg.caCertPathPrefix}
+        fi
+      '')
+      (optionalString cfg.genCfsslAPICerts ''
+        if [ ! -f "${dataDir}/cfssl.pem" ]; then
+          ${cfssl}/bin/cfssl gencert -ca "${cfg.caCertPathPrefix}.pem" -ca-key "${cfg.caCertPathPrefix}-key.pem" ${csrCfssl} | \
+            ${cfssl}/bin/cfssljson -bare ${cfsslCertPathPrefix}
+        fi
+      '')
+      (optionalString cfg.genCfsslAPIToken ''
+        if [ ! -f "${cfsslAPITokenPath}" ]; then
+          head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ' >"${cfsslAPITokenPath}"
+        fi
+        chown cfssl "${cfsslAPITokenPath}" && chmod 400 "${cfsslAPITokenPath}"
+      '')]);
+
+    systemd.services.kube-certmgr-bootstrap = {
+      description = "Kubernetes certmgr bootstrapper";
+      wantedBy = [ "certmgr.service" ];
+      after = [ "cfssl.target" ];
+      script = concatStringsSep "\n" [''
+        set -e
+
+        # If there's a cfssl (cert issuer) running locally, then don't rely on user to
+        # manually paste it in place. Just symlink.
+        # otherwise, create the target file, ready for users to insert the token
+
+        if [ -f "${cfsslAPITokenPath}" ]; then
+          ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}"
+        else
+          touch "${certmgrAPITokenPath}" && chmod 600 "${certmgrAPITokenPath}"
+        fi
+      ''
+      (optionalString (cfg.pkiTrustOnBootstrap) ''
+        if [ ! -f "${top.caFile}" ] || [ $(cat "${top.caFile}" | wc -c) -lt 1 ]; then
+          ${pkgs.curl}/bin/curl --fail-early -f -kd '{}' ${remote}/api/v1/cfssl/info | \
+            ${pkgs.cfssl}/bin/cfssljson -stdout >${top.caFile}
+        fi
+      '')
+      ];
+      serviceConfig = {
+        RestartSec = "10s";
+        Restart = "on-failure";
+      };
+    };
+
+    services.certmgr = {
+      enable = true;
+      package = pkgs.certmgr-selfsigned;
+      svcManager = "command";
+      specs =
+        let
+          mkSpec = _: cert: {
+            inherit (cert) action;
+            authority = {
+              inherit remote;
+              file.path = cert.caCert;
+              root_ca = cert.caCert;
+              profile = "default";
+              auth_key_file = certmgrAPITokenPath;
+            };
+            certificate = {
+              path = cert.cert;
+            };
+            private_key = cert.privateKeyOptions;
+            request = {
+              inherit (cert) CN hosts;
+              key = {
+                algo = "rsa";
+                size = 2048;
+              };
+              names = [ cert.fields ];
+            };
+          };
+        in
+          mapAttrs mkSpec cfg.certs;
+      };
+
+      #TODO: Get rid of kube-addon-manager in the future for the following reasons
+      # - it is basically just a shell script wrapped around kubectl
+      # - it assumes that it is clusterAdmin or can gain clusterAdmin rights through serviceAccount
+      # - it is designed to be used with k8s system components only
+      # - it would be better with a more Nix-oriented way of managing addons
+      systemd.services.kube-addon-manager = mkIf top.addonManager.enable (mkMerge [{
+        environment.KUBECONFIG = with cfg.certs.addonManager;
+          top.lib.mkKubeConfig "addon-manager" {
+            server = top.apiserverAddress;
+            certFile = cert;
+            keyFile = key;
+          };
+        }
+
+        (optionalAttrs (top.addonManager.bootstrapAddons != {}) {
+          serviceConfig.PermissionsStartOnly = true;
+          preStart = with pkgs;
+          let
+            files = mapAttrsToList (n: v: writeText "${n}.json" (builtins.toJSON v))
+              top.addonManager.bootstrapAddons;
+          in
+          ''
+            export KUBECONFIG=${clusterAdminKubeconfig}
+            ${kubectl}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files}
+          '';
+        })]);
+
+      environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (!isNull cfg.etcClusterAdminKubeconfig)
+        clusterAdminKubeconfig;
+
+      environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [
+      (pkgs.writeScriptBin "nixos-kubernetes-node-join" ''
+        set -e
+        exec 1>&2
+
+        if [ $# -gt 0 ]; then
+          echo "Usage: $(basename $0)"
+          echo ""
+          echo "No args. Apitoken must be provided on stdin."
+          echo "To get the apitoken, execute: 'sudo cat ${certmgrAPITokenPath}' on the master node."
+          exit 1
+        fi
+
+        if [ $(id -u) != 0 ]; then
+          echo "Run as root please."
+          exit 1
+        fi
+
+        read -r token
+        if [ ''${#token} != ${toString cfsslAPITokenLength} ]; then
+          echo "Token must be of length ${toString cfsslAPITokenLength}."
+          exit 1
+        fi
+
+        echo $token > ${certmgrAPITokenPath}
+        chmod 600 ${certmgrAPITokenPath}
+
+        echo "Restarting certmgr..." >&1
+        systemctl restart certmgr
+
+        echo "Waiting for certs to appear..." >&1
+
+        ${optionalString top.kubelet.enable ''
+          while [ ! -f ${cfg.certs.kubelet.cert} ]; do sleep 1; done
+          echo "Restarting kubelet..." >&1
+          systemctl restart kubelet
+        ''}
+
+        ${optionalString top.proxy.enable ''
+          while [ ! -f ${cfg.certs.kubeProxyClient.cert} ]; do sleep 1; done
+          echo "Restarting kube-proxy..." >&1
+          systemctl restart kube-proxy
+        ''}
+
+        ${optionalString top.flannel.enable ''
+          while [ ! -f ${cfg.certs.flannelClient.cert} ]; do sleep 1; done
+          echo "Restarting flannel..." >&1
+          systemctl restart flannel
+        ''}
+
+        echo "Node joined succesfully"
+      '')];
+
+      # isolate etcd on loopback at the master node
+      # easyCerts doesn't support multimaster clusters anyway atm.
+      services.etcd = with cfg.certs.etcd; {
+        listenClientUrls = ["https://127.0.0.1:2379"];
+        listenPeerUrls = ["https://127.0.0.1:2380"];
+        advertiseClientUrls = ["https://etcd.local:2379"];
+        initialCluster = ["${top.masterAddress}=https://etcd.local:2380"];
+        initialAdvertisePeerUrls = ["https://etcd.local:2380"];
+        certFile = mkDefault cert;
+        keyFile = mkDefault key;
+        trustedCaFile = mkDefault caCert;
+      };
+      networking.extraHosts = mkIf (config.services.etcd.enable) ''
+        127.0.0.1 etcd.${top.addons.dns.clusterDomain} etcd.local
+      '';
+
+      services.flannel = with cfg.certs.flannelClient; {
+        kubeconfig = top.lib.mkKubeConfig "flannel" {
+          server = top.apiserverAddress;
+          certFile = cert;
+          keyFile = key;
+        };
+      };
+
+      services.kubernetes = {
+
+        apiserver = mkIf top.apiserver.enable (with cfg.certs.apiServer; {
+          etcd = with cfg.certs.apiserverEtcdClient; {
+            servers = ["https://etcd.local:2379"];
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+            caFile = mkDefault caCert;
+          };
+          clientCaFile = mkDefault caCert;
+          tlsCertFile = mkDefault cert;
+          tlsKeyFile = mkDefault key;
+          serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.cert;
+          kubeletClientCaFile = mkDefault caCert;
+          kubeletClientCertFile = mkDefault cfg.certs.apiserverKubeletClient.cert;
+          kubeletClientKeyFile = mkDefault cfg.certs.apiserverKubeletClient.key;
+        });
+        controllerManager = mkIf top.controllerManager.enable {
+          serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.key;
+          rootCaFile = cfg.certs.controllerManagerClient.caCert;
+          kubeconfig = with cfg.certs.controllerManagerClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+        scheduler = mkIf top.scheduler.enable {
+          kubeconfig = with cfg.certs.schedulerClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+        kubelet = mkIf top.kubelet.enable {
+          clientCaFile = mkDefault cfg.certs.kubelet.caCert;
+          tlsCertFile = mkDefault cfg.certs.kubelet.cert;
+          tlsKeyFile = mkDefault cfg.certs.kubelet.key;
+          kubeconfig = with cfg.certs.kubeletClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+        proxy = mkIf top.proxy.enable {
+          kubeconfig = with cfg.certs.kubeProxyClient; {
+            certFile = mkDefault cert;
+            keyFile = mkDefault key;
+          };
+        };
+      };
+    });
+}
diff --git a/nixos/modules/services/cluster/kubernetes/proxy.nix b/nixos/modules/services/cluster/kubernetes/proxy.nix
new file mode 100644
index 00000000000..83cd3e23100
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/proxy.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.proxy;
+in
+{
+
+  ###### interface
+  options.services.kubernetes.proxy = with lib.types; {
+
+    bindAddress = mkOption {
+      description = "Kubernetes proxy listening address.";
+      default = "0.0.0.0";
+      type = str;
+    };
+
+    enable = mkEnableOption "Whether to enable Kubernetes proxy.";
+
+    extraOpts = mkOption {
+      description = "Kubernetes proxy extra command line options.";
+      default = "";
+      type = str;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      type = listOf str;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes proxy";
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.kube-proxy = {
+      description = "Kubernetes Proxy Service";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      path = with pkgs; [ iptables conntrack_tools ];
+      serviceConfig = {
+        Slice = "kubernetes.slice";
+        ExecStart = ''${top.package}/bin/kube-proxy \
+          --bind-address=${cfg.bindAddress} \
+          ${optionalString (top.clusterCidr!=null)
+            "--cluster-cidr=${top.clusterCidr}"} \
+          ${optionalString (cfg.featureGates != [])
+            "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+          --kubeconfig=${top.lib.mkKubeConfig "kube-proxy" cfg.kubeconfig} \
+          ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+          ${cfg.extraOpts}
+        '';
+        WorkingDirectory = top.dataDir;
+        Restart = "on-failure";
+        RestartSec = 5;
+      };
+    };
+
+    services.kubernetes.pki.certs = {
+      kubeProxyClient = top.lib.mkCert {
+        name = "kube-proxy-client";
+        CN = "system:kube-proxy";
+        action = "systemctl restart kube-proxy.service";
+      };
+    };
+
+    services.kubernetes.proxy.kubeconfig.server = mkDefault top.apiserverAddress;
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
new file mode 100644
index 00000000000..0305b9aefe5
--- /dev/null
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  top = config.services.kubernetes;
+  cfg = top.scheduler;
+in
+{
+  ###### interface
+  options.services.kubernetes.scheduler = with lib.types; {
+
+    address = mkOption {
+      description = "Kubernetes scheduler listening address.";
+      default = "127.0.0.1";
+      type = str;
+    };
+
+    enable = mkEnableOption "Whether to enable Kubernetes scheduler.";
+
+    extraOpts = mkOption {
+      description = "Kubernetes scheduler extra command line options.";
+      default = "";
+      type = str;
+    };
+
+    featureGates = mkOption {
+      description = "List set of feature gates";
+      default = top.featureGates;
+      type = listOf str;
+    };
+
+    kubeconfig = top.lib.mkKubeConfigOptions "Kubernetes scheduler";
+
+    leaderElect = mkOption {
+      description = "Whether to start leader election before executing main loop.";
+      type = bool;
+      default = true;
+    };
+
+    port = mkOption {
+      description = "Kubernetes scheduler listening port.";
+      default = 10251;
+      type = int;
+    };
+
+    verbosity = mkOption {
+      description = ''
+        Optional glog verbosity level for logging statements. See
+        <link xlink:href="https://github.com/kubernetes/community/blob/master/contributors/devel/logging.md"/>
+      '';
+      default = null;
+      type = nullOr int;
+    };
+
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    systemd.services.kube-scheduler = {
+      description = "Kubernetes Scheduler Service";
+      wantedBy = [ "kubernetes.target" ];
+      after = [ "kube-apiserver.service" ];
+      serviceConfig = {
+        Slice = "kubernetes.slice";
+        ExecStart = ''${top.package}/bin/kube-scheduler \
+          --address=${cfg.address} \
+          ${optionalString (cfg.featureGates != [])
+            "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \
+          --kubeconfig=${top.lib.mkKubeConfig "kube-scheduler" cfg.kubeconfig} \
+          --leader-elect=${boolToString cfg.leaderElect} \
+          --port=${toString cfg.port} \
+          ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+          ${cfg.extraOpts}
+        '';
+        WorkingDirectory = top.dataDir;
+        User = "kubernetes";
+        Group = "kubernetes";
+        Restart = "on-failure";
+        RestartSec = 5;
+      };
+    };
+
+    services.kubernetes.pki.certs = {
+      schedulerClient = top.lib.mkCert {
+        name = "kube-scheduler-client";
+        CN = "system:kube-scheduler";
+        action = "systemctl restart kube-scheduler.service";
+      };
+    };
+
+    services.kubernetes.scheduler.kubeconfig.server = mkDefault top.apiserverAddress;
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/buildkite-agent.nix b/nixos/modules/services/continuous-integration/buildkite-agent.nix
index 9daf391c73c..2136778aff4 100644
--- a/nixos/modules/services/continuous-integration/buildkite-agent.nix
+++ b/nixos/modules/services/continuous-integration/buildkite-agent.nix
@@ -24,7 +24,7 @@ let
       EOF
       chmod 755 $out/${name}
     '';
-  in pkgs.runCommand "buildkite-agent-hooks" {} ''
+  in pkgs.runCommand "buildkite-agent-hooks" { preferLocalBuild = true; } ''
     mkdir $out
     ${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))}
   '';
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index a0aff1b8b5b..3ceaa6f5ff3 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -8,6 +8,7 @@ let
     if (cfg.configFile == null) then
       (pkgs.runCommand "config.toml" {
         buildInputs = [ pkgs.remarshal ];
+        preferLocalBuild = true;
       } ''
         remarshal -if json -of toml \
           < ${pkgs.writeText "config.json" (builtins.toJSON cfg.configOptions)} \
diff --git a/nixos/modules/services/databases/hbase.nix b/nixos/modules/services/databases/hbase.nix
index 4772e897efe..52f2d95b4e0 100644
--- a/nixos/modules/services/databases/hbase.nix
+++ b/nixos/modules/services/databases/hbase.nix
@@ -18,7 +18,7 @@ let
     </configuration>
   '';
 
-  configDir = pkgs.runCommand "hbase-config-dir" {} ''
+  configDir = pkgs.runCommand "hbase-config-dir" { preferLocalBuild = true; } ''
     mkdir -p $out
     cp ${cfg.package}/conf/* $out/
     rm $out/hbase-site.xml
diff --git a/nixos/modules/services/databases/influxdb.nix b/nixos/modules/services/databases/influxdb.nix
index d7a028b25d8..888bf13c3df 100644
--- a/nixos/modules/services/databases/influxdb.nix
+++ b/nixos/modules/services/databases/influxdb.nix
@@ -98,6 +98,7 @@ let
 
   configFile = pkgs.runCommand "config.toml" {
     buildInputs = [ pkgs.remarshal ];
+    preferLocalBuild = true;
   } ''
     remarshal -if json -of toml \
       < ${pkgs.writeText "config.json" (builtins.toJSON configOptions)} \
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
index 8e7945cfdb5..467feb09b3a 100644
--- a/nixos/modules/services/databases/mysql.nix
+++ b/nixos/modules/services/databases/mysql.nix
@@ -249,6 +249,7 @@ in
 
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ config.environment.etc."my.cnf".source ];
 
         unitConfig.RequiresMountsFor = "${cfg.dataDir}";
 
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index e996211be7d..5c302752781 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -146,7 +146,7 @@ in
         chown -R "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
       '';
       serviceConfig.ExecStart =
-        "${openldap.out}/libexec/slapd -d ${cfg.logLevel} " +
+        "${openldap.out}/libexec/slapd -d '${cfg.logLevel}' " +
           "-u '${cfg.user}' -g '${cfg.group}' " +
           "-h '${concatStringsSep " " cfg.urlList}' " +
           "${configOpts}";
diff --git a/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix b/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix
new file mode 100644
index 00000000000..dbf0f4e9b11
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix
@@ -0,0 +1,45 @@
+# GNOME Settings Daemon
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.gnome3.gnome-settings-daemon;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.gnome3.gnome-settings-daemon = {
+
+      enable = mkEnableOption "GNOME Settings Daemon.";
+
+      # There are many forks of gnome-settings-daemon
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gnome3.gnome-settings-daemon;
+        description = "Which gnome-settings-daemon package to use.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    services.udev.packages = [ cfg.package ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/pantheon/contractor.nix b/nixos/modules/services/desktops/pantheon/contractor.nix
new file mode 100644
index 00000000000..bd538db7241
--- /dev/null
+++ b/nixos/modules/services/desktops/pantheon/contractor.nix
@@ -0,0 +1,39 @@
+# Contractor
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.pantheon.contractor = {
+
+      enable = mkEnableOption "contractor, a desktop-wide extension service used by pantheon";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.pantheon.contractor.enable {
+
+    environment.systemPackages = with  pkgs.pantheon; [
+      contractor
+      extra-elementary-contracts
+    ];
+
+    services.dbus.packages = [ pkgs.pantheon.contractor ];
+
+    environment.pathsToLink = [
+      "/share/contractor"
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/pantheon/files.nix b/nixos/modules/services/desktops/pantheon/files.nix
new file mode 100644
index 00000000000..2edbe5b3a6d
--- /dev/null
+++ b/nixos/modules/services/desktops/pantheon/files.nix
@@ -0,0 +1,36 @@
+# pantheon files daemon.
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.pantheon.files = {
+
+      enable = mkEnableOption "pantheon files daemon";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.pantheon.files.enable {
+
+    environment.systemPackages = [
+      pkgs.pantheon.elementary-files
+    ];
+
+    services.dbus.packages = [
+      pkgs.pantheon.elementary-files
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/tumbler.nix b/nixos/modules/services/desktops/tumbler.nix
new file mode 100644
index 00000000000..ccbb6d1434d
--- /dev/null
+++ b/nixos/modules/services/desktops/tumbler.nix
@@ -0,0 +1,50 @@
+# Tumbler
+
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tumbler;
+  tumbler = cfg.package;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.tumbler = {
+
+      enable = mkEnableOption "Tumbler, A D-Bus thumbnailer service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xfce4-13.tumbler;
+        description = "Which tumbler package to use";
+        example = pkgs.xfce4-12.tumbler;
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [
+      tumbler
+    ];
+
+    services.dbus.packages = [
+      tumbler
+    ];
+
+  };
+
+}
diff --git a/nixos/modules/services/games/minecraft-server.nix b/nixos/modules/services/games/minecraft-server.nix
index f50d2897843..7d26d150165 100644
--- a/nixos/modules/services/games/minecraft-server.nix
+++ b/nixos/modules/services/games/minecraft-server.nix
@@ -4,8 +4,41 @@ with lib;
 
 let
   cfg = config.services.minecraft-server;
-in
-{
+
+  # We don't allow eula=false anyways
+  eulaFile = builtins.toFile "eula.txt" ''
+    # eula.txt managed by NixOS Configuration
+    eula=true
+  '';
+
+  whitelistFile = pkgs.writeText "whitelist.json"
+    (builtins.toJSON
+      (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist));
+
+  cfgToString = v: if builtins.isBool v then boolToString v else toString v;
+
+  serverPropertiesFile = pkgs.writeText "server.properties" (''
+    # server.properties managed by NixOS configuration
+  '' + concatStringsSep "\n" (mapAttrsToList
+    (n: v: "${n}=${cfgToString v}") cfg.serverProperties));
+
+
+  # To be able to open the firewall, we need to read out port values in the
+  # server properties, but fall back to the defaults when those don't exist.
+  # These defaults are from https://minecraft.gamepedia.com/Server.properties#Java_Edition_3
+  defaultServerPort = 25565;
+
+  serverPort = cfg.serverProperties.server-port or defaultServerPort;
+
+  rconPort = if cfg.serverProperties.enable-rcon or false
+    then cfg.serverProperties."rcon.port" or 25575
+    else null;
+
+  queryPort = if cfg.serverProperties.enable-query or false
+    then cfg.serverProperties."query.port" or 25565
+    else null;
+
+in {
   options = {
     services.minecraft-server = {
 
@@ -13,10 +46,32 @@ in
         type = types.bool;
         default = false;
         description = ''
-          If enabled, start a Minecraft Server. The listening port for
-          the server is always <literal>25565</literal>. The server
+          If enabled, start a Minecraft Server. The server
           data will be loaded from and saved to
-          <literal>${cfg.dataDir}</literal>.
+          <option>services.minecraft-server.dataDir</option>.
+        '';
+      };
+
+      declarative = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use a declarative Minecraft server configuration.
+          Only if set to <literal>true</literal>, the options
+          <option>services.minecraft-server.whitelist</option> and
+          <option>services.minecraft-server.serverProperties</option> will be
+          applied.
+        '';
+      };
+
+      eula = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether you agree to
+          <link xlink:href="https://account.mojang.com/documents/minecraft_eula">
+          Mojangs EULA</link>. This option must be set to
+          <literal>true</literal> to run Minecraft server.
         '';
       };
 
@@ -24,7 +79,7 @@ in
         type = types.path;
         default = "/var/lib/minecraft";
         description = ''
-          Directory to store minecraft database and other state/data files.
+          Directory to store Minecraft database and other state/data files.
         '';
       };
 
@@ -32,21 +87,84 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Whether to open ports in the firewall (if enabled) for the server.
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      whitelist = mkOption {
+        type = let
+          minecraftUUID = types.strMatching
+            "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+              description = "Minecraft UUID";
+            };
+          in types.attrsOf minecraftUUID;
+        default = {};
+        description = ''
+          Whitelisted players, only has an effect when
+          <option>services.minecraft-server.declarative</option> is
+          <literal>true</literal> and the whitelist is enabled
+          via <option>services.minecraft-server.serverProperties</option> by
+          setting <literal>white-list</literal> to <literal>true</literal>.
+          This is a mapping from Minecraft usernames to UUIDs.
+          You can use <link xlink:href="https://mcuuid.net/"/> to get a
+          Minecraft UUID for a username.
+        '';
+        example = literalExample ''
+          {
+            username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+            username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+          };
+        '';
+      };
+
+      serverProperties = mkOption {
+        type = with types; attrsOf (either bool (either int str));
+        default = {};
+        example = literalExample ''
+          {
+            server-port = 43000;
+            difficulty = 3;
+            gamemode = 1;
+            max-players = 5;
+            motd = "NixOS Minecraft server!";
+            white-list = true;
+            enable-rcon = true;
+            "rcon.password" = "hunter2";
+          }
+        '';
+        description = ''
+          Minecraft server properties for the server.properties file. Only has
+          an effect when <option>services.minecraft-server.declarative</option>
+          is set to <literal>true</literal>. See
+          <link xlink:href="https://minecraft.gamepedia.com/Server.properties#Java_Edition_3"/>
+          for documentation on these values.
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.minecraft-server;
+        defaultText = "pkgs.minecraft-server";
+        example = literalExample "pkgs.minecraft-server_1_12_2";
+        description = "Version of minecraft-server to run.";
+      };
+
       jvmOpts = mkOption {
-        type = types.str;
+        type = types.separatedString " ";
         default = "-Xmx2048M -Xms2048M";
-        description = "JVM options for the Minecraft Service.";
+        # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
+        example = "-Xmx2048M -Xms4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+          + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
+          + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
+        description = "JVM options for the Minecraft server.";
       };
     };
   };
 
   config = mkIf cfg.enable {
+
     users.users.minecraft = {
-      description     = "Minecraft Server Service user";
+      description     = "Minecraft server service user";
       home            = cfg.dataDir;
       createHome      = true;
       uid             = config.ids.uids.minecraft;
@@ -57,17 +175,60 @@ in
       wantedBy      = [ "multi-user.target" ];
       after         = [ "network.target" ];
 
-      serviceConfig.Restart = "always";
-      serviceConfig.User    = "minecraft";
-      script = ''
-        cd ${cfg.dataDir}
-        exec ${pkgs.minecraft-server}/bin/minecraft-server ${cfg.jvmOpts}
-      '';
-    };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
+        Restart = "always";
+        User = "minecraft";
+        WorkingDirectory = cfg.dataDir;
+      };
+
+      preStart = ''
+        ln -sf ${eulaFile} eula.txt
+      '' + (if cfg.declarative then ''
+
+        if [ -e .declarative ]; then
+
+          # Was declarative before, no need to back up anything
+          ln -sf ${whitelistFile} whitelist.json
+          cp -f ${serverPropertiesFile} server.properties
+
+        else
 
-    networking.firewall = mkIf cfg.openFirewall {
-      allowedUDPPorts = [ 25565 ];
-      allowedTCPPorts = [ 25565 ];
+          # Declarative for the first time, backup stateful files
+          ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
+          cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
+
+          # server.properties must have write permissions, because every time
+          # the server starts it first parses the file and then regenerates it..
+          chmod +w server.properties
+          echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+            > .declarative
+
+        fi
+      '' else ''
+        if [ -e .declarative ]; then
+          rm .declarative
+        fi
+      '');
     };
+
+    networking.firewall = mkIf cfg.openFirewall (if cfg.declarative then {
+      allowedUDPPorts = [ serverPort ];
+      allowedTCPPorts = [ serverPort ]
+        ++ optional (! isNull queryPort) queryPort
+        ++ optional (! isNull rconPort) rconPort;
+    } else {
+      allowedUDPPorts = [ defaultServerPort ];
+      allowedTCPPorts = [ defaultServerPort ];
+    });
+
+    assertions = [
+      { assertion = cfg.eula;
+        message = "You must agree to Mojangs EULA to run minecraft-server."
+          + " Read https://account.mojang.com/documents/minecraft_eula and"
+          + " set `services.minecraft-server.eula` to `true` if you agree.";
+      }
+    ];
+
   };
 }
diff --git a/nixos/modules/services/hardware/acpid.nix b/nixos/modules/services/hardware/acpid.nix
index 0f05876aee3..4c97485d972 100644
--- a/nixos/modules/services/hardware/acpid.nix
+++ b/nixos/modules/services/hardware/acpid.nix
@@ -21,7 +21,7 @@ let
     };
   };
 
-  acpiConfDir = pkgs.runCommand "acpi-events" {}
+  acpiConfDir = pkgs.runCommand "acpi-events" { preferLocalBuild = true; }
     ''
       mkdir -p $out
       ${
diff --git a/nixos/modules/services/hardware/bolt.nix b/nixos/modules/services/hardware/bolt.nix
new file mode 100644
index 00000000000..32b60af0603
--- /dev/null
+++ b/nixos/modules/services/hardware/bolt.nix
@@ -0,0 +1,34 @@
+# Thunderbolt 3 device manager
+
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+{
+  options = {
+
+    services.hardware.bolt = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bolt, a userspace daemon to enable
+          security levels for Thunderbolt 3 on GNU/Linux.
+
+          Bolt is used by GNOME 3 to handle Thunderbolt settings.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf config.services.hardware.bolt.enable {
+
+    environment.systemPackages = [ pkgs.bolt ];
+    services.udev.packages = [ pkgs.bolt ];
+    systemd.packages = [ pkgs.bolt ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 7743f81fd62..cad9fa20de0 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -15,6 +15,19 @@ let
       mkName = p: "pki/fwupd/${baseNameOf (toString p)}";
       mkEtcFile = p: nameValuePair (mkName p) { source = p; };
     in listToAttrs (map mkEtcFile cfg.extraTrustedKeys);
+
+  # We cannot include the file in $out and rely on filesInstalledToEtc
+  # to install it because it would create a cyclic dependency between
+  # the outputs. We also need to enable the remote,
+  # which should not be done by default.
+  testRemote = if cfg.enableTestRemote then {
+    "fwupd/remotes.d/fwupd-tests.conf" = {
+      source = pkgs.runCommand "fwupd-tests-enabled.conf" {} ''
+        sed "s,^Enabled=false,Enabled=true," \
+        "${pkgs.fwupd.installedTests}/etc/fwupd/remotes.d/fwupd-tests.conf" > "$out"
+      '';
+    };
+  } else {};
 in {
 
   ###### interface
@@ -40,7 +53,7 @@ in {
 
       blacklistPlugins = mkOption {
         type = types.listOf types.string;
-        default = [];
+        default = [ "test" ];
         example = [ "udev" ];
         description = ''
           Allow blacklisting specific plugins
@@ -55,6 +68,15 @@ in {
           Installing a public key allows firmware signed with a matching private key to be recognized as trusted, which may require less authentication to install than for untrusted files. By default trusted firmware can be upgraded (but not downgraded) without the user or administrator password. Only very few keys are installed by default.
         '';
       };
+
+      enableTestRemote = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable test remote. This is used by
+          <link xlink:href="https://github.com/hughsie/fwupd/blob/master/data/installed-tests/README.md">installed tests</link>.
+        '';
+      };
     };
   };
 
@@ -78,7 +100,7 @@ in {
         '';
       };
 
-    } // originalEtc // extraTrustedKeys;
+    } // originalEtc // extraTrustedKeys // testRemote;
 
     services.dbus.packages = [ pkgs.fwupd ];
 
diff --git a/nixos/modules/services/hardware/sane_extra_backends/dsseries.nix b/nixos/modules/services/hardware/sane_extra_backends/dsseries.nix
new file mode 100644
index 00000000000..d71a17f5ea6
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/dsseries.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    hardware.sane.dsseries.enable =
+      mkEnableOption "Brother DSSeries scan backend" // {
+      description = ''
+        When enabled, will automatically register the "dsseries" SANE backend.
+
+        This supports the Brother DSmobile scanner series, including the
+        DS-620, DS-720D, DS-820W, and DS-920DW scanners.
+      '';
+    };
+  };
+
+  config = mkIf (config.hardware.sane.enable && config.hardware.sane.dsseries.enable) {
+
+    hardware.sane.extraBackends = [ pkgs.dsseries ];
+    services.udev.packages = [ pkgs.dsseries ];
+    boot.kernelModules = [ "sg" ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
index d17121ca1c5..7c105e99ca5 100644
--- a/nixos/modules/services/hardware/thinkfan.nix
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -47,6 +47,8 @@ let
     ${cfg.levels}
   '';
 
+  thinkfan = pkgs.thinkfan.override { smartSupport = cfg.smartSupport; };
+
 in {
 
   options = {
@@ -61,6 +63,15 @@ in {
         '';
       };
 
+      smartSupport = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to build thinkfan with SMART support to read temperatures 
+          directly from hard disks.
+        '';
+      };
+
       sensors = mkOption {
         type = types.lines;
         default = ''
@@ -77,7 +88,7 @@ in {
               Which may be provided by any hwmon drivers (keyword
               hwmon)
 
-            S.M.A.R.T. (since 0.9 and requires the USE_ATASMART compilation flag)
+            S.M.A.R.T. (requires smartSupport to be enabled)
               Which reads the temperature directly from the hard
               disk using libatasmart (keyword atasmart)
 
@@ -125,18 +136,17 @@ in {
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.thinkfan ];
+    environment.systemPackages = [ thinkfan ];
 
     systemd.services.thinkfan = {
       description = "Thinkfan";
       after = [ "basic.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.thinkfan ];
-      serviceConfig.ExecStart = "${pkgs.thinkfan}/bin/thinkfan -n -c ${configFile}";
+      path = [ thinkfan ];
+      serviceConfig.ExecStart = "${thinkfan}/bin/thinkfan -n -c ${configFile}";
     };
 
     boot.extraModprobeConfig = "options thinkpad_acpi experimental=1 fan_control=1";
 
   };
-
 }
diff --git a/nixos/modules/services/hardware/tlp.nix b/nixos/modules/services/hardware/tlp.nix
index b894025c0fd..092ff051a04 100644
--- a/nixos/modules/services/hardware/tlp.nix
+++ b/nixos/modules/services/hardware/tlp.nix
@@ -17,6 +17,7 @@ tlp = pkgs.tlp.override {
 confFile = pkgs.runCommand "tlp"
   { config = cfg.extraConfig;
     passAsFile = [ "config" ];
+    preferLocalBuild = true;
   }
   ''
     cat ${tlp}/etc/default/tlp > $out
diff --git a/nixos/modules/services/logging/logcheck.nix b/nixos/modules/services/logging/logcheck.nix
index 1477d273d5e..9c64160e92b 100644
--- a/nixos/modules/services/logging/logcheck.nix
+++ b/nixos/modules/services/logging/logcheck.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.logcheck;
 
-  defaultRules = pkgs.runCommand "logcheck-default-rules" {} ''
+  defaultRules = pkgs.runCommand "logcheck-default-rules" { preferLocalBuild = true; } ''
                    cp -prd ${pkgs.logcheck}/etc/logcheck $out
                    chmod u+w $out
                    rm -r $out/logcheck.*
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index aa019d855ea..9b707e9deb5 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -27,7 +27,10 @@ let
 
   logstashSettingsYml = pkgs.writeText "logstash.yml" cfg.extraSettings;
 
-  logstashSettingsDir = pkgs.runCommand "logstash-settings" {inherit logstashSettingsYml;} ''
+  logstashSettingsDir = pkgs.runCommand "logstash-settings" {
+      inherit logstashSettingsYml;
+      preferLocalBuild = true;
+    } ''
     mkdir -p $out
     ln -s $logstashSettingsYml $out/logstash.yml
   '';
diff --git a/nixos/modules/services/mail/davmail.nix b/nixos/modules/services/mail/davmail.nix
new file mode 100644
index 00000000000..a0cb81f84da
--- /dev/null
+++ b/nixos/modules/services/mail/davmail.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.davmail;
+
+  configType = with types;
+    either (either (attrsOf configType) str) (either int bool) // {
+      description = "davmail config type (str, int, bool or attribute set thereof)";
+    };
+
+  toStr = val: if isBool val then boolToString val else toString val;
+
+  linesForAttrs = attrs: concatMap (name: let value = attrs.${name}; in
+    if isAttrs value
+      then map (line: name + "." + line) (linesForAttrs value)
+      else [ "${name}=${toStr value}" ]
+  ) (attrNames attrs);
+
+  configFile = pkgs.writeText "davmail.properties" (concatStringsSep "\n" (linesForAttrs cfg.config));
+
+in
+
+  {
+    options.services.davmail = {
+      enable = mkEnableOption "davmail, an MS Exchange gateway";
+
+      url = mkOption {
+        type = types.str;
+        description = "Outlook Web Access URL to access the exchange server, i.e. the base webmail URL.";
+        example = "https://outlook.office365.com/EWS/Exchange.asmx";
+      };
+
+      config = mkOption {
+        type = configType;
+        default = {};
+        description = ''
+          Davmail configuration. Refer to
+          <link xlink:href="http://davmail.sourceforge.net/serversetup.html"/>
+          and <link xlink:href="http://davmail.sourceforge.net/advanced.html"/>
+          for details on supported values.
+        '';
+        example = literalExample ''
+          {
+            davmail.allowRemote = true;
+            davmail.imapPort = 55555;
+            davmail.bindAddress = "10.0.1.2";
+            davmail.smtpSaveInSent = true;
+            davmail.folderSizeLimit = 10;
+            davmail.caldavAutoSchedule = false;
+            log4j.logger.rootLogger = "DEBUG";
+          }
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+
+      services.davmail.config.davmail = mapAttrs (name: mkDefault) {
+        server = true;
+        disableUpdateCheck = true;
+        logFilePath = "/var/log/davmail/davmail.log";
+        logFileSize = "1MB";
+        mode = "auto";
+        url = cfg.url;
+        caldavPort = 1080;
+        imapPort = 1143;
+        ldapPort = 1389;
+        popPort = 1110;
+        smtpPort = 1025;
+      };
+
+      systemd.services.davmail = {
+        description = "DavMail POP/IMAP/SMTP Exchange Gateway";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${pkgs.davmail}/bin/davmail ${configFile}";
+          Restart = "on-failure";
+          DynamicUser = "yes";
+          LogsDirectory = "davmail";
+        };
+      };
+
+      environment.systemPackages = [ pkgs.davmail ];
+    };
+  }
diff --git a/nixos/modules/services/mail/opensmtpd.nix b/nixos/modules/services/mail/opensmtpd.nix
index 4276552d4f0..a870550ba50 100644
--- a/nixos/modules/services/mail/opensmtpd.nix
+++ b/nixos/modules/services/mail/opensmtpd.nix
@@ -8,7 +8,7 @@ let
   conf = pkgs.writeText "smtpd.conf" cfg.serverConfiguration;
   args = concatStringsSep " " cfg.extraServerArgs;
 
-  sendmail = pkgs.runCommand "opensmtpd-sendmail" {} ''
+  sendmail = pkgs.runCommand "opensmtpd-sendmail" { preferLocalBuild = true; } ''
     mkdir -p $out/bin
     ln -s ${cfg.package}/sbin/smtpctl $out/bin/sendmail
   '';
diff --git a/nixos/modules/services/mail/rmilter.nix b/nixos/modules/services/mail/rmilter.nix
index 492c6458321..466365b6b30 100644
--- a/nixos/modules/services/mail/rmilter.nix
+++ b/nixos/modules/services/mail/rmilter.nix
@@ -8,7 +8,7 @@ let
   postfixCfg = config.services.postfix;
   cfg = config.services.rmilter;
 
-  inetSocket = addr: port: "inet:[${toString port}@${addr}]";
+  inetSocket = addr: port: "inet:${addr}:${toString port}";
   unixSocket = sock: "unix:${sock}";
 
   systemdSocket = if cfg.bindSocket.type == "unix" then cfg.bindSocket.path
@@ -97,7 +97,7 @@ in
 
       bindSocket.address = mkOption {
         type = types.str;
-        default = "::1";
+        default = "[::1]";
         example = "0.0.0.0";
         description = ''
           Inet address to listen on.
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index 6d81c7374f4..66b1c1e3e6f 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -25,6 +25,20 @@ in
       description = "Hostname to use for the nginx vhost";
     };
 
+    package = mkOption {
+      type = types.package;
+      default = pkgs.roundcube;
+
+      example = literalExample ''
+        roundcube.withPlugins (plugins: [ plugins.persistent_login ])
+      '';
+
+      description = ''
+        The package which contains roundcube's sources. Can be overriden to create
+        an environment which contains roundcube and third-party plugins.
+      '';
+    };
+
     database = {
       username = mkOption {
         type = types.str;
@@ -86,7 +100,7 @@ in
           forceSSL = mkDefault true;
           enableACME = mkDefault true;
           locations."/" = {
-            root = pkgs.roundcube;
+            root = cfg.package;
             index = "index.php";
             extraConfig = ''
               location ~* \.php$ {
@@ -140,12 +154,12 @@ in
             ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create database ${cfg.database.dbname} with owner ${cfg.database.username}";
           fi
           PGPASSWORD=${cfg.database.password} ${pkgs.postgresql}/bin/psql -U ${cfg.database.username} \
-            -f ${pkgs.roundcube}/SQL/postgres.initial.sql \
+            -f ${cfg.package}/SQL/postgres.initial.sql \
             -h ${cfg.database.host} ${cfg.database.dbname}
           touch /var/lib/roundcube/db-created
         fi
 
-        ${pkgs.php}/bin/php ${pkgs.roundcube}/bin/update.sh
+        ${pkgs.php}/bin/php ${cfg.package}/bin/update.sh
       '';
       serviceConfig.Type = "oneshot";
     };
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 01d7b3cf6b9..8b2ec82c770 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -25,6 +25,14 @@ in {
         '';
       };
 
+      virtualHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
+        '';
+      };
+
       listenAddress = mkOption {
         type = types.string;
         default = "127.0.0.1";
@@ -116,6 +124,8 @@ in {
           -Dserver.port=${toString cfg.port} \
           -Dairsonic.contextPath=${cfg.contextPath} \
           -Djava.awt.headless=true \
+          ${optionalString (cfg.virtualHost != null)
+            "-Dserver.use-forward-headers=true"} \
           ${toString cfg.jvmOptions} \
           -verbose:gc \
           -jar ${pkgs.airsonic}/webapps/airsonic.war
@@ -126,6 +136,13 @@ in {
       };
     };
 
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      virtualHosts."${cfg.virtualHost}" = {
+        locations."${cfg.contextPath}".proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
+      };
+    };
+
     users.users.airsonic = {
       description = "Airsonic service user";
       name = cfg.user;
diff --git a/nixos/modules/services/misc/beanstalkd.nix b/nixos/modules/services/misc/beanstalkd.nix
new file mode 100644
index 00000000000..8a3e0ab1949
--- /dev/null
+++ b/nixos/modules/services/misc/beanstalkd.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.beanstalkd;
+  pkg = pkgs.beanstalkd;
+in
+
+{
+  # interface
+
+  options = {
+    services.beanstalkd = {
+      enable = mkEnableOption "Enable the Beanstalk work queue.";
+
+      listen = {
+        port = mkOption {
+          type = types.int;
+          description = "TCP port that will be used to accept client connections.";
+          default = 11300;
+        };
+
+        address = mkOption {
+          type = types.str;
+          description = "IP address to listen on.";
+          default = "127.0.0.1";
+          example = "0.0.0.0";
+        };
+      };
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkg ];
+
+    systemd.services.beanstalkd = {
+      description = "Beanstalk Work Queue";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        ExecStart = "${pkg}/bin/beanstalkd -l ${cfg.listen.address} -p ${toString cfg.listen.port}";
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/misc/bepasty.nix b/nixos/modules/services/misc/bepasty.nix
index 62835c194e4..006feca42b3 100644
--- a/nixos/modules/services/misc/bepasty.nix
+++ b/nixos/modules/services/misc/bepasty.nix
@@ -2,10 +2,10 @@
 
 with lib;
 let
-  gunicorn = pkgs.pythonPackages.gunicorn;
+  gunicorn = pkgs.python3Packages.gunicorn;
   bepasty = pkgs.bepasty;
-  gevent = pkgs.pythonPackages.gevent;
-  python = pkgs.pythonPackages.python;
+  gevent = pkgs.python3Packages.gevent;
+  python = pkgs.python3Packages.python;
   cfg = config.services.bepasty;
   user = "bepasty";
   group = "bepasty";
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index 9a3966ab30a..f3d90e532c8 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -18,7 +18,7 @@ let
       delete.enabled = cfg.enableDelete;
     };
     http = {
-      addr = ":${builtins.toString cfg.port}";
+      addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
       headers.X-Content-Type-Options = ["nosniff"];
     };
     health.storagedriver = {
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 769a9526cf6..baa1c855c11 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -22,7 +22,8 @@ let
       password = cfg.databasePassword;
       username = cfg.databaseUsername;
       encoding = "utf8";
-    };
+      pool = cfg.databasePool;
+    } // cfg.extraDatabaseConfig;
   };
 
   gitalyToml = pkgs.writeText "gitaly.toml" ''
@@ -253,6 +254,18 @@ in {
         description = "Gitlab database user.";
       };
 
+      databasePool = mkOption {
+        type = types.int;
+        default = 5;
+        description = "Database connection pool size.";
+      };
+
+      extraDatabaseConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        description = "Extra configuration in config/database.yml.";
+      };
+
       host = mkOption {
         type = types.str;
         default = config.networking.hostName;
@@ -497,7 +510,14 @@ in {
     systemd.services.gitaly = {
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ gitAndTools.git cfg.packages.gitaly.rubyEnv cfg.packages.gitaly.rubyEnv.wrappedRuby ];
+      path = with pkgs; [
+        openssh
+        gitAndTools.git
+        cfg.packages.gitaly.rubyEnv
+        cfg.packages.gitaly.rubyEnv.wrappedRuby
+        gzip
+        bzip2
+      ];
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
diff --git a/nixos/modules/services/misc/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
index b9c2a966e6f..c7f2a168f8a 100644
--- a/nixos/modules/services/misc/gitolite.nix
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -110,7 +110,7 @@ in
   config = mkIf cfg.enable (
   let
     manageGitoliteRc = cfg.extraGitoliteRc != "";
-    rcDir = pkgs.runCommand "gitolite-rc" { } rcDirScript;
+    rcDir = pkgs.runCommand "gitolite-rc" { preferLocalBuild = true; } rcDirScript;
     rcDirScript =
       ''
         mkdir "$out"
diff --git a/nixos/modules/services/misc/headphones.nix b/nixos/modules/services/misc/headphones.nix
new file mode 100644
index 00000000000..4a77045be28
--- /dev/null
+++ b/nixos/modules/services/misc/headphones.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  name = "headphones";
+
+  cfg = config.services.headphones;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.headphones = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the headphones server.";
+      };
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/${name}";
+        description = "Path where to store data files.";
+      };
+      configFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/config.ini";
+        description = "Path to config file.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Host to listen on.";
+      };
+      port = mkOption {
+        type = types.ints.u16;
+        default = 8181;
+        description = "Port to bind to.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = name;
+        description = "User to run the service as";
+      };
+      group = mkOption {
+        type = types.str;
+        default = name;
+        description = "Group to run the service as";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == name) (singleton {
+      name = name;
+      uid = config.ids.uids.headphones;
+      group = cfg.group;
+      description = "headphones user";
+      home = cfg.dataDir;
+      createHome = true;
+    });
+
+    users.groups = optionalAttrs (cfg.group == name) (singleton {
+      name = name;
+      gid = config.ids.gids.headphones;
+    });
+
+    systemd.services.headphones = {
+        description = "Headphones Server";
+        wantedBy    = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+          ExecStart = "${pkgs.headphones}/bin/headphones --datadir ${cfg.dataDir} --config ${cfg.configFile} --host ${cfg.host} --port ${toString cfg.port}";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 2e9aa33aeee..7f8d31bcf0b 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -6,11 +6,20 @@ let
   cfg = config.services.home-assistant;
 
   # cfg.config != null can be assumed here
-  configFile = pkgs.writeText "configuration.json"
+  configJSON = pkgs.writeText "configuration.json"
     (builtins.toJSON (if cfg.applyDefaultConfig then
-    (lib.recursiveUpdate defaultConfig cfg.config) else cfg.config));
+    (recursiveUpdate defaultConfig cfg.config) else cfg.config));
+  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
+    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
+  '';
 
-  availableComponents = pkgs.home-assistant.availableComponents;
+  lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
+    (builtins.toJSON cfg.lovelaceConfig);
+  lovelaceConfigFile = pkgs.runCommand "ui-lovelace.yaml" { preferLocalBuild = true; } ''
+    ${pkgs.remarshal}/bin/json2yaml -i ${lovelaceConfigJSON} -o $out
+  '';
+
+  availableComponents = cfg.package.availableComponents;
 
   # Given component "parentConfig.platform", returns whether config.parentConfig
   # is a list containing a set with set.platform == "platform".
@@ -20,14 +29,24 @@ let
   #   platform = "luftdaten";
   #   ...
   # } ];
+  #
+  # Beginning with 0.87 Home Assistant is migrating their components to the
+  # scheme "platform.subComponent", e.g. "hue.light" instead of "light.hue".
+  # See https://developers.home-assistant.io/blog/2019/02/19/the-great-migration.html.
+  # Hence, we also check whether we find an entry in the config when interpreting
+  # the first part of the path as the component.
   useComponentPlatform = component:
     let
       path = splitString "." component;
+      # old: platform is the last part of path
       parentConfig = attrByPath (init path) null cfg.config;
       platform = last path;
-    in isList parentConfig && any
-      (item: item.platform or null == platform)
-      parentConfig;
+      # new: platform is the first part of the path
+      parentConfig' = attrByPath (tail path) null cfg.config;
+      platform' = head path;
+    in
+      (isList parentConfig && any (item: item.platform or null == platform) parentConfig)
+      || (isList parentConfig' && any (item: item.platform or null == platform') parentConfig');
 
   # Returns whether component is used in config
   useComponent = component:
@@ -44,7 +63,9 @@ let
   # If you are changing this, please update the description in applyDefaultConfig
   defaultConfig = {
     homeassistant.time_zone = config.time.timeZone;
-    http.server_port = (toString cfg.port);
+    http.server_port = cfg.port;
+  } // optionalAttrs (cfg.lovelaceConfig != null) {
+    lovelace.mode = "yaml";
   };
 
 in {
@@ -99,6 +120,53 @@ in {
       '';
     };
 
+    configWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>configuration.yaml</filename> writable.
+        This only has an effect if <option>config</option> is set.
+        This will allow you to edit it from Home Assistant's web interface.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
+    lovelaceConfig = mkOption {
+      default = null;
+      type = with types; nullOr attrs;
+      # from https://www.home-assistant.io/lovelace/yaml-mode/
+      example = literalExample ''
+        {
+          title = "My Awesome Home";
+          views = [ {
+            title = "Example";
+            cards = [ {
+              type = "markdown";
+              title = "Lovelace";
+              content = "Welcome to your **Lovelace UI**.";
+            } ];
+          } ];
+        }
+      '';
+      description = ''
+        Your <filename>ui-lovelace.yaml</filename> as a Nix attribute set.
+        Setting this option will automatically add
+        <literal>lovelace.mode = "yaml";</literal> to your <option>config</option>.
+        Beware that setting this option will delete your previous <filename>ui-lovelace.yaml</filename>
+      '';
+    };
+
+    lovelaceConfigWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>ui-lovelace.yaml</filename> writable.
+        This only has an effect if <option>lovelaceConfig</option> is set.
+        This will allow you to edit it from Home Assistant's web interface.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
     package = mkOption {
       default = pkgs.home-assistant;
       defaultText = "pkgs.home-assistant";
@@ -144,12 +212,17 @@ in {
     systemd.services.home-assistant = {
       description = "Home Assistant";
       after = [ "network.target" ];
-      preStart = lib.optionalString (cfg.config != null) ''
-        config=${cfg.configDir}/configuration.yaml
-        rm -f $config
-        ${pkgs.remarshal}/bin/json2yaml -i ${configFile} -o $config
-        chmod 444 $config
-      '';
+      preStart = optionalString (cfg.config != null) (if cfg.configWritable then ''
+        cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
+      '' else ''
+        rm -f "${cfg.configDir}/configuration.yaml"
+        ln -s ${configFile} "${cfg.configDir}/configuration.yaml"
+      '') + optionalString (cfg.lovelaceConfig != null) (if cfg.lovelaceConfigWritable then ''
+        cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+      '' else ''
+        rm -f "${cfg.configDir}/ui-lovelace.yaml"
+        ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+      '');
       serviceConfig = {
         ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
         User = "hass";
diff --git a/nixos/modules/services/misc/jackett.nix b/nixos/modules/services/misc/jackett.nix
index 8d1b3d225a4..b18ce2b1f81 100644
--- a/nixos/modules/services/misc/jackett.nix
+++ b/nixos/modules/services/misc/jackett.nix
@@ -4,11 +4,36 @@ with lib;
 
 let
   cfg = config.services.jackett;
+
 in
 {
   options = {
     services.jackett = {
       enable = mkEnableOption "Jackett";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/jackett/.config/Jackett";
+        description = "The directory where Jackett stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Jackett web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "jackett";
+        description = "User account under which Jackett runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "jackett";
+        description = "Group under which Jackett runs.";
+      };
     };
   };
 
@@ -18,30 +43,38 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        test -d /var/lib/jackett/ || {
-          echo "Creating jackett data directory in /var/lib/jackett/"
-          mkdir -p /var/lib/jackett/
+        test -d ${cfg.dataDir} || {
+          echo "Creating jackett data directory in ${cfg.dataDir}"
+          mkdir -p ${cfg.dataDir}
         }
-        chown -R jackett:jackett /var/lib/jackett/
-        chmod 0700 /var/lib/jackett/
+        chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir}
+        chmod 0700 ${cfg.dataDir}
       '';
 
       serviceConfig = {
         Type = "simple";
-        User = "jackett";
-        Group = "jackett";
+        User = cfg.user;
+        Group = cfg.group;
         PermissionsStartOnly = "true";
-        ExecStart = "${pkgs.jackett}/bin/Jackett";
+        ExecStart = "${pkgs.jackett}/bin/Jackett --NoUpdates --DataFolder '${cfg.dataDir}'";
         Restart = "on-failure";
       };
     };
 
-    users.users.jackett = {
-      uid = config.ids.uids.jackett;
-      home = "/var/lib/jackett";
-      group = "jackett";
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 9117 ];
+    };
+
+    users.users = mkIf (cfg.user == "jackett") {
+      jackett = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.jackett;
+      };
     };
-    users.groups.jackett.gid = config.ids.gids.jackett;
 
+    users.groups = mkIf (cfg.group == "jackett") {
+      jackett.gid = config.ids.gids.jackett;
+    };
   };
 }
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 18e13f6ac03..87999c3614f 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -691,6 +691,7 @@ in {
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
             --keys-directory ${cfg.dataDir}
         '';
+        ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 9a8116a03e8..665215822af 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -33,7 +33,7 @@ let
       sh = pkgs.runtimeShell;
       binshDeps = pkgs.writeReferencesToFile sh;
     in
-      pkgs.runCommand "nix.conf" { extraOptions = cfg.extraOptions; } (''
+      pkgs.runCommand "nix.conf" { preferLocalBuild = true; extraOptions = cfg.extraOptions; } (''
         ${optionalString (!isNix20) ''
           extraPaths=$(for i in $(cat ${binshDeps}); do if test -d $i; then echo $i; fi; done)
         ''}
diff --git a/nixos/modules/services/misc/nzbget.nix b/nixos/modules/services/misc/nzbget.nix
index e24cecf2080..6ab98751c57 100644
--- a/nixos/modules/services/misc/nzbget.nix
+++ b/nixos/modules/services/misc/nzbget.nix
@@ -4,6 +4,7 @@ with lib;
 
 let
   cfg = config.services.nzbget;
+  dataDir = builtins.dirOf cfg.configFile;
 in {
   options = {
     services.nzbget = {
@@ -41,6 +42,12 @@ in {
         default = "nzbget";
         description = "Group under which NZBGet runs";
       };
+
+      configFile = mkOption {
+        type = types.str;
+        default = "/var/lib/nzbget/nzbget.conf";
+        description = "Path for NZBGet's config file. (If this doesn't exist, the default config template is copied here.)";
+      };
     };
   };
 
@@ -54,36 +61,25 @@ in {
         p7zip
       ];
       preStart = ''
-        datadir=${cfg.dataDir}
-        configfile=${cfg.dataDir}/nzbget.conf
         cfgtemplate=${cfg.package}/share/nzbget/nzbget.conf
-        test -d $datadir || {
-          echo "Creating nzbget data directory in $datadir"
-          mkdir -p $datadir
-        }
-        test -f $configfile || {
-          echo "nzbget.conf not found. Copying default config $cfgtemplate to $configfile"
-          cp $cfgtemplate $configfile
-          echo "Setting $configfile permissions to 0700 (needs to be written and contains plaintext credentials)"
-          chmod 0700 $configfile
+        if [ ! -f ${cfg.configFile} ]; then
+          echo "${cfg.configFile} not found. Copying default config $cfgtemplate to ${cfg.configFile}"
+          install -m 0700 $cfgtemplate ${cfg.configFile}
           echo "Setting temporary \$MAINDIR variable in default config required in order to allow nzbget to complete initial start"
           echo "Remember to change this to a proper value once NZBGet startup has been completed"
-          sed -i -e 's/MainDir=.*/MainDir=\/tmp/g' $configfile
-        }
-        echo "Ensuring proper ownership of $datadir (${cfg.user}:${cfg.group})."
-        chown -R ${cfg.user}:${cfg.group} $datadir
+          sed -i -e 's/MainDir=.*/MainDir=\/tmp/g' ${cfg.configFile}
+        fi
       '';
 
       script = ''
-        configfile=${cfg.dataDir}/nzbget.conf
-        args="--daemon --configfile $configfile"
-        # The script in preStart (above) copies nzbget's config template to datadir on first run, containing paths that point to the nzbget derivation installed at the time. 
-        # These paths break when nzbget is upgraded & the original derivation is garbage collected. If such broken paths are found in the config file, override them to point to 
+        args="--daemon --configfile ${cfg.configFile}"
+        # The script in preStart (above) copies nzbget's config template to datadir on first run, containing paths that point to the nzbget derivation installed at the time.
+        # These paths break when nzbget is upgraded & the original derivation is garbage collected. If such broken paths are found in the config file, override them to point to
         # the currently installed nzbget derivation.
         cfgfallback () {
-          local hit=`grep -Po "(?<=^$1=).*+" "$configfile" | sed 's/[ \t]*$//'` # Strip trailing whitespace
+          local hit=`grep -Po "(?<=^$1=).*+" "${cfg.configFile}" | sed 's/[ \t]*$//'` # Strip trailing whitespace
           ( test $hit && test -e $hit ) || {
-            echo "In $configfile, valid $1 not found; falling back to $1=$2"
+            echo "In ${cfg.configFile}, valid $1 not found; falling back to $1=$2"
             args+=" -o $1=$2"
           }
         }
@@ -93,6 +89,8 @@ in {
       '';
 
       serviceConfig = {
+        StateDirectory = dataDir;
+        StateDirectoryMode = "0700";
         Type = "forking";
         User = cfg.user;
         Group = cfg.group;
diff --git a/nixos/modules/services/misc/plex.nix b/nixos/modules/services/misc/plex.nix
index e4810ce9f87..b06c1c4bbc6 100644
--- a/nixos/modules/services/misc/plex.nix
+++ b/nixos/modules/services/misc/plex.nix
@@ -145,6 +145,7 @@ in
         PLEX_MEDIA_SERVER_HOME="${cfg.package}/usr/lib/plexmediaserver";
         PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6";
         PLEX_MEDIA_SERVER_TMPDIR="/tmp";
+        PLEX_MEDIA_SERVER_USE_SYSLOG="true";
         LD_LIBRARY_PATH="/run/opengl-driver/lib:${cfg.package}/usr/lib/plexmediaserver";
         LC_ALL="en_US.UTF-8";
         LANG="en_US.UTF-8";
diff --git a/nixos/modules/services/misc/radarr.nix b/nixos/modules/services/misc/radarr.nix
index 1a9fad3883c..9ab26d84832 100644
--- a/nixos/modules/services/misc/radarr.nix
+++ b/nixos/modules/services/misc/radarr.nix
@@ -4,11 +4,36 @@ with lib;
 
 let
   cfg = config.services.radarr;
+
 in
 {
   options = {
     services.radarr = {
       enable = mkEnableOption "Radarr";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/radarr/.config/Radarr";
+        description = "The directory where Radarr stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Radarr web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "radarr";
+        description = "User account under which Radarr runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "radarr";
+        description = "Group under which Radarr runs.";
+      };
     };
   };
 
@@ -18,30 +43,38 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        test -d /var/lib/radarr/ || {
-          echo "Creating radarr data directory in /var/lib/radarr/"
-          mkdir -p /var/lib/radarr/
+        test -d ${cfg.dataDir} || {
+          echo "Creating radarr data directory in ${cfg.dataDir}"
+          mkdir -p ${cfg.dataDir}
         }
-        chown -R radarr:radarr /var/lib/radarr/
-        chmod 0700 /var/lib/radarr/
+        chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir}
+        chmod 0700 ${cfg.dataDir}
       '';
 
       serviceConfig = {
         Type = "simple";
-        User = "radarr";
-        Group = "radarr";
+        User = cfg.user;
+        Group = cfg.group;
         PermissionsStartOnly = "true";
-        ExecStart = "${pkgs.radarr}/bin/Radarr";
+        ExecStart = "${pkgs.radarr}/bin/Radarr -nobrowser -data='${cfg.dataDir}'";
         Restart = "on-failure";
       };
     };
 
-    users.users.radarr = {
-      uid = config.ids.uids.radarr;
-      home = "/var/lib/radarr";
-      group = "radarr";
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 7878 ];
+    };
+
+    users.users = mkIf (cfg.user == "radarr") {
+      radarr = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        uid = config.ids.uids.radarr;
+      };
     };
-    users.groups.radarr.gid = config.ids.gids.radarr;
 
+    users.groups = mkIf (cfg.group == "radarr") {
+      radarr.gid = config.ids.gids.radarr;
+    };
   };
 }
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 8d25ac5cb76..91ddf2c3edf 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -30,6 +30,13 @@ let
     ${cfg.extraConfig}
   '';
 
+  additionalEnvironment = pkgs.writeText "additional_environment.rb" ''
+    config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
+    config.logger.level = Logger::INFO
+
+    ${cfg.extraEnv}
+  '';
+
   unpackTheme = unpack "theme";
   unpackPlugin = unpack "plugin";
   unpack = id: (name: source:
@@ -54,12 +61,20 @@ in
         description = "Enable the Redmine service.";
       };
 
+      # default to the 4.x series not forcing major version upgrade of those on the 3.x series
       package = mkOption {
         type = types.package;
-        default = pkgs.redmine;
+        default = if versionAtLeast config.system.stateVersion "19.03"
+          then pkgs.redmine_4
+          else pkgs.redmine
+        ;
         defaultText = "pkgs.redmine";
-        description = "Which Redmine package to use.";
-        example = "pkgs.redmine.override { ruby = pkgs.ruby_2_3; }";
+        description = ''
+          Which Redmine package to use. This defaults to version 3.x if
+          <literal>system.stateVersion &lt; 19.03</literal> and version 4.x
+          otherwise.
+        '';
+        example = "pkgs.redmine_4.override { ruby = pkgs.ruby_2_4; }";
       };
 
       user = mkOption {
@@ -103,6 +118,19 @@ in
         '';
       };
 
+      extraEnv = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration in additional_environment.rb.
+
+          See https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example
+        '';
+        example = literalExample ''
+          config.logger.level = Logger::DEBUG
+        '';
+      };
+
       themes = mkOption {
         type = types.attrsOf types.path;
         default = {};
@@ -206,16 +234,39 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
+    # create symlinks for the basic directory layout the redmine package expects
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/config' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/files' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/plugins' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public/plugin_assets' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/public/themes' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -"
+
+      "d /run/redmine - - - - -"
+      "d /run/redmine/public - - - - -"
+      "L+ /run/redmine/config - - - - ${cfg.stateDir}/config"
+      "L+ /run/redmine/files - - - - ${cfg.stateDir}/files"
+      "L+ /run/redmine/log - - - - ${cfg.stateDir}/log"
+      "L+ /run/redmine/plugins - - - - ${cfg.stateDir}/plugins"
+      "L+ /run/redmine/public/plugin_assets - - - - ${cfg.stateDir}/public/plugin_assets"
+      "L+ /run/redmine/public/themes - - - - ${cfg.stateDir}/public/themes"
+      "L+ /run/redmine/tmp - - - - ${cfg.stateDir}/tmp"
+    ];
+
     systemd.services.redmine = {
       after = [ "network.target" (if cfg.database.type == "mysql2" then "mysql.service" else "postgresql.service") ];
       wantedBy = [ "multi-user.target" ];
-      environment.HOME = "${cfg.package}/share/redmine";
       environment.RAILS_ENV = "production";
       environment.RAILS_CACHE = "${cfg.stateDir}/cache";
       environment.REDMINE_LANG = "en";
       environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
       path = with pkgs; [
-        imagemagickBig
+        imagemagick
         bazaar
         cvs
         darcs
@@ -224,34 +275,24 @@ in
         subversion
       ];
       preStart = ''
-        # ensure cache directory exists for db:migrate command
-        mkdir -p "${cfg.stateDir}/cache"
-
-        # create the basic directory layout the redmine package expects
-        mkdir -p /run/redmine/public
-
-        for i in config files log plugins tmp; do
-          mkdir -p "${cfg.stateDir}/$i"
-          ln -fs "${cfg.stateDir}/$i" /run/redmine/
-        done
-
-        for i in plugin_assets themes; do
-          mkdir -p "${cfg.stateDir}/public/$i"
-          ln -fs "${cfg.stateDir}/public/$i" /run/redmine/public/
-        done
-
+        rm -rf "${cfg.stateDir}/plugins/"*
+        rm -rf "${cfg.stateDir}/public/themes/"*
 
         # start with a fresh config directory
         # the config directory is copied instead of linked as some mutable data is stored in there
-        rm -rf "${cfg.stateDir}/config/"*
+        find "${cfg.stateDir}/config" ! -name "secret_token.rb" -type f -exec rm -f {} +
         cp -r ${cfg.package}/share/redmine/config.dist/* "${cfg.stateDir}/config/"
 
+        chmod -R u+w "${cfg.stateDir}/config"
+
         # link in the application configuration
         ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml"
 
+        # link in the additional environment configuration
+        ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb"
+
 
         # link in all user specified themes
-        rm -rf "${cfg.stateDir}/public/themes/"*
         for theme in ${concatStringsSep " " (mapAttrsToList unpackTheme cfg.themes)}; do
           ln -fs $theme/* "${cfg.stateDir}/public/themes"
         done
@@ -261,16 +302,11 @@ in
 
 
         # link in all user specified plugins
-        rm -rf "${cfg.stateDir}/plugins/"*
         for plugin in ${concatStringsSep " " (mapAttrsToList unpackPlugin cfg.plugins)}; do
           ln -fs $plugin/* "${cfg.stateDir}/plugins/''${plugin##*-redmine-plugin-}"
         done
 
 
-        # ensure correct permissions for most files
-        chmod -R ug+rwX,o-rwx+x "${cfg.stateDir}/"
-
-
         # handle database.passwordFile & permissions
         DBPASS=$(head -n1 ${cfg.database.passwordFile})
         cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml"
@@ -284,24 +320,13 @@ in
           chmod 440 "${cfg.stateDir}/config/initializers/secret_token.rb"
         fi
 
-
-        # ensure everything is owned by ${cfg.user}
-        chown -R ${cfg.user}:${cfg.group} "${cfg.stateDir}"
-
-
         # execute redmine required commands prior to starting the application
-        # NOTE: su required in case using mysql socket authentication
-        /run/wrappers/bin/su -s ${pkgs.bash}/bin/bash -m -l redmine -c '${bundle} exec rake db:migrate'
-        /run/wrappers/bin/su -s ${pkgs.bash}/bin/bash -m -l redmine -c '${bundle} exec rake redmine:load_default_data'
-
-
-        # log files don't exist until after first command has been executed
-        # correct ownership of files generated by calling exec rake ...
-        chown -R ${cfg.user}:${cfg.group} "${cfg.stateDir}/log"
+        ${bundle} exec rake db:migrate
+        ${bundle} exec rake redmine:plugins:migrate
+        ${bundle} exec rake redmine:load_default_data
       '';
 
       serviceConfig = {
-        PermissionsStartOnly = true; # preStart must be run as root
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
@@ -316,7 +341,6 @@ in
       { name = "redmine";
         group = cfg.group;
         home = cfg.stateDir;
-        createHome = true;
         uid = config.ids.uids.redmine;
       });
 
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index 9d9a0ba44da..cdf61730de3 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -85,70 +85,70 @@ let
   portOptions = { name, ...}: {
     options = {
       name = mkOption {
-	internal = true;
-	default = name;
+        internal = true;
+        default = name;
       };
 
       ip = mkOption {
-	default = "127.0.0.1";
-	description = "Ip where rippled listens.";
-	type = types.str;
+        default = "127.0.0.1";
+        description = "Ip where rippled listens.";
+        type = types.str;
       };
 
       port = mkOption {
-	description = "Port where rippled listens.";
-	type = types.int;
+        description = "Port where rippled listens.";
+        type = types.int;
       };
 
       protocol = mkOption {
-	description = "Protocols expose by rippled.";
-	type = types.listOf (types.enum ["http" "https" "ws" "wss" "peer"]);
+        description = "Protocols expose by rippled.";
+        type = types.listOf (types.enum ["http" "https" "ws" "wss" "peer"]);
       };
 
       user = mkOption {
-	description = "When set, these credentials will be required on HTTP/S requests.";
-	type = types.str;
-	default = "";
+        description = "When set, these credentials will be required on HTTP/S requests.";
+        type = types.str;
+        default = "";
       };
 
       password = mkOption {
-	description = "When set, these credentials will be required on HTTP/S requests.";
-	type = types.str;
-	default = "";
+        description = "When set, these credentials will be required on HTTP/S requests.";
+        type = types.str;
+        default = "";
       };
 
       admin = mkOption {
-	description = "A comma-separated list of admin IP addresses.";
-	type = types.listOf types.str;
-	default = ["127.0.0.1"];
+        description = "A comma-separated list of admin IP addresses.";
+        type = types.listOf types.str;
+        default = ["127.0.0.1"];
       };
 
       ssl = {
-	key = mkOption {
-	  description = ''
-	    Specifies the filename holding the SSL key in PEM format.
-	  '';
-	  default = null;
-	  type = types.nullOr types.path;
-	};
-
-	cert = mkOption {
-	  description = ''
-	    Specifies the path to the SSL certificate file in PEM format.
-	    This is not needed if the chain includes it.
-	  '';
-	  default = null;
-	  type = types.nullOr types.path;
-	};
-
-	chain = mkOption {
-	  description = ''
-	    If you need a certificate chain, specify the path to the
-	    certificate chain here. The chain may include the end certificate.
-	  '';
-	  default = null;
-	  type = types.nullOr types.path;
-	};
+        key = mkOption {
+          description = ''
+            Specifies the filename holding the SSL key in PEM format.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+
+        cert = mkOption {
+          description = ''
+            Specifies the path to the SSL certificate file in PEM format.
+            This is not needed if the chain includes it.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
+
+        chain = mkOption {
+          description = ''
+            If you need a certificate chain, specify the path to the
+            certificate chain here. The chain may include the end certificate.
+          '';
+          default = null;
+          type = types.nullOr types.path;
+        };
       };
     };
   };
@@ -175,14 +175,14 @@ let
 
       onlineDelete = mkOption {
         description = "Enable automatic purging of older ledger information.";
-        type = types.addCheck (types.nullOr types.int) (v: v > 256);
+        type = types.nullOr (types.addCheck types.int (v: v > 256));
         default = cfg.ledgerHistory;
       };
 
       advisoryDelete = mkOption {
         description = ''
-	        If set, then require administrative RPC call "can_delete"
-	        to enable online deletion of ledger records.
+          If set, then require administrative RPC call "can_delete"
+          to enable online deletion of ledger records.
         '';
         type = types.nullOr types.bool;
         default = null;
@@ -207,168 +207,168 @@ in
       enable = mkEnableOption "rippled";
 
       package = mkOption {
-	description = "Which rippled package to use.";
-	type = types.package;
-	default = pkgs.rippled;
-	defaultText = "pkgs.rippled";
+        description = "Which rippled package to use.";
+        type = types.package;
+        default = pkgs.rippled;
+        defaultText = "pkgs.rippled";
       };
 
       ports = mkOption {
-	description = "Ports exposed by rippled";
-	type = with types; attrsOf (submodule portOptions);
-	default = {
-	  rpc = {
-	    port = 5005;
-	    admin = ["127.0.0.1"];
-	    protocol = ["http"];
-	  };
-
-	  peer = {
-	    port = 51235;
-	    ip = "0.0.0.0";
-	    protocol = ["peer"];
-	  };
-
-	  ws_public = {
-	    port = 5006;
-	    ip = "0.0.0.0";
-	    protocol = ["ws" "wss"];
-	  };
-	};
+        description = "Ports exposed by rippled";
+        type = with types; attrsOf (submodule portOptions);
+        default = {
+          rpc = {
+            port = 5005;
+            admin = ["127.0.0.1"];
+            protocol = ["http"];
+          };
+
+          peer = {
+            port = 51235;
+            ip = "0.0.0.0";
+            protocol = ["peer"];
+          };
+
+          ws_public = {
+            port = 5006;
+            ip = "0.0.0.0";
+            protocol = ["ws" "wss"];
+          };
+        };
       };
 
       nodeDb = mkOption {
-	description = "Rippled main database options.";
-	type = with types; nullOr (submodule dbOptions);
-	default = {
-	  type = "rocksdb";
-	  extraOpts = ''
-	    open_files=2000
-	    filter_bits=12
-	    cache_mb=256
-	    file_size_pb=8
-	    file_size_mult=2;
-	  '';
-	};
+        description = "Rippled main database options.";
+        type = with types; nullOr (submodule dbOptions);
+        default = {
+          type = "rocksdb";
+          extraOpts = ''
+            open_files=2000
+            filter_bits=12
+            cache_mb=256
+            file_size_pb=8
+            file_size_mult=2;
+          '';
+        };
       };
 
       tempDb = mkOption {
-	description = "Rippled temporary database options.";
-	type = with types; nullOr (submodule dbOptions);
-	default = null;
+        description = "Rippled temporary database options.";
+        type = with types; nullOr (submodule dbOptions);
+        default = null;
       };
 
       importDb = mkOption {
-	description = "Settings for performing a one-time import.";
-	type = with types; nullOr (submodule dbOptions);
-	default = null;
+        description = "Settings for performing a one-time import.";
+        type = with types; nullOr (submodule dbOptions);
+        default = null;
       };
 
       nodeSize = mkOption {
-	description = ''
-	  Rippled size of the node you are running.
-	  "tiny", "small", "medium", "large", and "huge"
-	'';
-	type = types.enum ["tiny" "small" "medium" "large" "huge"];
-	default = "small";
+        description = ''
+          Rippled size of the node you are running.
+          "tiny", "small", "medium", "large", and "huge"
+        '';
+        type = types.enum ["tiny" "small" "medium" "large" "huge"];
+        default = "small";
       };
 
       ips = mkOption {
-	description = ''
-	  List of hostnames or ips where the Ripple protocol is served.
-	  For a starter list, you can either copy entries from:
-	  https://ripple.com/ripple.txt or if you prefer you can let it
-	   default to r.ripple.com 51235
-
-	  A port may optionally be specified after adding a space to the
-	  address. By convention, if known, IPs are listed in from most
-	  to least trusted.
-	'';
-	type = types.listOf types.str;
-	default = ["r.ripple.com 51235"];
+        description = ''
+          List of hostnames or ips where the Ripple protocol is served.
+          For a starter list, you can either copy entries from:
+          https://ripple.com/ripple.txt or if you prefer you can let it
+           default to r.ripple.com 51235
+
+          A port may optionally be specified after adding a space to the
+          address. By convention, if known, IPs are listed in from most
+          to least trusted.
+        '';
+        type = types.listOf types.str;
+        default = ["r.ripple.com 51235"];
       };
 
       ipsFixed = mkOption {
-	description = ''
-	  List of IP addresses or hostnames to which rippled should always
-	  attempt to maintain peer connections with. This is useful for
-	  manually forming private networks, for example to configure a
-	  validation server that connects to the Ripple network through a
-	  public-facing server, or for building a set of cluster peers.
+        description = ''
+          List of IP addresses or hostnames to which rippled should always
+          attempt to maintain peer connections with. This is useful for
+          manually forming private networks, for example to configure a
+          validation server that connects to the Ripple network through a
+          public-facing server, or for building a set of cluster peers.
 
-	  A port may optionally be specified after adding a space to the address
-	'';
-	type = types.listOf types.str;
-	default = [];
+          A port may optionally be specified after adding a space to the address
+        '';
+        type = types.listOf types.str;
+        default = [];
       };
 
       validators = mkOption {
-	description = ''
-	  List of nodes to always accept as validators. Nodes are specified by domain
-	  or public key.
-	'';
-	type = types.listOf types.str;
-	default = [
-	  "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7  RL1"
-	  "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj  RL2"
-	  "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C  RL3"
-	  "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS  RL4"
-	  "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA  RL5"
-	];
+        description = ''
+          List of nodes to always accept as validators. Nodes are specified by domain
+          or public key.
+        '';
+        type = types.listOf types.str;
+        default = [
+          "n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7  RL1"
+          "n9MD5h24qrQqiyBC8aeqqCWvpiBiYQ3jxSr91uiDvmrkyHRdYLUj  RL2"
+          "n9L81uNCaPgtUJfaHh89gmdvXKAmSt5Gdsw2g1iPWaPkAHW5Nm4C  RL3"
+          "n9KiYM9CgngLvtRCQHZwgC2gjpdaZcCcbt3VboxiNFcKuwFVujzS  RL4"
+          "n9LdgEtkmGB9E2h3K4Vp7iGUaKuq23Zr32ehxiU8FWY7xoxbWTSA  RL5"
+        ];
       };
 
       databasePath = mkOption {
-	description = ''
-	  Path to the ripple database.
-	'';
-	type = types.path;
-	default = "/var/lib/rippled";
+        description = ''
+          Path to the ripple database.
+        '';
+        type = types.path;
+        default = "/var/lib/rippled";
       };
 
       validationQuorum = mkOption {
-	description = ''
-	  The minimum number of trusted validations a ledger must have before
-	  the server considers it fully validated.
-	'';
-	type = types.int;
-	default = 3;
+        description = ''
+          The minimum number of trusted validations a ledger must have before
+          the server considers it fully validated.
+        '';
+        type = types.int;
+        default = 3;
       };
 
       ledgerHistory = mkOption {
-	description = ''
-	  The number of past ledgers to acquire on server startup and the minimum
-	  to maintain while running.
-	'';
-	type = types.either types.int (types.enum ["full"]);
-	default = 1296000; # 1 month
+        description = ''
+          The number of past ledgers to acquire on server startup and the minimum
+          to maintain while running.
+        '';
+        type = types.either types.int (types.enum ["full"]);
+        default = 1296000; # 1 month
       };
 
       fetchDepth = mkOption {
-	description = ''
-	  The number of past ledgers to serve to other peers that request historical
-	  ledger data (or "full" for no limit).
-	'';
-	type = types.either types.int (types.enum ["full"]);
-	default = "full";
+        description = ''
+          The number of past ledgers to serve to other peers that request historical
+          ledger data (or "full" for no limit).
+        '';
+        type = types.either types.int (types.enum ["full"]);
+        default = "full";
       };
 
       sntpServers = mkOption {
-	description = ''
-	  IP address or domain of NTP servers to use for time synchronization.;
-	'';
-	type = types.listOf types.str;
-	default = [
-	  "time.windows.com"
-	  "time.apple.com"
-	  "time.nist.gov"
-	  "pool.ntp.org"
-	];
+        description = ''
+          IP address or domain of NTP servers to use for time synchronization.;
+        '';
+        type = types.listOf types.str;
+        default = [
+          "time.windows.com"
+          "time.apple.com"
+          "time.nist.gov"
+          "pool.ntp.org"
+        ];
       };
 
       logLevel = mkOption {
         description = "Logging verbosity.";
-	type = types.enum ["debug" "error" "info"];
-	default = "error";
+        type = types.enum ["debug" "error" "info"];
+        default = "error";
       };
 
       statsd = {
@@ -389,14 +389,14 @@ in
 
       extraConfig = mkOption {
         default = "";
-	description = ''
-	  Extra lines to be added verbatim to the rippled.cfg configuration file.
-	'';
+        description = ''
+          Extra lines to be added verbatim to the rippled.cfg configuration file.
+        '';
       };
 
       config = mkOption {
-	internal = true;
-	default = pkgs.writeText "rippled.conf" rippledCfg;
+        internal = true;
+        default = pkgs.writeText "rippled.conf" rippledCfg;
       };
     };
   };
@@ -410,8 +410,8 @@ in
       { name = "rippled";
         description = "Ripple server user";
         uid = config.ids.uids.rippled;
-	home = cfg.databasePath;
-	createHome = true;
+        home = cfg.databasePath;
+        createHome = true;
       };
 
     systemd.services.rippled = {
@@ -421,8 +421,8 @@ in
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/rippled --fg --conf ${cfg.config}";
         User = "rippled";
-	Restart = "on-failure";
-	LimitNOFILE=10000;
+        Restart = "on-failure";
+        LimitNOFILE=10000;
       };
     };
 
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
index 5f97abf1871..483bc99ad94 100644
--- a/nixos/modules/services/misc/taskserver/default.nix
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -109,7 +109,7 @@ let
   nixos-taskserver = pkgs.pythonPackages.buildPythonApplication {
     name = "nixos-taskserver";
 
-    src = pkgs.runCommand "nixos-taskserver-src" {} ''
+    src = pkgs.runCommand "nixos-taskserver-src" { preferLocalBuild = true; } ''
       mkdir -p "$out"
       cat "${pkgs.substituteAll {
         src = ./helper-tool.py;
diff --git a/nixos/modules/services/misc/plexpy.nix b/nixos/modules/services/misc/tautulli.nix
index 2a589fdfb27..50e45036647 100644
--- a/nixos/modules/services/misc/plexpy.nix
+++ b/nixos/modules/services/misc/tautulli.nix
@@ -3,73 +3,69 @@
 with lib;
 
 let
-  cfg = config.services.plexpy;
+  cfg = config.services.tautulli;
 in
 {
   options = {
-    services.plexpy = {
-      enable = mkEnableOption "PlexPy Plex Monitor";
+    services.tautulli = {
+      enable = mkEnableOption "Tautulli Plex Monitor";
 
       dataDir = mkOption {
         type = types.str;
         default = "/var/lib/plexpy";
-        description = "The directory where PlexPy stores its data files.";
+        description = "The directory where Tautulli stores its data files.";
       };
 
       configFile = mkOption {
         type = types.str;
         default = "/var/lib/plexpy/config.ini";
-        description = "The location of PlexPy's config file.";
+        description = "The location of Tautulli's config file.";
       };
 
       port = mkOption {
         type = types.int;
         default = 8181;
-        description = "TCP port where PlexPy listens.";
+        description = "TCP port where Tautulli listens.";
       };
 
       user = mkOption {
         type = types.str;
         default = "plexpy";
-        description = "User account under which PlexPy runs.";
+        description = "User account under which Tautulli runs.";
       };
 
       group = mkOption {
         type = types.str;
         default = "nogroup";
-        description = "Group under which PlexPy runs.";
+        description = "Group under which Tautulli runs.";
       };
 
       package = mkOption {
         type = types.package;
-        default = pkgs.plexpy;
-        defaultText = "pkgs.plexpy";
+        default = pkgs.tautulli;
+        defaultText = "pkgs.tautulli";
         description = ''
-          The PlexPy package to use.
+          The Tautulli package to use.
         '';
       };
     };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.plexpy = {
-      description = "PlexPy Plex Monitor";
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.tautulli = {
+      description = "Tautulli Plex Monitor";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        test -d "${cfg.dataDir}" || {
-          echo "Creating initial PlexPy data directory in \"${cfg.dataDir}\"."
-          mkdir -p "${cfg.dataDir}"
-          chown ${cfg.user}:${cfg.group} "${cfg.dataDir}"
-        }
-     '';
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
-        PermissionsStartOnly = "true";
         GuessMainPID = "false";
-        ExecStart = "${cfg.package}/bin/plexpy --datadir ${cfg.dataDir} --config ${cfg.configFile} --port ${toString cfg.port} --pidfile ${cfg.dataDir}/plexpy.pid --nolaunch";
+        ExecStart = "${cfg.package}/bin/tautulli --datadir ${cfg.dataDir} --config ${cfg.configFile} --port ${toString cfg.port} --pidfile ${cfg.dataDir}/tautulli.pid --nolaunch";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/services/misc/weechat.xml b/nixos/modules/services/misc/weechat.xml
index b7f755bbc5c..7255edfb9da 100644
--- a/nixos/modules/services/misc/weechat.xml
+++ b/nixos/modules/services/misc/weechat.xml
@@ -8,7 +8,7 @@
   <link xlink:href="https://weechat.org/">WeeChat</link> is a fast and
   extensible IRC client.
  </para>
- <section>
+ <section xml:id="module-services-weechat-basic-usage">
   <title>Basic Usage</title>
 
   <para>
@@ -35,7 +35,7 @@
    in the state directory <literal>/var/lib/weechat</literal>.
   </para>
  </section>
- <section>
+ <section xml:id="module-services-weechat-reattach">
   <title>Re-attaching to WeeChat</title>
 
   <para>
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index a40e9e84613..2bd2f3c7cc0 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -50,7 +50,7 @@ let
     ZM_DB_TYPE=mysql
     ZM_DB_HOST=${cfg.database.host}
     ZM_DB_NAME=${cfg.database.name}
-    ZM_DB_USER=${cfg.database.username}
+    ZM_DB_USER=${if cfg.database.createLocally then user else cfg.database.username}
     ZM_DB_PASS=${cfg.database.password}
 
     # Web
@@ -205,15 +205,13 @@ in {
 
       mysql = lib.mkIf cfg.database.createLocally {
         ensureDatabases = [ cfg.database.name ];
-        ensureUsers = {
+        initialDatabases = [{
+          inherit (cfg.database) name; schema = "${pkg}/share/zoneminder/db/zm_create.sql";
+        }];
+        ensureUsers = [{
           name = cfg.database.username;
-          ensurePermissions = [
-            { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }
-          ];
-          initialDatabases = [
-            { inherit (cfg.database) name; schema = "${pkg}/share/zoneminder/db/zm_create.sql"; }
-          ];
-        };
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }];
       };
 
       nginx = lib.mkIf useNginx {
@@ -277,14 +275,14 @@ in {
       };
 
       phpfpm = lib.mkIf useNginx {
-        phpOptions = ''
-          date.timezone = "${config.time.timeZone}"
-
-          ${lib.concatStringsSep "\n" (map (e:
-          "extension=${e.pkg}/lib/php/extensions/${e.name}.so") phpExtensions)}
-        '';
         pools.zoneminder = {
           listen = socket;
+          phpOptions = ''
+            date.timezone = "${config.time.timeZone}"
+
+            ${lib.concatStringsSep "\n" (map (e:
+            "extension=${e.pkg}/lib/php/extensions/${e.name}.so") phpExtensions)}
+          '';
           extraConfig = ''
             user = ${user}
             group = ${group}
diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix
index 7ee870183ca..49957e65290 100644
--- a/nixos/modules/services/monitoring/apcupsd.nix
+++ b/nixos/modules/services/monitoring/apcupsd.nix
@@ -45,7 +45,7 @@ let
 
   eventToShellCmds = event: if builtins.hasAttr event cfg.hooks then (shellCmdsForEventScript event (builtins.getAttr event cfg.hooks)) else "";
 
-  scriptDir = pkgs.runCommand "apcupsd-scriptdir" {} (''
+  scriptDir = pkgs.runCommand "apcupsd-scriptdir" { preferLocalBuild = true; } (''
     mkdir "$out"
     # Copy SCRIPTDIR from apcupsd package
     cp -r ${pkgs.apcupsd}/etc/apcupsd/* "$out"/
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index a4d29d45bac..ce3d53fb2c1 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -202,7 +202,7 @@ in {
     };
   };
   config = mkIf cfg.enable {
-    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps ];
+    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute ];
 
     users.extraUsers.datadog = {
       description = "Datadog Agent User";
@@ -216,7 +216,7 @@ in {
 
     systemd.services = let
       makeService = attrs: recursiveUpdate {
-        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps ];
+        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           User = "datadog";
@@ -235,7 +235,7 @@ in {
         '';
         script = ''
           export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile})
-          exec ${datadogPkg}/bin/agent start -c /etc/datadog-agent/datadog.yaml
+          exec ${datadogPkg}/bin/agent run -c /etc/datadog-agent/datadog.yaml
         '';
         serviceConfig.PermissionsStartOnly = true;
       };
@@ -260,7 +260,7 @@ in {
         path = [ ];
         script = ''
           export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile})
-          ${pkgs.datadog-trace-agent}/bin/trace-agent -config /etc/datadog-agent/datadog.yaml
+          ${datadogPkg}/bin/trace-agent -config /etc/datadog-agent/datadog.yaml
         '';
       });
 
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 5fb3e377122..85879cfe0b3 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -50,6 +50,158 @@ let
     SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
   } // cfg.extraOptions;
 
+  datasourceConfiguration = {
+    apiVersion = 1;
+    datasources = cfg.provision.datasources;
+  };
+
+  datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration);
+
+  dashboardConfiguration = {
+    apiVersion = 1;
+    providers = cfg.provision.dashboards;
+  };
+
+  dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
+
+  provisionConfDir =  pkgs.runCommand "grafana-provisioning" { } ''
+    mkdir -p $out/{datasources,dashboards}
+    ln -sf ${datasourceFile} $out/datasources/datasource.yaml
+    ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
+  '';
+
+  # Get a submodule without any embedded metadata:
+  _filter = x: filterAttrs (k: v: k != "_module") x;
+
+  # http://docs.grafana.org/administration/provisioning/#datasources
+  grafanaTypes.datasourceConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        description = "Name of the datasource. Required";
+      };
+      type = mkOption {
+        type = types.enum ["graphite" "prometheus" "cloudwatch" "elasticsearch" "influxdb" "opentsdb" "mysql" "mssql" "postgres" "loki"];
+        description = "Datasource type. Required";
+      };
+      access = mkOption {
+        type = types.enum ["proxy" "direct"];
+        default = "proxy";
+        description = "Access mode. proxy or direct (Server or Browser in the UI). Required";
+      };
+      orgId = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Org id. will default to orgId 1 if not specified";
+      };
+      url = mkOption {
+        type = types.str;
+        description = "Url of the datasource";
+      };
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database password, if used";
+      };
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database user, if used";
+      };
+      database = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database name, if used";
+      };
+      basicAuth = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        description = "Enable/disable basic auth";
+      };
+      basicAuthUser = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Basic auth username";
+      };
+      basicAuthPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Basic auth password";
+      };
+      withCredentials = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable/disable with credentials headers";
+      };
+      isDefault = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Mark as default datasource. Max one per org";
+      };
+      jsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Datasource specific configuration";
+      };
+      secureJsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Datasource specific secure configuration";
+      };
+      version = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Version";
+      };
+      editable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allow users to edit datasources from the UI.";
+      };
+    };
+  };
+
+  # http://docs.grafana.org/administration/provisioning/#dashboards
+  grafanaTypes.dashboardConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = "default";
+        description = "Provider name";
+      };
+      orgId = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Organization ID";
+      };
+      folder = mkOption {
+        type = types.str;
+        default = "";
+        description = "Add dashboards to the speciied folder";
+      };
+      type = mkOption {
+        type = types.str;
+        default = "file";
+        description = "Dashboard provider type";
+      };
+      disableDeletion = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable deletion when JSON file is removed";
+      };
+      updateIntervalSeconds = mkOption {
+        type = types.int;
+        default = 10;
+        description = "How often Grafana will scan for changed dashboards";
+      };
+      options = {
+        path = mkOption {
+          type = types.path;
+          description = "Path grafana will watch for dashboards";
+        };
+      };
+    };
+  };
 in {
   options.services.grafana = {
     enable = mkEnableOption "grafana";
@@ -175,6 +327,22 @@ in {
       };
     };
 
+    provision = {
+      enable = mkEnableOption "provision";
+      datasources = mkOption {
+        description = "Grafana datasources configuration";
+        default = [];
+        type = types.listOf grafanaTypes.datasourceConfig;
+        apply = x: map _filter x;
+      };
+      dashboards = mkOption {
+        description = "Grafana dashboard configuration";
+        default = [];
+        type = types.listOf grafanaTypes.dashboardConfig;
+        apply = x: map _filter x;
+      };
+    };
+
     security = {
       adminUser = mkOption {
         description = "Default admin username.";
@@ -313,10 +481,15 @@ in {
   };
 
   config = mkIf cfg.enable {
-    warnings = optional (
-      cfg.database.password != opt.database.password.default ||
-      cfg.security.adminPassword != opt.security.adminPassword.default
-    ) "Grafana passwords will be stored as plaintext in the Nix store!";
+    warnings = flatten [
+      (optional (
+        cfg.database.password != opt.database.password.default ||
+        cfg.security.adminPassword != opt.security.adminPassword.default
+      ) "Grafana passwords will be stored as plaintext in the Nix store!")
+      (optional (
+        any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources
+      ) "Datasource passwords will be stored as plaintext in the Nix store!")
+    ];
 
     environment.systemPackages = [ cfg.package ];
 
@@ -359,6 +532,9 @@ in {
         ${optionalString (cfg.smtp.passwordFile != null) ''
           export GF_SMTP_PASSWORD="$(cat ${escapeShellArg cfg.smtp.passwordFile})"
         ''}
+        ${optionalString cfg.provision.enable ''
+          export GF_PATHS_PROVISIONING=${provisionConfDir};
+        ''}
         exec ${cfg.package.bin}/bin/grafana-server -homepath ${cfg.dataDir}
       '';
       serviceConfig = {
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
index cdc98b407e9..f59bc56962b 100644
--- a/nixos/modules/services/monitoring/graphite.nix
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -9,8 +9,10 @@ let
   dataDir = cfg.dataDir;
   staticDir = cfg.dataDir + "/static";
 
-  graphiteLocalSettingsDir = pkgs.runCommand "graphite_local_settings"
-    {inherit graphiteLocalSettings;} ''
+  graphiteLocalSettingsDir = pkgs.runCommand "graphite_local_settings" {
+      inherit graphiteLocalSettings;
+      preferLocalBuild = true; 
+    } ''
     mkdir -p $out
     ln -s $graphiteLocalSettings $out/graphite_local_settings.py
   '';
diff --git a/nixos/modules/services/monitoring/hdaps.nix b/nixos/modules/services/monitoring/hdaps.nix
index be26c44e78d..2cad3b84d84 100644
--- a/nixos/modules/services/monitoring/hdaps.nix
+++ b/nixos/modules/services/monitoring/hdaps.nix
@@ -16,6 +16,7 @@ in
   };
 
   config = mkIf cfg.enable {
+    boot.kernelModules = [ "hdapsd" ];
     services.udev.packages = hdapsd;
     systemd.packages = hdapsd;
   };
diff --git a/nixos/modules/services/monitoring/munin.nix b/nixos/modules/services/monitoring/munin.nix
index 2b265d5b5a9..f6798632724 100644
--- a/nixos/modules/services/monitoring/munin.nix
+++ b/nixos/modules/services/monitoring/munin.nix
@@ -4,7 +4,7 @@
 # TODO: LWP/Pg perl libs aren't recognized
 
 # TODO: support fastcgi
-# http://munin-monitoring.org/wiki/CgiHowto2
+# http://guide.munin-monitoring.org/en/latest/example/webserver/apache-cgi.html
 # spawn-fcgi -s /run/munin/fastcgi-graph.sock -U www-data   -u munin -g munin /usr/lib/munin/cgi/munin-cgi-graph
 # spawn-fcgi -s /run/munin/fastcgi-html.sock  -U www-data   -u munin -g munin /usr/lib/munin/cgi/munin-cgi-html
 # https://paste.sh/vofcctHP#-KbDSXVeWoifYncZmLfZzgum
@@ -24,6 +24,8 @@ let
       logdir    /var/log/munin
       rundir    /run/munin
 
+      ${lib.optionalString (cronCfg.extraCSS != "") "staticdir ${customStaticDir}"}
+
       ${cronCfg.extraGlobalConfig}
 
       ${cronCfg.hosts}
@@ -63,6 +65,11 @@ let
       [ipmi*]
       user root
       group root
+
+      [munin*]
+      env.UPDATE_STATSFILE /var/lib/munin/munin-update.stats
+
+      ${nodeCfg.extraPluginConfig}
     '';
 
   pluginConfDir = pkgs.stdenv.mkDerivation {
@@ -72,6 +79,54 @@ let
       ln -s ${pluginConf} $out/nixos-config
     '';
   };
+
+  # Copy one Munin plugin into the Nix store with a specific name.
+  # This is suitable for use with plugins going directly into /etc/munin/plugins,
+  # i.e. munin.extraPlugins.
+  internOnePlugin = name: path:
+    "cp -a '${path}' '${name}'";
+
+  # Copy an entire tree of Munin plugins into a single directory in the Nix
+  # store, with no renaming.
+  # This is suitable for use with munin-node-configure --suggest, i.e.
+  # munin.extraAutoPlugins.
+  internManyPlugins = name: path:
+    "find '${path}' -type f -perm /a+x -exec cp -a -t . '{}' '+'";
+
+  # Use the appropriate intern-fn to copy the plugins into the store and patch
+  # them afterwards in an attempt to get them to run on NixOS.
+  internAndFixPlugins = name: intern-fn: paths:
+    pkgs.runCommand name {} ''
+      mkdir -p "$out"
+      cd "$out"
+      ${lib.concatStringsSep "\n"
+          (lib.attrsets.mapAttrsToList intern-fn paths)}
+      chmod -R u+w .
+      find . -type f -exec sed -E -i '
+        s,(/usr)?/s?bin/,/run/current-system/sw/bin/,g
+      ' '{}' '+'
+    '';
+
+  # TODO: write a derivation for munin-contrib, so that for contrib plugins
+  # you can just refer to them by name rather than needing to include a copy
+  # of munin-contrib in your nixos configuration.
+  extraPluginDir = internAndFixPlugins "munin-extra-plugins.d"
+    internOnePlugin nodeCfg.extraPlugins;
+
+  extraAutoPluginDir = internAndFixPlugins "munin-extra-auto-plugins.d"
+    internManyPlugins
+    (builtins.listToAttrs
+      (map
+        (path: { name = baseNameOf path; value = path; })
+        nodeCfg.extraAutoPlugins));
+
+  customStaticDir = pkgs.runCommand "munin-custom-static-data" {} ''
+    cp -a "${pkgs.munin}/etc/opt/munin/static" "$out"
+    cd "$out"
+    chmod -R u+w .
+    echo "${cronCfg.extraCSS}" >> style.css
+    echo "${cronCfg.extraCSS}" >> style-new.css
+  '';
 in
 
 {
@@ -82,11 +137,12 @@ in
 
       enable = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Enable Munin Node agent. Munin node listens on 0.0.0.0 and
           by default accepts connections only from 127.0.0.1 for security reasons.
 
-          See <link xlink:href='http://munin-monitoring.org/wiki/munin-node.conf' />.
+          See <link xlink:href='http://guide.munin-monitoring.org/en/latest/architecture/index.html' />.
         '';
       };
 
@@ -95,18 +151,108 @@ in
         type = types.lines;
         description = ''
           <filename>munin-node.conf</filename> extra configuration. See
-          <link xlink:href='http://munin-monitoring.org/wiki/munin-node.conf' />
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html' />
+        '';
+      };
+
+      extraPluginConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          <filename>plugin-conf.d</filename> extra plugin configuration. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/plugin/use.html' />
+        '';
+        example = ''
+          [fail2ban_*]
+          user root
         '';
       };
 
-      # TODO: add option to add additional plugins
+      extraPlugins = mkOption {
+        default = {};
+        type = with types; attrsOf path;
+        description = ''
+          Additional Munin plugins to activate. Keys are the name of the plugin
+          symlink, values are the path to the underlying plugin script. You
+          can use the same plugin script multiple times (e.g. for wildcard
+          plugins).
+
+          Note that these plugins do not participate in autoconfiguration. If
+          you want to autoconfigure additional plugins, use
+          <option>services.munin-node.extraAutoPlugins</option>.
+
+          Plugins enabled in this manner take precedence over autoconfigured
+          plugins.
+
+          Plugins will be copied into the Nix store, and it will attempt to
+          modify them to run properly by fixing hardcoded references to
+          <literal>/bin</literal>, <literal>/usr/bin</literal>,
+          <literal>/sbin</literal>, and <literal>/usr/sbin</literal>.
+        '';
+        example = literalExample ''
+          {
+            zfs_usage_bigpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
+            zfs_usage_smallpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
+            zfs_list = /src/munin-contrib/plugins/zfs/zfs_list;
+          };
+        '';
+      };
 
+      extraAutoPlugins = mkOption {
+        default = [];
+        type = with types; listOf path;
+        description = ''
+          Additional Munin plugins to autoconfigure, using
+          <literal>munin-node-configure --suggest</literal>. These should be
+          the actual paths to the plugin files (or directories containing them),
+          not just their names.
+
+          If you want to manually enable individual plugins instead, use
+          <option>services.munin-node.extraPlugins</option>.
+
+          Note that only plugins that have the 'autoconfig' capability will do
+          anything if listed here, since plugins that cannot autoconfigure
+          won't be automatically enabled by
+          <literal>munin-node-configure</literal>.
+
+          Plugins will be copied into the Nix store, and it will attempt to
+          modify them to run properly by fixing hardcoded references to
+          <literal>/bin</literal>, <literal>/usr/bin</literal>,
+          <literal>/sbin</literal>, and <literal>/usr/sbin</literal>.
+        '';
+        example = literalExample ''
+          [
+            /src/munin-contrib/plugins/zfs
+            /src/munin-contrib/plugins/ssh
+          ];
+        '';
+      };
+
+      disabledPlugins = mkOption {
+        # TODO: figure out why Munin isn't writing the log file and fix it.
+        # In the meantime this at least suppresses a useless graph full of
+        # NaNs in the output.
+        default = [ "munin_stats" ];
+        type = with types; listOf string;
+        description = ''
+          Munin plugins to disable, even if
+          <literal>munin-node-configure --suggest</literal> tries to enable
+          them. To disable a wildcard plugin, use an actual wildcard, as in
+          the example.
+
+          munin_stats is disabled by default as it tries to read
+          <literal>/var/log/munin/munin-update.log</literal> for timing
+          information, and the NixOS build of Munin does not write this file.
+        '';
+        example = [ "diskstats" "zfs_usage_*" ];
+      };
     };
 
     services.munin-cron = {
 
       enable = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Enable munin-cron. Takes care of all heavy lifting to collect data from
           nodes and draws graphs to html. Runs munin-update, munin-limits,
@@ -119,11 +265,12 @@ in
 
       extraGlobalConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           <filename>munin.conf</filename> extra global configuration.
-          See <link xlink:href='http://munin-monitoring.org/wiki/munin.conf' />.
+          See <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html' />.
           Useful to setup notifications, see
-          <link xlink:href='http://munin-monitoring.org/wiki/HowToContact' />
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/tutorial/alert.html' />
         '';
         example = ''
           contact.email.command mail -s "Munin notification for ''${var:host}" someone@example.com
@@ -131,14 +278,34 @@ in
       };
 
       hosts = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Definitions of hosts of nodes to collect data from. Needs at least one
+          host for cron to succeed. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html' />
+        '';
         example = ''
           [''${config.networking.hostName}]
           address localhost
         '';
+      };
+
+      extraCSS = mkOption {
+        default = "";
+        type = types.lines;
         description = ''
-          Definitions of hosts of nodes to collect data from. Needs at least one
-          hosts for cron to succeed. See
-          <link xlink:href='http://munin-monitoring.org/wiki/munin.conf' />
+          Custom styling for the HTML that munin-cron generates. This will be
+          appended to the CSS files used by munin-cron and will thus take
+          precedence over the builtin styles.
+        '';
+        example = ''
+          /* A simple dark theme. */
+          html, body { background: #222222; }
+          #header, #footer { background: #333333; }
+          img.i, img.iwarn, img.icrit, img.iunkn {
+            filter: invert(100%) hue-rotate(-30deg);
+          }
         '';
       };
 
@@ -155,6 +322,7 @@ in
       description = "Munin monitoring user";
       group = "munin";
       uid = config.ids.uids.munin;
+      home = "/var/lib/munin";
     }];
 
     users.groups = [{
@@ -173,14 +341,27 @@ in
       environment.MUNIN_PLUGSTATE = "/run/munin";
       environment.MUNIN_LOGDIR = "/var/log/munin";
       preStart = ''
-        echo "updating munin plugins..."
+        echo "Updating munin plugins..."
 
         mkdir -p /etc/munin/plugins
         rm -rf /etc/munin/plugins/*
+
+        # Autoconfigure builtin plugins
         ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${pkgs.munin}/lib/plugins --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
 
-        # NOTE: we disable disktstats because plugin seems to fail and it hangs html generation (100% CPU + memory leak)
-        rm /etc/munin/plugins/diskstats || true
+        # Autoconfigure extra plugins
+        ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${extraAutoPluginDir} --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
+
+        ${lib.optionalString (nodeCfg.extraPlugins != {}) ''
+            # Link in manually enabled plugins
+            ln -f -s -t /etc/munin/plugins ${extraPluginDir}/*
+          ''}
+
+        ${lib.optionalString (nodeCfg.disabledPlugins != []) ''
+            # Disable plugins
+            cd /etc/munin/plugins
+            rm -f ${toString nodeCfg.disabledPlugins}
+          ''}
       '';
       serviceConfig = {
         ExecStart = "${pkgs.munin}/sbin/munin-node --config ${nodeConf} --servicedir /etc/munin/plugins/ --sconfdir=${pluginConfDir}";
@@ -192,6 +373,10 @@ in
 
   }) (mkIf cronCfg.enable {
 
+    # Munin is hardcoded to use DejaVu Mono and the graphs come out wrong if
+    # it's not available.
+    fonts.fonts = [ pkgs.dejavu_fonts ];
+
     systemd.timers.munin-cron = {
       description = "batch Munin master programs";
       wantedBy = [ "timers.target" ];
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 3e1d727b416..e5496209f82 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -11,8 +11,10 @@ let
 
   nagiosObjectDefs = cfg.objectDefs;
 
-  nagiosObjectDefsDir = pkgs.runCommand "nagios-objects" {inherit nagiosObjectDefs;}
-    "mkdir -p $out; ln -s $nagiosObjectDefs $out/";
+  nagiosObjectDefsDir = pkgs.runCommand "nagios-objects" {
+      inherit nagiosObjectDefs;
+      preferLocalBuild = true;
+    } "mkdir -p $out; ln -s $nagiosObjectDefs $out/";
 
   nagiosCfgFile = pkgs.writeText "nagios.cfg"
     ''
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index 4873ab1fc60..a49555cf677 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -5,14 +5,19 @@ with lib;
 let
   cfg = config.services.netdata;
 
-  wrappedPlugins = pkgs.runCommand "wrapped-plugins" {} ''
+  wrappedPlugins = pkgs.runCommand "wrapped-plugins" { preferLocalBuild = true; } ''
     mkdir -p $out/libexec/netdata/plugins.d
     ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin
   '';
 
+  plugins = [
+    "${pkgs.netdata}/libexec/netdata/plugins.d"
+    "${wrappedPlugins}/libexec/netdata/plugins.d"
+  ] ++ cfg.extraPluginPaths;
+
   localConfig = {
     global = {
-      "plugins directory" = "${pkgs.netdata}/libexec/netdata/plugins.d ${wrappedPlugins}/libexec/netdata/plugins.d";
+      "plugins directory" = concatStringsSep " " plugins;
     };
     web = {
       "web files owner" = "root";
@@ -78,6 +83,24 @@ in {
         };
       };
 
+      extraPluginPaths = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        example = literalExample ''
+          [ "/path/to/plugins.d" ]
+        '';
+        description = ''
+          Extra paths to add to the netdata global "plugins directory"
+          option.  Useful for when you want to include your own
+          collection scripts.
+          </para><para>
+          Details about writing a custom netdata plugin are available at:
+          <link xlink:href="https://docs.netdata.cloud/collectors/plugins.d/"/>
+          </para><para>
+          Cannot be combined with configText.
+        '';
+      };
+
       config = mkOption {
         type = types.attrsOf types.attrs;
         default = {};
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index 43b4a41eaf3..7d790b6b590 100644
--- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -106,7 +106,8 @@ in {
         type = types.str;
         default = "";
         description = ''
-          Address to listen on for the web interface and API.
+          Address to listen on for the web interface and API. Empty string will listen on all interfaces.
+          "localhost" will listen on 127.0.0.1 (but not ::1).
         '';
       };
 
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index 1b1503ab5fc..cc703573d8c 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -19,7 +19,7 @@ let
 
   # Pretty-print JSON to a file
   writePrettyJSON = name: x:
-    pkgs.runCommand name { } ''
+    pkgs.runCommand name { preferLocalBuild = true; } ''
       echo '${builtins.toJSON x}' | ${pkgs.jq}/bin/jq . > $out
     '';
 
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index 0a084561002..fa53107ef24 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -119,7 +119,7 @@ let
   mkExporterConf = { name, conf, serviceOpts }:
     mkIf conf.enable {
       networking.firewall.extraCommands = mkIf conf.openFirewall (concatStrings [
-        "ip46tables -I nixos-fw ${conf.firewallFilter} "
+        "ip46tables -A nixos-fw ${conf.firewallFilter} "
         "-m comment --comment ${name}-exporter -j nixos-fw-accept"
       ]);
       systemd.services."prometheus-${name}-exporter" = mkMerge ([{
diff --git a/nixos/modules/services/monitoring/scollector.nix b/nixos/modules/services/monitoring/scollector.nix
index 6ecb21d628d..fbded746a5f 100644
--- a/nixos/modules/services/monitoring/scollector.nix
+++ b/nixos/modules/services/monitoring/scollector.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.scollector;
 
-  collectors = pkgs.runCommand "collectors" {}
+  collectors = pkgs.runCommand "collectors" { preferLocalBuild = true; }
     ''
     mkdir -p $out
     ${lib.concatStringsSep
diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix
index 6bfcd7143e1..d8786732668 100644
--- a/nixos/modules/services/monitoring/telegraf.nix
+++ b/nixos/modules/services/monitoring/telegraf.nix
@@ -7,6 +7,7 @@ let
 
   configFile = pkgs.runCommand "config.toml" {
     buildInputs = [ pkgs.remarshal ];
+    preferLocalBuild = true;
   } ''
     remarshal -if json -of toml \
       < ${pkgs.writeText "config.json" (builtins.toJSON cfg.extraConfig)} \
diff --git a/nixos/modules/services/monitoring/uptime.nix b/nixos/modules/services/monitoring/uptime.nix
index b4d3a264010..c0993f3bc2e 100644
--- a/nixos/modules/services/monitoring/uptime.nix
+++ b/nixos/modules/services/monitoring/uptime.nix
@@ -4,7 +4,8 @@ let
 
   cfg = config.services.uptime;
 
-  configDir = pkgs.runCommand "config" {} (if cfg.configFile != null then ''
+  configDir = pkgs.runCommand "config" { preferLocalBuild = true; }
+  (if cfg.configFile != null then ''
     mkdir $out
     ext=`echo ${cfg.configFile} | grep -o \\..*`
     ln -sv ${cfg.configFile} $out/default$ext
diff --git a/nixos/modules/services/network-filesystems/beegfs.nix b/nixos/modules/services/network-filesystems/beegfs.nix
index d9dde3f6bb6..86b1bb9160f 100644
--- a/nixos/modules/services/network-filesystems/beegfs.nix
+++ b/nixos/modules/services/network-filesystems/beegfs.nix
@@ -102,7 +102,10 @@ let
 
   # wrappers to beegfs tools. Avoid typing path of config files
   utilWrappers = mapAttrsToList ( name: cfg:
-      ( pkgs.runCommand "beegfs-utils-${name}" { nativeBuildInputs = [ pkgs.makeWrapper ]; } ''
+    ( pkgs.runCommand "beegfs-utils-${name}" {
+        nativeBuildInputs = [ pkgs.makeWrapper ];
+        preferLocalBuild = true;
+        } ''
         mkdir -p $out/bin
 
         makeWrapper ${pkgs.beegfs}/bin/beegfs-check-servers \
diff --git a/nixos/modules/services/network-filesystems/diod.nix b/nixos/modules/services/network-filesystems/diod.nix
index 556fad4d8ab..063bae6ddb1 100644
--- a/nixos/modules/services/network-filesystems/diod.nix
+++ b/nixos/modules/services/network-filesystems/diod.nix
@@ -153,7 +153,6 @@ in
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = "${pkgs.diod}/sbin/diod -f -c ${diodConfig}";
-        CapabilityBoundingSet = "cap_net_bind_service+=ep";
       };
     };
   };
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index 602cd50d8f5..d4fa1eccdf3 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -19,7 +19,7 @@ let
     "/var/lib/ipfs/.ipfs";
 
   # Wrapping the ipfs binary with the environment variable IPFS_PATH set to dataDir because we can't set it in the user environment
-  wrapped = runCommand "ipfs" { buildInputs = [ makeWrapper ]; } ''
+  wrapped = runCommand "ipfs" { buildInputs = [ makeWrapper ]; preferLocalBuild = true; } ''
     mkdir -p "$out/bin"
     makeWrapper "${ipfs}/bin/ipfs" "$out/bin/ipfs" \
       --set IPFS_PATH ${cfg.dataDir} \
diff --git a/nixos/modules/services/network-filesystems/openafs/client.nix b/nixos/modules/services/network-filesystems/openafs/client.nix
index 240c1392088..79c4b7aee06 100644
--- a/nixos/modules/services/network-filesystems/openafs/client.nix
+++ b/nixos/modules/services/network-filesystems/openafs/client.nix
@@ -15,7 +15,7 @@ let
 
   clientServDB = pkgs.writeText "client-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.cellServDB);
 
-  afsConfig = pkgs.runCommand "afsconfig" {} ''
+  afsConfig = pkgs.runCommand "afsconfig" { preferLocalBuild = true; } ''
     mkdir -p $out
     echo ${cfg.cellName} > $out/ThisCell
     cat ${cellServDB} ${clientServDB} > $out/CellServDB
@@ -155,7 +155,7 @@ in
         };
         programs = mkOption {
           default = getBin pkgs.openafs;
-          defaultText = "config.boot.kernelPackages.openafs";
+          defaultText = "getBin pkgs.openafs";
           type = types.package;
           description = "OpenAFS programs package. MUST match the kernel module package!";
         };
@@ -198,7 +198,7 @@ in
 
     environment.etc = {
       clientCellServDB = {
-        source = pkgs.runCommand "CellServDB" {} ''
+        source = pkgs.runCommand "CellServDB" { preferLocalBuild = true; } ''
           cat ${cellServDB} ${clientServDB} > $out
         '';
         target = "openafs/CellServDB";
diff --git a/nixos/modules/services/networking/coredns.nix b/nixos/modules/services/networking/coredns.nix
new file mode 100644
index 00000000000..afb2b547a46
--- /dev/null
+++ b/nixos/modules/services/networking/coredns.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.coredns;
+  configFile = pkgs.writeText "Corefile" cfg.config;
+in {
+  options.services.coredns = {
+    enable = mkEnableOption "Coredns dns server";
+
+    config = mkOption {
+      default = "";
+      example = ''
+        . {
+          whoami
+        }
+      '';
+      type = types.lines;
+      description = "Verbatim Corefile to use. See <link xlink:href=\"https://coredns.io/manual/toc/#configuration\"/> for details.";
+    };
+
+    package = mkOption {
+      default = pkgs.coredns;
+      defaultText = "pkgs.coredns";
+      type = types.package;
+      description = "Coredns package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.coredns = {
+      description = "Coredns dns server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        PermissionsStartOnly = true;
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        CapabilityBoundingSet = "cap_net_bind_service";
+        AmbientCapabilities = "cap_net_bind_service";
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        ExecStart = "${getBin cfg.package}/bin/coredns -conf=${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR1 $MAINPID";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dnscache.nix b/nixos/modules/services/networking/dnscache.nix
index fc30f50317f..5051fc916d9 100644
--- a/nixos/modules/services/networking/dnscache.nix
+++ b/nixos/modules/services/networking/dnscache.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.dnscache;
 
-  dnscache-root = pkgs.runCommand "dnscache-root" {} ''
+  dnscache-root = pkgs.runCommand "dnscache-root" { preferLocalBuild = true; } ''
     mkdir -p $out/{servers,ip}
 
     ${concatMapStrings (ip: ''
diff --git a/nixos/modules/services/networking/firefox/sync-server.nix b/nixos/modules/services/networking/firefox/sync-server.nix
index 97d223a56ca..6842aa73561 100644
--- a/nixos/modules/services/networking/firefox/sync-server.nix
+++ b/nixos/modules/services/networking/firefox/sync-server.nix
@@ -13,7 +13,7 @@ let
     overrides = ${cfg.privateConfig}
 
     [server:main]
-    use = egg:Paste#http
+    use = egg:gunicorn
     host = ${cfg.listen.address}
     port = ${toString cfg.listen.port}
 
@@ -30,6 +30,8 @@ let
     audiences = ${removeSuffix "/" cfg.publicUrl}
   '';
 
+  user = "syncserver";
+  group = "syncserver";
 in
 
 {
@@ -126,15 +128,14 @@ in
 
   config = mkIf cfg.enable {
 
-    systemd.services.syncserver = let
-      syncServerEnv = pkgs.python.withPackages(ps: with ps; [ syncserver pasteScript requests ]);
-      user = "syncserver";
-      group = "syncserver";
-    in {
+    systemd.services.syncserver = {
       after = [ "network.target" ];
       description = "Firefox Sync Server";
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.coreutils syncServerEnv ];
+      path = [
+        pkgs.coreutils
+        (pkgs.python.withPackages (ps: [ pkgs.syncserver ps.gunicorn ]))
+      ];
 
       serviceConfig = {
         User = user;
@@ -166,14 +167,17 @@ in
           chown ${user}:${group} ${defaultDbLocation}
         fi
       '';
-      serviceConfig.ExecStart = "${syncServerEnv}/bin/paster serve ${syncServerIni}";
+
+      script = ''
+        gunicorn --paste ${syncServerIni}
+      '';
     };
 
-    users.users.syncserver = {
-      group = "syncserver";
+    users.users.${user} = {
+      inherit group;
       isSystemUser = true;
     };
 
-    users.groups.syncserver = {};
+    users.groups.${group} = {};
   };
 }
diff --git a/nixos/modules/services/networking/flannel.nix b/nixos/modules/services/networking/flannel.nix
index b93e28e34ef..c1f778ac139 100644
--- a/nixos/modules/services/networking/flannel.nix
+++ b/nixos/modules/services/networking/flannel.nix
@@ -73,11 +73,35 @@ in {
       };
     };
 
+    kubeconfig = mkOption {
+      description = ''
+        Path to kubeconfig to use for storing flannel config using the
+        Kubernetes API
+      '';
+      type = types.nullOr types.path;
+      default = null;
+    };
+
     network = mkOption {
       description = " IPv4 network in CIDR format to use for the entire flannel network.";
       type = types.str;
     };
 
+    nodeName = mkOption {
+      description = ''
+        Needed when running with Kubernetes as backend as this cannot be auto-detected";
+      '';
+      type = types.nullOr types.str;
+      default = with config.networking; (hostName + optionalString (!isNull domain) ".${domain}");
+      example = "node1.example.com";
+    };
+
+    storageBackend = mkOption {
+      description = "Determines where flannel stores its configuration at runtime";
+      type = types.enum ["etcd" "kubernetes"];
+      default = "etcd";
+    };
+
     subnetLen = mkOption {
       description = ''
         The size of the subnet allocated to each host. Defaults to 24 (i.e. /24)
@@ -122,17 +146,26 @@ in {
       after = [ "network.target" ];
       environment = {
         FLANNELD_PUBLIC_IP = cfg.publicIp;
+        FLANNELD_IFACE = cfg.iface;
+      } // optionalAttrs (cfg.storageBackend == "etcd") {
         FLANNELD_ETCD_ENDPOINTS = concatStringsSep "," cfg.etcd.endpoints;
         FLANNELD_ETCD_KEYFILE = cfg.etcd.keyFile;
         FLANNELD_ETCD_CERTFILE = cfg.etcd.certFile;
         FLANNELD_ETCD_CAFILE = cfg.etcd.caFile;
-        FLANNELD_IFACE = cfg.iface;
         ETCDCTL_CERT_FILE = cfg.etcd.certFile;
         ETCDCTL_KEY_FILE = cfg.etcd.keyFile;
         ETCDCTL_CA_FILE = cfg.etcd.caFile;
         ETCDCTL_PEERS = concatStringsSep "," cfg.etcd.endpoints;
+      } // optionalAttrs (cfg.storageBackend == "kubernetes") {
+        FLANNELD_KUBE_SUBNET_MGR = "true";
+        FLANNELD_KUBECONFIG_FILE = cfg.kubeconfig;
+        NODE_NAME = cfg.nodeName;
       };
+      path = [ pkgs.iptables ];
       preStart = ''
+        mkdir -p /run/flannel
+        touch /run/flannel/docker
+      '' + optionalString (cfg.storageBackend == "etcd") ''
         echo "setting network configuration"
         until ${pkgs.etcdctl.bin}/bin/etcdctl set /coreos.com/network/config '${builtins.toJSON networkConfig}'
         do
@@ -140,15 +173,19 @@ in {
           sleep 1
         done
       '';
-      postStart = ''
-        while [ ! -f /run/flannel/subnet.env ]
-        do
-          sleep 1
-        done
-      '';
-      serviceConfig.ExecStart = "${cfg.package}/bin/flannel";
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/flannel";
+        Restart = "always";
+        RestartSec = "10s";
+      };
     };
 
-    services.etcd.enable = mkDefault (cfg.etcd.endpoints == ["http://127.0.0.1:2379"]);
+    services.etcd.enable = mkDefault (cfg.storageBackend == "etcd" && cfg.etcd.endpoints == ["http://127.0.0.1:2379"]);
+
+    # for some reason, flannel doesn't let you configure this path
+    # see: https://github.com/coreos/flannel/blob/master/Documentation/configuration.md#configuration
+    environment.etc."kube-flannel/net-conf.json" = mkIf (cfg.storageBackend == "kubernetes") {
+      source = pkgs.writeText "net-conf.json" (builtins.toJSON networkConfig);
+    };
   };
 }
diff --git a/nixos/modules/services/networking/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
index 6a1db81413c..178a832c166 100644
--- a/nixos/modules/services/networking/gnunet.nix
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -130,7 +130,7 @@ in
       group = "gnunet";
       description = "GNUnet User";
       home = homeDir;
-      createHome = true; 
+      createHome = true;
       uid = config.ids.uids.gnunet;
     };
 
@@ -146,7 +146,7 @@ in
       wantedBy = [ "multi-user.target" ];
       path = [ cfg.package pkgs.miniupnpc ];
       environment.TMPDIR = "/tmp";
-      serviceConfig.PrivateTemp = true;
+      serviceConfig.PrivateTmp = true;
       serviceConfig.ExecStart = "${cfg.package}/lib/gnunet/libexec/gnunet-service-arm -c ${configFile}";
       serviceConfig.User = "gnunet";
       serviceConfig.UMask = "0007";
diff --git a/nixos/modules/services/networking/hylafax/systemd.nix b/nixos/modules/services/networking/hylafax/systemd.nix
index 91d9c1a37da..ef177e4be34 100644
--- a/nixos/modules/services/networking/hylafax/systemd.nix
+++ b/nixos/modules/services/networking/hylafax/systemd.nix
@@ -41,7 +41,7 @@ let
           "$out/config.${name}"
       '';
     in
-      pkgs.runCommand "hylafax-config-modems" {}
+      pkgs.runCommand "hylafax-config-modems" { preferLocalBuild = true; }
       ''mkdir --parents "$out/" ${concatStringsSep "\n" (mapModems mkLine)}'';
 
   setupSpoolScript = pkgs.substituteAll {
diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix
new file mode 100644
index 00000000000..1cc1dd3f2f6
--- /dev/null
+++ b/nixos/modules/services/networking/knot.nix
@@ -0,0 +1,95 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.knot;
+
+  configFile = pkgs.writeText "knot.conf" cfg.extraConfig;
+  socketFile = "/run/knot/knot.sock";
+
+  knotConfCheck = file: pkgs.runCommand "knot-config-checked"
+    { buildInputs = [ cfg.package ]; } ''
+    ln -s ${configFile} $out
+    knotc --config=${configFile} conf-check
+  '';
+
+  knot-cli-wrappers = pkgs.stdenv.mkDerivation {
+    name = "knot-cli-wrappers";
+    buildInputs = [ pkgs.makeWrapper ];
+    buildCommand = ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.package}/bin/knotc "$out/bin/knotc" \
+        --add-flags "--config=${configFile}" \
+        --add-flags "--socket=${socketFile}"
+      makeWrapper ${cfg.package}/bin/keymgr "$out/bin/keymgr" \
+        --add-flags "--config=${configFile}"
+      for executable in kdig khost kjournalprint knsec3hash knsupdate kzonecheck
+      do
+        ln -s "${cfg.package}/bin/$executable" "$out/bin/$executable"
+      done
+      mkdir -p "$out/share"
+      ln -s '${cfg.package}/share/man' "$out/share/"
+    '';
+  };
+in {
+  options = {
+    services.knot = {
+      enable = mkEnableOption "Knot authoritative-only DNS server";
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of additional command line paramters for knotd
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra lines to be added verbatim to knot.conf
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.knot-dns;
+        description = ''
+          Which Knot DNS package to use
+        '';
+      };
+    };
+  };
+
+  config = mkIf config.services.knot.enable {
+    systemd.services.knot = {
+      unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";
+      description = cfg.package.meta.description;
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      after = ["network.target" ];
+
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${cfg.package}/bin/knotd --config=${knotConfCheck configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}";
+        ExecReload = "${knot-cli-wrappers}/bin/knotc reload";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE CAP_SETPCAP";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE CAP_SETPCAP";
+        NoNewPrivileges = true;
+        DynamicUser = "yes";
+        RuntimeDirectory = "knot";
+        StateDirectory = "knot";
+        StateDirectoryMode = "0700";
+        PrivateDevices = true;
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+        SystemCallArchitectures = "native";
+        Restart = "on-abort";
+      };
+    };
+
+    environment.systemPackages = [ knot-cli-wrappers ];
+  };
+}
+
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index 332dc541345..9974cbd89d1 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -17,7 +17,6 @@ let
   '';
 
   mosquittoConf = pkgs.writeText "mosquitto.conf" ''
-    pid_file /run/mosquitto/pid
     acl_file ${aclFile}
     persistence true
     allow_anonymous ${boolToString cfg.allowAnonymous}
@@ -196,15 +195,15 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       serviceConfig = {
-        Type = "forking";
+        Type = "notify";
+        NotifyAccess = "main";
         User = "mosquitto";
         Group = "mosquitto";
         RuntimeDirectory = "mosquitto";
         WorkingDirectory = cfg.dataDir;
         Restart = "on-failure";
-        ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf} -d";
+        ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-        PIDFile = "/run/mosquitto/pid";
       };
       preStart = ''
         rm -f ${cfg.dataDir}/passwd
@@ -214,7 +213,7 @@ in
           if c.hashedPassword != null then
             "echo '${n}:${c.hashedPassword}' >> ${cfg.dataDir}/passwd"
           else optionalString (c.password != null)
-            "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} ${c.password}"
+            "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} '${c.password}'"
         ) cfg.users);
     };
 
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 1d6c48dd8d3..ba17f1ba825 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -5,43 +5,163 @@ with lib;
 let
   cfg = config.services.ndppd;
 
-  configFile = pkgs.runCommand "ndppd.conf" {} ''
-    substitute ${pkgs.ndppd}/etc/ndppd.conf $out \
-      --replace eth0 ${cfg.interface} \
-      --replace 1111:: ${cfg.network}
-  '';
-in {
-  options = {
-    services.ndppd = {
-      enable = mkEnableOption "daemon that proxies NDP (Neighbor Discovery Protocol) messages between interfaces";
+  render = s: f: concatStringsSep "\n" (mapAttrsToList f s);
+  prefer = a: b: if a != null then a else b;
+
+  ndppdConf = prefer cfg.configFile (pkgs.writeText "ndppd.conf" ''
+    route-ttl ${toString cfg.routeTTL}
+    ${render cfg.proxies (proxyInterfaceName: proxy: ''
+    proxy ${prefer proxy.interface proxyInterfaceName} {
+      router ${boolToString proxy.router}
+      timeout ${toString proxy.timeout}
+      ttl ${toString proxy.ttl}
+      ${render proxy.rules (ruleNetworkName: rule: ''
+      rule ${prefer rule.network ruleNetworkName} {
+        ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""}
+      }'')}
+    }'')}
+  '');
+
+  proxy = types.submodule {
+    options = {
       interface = mkOption {
-        type = types.string;
-        default = "eth0";
-        example = "ens3";
-        description = "Interface which is on link-level with router.";
+        type = types.nullOr types.str;
+        description = ''
+          Listen for any Neighbor Solicitation messages on this interface,
+          and respond to them according to a set of rules.
+          Defaults to the name of the attrset.
+        '';
+        default = null;
+      };
+      router = mkOption {
+        type = types.bool;
+        description = ''
+          Turns on or off the router flag for Neighbor Advertisement Messages.
+        '';
+        default = true;
+      };
+      timeout = mkOption {
+        type = types.int;
+        description = ''
+          Controls how long to wait for a Neighbor Advertisment Message before 
+          invalidating the entry, in milliseconds.
+        '';
+        default = 500;
+      };
+      ttl = mkOption {
+        type = types.int;
+        description = ''
+          Controls how long a valid or invalid entry remains in the cache, in 
+          milliseconds.
+        '';
+        default = 30000;
       };
+      rules = mkOption {
+        type = types.attrsOf rule;
+        description = ''
+          This is a rule that the target address is to match against. If no netmask
+          is provided, /128 is assumed. You may have several rule sections, and the
+          addresses may or may not overlap.
+        '';
+        default = {};
+      };
+    };
+  };
+
+  rule = types.submodule {
+    options = {
       network = mkOption {
-        type = types.string;
-        default = "1111::";
-        example = "2001:DB8::/32";
-        description = "Network that we proxy.";
+        type = types.nullOr types.str;
+        description = ''
+          This is the target address is to match against. If no netmask
+          is provided, /128 is assumed. The addresses of serveral rules
+          may or may not overlap.
+          Defaults to the name of the attrset.
+        '';
+        default = null;
+      };
+      method = mkOption {
+        type = types.enum [ "static" "iface" "auto" ];
+        description = ''
+          static: Immediately answer any Neighbor Solicitation Messages
+            (if they match the IP rule).
+          iface: Forward the Neighbor Solicitation Message through the specified
+            interface and only respond if a matching Neighbor Advertisement
+            Message is received.
+          auto: Same as iface, but instead of manually specifying the outgoing
+            interface, check for a matching route in /proc/net/ipv6_route.
+        '';
+        default = "auto";
       };
-      configFile = mkOption {
-        type = types.nullOr types.path;
+      interface = mkOption {
+        type = types.nullOr types.str;
+        description = "Interface to use when method is iface.";
         default = null;
-        description = "Path to configuration file.";
       };
     };
   };
 
+in {
+  options.services.ndppd = {
+    enable = mkEnableOption "daemon that proxies NDP (Neighbor Discovery Protocol) messages between interfaces";
+    interface = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Interface which is on link-level with router.
+        (Legacy option, use services.ndppd.proxies.&lt;interface&gt;.rules.&lt;network&gt; instead)
+      '';
+      default = null;
+      example = "eth0";
+    };
+    network = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Network that we proxy.
+        (Legacy option, use services.ndppd.proxies.&lt;interface&gt;.rules.&lt;network&gt; instead)
+      '';
+      default = null;
+      example = "1111::/64";
+    };
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      description = "Path to configuration file.";
+      default = null;
+    };
+    routeTTL = mkOption {
+      type = types.int;
+      description = ''
+        This tells 'ndppd' how often to reload the route file /proc/net/ipv6_route,
+        in milliseconds.
+      '';
+      default = 30000;
+    };
+    proxies = mkOption {
+      type = types.attrsOf proxy;
+      description = ''
+        This sets up a listener, that will listen for any Neighbor Solicitation
+        messages, and respond to them according to a set of rules.
+      '';
+      default = {};
+      example = { "eth0".rules."1111::/64" = {}; };
+    };
+  };
+
   config = mkIf cfg.enable {
-    systemd.packages = [ pkgs.ndppd ];
-    environment.etc."ndppd.conf".source = if (cfg.configFile != null) then cfg.configFile else configFile;
+    warnings = mkIf (cfg.interface != null && cfg.network != null) [ ''
+      The options services.ndppd.interface and services.ndppd.network will probably be removed soon,
+      please use services.ndppd.proxies.<interface>.rules.<network> instead.
+    '' ];
+
+    services.ndppd.proxies = mkIf (cfg.interface != null && cfg.network != null) {
+      "${cfg.interface}".rules."${cfg.network}" = {};
+    };
+
     systemd.services.ndppd = {
-      serviceConfig.RuntimeDirectory = [ "ndppd" ];
+      description = "NDP Proxy Daemon";
+      documentation = [ "man:ndppd(1)" "man:ndppd.conf(5)" ];
+      after = [ "network-pre.target" ];
       wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
     };
   };
-
-  meta.maintainers = with maintainers; [ gnidorah ];
 }
diff --git a/nixos/modules/services/networking/nix-serve.nix b/nixos/modules/services/networking/nix-serve.nix
index e83cad949ae..ca458d089dc 100644
--- a/nixos/modules/services/networking/nix-serve.nix
+++ b/nixos/modules/services/networking/nix-serve.nix
@@ -31,6 +31,15 @@ in
         default = null;
         description = ''
           The path to the file used for signing derivation data.
+          Generate with:
+
+          ```
+          nix-store --generate-binary-cache-key key-name secret-key-file public-key-file
+          ```
+
+          Make sure user `nix-serve` has read access to the private key file.
+
+          For more details see <citerefentry><refentrytitle>nix-store</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
         '';
       };
 
diff --git a/nixos/modules/services/networking/nylon.nix b/nixos/modules/services/networking/nylon.nix
index 613b0e0fb51..b061ce34ed2 100644
--- a/nixos/modules/services/networking/nylon.nix
+++ b/nixos/modules/services/networking/nylon.nix
@@ -142,7 +142,6 @@ in
       description = "Collection of named nylon instances";
       type = with types; loaOf (submodule nylonOpts);
       internal = true;
-      options = [ nylonOpts ];
     };
 
   };
diff --git a/nixos/modules/services/networking/prayer.nix b/nixos/modules/services/networking/prayer.nix
index f63f86496be..c936417e68c 100644
--- a/nixos/modules/services/networking/prayer.nix
+++ b/nixos/modules/services/networking/prayer.nix
@@ -25,7 +25,7 @@ let
     ${cfg.extraConfig}
   '';
 
-  prayerCfg = pkgs.runCommand "prayer.cf" { } ''
+  prayerCfg = pkgs.runCommand "prayer.cf" { preferLocalBuild = true; } ''
     # We have to remove the http_port 80, or it will start a server there
     cat ${prayer}/etc/prayer.cf | grep -v http_port > $out
     cat ${prayerExtraCfg} >> $out
diff --git a/nixos/modules/services/networking/quassel.nix b/nixos/modules/services/networking/quassel.nix
index d850bb8b130..b223a48e055 100644
--- a/nixos/modules/services/networking/quassel.nix
+++ b/nixos/modules/services/networking/quassel.nix
@@ -23,6 +23,22 @@ in
         '';
       };
 
+      certificateFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the certificate used for SSL connections with clients.
+        '';
+      };
+
+      requireSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Require SSL for connections from clients.
+        '';
+      };
+
       package = mkOption {
         type = types.package;
         default = pkgs.quasselDaemon;
@@ -71,6 +87,10 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.requireSSL -> cfg.certificateFile != null;
+        message = "Quassel needs a certificate file in order to require SSL";
+      }];
 
     users.users = mkIf (cfg.user == null) [
       { name = "quassel";
@@ -98,7 +118,13 @@ in
 
         serviceConfig =
         {
-          ExecStart = "${quassel}/bin/quasselcore --listen=${concatStringsSep '','' cfg.interfaces} --port=${toString cfg.portNumber} --configdir=${cfg.dataDir}";
+          ExecStart = concatStringsSep " " ([
+            "${quassel}/bin/quasselcore"
+            "--listen=${concatStringsSep "," cfg.interfaces}"
+            "--port=${toString cfg.portNumber}"
+            "--configdir=${cfg.dataDir}"
+          ] ++ optional cfg.requireSSL "--require-ssl"
+            ++ optional (cfg.certificateFile != null) "--ssl-cert=${cfg.certificateFile}");
           User = user;
           PermissionsStartOnly = true;
         };
diff --git a/nixos/modules/services/networking/shout.nix b/nixos/modules/services/networking/shout.nix
index 1ea676d0f92..f511a9af256 100644
--- a/nixos/modules/services/networking/shout.nix
+++ b/nixos/modules/services/networking/shout.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.shout;
   shoutHome = "/var/lib/shout";
 
-  defaultConfig = pkgs.runCommand "config.js" {} ''
+  defaultConfig = pkgs.runCommand "config.js" { preferLocalBuild = true; } ''
     EDITOR=true ${pkgs.shout}/bin/shout config --home $PWD
     mv config.js $out
   '';
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 90d08ca3131..b9b5d40c457 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -11,7 +11,7 @@ let
 
   userOptions = {
 
-    openssh.authorizedKeys = {
+    options.openssh.authorizedKeys = {
       keys = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -320,7 +320,7 @@ in
     };
 
     users.users = mkOption {
-      options = [ userOptions ];
+      type = with types; loaOf (submodule userOptions);
     };
 
   };
@@ -400,7 +400,10 @@ in
         sockets.sshd =
           { description = "SSH Socket";
             wantedBy = [ "sockets.target" ];
-            socketConfig.ListenStream = cfg.ports;
+            socketConfig.ListenStream = if cfg.listenAddresses != [] then
+              map (l: "${l.addr}:${toString (if l.port != null then l.port else 22)}") cfg.listenAddresses
+            else
+              cfg.ports;
             socketConfig.Accept = true;
           };
 
diff --git a/nixos/modules/services/networking/strongswan-swanctl/module.nix b/nixos/modules/services/networking/strongswan-swanctl/module.nix
index d770094960b..817b5ec55f7 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/module.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/module.nix
@@ -65,9 +65,12 @@ in  {
       after    = [ "network-online.target" "keys.target" ];
       wants    = [ "keys.target" ];
       path = with pkgs; [ kmod iproute iptables utillinux ];
-      environment.STRONGSWAN_CONF = pkgs.writeTextFile {
-        name = "strongswan.conf";
-        text = cfg.strongswan.extraConfig;
+      environment = {
+        STRONGSWAN_CONF = pkgs.writeTextFile {
+          name = "strongswan.conf";
+          text = cfg.strongswan.extraConfig;
+        };
+        SWANCTL_DIR = "/etc/swanctl";
       };
       restartTriggers = [ config.environment.etc."swanctl/swanctl.conf".source ];
       serviceConfig = {
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index b2ef1885a95..702481ec517 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -122,7 +122,7 @@ in {
 
     systemd.packages = [ pkgs.syncthing ];
 
-    users = mkIf (cfg.user == defaultUser) {
+    users = mkIf (cfg.systemService && cfg.user == defaultUser) {
       users."${defaultUser}" =
         { group = cfg.group;
           home  = cfg.dataDir;
diff --git a/nixos/modules/services/networking/teamspeak3.nix b/nixos/modules/services/networking/teamspeak3.nix
index 410d650b1f6..9ea9c83e37c 100644
--- a/nixos/modules/services/networking/teamspeak3.nix
+++ b/nixos/modules/services/networking/teamspeak3.nix
@@ -41,8 +41,9 @@ in
       };
 
       voiceIP = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
+        type = types.nullOr types.str;
+        default = null;
+        example = "0.0.0.0";
         description = ''
           IP on which the server instance will listen for incoming voice connections. Defaults to any IP.
         '';
@@ -57,8 +58,9 @@ in
       };
 
       fileTransferIP = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
+        type = types.nullOr types.str;
+        default = null;
+        example = "0.0.0.0";
         description = ''
           IP on which the server instance will listen for incoming file transfer connections. Defaults to any IP.
         '';
@@ -73,8 +75,9 @@ in
       };
 
       queryIP = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
+        type = types.nullOr types.str;
+        default = null;
+        example = "0.0.0.0";
         description = ''
           IP on which the server instance will listen for incoming ServerQuery connections. Defaults to any IP.
         '';
@@ -122,9 +125,12 @@ in
         ExecStart = ''
           ${ts3}/bin/ts3server \
             dbsqlpath=${ts3}/lib/teamspeak/sql/ logpath=${cfg.logPath} \
-            voice_ip=${cfg.voiceIP} default_voice_port=${toString cfg.defaultVoicePort} \
-            filetransfer_ip=${cfg.fileTransferIP} filetransfer_port=${toString cfg.fileTransferPort} \
-            query_ip=${cfg.queryIP} query_port=${toString cfg.queryPort} license_accepted=1
+            ${optionalString (cfg.voiceIP != null) "voice_ip=${cfg.voiceIP}"} \
+            default_voice_port=${toString cfg.defaultVoicePort} \
+            ${optionalString (cfg.fileTransferIP != null) "filetransfer_ip=${cfg.fileTransferIP}"} \
+            filetransfer_port=${toString cfg.fileTransferPort} \
+            ${optionalString (cfg.queryIP != null) "query_ip=${cfg.queryIP}"} \
+            query_port=${toString cfg.queryPort} license_accepted=1
         '';
         WorkingDirectory = cfg.dataDir;
         User = user;
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index ac10e77ba30..c82e0af2803 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -121,11 +121,12 @@ in
     };
 
     networking.firewall = mkIf cfg.openPorts {
-      # https://help.ubnt.com/hc/en-us/articles/204910084-UniFi-Change-Default-Ports-for-Controller-and-UAPs
+      # https://help.ubnt.com/hc/en-us/articles/218506997
       allowedTCPPorts = [
         8080  # Port for UAP to inform controller.
         8880  # Port for HTTP portal redirect, if guest portal is enabled.
         8843  # Port for HTTPS portal redirect, ditto.
+        6789  # Port for UniFi mobile speed test.
       ];
       allowedUDPPorts = [
         3478  # UDP port used for STUN.
@@ -184,4 +185,5 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ erictapen ];
 }
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index c788528fa47..cdfe98aa034 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -86,7 +86,12 @@ in {
               '';
               description = ''
                 Use this option to configure advanced authentication methods like EAP.
-                See wpa_supplicant.conf(5) for example configurations.
+                See
+                <citerefentry>
+                  <refentrytitle>wpa_supplicant.conf</refentrytitle>
+                  <manvolnum>5</manvolnum>
+                </citerefentry>
+                for example configurations.
 
                 Mutually exclusive with <varname>psk</varname> and <varname>pskRaw</varname>.
               '';
@@ -122,7 +127,12 @@ in {
               '';
               description = ''
                 Extra configuration lines appended to the network block.
-                See wpa_supplicant.conf(5) for available options.
+                See
+                <citerefentry>
+                  <refentrytitle>wpa_supplicant.conf</refentrytitle>
+                  <manvolnum>5</manvolnum>
+                </citerefentry>
+                for available options.
               '';
             };
 
@@ -174,7 +184,12 @@ in {
         '';
         description = ''
           Extra lines appended to the configuration file.
-          See wpa_supplicant.conf(5) for available options.
+          See
+          <citerefentry>
+            <refentrytitle>wpa_supplicant.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for available options.
         '';
       };
     };
@@ -193,7 +208,7 @@ in {
     # FIXME: start a separate wpa_supplicant instance per interface.
     systemd.services.wpa_supplicant = let
       ifaces = cfg.interfaces;
-      deviceUnit = interface: [ "sys-subsystem-net-devices-${interface}.device" ];
+      deviceUnit = interface: [ "sys-subsystem-net-devices-${utils.escapeSystemdPath interface}.device" ];
     in {
       description = "WPA Supplicant";
 
diff --git a/nixos/modules/services/networking/xrdp.nix b/nixos/modules/services/networking/xrdp.nix
index a1c5d879f3c..cc18f6d0064 100644
--- a/nixos/modules/services/networking/xrdp.nix
+++ b/nixos/modules/services/networking/xrdp.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.xrdp;
-  confDir = pkgs.runCommand "xrdp.conf" { } ''
+  confDir = pkgs.runCommand "xrdp.conf" { preferLocalBuild = true; } ''
     mkdir $out
 
     cp ${cfg.package}/etc/xrdp/{km-*,xrdp,sesman,xrdp_keyboard}.ini $out
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index 1031d6f3d7e..854c76cc0a1 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -11,8 +11,9 @@ let
   avahiEnabled = config.services.avahi.enable;
   polkitEnabled = config.security.polkit.enable;
 
-  additionalBackends = pkgs.runCommand "additional-cups-backends" { }
-    ''
+  additionalBackends = pkgs.runCommand "additional-cups-backends" {
+      preferLocalBuild = true;
+    } ''
       mkdir -p $out
       if [ ! -e ${cups.out}/lib/cups/backend/smb ]; then
         mkdir -p $out/lib/cups/backend
@@ -316,6 +317,10 @@ in
             mkdir -m 0755 -p ${cfg.tempDir}
 
             mkdir -m 0755 -p /var/lib/cups
+            # While cups will automatically create self-signed certificates if accessed via TLS,
+            # this directory to store the certificates needs to be created manually.
+            mkdir -m 0700 -p /var/lib/cups/ssl
+
             # Backwards compatibility
             if [ ! -L /etc/cups ]; then
               mv /etc/cups/* /var/lib/cups
diff --git a/nixos/modules/services/security/certmgr.nix b/nixos/modules/services/security/certmgr.nix
index 22d5817ec4f..e89078883eb 100644
--- a/nixos/modules/services/security/certmgr.nix
+++ b/nixos/modules/services/security/certmgr.nix
@@ -30,13 +30,20 @@ let
 
   preStart = ''
     ${concatStringsSep " \\\n" (["mkdir -p"] ++ map escapeShellArg specPaths)}
-    ${pkgs.certmgr}/bin/certmgr -f ${certmgrYaml} check
+    ${cfg.package}/bin/certmgr -f ${certmgrYaml} check
   '';
 in
 {
   options.services.certmgr = {
     enable = mkEnableOption "certmgr";
 
+    package = mkOption {
+      type = types.package;
+      default = pkgs.certmgr;
+      defaultText = "pkgs.certmgr";
+      description = "Which certmgr package to use in the service.";
+    };
+
     defaultRemote = mkOption {
       type = types.str;
       default = "127.0.0.1:8888";
@@ -187,7 +194,7 @@ in
       serviceConfig = {
         Restart = "always";
         RestartSec = "10s";
-        ExecStart = "${pkgs.certmgr}/bin/certmgr -f ${certmgrYaml}";
+        ExecStart = "${cfg.package}/bin/certmgr -f ${certmgrYaml}";
       };
     };
   };
diff --git a/nixos/modules/services/security/munge.nix b/nixos/modules/services/security/munge.nix
index fda864f2c30..504bc66c6d1 100644
--- a/nixos/modules/services/security/munge.nix
+++ b/nixos/modules/services/security/munge.nix
@@ -50,7 +50,7 @@ in
       path = [ pkgs.munge pkgs.coreutils ];
 
       preStart = ''
-        chmod 0700 ${cfg.password}
+        chmod 0400 ${cfg.password}
         mkdir -p /var/lib/munge -m 0711
         chown -R munge:munge /var/lib/munge
         mkdir -p /run/munge -m 0755
diff --git a/nixos/modules/services/security/nginx-sso.nix b/nixos/modules/services/security/nginx-sso.nix
new file mode 100644
index 00000000000..d792f90abe6
--- /dev/null
+++ b/nixos/modules/services/security/nginx-sso.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nginx.sso;
+  pkg = getBin pkgs.nginx-sso;
+  configYml = pkgs.writeText "nginx-sso.yml" (builtins.toJSON cfg.configuration);
+in {
+  options.services.nginx.sso = {
+    enable = mkEnableOption "nginx-sso service";
+
+    configuration = mkOption {
+      type = types.attrsOf types.unspecified;
+      default = {};
+      example = literalExample ''
+        {
+          listen = { addr = "127.0.0.1"; port = 8080; };
+
+          providers.token.tokens = {
+            myuser = "MyToken";
+          };
+
+          acl = {
+            rule_sets = [
+              {
+                rules = [ { field = "x-application"; equals = "MyApp"; } ];
+                allow = [ "myuser" ];
+              }
+            ];
+          };
+        }
+      '';
+      description = ''
+        nginx-sso configuration
+        (<link xlink:href="https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration">documentation</link>)
+        as a Nix attribute set.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nginx-sso = {
+      description = "Nginx SSO Backend";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkg}/bin/nginx-sso \
+            --config ${configYml} \
+            --frontend-dir ${pkg}/share/frontend
+        '';
+        Restart = "always";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/sks.nix b/nixos/modules/services/security/sks.nix
index 9f0261038d5..8136a5c763a 100644
--- a/nixos/modules/services/security/sks.nix
+++ b/nixos/modules/services/security/sks.nix
@@ -5,6 +5,9 @@ with lib;
 let
   cfg = config.services.sks;
   sksPkg = cfg.package;
+  dbConfig = pkgs.writeText "DB_CONFIG" ''
+    ${cfg.extraDbConfig}
+  '';
 
 in {
   meta.maintainers = with maintainers; [ primeos calbrecht jcumming ];
@@ -39,6 +42,20 @@ in {
         '';
       };
 
+      extraDbConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Set contents of the files "KDB/DB_CONFIG" and "PTree/DB_CONFIG" within
+          the ''${dataDir} directory. This is used to configure options for the
+          database for the sks key server.
+
+          Documentation of available options are available in the file named
+          "sampleConfig/DB_CONFIG" in the following repository:
+          https://bitbucket.org/skskeyserver/sks-keyserver/src
+        '';
+      };
+
       hkpAddress = mkOption {
         default = [ "127.0.0.1" "::1" ];
         type = types.listOf types.str;
@@ -99,6 +116,17 @@ in {
           ${lib.optionalString (cfg.webroot != null)
             "ln -sfT \"${cfg.webroot}\" web"}
           mkdir -p dump
+          # Check that both database configs are symlinks before overwriting them
+          if [ -e KDB/DB_CONFIG ] && [ ! -L KBD/DB_CONFIG ]; then
+            echo "KDB/DB_CONFIG exists but is not a symlink." >&2
+            exit 1
+          fi
+          if [ -e PTree/DB_CONFIG ] && [ ! -L PTree/DB_CONFIG ]; then
+            echo "PTree/DB_CONFIG exists but is not a symlink." >&2
+            exit 1
+          fi
+          ln -sf ${dbConfig} KDB/DB_CONFIG
+          ln -sf ${dbConfig} PTree/DB_CONFIG
           ${sksPkg}/bin/sks build dump/*.gpg -n 10 -cache 100 || true #*/
           ${sksPkg}/bin/sks cleandb || true
           ${sksPkg}/bin/sks pbuild -cache 20 -ptree_cache 70 || true
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index 137c3d61018..3892cd5c72b 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -4,6 +4,7 @@ with lib;
 
 let
   cfg = config.services.sshguard;
+
 in {
 
   ###### interface
@@ -77,65 +78,65 @@ in {
             Systemd services sshguard should receive logs of.
           '';
       };
-
     };
-
   };
 
-
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.sshguard pkgs.iptables pkgs.ipset ];
-
     environment.etc."sshguard.conf".text = let
-        list_services = ( name:  "-t ${name} ");
-      in ''
-        BACKEND="${pkgs.sshguard}/libexec/sshg-fw-ipset"
-        LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl -afb -p info -n1 ${toString (map list_services cfg.services)} -o cat"
+      args = lib.concatStringsSep " " ([
+        "-afb"
+        "-p info"
+        "-o cat"
+        "-n1"
+      ] ++ (map (name: "-t ${escapeShellArg name}") cfg.services));
+    in ''
+      BACKEND="${pkgs.sshguard}/libexec/sshg-fw-ipset"
+      LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
+    '';
+
+    systemd.services.sshguard = {
+      description = "SSHGuard brute-force attacks protection system";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      partOf = optional config.networking.firewall.enable "firewall.service";
+
+      path = with pkgs; [ iptables ipset iproute systemd ];
+
+      postStart = ''
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:ip family inet
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:ip family inet6
+        ${pkgs.iptables}/bin/iptables  -I INPUT -m set --match-set sshguard4 src -j DROP
+        ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
+      '';
+
+      preStop = ''
+        ${pkgs.iptables}/bin/iptables  -D INPUT -m set --match-set sshguard4 src -j DROP
+        ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
       '';
 
-    systemd.services.sshguard =
-      { description = "SSHGuard brute-force attacks protection system";
-
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
-        partOf = optional config.networking.firewall.enable "firewall.service";
-
-        path = [ pkgs.iptables pkgs.ipset pkgs.iproute pkgs.systemd ];
-
-        postStart = ''
-          mkdir -p /var/lib/sshguard
-          ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:ip family inet
-          ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:ip family inet6
-          ${pkgs.iptables}/bin/iptables -I INPUT -m set --match-set sshguard4 src -j DROP
-          ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
-        '';
-
-        preStop = ''
-          ${pkgs.iptables}/bin/iptables -D INPUT -m set --match-set sshguard4 src -j DROP
-          ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
-        '';
-
-        unitConfig.Documentation = "man:sshguard(8)";
-
-        serviceConfig = {
-            Type = "simple";
-            ExecStart = let
-                list_whitelist = ( name:  "-w ${name} ");
-              in ''
-                 ${pkgs.sshguard}/bin/sshguard -a ${toString cfg.attack_threshold} ${optionalString (cfg.blacklist_threshold != null) "-b ${toString cfg.blacklist_threshold}:${cfg.blacklist_file} "}-i /run/sshguard/sshguard.pid -p ${toString cfg.blocktime} -s ${toString cfg.detection_time} ${toString (map list_whitelist cfg.whitelist)}
-              '';
-            PIDFile = "/run/sshguard/sshguard.pid";
-            Restart = "always";
-
-            ReadOnlyDirectories = "/";
-            ReadWriteDirectories = "/run/sshguard /var/lib/sshguard";
-            RuntimeDirectory = "sshguard";
-            StateDirectory = "sshguard";
-            CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
-         };
+      unitConfig.Documentation = "man:sshguard(8)";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = let
+          args = lib.concatStringsSep " " ([
+            "-a ${toString cfg.attack_threshold}"
+            "-p ${toString cfg.blocktime}"
+            "-s ${toString cfg.detection_time}"
+            (optionalString (cfg.blacklist_threshold != null) "-b ${toString cfg.blacklist_threshold}:${cfg.blacklist_file}")
+          ] ++ (map (name: "-w ${escapeShellArg name}") cfg.whitelist));
+        in "${pkgs.sshguard}/bin/sshguard ${args}";
+        Restart = "always";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+        RuntimeDirectory = "sshguard";
+        StateDirectory = "sshguard";
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
       };
+    };
   };
 }
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 719eb76f42c..f544928fb6b 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -143,6 +143,9 @@ in
           ${getLib pkgs.lz4}/lib/liblz4*.so*               mr,
           ${getLib pkgs.libkrb5}/lib/lib*.so*              mr,
           ${getLib pkgs.keyutils}/lib/libkeyutils*.so*     mr,
+          ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
+          ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
+          ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
 
           @{PROC}/sys/kernel/random/uuid   r,
           @{PROC}/sys/vm/overcommit_memory r,
diff --git a/nixos/modules/services/ttys/kmscon.nix b/nixos/modules/services/ttys/kmscon.nix
index 82b6a51028e..dc37f9bee4b 100644
--- a/nixos/modules/services/ttys/kmscon.nix
+++ b/nixos/modules/services/ttys/kmscon.nix
@@ -82,7 +82,7 @@ in {
       X-RestartIfChanged=false
     '';
 
-    systemd.units."autovt@.service".unit = pkgs.runCommand "unit" { }
+    systemd.units."autovt@.service".unit = pkgs.runCommand "unit" { preferLocalBuild = true; }
         ''
           mkdir -p $out
           ln -s ${config.systemd.units."kmsconvt@.service".unit}/kmsconvt@.service $out/autovt@.service
diff --git a/nixos/modules/services/web-apps/atlassian/confluence.nix b/nixos/modules/services/web-apps/atlassian/confluence.nix
index b71887fcc6e..15744d90cc7 100644
--- a/nixos/modules/services/web-apps/atlassian/confluence.nix
+++ b/nixos/modules/services/web-apps/atlassian/confluence.nix
@@ -6,7 +6,7 @@ let
 
   cfg = config.services.confluence;
 
-  pkg = pkgs.atlassian-confluence.override (optionalAttrs cfg.sso.enable {
+  pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
     enableSSO = cfg.sso.enable;
     crowdProperties = ''
       application.name                        ${cfg.sso.applicationName}
@@ -125,7 +125,12 @@ in
         };
       };
 
-
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atlassian-confluence;
+        defaultText = "pkgs.atlassian-confluence";
+        description = "Atlassian Confluence package to use.";
+      };
 
       jrePackage = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/web-apps/atlassian/crowd.nix b/nixos/modules/services/web-apps/atlassian/crowd.nix
index 9f48d1e16a4..c144b21bdaf 100644
--- a/nixos/modules/services/web-apps/atlassian/crowd.nix
+++ b/nixos/modules/services/web-apps/atlassian/crowd.nix
@@ -6,7 +6,7 @@ let
 
   cfg = config.services.crowd;
 
-  pkg = pkgs.atlassian-crowd.override {
+  pkg = cfg.package.override {
     home = cfg.home;
     port = cfg.listenPort;
     openidPassword = cfg.openidPassword;
@@ -93,6 +93,13 @@ in
         };
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atlassian-crowd;
+        defaultText = "pkgs.atlassian-crowd";
+        description = "Atlassian Crowd package to use.";
+      };
+
       jrePackage = mkOption {
         type = types.package;
         default = pkgs.oraclejre8;
diff --git a/nixos/modules/services/web-apps/atlassian/jira.nix b/nixos/modules/services/web-apps/atlassian/jira.nix
index dba970c612b..0b3a5722d6c 100644
--- a/nixos/modules/services/web-apps/atlassian/jira.nix
+++ b/nixos/modules/services/web-apps/atlassian/jira.nix
@@ -6,7 +6,7 @@ let
 
   cfg = config.services.jira;
 
-  pkg = pkgs.atlassian-jira.override (optionalAttrs cfg.sso.enable {
+  pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
     enableSSO = cfg.sso.enable;
     crowdProperties = ''
       application.name                        ${cfg.sso.applicationName}
@@ -131,6 +131,13 @@ in
         };
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.atlassian-jira;
+        defaultText = "pkgs.atlassian-jira";
+        description = "Atlassian JIRA package to use.";
+      };
+
       jrePackage = mkOption {
         type = types.package;
         default = pkgs.oraclejre8;
diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/codimd.nix
index a0af28eac7c..56e1de17e3c 100644
--- a/nixos/modules/services/web-apps/codimd.nix
+++ b/nixos/modules/services/web-apps/codimd.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.codimd;
 
   prettyJSON = conf:
-    pkgs.runCommand "codimd-config.json" { } ''
+    pkgs.runCommand "codimd-config.json" { preferLocalBuild = true; } ''
       echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq \
         '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
     '';
diff --git a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
new file mode 100644
index 00000000000..50775c5262f
--- /dev/null
+++ b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -0,0 +1,626 @@
+{ config, lib, pkgs, ... }: with lib; let
+  cfg = config.services.icingaweb2;
+  poolName = "icingaweb2";
+  phpfpmSocketName = "/var/run/phpfpm/${poolName}.sock";
+
+  formatBool = b: if b then "1" else "0";
+
+  configIni = let
+    config = cfg.generalConfig;
+  in ''
+    [global]
+    show_stacktraces = "${formatBool config.showStacktraces}"
+    show_application_state_messages = "${formatBool config.showApplicationStateMessages}"
+    module_path = "${pkgs.icingaweb2}/modules${optionalString (builtins.length config.modulePath > 0) ":${concatStringsSep ":" config.modulePath}"}"
+    config_backend = "${config.configBackend}"
+    ${optionalString (config.configBackend == "db") ''config_resource = "${config.configResource}"''}
+
+    [logging]
+    log = "${config.log}"
+    ${optionalString (config.log != "none") ''level = "${config.logLevel}"''}
+    ${optionalString (config.log == "php" || config.log == "syslog") ''application = "${config.logApplication}"''}
+    ${optionalString (config.log == "syslog") ''facility = "${config.logFacility}"''}
+    ${optionalString (config.log == "file") ''file = "${config.logFile}"''}
+
+    [themes]
+    default = "${config.themeDefault}"
+    disabled = "${formatBool config.themeDisabled}"
+
+    [authentication]
+    ${optionalString (config.authDefaultDomain != null) ''default_domain = "${config.authDefaultDomain}"''}
+  '';
+
+  resourcesIni = concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    type = "${config.type}"
+    ${optionalString (config.type == "db") ''
+      db = "${config.db}"
+      host = "${config.host}"
+      ${optionalString (config.port != null) ''port = "${toString config.port}"''}
+      username = "${config.username}"
+      password = "${config.password}"
+      dbname = "${config.dbname}"
+      ${optionalString (config.charset != null) ''charset = "${config.charset}"''}
+      use_ssl = "${formatBool config.useSSL}"
+      ${optionalString (config.sslCert != null) ''ssl_cert = "${config.sslCert}"''}
+      ${optionalString (config.sslKey != null) ''ssl_cert = "${config.sslKey}"''}
+      ${optionalString (config.sslCA != null) ''ssl_cert = "${config.sslCA}"''}
+      ${optionalString (config.sslCApath != null) ''ssl_cert = "${config.sslCApath}"''}
+      ${optionalString (config.sslCipher != null) ''ssl_cert = "${config.sslCipher}"''}
+    ''}
+    ${optionalString (config.type == "ldap") ''
+      hostname = "${config.host}"
+      ${optionalString (config.port != null) ''port = "${toString config.port}"''}
+      root_dn = "${config.rootDN}"
+      bind_dn = "${config.username}"
+      bind_pw = "${config.password}"
+      encryption = "${config.ldapEncryption}"
+      timeout = "${toString config.ldapTimeout}"
+    ''}
+    ${optionalString (config.type == "ssh") ''
+      user = "${config.username}"
+      private_key = "${config.sshPrivateKey}"
+    ''}
+
+  '') cfg.resources);
+
+  authenticationIni = concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    backend = "${config.backend}"
+    ${optionalString (config.domain != null) ''domain = "${config.domain}"''}
+    ${optionalString (config.backend == "external" && config.externalStripRegex != null) ''strip_username_regexp = "${config.externalStripRegex}"''}
+    ${optionalString (config.backend != "external") ''resource = "${config.resource}"''}
+    ${optionalString (config.backend == "ldap" || config.backend == "msldap") ''
+      ${optionalString (config.ldapUserClass != null) ''user_class = "${config.ldapUserClass}"''}
+      ${optionalString (config.ldapUserNameAttr != null) ''user_name_attribute = "${config.ldapUserNameAttr}"''}
+      ${optionalString (config.ldapFilter != null) ''filter = "${config.ldapFilter}"''}
+    ''}
+  '') cfg.authentications);
+
+  groupsIni = concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    backend = "${config.backend}"
+    resource = "${config.resource}"
+    ${optionalString (config.backend != "db") ''
+      ${optionalString (config.ldapUserClass != null) ''user_class = "${config.ldapUserClass}"''}
+      ${optionalString (config.ldapUserNameAttr != null) ''user_name_attribute = "${config.ldapUserNameAttr}"''}
+      ${optionalString (config.ldapGroupClass != null) ''group_class = "${config.ldapGroupClass}"''}
+      ${optionalString (config.ldapGroupNameAttr != null) ''group_name_attribute = "${config.ldapGroupNameAttr}"''}
+      ${optionalString (config.ldapGroupFilter != null) ''group_filter = "${config.ldapGroupFilter}"''}
+    ''}
+    ${optionalString (config.backend == "msldap" && config.ldapNestedSearch) ''nested_group_search = "1"''}
+  '') cfg.groupBackends);
+
+  rolesIni = let
+    optionalList = var: attribute: optionalString (builtins.length var > 0) ''${attribute} = "${concatStringsSep "," var}"'';
+  in concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    ${optionalList config.users "users"}
+    ${optionalList config.groups "groups"}
+    ${optionalList config.permissions "permissions"}
+    ${optionalList config.permissions "permissions"}
+    ${concatStringsSep "\n" (mapAttrsToList (key: value: optionalList value key) config.extraAssignments)}
+  '') cfg.roles);
+
+in {
+  options.services.icingaweb2 = with types; {
+    enable = mkEnableOption "the icingaweb2 web interface";
+
+    pool = mkOption {
+      type = str;
+      default = "${poolName}";
+      description = ''
+         Name of existing PHP-FPM pool that is used to run Icingaweb2.
+         If not specified, a pool will automatically created with default values.
+      '';
+    };
+
+    virtualHost = mkOption {
+      type = nullOr str;
+      default = "icingaweb2";
+      description = ''
+        Name of the nginx virtualhost to use and setup. If null, no virtualhost is set up.
+      '';
+    };
+
+    timezone = mkOption {
+      type = str;
+      default = "UTC";
+      example = "Europe/Berlin";
+      description = "PHP-compliant timezone specification";
+    };
+
+    modules = {
+      doc.enable = mkEnableOption "the icingaweb2 doc module";
+      migrate.enable = mkEnableOption "the icingaweb2 migrate module";
+      setup.enable = mkEnableOption "the icingaweb2 setup module";
+      test.enable = mkEnableOption "the icingaweb2 test module";
+      translation.enable = mkEnableOption "the icingaweb2 translation module";
+    };
+
+    modulePackages = mkOption {
+      type = attrsOf package;
+      default = {};
+      example = literalExample ''
+        {
+          "snow" = pkgs.icingaweb2Modules.theme-snow;
+        }
+      '';
+      description = ''
+        Name-package attrset of Icingaweb 2 modules packages to enable.
+
+        If you enable modules manually (e.g. via the web ui), they will not be touched.
+      '';
+    };
+
+    generalConfig = {
+      mutable = mkOption {
+        type = bool;
+        default = false;
+        description = ''
+          Make config.ini mutable (e.g. via the web interface).
+          Not that you need to update module_path manually.
+        '';
+      };
+
+      showStacktraces = mkOption {
+        type = bool;
+        default = true;
+        description = "Enable stack traces in the Web UI";
+      };
+
+      showApplicationStateMessages = mkOption {
+        type = bool;
+        default = true;
+        description = "Enable application state messages in the Web UI";
+      };
+
+      modulePath = mkOption {
+        type = listOf str;
+        default = [];
+        description = "List of additional module search paths";
+      };
+
+      configBackend = mkOption {
+        type = enum [ "ini" "db" "none" ];
+        default = "db";
+        description = "Where to store user preferences";
+      };
+
+      configResource = mkOption {
+        type = nullOr str;
+        default = null;
+        description = "Database resource where user preferences are stored (if they are stored in a database)";
+      };
+
+      log = mkOption {
+        type = enum [ "syslog" "php" "file" "none" ];
+        default = "syslog";
+        description = "Logging target";
+      };
+
+      logLevel = mkOption {
+        type = enum [ "ERROR" "WARNING" "INFO" "DEBUG" ];
+        default = "ERROR";
+        description = "Maximum logging level to emit";
+      };
+
+      logApplication = mkOption {
+        type = str;
+        default = "icingaweb2";
+        description = "Application name to log under (syslog and php log)";
+      };
+
+      logFacility = mkOption {
+        type = enum [ "user" "local0" "local1" "local2" "local3" "local4" "local5" "local6" "local7" ];
+        default = "user";
+        description = "Syslog facility to log to";
+      };
+
+      logFile = mkOption {
+        type = str;
+        default = "/var/log/icingaweb2/icingaweb2.log";
+        description = "File to log to";
+      };
+
+      themeDefault = mkOption {
+        type = str;
+        default = "Icinga";
+        description = "Name of the default theme";
+      };
+
+      themeDisabled = mkOption {
+        type = bool;
+        default = false;
+        description = "Disallow users to change the theme";
+      };
+
+      authDefaultDomain = mkOption {
+        type = nullOr str;
+        default = null;
+        description = "Domain for users logging in without a qualified domain";
+      };
+    };
+
+    mutableResources = mkOption {
+      type = bool;
+      default = false;
+      description = "Make resources.ini mutable (e.g. via the web interface)";
+    };
+
+    resources = mkOption {
+      default = {};
+      description = "Icingaweb 2 resources to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this resource";
+          };
+
+          type = mkOption {
+            type = enum [ "db" "ldap" "ssh" ];
+            default = "db";
+            description = "Type of this resouce";
+          };
+
+          db = mkOption {
+            type = enum [ "mysql" "pgsql" ];
+            default = "mysql";
+            description = "Type of this database resource";
+          };
+
+          host = mkOption {
+            type = str;
+            description = "Host to connect to";
+          };
+
+          port = mkOption {
+            type = nullOr port;
+            default = null;
+            description = "Port to connect on";
+          };
+
+          username = mkOption {
+            type = str;
+            description = "Database or SSH user or LDAP bind DN to connect with";
+          };
+
+          password = mkOption {
+            type = str;
+            description = "Password for the database user or LDAP bind DN";
+          };
+
+          dbname = mkOption {
+            type = str;
+            description = "Name of the database to connect to";
+          };
+
+          charset = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "utf8";
+            description = "Database character set to connect with";
+          };
+
+          useSSL = mkOption {
+            type = nullOr bool;
+            default = false;
+            description = "Whether to connect to the database using SSL";
+          };
+
+          sslCert = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The file path to the SSL certificate. Only available for the mysql database.";
+          };
+
+          sslKey = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The file path to the SSL key. Only available for the mysql database.";
+          };
+
+          sslCA = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The file path to the SSL certificate authority. Only available for the mysql database.";
+          };
+
+          sslCApath = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The file path to the directory that contains the trusted SSL CA certificates in PEM format. Only available for the mysql database.";
+          };
+
+          sslCipher = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "A list of one or more permissible ciphers to use for SSL encryption, in a format understood by OpenSSL. Only available for the mysql database.";
+          };
+
+          rootDN = mkOption {
+            type = str;
+            description = "Root object of the LDAP tree";
+          };
+
+          ldapEncryption = mkOption {
+            type = enum [ "none" "starttls" "ldaps" ];
+            default = "none";
+            description = "LDAP encryption to use";
+          };
+
+          ldapTimeout = mkOption {
+            type = ints.positive;
+            default = 5;
+            description = "Connection timeout for every LDAP connection";
+          };
+
+          sshPrivateKey = mkOption {
+            type = str;
+            description = "The path to the private key of the user";
+          };
+        };
+      }));
+    };
+
+    mutableAuthConfig = mkOption {
+      type = bool;
+      default = true;
+      description = "Make authentication.ini mutable (e.g. via the web interface)";
+    };
+
+    authentications = mkOption {
+      default = {};
+      description = "Icingaweb 2 authentications to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this authentication";
+          };
+
+          backend = mkOption {
+            type = enum [ "external" "ldap" "msldap" "db" ];
+            default = "db";
+            description = "The type of this authentication backend";
+          };
+
+          domain = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Domain for domain-aware authentication";
+          };
+
+          externalStripRegex = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Regular expression to strip off specific user name parts";
+          };
+
+          resource = mkOption {
+            type = str;
+            description = "Name of the database/LDAP resource";
+          };
+
+          ldapUserClass = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP user class";
+          };
+
+          ldapUserNameAttr = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP attribute which contains the username";
+          };
+
+          ldapFilter = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP search filter";
+          };
+        };
+      }));
+    };
+
+    mutableGroupsConfig = mkOption {
+      type = bool;
+      default = true;
+      description = "Make groups.ini mutable (e.g. via the web interface)";
+    };
+
+    groupBackends = mkOption {
+      default = {};
+      description = "Icingaweb 2 group backends to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this group backend";
+          };
+
+          backend = mkOption {
+            type = enum [ "ldap" "msldap" "db" ];
+            default = "db";
+            description = "The type of this group backend";
+          };
+
+          resource = mkOption {
+            type = str;
+            description = "Name of the database/LDAP resource";
+          };
+
+          ldapUserClass = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP user class";
+          };
+
+          ldapUserNameAttr = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP attribute which contains the username";
+          };
+
+          ldapGroupClass = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP group class";
+          };
+
+          ldapGroupNameAttr = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP attribute which contains the groupname";
+          };
+
+          ldapGroupFilter = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "LDAP group search filter";
+          };
+
+          ldapNestedSearch = mkOption {
+            type = bool;
+            default = false;
+            description = "Enable nested group search in Active Directory based on the user";
+          };
+        };
+      }));
+    };
+
+    mutableRolesConfig = mkOption {
+      type = bool;
+      default = true;
+      description = "Make roles.ini mutable (e.g. via the web interface)";
+    };
+
+    roles = mkOption {
+      default = {};
+      description = "Icingaweb 2 roles to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this role";
+          };
+
+          users = mkOption {
+            type = listOf str;
+            default = [];
+            description = "List of users that are assigned to the role";
+          };
+
+          groups = mkOption {
+            type = listOf str;
+            default = [];
+            description = "List of groups that are assigned to the role";
+          };
+
+          permissions = mkOption {
+            type = listOf str;
+            default = [];
+            example = [ "application/share/navigation" "config/*" ];
+            description = "The permissions to grant";
+          };
+
+          extraAssignments = mkOption {
+            type = attrsOf (listOf str);
+            default = {};
+            example = { "monitoring/blacklist/properties" = [ "sla" "customer"]; };
+            description = "Additional assignments of this role";
+          };
+        };
+      }));
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.phpfpm.poolConfigs = mkIf (cfg.pool == "${poolName}") {
+      "${poolName}" = {
+        listen = phpfpmSocketName;
+        phpOptions = ''
+          extension = ${pkgs.phpPackages.imagick}/lib/php/extensions/imagick.so
+          date.timezone = "${cfg.timezone}"
+        '';
+        extraConfig = ''
+          listen.owner = nginx
+          listen.group = nginx
+          listen.mode = 0600
+          user = icingaweb2
+          pm = dynamic
+          pm.max_children = 75
+          pm.start_servers = 2
+          pm.min_spare_servers = 2
+          pm.max_spare_servers = 10
+        '';
+      };
+    };
+
+    systemd.services."phpfpm-${poolName}".serviceConfig.ReadWritePaths = [ "/etc/icingaweb2" ];
+
+    services.nginx = {
+      enable = true;
+      virtualHosts = mkIf (cfg.virtualHost != null) {
+        "${cfg.virtualHost}" = {
+          root = "${pkgs.icingaweb2}/public";
+
+          extraConfig = ''
+            index index.php;
+            try_files $1 $uri $uri/ /index.php$is_args$args;
+          '';
+
+          locations."~ ..*/.*.php$".extraConfig = ''
+            return 403;
+          '';
+
+          locations."~ ^/index.php(.*)$".extraConfig = ''
+            fastcgi_intercept_errors on;
+            fastcgi_index index.php;
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+            try_files $uri =404;
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass unix:${phpfpmSocketName};
+            fastcgi_param SCRIPT_FILENAME ${pkgs.icingaweb2}/public/index.php;
+          '';
+        };
+      };
+    };
+
+    # /etc/icingaweb2
+    environment.etc = let
+      doModule = name: optionalAttrs (cfg.modules."${name}".enable) (nameValuePair "icingaweb2/enabledModules/${name}" { source = "${pkgs.icingaweb2}/modules/${name}"; });
+    in {}
+      # Module packages
+      // (mapAttrs' (k: v: nameValuePair "icingaweb2/enabledModules/${k}" { source = v; }) cfg.modulePackages)
+      # Built-in modules
+      // doModule "doc"
+      // doModule "migrate"
+      // doModule "setup"
+      // doModule "test"
+      // doModule "translation"
+      # Configs
+      // optionalAttrs (!cfg.generalConfig.mutable) { "icingaweb2/config.ini".text = configIni; }
+      // optionalAttrs (!cfg.mutableResources) { "icingaweb2/resources.ini".text = resourcesIni; }
+      // optionalAttrs (!cfg.mutableAuthConfig) { "icingaweb2/authentication.ini".text = authenticationIni; }
+      // optionalAttrs (!cfg.mutableGroupsConfig) { "icingaweb2/groups.ini".text = groupsIni; }
+      // optionalAttrs (!cfg.mutableRolesConfig) { "icingaweb2/roles.ini".text = rolesIni; };
+
+    # User and group
+    users.groups.icingaweb2 = {};
+    users.users.icingaweb2 = {
+      description = "Icingaweb2 service user";
+      group = "icingaweb2";
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix b/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
new file mode 100644
index 00000000000..167e5e38956
--- /dev/null
+++ b/nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
@@ -0,0 +1,157 @@
+{ config, lib, pkgs, ... }: with lib; let
+  cfg = config.services.icingaweb2.modules.monitoring;
+
+  configIni = ''
+    [security]
+    protected_customvars = "${concatStringsSep "," cfg.generalConfig.protectedVars}"
+  '';
+
+  backendsIni = let
+    formatBool = b: if b then "1" else "0";
+  in concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    type = "ido"
+    resource = "${config.resource}"
+    disabled = "${formatBool config.disabled}"
+  '') cfg.backends);
+
+  transportsIni = concatStringsSep "\n" (mapAttrsToList (name: config: ''
+    [${name}]
+    type = "${config.type}"
+    ${optionalString (config.instance != null) ''instance = "${config.instance}"''}
+    ${optionalString (config.type == "local" || config.type == "remote") ''path = "${config.path}"''}
+    ${optionalString (config.type != "local") ''
+      host = "${config.host}"
+      ${optionalString (config.port != null) ''port = "${toString config.port}"''}
+      user${optionalString (config.type == "api") "name"} = "${config.username}"
+    ''}
+    ${optionalString (config.type == "api") ''password = "${config.password}"''}
+    ${optionalString (config.type == "remote") ''resource = "${config.resource}"''}
+  '') cfg.transports);
+
+in {
+  options.services.icingaweb2.modules.monitoring = with types; {
+    enable = mkOption {
+      type = bool;
+      default = true;
+      description = "Whether to enable the icingaweb2 monitoring module.";
+    };
+
+    generalConfig = {
+      mutable = mkOption {
+        type = bool;
+        default = false;
+        description = "Make config.ini of the monitoring module mutable (e.g. via the web interface).";
+      };
+
+      protectedVars = mkOption {
+        type = listOf str;
+        default = [ "*pw*" "*pass*" "community" ];
+        description = "List of string patterns for custom variables which should be excluded from user’s view.";
+      };
+    };
+
+    mutableBackends = mkOption {
+      type = bool;
+      default = false;
+      description = "Make backends.ini of the monitoring module mutable (e.g. via the web interface).";
+    };
+
+    backends = mkOption {
+      default = { "icinga" = { resource = "icinga_ido"; }; };
+      description = "Monitoring backends to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this backend";
+          };
+
+          resource = mkOption {
+            type = str;
+            description = "Name of the IDO resource";
+          };
+
+          disabled = mkOption {
+            type = bool;
+            default = false;
+            description = "Disable this backend";
+          };
+        };
+      }));
+    };
+
+    mutableTransports = mkOption {
+      type = bool;
+      default = true;
+      description = "Make commandtransports.ini of the monitoring module mutable (e.g. via the web interface).";
+    };
+
+    transports = mkOption {
+      default = {};
+      description = "Command transports to define";
+      type = attrsOf (submodule ({ name, ... }: {
+        options = {
+          name = mkOption {
+            visible = false;
+            default = name;
+            type = str;
+            description = "Name of this transport";
+          };
+
+          type = mkOption {
+            type = enum [ "api" "local" "remote" ];
+            default = "api";
+            description = "Type of  this transport";
+          };
+
+          instance = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Assign a icinga instance to this transport";
+          };
+
+          path = mkOption {
+            type = str;
+            description = "Path to the socket for local or remote transports";
+          };
+
+          host = mkOption {
+            type = str;
+            description = "Host for the api or remote transport";
+          };
+
+          port = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "Port to connect to for the api or remote transport";
+          };
+
+          username = mkOption {
+            type = str;
+            description = "Username for the api or remote transport";
+          };
+
+          password = mkOption {
+            type = str;
+            description = "Password for the api transport";
+          };
+
+          resource = mkOption {
+            type = str;
+            description = "SSH identity resource for the remote transport";
+          };
+        };
+      }));
+    };
+  };
+
+  config = mkIf (config.services.icingaweb2.enable && cfg.enable) {
+    environment.etc = { "icingaweb2/enabledModules/monitoring" = { source = "${pkgs.icingaweb2}/modules/monitoring"; }; }
+      // optionalAttrs (!cfg.generalConfig.mutable) { "icingaweb2/modules/monitoring/config.ini".text = configIni; }
+      // optionalAttrs (!cfg.mutableBackends) { "icingaweb2/modules/monitoring/backends.ini".text = backendsIni; }
+      // optionalAttrs (!cfg.mutableTransports) { "icingaweb2/modules/monitoring/commandtransports.ini".text = transportsIni; };
+  };
+}
diff --git a/nixos/modules/services/web-apps/matomo-doc.xml b/nixos/modules/services/web-apps/matomo-doc.xml
index 510a335edc3..20d2de9f418 100644
--- a/nixos/modules/services/web-apps/matomo-doc.xml
+++ b/nixos/modules/services/web-apps/matomo-doc.xml
@@ -12,15 +12,15 @@
   An automatic setup is not suported by Matomo, so you need to configure Matomo
   itself in the browser-based Matomo setup.
  </para>
+
  <section xml:id="module-services-matomo-database-setup">
   <title>Database Setup</title>
-
   <para>
    You also need to configure a MariaDB or MySQL database and -user for Matomo
    yourself, and enter those credentials in your browser. You can use
    passwordless database authentication via the UNIX_SOCKET authentication
    plugin with the following SQL commands:
-<programlisting>
+   <programlisting>
         # For MariaDB
         INSTALL PLUGIN unix_socket SONAME 'auth_socket';
         CREATE DATABASE matomo;
@@ -32,7 +32,7 @@
         CREATE DATABASE matomo;
         CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
         GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-      </programlisting>
+   </programlisting>
    Then fill in <literal>matomo</literal> as database user and database name,
    and leave the password field blank. This authentication works by allowing
    only the <literal>matomo</literal> unix user to authenticate as the
@@ -46,9 +46,30 @@
    database is not on the same host.
   </para>
  </section>
+
+ <section xml:id="module-services-matomo-archive-processing">
+  <title>Archive Processing</title>
+  <para>
+   This module comes with the systemd service <literal>matomo-archive-processing.service</literal>
+   and a timer that automatically triggers archive processing every hour.
+   This means that you can safely
+   <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
+    disable browser triggers for Matomo archiving
+   </link> at <literal>Administration > System > General Settings</literal>.
+  </para>
+  <para>
+   With automatic archive processing, you can now also enable to
+   <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
+    delete old visitor logs
+   </link> at <literal>Administration > System > Privacy</literal>,
+   but make sure that you run <literal>systemctl start matomo-archive-processing.service</literal>
+   at least once without errors if you have already collected data before,
+   so that the reports get archived before the source data gets deleted.
+  </para>
+ </section>
+
  <section xml:id="module-services-matomo-backups">
   <title>Backup</title>
-
   <para>
    You only need to take backups of your MySQL database and the
    <filename>/var/lib/matomo/config/config.ini.php</filename> file. Use a user
@@ -57,9 +78,9 @@
    <link xlink:href="https://matomo.org/faq/how-to-install/faq_138/" />.
   </para>
  </section>
+
  <section xml:id="module-services-matomo-issues">
   <title>Issues</title>
-
   <itemizedlist>
    <listitem>
     <para>
@@ -76,6 +97,7 @@
    </listitem>
   </itemizedlist>
  </section>
+
  <section xml:id="module-services-matomo-other-web-servers">
   <title>Using other Web Servers than nginx</title>
 
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 9fddf832074..14aca45a342 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -23,20 +23,24 @@ in {
   options = {
     services.matomo = {
       # NixOS PR for database setup: https://github.com/NixOS/nixpkgs/pull/6963
-      # matomo issue for automatic matomo setup: https://github.com/matomo-org/matomo/issues/10257
-      # TODO: find a nice way to do this when more NixOS MySQL and / or matomo automatic setup stuff is implemented.
+      # Matomo issue for automatic Matomo setup: https://github.com/matomo-org/matomo/issues/10257
+      # TODO: find a nice way to do this when more NixOS MySQL and / or Matomo automatic setup stuff is implemented.
       enable = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Enable matomo web analytics with php-fpm backend.
+          Enable Matomo web analytics with php-fpm backend.
           Either the nginx option or the webServerUser option is mandatory.
         '';
       };
 
       package = mkOption {
         type = types.package;
-        description = "Matomo package to use";
+        description = ''
+          Matomo package for the service to use.
+          This can be used to point to newer releases from nixos-unstable,
+          as they don't get backported if they are not security-relevant.
+        '';
         default = pkgs.matomo;
         defaultText = "pkgs.matomo";
       };
@@ -45,12 +49,25 @@ in {
         type = types.nullOr types.str;
         default = null;
         example = "lighttpd";
-        # TODO: piwik.php might get renamed to matomo.php in future releases
         description = ''
-          Name of the web server user that forwards requests to the ${phpSocket} fastcgi socket for matomo if the nginx
+          Name of the web server user that forwards requests to the ${phpSocket} fastcgi socket for Matomo if the nginx
           option is not used. Either this option or the nginx option is mandatory.
           If you want to use another webserver than nginx, you need to set this to that server's user
-          and pass fastcgi requests to `index.php` and `piwik.php` to this socket.
+          and pass fastcgi requests to `index.php`, `matomo.php` and `piwik.php` (legacy name) to this socket.
+        '';
+      };
+
+      periodicArchiveProcessing = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable periodic archive processing, which generates aggregated reports from the visits.
+
+          This means that you can safely disable browser triggers for Matomo archiving,
+          and safely enable to delete old visitor logs.
+          Before deleting visitor logs,
+          make sure though that you run <literal>systemctl start matomo-archive-processing.service</literal>
+          at least once without errors if you have already collected data before.
         '';
       };
 
@@ -69,7 +86,7 @@ in {
           catch_workers_output = yes
         '';
         description = ''
-          Settings for phpfpm's process manager. You might need to change this depending on the load for matomo.
+          Settings for phpfpm's process manager. You might need to change this depending on the load for Matomo.
         '';
       };
 
@@ -79,7 +96,7 @@ in {
             (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
             {
               # enable encryption by default,
-              # as sensitive login and matomo data should not be transmitted in clear text.
+              # as sensitive login and Matomo data should not be transmitted in clear text.
               options.forceSSL.default = true;
               options.enableACME.default = true;
             }
@@ -94,7 +111,7 @@ in {
           enableACME = false;
         };
         description = ''
-            With this option, you can customize an nginx virtualHost which already has sensible defaults for matomo.
+            With this option, you can customize an nginx virtualHost which already has sensible defaults for Matomo.
             Either this option or the webServerUser option is mandatory.
             Set this to {} to just enable the virtualHost if you don't need any customization.
             If enabled, then by default, the <option>serverName</option> is
@@ -124,29 +141,30 @@ in {
     };
     users.groups.${user} = {};
 
-    systemd.services.matomo_setup_update = {
-      # everything needs to set up and up to date before matomo php files are executed
+    systemd.services.matomo-setup-update = {
+      # everything needs to set up and up to date before Matomo php files are executed
       requiredBy = [ "${phpExecutionUnit}.service" ];
       before = [ "${phpExecutionUnit}.service" ];
       # the update part of the script can only work if the database is already up and running
       requires = [ databaseService ];
       after = [ databaseService ];
       path = [ cfg.package ];
+      environment.PIWIK_USER_PATH = dataDir;
       serviceConfig = {
         Type = "oneshot";
         User = user;
         # hide especially config.ini.php from other
         UMask = "0007";
         # TODO: might get renamed to MATOMO_USER_PATH in future versions
-        Environment = "PIWIK_USER_PATH=${dataDir}";
         # chown + chmod in preStart needs root
         PermissionsStartOnly = true;
       };
+
       # correct ownership and permissions in case they're not correct anymore,
       # e.g. after restoring from backup or moving from another system.
       # Note that ${dataDir}/config/config.ini.php might contain the MySQL password.
       preStart = ''
-        # migrate data from piwik to matomo folder
+        # migrate data from piwik to Matomo folder
         if [ -d ${deprecatedDataDir} ]; then
           echo "Migrating from ${deprecatedDataDir} to ${dataDir}"
           mv -T ${deprecatedDataDir} ${dataDir}
@@ -155,7 +173,7 @@ in {
         chmod -R ug+rwX,o-rwx ${dataDir}
         '';
       script = ''
-            # Use User-Private Group scheme to protect matomo data, but allow administration / backup via matomo group
+            # Use User-Private Group scheme to protect Matomo data, but allow administration / backup via 'matomo' group
             # Copy config folder
             chmod g+s "${dataDir}"
             cp -r "${cfg.package}/config" "${dataDir}/"
@@ -169,8 +187,39 @@ in {
       '';
     };
 
+    # If this is run regularly via the timer,
+    # 'Browser trigger archiving' can be disabled in Matomo UI > Settings > General Settings.
+    systemd.services.matomo-archive-processing = {
+      description = "Archive Matomo reports";
+      # the archiving can only work if the database is already up and running
+      requires = [ databaseService ];
+      after = [ databaseService ];
+
+      # TODO: might get renamed to MATOMO_USER_PATH in future versions
+      environment.PIWIK_USER_PATH = dataDir;
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        UMask = "0007";
+        CPUSchedulingPolicy = "idle";
+        IOSchedulingClass = "idle";
+        ExecStart = "${cfg.package}/bin/matomo-console core:archive --url=https://${user}.${fqdn}";
+      };
+    };
+
+    systemd.timers.matomo-archive-processing = mkIf cfg.periodicArchiveProcessing {
+      description = "Automatically archive Matomo reports every hour";
+
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = "hourly";
+        Persistent = "yes";
+        AccuracySec = "10m";
+      };
+    };
+
     systemd.services.${phpExecutionUnit} = {
-      # stop phpfpm on package upgrade, do database upgrade via matomo_setup_update, and then restart
+      # stop phpfpm on package upgrade, do database upgrade via matomo-setup-update, and then restart
       restartTriggers = [ cfg.package ];
       # stop config.ini.php from getting written with read permission for others
       serviceConfig.UMask = "0007";
@@ -200,13 +249,13 @@ in {
       # https://fralef.me/piwik-hardening-with-nginx-and-php-fpm.html
       # https://github.com/perusio/piwik-nginx
       "${user}.${fqdn}" = mkMerge [ cfg.nginx {
-        # don't allow to override the root easily, as it will almost certainly break matomo.
+        # don't allow to override the root easily, as it will almost certainly break Matomo.
         # disadvantage: not shown as default in docs.
         root = mkForce "${cfg.package}/share";
 
         # define locations here instead of as the submodule option's default
         # so that they can easily be extended with additional locations if required
-        # without needing to redefine the matomo ones.
+        # without needing to redefine the Matomo ones.
         # disadvantage: not shown as default in docs.
         locations."/" = {
           index = "index.php";
@@ -215,8 +264,11 @@ in {
         locations."= /index.php".extraConfig = ''
           fastcgi_pass unix:${phpSocket};
         '';
-        # TODO: might get renamed to matomo.php in future versions
-        # allow piwik.php for tracking
+        # allow matomo.php for tracking
+        locations."= /matomo.php".extraConfig = ''
+          fastcgi_pass unix:${phpSocket};
+        '';
+        # allow piwik.php for tracking (deprecated name)
         locations."= /piwik.php".extraConfig = ''
           fastcgi_pass unix:${phpSocket};
         '';
@@ -237,8 +289,11 @@ in {
         locations."= /robots.txt".extraConfig = ''
           return 200 "User-agent: *\nDisallow: /\n";
         '';
-        # TODO: might get renamed to matomo.js in future versions
-        # let browsers cache piwik.js
+        # let browsers cache matomo.js
+        locations."= /matomo.js".extraConfig = ''
+          expires 1M;
+        '';
+        # let browsers cache piwik.js (deprecated name)
         locations."= /piwik.js".extraConfig = ''
           expires 1M;
         '';
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index ecb1c5615d5..5ad241ace5c 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -5,14 +5,18 @@ with lib;
 let
   cfg = config.services.nextcloud;
 
+  phpPackage = pkgs.php73;
+  phpPackages = pkgs.php73Packages;
+
   toKeyValue = generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {} " = ";
   };
 
   phpOptionsExtensions = ''
-    ${optionalString cfg.caching.apcu "extension=${cfg.phpPackages.apcu}/lib/php/extensions/apcu.so"}
-    ${optionalString cfg.caching.redis "extension=${cfg.phpPackages.redis}/lib/php/extensions/redis.so"}
-    ${optionalString cfg.caching.memcached "extension=${cfg.phpPackages.memcached}/lib/php/extensions/memcached.so"}
+    ${optionalString cfg.caching.apcu "extension=${phpPackages.apcu}/lib/php/extensions/apcu.so"}
+    ${optionalString cfg.caching.redis "extension=${phpPackages.redis}/lib/php/extensions/redis.so"}
+    ${optionalString cfg.caching.memcached "extension=${phpPackages.memcached}/lib/php/extensions/memcached.so"}
+    extension=${phpPackages.imagick}/lib/php/extensions/imagick.so
     zend_extension = opcache.so
     opcache.enable = 1
   '';
@@ -45,6 +49,11 @@ in {
       default = "/var/lib/nextcloud";
       description = "Storage path of nextcloud.";
     };
+    logLevel = mkOption {
+      type = types.ints.between 0 4;
+      default = 2;
+      description = "Log level value between 0 (DEBUG) and 4 (FATAL).";
+    };
     https = mkOption {
       type = types.bool;
       default = false;
@@ -89,18 +98,6 @@ in {
       '';
     };
 
-    phpPackages = mkOption {
-      type = types.attrs;
-      default = pkgs.php71Packages;
-      defaultText = "pkgs.php71Packages";
-      description = ''
-        Overridable attribute of the PHP packages set to use.  If any caching
-        module is enabled, it will be taken from here.  Therefore it should
-        match the version of PHP given to
-        <literal>services.phpfpm.phpPackage</literal>.
-      '';
-    };
-
     phpOptions = mkOption {
       type = types.attrsOf types.str;
       default = {
@@ -218,6 +215,19 @@ in {
           <literal>services.nextcloud.hostname</literal> here.
         '';
       };
+
+      overwriteProtocol = mkOption {
+        type = types.nullOr (types.enum [ "http" "https" ]);
+        default = null;
+        example = "https";
+
+        description = ''
+          Force Nextcloud to always use HTTPS i.e. for link generation. Nextcloud
+          uses the currently used protocol by default, but when behind a reverse-proxy,
+          it may use <literal>http</literal> for everything although Nextcloud
+          may be served via HTTPS.
+        '';
+      };
     };
 
     caching = {
@@ -281,6 +291,8 @@ in {
               'skeletondirectory' => '${cfg.skeletonDirectory}',
               ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
               'log_type' => 'syslog',
+              'log_level' => '${builtins.toString cfg.logLevel}',
+              ${optionalString (cfg.config.overwriteProtocol != null) "'overwriteprotocol' => '${cfg.config.overwriteProtocol}',"}
             ];
           '';
           occInstallCmd = let
@@ -353,14 +365,14 @@ in {
       };
 
       services.phpfpm = {
-        phpOptions = phpOptionsExtensions;
-        phpPackage = pkgs.php71;
         pools.nextcloud = let
           phpAdminValues = (toKeyValue
             (foldr (a: b: a // b) {}
               (mapAttrsToList (k: v: { "php_admin_value[${k}]" = v; })
                 phpOptions)));
         in {
+          phpOptions = phpOptionsExtensions;
+          phpPackage = phpPackage;
           listen = "/run/phpfpm/nextcloud";
           extraConfig = ''
             listen.owner = nginx
@@ -401,7 +413,7 @@ in {
               };
               "/" = {
                 priority = 200;
-                extraConfig = "rewrite ^ /index.php$uri;";
+                extraConfig = "rewrite ^ /index.php$request_uri;";
               };
               "~ ^/store-apps" = {
                 priority = 201;
@@ -415,19 +427,19 @@ in {
                 priority = 210;
                 extraConfig = "return 301 $scheme://$host/remote.php/dav;";
               };
-              "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/" = {
+              "~ ^\\/(?:build|tests|config|lib|3rdparty|templates|data)\\/" = {
                 priority = 300;
                 extraConfig = "deny all;";
               };
-              "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)" = {
+              "~ ^\\/(?:\\.|autotest|occ|issue|indie|db_|console)" = {
                 priority = 300;
                 extraConfig = "deny all;";
               };
-              "~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\\.php(?:$|/)" = {
+              "~ ^\\/(?:index|remote|public|cron|core/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|ocs-provider\\/.+|ocm-provider\\/.+)\\.php(?:$|\\/)" = {
                 priority = 500;
                 extraConfig = ''
-                  include ${pkgs.nginxMainline}/conf/fastcgi.conf;
-                  fastcgi_split_path_info ^(.+\.php)(/.*)$;
+                  include ${config.services.nginx.package}/conf/fastcgi.conf;
+                  fastcgi_split_path_info ^(.+\.php)(\\/.*)$;
                   fastcgi_param PATH_INFO $fastcgi_path_info;
                   fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
                   fastcgi_param modHeadersAvailable true;
@@ -438,22 +450,23 @@ in {
                   fastcgi_read_timeout 120s;
                 '';
               };
-              "~ ^/(?:updater|ocs-provider)(?:$|/)".extraConfig = ''
+              "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
                 try_files $uri/ =404;
                 index index.php;
               '';
-              "~ \\.(?:css|js|woff|svg|gif)$".extraConfig = ''
-                try_files $uri /index.php$uri$is_args$args;
+              "~ \\.(?:css|js|woff2?|svg|gif)$".extraConfig = ''
+                try_files $uri /index.php$request_uri;
                 add_header Cache-Control "public, max-age=15778463";
                 add_header X-Content-Type-Options nosniff;
                 add_header X-XSS-Protection "1; mode=block";
                 add_header X-Robots-Tag none;
                 add_header X-Download-Options noopen;
                 add_header X-Permitted-Cross-Domain-Policies none;
+                add_header Referrer-Policy no-referrer;
                 access_log off;
               '';
               "~ \\.(?:png|html|ttf|ico|jpg|jpeg)$".extraConfig = ''
-                try_files $uri /index.php$uri$is_args$args;
+                try_files $uri /index.php$request_uri;
                 access_log off;
               '';
             };
@@ -463,10 +476,12 @@ in {
               add_header X-Robots-Tag none;
               add_header X-Download-Options noopen;
               add_header X-Permitted-Cross-Domain-Policies none;
+              add_header Referrer-Policy no-referrer;
               error_page 403 /core/templates/403.php;
               error_page 404 /core/templates/404.php;
               client_max_body_size ${cfg.maxUploadSize};
               fastcgi_buffers 64 4K;
+              fastcgi_hide_header X-Powered-By;
               gzip on;
               gzip_vary on;
               gzip_comp_level 4;
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
index bc6689bdb27..b064eae248e 100644
--- a/nixos/modules/services/web-apps/restya-board.nix
+++ b/nixos/modules/services/web-apps/restya-board.nix
@@ -179,34 +179,35 @@ in
   config = mkIf cfg.enable {
 
     services.phpfpm.poolConfigs = {
-      "${poolName}" = ''
-        listen = "${phpfpmSocketName}";
-        listen.owner = nginx
-        listen.group = nginx
-        listen.mode = 0600
-        user = ${cfg.user}
-        group = ${cfg.group}
-        pm = dynamic
-        pm.max_children = 75
-        pm.start_servers = 10
-        pm.min_spare_servers = 5
-        pm.max_spare_servers = 20
-        pm.max_requests = 500
-        catch_workers_output = 1
-      '';
+      "${poolName}" = {
+        listen = phpfpmSocketName;
+        phpOptions = ''
+          date.timezone = "CET"
+
+          ${optionalString (!isNull cfg.email.server) ''
+            SMTP = ${cfg.email.server}
+            smtp_port = ${toString cfg.email.port}
+            auth_username = ${cfg.email.login}
+            auth_password = ${cfg.email.password}
+          ''}
+        '';
+        extraConfig = ''
+          listen.owner = nginx
+          listen.group = nginx
+          listen.mode = 0600
+          user = ${cfg.user}
+          group = ${cfg.group}
+          pm = dynamic
+          pm.max_children = 75
+          pm.start_servers = 10
+          pm.min_spare_servers = 5
+          pm.max_spare_servers = 20
+          pm.max_requests = 500
+          catch_workers_output = 1
+        '';
+      };
     };
 
-    services.phpfpm.phpOptions = ''
-      date.timezone = "CET"
-
-      ${optionalString (!isNull cfg.email.server) ''
-        SMTP = ${cfg.email.server}
-        smtp_port = ${toString cfg.email.port}
-        auth_username = ${cfg.email.login}
-        auth_password = ${cfg.email.password}
-      ''}
-    '';
-
     services.nginx.enable = true;
     services.nginx.virtualHosts."${cfg.virtualHost.serverName}" = {
       listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ];
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index 90b35d19ea1..f7a3daa5fdd 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -34,7 +34,14 @@ let
       define('DB_HOST', '${optionalString (cfg.database.host != null) cfg.database.host}');
       define('DB_USER', '${cfg.database.user}');
       define('DB_NAME', '${cfg.database.name}');
-      define('DB_PASS', '${optionalString (cfg.database.password != null) (escape ["'" "\\"] cfg.database.password)}');
+      define('DB_PASS', ${
+        if (cfg.database.password != null) then
+          "'${(escape ["'" "\\"] cfg.database.password)}'"
+        else if (cfg.database.passwordFile != null) then
+          "file_get_contents('${cfg.database.passwordFile}')"
+        else
+          "''"
+      });
       define('DB_PORT', '${toString dbPort}');
 
       define('AUTH_AUTO_CREATE', ${boolToString cfg.auth.autoCreate});
@@ -46,7 +53,17 @@ let
       define('SINGLE_USER_MODE', ${boolToString cfg.singleUserMode});
 
       define('SIMPLE_UPDATE_MODE', ${boolToString cfg.simpleUpdateMode});
-      define('CHECK_FOR_UPDATES', ${boolToString cfg.checkForUpdates});
+
+      // Never check for updates - the running version of the code should be
+      // controlled entirely by the version of TT-RSS active in the current Nix
+      // profile. If TT-RSS updates itself to a version requiring a database
+      // schema upgrade, and then the SystemD tt-rss.service is restarted, the
+      // old code copied from the Nix store will overwrite the updated version,
+      // causing the code to detect the need for a schema "upgrade" (since the
+      // schema version in the database is different than in the code), but the
+      // update schema operation in TT-RSS will do nothing because the schema
+      // version in the database is newer than that in the code.
+      define('CHECK_FOR_UPDATES', false);
 
       define('FORCE_ARTICLE_PURGE', ${toString cfg.forceArticlePurge});
       define('SESSION_COOKIE_LIFETIME', ${toString cfg.sessionCookieLifetime});
@@ -168,6 +185,14 @@ let
           '';
         };
 
+        passwordFile = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The database user's password.
+          '';
+        };
+
         port = mkOption {
           type = types.nullOr types.int;
           default = null;
@@ -399,14 +424,6 @@ let
         '';
       };
 
-      checkForUpdates = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Check for updates automatically if running Git version
-        '';
-      };
-
       enableGZipOutput = mkOption {
         type = types.bool;
         default = true;
@@ -474,26 +491,43 @@ let
     };
   };
 
+  imports = [
+    (mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] ''
+      This option was removed because setting this to true will cause TT-RSS
+      to be unable to start if an automatic update of the code in
+      services.tt-rss.root leads to a database schema upgrade that is not
+      supported by the code active in the Nix store.
+    '')
+  ];
 
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    services.phpfpm.poolConfigs = mkIf (cfg.pool == "${poolName}") {
-      "${poolName}" = ''
-        listen = "${phpfpmSocketName}";
-        listen.owner = nginx
-        listen.group = nginx
-        listen.mode = 0600
-        user = ${cfg.user}
-        pm = dynamic
-        pm.max_children = 75
-        pm.start_servers = 10
-        pm.min_spare_servers = 5
-        pm.max_spare_servers = 20
-        pm.max_requests = 500
-        catch_workers_output = 1
-      '';
+    assertions = [
+      {
+        assertion = cfg.database.password != null -> cfg.database.passwordFile == null;
+        message = "Cannot set both password and passwordFile";
+      }
+    ];
+
+    services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
+      "${poolName}" = {
+        listen = "/var/run/phpfpm/${poolName}.sock";
+        extraConfig = ''
+          listen.owner = nginx
+          listen.group = nginx
+          listen.mode = 0600
+          user = ${cfg.user}
+          pm = dynamic
+          pm.max_children = 75
+          pm.start_servers = 10
+          pm.min_spare_servers = 5
+          pm.max_spare_servers = 20
+          pm.max_requests = 500
+          catch_workers_output = 1
+        '';
+      };
     };
 
     # NOTE: No configuration is done if not using virtual host
@@ -510,7 +544,7 @@ let
           locations."~ \.php$" = {
             extraConfig = ''
               fastcgi_split_path_info ^(.+\.php)(/.+)$;
-              fastcgi_pass unix:${phpfpmSocketName};
+              fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.listen};
               fastcgi_index index.php;
             '';
           };
@@ -528,6 +562,7 @@ let
           callSql = e:
               if cfg.database.type == "pgsql" then ''
                   ${optionalString (cfg.database.password != null) "PGPASSWORD=${cfg.database.password}"} \
+                  ${optionalString (cfg.database.passwordFile != null) "PGPASSWORD=$(cat ${cfg.database.passwordFile})"} \
                   ${pkgs.sudo}/bin/sudo -u ${cfg.user} ${config.services.postgresql.package}/bin/psql \
                     -U ${cfg.database.user} \
                     ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} --port ${toString dbPort}"} \
diff --git a/nixos/modules/services/web-apps/youtrack.nix b/nixos/modules/services/web-apps/youtrack.nix
index 6ad38028a64..691cbdc8d1d 100644
--- a/nixos/modules/services/web-apps/youtrack.nix
+++ b/nixos/modules/services/web-apps/youtrack.nix
@@ -121,6 +121,7 @@ in
       environment.YOUTRACK_JVM_OPTS = "${extraAttr}";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ unixtools.hostname ];
       serviceConfig = {
         Type = "simple";
         User = "youtrack";
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 2d6ed853074..1eac5be2f8d 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -151,7 +151,7 @@ let
 
 
   loggingConf = (if mainCfg.logFormat != "none" then ''
-    ErrorLog ${mainCfg.logDir}/error_log
+    ErrorLog ${mainCfg.logDir}/error.log
 
     LogLevel notice
 
@@ -160,7 +160,7 @@ let
     LogFormat "%{Referer}i -> %U" referer
     LogFormat "%{User-agent}i" agent
 
-    CustomLog ${mainCfg.logDir}/access_log ${mainCfg.logFormat}
+    CustomLog ${mainCfg.logDir}/access.log ${mainCfg.logFormat}
   '' else ''
     ErrorLog /dev/null
   '');
@@ -217,7 +217,7 @@ let
     ) null ([ cfg ] ++ subservices);
 
     documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else
-      pkgs.runCommand "empty" {} "mkdir -p $out";
+      pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out";
 
     documentRootConf = ''
       DocumentRoot "${documentRoot}"
@@ -261,8 +261,8 @@ let
     '' else ""}
 
     ${if !isMainServer && mainCfg.logPerVirtualHost then ''
-      ErrorLog ${mainCfg.logDir}/error_log-${cfg.hostName}
-      CustomLog ${mainCfg.logDir}/access_log-${cfg.hostName} ${cfg.logFormat}
+      ErrorLog ${mainCfg.logDir}/error-${cfg.hostName}.log
+      CustomLog ${mainCfg.logDir}/access-${cfg.hostName}.log ${cfg.logFormat}
     '' else ""}
 
     ${optionalString (robotsTxt != "") ''
@@ -376,6 +376,8 @@ let
     Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf
     Include ${httpd}/conf/extra/httpd-languages.conf
 
+    TraceEnable off
+
     ${if enableSSL then sslConf else ""}
 
     # Fascist default - deny access to everything.
@@ -424,6 +426,7 @@ let
   phpIni = pkgs.runCommand "php.ini"
     { options = concatStringsSep "\n"
         ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices));
+      preferLocalBuild = true;
     }
     ''
       cat ${php}/etc/php.ini > $out
@@ -495,8 +498,8 @@ in
         default = false;
         description = ''
           If enabled, each virtual host gets its own
-          <filename>access_log</filename> and
-          <filename>error_log</filename>, namely suffixed by the
+          <filename>access.log</filename> and
+          <filename>error.log</filename>, namely suffixed by the
           <option>hostName</option> of the virtual host.
         '';
       };
@@ -639,8 +642,8 @@ in
 
       sslProtocols = mkOption {
         type = types.str;
-        default = "All -SSLv2 -SSLv3";
-        example = "All -SSLv2 -SSLv3 -TLSv1";
+        default = "All -SSLv2 -SSLv3 -TLSv1";
+        example = "All -SSLv2 -SSLv3";
         description = "Allowed SSL/TLS protocol versions.";
       };
     }
@@ -684,6 +687,9 @@ in
       ''
         ; Needed for PHP's mail() function.
         sendmail_path = sendmail -t -i
+
+        ; Don't advertise PHP
+        expose_php = off
       '' + optionalString (!isNull config.time.timeZone) ''
 
         ; Apparently PHP doesn't use $TZ.
diff --git a/nixos/modules/services/web-servers/apache-httpd/mediawiki.nix b/nixos/modules/services/web-servers/apache-httpd/mediawiki.nix
index e871ae6ff15..6234478014c 100644
--- a/nixos/modules/services/web-servers/apache-httpd/mediawiki.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/mediawiki.nix
@@ -116,9 +116,10 @@ let
       '';
   };
 
-  mediawikiScripts = pkgs.runCommand "mediawiki-${config.id}-scripts"
-    { buildInputs = [ pkgs.makeWrapper ]; }
-    ''
+  mediawikiScripts = pkgs.runCommand "mediawiki-${config.id}-scripts" {
+      buildInputs = [ pkgs.makeWrapper ];
+      preferLocalBuild = true;
+    } ''
       mkdir -p $out/bin
       for i in changePassword.php createAndPromote.php userOptions.php edit.php nukePage.php update.php; do
         makeWrapper ${php}/bin/php $out/bin/mediawiki-${config.id}-$(basename $i .php) \
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 6c733f093ba..1c9fbe048f8 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -44,7 +44,19 @@ let
     }
   ''));
 
-  configFile = pkgs.writeText "nginx.conf" ''
+  awkFormat = builtins.toFile "awkFormat-nginx.awk" ''
+    awk -f
+    {sub(/^[ \t]+/,"");idx=0}
+    /\{/{ctx++;idx=1}
+    /\}/{ctx--}
+    {id="";for(i=idx;i<ctx;i++)id=sprintf("%s%s", id, "\t");printf "%s%s\n", id, $0}
+  '';
+
+  configFile = pkgs.runCommand "nginx.conf" {} (''
+    awk -f ${awkFormat} ${pre-configFile} | sed '/^\s*$/d' > $out
+  '');
+
+  pre-configFile = pkgs.writeText "pre-nginx.conf" ''
     user ${cfg.user} ${cfg.group};
     error_log ${cfg.logError};
     daemon off;
@@ -182,11 +194,12 @@ let
             then filter (x: x.ssl) defaultListen
             else defaultListen;
 
-        listenString = { addr, port, ssl, ... }:
+        listenString = { addr, port, ssl, extraParameters ? [], ... }:
           "listen ${addr}:${toString port} "
           + optionalString ssl "ssl "
           + optionalString (ssl && vhost.http2) "http2 "
           + optionalString vhost.default "default_server "
+          + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
           + ";";
 
         redirectListen = filter (x: !x.ssl) defaultListen;
@@ -479,8 +492,8 @@ in
 
       sslProtocols = mkOption {
         type = types.str;
-        default = "TLSv1.2";
-        example = "TLSv1 TLSv1.1 TLSv1.2";
+        default = "TLSv1.2 TLSv1.3";
+        example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3";
         description = "Allowed TLS protocol versions.";
       };
 
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 6a50d8ed5cd..15b933c984a 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -31,6 +31,7 @@ with lib;
         addr = mkOption { type = str;  description = "IP address.";  };
         port = mkOption { type = int;  description = "Port number."; default = 80; };
         ssl  = mkOption { type = bool; description = "Enable SSL.";  default = false; };
+        extraParameters = mkOption { type = listOf str; description = "Extra parameters of this listen directive."; default = []; example = [ "reuseport" "deferred" ]; };
       }; });
       default = [];
       example = [
diff --git a/nixos/modules/services/web-servers/phpfpm/default.nix b/nixos/modules/services/web-servers/phpfpm/default.nix
index 152c89a2cae..ffafbc5e92f 100644
--- a/nixos/modules/services/web-servers/phpfpm/default.nix
+++ b/nixos/modules/services/web-servers/phpfpm/default.nix
@@ -14,11 +14,13 @@ let
 
   mapPoolConfig = n: p: {
     phpPackage = cfg.phpPackage;
+    phpOptions = cfg.phpOptions;
     config = p;
   };
 
   mapPool = n: p: {
     phpPackage = p.phpPackage;
+    phpOptions = p.phpOptions;
     config = ''
       listen = ${p.listen}
       ${p.extraConfig}
@@ -35,8 +37,9 @@ let
     ${conf}
   '';
 
-  phpIni = pkgs.runCommand "php.ini" {
-    inherit (cfg) phpPackage phpOptions;
+  phpIni = pool: pkgs.runCommand "php.ini" {
+    inherit (pool) phpPackage phpOptions;
+    preferLocalBuild = true;
     nixDefaults = ''
       sendmail_path = "/run/wrappers/bin/sendmail -t -i"
     '';
@@ -156,6 +159,7 @@ in {
         '';
         serviceConfig = let
           cfgFile = fpmCfgFile pool poolConfig.config;
+          iniFile = phpIni poolConfig;
         in {
           Slice = "phpfpm.slice";
           PrivateDevices = true;
@@ -164,7 +168,7 @@ in {
           # XXX: We need AF_NETLINK to make the sendmail SUID binary from postfix work
           RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
           Type = "notify";
-          ExecStart = "${poolConfig.phpPackage}/bin/php-fpm -y ${cfgFile} -c ${phpIni}";
+          ExecStart = "${poolConfig.phpPackage}/bin/php-fpm -y ${cfgFile} -c ${iniFile}";
           ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
         };
       }
diff --git a/nixos/modules/services/web-servers/phpfpm/pool-options.nix b/nixos/modules/services/web-servers/phpfpm/pool-options.nix
index 40c83cddb95..d9ad7eff71f 100644
--- a/nixos/modules/services/web-servers/phpfpm/pool-options.nix
+++ b/nixos/modules/services/web-servers/phpfpm/pool-options.nix
@@ -25,6 +25,15 @@ with lib; {
       '';
     };
 
+    phpOptions = mkOption {
+      type = types.lines;
+      default = fpmCfg.phpOptions;
+      defaultText = "config.services.phpfpm.phpOptions";
+      description = ''
+        "Options appended to the PHP configuration file <filename>php.ini</filename> used for this PHP-FPM pool."
+      '';
+    };
+
     extraConfig = mkOption {
       type = types.lines;
       example = ''
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
index 700202b1d28..4674ed0177e 100644
--- a/nixos/modules/services/web-servers/traefik.nix
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -8,6 +8,7 @@ let
     if cfg.configFile == null then
       pkgs.runCommand "config.toml" {
         buildInputs = [ pkgs.remarshal ];
+        preferLocalBuild = true;
       } ''
         remarshal -if json -of toml \
           < ${pkgs.writeText "config.json" (builtins.toJSON cfg.configOptions)} \
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
new file mode 100644
index 00000000000..a4a9d370d64
--- /dev/null
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.unit;
+
+  configFile = pkgs.writeText "unit.json" cfg.config;
+
+in {
+  options = {
+    services.unit = {
+      enable = mkEnableOption "Unit App Server";
+      package = mkOption {
+        type = types.package;
+        default = pkgs.unit;
+        defaultText = "pkgs.unit";
+        description = "Unit package to use.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = "unit";
+        description = "User account under which unit runs.";
+      };
+      group = mkOption {
+        type = types.str;
+        default = "unit";
+        description = "Group account under which unit runs.";
+      };
+      stateDir = mkOption {
+        default = "/var/spool/unit";
+        description = "Unit data directory.";
+      };
+      logDir = mkOption {
+        default = "/var/log/unit";
+        description = "Unit log directory.";
+      };
+      config = mkOption {
+        type = types.str;
+        default = ''
+          {
+            "listeners": {},
+            "applications": {}
+          }
+        '';
+        example = literalExample ''
+          {
+            "listeners": {
+              "*:8300": {
+                "application": "example-php-72"
+              }
+            },
+            "applications": {
+              "example-php-72": {
+                "type": "php 7.2",
+                "processes": 4,
+                "user": "nginx",
+                "group": "nginx",
+                "root": "/var/www",
+                "index": "index.php",
+                "options": {
+                  "file": "/etc/php.d/default.ini",
+                  "admin": {
+                    "max_execution_time": "30",
+                    "max_input_time": "30",
+                    "display_errors": "off",
+                    "display_startup_errors": "off",
+                    "open_basedir": "/dev/urandom:/proc/cpuinfo:/proc/meminfo:/etc/ssl/certs:/var/www",
+                    "disable_functions": "exec,passthru,shell_exec,system"
+                  }
+                }
+              }
+            }
+          }
+        '';
+        description = "Unit configuration in JSON format. More details here https://unit.nginx.org/configuration";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.logDir}' 0750 ${cfg.user} ${cfg.group} - -"
+     ];
+
+    systemd.services.unit = {
+      description = "Unit App Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [ curl ];
+      preStart = ''
+        test -f '/run/unit/control.unit.sock' || rm -f '/run/unit/control.unit.sock'
+      '';
+      postStart = ''
+        curl -X PUT --data-binary '@${configFile}' --unix-socket '/run/unit/control.unit.sock' 'http://localhost/config'
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID";
+        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID";
+        ExecStart = ''
+          ${cfg.package}/bin/unitd --control 'unix:/run/unit/control.unit.sock' --pid '/run/unit/unit.pid' \
+                                   --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' --no-daemon \
+                                   --user ${cfg.user} --group ${cfg.group}
+        '';
+        RuntimeDirectory = "unit";
+        RuntimeDirectoryMode = "0750";
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "unit") (singleton {
+      name = "unit";
+      group = cfg.group;
+    });
+
+    users.groups = optionalAttrs (cfg.group == "unit") (singleton {
+      name = "unit";
+    });
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index cce35aa28ba..2b1e9169e5f 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -20,7 +20,7 @@ in
   imports = [
     ./none.nix ./xterm.nix ./xfce.nix ./plasma5.nix ./lumina.nix
     ./lxqt.nix ./enlightenment.nix ./gnome3.nix ./kodi.nix ./maxx.nix
-    ./mate.nix
+    ./mate.nix ./pantheon.nix
   ];
 
   options = {
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index 7544ba4638a..ea01749349d 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -15,7 +15,7 @@ let
     '';
   };
 
-  nixos-gsettings-desktop-schemas = pkgs.runCommand "nixos-gsettings-desktop-schemas" {}
+  nixos-gsettings-desktop-schemas = pkgs.runCommand "nixos-gsettings-desktop-schemas" { preferLocalBuild = true; }
     ''
      mkdir -p $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
      cp -rf ${pkgs.gnome3.gsettings-desktop-schemas}/share/gsettings-schemas/gsettings-desktop-schemas*/glib-2.0/schemas/*.xml $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
@@ -133,6 +133,7 @@ in {
     services.gnome3.gnome-keyring.enable = true;
     services.gnome3.gnome-online-accounts.enable = mkDefault true;
     services.gnome3.gnome-remote-desktop.enable = mkDefault true;
+    services.gnome3.gnome-settings-daemon.enable = true;
     services.gnome3.gnome-terminal-server.enable = mkDefault true;
     services.gnome3.gnome-user-share.enable = mkDefault true;
     services.gnome3.gvfs.enable = true;
@@ -151,8 +152,8 @@ in {
     services.colord.enable = mkDefault true;
     services.packagekit.enable = mkDefault true;
     hardware.bluetooth.enable = mkDefault true;
+    services.hardware.bolt.enable = mkDefault true;
     services.xserver.libinput.enable = mkDefault true; # for controlling touchpad settings via gnome control center
-    services.udev.packages = [ pkgs.gnome3.gnome-settings-daemon ];
     systemd.packages = [ pkgs.gnome3.vino ];
     services.flatpak.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/mate.nix b/nixos/modules/services/x11/desktop-managers/mate.nix
index 4d2fafd1496..bf6685ff7ea 100644
--- a/nixos/modules/services/x11/desktop-managers/mate.nix
+++ b/nixos/modules/services/x11/desktop-managers/mate.nix
@@ -56,9 +56,6 @@ in
 
         export XDG_MENU_PREFIX=mate-
 
-        # Find the mouse
-        export XCURSOR_PATH=~/.icons:${config.system.path}/share/icons
-
         # Let caja find extensions
         export CAJA_EXTENSION_DIRS=$CAJA_EXTENSION_DIRS''${CAJA_EXTENSION_DIRS:+:}${config.system.path}/lib/caja/extensions-2.0
 
@@ -78,9 +75,6 @@ in
         # Add mate-control-center paths to some XDG variables because its schemas are needed by mate-settings-daemon, and mate-settings-daemon is a dependency for mate-control-center (that is, they are mutually recursive)
         ${addToXDGDirs pkgs.mate.mate-control-center}
 
-        # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
-        ${pkgs.xdg-user-dirs}/bin/xdg-user-dirs-update
-
         ${pkgs.mate.mate-session-manager}/bin/mate-session ${optionalString cfg.debug "--debug"} &
         waitPID=$!
       '';
@@ -90,18 +84,27 @@ in
       pkgs.mate.basePackages ++
       (pkgs.gnome3.removePackagesByName
         pkgs.mate.extraPackages
-        config.environment.mate.excludePackages);
-
-    services.dbus.packages = [
-      pkgs.gnome3.dconf
-      pkgs.at-spi2-core
-    ];
-
+        config.environment.mate.excludePackages) ++
+      [
+        pkgs.desktop-file-utils
+        pkgs.glib
+        pkgs.gtk3.out
+        pkgs.shared-mime-info
+        pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
+      ];
+
+    programs.dconf.enable = true;
+    services.gnome3.at-spi2-core.enable = true;
     services.gnome3.gnome-keyring.enable = true;
+    services.gnome3.gnome-settings-daemon.enable = true;
+    services.gnome3.gnome-settings-daemon.package = pkgs.mate.mate-settings-daemon;
+    services.gnome3.gvfs.enable = true;
     services.upower.enable = config.powerManagement.enable;
 
     security.pam.services."mate-screensaver".unixAuth = true;
 
+    environment.variables.GIO_EXTRA_MODULES = [ "${pkgs.gnome3.gvfs}/lib/gio/modules" ];
+
     environment.pathsToLink = [ "/share" ];
   };
 
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
new file mode 100644
index 00000000000..67faddb1ddb
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -0,0 +1,196 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.pantheon;
+
+  nixos-gsettings-desktop-schemas = pkgs.pantheon.elementary-gsettings-schemas.override {
+    extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
+    extraGSettingsOverrides = cfg.extraGSettingsOverrides;
+  };
+
+in
+
+{
+  options = {
+
+    services.xserver.desktopManager.pantheon = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable the pantheon desktop manager";
+      };
+
+      sessionPath = mkOption {
+        default = [];
+        example = literalExample "[ pkgs.gnome3.gpaste ]";
+        description = ''
+          Additional list of packages to be added to the session search path.
+          Useful for GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+        apply = list: list ++
+        [
+          pkgs.pantheon.pantheon-agent-geoclue2
+        ];
+      };
+
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "Additional gsettings overrides.";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "List of packages for which gsettings are overridden.";
+      };
+
+      debug = mkEnableOption "gnome-session debug messages";
+
+    };
+
+    environment.pantheon.excludePackages = mkOption {
+      default = [];
+      example = literalExample "[ pkgs.pantheon.elementary-camera ]";
+      type = types.listOf types.package;
+      description = "Which packages pantheon should exclude from the default environment";
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+
+    services.xserver.displayManager.extraSessionFilePackages = [ pkgs.pantheon.elementary-session-settings ];
+
+    # Ensure lightdm is used when Pantheon is enabled
+    # Without it screen locking will be nonfunctional because of the use of lightlocker
+    services.xserver.displayManager.lightdm.enable = mkDefault true;
+    services.xserver.displayManager.lightdm.greeters.pantheon.enable = mkDefault true;
+
+    # If not set manually Pantheon session cannot be started
+    # Known issue of https://github.com/NixOS/nixpkgs/pull/43992
+    services.xserver.desktopManager.default = mkForce "pantheon";
+
+    services.xserver.displayManager.sessionCommands = ''
+      if test "$XDG_CURRENT_DESKTOP" = "Pantheon"; then
+          ${concatMapStrings (p: ''
+            if [ -d "${p}/share/gsettings-schemas/${p.name}" ]; then
+              export XDG_DATA_DIRS=$XDG_DATA_DIRS''${XDG_DATA_DIRS:+:}${p}/share/gsettings-schemas/${p.name}
+            fi
+
+            if [ -d "${p}/lib/girepository-1.0" ]; then
+              export GI_TYPELIB_PATH=$GI_TYPELIB_PATH''${GI_TYPELIB_PATH:+:}${p}/lib/girepository-1.0
+              export LD_LIBRARY_PATH=$LD_LIBRARY_PATH''${LD_LIBRARY_PATH:+:}${p}/lib
+            fi
+          '') cfg.sessionPath}
+
+          # Makes qt applications look less alien
+          export QT_QPA_PLATFORMTHEME=gtk3
+          export QT_STYLE_OVERRIDE=adwaita
+      fi
+    '';
+
+    hardware.bluetooth.enable = mkDefault true;
+    hardware.pulseaudio.enable = mkDefault true;
+    security.polkit.enable = true;
+    services.accounts-daemon.enable = true;
+    services.bamf.enable = true;
+    services.colord.enable = mkDefault true;
+    services.pantheon.files.enable = mkDefault true;
+    services.tumbler.enable = mkDefault true;
+    services.dbus.packages = mkMerge [
+      ([ pkgs.pantheon.switchboard-plug-power ])
+      (mkIf config.services.printing.enable  ([pkgs.system-config-printer]) )
+    ];
+    services.pantheon.contractor.enable = mkDefault true;
+    services.geoclue2.enable = mkDefault true;
+    # pantheon has pantheon-agent-geoclue2
+    services.geoclue2.enableDemoAgent = false;
+    services.gnome3.at-spi2-core.enable = true;
+    services.gnome3.evolution-data-server.enable = true;
+    services.gnome3.file-roller.enable = mkDefault true;
+    # TODO: gnome-keyring's xdg autostarts will still be in the environment (from elementary-session-settings) if disabled forcefully
+    services.gnome3.gnome-keyring.enable = true;
+    services.gnome3.gnome-settings-daemon.enable = true;
+    services.gnome3.gnome-settings-daemon.package = pkgs.pantheon.elementary-settings-daemon;
+    services.gnome3.gvfs.enable = true;
+    services.gnome3.rygel.enable = mkDefault true;
+    services.gsignond.enable = mkDefault true;
+    services.gsignond.plugins = with pkgs.gsignondPlugins; [ lastfm mail oauth ];
+    services.udisks2.enable = true;
+    services.upower.enable = config.powerManagement.enable;
+    services.xserver.libinput.enable = mkDefault true;
+    services.xserver.updateDbusEnvironment = true;
+    services.zeitgeist.enable = mkDefault true;
+
+    networking.networkmanager.enable = mkDefault true;
+    networking.networkmanager.basePackages =
+      { inherit (pkgs) networkmanager modemmanager wpa_supplicant;
+        inherit (pkgs.gnome3) networkmanager-openvpn networkmanager-vpnc
+                              networkmanager-openconnect networkmanager-fortisslvpn
+                              networkmanager-iodine networkmanager-l2tp; };
+
+    # Override GSettings schemas
+    environment.variables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+    environment.variables.GNOME_SESSION_DEBUG = optionalString cfg.debug "1";
+
+    environment.variables.GIO_EXTRA_MODULES = [
+      "${lib.getLib pkgs.gnome3.dconf}/lib/gio/modules"
+      "${pkgs.gnome3.glib-networking.out}/lib/gio/modules"
+      "${pkgs.gnome3.gvfs}/lib/gio/modules"
+    ];
+
+    environment.pathsToLink = [
+      # FIXME: modules should link subdirs of `/share` rather than relying on this
+      "/share"
+    ];
+
+    environment.systemPackages =
+      pkgs.pantheon.artwork ++ pkgs.pantheon.desktop ++ pkgs.pantheon.services ++ cfg.sessionPath
+      ++ (with pkgs; gnome3.removePackagesByName
+      ([
+        gnome3.geary
+        gnome3.epiphany
+        gnome3.gnome-font-viewer
+        evince
+      ] ++ pantheon.apps) config.environment.pantheon.excludePackages)
+      ++ (with pkgs;
+      [
+        adwaita-qt
+        desktop-file-utils
+        glib
+        glib-networking
+        gnome-menus
+        gnome3.adwaita-icon-theme
+        gnome3.dconf
+        gtk3.out
+        hicolor-icon-theme
+        lightlocker
+        plank
+        qgnomeplatform
+        shared-mime-info
+        sound-theme-freedesktop
+        xdg-user-dirs
+      ]);
+
+    fonts.fonts = with pkgs; [
+      opensans-ttf
+      roboto-mono
+      pantheon.elementary-redacted-script # needed by screenshot-tool
+    ];
+
+    fonts.fontconfig.defaultFonts = {
+      monospace = [ "Roboto Mono" ];
+      sansSerif = [ "Open Sans" ];
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 704cc78c152..a9e55eb846c 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -163,6 +163,8 @@ in
 
           libsForQt56.phonon-backend-gstreamer
           libsForQt5.phonon-backend-gstreamer
+
+          xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
         ]
 
         ++ lib.optionals cfg.enableQt4Support [ pkgs.phonon-backend-gstreamer ]
@@ -175,9 +177,9 @@ in
         ++ lib.optional config.services.colord.enable colord-kde
         ++ lib.optionals config.services.samba.enable [ kdenetwork-filesharing pkgs.samba ];
 
-      environment.pathsToLink = [ 
+      environment.pathsToLink = [
         # FIXME: modules should link subdirs of `/share` rather than relying on this
-        "/share" 
+        "/share"
       ];
 
       environment.etc = singleton {
@@ -224,7 +226,29 @@ in
       security.pam.services.slim.enableKwallet = true;
 
       # Update the start menu for each user that is currently logged in
-      system.userActivationScripts.plasmaSetup = "${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5";
+      system.userActivationScripts.plasmaSetup = ''
+        # The KDE icon cache is supposed to update itself
+        # automatically, but it uses the timestamp on the icon
+        # theme directory as a trigger.  Since in Nix the
+        # timestamp is always the same, this doesn't work.  So as
+        # a workaround, nuke the icon cache on login.  This isn't
+        # perfect, since it may require logging out after
+        # installing new applications to update the cache.
+        # See http://lists-archives.org/kde-devel/26175-what-when-will-icon-cache-refresh.html
+        rm -fv $HOME/.cache/icon-cache.kcache
+
+        # xdg-desktop-settings generates this empty file but
+        # it makes kbuildsyscoca5 fail silently. To fix this
+        # remove that menu if it exists.
+        rm -fv $HOME/.config/menus/applications-merged/xdg-desktop-menu-dummy.menu
+
+        # Remove the kbuildsyscoca5 cache. It will be regenerated
+        # immediately after. This is necessary for kbuildsyscoca5 to
+        # recognize that software that has been removed.
+        rm -fv $HOME/.cache/ksycoca*
+
+        ${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5
+      '';
     })
   ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index dabf09418da..6852154378d 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -53,7 +53,7 @@ in
 
       # Supplies some abstract icons such as:
       # utilities-terminal, accessories-text-editor
-      gnome3.defaultIconTheme
+      gnome3.adwaita-icon-theme
 
       hicolor-icon-theme
       tango-icon-theme
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
index 7c794b1ba17..de128809ce3 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
@@ -12,9 +12,10 @@ let
 
   # We need a few things in the environment for the greeter to run with
   # fonts/icons.
-  wrappedEnsoGreeter = pkgs.runCommand "lightdm-enso-os-greeter"
-    { buildInputs = [ pkgs.makeWrapper ]; }
-    ''
+  wrappedEnsoGreeter = pkgs.runCommand "lightdm-enso-os-greeter" {
+      buildInputs = [ pkgs.makeWrapper ];
+      preferLocalBuild = true;
+    } ''
       # This wrapper ensures that we actually get themes
       makeWrapper ${pkgs.lightdm-enso-os-greeter}/bin/pantheon-greeter \
         $out/greeter \
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
index d1ee076e918..5b280b02423 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
@@ -6,19 +6,22 @@ let
 
   dmcfg = config.services.xserver.displayManager;
   ldmcfg = dmcfg.lightdm;
+  xcfg = config.services.xserver;
   cfg = ldmcfg.greeters.gtk;
 
   inherit (pkgs) writeText;
 
   theme = cfg.theme.package;
   icons = cfg.iconTheme.package;
+  cursors = cfg.cursorTheme.package;
 
   # The default greeter provided with this expression is the GTK greeter.
   # Again, we need a few things in the environment for the greeter to run with
   # fonts/icons.
-  wrappedGtkGreeter = pkgs.runCommand "lightdm-gtk-greeter"
-    { buildInputs = [ pkgs.makeWrapper ]; }
-    ''
+  wrappedGtkGreeter = pkgs.runCommand "lightdm-gtk-greeter" {
+      buildInputs = [ pkgs.makeWrapper ];
+      preferLocalBuild = true;
+    } ''
       # This wrapper ensures that we actually get themes
       makeWrapper ${pkgs.lightdm_gtk_greeter}/sbin/lightdm-gtk-greeter \
         $out/greeter \
@@ -28,7 +31,8 @@ let
         --set GTK_EXE_PREFIX "${theme}" \
         --set GTK_DATA_PREFIX "${theme}" \
         --set XDG_DATA_DIRS "${theme}/share:${icons}/share" \
-        --set XDG_CONFIG_HOME "${theme}/share"
+        --set XDG_CONFIG_HOME "${theme}/share" \
+        --set XCURSOR_PATH "${cursors}/share/icons"
 
       cat - > $out/lightdm-gtk-greeter.desktop << EOF
       [Desktop Entry]
@@ -44,9 +48,12 @@ let
     [greeter]
     theme-name = ${cfg.theme.name}
     icon-theme-name = ${cfg.iconTheme.name}
+    cursor-theme-name = ${cfg.cursorTheme.name}
+    cursor-theme-size = ${toString cfg.cursorTheme.size}
     background = ${ldmcfg.background}
     ${optionalString (cfg.clock-format != null) "clock-format = ${cfg.clock-format}"}
     ${optionalString (cfg.indicators != null) "indicators = ${concatStringsSep ";" cfg.indicators}"}
+    ${optionalString (xcfg.dpi != null) "xft-dpi=${toString xcfg.dpi}"}
     ${cfg.extraConfig}
     '';
 
@@ -89,8 +96,8 @@ in
 
         package = mkOption {
           type = types.package;
-          default = pkgs.gnome3.defaultIconTheme;
-          defaultText = "pkgs.gnome3.defaultIconTheme";
+          default = pkgs.gnome3.adwaita-icon-theme;
+          defaultText = "pkgs.gnome3.adwaita-icon-theme";
           description = ''
             The package path that contains the icon theme given in the name option.
           '';
@@ -106,6 +113,33 @@ in
 
       };
 
+      cursorTheme = {
+
+        package = mkOption {
+          default = pkgs.gnome3.adwaita-icon-theme;
+          defaultText = "pkgs.gnome3.adwaita-icon-theme";
+          description = ''
+            The package path that contains the cursor theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "Adwaita";
+          description = ''
+            Name of the cursor theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+
+        size = mkOption {
+          type = types.int;
+          default = 16;
+          description = ''
+            Size of the cursor theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+      };
+
       clock-format = mkOption {
         type = types.nullOr types.str;
         default = null;
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
new file mode 100644
index 00000000000..05011b999f2
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.pantheon;
+
+  xgreeters = pkgs.linkFarm "pantheon-greeter-xgreeters" [{
+    path = "${pkgs.pantheon.elementary-greeter}/share/xgreeters/io.elementary.greeter.desktop";
+    name = "io.elementary.greeter.desktop";
+  }];
+
+in
+{
+  options = {
+
+    services.xserver.displayManager.lightdm.greeters.pantheon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable elementary-greeter as the lightdm greeter.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+
+    services.xserver.displayManager.lightdm.greeters.gtk.enable = false;
+
+    services.xserver.displayManager.lightdm.greeter = mkDefault {
+      package = xgreeters;
+      name = "io.elementary.greeter";
+    };
+
+    environment.etc."lightdm/io.elementary.greeter.conf".source = "${pkgs.pantheon.elementary-greeter}/etc/lightdm/io.elementary.greeter.conf";
+    environment.etc."wingpanel.d/io.elementary.greeter.whitelist".source = "${pkgs.pantheon.elementary-default-settings}/etc/wingpanel.d/io.elementary.greeter.whitelist";
+
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 567c3ac3454..3ab4f26399f 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -81,6 +81,7 @@ in
     ./lightdm-greeters/gtk.nix
     ./lightdm-greeters/mini.nix
     ./lightdm-greeters/enso-os.nix
+    ./lightdm-greeters/pantheon.nix
   ];
 
   options = {
diff --git a/nixos/modules/services/x11/display-managers/slim.nix b/nixos/modules/services/x11/display-managers/slim.nix
index 4e411c8ceb0..124660a43f0 100644
--- a/nixos/modules/services/x11/display-managers/slim.nix
+++ b/nixos/modules/services/x11/display-managers/slim.nix
@@ -28,7 +28,7 @@ let
   # Unpack the SLiM theme, or use the default.
   slimThemesDir =
     let
-      unpackedTheme = pkgs.runCommand "slim-theme" {}
+      unpackedTheme = pkgs.runCommand "slim-theme" { preferLocalBuild = true; }
         ''
           mkdir -p $out
           cd $out
diff --git a/nixos/modules/services/x11/gdk-pixbuf.nix b/nixos/modules/services/x11/gdk-pixbuf.nix
index 58faa8e2f9d..2dc8eabd95a 100644
--- a/nixos/modules/services/x11/gdk-pixbuf.nix
+++ b/nixos/modules/services/x11/gdk-pixbuf.nix
@@ -10,7 +10,7 @@ let
 
   # Generate the cache file by running gdk-pixbuf-query-loaders for each
   # package and concatenating the results.
-  loadersCache = pkgs.runCommand "gdk-pixbuf-loaders.cache" {} ''
+  loadersCache = pkgs.runCommand "gdk-pixbuf-loaders.cache" { preferLocalBuild = true; } ''
     (
       for package in ${concatStringsSep " " effectivePackages}; do
         module_dir="$package/${pkgs.gdk_pixbuf.moduleDir}"
diff --git a/nixos/modules/services/x11/xautolock.nix b/nixos/modules/services/x11/xautolock.nix
index a614559970e..cbe000058dc 100644
--- a/nixos/modules/services/x11/xautolock.nix
+++ b/nixos/modules/services/x11/xautolock.nix
@@ -21,7 +21,7 @@ in
           type = types.int;
 
           description = ''
-            Idle time to wait until xautolock locks the computer.
+            Idle time (in minutes) to wait until xautolock locks the computer.
           '';
         };
 
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 34ae8c11a3f..c4d5b6a9cde 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -61,7 +61,9 @@ let
       '';
       description = ''
         Extra lines to append to the <literal>Monitor</literal> section
-        verbatim.
+        verbatim. Available options are documented in the MONITOR section in
+        <citerefentry><refentrytitle>xorg.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry>.
       '';
     };
   };
@@ -113,6 +115,7 @@ let
     { xfs = optionalString (cfg.useXFS != false)
         ''FontPath "${toString cfg.useXFS}"'';
       inherit (cfg) config;
+      preferLocalBuild = true;
     }
       ''
         echo 'Section "Files"' >> $out
@@ -240,7 +243,7 @@ in
       videoDrivers = mkOption {
         type = types.listOf types.str;
         # !!! We'd like "nv" here, but it segfaults the X server.
-        default = [ "ati" "cirrus" "intel" "vesa" "vmware" "modesetting" ];
+        default = [ "ati" "cirrus" "vesa" "vmware" "modesetting" ];
         example = [
           "ati_unfree" "amdgpu" "amdgpu-pro"
           "nv" "nvidia" "nvidiaLegacy340" "nvidiaLegacy304"
@@ -632,7 +635,7 @@ in
 
     environment.pathsToLink = [ "/share/X11" ];
 
-    xdg = { 
+    xdg = {
       autostart.enable = true;
       menus.enable = true;
       mime.enable = true;
@@ -705,6 +708,7 @@ in
     system.extraDependencies = singleton (pkgs.runCommand "xkb-validated" {
       inherit (cfg) xkbModel layout xkbVariant xkbOptions;
       nativeBuildInputs = [ pkgs.xkbvalidate ];
+      preferLocalBuild = true;
     } ''
       validate "$xkbModel" "$layout" "$xkbVariant" "$xkbOptions"
       touch "$out"
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index a560af5ce96..5c88d27b6c6 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -130,11 +130,9 @@ let
 
   failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
 
-  showWarnings = res: fold (w: x: builtins.trace "warning: ${w}" x) res config.warnings;
-
   baseSystemAssertWarn = if failedAssertions != []
     then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
-    else showWarnings baseSystem;
+    else showWarnings config.warnings baseSystem;
 
   # Replace runtime dependencies
   system = fold ({ oldDependency, newDependency }: drv:
diff --git a/nixos/modules/system/boot/kernel_config.nix b/nixos/modules/system/boot/kernel_config.nix
new file mode 100644
index 00000000000..fbbd0982b2c
--- /dev/null
+++ b/nixos/modules/system/boot/kernel_config.nix
@@ -0,0 +1,137 @@
+{ lib, config, ... }:
+
+with lib;
+let
+  findWinner = candidates: winner:
+    any (x: x == winner) candidates;
+
+  # winners is an ordered list where first item wins over 2nd etc
+  mergeAnswer = winners: locs: defs:
+    let
+      values = map (x: x.value) defs;
+      freeformAnswer = intersectLists values winners;
+      inter = intersectLists values winners;
+      winner = head winners;
+    in
+    if defs == [] then abort "This case should never happen."
+    else if winner == [] then abort "Give a valid list of winner"
+    else if inter == [] then mergeOneOption locs defs
+    else if findWinner values winner then
+      winner
+    else
+      mergeAnswer (tail winners) locs defs;
+
+  mergeFalseByDefault = locs: defs:
+    if defs == [] then abort "This case should never happen."
+    else if any (x: x == false) defs then false
+    else true;
+
+  kernelItem = types.submodule {
+    options = {
+      tristate = mkOption {
+        type = types.enum [ "y" "m" "n" null ] // {
+          merge = mergeAnswer [ "y" "m" "n" ];
+        };
+        default = null;
+        internal = true;
+        visible = true;
+        description = ''
+          Use this field for tristate kernel options expecting a "y" or "m" or "n".
+        '';
+      };
+
+      freeform = mkOption {
+        type = types.nullOr types.str // {
+          merge = mergeEqualOption;
+        };
+        default = null;
+        example = ''MMC_BLOCK_MINORS.freeform = "32";'';
+        description = ''
+          Freeform description of a kernel configuration item value.
+        '';
+      };
+
+      optional = mkOption {
+        type = types.bool // { merge = mergeFalseByDefault; };
+        default = false;
+        description = ''
+          Wether option should generate a failure when unused.
+        '';
+      };
+    };
+  };
+
+  mkValue = with lib; val:
+  let
+    isNumber = c: elem c ["0" "1" "2" "3" "4" "5" "6" "7" "8" "9"];
+
+  in
+    if (val == "") then "\"\""
+    else if val == "y" || val == "m" || val == "n" then val
+    else if all isNumber (stringToCharacters val) then val
+    else if substring 0 2 val == "0x" then val
+    else val; # FIXME: fix quoting one day
+
+
+  # generate nix intermediate kernel config file of the form
+  #
+  #       VIRTIO_MMIO m
+  #       VIRTIO_BLK y
+  #       VIRTIO_CONSOLE n
+  #       NET_9P_VIRTIO? y
+  #
+  # Borrowed from copumpkin https://github.com/NixOS/nixpkgs/pull/12158
+  # returns a string, expr should be an attribute set
+  # Use mkValuePreprocess to preprocess option values, aka mark 'modules' as 'yes' or vice-versa
+  # use the identity if you don't want to override the configured values
+  generateNixKConf = exprs:
+  let
+    mkConfigLine = key: item:
+      let
+        val = if item.freeform != null then item.freeform else item.tristate;
+      in
+        if val == null
+          then ""
+          else if (item.optional)
+            then "${key}? ${mkValue val}\n"
+            else "${key} ${mkValue val}\n";
+
+    mkConf = cfg: concatStrings (mapAttrsToList mkConfigLine cfg);
+  in mkConf exprs;
+
+in
+{
+
+  options = {
+
+    intermediateNixConfig = mkOption {
+      readOnly = true;
+      type = types.lines;
+      example = ''
+        USB? y
+        DEBUG n
+      '';
+      description = ''
+        The result of converting the structured kernel configuration in settings
+        to an intermediate string that can be parsed by generate-config.pl to
+        answer the kernel `make defconfig`.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.attrsOf kernelItem;
+      example = literalExample '' with lib.kernel; {
+        "9P_NET" = yes;
+        USB = optional yes;
+        MMC_BLOCK_MINORS = freeform "32";
+      }'';
+      description = ''
+        Structured kernel configuration.
+      '';
+    };
+  };
+
+  config = {
+    intermediateNixConfig = generateNixKConf config.settings;
+  };
+}
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
index 047651dc642..7db60daa60b 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
@@ -33,7 +33,7 @@ let
       avoid_warnings=1
     '' + optional isAarch64 ''
       # Boot in 64-bit mode.
-      arm_control=0x200
+      arm_64bit=1
     '' + (if cfg.uboot.enable then ''
       kernel=u-boot-rpi.bin
     '' else ''
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index c8ea1401528..9984a97bbdd 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -196,9 +196,10 @@ let
     ''; # */
 
 
-  udevRules = pkgs.runCommand "udev-rules"
-    { allowedReferences = [ extraUtils ]; }
-    ''
+  udevRules = pkgs.runCommand "udev-rules" {
+      allowedReferences = [ extraUtils ];
+      preferLocalBuild = true;
+    } ''
       mkdir -p $out
 
       echo 'ENV{LD_LIBRARY_PATH}="${extraUtils}/lib"' > $out/00-env.rules
@@ -298,9 +299,10 @@ let
         { object = pkgs.writeText "mdadm.conf" config.boot.initrd.mdadmConf;
           symlink = "/etc/mdadm.conf";
         }
-        { object = pkgs.runCommand "initrd-kmod-blacklist-ubuntu"
-            { src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf"; }
-            ''
+        { object = pkgs.runCommand "initrd-kmod-blacklist-ubuntu" {
+              src = "${pkgs.kmod-blacklist-ubuntu}/modprobe.conf";
+              preferLocalBuild = true;
+            } ''
               target=$out
               ${pkgs.buildPackages.perl}/bin/perl -0pe 's/## file: iwlwifi.conf(.+?)##/##/s;' $src > $out
             '';
@@ -525,16 +527,18 @@ in
       };
 
     fileSystems = mkOption {
-      options.neededForBoot = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          If set, this file system will be mounted in the initial
-          ramdisk.  By default, this applies to the root file system
-          and to the file system containing
-          <filename>/nix/store</filename>.
-        '';
-      };
+      type = with lib.types; loaOf (submodule {
+        options.neededForBoot = mkOption {
+          default = false;
+          type = types.bool;
+          description = ''
+            If set, this file system will be mounted in the initial
+            ramdisk.  By default, this applies to the root file system
+            and to the file system containing
+            <filename>/nix/store</filename>.
+          '';
+        };
+      });
     };
 
   };
diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix
index 5f2bec5c34a..63f974b704f 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/modules/system/boot/systemd-unit-options.nix
@@ -210,6 +210,15 @@ in rec {
       '';
     };
 
+    startLimitIntervalSec = mkOption {
+       type = types.int;
+       description = ''
+         Configure unit start rate limiting. Units which are started
+         more than burst times within an interval time interval are
+         not permitted to start any more.
+       '';
+    };
+
   };
 
 
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 860268ab23a..18ee2ef1b8f 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -193,7 +193,7 @@ let
     let mkScriptName =  s: "unit-script-" + (replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape s) );
     in  pkgs.writeTextFile { name = mkScriptName name; executable = true; inherit text; };
 
-  unitConfig = { config, ... }: {
+  unitConfig = { config, options, ... }: {
     config = {
       unitConfig =
         optionalAttrs (config.requires != [])
@@ -219,7 +219,9 @@ let
         // optionalAttrs (config.documentation != []) {
           Documentation = toString config.documentation; }
         // optionalAttrs (config.onFailure != []) {
-          OnFailure = toString config.onFailure;
+          OnFailure = toString config.onFailure; }
+        // optionalAttrs (options.startLimitIntervalSec.isDefined) {
+          StartLimitIntervalSec = toString config.startLimitIntervalSec;
         };
     };
   };
@@ -319,7 +321,9 @@ let
             in concatMapStrings (n:
               let s = optionalString (env."${n}" != null)
                 "Environment=${builtins.toJSON "${n}=${env.${n}}"}\n";
-              in if stringLength s >= 2048 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
+              # systemd max line length is now 1MiB
+              # https://github.com/systemd/systemd/commit/e6dde451a51dc5aaa7f4d98d39b8fe735f73d2af
+              in if stringLength s >= 1048576 then throw "The value of the environment variable ‘${n}’ in systemd service ‘${name}.service’ is too long." else s) (attrNames env)}
           ${if def.reloadIfChanged then ''
             X-ReloadIfChanged=true
           '' else if !def.restartIfChanged then ''
@@ -589,7 +593,7 @@ in
 
     services.journald.forwardToSyslog = mkOption {
       default = config.services.rsyslogd.enable || config.services.syslog-ng.enable;
-      defaultText = "config.services.rsyslogd.enable || config.services.syslog-ng.enable";
+      defaultText = "services.rsyslogd.enable || services.syslog-ng.enable";
       type = types.bool;
       description = ''
         Whether to forward log messages to syslog.
@@ -646,6 +650,19 @@ in
       '';
     };
 
+    services.logind.lidSwitchExternalPower = mkOption {
+      default = config.services.logind.lidSwitch;
+      defaultText = "services.logind.lidSwitch";
+      example = "ignore";
+      type = logindHandlerType;
+
+      description = ''
+        Specifies what to do when the laptop lid is closed and the system is
+        on external power. By default use the same action as specified in
+        services.logind.lidSwitch.
+      '';
+    };
+
     systemd.user.extraConfig = mkOption {
       default = "";
       type = types.lines;
@@ -743,7 +760,10 @@ in
     environment.etc = let
       # generate contents for /etc/systemd/system-generators from
       # systemd.generators and systemd.generator-packages
-      generators = pkgs.runCommand "system-generators" { packages = cfg.generator-packages; } ''
+      generators = pkgs.runCommand "system-generators" {
+          preferLocalBuild = true;
+          packages = cfg.generator-packages;
+        } ''
         mkdir -p $out
         for package in $packages
         do
@@ -793,6 +813,7 @@ in
         KillUserProcesses=${if config.services.logind.killUserProcesses then "yes" else "no"}
         HandleLidSwitch=${config.services.logind.lidSwitch}
         HandleLidSwitchDocked=${config.services.logind.lidSwitchDocked}
+        HandleLidSwitchExternalPower=${config.services.logind.lidSwitchExternalPower}
         ${config.services.logind.extraConfig}
       '';
 
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix
index 7b756b70e2f..91f4ae79ee9 100644
--- a/nixos/modules/tasks/auto-upgrade.nix
+++ b/nixos/modules/tasks/auto-upgrade.nix
@@ -57,7 +57,7 @@ let cfg = config.system.autoUpgrade; in
 
   };
 
-  config = {
+  config = lib.mkIf cfg.enable {
 
     system.autoUpgrade.flags =
       [ "--no-build-output" ]
@@ -78,13 +78,13 @@ let cfg = config.system.autoUpgrade; in
           HOME = "/root";
         } // config.networking.proxy.envVars;
 
-      path = [ pkgs.gnutar pkgs.xz.bin config.nix.package.out ];
+      path = [ pkgs.gnutar pkgs.xz.bin pkgs.gitMinimal config.nix.package.out ];
 
       script = ''
         ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch ${toString cfg.flags}
       '';
 
-      startAt = optional cfg.enable cfg.dates;
+      startAt = cfg.dates;
     };
 
   };
diff --git a/nixos/modules/tasks/encrypted-devices.nix b/nixos/modules/tasks/encrypted-devices.nix
index 11ed5d7e4d0..2c9231f5523 100644
--- a/nixos/modules/tasks/encrypted-devices.nix
+++ b/nixos/modules/tasks/encrypted-devices.nix
@@ -12,7 +12,7 @@ let
 
   encryptedFSOptions = {
 
-    encrypted = {
+    options.encrypted = {
       enable = mkOption {
         default = false;
         type = types.bool;
@@ -47,10 +47,10 @@ in
 
   options = {
     fileSystems = mkOption {
-      options = [encryptedFSOptions];
+      type = with lib.types; loaOf (submodule encryptedFSOptions);
     };
     swapDevices = mkOption {
-      options = [encryptedFSOptions];
+      type = with lib.types; listOf (submodule encryptedFSOptions);
     };
   };
 
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index 9e4057b5089..07f8214cea2 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -231,7 +231,7 @@ in
         fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" ];
         skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
         # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
-        escape = string: builtins.replaceStrings [ " " ] [ "\\040" ] string;
+        escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
       in ''
         # This is a generated file.  Do not edit!
         #
diff --git a/nixos/modules/tasks/filesystems/vboxsf.nix b/nixos/modules/tasks/filesystems/vboxsf.nix
index 87f1984f084..5497194f6a8 100644
--- a/nixos/modules/tasks/filesystems/vboxsf.nix
+++ b/nixos/modules/tasks/filesystems/vboxsf.nix
@@ -6,7 +6,7 @@ let
 
   inInitrd = any (fs: fs == "vboxsf") config.boot.initrd.supportedFilesystems;
 
-  package = pkgs.runCommand "mount.vboxsf" {} ''
+  package = pkgs.runCommand "mount.vboxsf" { preferLocalBuild = true; } ''
     mkdir -p $out/bin
     cp ${pkgs.linuxPackages.virtualboxGuestAdditions}/bin/mount.vboxsf $out/bin
   '';
diff --git a/nixos/modules/tasks/kbd.nix b/nixos/modules/tasks/kbd.nix
index fbe42b8e8f0..6d34f897d18 100644
--- a/nixos/modules/tasks/kbd.nix
+++ b/nixos/modules/tasks/kbd.nix
@@ -15,6 +15,7 @@ let
   optimizedKeymap = pkgs.runCommand "keymap" {
     nativeBuildInputs = [ pkgs.buildPackages.kbd ];
     LOADKEYS_KEYMAP_PATH = "${kbdEnv}/share/keymaps/**";
+    preferLocalBuild = true;
   } ''
     loadkeys -b ${optionalString isUnicode "-u"} "${config.i18n.consoleKeyMap}" > $out
   '';
diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix
index 93dfefdce90..c12ada7a030 100644
--- a/nixos/modules/tasks/network-interfaces-scripted.nix
+++ b/nixos/modules/tasks/network-interfaces-scripted.nix
@@ -103,16 +103,18 @@ let
 
             script =
               ''
-                # Set the static DNS configuration, if given.
-                ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF
-                ${optionalString (cfg.nameservers != [] && cfg.domain != null) ''
-                  domain ${cfg.domain}
+                ${optionalString (!config.environment.etc?"resolv.conf") ''
+                  # Set the static DNS configuration, if given.
+                  ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF
+                  ${optionalString (cfg.nameservers != [] && cfg.domain != null) ''
+                    domain ${cfg.domain}
+                  ''}
+                  ${optionalString (cfg.search != []) ("search " + concatStringsSep " " cfg.search)}
+                  ${flip concatMapStrings cfg.nameservers (ns: ''
+                    nameserver ${ns}
+                  '')}
+                  EOF
                 ''}
-                ${optionalString (cfg.search != []) ("search " + concatStringsSep " " cfg.search)}
-                ${flip concatMapStrings cfg.nameservers (ns: ''
-                  nameserver ${ns}
-                '')}
-                EOF
 
                 # Set the default gateway.
                 ${optionalString (cfg.defaultGateway != null && cfg.defaultGateway.address != "") ''
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 815523093dd..f9b0eb330bf 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -995,7 +995,7 @@ in
       '';
 
     environment.etc."hostid" = mkIf (cfg.hostId != null)
-      { source = pkgs.runCommand "gen-hostid" {} ''
+      { source = pkgs.runCommand "gen-hostid" { preferLocalBuild = true; } ''
           hi="${cfg.hostId}"
           ${if pkgs.stdenv.isBigEndian then ''
             echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > $out
diff --git a/nixos/modules/testing/service-runner.nix b/nixos/modules/testing/service-runner.nix
index 5ead75788e5..17d5e337690 100644
--- a/nixos/modules/testing/service-runner.nix
+++ b/nixos/modules/testing/service-runner.nix
@@ -92,23 +92,24 @@ let
       exit($mainRes & 127 ? 255 : $mainRes << 8);
     '';
 
+  opts = { config, name, ... }: {
+    options.runner = mkOption {
+    internal = true;
+    description = ''
+        A script that runs the service outside of systemd,
+        useful for testing or for using NixOS services outside
+        of NixOS.
+    '';
+    };
+    config.runner = makeScript name config;
+  };
+
 in
 
 {
   options = {
     systemd.services = mkOption {
-      options =
-        { config, name, ... }:
-        { options.runner = mkOption {
-            internal = true;
-            description = ''
-              A script that runs the service outside of systemd,
-              useful for testing or for using NixOS services outside
-              of NixOS.
-            '';
-          };
-          config.runner = makeScript name config;
-        };
+      type = with types; attrsOf (submodule opts);
     };
   };
 }
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index 9015200beea..d67790702f1 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -8,7 +8,13 @@
 
 with lib;
 
-let cfg = config.ec2; in
+let
+  cfg = config.ec2;
+  metadataFetcher = import ./ec2-metadata-fetcher.nix {
+    targetRoot = "$targetRoot/";
+    wgetExtraOptions = "-q";
+  };
+in
 
 {
   imports = [ ../profiles/headless.nix ./ec2-data.nix ./amazon-init.nix ];
@@ -25,6 +31,7 @@ let cfg = config.ec2; in
 
     fileSystems."/" = {
       device = "/dev/disk/by-label/nixos";
+      fsType = "ext4";
       autoResize = true;
     };
 
@@ -61,26 +68,7 @@ let cfg = config.ec2; in
     # Nix operations.
     boot.initrd.postMountCommands =
       ''
-        metaDir=$targetRoot/etc/ec2-metadata
-        mkdir -m 0755 -p "$metaDir"
-
-        echo "getting EC2 instance metadata..."
-
-        if ! [ -e "$metaDir/ami-manifest-path" ]; then
-          wget -q -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
-        fi
-
-        if ! [ -e "$metaDir/user-data" ]; then
-          wget -q -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
-        fi
-
-        if ! [ -e "$metaDir/hostname" ]; then
-          wget -q -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
-        fi
-
-        if ! [ -e "$metaDir/public-keys-0-openssh-key" ]; then
-          wget -q -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
-        fi
+        ${metadataFetcher}
 
         diskNr=0
         diskForUnionfs=
diff --git a/nixos/modules/virtualisation/cloudstack-config.nix b/nixos/modules/virtualisation/cloudstack-config.nix
new file mode 100644
index 00000000000..81c54567627
--- /dev/null
+++ b/nixos/modules/virtualisation/cloudstack-config.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+  ];
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+    };
+
+    boot.growPartition = true;
+    boot.kernelParams = [ "console=tty0" ];
+    boot.loader.grub.device = "/dev/vda";
+    boot.loader.timeout = 0;
+
+    # Allow root logins
+    services.openssh = {
+      enable = true;
+      permitRootLogin = "prohibit-password";
+    };
+
+    # Cloud-init configuration.
+    services.cloud-init.enable = true;
+    # Wget is needed for setting password. This is of little use as
+    # root password login is disabled above.
+    environment.systemPackages = [ pkgs.wget ];
+    # Only enable CloudStack datasource for faster boot speed.
+    environment.etc."cloud/cloud.cfg.d/99_cloudstack.cfg".text = ''
+      datasource:
+        CloudStack: {}
+        None: {}
+      datasource_list: ["CloudStack"]
+    '';
+  };
+}
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index c2e6e9f6a13..7c9909ae278 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -36,7 +36,7 @@ let
         #! ${pkgs.runtimeShell} -e
 
         # Initialise the container side of the veth pair.
-        if [ -n "$HOST_ADDRESS" ] || [ -n "$LOCAL_ADDRESS" ]; then
+        if [ -n "$HOST_ADDRESS" ] || [ -n "$LOCAL_ADDRESS" ] || [ -n "$HOST_BRIDGE" ]; then
 
           ip link set host0 name eth0
           ip link set dev eth0 up
@@ -90,18 +90,20 @@ let
 
       if [ -n "$HOST_ADDRESS" ] || [ -n "$LOCAL_ADDRESS" ]; then
         extraFlags+=" --network-veth"
-        if [ -n "$HOST_BRIDGE" ]; then
-          extraFlags+=" --network-bridge=$HOST_BRIDGE"
-        fi
-        if [ -n "$HOST_PORT" ]; then
-          OIFS=$IFS
-          IFS=","
-          for i in $HOST_PORT
-          do
-              extraFlags+=" --port=$i"
-          done
-          IFS=$OIFS
-        fi
+      fi
+
+      if [ -n "$HOST_PORT" ]; then
+        OIFS=$IFS
+        IFS=","
+        for i in $HOST_PORT
+        do
+            extraFlags+=" --port=$i"
+        done
+        IFS=$OIFS
+      fi
+
+      if [ -n "$HOST_BRIDGE" ]; then
+        extraFlags+=" --network-bridge=$HOST_BRIDGE"
       fi
 
       extraFlags+=" ${concatStringsSep " " (mapAttrsToList nspawnExtraVethArgs cfg.extraVeths)}"
diff --git a/nixos/modules/virtualisation/docker-preloader.nix b/nixos/modules/virtualisation/docker-preloader.nix
index faa94f53d98..6ab83058dee 100644
--- a/nixos/modules/virtualisation/docker-preloader.nix
+++ b/nixos/modules/virtualisation/docker-preloader.nix
@@ -78,12 +78,11 @@ in
     };
   };
 
-  config = {
+  config = mkIf (cfg.dockerPreloader.images != []) {
     assertions = [{
       # If docker.storageDriver is null, Docker choose the storage
       # driver. So, in this case, we cannot be sure overlay2 is used.
-      assertion = cfg.dockerPreloader.images == []
-        || cfg.docker.storageDriver == "overlay2"
+      assertion = cfg.docker.storageDriver == "overlay2"
         || cfg.docker.storageDriver == "overlay"
         || cfg.docker.storageDriver == null;
       message = "The Docker image Preloader only works with overlay2 storage driver!";
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index a1a32c1c59a..4ee84c5268e 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -52,6 +52,15 @@ in
           '';
       };
 
+    enableNvidia =
+      mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable nvidia-docker wrapper, supporting NVIDIA GPUs inside docker containers.
+        '';
+      };
+
     liveRestore =
       mkOption {
         type = types.bool;
@@ -140,7 +149,8 @@ in
   ###### implementation
 
   config = mkIf cfg.enable (mkMerge [{
-      environment.systemPackages = [ cfg.package ];
+      environment.systemPackages = [ cfg.package ]
+        ++ optional cfg.enableNvidia pkgs.nvidia-docker;
       users.groups.docker.gid = config.ids.gids.docker;
       systemd.packages = [ cfg.package ];
 
@@ -157,6 +167,7 @@ in
                 --log-driver=${cfg.logDriver} \
                 ${optionalString (cfg.storageDriver != null) "--storage-driver=${cfg.storageDriver}"} \
                 ${optionalString cfg.liveRestore "--live-restore" } \
+                ${optionalString cfg.enableNvidia "--add-runtime nvidia=${pkgs.nvidia-docker}/bin/nvidia-container-runtime" } \
                 ${cfg.extraOptions}
             ''];
           ExecReload=[
@@ -165,7 +176,8 @@ in
           ];
         };
 
-        path = [ pkgs.kmod ] ++ (optional (cfg.storageDriver == "zfs") pkgs.zfs);
+        path = [ pkgs.kmod ] ++ optional (cfg.storageDriver == "zfs") pkgs.zfs
+          ++ optional cfg.enableNvidia pkgs.nvidia-docker;
       };
 
       systemd.sockets.docker = {
@@ -179,7 +191,6 @@ in
         };
       };
 
-
       systemd.services.docker-prune = {
         description = "Prune docker resources";
 
@@ -194,7 +205,15 @@ in
 
         startAt = optional cfg.autoPrune.enable cfg.autoPrune.dates;
       };
+
+      assertions = [
+        { assertion = cfg.enableNvidia -> config.hardware.opengl.driSupport32Bit or false;
+          message = "Option enableNvidia requires 32bit support libraries";
+        }];
     }
+    (mkIf cfg.enableNvidia {
+      environment.etc."nvidia-container-runtime/config.toml".source = "${pkgs.nvidia-docker}/etc/config.toml";
+    })
   ]);
 
   imports = [
diff --git a/nixos/modules/virtualisation/ec2-metadata-fetcher.nix b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
new file mode 100644
index 00000000000..b531787c31a
--- /dev/null
+++ b/nixos/modules/virtualisation/ec2-metadata-fetcher.nix
@@ -0,0 +1,23 @@
+{ targetRoot, wgetExtraOptions }:
+''
+  metaDir=${targetRoot}etc/ec2-metadata
+  mkdir -m 0755 -p "$metaDir"
+
+  echo "getting EC2 instance metadata..."
+
+  if ! [ -e "$metaDir/ami-manifest-path" ]; then
+    wget ${wgetExtraOptions} -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
+  fi
+
+  if ! [ -e "$metaDir/user-data" ]; then
+    wget ${wgetExtraOptions} -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data && chmod 600 "$metaDir/user-data"
+  fi
+
+  if ! [ -e "$metaDir/hostname" ]; then
+    wget ${wgetExtraOptions} -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
+  fi
+
+  if ! [ -e "$metaDir/public-keys-0-openssh-key" ]; then
+    wget ${wgetExtraOptions} -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key
+  fi
+''
diff --git a/nixos/modules/virtualisation/nova-config.nix b/nixos/modules/virtualisation/nova-config.nix
deleted file mode 100644
index cecf2a3f144..00000000000
--- a/nixos/modules/virtualisation/nova-config.nix
+++ /dev/null
@@ -1,60 +0,0 @@
-{ lib, ... }:
-
-with lib;
-
-{
-  imports = [
-    ../profiles/qemu-guest.nix
-    ../profiles/headless.nix
-  ];
-
-  config = {
-    fileSystems."/" = {
-      device = "/dev/disk/by-label/nixos";
-      autoResize = true;
-    };
-
-    boot.growPartition = true;
-    boot.kernelParams = [ "console=ttyS0" ];
-    boot.loader.grub.device = "/dev/vda";
-    boot.loader.timeout = 0;
-
-    # Allow root logins
-    services.openssh = {
-      enable = true;
-      permitRootLogin = "prohibit-password";
-      passwordAuthentication = mkDefault false;
-    };
-
-    services.cloud-init.enable = true;
-
-    # Put /tmp and /var on /ephemeral0, which has a lot more space.
-    # Unfortunately we can't do this with the `fileSystems' option
-    # because it has no support for creating the source of a bind
-    # mount.  Also, "move" /nix to /ephemeral0 by layering a unionfs-fuse
-    # mount on top of it so we have a lot more space for Nix operations.
-
-    /*
-    boot.initrd.postMountCommands =
-      ''
-        mkdir -m 1777 -p $targetRoot/ephemeral0/tmp
-        mkdir -m 1777 -p $targetRoot/tmp
-        mount --bind $targetRoot/ephemeral0/tmp $targetRoot/tmp
-
-        mkdir -m 755 -p $targetRoot/ephemeral0/var
-        mkdir -m 755 -p $targetRoot/var
-        mount --bind $targetRoot/ephemeral0/var $targetRoot/var
-
-        mkdir -p /unionfs-chroot/ro-nix
-        mount --rbind $targetRoot/nix /unionfs-chroot/ro-nix
-
-        mkdir -p /unionfs-chroot/rw-nix
-        mkdir -m 755 -p $targetRoot/ephemeral0/nix
-        mount --rbind $targetRoot/ephemeral0/nix /unionfs-chroot/rw-nix
-        unionfs -o allow_other,cow,nonempty,chroot=/unionfs-chroot,max_files=32768 /rw-nix=RW:/ro-nix=RO $targetRoot/nix
-      '';
-
-      boot.initrd.supportedFilesystems = [ "unionfs-fuse" ];
-    */
-  };
-}
diff --git a/nixos/modules/virtualisation/openstack-config.nix b/nixos/modules/virtualisation/openstack-config.nix
new file mode 100644
index 00000000000..d5e862da0ea
--- /dev/null
+++ b/nixos/modules/virtualisation/openstack-config.nix
@@ -0,0 +1,57 @@
+{ pkgs, lib, ... }:
+
+with lib;
+
+let
+  metadataFetcher = import ./ec2-metadata-fetcher.nix {
+    targetRoot = "/";
+    wgetExtraOptions = "--retry-connrefused";
+  };
+in
+{
+  imports = [
+    ../profiles/qemu-guest.nix
+    ../profiles/headless.nix
+    # The Openstack Metadata service exposes data on an EC2 API also.
+    ./ec2-data.nix
+    ./amazon-init.nix
+  ];
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-label/nixos";
+      autoResize = true;
+    };
+
+    boot.growPartition = true;
+    boot.kernelParams = [ "console=ttyS0" ];
+    boot.loader.grub.device = "/dev/vda";
+    boot.loader.timeout = 0;
+
+    # Allow root logins
+    services.openssh = {
+      enable = true;
+      permitRootLogin = "prohibit-password";
+      passwordAuthentication = mkDefault false;
+    };
+
+    # Force getting the hostname from Openstack metadata.
+    networking.hostName = mkDefault "";
+
+    systemd.services.openstack-init = {
+      path = [ pkgs.wget ];
+      description = "Fetch Metadata on startup";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "apply-ec2-data.service" "amazon-init.service"];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      script = metadataFetcher;
+      restartIfChanged = false;
+      unitConfig.X-StopOnRemoval = false;
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/virtualbox-host.nix b/nixos/modules/virtualisation/virtualbox-host.nix
index 60779579402..6f737018174 100644
--- a/nixos/modules/virtualisation/virtualbox-host.nix
+++ b/nixos/modules/virtualisation/virtualbox-host.nix
@@ -83,6 +83,8 @@ in
   };
 
   config = mkIf cfg.enable (mkMerge [{
+    warnings = mkIf (config.nixpkgs.config.virtualbox.enableExtensionPack or false)
+      ["'nixpkgs.virtualbox.enableExtensionPack' has no effect, please use 'virtualisation.virtualbox.host.enableExtensionPack'"];
     boot.kernelModules = [ "vboxdrv" "vboxnetadp" "vboxnetflt" ];
     boot.extraModulePackages = [ kernelModules ];
     environment.systemPackages = [ virtualbox ];
diff --git a/nixos/modules/virtualisation/vmware-guest.nix b/nixos/modules/virtualisation/vmware-guest.nix
index 15c78f14c52..d18778f8158 100644
--- a/nixos/modules/virtualisation/vmware-guest.nix
+++ b/nixos/modules/virtualisation/vmware-guest.nix
@@ -3,19 +3,17 @@
 with lib;
 
 let
-  cfg = config.services.vmwareGuest;
+  cfg = config.virtualisation.vmware.guest;
   open-vm-tools = if cfg.headless then pkgs.open-vm-tools-headless else pkgs.open-vm-tools;
   xf86inputvmmouse = pkgs.xorg.xf86inputvmmouse;
 in
 {
-  options = {
-    services.vmwareGuest = {
-      enable = mkEnableOption "VMWare Guest Support";
-      headless = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to disable X11-related features.";
-      };
+  options.virtualisation.vmware.guest = {
+    enable = mkEnableOption "VMWare Guest Support";
+    headless = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to disable X11-related features.";
     };
   };
 
@@ -25,6 +23,8 @@ in
       message = "VMWare guest is not currently supported on ${pkgs.stdenv.hostPlatform.system}";
     } ];
 
+    boot.initrd.kernelModules = [ "vmw_pvscsi" ];
+
     environment.systemPackages = [ open-vm-tools ];
 
     systemd.services.vmware =
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index ea8b92e94f0..6c313f8dd3e 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -68,6 +68,7 @@ in rec {
         (all nixos.tests.firefox)
         (all nixos.tests.firewall)
         (except ["aarch64-linux"] nixos.tests.gnome3)
+        (except ["aarch64-linux"] nixos.tests.pantheon)
         nixos.tests.installer.zfsroot.x86_64-linux or [] # ZFS is 64bit only
         (except ["aarch64-linux"] nixos.tests.installer.lvm)
         (except ["aarch64-linux"] nixos.tests.installer.luksroot)
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 7bc2f3076f1..69510c1420f 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -25,6 +25,7 @@ in
   atd = handleTest ./atd.nix {};
   avahi = handleTest ./avahi.nix {};
   bcachefs = handleTestOn ["x86_64-linux"] ./bcachefs.nix {}; # linux-4.18.2018.10.12 is unsupported on aarch64
+  beanstalkd = handleTest ./beanstalkd.nix {};
   beegfs = handleTestOn ["x86_64-linux"] ./beegfs.nix {}; # beegfs is unsupported on aarch64
   bind = handleTest ./bind.nix {};
   bittorrent = handleTest ./bittorrent.nix {};
@@ -42,6 +43,7 @@ in
   clickhouse = handleTest ./clickhouse.nix {};
   cloud-init = handleTest ./cloud-init.nix {};
   codimd = handleTest ./codimd.nix {};
+  colord = handleTest ./colord.nix {};
   containers-bridge = handleTest ./containers-bridge.nix {};
   containers-extra_veth = handleTest ./containers-extra_veth.nix {};
   containers-hosts = handleTest ./containers-hosts.nix {};
@@ -73,6 +75,8 @@ in
   ferm = handleTest ./ferm.nix {};
   firefox = handleTest ./firefox.nix {};
   firewall = handleTest ./firewall.nix {};
+  fish = handleTest ./fish.nix {};
+  flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
   flatpak = handleTest ./flatpak.nix {};
   fsck = handleTest ./fsck.nix {};
   fwupd = handleTestOn ["x86_64-linux"] ./fwupd.nix {}; # libsmbios is unsupported on aarch64
@@ -114,6 +118,7 @@ in
   kernel-latest = handleTest ./kernel-latest.nix {};
   kernel-lts = handleTest ./kernel-lts.nix {};
   keymap = handleTest ./keymap.nix {};
+  knot = handleTest ./knot.nix {};
   kubernetes.dns = handleTestOn ["x86_64-linux"] ./kubernetes/dns.nix {};
   # kubernetes.e2e should eventually replace kubernetes.rbac when it works
   #kubernetes.e2e = handleTestOn ["x86_64-linux"] ./kubernetes/e2e.nix {};
@@ -129,6 +134,7 @@ in
   matrix-synapse = handleTest ./matrix-synapse.nix {};
   memcached = handleTest ./memcached.nix {};
   mesos = handleTest ./mesos.nix {};
+  minio = handleTest ./minio.nix {};
   misc = handleTest ./misc.nix {};
   mongodb = handleTest ./mongodb.nix {};
   morty = handleTest ./morty.nix {};
@@ -142,6 +148,8 @@ in
   nat.firewall = handleTest ./nat.nix { withFirewall = true; };
   nat.firewall-conntrack = handleTest ./nat.nix { withFirewall = true; withConntrackHelpers = true; };
   nat.standalone = handleTest ./nat.nix { withFirewall = false; };
+  ndppd = handleTest ./ndppd.nix {};
+  neo4j = handleTest ./neo4j.nix {};
   netdata = handleTest ./netdata.nix {};
   networking.networkd = handleTest ./networking.nix { networkd = true; };
   networking.scripted = handleTest ./networking.nix { networkd = false; };
@@ -153,15 +161,23 @@ in
   nfs4 = handleTest ./nfs.nix { version = 4; };
   nghttpx = handleTest ./nghttpx.nix {};
   nginx = handleTest ./nginx.nix {};
+  nginx-sso = handleTest ./nginx-sso.nix {};
   nix-ssh-serve = handleTest ./nix-ssh-serve.nix {};
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
   nsd = handleTest ./nsd.nix {};
   openldap = handleTest ./openldap.nix {};
   opensmtpd = handleTest ./opensmtpd.nix {};
   openssh = handleTest ./openssh.nix {};
+  # openstack-image-userdata doesn't work in a sandbox as the simulated openstack instance needs network access
+  #openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {};
+  openstack-image-metadata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).metadata or {};
   osquery = handleTest ./osquery.nix {};
+  osrm-backend = handleTest ./osrm-backend.nix {};
   ostree = handleTest ./ostree.nix {};
+  overlayfs = handleTest ./overlayfs.nix {};
   pam-oath-login = handleTest ./pam-oath-login.nix {};
+  pam-u2f = handleTest ./pam-u2f.nix {};
+  pantheon = handleTest ./pantheon.nix {};
   peerflix = handleTest ./peerflix.nix {};
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
diff --git a/nixos/tests/avahi.nix b/nixos/tests/avahi.nix
index dfb60998941..56b21a40155 100644
--- a/nixos/tests/avahi.nix
+++ b/nixos/tests/avahi.nix
@@ -2,7 +2,7 @@
 import ./make-test.nix ({ pkgs, ... } : {
   name = "avahi";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes = let
diff --git a/nixos/tests/beanstalkd.nix b/nixos/tests/beanstalkd.nix
new file mode 100644
index 00000000000..fa2fbc2c92a
--- /dev/null
+++ b/nixos/tests/beanstalkd.nix
@@ -0,0 +1,45 @@
+import ./make-test.nix ({ pkgs, lib, ... }:
+
+let
+  pythonEnv = pkgs.python3.withPackages (p: [p.beanstalkc]);
+
+  produce = pkgs.writeScript "produce.py" ''
+    #!${pythonEnv.interpreter}
+    import beanstalkc
+
+    queue = beanstalkc.Connection(host='localhost', port=11300, parse_yaml=False);
+    queue.put(b'this is a job')
+    queue.put(b'this is another job')
+  '';
+
+  consume = pkgs.writeScript "consume.py" ''
+    #!${pythonEnv.interpreter}
+    import beanstalkc
+
+    queue = beanstalkc.Connection(host='localhost', port=11300, parse_yaml=False);
+
+    job = queue.reserve(timeout=0)
+    print(job.body.decode('utf-8'))
+    job.delete()
+  '';
+
+in
+{
+  name = "beanstalkd";
+  meta.maintainers = [ lib.maintainers.aanderse ];
+
+  machine =
+    { ... }:
+    { services.beanstalkd.enable = true;
+    };
+
+  testScript = ''
+    startAll;
+
+    $machine->waitForUnit('beanstalkd.service');
+
+    $machine->succeed("${produce}");
+    $machine->succeed("${consume}") eq "this is a job\n" or die;
+    $machine->succeed("${consume}") eq "this is another job\n" or die;
+  '';
+})
diff --git a/nixos/tests/bittorrent.nix b/nixos/tests/bittorrent.nix
index 8977be9b859..3b1169a1b7f 100644
--- a/nixos/tests/bittorrent.nix
+++ b/nixos/tests/bittorrent.nix
@@ -23,7 +23,7 @@ in
 {
   name = "bittorrent";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ domenkozar eelco chaoflow rob wkennington bobvanderlinden ];
+    maintainers = [ domenkozar eelco rob bobvanderlinden ];
   };
 
   nodes =
diff --git a/nixos/tests/colord.nix b/nixos/tests/colord.nix
new file mode 100644
index 00000000000..ce38aaca4bf
--- /dev/null
+++ b/nixos/tests/colord.nix
@@ -0,0 +1,18 @@
+# run installed tests
+import ./make-test.nix ({ pkgs, ... }:
+
+{
+  name = "colord";
+
+  meta = {
+    maintainers = pkgs.colord.meta.maintainers;
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
+  };
+
+  testScript = ''
+    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.colord.installedTests}/share'");
+  '';
+})
diff --git a/nixos/tests/common/ec2.nix b/nixos/tests/common/ec2.nix
new file mode 100644
index 00000000000..1e69b63191a
--- /dev/null
+++ b/nixos/tests/common/ec2.nix
@@ -0,0 +1,49 @@
+{ pkgs, makeTest }:
+
+with pkgs.lib;
+
+{
+  makeEc2Test = { name, image, userData, script, hostname ? "ec2-instance", sshPublicKey ? null }:
+    let
+      metaData = pkgs.stdenv.mkDerivation {
+        name = "metadata";
+        buildCommand = ''
+          mkdir -p $out/1.0/meta-data
+          ln -s ${pkgs.writeText "userData" userData} $out/1.0/user-data
+          echo "${hostname}" > $out/1.0/meta-data/hostname
+          echo "(unknown)" > $out/1.0/meta-data/ami-manifest-path
+        '' + optionalString (sshPublicKey != null) ''
+          mkdir -p $out/1.0/meta-data/public-keys/0
+          ln -s ${pkgs.writeText "sshPublicKey" sshPublicKey} $out/1.0/meta-data/public-keys/0/openssh-key
+        '';
+      };
+    in makeTest {
+      name = "ec2-" + name;
+      nodes = {};
+      testScript =
+        ''
+          my $imageDir = ($ENV{'TMPDIR'} // "/tmp") . "/vm-state-machine";
+          mkdir $imageDir, 0700;
+          my $diskImage = "$imageDir/machine.qcow2";
+          system("qemu-img create -f qcow2 -o backing_file=${image}/nixos.qcow2 $diskImage") == 0 or die;
+          system("qemu-img resize $diskImage 10G") == 0 or die;
+
+          # Note: we use net=169.0.0.0/8 rather than
+          # net=169.254.0.0/16 to prevent dhcpcd from getting horribly
+          # confused. (It would get a DHCP lease in the 169.254.*
+          # range, which it would then configure and prompty delete
+          # again when it deletes link-local addresses.) Ideally we'd
+          # turn off the DHCP server, but qemu does not have an option
+          # to do that.
+          my $startCommand = "qemu-kvm -m 768";
+          $startCommand .= " -device virtio-net-pci,netdev=vlan0";
+          $startCommand .= " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'";
+          $startCommand .= " -drive file=$diskImage,if=virtio,werror=report";
+          $startCommand .= " \$QEMU_OPTS";
+
+          my $machine = createMachine({ startCommand => $startCommand });
+
+          ${script}
+        '';
+    };
+}
diff --git a/nixos/tests/containers-bridge.nix b/nixos/tests/containers-bridge.nix
index 777cf9a7e7f..38db64eb793 100644
--- a/nixos/tests/containers-bridge.nix
+++ b/nixos/tests/containers-bridge.nix
@@ -10,7 +10,7 @@ in
 import ./make-test.nix ({ pkgs, ...} : {
   name = "containers-bridge";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aristid aszlig eelco chaoflow kampfschlaefer ];
+    maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
   machine =
@@ -45,6 +45,19 @@ import ./make-test.nix ({ pkgs, ...} : {
             };
         };
 
+      containers.web-noip =
+        {
+          autoStart = true;
+          privateNetwork = true;
+          hostBridge = "br0";
+          config =
+            { services.httpd.enable = true;
+              services.httpd.adminAddr = "foo@example.org";
+              networking.firewall.allowedTCPPorts = [ 80 ];
+            };
+        };
+
+
       virtualisation.pathsInNixDB = [ pkgs.stdenv ];
     };
 
@@ -56,6 +69,10 @@ import ./make-test.nix ({ pkgs, ...} : {
       # Start the webserver container.
       $machine->succeed("nixos-container status webserver") =~ /up/ or die;
 
+      # Check if bridges exist inside containers
+      $machine->succeed("nixos-container run webserver -- ip link show eth0");
+      $machine->succeed("nixos-container run web-noip -- ip link show eth0");
+
       "${containerIp}" =~ /([^\/]+)\/([0-9+])/;
       my $ip = $1;
       chomp $ip;
diff --git a/nixos/tests/containers-imperative.nix b/nixos/tests/containers-imperative.nix
index 782095a09da..0c101037aa7 100644
--- a/nixos/tests/containers-imperative.nix
+++ b/nixos/tests/containers-imperative.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "containers-imperative";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aristid aszlig eelco chaoflow kampfschlaefer ];
+    maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
   machine =
diff --git a/nixos/tests/containers-ipv4.nix b/nixos/tests/containers-ipv4.nix
index 5f83a33b107..ace68ff2df8 100644
--- a/nixos/tests/containers-ipv4.nix
+++ b/nixos/tests/containers-ipv4.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "containers-ipv4";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aristid aszlig eelco chaoflow kampfschlaefer ];
+    maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
   machine =
diff --git a/nixos/tests/containers-ipv6.nix b/nixos/tests/containers-ipv6.nix
index 5866e51b731..a9499d192bd 100644
--- a/nixos/tests/containers-ipv6.nix
+++ b/nixos/tests/containers-ipv6.nix
@@ -8,7 +8,7 @@ in
 import ./make-test.nix ({ pkgs, ...} : {
   name = "containers-ipv6";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aristid aszlig eelco chaoflow kampfschlaefer ];
+    maintainers = [ aristid aszlig eelco kampfschlaefer ];
   };
 
   machine =
diff --git a/nixos/tests/containers-portforward.nix b/nixos/tests/containers-portforward.nix
index d2dda926fc0..ec8e9629c21 100644
--- a/nixos/tests/containers-portforward.nix
+++ b/nixos/tests/containers-portforward.nix
@@ -10,7 +10,7 @@ in
 import ./make-test.nix ({ pkgs, ...} : {
   name = "containers-portforward";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aristid aszlig eelco chaoflow kampfschlaefer ianwookim ];
+    maintainers = [ aristid aszlig eelco kampfschlaefer ianwookim ];
   };
 
   machine =
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index 58f106314ab..399e4d4e428 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -34,8 +34,8 @@ import ./make-test.nix ({ pkgs, ... }: {
 
       # To test the pullImage tool
       $docker->succeed("docker load --input='${pkgs.dockerTools.examples.nixFromDockerHub}'");
-      $docker->succeed("docker run --rm nixos/nix:1.11 nix-store --version");
-      $docker->succeed("docker rmi nixos/nix:1.11");
+      $docker->succeed("docker run --rm nixos/nix:2.2.1 nix-store --version");
+      $docker->succeed("docker rmi nixos/nix:2.2.1");
 
       # To test runAsRoot and entry point
       $docker->succeed("docker load --input='${pkgs.dockerTools.examples.nginx}'");
diff --git a/nixos/tests/ec2.nix b/nixos/tests/ec2.nix
index ed6bf7da988..384fce67c22 100644
--- a/nixos/tests/ec2.nix
+++ b/nixos/tests/ec2.nix
@@ -6,6 +6,8 @@
 with import ../lib/testing.nix { inherit system pkgs; };
 with pkgs.lib;
 
+with import common/ec2.nix { inherit makeTest pkgs; };
+
 let
   image =
     (import ../lib/eval-config.nix {
@@ -39,65 +41,14 @@ let
       ];
     }).config.system.build.amazonImage;
 
-  makeEc2Test = { name, userData, script, hostname ? "ec2-instance", sshPublicKey ? null }:
-    let
-      metaData = pkgs.stdenv.mkDerivation {
-        name = "metadata";
-        buildCommand = ''
-          mkdir -p $out/1.0/meta-data
-          ln -s ${pkgs.writeText "userData" userData} $out/1.0/user-data
-          echo "${hostname}" > $out/1.0/meta-data/hostname
-          echo "(unknown)" > $out/1.0/meta-data/ami-manifest-path
-        '' + optionalString (sshPublicKey != null) ''
-          mkdir -p $out/1.0/meta-data/public-keys/0
-          ln -s ${pkgs.writeText "sshPublicKey" sshPublicKey} $out/1.0/meta-data/public-keys/0/openssh-key
-        '';
-      };
-    in makeTest {
-      name = "ec2-" + name;
-      nodes = {};
-      testScript =
-        ''
-          my $imageDir = ($ENV{'TMPDIR'} // "/tmp") . "/vm-state-machine";
-          mkdir $imageDir, 0700;
-          my $diskImage = "$imageDir/machine.qcow2";
-          system("qemu-img create -f qcow2 -o backing_file=${image}/nixos.qcow2 $diskImage") == 0 or die;
-          system("qemu-img resize $diskImage 10G") == 0 or die;
-
-          # Note: we use net=169.0.0.0/8 rather than
-          # net=169.254.0.0/16 to prevent dhcpcd from getting horribly
-          # confused. (It would get a DHCP lease in the 169.254.*
-          # range, which it would then configure and prompty delete
-          # again when it deletes link-local addresses.) Ideally we'd
-          # turn off the DHCP server, but qemu does not have an option
-          # to do that.
-          my $startCommand = "qemu-kvm -m 768";
-          $startCommand .= " -device virtio-net-pci,netdev=vlan0";
-          $startCommand .= " -netdev 'user,id=vlan0,net=169.0.0.0/8,guestfwd=tcp:169.254.169.254:80-cmd:${pkgs.micro-httpd}/bin/micro_httpd ${metaData}'";
-          $startCommand .= " -drive file=$diskImage,if=virtio,werror=report";
-          $startCommand .= " \$QEMU_OPTS";
-
-          my $machine = createMachine({ startCommand => $startCommand });
-
-          ${script}
-        '';
-    };
-
-  snakeOilPrivateKey = ''
-    -----BEGIN OPENSSH PRIVATE KEY-----
-    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
-    QyNTUxOQAAACDEPmwZv5dDPrMUaq0dDP+6eBTTe+QNrz14KBEIdhHd1QAAAJDufJ4S7nye
-    EgAAAAtzc2gtZWQyNTUxOQAAACDEPmwZv5dDPrMUaq0dDP+6eBTTe+QNrz14KBEIdhHd1Q
-    AAAECgwbDlYATM5/jypuptb0GF/+zWZcJfoVIFBG3LQeRyGsQ+bBm/l0M+sxRqrR0M/7p4
-    FNN75A2vPXgoEQh2Ed3VAAAADEVDMiB0ZXN0IGtleQE=
-    -----END OPENSSH PRIVATE KEY-----
-  '';
-
-  snakeOilPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMQ+bBm/l0M+sxRqrR0M/7p4FNN75A2vPXgoEQh2Ed3V EC2 test key";
+  sshKeys = import ./ssh-keys.nix pkgs;
+  snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
+  snakeOilPublicKey = sshKeys.snakeOilPublicKey;
 
 in {
   boot-ec2-nixops = makeEc2Test {
     name         = "nixops-userdata";
+    inherit image;
     sshPublicKey = snakeOilPublicKey; # That's right folks! My user's key is also the host key!
 
     userData = ''
@@ -142,6 +93,7 @@ in {
 
   boot-ec2-config = makeEc2Test {
     name         = "config-userdata";
+    inherit image;
     sshPublicKey = snakeOilPublicKey;
 
     # ### http://nixos.org/channels/nixos-unstable nixos
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
index d787ac97300..e7ae023f3ff 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -13,7 +13,7 @@ let
   mkElkTest = name : elk : makeTest {
     inherit name;
     meta = with pkgs.stdenv.lib.maintainers; {
-      maintainers = [ eelco chaoflow offline basvandijk ];
+      maintainers = [ eelco offline basvandijk ];
     };
     nodes = {
       one =
diff --git a/nixos/tests/firefox.nix b/nixos/tests/firefox.nix
index 58a80243ea9..f5b946a0881 100644
--- a/nixos/tests/firefox.nix
+++ b/nixos/tests/firefox.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ... }: {
   name = "firefox";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow shlevy ];
+    maintainers = [ eelco shlevy ];
   };
 
   machine =
diff --git a/nixos/tests/firewall.nix b/nixos/tests/firewall.nix
index 7207a880d8e..fcf758910e0 100644
--- a/nixos/tests/firewall.nix
+++ b/nixos/tests/firewall.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ( { pkgs, ... } : {
   name = "firewall";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes =
diff --git a/nixos/tests/fish.nix b/nixos/tests/fish.nix
new file mode 100644
index 00000000000..97c4e8e37ac
--- /dev/null
+++ b/nixos/tests/fish.nix
@@ -0,0 +1,21 @@
+import ./make-test.nix ({ pkgs, ... }: {
+  name = "fish";
+
+  machine =
+    { pkgs, ... }:
+
+    {
+      programs.fish.enable = true;
+      environment.systemPackages = with pkgs; [
+        coreutils
+        procps # kill collides with coreutils' to test https://github.com/NixOS/nixpkgs/issues/56432
+      ];
+    };
+
+  testScript =
+    ''
+      $machine->waitForFile("/etc/fish/generated_completions/coreutils.fish");
+      $machine->waitForFile("/etc/fish/generated_completions/kill.fish");
+      $machine->succeed("fish -ic 'echo \$fish_complete_path' | grep -q '/share/fish/completions /etc/fish/generated_completions /root/.local/share/fish/generated_completions\$'");
+    '';
+})
diff --git a/nixos/tests/flannel.nix b/nixos/tests/flannel.nix
index fb66fe28209..0b261a68477 100644
--- a/nixos/tests/flannel.nix
+++ b/nixos/tests/flannel.nix
@@ -21,8 +21,9 @@ import ./make-test.nix ({ pkgs, ...} : rec {
       services = {
         etcd = {
           enable = true;
-          listenClientUrls = ["http://etcd:2379"];
-          listenPeerUrls = ["http://etcd:2380"];
+          listenClientUrls = ["http://0.0.0.0:2379"]; # requires ip-address for binding
+          listenPeerUrls = ["http://0.0.0.0:2380"]; # requires ip-address for binding
+          advertiseClientUrls = ["http://etcd:2379"];
           initialAdvertisePeerUrls = ["http://etcd:2379"];
           initialCluster = ["etcd=http://etcd:2379"];
         };
diff --git a/nixos/tests/fwupd.nix b/nixos/tests/fwupd.nix
index 2e64149b2db..88dac8ccbcd 100644
--- a/nixos/tests/fwupd.nix
+++ b/nixos/tests/fwupd.nix
@@ -8,6 +8,8 @@ import ./make-test.nix ({ pkgs, ... }: {
 
   machine = { pkgs, ... }: {
     services.fwupd.enable = true;
+    services.fwupd.blacklistPlugins = []; # don't blacklist test plugin
+    services.fwupd.enableTestRemote = true;
     environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
     environment.variables.XDG_DATA_DIRS = [ "${pkgs.fwupd.installedTests}/share" ];
     virtualisation.memorySize = 768;
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
index 28e6479e9cb..d43efc3687a 100644
--- a/nixos/tests/gitea.nix
+++ b/nixos/tests/gitea.nix
@@ -45,7 +45,7 @@ with pkgs.lib;
       {
         services.gitea.enable = true;
         services.gitea.database.type = "postgres";
-        services.gitea.database.password = "secret";
+        services.gitea.database.passwordFile = pkgs.writeText "db-password" "secret";
       };
 
     testScript = ''
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
index 16e0dd723ec..ac733461932 100644
--- a/nixos/tests/gitlab.nix
+++ b/nixos/tests/gitlab.nix
@@ -1,5 +1,8 @@
 # This test runs gitlab and checks if it works
 
+let
+  initialRootPassword = "notproduction";
+in
 import ./make-test.nix ({ pkgs, lib, ...} : with lib; {
   name = "gitlab";
   meta = with pkgs.stdenv.lib.maintainers; {
@@ -27,7 +30,7 @@ import ./make-test.nix ({ pkgs, lib, ...} : with lib; {
       services.gitlab = {
         enable = true;
         databasePassword = "dbPassword";
-        initialRootPassword = "notproduction";
+        inherit initialRootPassword;
         smtp.enable = true;
         secrets = {
           secret = "secret";
@@ -69,7 +72,27 @@ import ./make-test.nix ({ pkgs, lib, ...} : with lib; {
     };
   };
 
-  testScript = ''
+  testScript =
+  let
+    auth = pkgs.writeText "auth.json" (builtins.toJSON {
+      grant_type = "password";
+      username = "root";
+      password = initialRootPassword;
+    });
+
+    createProject = pkgs.writeText "create-project.json" (builtins.toJSON {
+      name = "test";
+    });
+
+    putFile = pkgs.writeText "put-file.json" (builtins.toJSON {
+      branch = "master";
+      author_email = "author@example.com";
+      author_name = "Firstname Lastname";
+      content = "some content";
+      commit_message = "create a new file";
+    });
+  in
+  ''
     $gitlab->start();
     $gitlab->waitForUnit("gitaly.service");
     $gitlab->waitForUnit("gitlab-workhorse.service");
@@ -78,6 +101,13 @@ import ./make-test.nix ({ pkgs, lib, ...} : with lib; {
     $gitlab->waitForFile("/var/gitlab/state/tmp/sockets/gitlab.socket");
     $gitlab->waitUntilSucceeds("curl -sSf http://gitlab/users/sign_in");
     $gitlab->succeed("curl -isSf http://gitlab  | grep -i location | grep -q http://gitlab/users/sign_in");
-    $gitlab->succeed("${pkgs.sudo}/bin/sudo -u gitlab -H gitlab-rake gitlab:check 1>&2")
+    $gitlab->succeed("${pkgs.sudo}/bin/sudo -u gitlab -H gitlab-rake gitlab:check 1>&2");
+    $gitlab->succeed("echo \"Authorization: Bearer \$(curl -X POST -H 'Content-Type: application/json' -d @${auth} http://gitlab/oauth/token | ${pkgs.jq}/bin/jq -r '.access_token')\" >/tmp/headers");
+    $gitlab->succeed("curl -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${createProject} http://gitlab/api/v4/projects");
+    $gitlab->succeed("curl -X POST -H 'Content-Type: application/json' -H @/tmp/headers -d @${putFile} http://gitlab/api/v4/projects/1/repository/files/some-file.txt");
+    $gitlab->succeed("curl -H @/tmp/headers http://gitlab/api/v4/projects/1/repository/archive.tar.gz > /tmp/archive.tar.gz");
+    $gitlab->succeed("curl -H @/tmp/headers http://gitlab/api/v4/projects/1/repository/archive.tar.bz2 > /tmp/archive.tar.bz2");
+    $gitlab->succeed("test -s /tmp/archive.tar.gz");
+    $gitlab->succeed("test -s /tmp/archive.tar.bz2");
   '';
 })
diff --git a/nixos/tests/gnome3.nix b/nixos/tests/gnome3.nix
index 95694ea4828..b58c9e5a0e3 100644
--- a/nixos/tests/gnome3.nix
+++ b/nixos/tests/gnome3.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "gnome3";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ domenkozar eelco chaoflow lethalman ];
+    maintainers = [ domenkozar eelco lethalman ];
   };
 
   machine =
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 73c1e71eb51..8def0a6f9b9 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -50,6 +50,18 @@ in {
               }
             ];
           };
+          lovelaceConfig = {
+            title = "My Awesome Home";
+            views = [ {
+              title = "Example";
+              cards = [ {
+                type = "markdown";
+                title = "Lovelace";
+                content = "Welcome to your **Lovelace UI**.";
+              } ];
+            } ];
+          };
+          lovelaceConfigWritable = true;
         };
       };
   };
@@ -59,8 +71,10 @@ in {
     $hass->waitForUnit("home-assistant.service");
 
     # The config is specified using a Nix attribute set,
-    # but then converted from JSON to YAML
-    $hass->succeed("test -f ${configDir}/configuration.yaml");
+    # converted from JSON to YAML, and linked to the config dir
+    $hass->succeed("test -L ${configDir}/configuration.yaml");
+    # The lovelace config is copied because lovelaceConfigWritable = true
+    $hass->succeed("test -f ${configDir}/ui-lovelace.yaml");
 
     # Check that Home Assistant's web interface and API can be reached
     $hass->waitForOpenPort(8123);
@@ -73,8 +87,8 @@ in {
     $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"on\"'");
 
     # Toggle a binary sensor using hass-cli
-    $hass->succeed("${hassCli} entity get binary_sensor.mqtt_binary_sensor | grep -qF '\"state\": \"on\"'");
-    $hass->succeed("${hassCli} entity edit binary_sensor.mqtt_binary_sensor --json='{\"state\": \"off\"}'");
+    $hass->succeed("${hassCli} --output json state get binary_sensor.mqtt_binary_sensor | grep -qF '\"state\": \"on\"'");
+    $hass->succeed("${hassCli} state edit binary_sensor.mqtt_binary_sensor --json='{\"state\": \"off\"}'");
     $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"off\"'");
 
     # Print log to ease debugging
diff --git a/nixos/tests/hydra/default.nix b/nixos/tests/hydra/default.nix
index db4e97e0039..882bced86d3 100644
--- a/nixos/tests/hydra/default.nix
+++ b/nixos/tests/hydra/default.nix
@@ -1,77 +1,91 @@
-import ../make-test.nix ({ pkgs, ...} :
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
 
 let
-   trivialJob = pkgs.writeTextDir "trivial.nix" ''
-     { trivial = builtins.derivation {
-         name = "trivial";
-         system = "x86_64-linux";
-         builder = "/bin/sh";
-         args = ["-c" "echo success > $out; exit 0"];
-       };
-     }
-   '';
-
-    createTrivialProject = pkgs.stdenv.mkDerivation {
-      name = "create-trivial-project";
-      unpackPhase = ":";
-      buildInputs = [ pkgs.makeWrapper ];
-      installPhase = "install -m755 -D ${./create-trivial-project.sh} $out/bin/create-trivial-project.sh";
-      postFixup = ''
-        wrapProgram "$out/bin/create-trivial-project.sh" --prefix PATH ":" ${pkgs.stdenv.lib.makeBinPath [ pkgs.curl ]} --set EXPR_PATH ${trivialJob}
-      '';
-    };
-
-in {
-  name = "hydra-init-localdb";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ pstn lewo ma27 ];
-  };
 
-  machine =
-    { pkgs, ... }:
-
-    {
-      virtualisation.memorySize = 1024;
-      time.timeZone = "UTC";
-
-      environment.systemPackages = [ createTrivialProject pkgs.jq ];
-      services.hydra = {
-        enable = true;
-
-        #Hydra needs those settings to start up, so we add something not harmfull.
-        hydraURL = "example.com";
-        notificationSender = "example@example.com";
-      };
-      nix = {
-        buildMachines = [{
-          hostName = "localhost";
-          systems = [ "x86_64-linux" ];
-        }];
-
-        binaryCaches = [];
-      };
-    };
-
-  testScript =
-    ''
-      # let the system boot up
-      $machine->waitForUnit("multi-user.target");
-      # test whether the database is running
-      $machine->succeed("systemctl status postgresql.service");
-      # test whether the actual hydra daemons are running
-      $machine->succeed("systemctl status hydra-queue-runner.service");
-      $machine->succeed("systemctl status hydra-init.service");
-      $machine->succeed("systemctl status hydra-evaluator.service");
-      $machine->succeed("systemctl status hydra-send-stats.service");
-
-      $machine->succeed("hydra-create-user admin --role admin --password admin");
-
-      # create a project with a trivial job
-      $machine->waitForOpenPort(3000);
-
-      # make sure the build as been successfully built
-      $machine->succeed("create-trivial-project.sh");
-
-      $machine->waitUntilSucceeds('curl -L -s http://localhost:3000/build/1 -H "Accept: application/json" |  jq .buildstatus | xargs test 0 -eq');
+  trivialJob = pkgs.writeTextDir "trivial.nix" ''
+   { trivial = builtins.derivation {
+       name = "trivial";
+       system = "x86_64-linux";
+       builder = "/bin/sh";
+       args = ["-c" "echo success > $out; exit 0"];
+     };
+   }
+  '';
+
+  createTrivialProject = pkgs.stdenv.mkDerivation {
+    name = "create-trivial-project";
+    unpackPhase = ":";
+    buildInputs = [ pkgs.makeWrapper ];
+    installPhase = "install -m755 -D ${./create-trivial-project.sh} $out/bin/create-trivial-project.sh";
+    postFixup = ''
+      wrapProgram "$out/bin/create-trivial-project.sh" --prefix PATH ":" ${pkgs.stdenv.lib.makeBinPath [ pkgs.curl ]} --set EXPR_PATH ${trivialJob}
     '';
-})
+  };
+
+  callTest = f: f { inherit system pkgs; };
+
+  hydraPkgs = {
+    inherit (pkgs) nixStable nixUnstable;
+  };
+
+  tests = pkgs.lib.flip pkgs.lib.mapAttrs hydraPkgs (name: nix:
+    callTest (import ../make-test.nix ({ pkgs, lib, ... }:
+      {
+        name = "hydra-with-${name}";
+        meta = with pkgs.stdenv.lib.maintainers; {
+          maintainers = [ pstn lewo ma27 ];
+        };
+
+        machine = { pkgs, ... }:
+          {
+            virtualisation.memorySize = 1024;
+            time.timeZone = "UTC";
+
+            environment.systemPackages = [ createTrivialProject pkgs.jq ];
+            services.hydra = {
+              enable = true;
+
+              #Hydra needs those settings to start up, so we add something not harmfull.
+              hydraURL = "example.com";
+              notificationSender = "example@example.com";
+
+              package = pkgs.hydra.override { inherit nix; };
+            };
+            nix = {
+              buildMachines = [{
+                hostName = "localhost";
+                systems = [ "x86_64-linux" ];
+              }];
+
+              binaryCaches = [];
+            };
+          };
+
+        testScript = ''
+          # let the system boot up
+          $machine->waitForUnit("multi-user.target");
+          # test whether the database is running
+          $machine->succeed("systemctl status postgresql.service");
+          # test whether the actual hydra daemons are running
+          $machine->succeed("systemctl status hydra-queue-runner.service");
+          $machine->succeed("systemctl status hydra-init.service");
+          $machine->succeed("systemctl status hydra-evaluator.service");
+          $machine->succeed("systemctl status hydra-send-stats.service");
+
+          $machine->succeed("hydra-create-user admin --role admin --password admin");
+
+          # create a project with a trivial job
+          $machine->waitForOpenPort(3000);
+
+          # make sure the build as been successfully built
+          $machine->succeed("create-trivial-project.sh");
+
+          $machine->waitUntilSucceeds('curl -L -s http://localhost:3000/build/1 -H "Accept: application/json" |  jq .buildstatus | xargs test 0 -eq');
+        '';
+      })));
+
+in
+  tests
diff --git a/nixos/tests/influxdb.nix b/nixos/tests/influxdb.nix
index 440049d9511..61201202204 100644
--- a/nixos/tests/influxdb.nix
+++ b/nixos/tests/influxdb.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "influxdb";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ chaoflow offline ];
+    maintainers = [ offline ];
   };
 
   nodes = {
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index c8edaaba158..5e363f5d09e 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -200,7 +200,7 @@ let
       name = "installer-" + name;
       meta = with pkgs.stdenv.lib.maintainers; {
         # put global maintainers here, individuals go into makeInstallerTest fkt call
-        maintainers = [ wkennington ] ++ (meta.maintainers or []);
+        maintainers = (meta.maintainers or []);
       };
       nodes = {
 
@@ -273,6 +273,37 @@ let
       };
     };
 
+    makeLuksRootTest = name: luksFormatOpts: makeInstallerTest "luksroot-format2"
+      { createPartitions = ''
+          $machine->succeed(
+            "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
+            . " mkpart primary ext2 1M 50MB" # /boot
+            . " mkpart primary linux-swap 50M 1024M"
+            . " mkpart primary 1024M -1s", # LUKS
+            "udevadm settle",
+            "mkswap /dev/vda2 -L swap",
+            "swapon -L swap",
+            "modprobe dm_mod dm_crypt",
+            "echo -n supersecret | cryptsetup luksFormat ${luksFormatOpts} -q /dev/vda3 -",
+            "echo -n supersecret | cryptsetup luksOpen --key-file - /dev/vda3 cryptroot",
+            "mkfs.ext3 -L nixos /dev/mapper/cryptroot",
+            "mount LABEL=nixos /mnt",
+            "mkfs.ext3 -L boot /dev/vda1",
+            "mkdir -p /mnt/boot",
+            "mount LABEL=boot /mnt/boot",
+          );
+        '';
+        extraConfig = ''
+          boot.kernelParams = lib.mkAfter [ "console=tty0" ];
+        '';
+        enableOCR = true;
+        preBootCommands = ''
+          $machine->start;
+          $machine->waitForText(qr/Passphrase for/);
+          $machine->sendChars("supersecret\n");
+        '';
+      };
+
 
 in {
 
@@ -446,37 +477,14 @@ in {
         '';
     };
 
-  # Boot off an encrypted root partition
-  luksroot = makeInstallerTest "luksroot"
-    { createPartitions = ''
-        $machine->succeed(
-          "flock /dev/vda parted --script /dev/vda -- mklabel msdos"
-          . " mkpart primary ext2 1M 50MB" # /boot
-          . " mkpart primary linux-swap 50M 1024M"
-          . " mkpart primary 1024M -1s", # LUKS
-          "udevadm settle",
-          "mkswap /dev/vda2 -L swap",
-          "swapon -L swap",
-          "modprobe dm_mod dm_crypt",
-          "echo -n supersecret | cryptsetup luksFormat -q /dev/vda3 -",
-          "echo -n supersecret | cryptsetup luksOpen --key-file - /dev/vda3 cryptroot",
-          "mkfs.ext3 -L nixos /dev/mapper/cryptroot",
-          "mount LABEL=nixos /mnt",
-          "mkfs.ext3 -L boot /dev/vda1",
-          "mkdir -p /mnt/boot",
-          "mount LABEL=boot /mnt/boot",
-        );
-      '';
-      extraConfig = ''
-        boot.kernelParams = lib.mkAfter [ "console=tty0" ];
-      '';
-      enableOCR = true;
-      preBootCommands = ''
-        $machine->start;
-        $machine->waitForText(qr/Passphrase for/);
-        $machine->sendChars("supersecret\n");
-      '';
-    };
+  # Boot off an encrypted root partition with the default LUKS header format
+  luksroot = makeLuksRootTest "luksroot-format1" "";
+
+  # Boot off an encrypted root partition with LUKS1 format
+  luksroot-format1 = makeLuksRootTest "luksroot-format1" "--type=LUKS1";
+
+  # Boot off an encrypted root partition with LUKS2 format
+  luksroot-format2 = makeLuksRootTest "luksroot-format2" "--type=LUKS2";
 
   # Test whether opening encrypted filesystem with keyfile
   # Checks for regression of missing cryptsetup, when no luks device without
diff --git a/nixos/tests/ipv6.nix b/nixos/tests/ipv6.nix
index 97f348a9bee..14f24c29cfe 100644
--- a/nixos/tests/ipv6.nix
+++ b/nixos/tests/ipv6.nix
@@ -4,7 +4,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "ipv6";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes =
diff --git a/nixos/tests/jenkins.nix b/nixos/tests/jenkins.nix
index 4f2d2085cd1..a6eec411ff2 100644
--- a/nixos/tests/jenkins.nix
+++ b/nixos/tests/jenkins.nix
@@ -6,7 +6,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "jenkins";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ bjornfor coconnor domenkozar eelco chaoflow ];
+    maintainers = [ bjornfor coconnor domenkozar eelco ];
   };
 
   nodes = {
diff --git a/nixos/tests/kexec.nix b/nixos/tests/kexec.nix
index db596189d46..b13b4131091 100644
--- a/nixos/tests/kexec.nix
+++ b/nixos/tests/kexec.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "kexec";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   machine = { ... }:
diff --git a/nixos/tests/knot.nix b/nixos/tests/knot.nix
new file mode 100644
index 00000000000..e46159836cc
--- /dev/null
+++ b/nixos/tests/knot.nix
@@ -0,0 +1,197 @@
+import ./make-test.nix ({ pkgs, lib, ...} :
+let
+  common = {
+    networking.firewall.enable = false;
+    networking.useDHCP = false;
+  };
+  exampleZone = pkgs.writeTextDir "example.com.zone" ''
+      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
+      @       NS      ns1
+      @       NS      ns2
+      ns1     A       192.168.0.1
+      ns1     AAAA    fd00::1
+      ns2     A       192.168.0.2
+      ns2     AAAA    fd00::2
+      www     A       192.0.2.1
+      www     AAAA    2001:DB8::1
+      sub     NS      ns.example.com.
+  '';
+  delegatedZone = pkgs.writeTextDir "sub.example.com.zone" ''
+      @ SOA ns.example.com. noc.example.com. 2019031301 86400 7200 3600000 172800
+      @       NS      ns1.example.com.
+      @       NS      ns2.example.com.
+      @       A       192.0.2.2
+      @       AAAA    2001:DB8::2
+  '';
+
+  knotZonesEnv = pkgs.buildEnv {
+    name = "knot-zones";
+    paths = [ exampleZone delegatedZone ];
+  };
+in {
+  name = "knot";
+
+  nodes = {
+    master = { lib, ... }: {
+      imports = [ common ];
+      networking.interfaces.eth1 = {
+        ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.1"; prefixLength = 24; }
+        ];
+        ipv6.addresses = lib.mkForce [
+          { address = "fd00::1"; prefixLength = 64; }
+        ];
+      };
+      services.knot.enable = true;
+      services.knot.extraArgs = [ "-v" ];
+      services.knot.extraConfig = ''
+        server:
+            listen: 0.0.0.0@53
+            listen: ::@53
+
+        acl:
+          - id: slave_acl
+            address: 192.168.0.2
+            action: transfer
+
+        remote:
+          - id: slave
+            address: 192.168.0.2@53
+
+        template:
+          - id: default
+            storage: ${knotZonesEnv}
+            notify: [slave]
+            acl: [slave_acl]
+            dnssec-signing: on
+            # Input-only zone files
+            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
+            # prevents modification of the zonefiles, since the zonefiles are immutable
+            zonefile-sync: -1
+            zonefile-load: difference
+            journal-content: changes
+            # move databases below the state directory, because they need to be writable
+            journal-db: /var/lib/knot/journal
+            kasp-db: /var/lib/knot/kasp
+            timer-db: /var/lib/knot/timer
+
+        zone:
+          - domain: example.com
+            file: example.com.zone
+
+          - domain: sub.example.com
+            file: sub.example.com.zone
+
+        log:
+          - target: syslog
+            any: info
+      '';
+    };
+
+    slave = { lib, ... }: {
+      imports = [ common ];
+      networking.interfaces.eth1 = {
+        ipv4.addresses = lib.mkForce [
+          { address = "192.168.0.2"; prefixLength = 24; }
+        ];
+        ipv6.addresses = lib.mkForce [
+          { address = "fd00::2"; prefixLength = 64; }
+        ];
+      };
+      services.knot.enable = true;
+      services.knot.extraArgs = [ "-v" ];
+      services.knot.extraConfig = ''
+        server:
+            listen: 0.0.0.0@53
+            listen: ::@53
+
+        acl:
+          - id: notify_from_master
+            address: 192.168.0.1
+            action: notify
+
+        remote:
+          - id: master
+            address: 192.168.0.1@53
+
+        template:
+          - id: default
+            master: master
+            acl: [notify_from_master]
+            # zonefileless setup
+            # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
+            zonefile-sync: -1
+            zonefile-load: none
+            journal-content: all
+            # move databases below the state directory, because they need to be writable
+            journal-db: /var/lib/knot/journal
+            kasp-db: /var/lib/knot/kasp
+            timer-db: /var/lib/knot/timer
+
+        zone:
+          - domain: example.com
+            file: example.com.zone
+
+          - domain: sub.example.com
+            file: sub.example.com.zone
+
+        log:
+          - target: syslog
+            any: info
+      '';
+    };
+    client = { lib, nodes, ... }: {
+      imports = [ common ];
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          { address = "192.168.0.3"; prefixLength = 24; }
+        ];
+        ipv6.addresses = [
+          { address = "fd00::3"; prefixLength = 64; }
+        ];
+      };
+      environment.systemPackages = [ pkgs.knot-dns ];
+    };    
+  };
+
+  testScript = { nodes, ... }: let 
+    master4 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv4.addresses).address;
+    master6 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv6.addresses).address;
+
+    slave4 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv4.addresses).address;
+    slave6 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv6.addresses).address;
+  in ''
+    startAll;
+
+    $client->waitForUnit("network.target");
+    $master->waitForUnit("knot.service");
+    $slave->waitForUnit("knot.service");
+
+    sub assertResponse {
+      my ($knot, $query_type, $query, $expected) = @_;
+      my $out = $client->succeed("khost -t $query_type $query $knot");
+      $client->log("$knot replies with: $out");
+      chomp $out;
+      die "DNS query for $query ($query_type) against $knot gave '$out' instead of '$expected'"
+        if ($out !~ $expected);
+    }
+
+    foreach ("${master4}", "${master6}", "${slave4}", "${slave6}") {
+      subtest $_, sub {
+        assertResponse($_, "SOA", "example.com", qr/start of authority.*?noc\.example\.com/);
+        assertResponse($_, "A", "example.com", qr/has no [^ ]+ record/);
+        assertResponse($_, "AAAA", "example.com", qr/has no [^ ]+ record/);
+
+        assertResponse($_, "A", "www.example.com", qr/address 192.0.2.1$/);
+        assertResponse($_, "AAAA", "www.example.com", qr/address 2001:db8::1$/);
+
+        assertResponse($_, "NS", "sub.example.com", qr/nameserver is ns\d\.example\.com.$/);
+        assertResponse($_, "A", "sub.example.com", qr/address 192.0.2.2$/);
+        assertResponse($_, "AAAA", "sub.example.com", qr/address 2001:db8::2$/);
+
+        assertResponse($_, "RRSIG", "www.example.com", qr/RR set signature is/);
+        assertResponse($_, "DNSKEY", "example.com", qr/DNSSEC key is/);
+      };
+    }
+  '';
+})
diff --git a/nixos/tests/kubernetes/base.nix b/nixos/tests/kubernetes/base.nix
index 9d77be13175..ec1a75e74c4 100644
--- a/nixos/tests/kubernetes/base.nix
+++ b/nixos/tests/kubernetes/base.nix
@@ -10,7 +10,6 @@ let
   mkKubernetesBaseTest =
     { name, domain ? "my.zyx", test, machines
     , pkgs ? import <nixpkgs> { inherit system; }
-    , certs ? import ./certs.nix { inherit pkgs; externalDomain = domain; kubelets = attrNames machines; }
     , extraConfiguration ? null }:
     let
       masterName = head (filter (machineName: any (role: role == "master") machines.${machineName}.roles) (attrNames machines));
@@ -20,6 +19,10 @@ let
         ${master.ip}  api.${domain}
         ${concatMapStringsSep "\n" (machineName: "${machines.${machineName}.ip}  ${machineName}.${domain}") (attrNames machines)}
       '';
+      kubectl = with pkgs; runCommand "wrap-kubectl" { buildInputs = [ makeWrapper ]; } ''
+        mkdir -p $out/bin
+        makeWrapper ${pkgs.kubernetes}/bin/kubectl $out/bin/kubectl --set KUBECONFIG "/etc/kubernetes/cluster-admin.kubeconfig"
+      '';
     in makeTest {
       inherit name;
 
@@ -27,6 +30,7 @@ let
         { config, pkgs, lib, nodes, ... }:
           mkMerge [
             {
+              boot.postBootCommands = "rm -fr /var/lib/kubernetes/secrets /tmp/shared/*";
               virtualisation.memorySize = mkDefault 1536;
               virtualisation.diskSize = mkDefault 4096;
               networking = {
@@ -45,34 +49,25 @@ let
                 };
               };
               programs.bash.enableCompletion = true;
-              environment.variables = {
-                ETCDCTL_CERT_FILE = "${certs.worker}/etcd-client.pem";
-                ETCDCTL_KEY_FILE = "${certs.worker}/etcd-client-key.pem";
-                ETCDCTL_CA_FILE = "${certs.worker}/ca.pem";
-                ETCDCTL_PEERS = "https://etcd.${domain}:2379";
-              };
+              environment.systemPackages = [ kubectl ];
               services.flannel.iface = "eth1";
-              services.kubernetes.apiserver.advertiseAddress = master.ip;
+              services.kubernetes = {
+                addons.dashboard.enable = true;
+
+                easyCerts = true;
+                inherit (machine) roles;
+                apiserver = {
+                  securePort = 443;
+                  advertiseAddress = master.ip;
+                };
+                masterAddress = "${masterName}.${config.networking.domain}";
+              };
             }
             (optionalAttrs (any (role: role == "master") machine.roles) {
               networking.firewall.allowedTCPPorts = [
-                2379 2380  # etcd
                 443 # kubernetes apiserver
               ];
-              services.etcd = {
-                enable = true;
-                certFile = "${certs.master}/etcd.pem";
-                keyFile = "${certs.master}/etcd-key.pem";
-                trustedCaFile = "${certs.master}/ca.pem";
-                peerClientCertAuth = true;
-                listenClientUrls = ["https://0.0.0.0:2379"];
-                listenPeerUrls = ["https://0.0.0.0:2380"];
-                advertiseClientUrls = ["https://etcd.${config.networking.domain}:2379"];
-                initialCluster = ["${masterName}=https://etcd.${config.networking.domain}:2380"];
-                initialAdvertisePeerUrls = ["https://etcd.${config.networking.domain}:2380"];
-              };
             })
-            (import ./kubernetes-common.nix { inherit (machine) roles; inherit pkgs config certs; })
             (optionalAttrs (machine ? "extraConfiguration") (machine.extraConfiguration { inherit config pkgs lib nodes; }))
             (optionalAttrs (extraConfiguration != null) (extraConfiguration { inherit config pkgs lib nodes; }))
           ]
diff --git a/nixos/tests/kubernetes/certs.nix b/nixos/tests/kubernetes/certs.nix
deleted file mode 100644
index 85e92f6330c..00000000000
--- a/nixos/tests/kubernetes/certs.nix
+++ /dev/null
@@ -1,219 +0,0 @@
-{
-  pkgs ? import <nixpkgs> {},
-  externalDomain ? "myawesomecluster.cluster.yourdomain.net",
-  serviceClusterIp ? "10.0.0.1",
-  kubelets,
-  ...
-}:
-let
-   runWithCFSSL = name: cmd:
-     let secrets = pkgs.runCommand "${name}-cfss.json" {
-         buildInputs = [ pkgs.cfssl pkgs.jq ];
-         outputs = [ "out" "cert" "key" "csr" ];
-       }
-       ''
-         (
-           echo "${cmd}"
-           cfssl ${cmd} > tmp
-           cat tmp | jq -r .key > $key
-           cat tmp | jq -r .cert > $cert
-           cat tmp | jq -r .csr > $csr
-
-           touch $out
-         ) 2>&1 | fold -w 80 -s
-       '';
-     in {
-       key = secrets.key;
-       cert = secrets.cert;
-       csr = secrets.csr;
-     };
-
-   writeCFSSL = content:
-     pkgs.runCommand content.name {
-      buildInputs = [ pkgs.cfssl pkgs.jq ];
-     } ''
-       mkdir -p $out
-       cd $out
-
-       json=${pkgs.lib.escapeShellArg (builtins.toJSON content)}
-
-       # for a given $field in the $json, treat the associated value as a
-       # file path and substitute the contents thereof into the $json
-       # object.
-       expandFileField() {
-         local field=$1
-         if jq -e --arg field "$field" 'has($field)'; then
-           local path="$(echo "$json" | jq -r ".$field")"
-           json="$(echo "$json" | jq --arg val "$(cat "$path")" ".$field = \$val")"
-         fi
-       }
-
-       expandFileField key
-       expandFileField ca
-       expandFileField cert
-
-       echo "$json" | cfssljson -bare ${content.name}
-     '';
-
-  noCSR = content: pkgs.lib.filterAttrs (n: v: n != "csr") content;
-  noKey = content: pkgs.lib.filterAttrs (n: v: n != "key") content;
-
-  writeFile = content:
-    if pkgs.lib.isDerivation content
-    then content
-    else pkgs.writeText "content" (builtins.toJSON content);
-
-  createServingCertKey = { ca, cn, hosts? [], size ? 2048, name ? cn }:
-    noCSR (
-      (runWithCFSSL name "gencert -ca=${writeFile ca.cert} -ca-key=${writeFile ca.key} -profile=server -config=${writeFile ca.config} ${writeFile {
-        CN = cn;
-        hosts = hosts;
-        key = { algo = "rsa"; inherit size; };
-      }}") // { inherit name; }
-    );
-
-  createClientCertKey = { ca, cn, groups ? [], size ? 2048, name ? cn }:
-    noCSR (
-      (runWithCFSSL name "gencert -ca=${writeFile ca.cert} -ca-key=${writeFile ca.key} -profile=client -config=${writeFile ca.config} ${writeFile {
-        CN = cn;
-        names = map (group: {O = group;}) groups;
-        hosts = [""];
-        key = { algo = "rsa"; inherit size; };
-      }}") // { inherit name; }
-    );
-
-  createSigningCertKey = { C ? "xx", ST ? "x", L ? "x", O ? "x", OU ? "x", CN ? "ca", emailAddress ? "x", expiry ? "43800h", size ? 2048, name ? CN }:
-    (noCSR (runWithCFSSL CN "genkey -initca ${writeFile {
-      key = { algo = "rsa"; inherit size; };
-      names = [{ inherit C ST L O OU CN emailAddress; }];
-    }}")) // {
-      inherit name;
-      config.signing = {
-        default.expiry = expiry;
-        profiles = {
-          server = {
-            inherit expiry;
-            usages = [
-              "signing"
-              "key encipherment"
-              "server auth"
-            ];
-          };
-          client = {
-            inherit expiry;
-            usages = [
-              "signing"
-              "key encipherment"
-              "client auth"
-            ];
-          };
-          peer = {
-            inherit expiry;
-            usages = [
-              "signing"
-              "key encipherment"
-              "server auth"
-              "client auth"
-            ];
-          };
-        };
-      };
-    };
-
-  ca = createSigningCertKey {};
-
-  kube-apiserver = createServingCertKey {
-    inherit ca;
-    cn = "kube-apiserver";
-    hosts = ["kubernetes.default" "kubernetes.default.svc" "localhost" "api.${externalDomain}" serviceClusterIp];
-  };
-
-  kubelet = createServingCertKey {
-    inherit ca;
-    cn = "kubelet";
-    hosts = ["*.${externalDomain}"];
-  };
-
-  service-accounts = createServingCertKey {
-    inherit ca;
-    cn = "kube-service-accounts";
-  };
-
-  etcd = createServingCertKey {
-    inherit ca;
-    cn = "etcd";
-    hosts = ["etcd.${externalDomain}"];
-  };
-
-  etcd-client = createClientCertKey {
-    inherit ca;
-    cn = "etcd-client";
-  };
-
-  kubelet-client = createClientCertKey {
-    inherit ca;
-    cn = "kubelet-client";
-    groups = ["system:masters"];
-  };
-
-  apiserver-client = {
-    kubelet = hostname: createClientCertKey {
-      inherit ca;
-      name = "apiserver-client-kubelet-${hostname}";
-      cn = "system:node:${hostname}.${externalDomain}";
-      groups = ["system:nodes"];
-    };
-
-    kube-proxy = createClientCertKey {
-      inherit ca;
-      name = "apiserver-client-kube-proxy";
-      cn = "system:kube-proxy";
-      groups = ["system:kube-proxy" "system:nodes"];
-    };
-
-    kube-controller-manager = createClientCertKey {
-      inherit ca;
-      name = "apiserver-client-kube-controller-manager";
-      cn = "system:kube-controller-manager";
-      groups = ["system:masters"];
-    };
-
-    kube-scheduler = createClientCertKey {
-      inherit ca;
-      name = "apiserver-client-kube-scheduler";
-      cn = "system:kube-scheduler";
-      groups = ["system:kube-scheduler"];
-    };
-
-    admin = createClientCertKey {
-      inherit ca;
-      cn = "admin";
-      groups = ["system:masters"];
-    };
-  };
-in {
-  master = pkgs.buildEnv {
-    name = "master-keys";
-    paths = [
-      (writeCFSSL (noKey ca))
-      (writeCFSSL kube-apiserver)
-      (writeCFSSL kubelet-client)
-      (writeCFSSL apiserver-client.kube-controller-manager)
-      (writeCFSSL apiserver-client.kube-scheduler)
-      (writeCFSSL service-accounts)
-      (writeCFSSL etcd)
-    ];
-  };
-
-  worker = pkgs.buildEnv {
-    name = "worker-keys";
-    paths = [
-      (writeCFSSL (noKey ca))
-      (writeCFSSL kubelet)
-      (writeCFSSL apiserver-client.kube-proxy)
-      (writeCFSSL etcd-client)
-    ] ++ map (hostname: writeCFSSL (apiserver-client.kubelet hostname)) kubelets;
-  };
-
-  admin = writeCFSSL apiserver-client.admin;
-}
diff --git a/nixos/tests/kubernetes/dns.nix b/nixos/tests/kubernetes/dns.nix
index f25ea5b9ed8..46bcb01a526 100644
--- a/nixos/tests/kubernetes/dns.nix
+++ b/nixos/tests/kubernetes/dns.nix
@@ -71,17 +71,17 @@ let
 
   base = {
     name = "dns";
-    inherit domain certs extraConfiguration;
+    inherit domain extraConfiguration;
   };
 
   singleNodeTest = {
     test = ''
       # prepare machine1 for test
       $machine1->waitUntilSucceeds("kubectl get node machine1.${domain} | grep -w Ready");
-      $machine1->execute("docker load < ${redisImage}");
+      $machine1->waitUntilSucceeds("docker load < ${redisImage}");
       $machine1->waitUntilSucceeds("kubectl create -f ${redisPod}");
       $machine1->waitUntilSucceeds("kubectl create -f ${redisService}");
-      $machine1->execute("docker load < ${probeImage}");
+      $machine1->waitUntilSucceeds("docker load < ${probeImage}");
       $machine1->waitUntilSucceeds("kubectl create -f ${probePod}");
 
       # check if pods are running
@@ -99,13 +99,16 @@ let
 
   multiNodeTest = {
     test = ''
+      # Node token exchange
+      $machine1->waitUntilSucceeds("cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret");
+      $machine2->waitUntilSucceeds("cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join");
+
       # prepare machines for test
-      $machine1->waitUntilSucceeds("kubectl get node machine1.${domain} | grep -w Ready");
       $machine1->waitUntilSucceeds("kubectl get node machine2.${domain} | grep -w Ready");
-      $machine2->execute("docker load < ${redisImage}");
+      $machine2->waitUntilSucceeds("docker load < ${redisImage}");
       $machine1->waitUntilSucceeds("kubectl create -f ${redisPod}");
       $machine1->waitUntilSucceeds("kubectl create -f ${redisService}");
-      $machine2->execute("docker load < ${probeImage}");
+      $machine2->waitUntilSucceeds("docker load < ${probeImage}");
       $machine1->waitUntilSucceeds("kubectl create -f ${probePod}");
 
       # check if pods are running
diff --git a/nixos/tests/kubernetes/kubernetes-common.nix b/nixos/tests/kubernetes/kubernetes-common.nix
deleted file mode 100644
index 87c65b88365..00000000000
--- a/nixos/tests/kubernetes/kubernetes-common.nix
+++ /dev/null
@@ -1,57 +0,0 @@
-{ roles, config, pkgs, certs }:
-with pkgs.lib;
-let
-  base = {
-    inherit roles;
-    flannel.enable = true;
-    addons.dashboard.enable = true;
-
-    caFile = "${certs.master}/ca.pem";
-    apiserver = {
-      tlsCertFile = "${certs.master}/kube-apiserver.pem";
-      tlsKeyFile = "${certs.master}/kube-apiserver-key.pem";
-      kubeletClientCertFile = "${certs.master}/kubelet-client.pem";
-      kubeletClientKeyFile = "${certs.master}/kubelet-client-key.pem";
-      serviceAccountKeyFile = "${certs.master}/kube-service-accounts.pem";
-    };
-    etcd = {
-      servers = ["https://etcd.${config.networking.domain}:2379"];
-      certFile = "${certs.worker}/etcd-client.pem";
-      keyFile = "${certs.worker}/etcd-client-key.pem";
-    };
-    kubeconfig = {
-      server = "https://api.${config.networking.domain}";
-    };
-    kubelet = {
-      tlsCertFile = "${certs.worker}/kubelet.pem";
-      tlsKeyFile = "${certs.worker}/kubelet-key.pem";
-      hostname = "${config.networking.hostName}.${config.networking.domain}";
-      kubeconfig = {
-        certFile = "${certs.worker}/apiserver-client-kubelet-${config.networking.hostName}.pem";
-        keyFile = "${certs.worker}/apiserver-client-kubelet-${config.networking.hostName}-key.pem";
-      };
-    };
-    controllerManager = {
-      serviceAccountKeyFile = "${certs.master}/kube-service-accounts-key.pem";
-      kubeconfig = {
-        certFile = "${certs.master}/apiserver-client-kube-controller-manager.pem";
-        keyFile = "${certs.master}/apiserver-client-kube-controller-manager-key.pem";
-      };
-    };
-    scheduler = {
-      kubeconfig = {
-        certFile = "${certs.master}/apiserver-client-kube-scheduler.pem";
-        keyFile = "${certs.master}/apiserver-client-kube-scheduler-key.pem";
-      };
-    };
-    proxy = {
-      kubeconfig = {
-        certFile = "${certs.worker}/apiserver-client-kube-proxy.pem";
-        keyFile = "${certs.worker}//apiserver-client-kube-proxy-key.pem";
-      };
-    };
-  };
-
-in {
-  services.kubernetes = base;
-}
diff --git a/nixos/tests/kubernetes/rbac.nix b/nixos/tests/kubernetes/rbac.nix
index 226808c4b26..3ce7adcd0d7 100644
--- a/nixos/tests/kubernetes/rbac.nix
+++ b/nixos/tests/kubernetes/rbac.nix
@@ -96,7 +96,7 @@ let
     test = ''
       $machine1->waitUntilSucceeds("kubectl get node machine1.my.zyx | grep -w Ready");
 
-      $machine1->execute("docker load < ${kubectlImage}");
+      $machine1->waitUntilSucceeds("docker load < ${kubectlImage}");
 
       $machine1->waitUntilSucceeds("kubectl apply -f ${roServiceAccount}");
       $machine1->waitUntilSucceeds("kubectl apply -f ${roRole}");
@@ -105,7 +105,7 @@ let
 
       $machine1->waitUntilSucceeds("kubectl get pod kubectl | grep Running");
 
-      $machine1->succeed("kubectl exec -ti kubectl -- kubectl get pods");
+      $machine1->waitUntilSucceeds("kubectl exec -ti kubectl -- kubectl get pods");
       $machine1->fail("kubectl exec -ti kubectl -- kubectl create -f /kubectl-pod-2.json");
       $machine1->fail("kubectl exec -ti kubectl -- kubectl delete pods -l name=kubectl");
     '';
@@ -113,10 +113,13 @@ let
 
   multinode = base // {
     test = ''
-      $machine1->waitUntilSucceeds("kubectl get node machine1.my.zyx | grep -w Ready");
+      # Node token exchange
+      $machine1->waitUntilSucceeds("cp -f /var/lib/cfssl/apitoken.secret /tmp/shared/apitoken.secret");
+      $machine2->waitUntilSucceeds("cat /tmp/shared/apitoken.secret | nixos-kubernetes-node-join");
+
       $machine1->waitUntilSucceeds("kubectl get node machine2.my.zyx | grep -w Ready");
 
-      $machine2->execute("docker load < ${kubectlImage}");
+      $machine2->waitUntilSucceeds("docker load < ${kubectlImage}");
 
       $machine1->waitUntilSucceeds("kubectl apply -f ${roServiceAccount}");
       $machine1->waitUntilSucceeds("kubectl apply -f ${roRole}");
@@ -125,7 +128,7 @@ let
 
       $machine1->waitUntilSucceeds("kubectl get pod kubectl | grep Running");
 
-      $machine1->succeed("kubectl exec -ti kubectl -- kubectl get pods");
+      $machine1->waitUntilSucceeds("kubectl exec -ti kubectl -- kubectl get pods");
       $machine1->fail("kubectl exec -ti kubectl -- kubectl create -f /kubectl-pod-2.json");
       $machine1->fail("kubectl exec -ti kubectl -- kubectl delete pods -l name=kubectl");
     '';
diff --git a/nixos/tests/ldap.nix b/nixos/tests/ldap.nix
index 035a8192417..b3fd42e7588 100644
--- a/nixos/tests/ldap.nix
+++ b/nixos/tests/ldap.nix
@@ -1,41 +1,23 @@
 import ./make-test.nix ({ pkgs, lib, ...} :
 
 let
+  unlines = lib.concatStringsSep "\n";
+  unlinesAttrs = f: as: unlines (lib.mapAttrsToList f as);
 
+  dbDomain = "example.com";
   dbSuffix = "dc=example,dc=com";
-  dbPath = "/var/db/openldap";
   dbAdminDn = "cn=admin,${dbSuffix}";
-  dbAdminPwd = "test";
-  serverUri = "ldap:///";
+  dbAdminPwd = "admin-password";
+  # NOTE: slappasswd -h "{SSHA}" -s '${dbAdminPwd}'
+  dbAdminPwdHash = "{SSHA}i7FopSzkFQMrHzDMB1vrtkI0rBnwouP8";
   ldapUser = "test-ldap-user";
   ldapUserId = 10000;
-  ldapUserPwd = "test";
+  ldapUserPwd = "user-password";
+  # NOTE: slappasswd -h "{SSHA}" -s '${ldapUserPwd}'
+  ldapUserPwdHash = "{SSHA}v12XICMZNGT6r2KJ26rIkN8Vvvp4QX6i";
   ldapGroup = "test-ldap-group";
   ldapGroupId = 10000;
-  setupLdif = pkgs.writeText "test-ldap.ldif" ''
-    dn: ${dbSuffix}
-    dc: ${with lib; let dc = head (splitString "," dbSuffix); dcName = head (tail (splitString "=" dc)); in dcName}
-    o: ${dbSuffix}
-    objectclass: top
-    objectclass: dcObject
-    objectclass: organization
-
-    dn: cn=${ldapUser},${dbSuffix}
-    sn: ${ldapUser}
-    objectClass: person
-    objectClass: posixAccount
-    uid: ${ldapUser}
-    uidNumber: ${toString ldapUserId}
-    gidNumber: ${toString ldapGroupId}
-    homeDirectory: /home/${ldapUser}
-    loginShell: /bin/sh
-    userPassword: ${ldapUserPwd}
-
-    dn: cn=${ldapGroup},${dbSuffix}
-    objectClass: posixGroup
-    gidNumber: ${toString ldapGroupId}
-    memberUid: ${ldapUser}
-  '';
+
   mkClient = useDaemon:
     { lib, ... }:
     {
@@ -43,13 +25,24 @@ let
       virtualisation.vlans = [ 1 ];
       security.pam.services.su.rootOK = lib.mkForce false;
       users.ldap.enable = true;
-      users.ldap.daemon.enable = useDaemon;
+      users.ldap.daemon = {
+        enable = useDaemon;
+        rootpwmoddn = "cn=admin,${dbSuffix}";
+        rootpwmodpw = "/etc/nslcd.rootpwmodpw";
+      };
+      # NOTE: password stored in clear in Nix's store, but this is a test.
+      environment.etc."nslcd.rootpwmodpw".source = pkgs.writeText "rootpwmodpw" dbAdminPwd;
       users.ldap.loginPam = true;
       users.ldap.nsswitch = true;
       users.ldap.server = "ldap://server";
-      users.ldap.base = "${dbSuffix}";
+      users.ldap.base = "ou=posix,${dbSuffix}";
+      users.ldap.bind = {
+        distinguishedName = "cn=admin,${dbSuffix}";
+        password = "/etc/ldap/bind.password";
+      };
+      # NOTE: password stored in clear in Nix's store, but this is a test.
+      environment.etc."ldap/bind.password".source = pkgs.writeText "password" dbAdminPwd;
     };
-
 in
 
 {
@@ -61,28 +54,237 @@ in
   nodes = {
 
     server =
-      { pkgs, ... }:
+      { pkgs, config, ... }:
+      let
+        inherit (config.services) openldap;
+
+        slapdConfig = pkgs.writeText "cn=config.ldif" (''
+          dn: cn=config
+          objectClass: olcGlobal
+          #olcPidFile: /run/slapd/slapd.pid
+          # List of arguments that were passed to the server
+          #olcArgsFile: /run/slapd/slapd.args
+          # Read slapd-config(5) for possible values
+          olcLogLevel: none
+          # The tool-threads parameter sets the actual amount of CPU's
+          # that is used for indexing.
+          olcToolThreads: 1
+
+          dn: olcDatabase={-1}frontend,cn=config
+          objectClass: olcDatabaseConfig
+          objectClass: olcFrontendConfig
+          # The maximum number of entries that is returned for a search operation
+          olcSizeLimit: 500
+          # Allow unlimited access to local connection from the local root user
+          olcAccess: to *
+            by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
+            by * break
+          # Allow unauthenticated read access for schema and base DN autodiscovery
+          olcAccess: to dn.exact=""
+            by * read
+          olcAccess: to dn.base="cn=Subschema"
+            by * read
+
+          dn: olcDatabase=config,cn=config
+          objectClass: olcDatabaseConfig
+          olcRootDN: cn=admin,cn=config
+          #olcRootPW:
+          # NOTE: access to cn=config, system root can be manager
+          # with SASL mechanism (-Y EXTERNAL) over unix socket (-H ldapi://)
+          olcAccess: to *
+            by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
+            by * break
+
+          dn: cn=schema,cn=config
+          objectClass: olcSchemaConfig
+
+          include: file://${pkgs.openldap}/etc/schema/core.ldif
+          include: file://${pkgs.openldap}/etc/schema/cosine.ldif
+          include: file://${pkgs.openldap}/etc/schema/nis.ldif
+          include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif
+
+          dn: cn=module{0},cn=config
+          objectClass: olcModuleList
+          # Where the dynamically loaded modules are stored
+          #olcModulePath: /usr/lib/ldap
+          olcModuleLoad: back_mdb
+
+          ''
+          + unlinesAttrs (olcSuffix: {conf, ...}:
+              "include: file://" + pkgs.writeText "config.ldif" conf
+            ) slapdDatabases
+          );
+
+        slapdDatabases = {
+          "${dbSuffix}" = {
+            conf = ''
+              dn: olcBackend={1}mdb,cn=config
+              objectClass: olcBackendConfig
+
+              dn: olcDatabase={1}mdb,cn=config
+              olcSuffix: ${dbSuffix}
+              olcDbDirectory: ${openldap.dataDir}/${dbSuffix}
+              objectClass: olcDatabaseConfig
+              objectClass: olcMdbConfig
+              # NOTE: checkpoint the database periodically in case of system failure
+              # and to speed up slapd shutdown.
+              olcDbCheckpoint: 512 30
+              # Database max size is 1G
+              olcDbMaxSize: 1073741824
+              olcLastMod: TRUE
+              # NOTE: database superuser. Needed for syncrepl,
+              # and used to auth as admin through a TCP connection.
+              olcRootDN: cn=admin,${dbSuffix}
+              olcRootPW: ${dbAdminPwdHash}
+              #
+              olcDbIndex: objectClass eq
+              olcDbIndex: cn,uid eq
+              olcDbIndex: uidNumber,gidNumber eq
+              olcDbIndex: member,memberUid eq
+              #
+              olcAccess: to attrs=userPassword
+                by self write
+                by anonymous auth
+                by dn="cn=admin,${dbSuffix}" write
+                by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
+                by * none
+              olcAccess: to attrs=shadowLastChange
+                by self write
+                by dn="cn=admin,${dbSuffix}" write
+                by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
+                by * none
+              olcAccess: to dn.sub="ou=posix,${dbSuffix}"
+                by self read
+                by dn="cn=admin,${dbSuffix}" read
+                by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
+              olcAccess: to *
+                by self read
+                by * none
+            '';
+            data = ''
+              dn: ${dbSuffix}
+              objectClass: top
+              objectClass: dcObject
+              objectClass: organization
+              o: ${dbDomain}
+
+              dn: cn=admin,${dbSuffix}
+              objectClass: simpleSecurityObject
+              objectClass: organizationalRole
+              description: ${dbDomain} LDAP administrator
+              roleOccupant: ${dbSuffix}
+              userPassword: ${ldapUserPwdHash}
+
+              dn: ou=posix,${dbSuffix}
+              objectClass: top
+              objectClass: organizationalUnit
+
+              dn: ou=accounts,ou=posix,${dbSuffix}
+              objectClass: top
+              objectClass: organizationalUnit
+
+              dn: ou=groups,ou=posix,${dbSuffix}
+              objectClass: top
+              objectClass: organizationalUnit
+            ''
+            + lib.concatMapStrings posixAccount [
+              { uid=ldapUser; uidNumber=ldapUserId; gidNumber=ldapGroupId; userPassword=ldapUserPwdHash; }
+            ]
+            + lib.concatMapStrings posixGroup [
+              { gid=ldapGroup; gidNumber=ldapGroupId; members=[]; }
+            ];
+          };
+        };
+
+        # NOTE: create a user account using the posixAccount objectClass.
+        posixAccount =
+          { uid
+          , uidNumber ? null
+          , gidNumber ? null
+          , cn ? ""
+          , sn ? ""
+          , userPassword ? ""
+          , loginShell ? "/bin/sh"
+          }: ''
+
+            dn: uid=${uid},ou=accounts,ou=posix,${dbSuffix}
+            objectClass: person
+            objectClass: posixAccount
+            objectClass: shadowAccount
+            cn: ${cn}
+            gecos:
+            ${if gidNumber == null then "#" else "gidNumber: ${toString gidNumber}"}
+            homeDirectory: /home/${uid}
+            loginShell: ${loginShell}
+            sn: ${sn}
+            ${if uidNumber == null then "#" else "uidNumber: ${toString uidNumber}"}
+            ${if userPassword == "" then "#" else "userPassword: ${userPassword}"}
+          '';
+
+        # NOTE: create a group using the posixGroup objectClass.
+        posixGroup =
+          { gid
+          , gidNumber
+          , members
+          }: ''
+
+            dn: cn=${gid},ou=groups,ou=posix,${dbSuffix}
+            objectClass: top
+            objectClass: posixGroup
+            gidNumber: ${toString gidNumber}
+            ${lib.concatMapStrings (member: "memberUid: ${member}\n") members}
+          '';
+      in
       {
         virtualisation.memorySize = 256;
         virtualisation.vlans = [ 1 ];
         networking.firewall.allowedTCPPorts = [ 389 ];
         services.openldap.enable = true;
-        services.openldap.dataDir = dbPath;
+        services.openldap.dataDir = "/var/db/openldap";
+        services.openldap.configDir = "/var/db/slapd";
         services.openldap.urlList = [
-          serverUri
+          "ldap:///"
+          "ldapi:///"
         ];
-        services.openldap.extraConfig = ''
-          include ${pkgs.openldap.out}/etc/schema/core.schema
-          include ${pkgs.openldap.out}/etc/schema/cosine.schema
-          include ${pkgs.openldap.out}/etc/schema/inetorgperson.schema
-          include ${pkgs.openldap.out}/etc/schema/nis.schema
-
-          database mdb
-          suffix ${dbSuffix}
-          rootdn ${dbAdminDn}
-          rootpw ${dbAdminPwd}
-          directory ${dbPath}
-        '';
+        systemd.services.openldap = {
+          preStart = ''
+              set -e
+              # NOTE: slapd's config is always re-initialized.
+              rm -rf "${openldap.configDir}"/cn=config \
+                     "${openldap.configDir}"/cn=config.ldif
+              install -D -d -m 0700 -o "${openldap.user}" -g "${openldap.group}" "${openldap.configDir}"
+              # NOTE: olcDbDirectory must be created before adding the config.
+              '' +
+              unlinesAttrs (olcSuffix: {data, ...}: ''
+                # NOTE: database is always re-initialized.
+                rm -rf "${openldap.dataDir}/${olcSuffix}"
+                install -D -d -m 0700 -o "${openldap.user}" -g "${openldap.group}" \
+                 "${openldap.dataDir}/${olcSuffix}"
+                '') slapdDatabases
+              + ''
+              # NOTE: slapd is supposed to be stopped while in preStart,
+              #       hence slap* commands can safely be used.
+              umask 0077
+              ${pkgs.openldap}/bin/slapadd -n 0 \
+               -F "${openldap.configDir}" \
+               -l ${slapdConfig}
+              chown -R "${openldap.user}:${openldap.group}" "${openldap.configDir}"
+              # NOTE: slapadd(8): To populate the config database slapd-config(5),
+              #                   use -n 0 as it is always the first database.
+              #                   It must physically exist on the filesystem prior to this, however.
+            '' +
+            unlinesAttrs (olcSuffix: {data, ...}: ''
+              # NOTE: load database ${olcSuffix}
+              # (as root to avoid depending on sudo or chpst)
+              ${pkgs.openldap}/bin/slapadd \
+               -F "${openldap.configDir}" \
+               -l ${pkgs.writeText "data.ldif" data}
+              '' + ''
+              # NOTE: redundant with default openldap's preStart, but do not harm.
+              chown -R "${openldap.user}:${openldap.group}" \
+               "${openldap.dataDir}/${olcSuffix}"
+            '') slapdDatabases;
+        };
       };
 
     client1 = mkClient true; # use nss_pam_ldapd
@@ -91,15 +293,91 @@ in
   };
 
   testScript = ''
-    startAll;
+    $server->start;
     $server->waitForUnit("default.target");
+
+    subtest "slapd", sub {
+      subtest "auth as database admin with SASL and check a POSIX account", sub {
+        $server->succeed(join ' ', 'test',
+         '"$(ldapsearch -LLL -H ldapi:// -Y EXTERNAL',
+             '-b \'uid=${ldapUser},ou=accounts,ou=posix,${dbSuffix}\' ',
+             '-s base uidNumber |',
+           'sed -ne \'s/^uidNumber: \\(.*\\)/\\1/p\' ',
+         ')" -eq ${toString ldapUserId}');
+      };
+      subtest "auth as database admin with password and check a POSIX account", sub {
+        $server->succeed(join ' ', 'test',
+         '"$(ldapsearch -LLL -H ldap://server',
+             '-D \'cn=admin,${dbSuffix}\' -w \'${dbAdminPwd}\' ',
+             '-b \'uid=${ldapUser},ou=accounts,ou=posix,${dbSuffix}\' ',
+             '-s base uidNumber |',
+           'sed -ne \'s/^uidNumber: \\(.*\\)/\\1/p\' ',
+         ')" -eq ${toString ldapUserId}');
+      };
+    };
+
+    $client1->start;
     $client1->waitForUnit("default.target");
-    $client2->waitForUnit("default.target");
 
-    $server->succeed("ldapadd -D '${dbAdminDn}' -w ${dbAdminPwd} -H ${serverUri} -f '${setupLdif}'");
+    subtest "password", sub {
+      subtest "su with password to a POSIX account", sub {
+        $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';',
+          'spawn su "${ldapUser}"',
+          'expect "Password:"',
+          'send "${ldapUserPwd}\n"',
+          'expect "*"',
+          'send "whoami\n"',
+          'expect -ex "${ldapUser}" {exit}',
+          'exit 1' . "'");
+      };
+      subtest "change password of a POSIX account as root", sub {
+        $client1->succeed("chpasswd <<<'${ldapUser}:new-password'");
+        $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';',
+          'spawn su "${ldapUser}"',
+          'expect "Password:"',
+          'send "new-password\n"',
+          'expect "*"',
+          'send "whoami\n"',
+          'expect -ex "${ldapUser}" {exit}',
+          'exit 1' . "'");
+        $client1->succeed('chpasswd <<<\'${ldapUser}:${ldapUserPwd}\' ');
+      };
+      subtest "change password of a POSIX account from itself", sub {
+        $client1->succeed('chpasswd <<<\'${ldapUser}:${ldapUserPwd}\' ');
+        $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';',
+          'spawn su --login ${ldapUser} -c passwd',
+          'expect "Password: "',
+          'send "${ldapUserPwd}\n"',
+          'expect "(current) UNIX password: "',
+          'send "${ldapUserPwd}\n"',
+          'expect "New password: "',
+          'send "new-password\n"',
+          'expect "Retype new password: "',
+          'send "new-password\n"',
+          'expect "passwd: password updated successfully" {exit}',
+          'exit 1' . "'");
+        $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';',
+          'spawn su "${ldapUser}"',
+          'expect "Password:"',
+          'send "${ldapUserPwd}\n"',
+          'expect "su: Authentication failure" {exit}',
+          'exit 1' . "'");
+        $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';',
+          'spawn su "${ldapUser}"',
+          'expect "Password:"',
+          'send "new-password\n"',
+          'expect "*"',
+          'send "whoami\n"',
+          'expect -ex "${ldapUser}" {exit}',
+          'exit 1' . "'");
+        $client1->succeed('chpasswd <<<\'${ldapUser}:${ldapUserPwd}\' ');
+      };
+    };
+
+    $client2->start;
+    $client2->waitForUnit("default.target");
 
-    # NSS tests
-    subtest "nss", sub {
+    subtest "NSS", sub {
         $client1->succeed("test \"\$(id -u '${ldapUser}')\" -eq ${toString ldapUserId}");
         $client1->succeed("test \"\$(id -u -n '${ldapUser}')\" = '${ldapUser}'");
         $client1->succeed("test \"\$(id -g '${ldapUser}')\" -eq ${toString ldapGroupId}");
@@ -110,8 +388,7 @@ in
         $client2->succeed("test \"\$(id -g -n '${ldapUser}')\" = '${ldapGroup}'");
     };
 
-    # PAM tests
-    subtest "pam", sub {
+    subtest "PAM", sub {
         $client1->succeed("echo ${ldapUserPwd} | su -l '${ldapUser}' -c true");
         $client2->succeed("echo ${ldapUserPwd} | su -l '${ldapUser}' -c true");
     };
diff --git a/nixos/tests/login.nix b/nixos/tests/login.nix
index 3dbb494b689..9844ad492e8 100644
--- a/nixos/tests/login.nix
+++ b/nixos/tests/login.nix
@@ -3,7 +3,7 @@ import ./make-test.nix ({ pkgs, latestKernel ? false, ... }:
 {
   name = "login";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   machine =
diff --git a/nixos/tests/matrix-synapse.nix b/nixos/tests/matrix-synapse.nix
index 8504a7c0d05..882e4b75814 100644
--- a/nixos/tests/matrix-synapse.nix
+++ b/nixos/tests/matrix-synapse.nix
@@ -1,4 +1,32 @@
-import ./make-test.nix ({ pkgs, ... } : {
+import ./make-test.nix ({ pkgs, ... } : let
+
+
+  runWithOpenSSL = file: cmd: pkgs.runCommand file {
+    buildInputs = [ pkgs.openssl ];
+  } cmd;
+
+
+  ca_key = runWithOpenSSL "ca-key.pem" "openssl genrsa -out $out 2048";
+  ca_pem = runWithOpenSSL "ca.pem" ''
+    openssl req \
+      -x509 -new -nodes -key ${ca_key} \
+      -days 10000 -out $out -subj "/CN=snakeoil-ca"
+  '';
+  key = runWithOpenSSL "matrix_key.pem" "openssl genrsa -out $out 2048";
+  csr = runWithOpenSSL "matrix.csr" ''
+    openssl req \
+       -new -key ${key} \
+       -out $out -subj "/CN=localhost" \
+  '';
+  cert = runWithOpenSSL "matrix_cert.pem" ''
+    openssl x509 \
+      -req -in ${csr} \
+      -CA ${ca_pem} -CAkey ${ca_key} \
+      -CAcreateserial -out $out \
+      -days 365
+  '';
+
+in {
 
   name = "matrix-synapse";
   meta = with pkgs.stdenv.lib.maintainers; {
@@ -8,23 +36,31 @@ import ./make-test.nix ({ pkgs, ... } : {
   nodes = {
     # Since 0.33.0, matrix-synapse doesn't allow underscores in server names
     serverpostgres = args: {
-      services.matrix-synapse.enable = true;
-      services.matrix-synapse.database_type = "psycopg2";
+      services.matrix-synapse = {
+        enable = true;
+        database_type = "psycopg2";
+        tls_certificate_path = "${cert}";
+        tls_private_key_path = "${key}";
+      };
     };
 
     serversqlite = args: {
-      services.matrix-synapse.enable = true;
-      services.matrix-synapse.database_type = "sqlite3";
+      services.matrix-synapse = {
+        enable = true;
+        database_type = "sqlite3";
+        tls_certificate_path = "${cert}";
+        tls_private_key_path = "${key}";
+      };
     };
   };
 
   testScript = ''
     startAll;
     $serverpostgres->waitForUnit("matrix-synapse.service");
-    $serverpostgres->waitUntilSucceeds("curl -Lk https://localhost:8448/");
+    $serverpostgres->waitUntilSucceeds("curl -L --cacert ${ca_pem} https://localhost:8448/");
     $serverpostgres->requireActiveUnit("postgresql.service");
     $serversqlite->waitForUnit("matrix-synapse.service");
-    $serversqlite->waitUntilSucceeds("curl -Lk https://localhost:8448/");
+    $serversqlite->waitUntilSucceeds("curl -L --cacert ${ca_pem} https://localhost:8448/");
     $serversqlite->mustSucceed("[ -e /var/lib/matrix-synapse/homeserver.db ]");
   '';
 
diff --git a/nixos/tests/misc.nix b/nixos/tests/misc.nix
index 3ad55651b11..ca28bc31cf1 100644
--- a/nixos/tests/misc.nix
+++ b/nixos/tests/misc.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({ pkgs, ...} : rec {
   name = "misc";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   foo = pkgs.writeText "foo" "Hello World";
diff --git a/nixos/tests/mongodb.nix b/nixos/tests/mongodb.nix
index 2f380ff543e..c9439b65292 100644
--- a/nixos/tests/mongodb.nix
+++ b/nixos/tests/mongodb.nix
@@ -8,7 +8,7 @@ import ./make-test.nix ({ pkgs, ...} : let
 in {
   name = "mongodb";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ bluescreen303 offline wkennington cstrahan rvl ];
+    maintainers = [ bluescreen303 offline cstrahan rvl ];
   };
 
   nodes = {
diff --git a/nixos/tests/mumble.nix b/nixos/tests/mumble.nix
index 8146453bfd5..dadd16fd9a0 100644
--- a/nixos/tests/mumble.nix
+++ b/nixos/tests/mumble.nix
@@ -9,7 +9,7 @@ in
 {
   name = "mumble";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ thoughtpolice eelco chaoflow ];
+    maintainers = [ thoughtpolice eelco ];
   };
 
   nodes = {
diff --git a/nixos/tests/munin.nix b/nixos/tests/munin.nix
index 9f66005292a..31374aaf77e 100644
--- a/nixos/tests/munin.nix
+++ b/nixos/tests/munin.nix
@@ -4,7 +4,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "munin";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ domenkozar eelco chaoflow ];
+    maintainers = [ domenkozar eelco ];
   };
 
   nodes = {
@@ -15,9 +15,7 @@ import ./make-test.nix ({ pkgs, ...} : {
            munin-node = {
              enable = true;
              # disable a failing plugin to prevent irrelevant error message, see #23049
-             extraConfig = ''
-               ignore_file ^apc_nis$
-             '';
+             disabledPlugins = [ "apc_nis" ];
            };
            munin-cron = {
             enable = true;
diff --git a/nixos/tests/mysql-replication.nix b/nixos/tests/mysql-replication.nix
index 84d70cf3524..c75a862106f 100644
--- a/nixos/tests/mysql-replication.nix
+++ b/nixos/tests/mysql-replication.nix
@@ -8,7 +8,7 @@ in
 {
   name = "mysql-replication";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow shlevy ];
+    maintainers = [ eelco shlevy ];
   };
 
   nodes = {
diff --git a/nixos/tests/mysql.nix b/nixos/tests/mysql.nix
index 7251c4a8649..1a611779366 100644
--- a/nixos/tests/mysql.nix
+++ b/nixos/tests/mysql.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "mysql";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow shlevy ];
+    maintainers = [ eelco shlevy ];
   };
 
   nodes = {
diff --git a/nixos/tests/nat.nix b/nixos/tests/nat.nix
index 04b4f0f045f..51d9cf166bb 100644
--- a/nixos/tests/nat.nix
+++ b/nixos/tests/nat.nix
@@ -24,7 +24,7 @@ import ./make-test.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false,
     name = "nat" + (if withFirewall then "WithFirewall" else "Standalone")
                  + (lib.optionalString withConntrackHelpers "withConntrackHelpers");
     meta = with pkgs.stdenv.lib.maintainers; {
-      maintainers = [ eelco chaoflow rob wkennington ];
+      maintainers = [ eelco rob ];
     };
 
     nodes =
diff --git a/nixos/tests/ndppd.nix b/nixos/tests/ndppd.nix
new file mode 100644
index 00000000000..c53ff93a91f
--- /dev/null
+++ b/nixos/tests/ndppd.nix
@@ -0,0 +1,60 @@
+import ./make-test.nix ({ pkgs, lib, ...} : {
+  name = "ndppd";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ fpletz ];
+  };
+
+  nodes = {
+    upstream = { pkgs, ... }: {
+      environment.systemPackages = [ pkgs.tcpdump ];
+      networking.useDHCP = false;
+      networking.interfaces = {
+        eth1 = {
+          ipv6.addresses = [
+            { address = "fd23::1"; prefixLength = 112; }
+          ];
+          ipv6.routes = [
+            { address = "fd42::";
+              prefixLength = 112;
+            }
+          ];
+        };
+      };
+    };
+    server = { pkgs, ... }: {
+      boot.kernel.sysctl = {
+        "net.ipv6.conf.all.forwarding" = "1";
+        "net.ipv6.conf.default.forwarding" = "1";
+      };
+      environment.systemPackages = [ pkgs.tcpdump ];
+      networking.useDHCP = false;
+      networking.interfaces = {
+        eth1 = {
+          ipv6.addresses = [
+            { address = "fd23::2"; prefixLength = 112; }
+          ];
+        };
+      };
+      services.ndppd = {
+        enable = true;
+        proxies."eth1".rules."fd42::/112" = {};
+      };
+      containers.client = {
+        autoStart = true;
+        privateNetwork = true;
+        hostAddress = "192.168.255.1";
+        localAddress = "192.168.255.2";
+        hostAddress6 = "fd42::1";
+        localAddress6 = "fd42::2";
+        config = {};
+      };
+    };
+  };
+
+  testScript = ''
+    startAll;
+    $server->waitForUnit("multi-user.target");
+    $upstream->waitForUnit("multi-user.target");
+    $upstream->waitUntilSucceeds("ping -c5 fd42::2");
+  '';
+})
diff --git a/nixos/tests/neo4j.nix b/nixos/tests/neo4j.nix
new file mode 100644
index 00000000000..86ed8970517
--- /dev/null
+++ b/nixos/tests/neo4j.nix
@@ -0,0 +1,20 @@
+import ./make-test.nix {
+  name = "neo4j";
+
+  nodes = {
+    master =
+      { ... }:
+
+      {
+        services.neo4j.enable = true;
+      };
+  };
+
+  testScript = ''
+    startAll;
+
+    $master->waitForUnit("neo4j");
+    $master->sleep(20); # Hopefully this is long enough!!
+    $master->succeed("curl http://localhost:7474/");
+  '';
+}
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index e689eadf1dd..ed9f287d558 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -606,7 +606,4 @@ let
 
 in mapAttrs (const (attrs: makeTest (attrs // {
   name = "${attrs.name}-Networking-${if networkd then "Networkd" else "Scripted"}";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ wkennington ];
-  };
 }))) testCases
diff --git a/nixos/tests/nfs.nix b/nixos/tests/nfs.nix
index 0ef44f1a489..2f655336e75 100644
--- a/nixos/tests/nfs.nix
+++ b/nixos/tests/nfs.nix
@@ -20,7 +20,7 @@ in
 {
   name = "nfs";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow wkennington ];
+    maintainers = [ eelco ];
   };
 
   nodes =
diff --git a/nixos/tests/nginx-sso.nix b/nixos/tests/nginx-sso.nix
new file mode 100644
index 00000000000..e19992cb6bf
--- /dev/null
+++ b/nixos/tests/nginx-sso.nix
@@ -0,0 +1,44 @@
+import ./make-test.nix ({ pkgs, ... }: {
+  name = "nginx-sso";
+  meta = {
+    maintainers = with pkgs.stdenv.lib.maintainers; [ delroth ];
+  };
+
+  machine = {
+    services.nginx.sso = {
+      enable = true;
+      configuration = {
+        listen = { addr = "127.0.0.1"; port = 8080; };
+
+        providers.token.tokens = {
+          myuser = "MyToken";
+        };
+
+        acl = {
+          rule_sets = [
+            {
+              rules = [ { field = "x-application"; equals = "MyApp"; } ];
+              allow = [ "myuser" ];
+            }
+          ];
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    startAll;
+
+    $machine->waitForUnit("nginx-sso.service");
+    $machine->waitForOpenPort(8080);
+
+    # No valid user -> 401.
+    $machine->fail("curl -sSf http://localhost:8080/auth");
+
+    # Valid user but no matching ACL -> 403.
+    $machine->fail("curl -sSf -H 'Authorization: Token MyToken' http://localhost:8080/auth");
+
+    # Valid user and matching ACL -> 200.
+    $machine->succeed("curl -sSf -H 'Authorization: Token MyToken' -H 'X-Application: MyApp' http://localhost:8080/auth");
+  '';
+})
diff --git a/nixos/tests/openssh.nix b/nixos/tests/openssh.nix
index c66b90b802d..8b9e2170f15 100644
--- a/nixos/tests/openssh.nix
+++ b/nixos/tests/openssh.nix
@@ -5,7 +5,7 @@ let inherit (import ./ssh-keys.nix pkgs)
 in {
   name = "openssh";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ aszlig eelco chaoflow ];
+    maintainers = [ aszlig eelco ];
   };
 
   nodes = {
@@ -34,6 +34,24 @@ in {
         ];
       };
 
+    server_localhost_only =
+      { ... }:
+
+      {
+        services.openssh = {
+          enable = true; listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+        };
+      };
+
+    server_localhost_only_lazy =
+      { ... }:
+
+      {
+        services.openssh = {
+          enable = true; startWhenNeeded = true; listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+        };
+      };
+
     client =
       { ... }: { };
 
@@ -77,5 +95,10 @@ in {
                        " server_lazy true");
 
     };
+
+    subtest "localhost-only", sub {
+      $server_localhost_only->succeed("ss -nlt | grep '127.0.0.1:22'");
+      $server_localhost_only_lazy->succeed("ss -nlt | grep '127.0.0.1:22'");
+    }
   '';
 })
diff --git a/nixos/tests/openstack-image.nix b/nixos/tests/openstack-image.nix
new file mode 100644
index 00000000000..d0225016ab7
--- /dev/null
+++ b/nixos/tests/openstack-image.nix
@@ -0,0 +1,88 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing.nix { inherit system pkgs; };
+with pkgs.lib;
+
+with import common/ec2.nix { inherit makeTest pkgs; };
+
+let
+  image =
+    (import ../lib/eval-config.nix {
+      inherit system;
+      modules = [
+        ../maintainers/scripts/openstack/openstack-image.nix
+        ../modules/testing/test-instrumentation.nix
+        ../modules/profiles/qemu-guest.nix
+      ];
+    }).config.system.build.openstackImage;
+
+  sshKeys = import ./ssh-keys.nix pkgs;
+  snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
+  snakeOilPublicKey = sshKeys.snakeOilPublicKey;
+
+in {
+  metadata = makeEc2Test {
+    name = "openstack-ec2-metadata";
+    inherit image;
+    sshPublicKey = snakeOilPublicKey;
+    userData = ''
+      SSH_HOST_ED25519_KEY_PUB:${snakeOilPublicKey}
+      SSH_HOST_ED25519_KEY:${replaceStrings ["\n"] ["|"] snakeOilPrivateKey}
+    '';
+    script = ''
+      $machine->start;
+      $machine->waitForFile("/etc/ec2-metadata/user-data");
+      $machine->waitForUnit("sshd.service");
+
+      $machine->succeed("grep unknown /etc/ec2-metadata/ami-manifest-path");
+
+      # We have no keys configured on the client side yet, so this should fail
+      $machine->fail("ssh -o BatchMode=yes localhost exit");
+
+      # Let's install our client private key
+      $machine->succeed("mkdir -p ~/.ssh");
+
+      $machine->succeed("echo '${snakeOilPrivateKey}' > ~/.ssh/id_ed25519");
+      $machine->succeed("chmod 600 ~/.ssh/id_ed25519");
+
+      # We haven't configured the host key yet, so this should still fail
+      $machine->fail("ssh -o BatchMode=yes localhost exit");
+
+      # Add the host key; ssh should finally succeed
+      $machine->succeed("echo localhost,127.0.0.1 ${snakeOilPublicKey} > ~/.ssh/known_hosts");
+      $machine->succeed("ssh -o BatchMode=yes localhost exit");
+
+      # Just to make sure resizing is idempotent.
+      $machine->shutdown;
+      $machine->start;
+      $machine->waitForFile("/etc/ec2-metadata/user-data");
+    '';
+  };
+
+  userdata = makeEc2Test {
+    name = "openstack-ec2-metadata";
+    inherit image;
+    sshPublicKey = snakeOilPublicKey;
+    userData = ''
+      { pkgs, ... }:
+      {
+        imports = [
+          <nixpkgs/nixos/modules/virtualisation/openstack-config.nix>
+          <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
+          <nixpkgs/nixos/modules/profiles/qemu-guest.nix>
+        ];
+        environment.etc.testFile = {
+          text = "whoa";
+        };
+      }
+    '';
+    script = ''
+      $machine->start;
+      $machine->waitForFile("/etc/testFile");
+      $machine->succeed("cat /etc/testFile | grep -q 'whoa'");
+    '';
+  };
+}
diff --git a/nixos/tests/osrm-backend.nix b/nixos/tests/osrm-backend.nix
new file mode 100644
index 00000000000..6e2d098d4ad
--- /dev/null
+++ b/nixos/tests/osrm-backend.nix
@@ -0,0 +1,53 @@
+import ./make-test.nix ({ pkgs, lib, ... }:
+let
+  port = 5000;
+in {
+  name = "osrm-backend";
+  meta.maintainers = [ lib.maintainers.erictapen ];
+
+  machine = { config, pkgs, ... }:{
+
+    services.osrm = {
+      enable = true;
+      inherit port;
+      dataFile = let
+        filename = "monaco";
+        osrm-data = pkgs.stdenv.mkDerivation {
+          name = "osrm-data";
+
+          buildInputs = [ pkgs.osrm-backend ];
+
+          # This is a pbf file of monaco, downloaded at 2019-01-04 from
+          # http://download.geofabrik.de/europe/monaco-latest.osm.pbf
+          # as apparently no provider of OSM files guarantees immutability,
+          # this is hosted as a gist on GitHub.
+          src = pkgs.fetchgit {
+            url = "https://gist.github.com/erictapen/01e39f73a6c856eac53ba809a94cdb83";
+            rev = "9b1ff0f24deb40e5cf7df51f843dbe860637b8ce";
+            sha256 = "1scqhmrfnpwsy5i2a9jpggqnvfgj4hv9p4qyvc79321pzkbv59nx";
+          };
+
+          buildCommand = ''
+            cp $src/${filename}.osm.pbf .
+            ${pkgs.osrm-backend}/bin/osrm-extract -p ${pkgs.osrm-backend}/share/osrm/profiles/car.lua ${filename}.osm.pbf
+            ${pkgs.osrm-backend}/bin/osrm-partition ${filename}.osrm
+            ${pkgs.osrm-backend}/bin/osrm-customize ${filename}.osrm
+            mkdir -p $out
+            cp ${filename}* $out/
+          '';
+        };
+      in "${osrm-data}/${filename}.osrm";
+    };
+
+    environment.systemPackages = [ pkgs.jq ];
+  };
+
+  testScript = let
+    query = "http://localhost:${toString port}/route/v1/driving/7.41720,43.73304;7.42463,43.73886?steps=true";
+  in ''
+    $machine->waitForUnit("osrm.service");
+    $machine->waitForOpenPort(${toString port});
+    $machine->succeed("curl --silent '${query}' | jq .waypoints[0].name | grep -F 'Boulevard Rainier III'");
+    $machine->succeed("curl --silent '${query}' | jq .waypoints[1].name | grep -F 'Avenue de la Costa'");
+  '';
+})
diff --git a/nixos/tests/overlayfs.nix b/nixos/tests/overlayfs.nix
new file mode 100644
index 00000000000..99bb6b0f553
--- /dev/null
+++ b/nixos/tests/overlayfs.nix
@@ -0,0 +1,57 @@
+import ./make-test.nix ({ pkgs, ... }: {
+  name = "overlayfs";
+  meta.maintainers = with pkgs.stdenv.lib.maintainers; [ bachp ];
+
+  machine = { pkgs, ... }: {
+    virtualisation.emptyDiskImages = [ 512 ];
+    networking.hostId = "deadbeef";
+    environment.systemPackages = with pkgs; [ parted ];
+  };
+
+  testScript = ''
+    $machine->succeed("ls /dev");
+
+    $machine->succeed("mkdir -p /tmp/mnt");
+
+    # Test ext4 + overlayfs
+    $machine->succeed(
+
+      "mkfs.ext4 -F -L overlay-ext4 /dev/vdb",
+      "mount -t ext4 /dev/vdb /tmp/mnt",
+
+      "mkdir -p /tmp/mnt/upper /tmp/mnt/lower /tmp/mnt/work /tmp/mnt/merged",
+
+      # Setup some existing files
+      "echo 'Replace' > /tmp/mnt/lower/replace.txt",
+      "echo 'Append' > /tmp/mnt/lower/append.txt",
+      "echo 'Overwrite' > /tmp/mnt/lower/overwrite.txt",
+
+      "mount -t overlay overlay -o lowerdir=/tmp/mnt/lower,upperdir=/tmp/mnt/upper,workdir=/tmp/mnt/work /tmp/mnt/merged",
+
+      # Test new
+      "echo 'New' > /tmp/mnt/merged/new.txt",
+      "[[ \"\$(cat /tmp/mnt/merged/new.txt)\" == \"New\" ]]",
+
+      # Test replace
+      "[[ \"\$(cat /tmp/mnt/merged/replace.txt)\" == \"Replace\" ]]",
+      "echo 'Replaced' > /tmp/mnt/merged/replace-tmp.txt",
+      "mv /tmp/mnt/merged/replace-tmp.txt /tmp/mnt/merged/replace.txt",
+      "[[ \"\$(cat /tmp/mnt/merged/replace.txt)\" == \"Replaced\" ]]",
+
+      # Overwrite
+      "[[ \"\$(cat /tmp/mnt/merged/overwrite.txt)\" == \"Overwrite\" ]]",
+      "echo 'Overwritten' > /tmp/mnt/merged/overwrite.txt",
+      "[[ \"\$(cat /tmp/mnt/merged/overwrite.txt)\" == \"Overwritten\" ]]",
+
+      # Test append
+      "[[ \"\$(cat /tmp/mnt/merged/append.txt)\" == \"Append\" ]]",
+      "echo 'ed' >> /tmp/mnt/merged/append.txt",
+      #"cat /tmp/mnt/merged/append.txt && exit 1",
+      "[[ \"\$(cat /tmp/mnt/merged/append.txt)\" == \"Append\ned\" ]]",
+
+      "umount /tmp/mnt/merged",
+      "umount /tmp/mnt",
+      "udevadm settle"
+    );
+  '';
+})
diff --git a/nixos/tests/pam-u2f.nix b/nixos/tests/pam-u2f.nix
new file mode 100644
index 00000000000..1052a2f3b91
--- /dev/null
+++ b/nixos/tests/pam-u2f.nix
@@ -0,0 +1,23 @@
+import ./make-test.nix ({ ... }:
+
+{
+  name = "pam-u2f";
+
+  machine =
+    { ... }:
+    {
+      security.pam.u2f = {
+        control = "required";
+        cue = true;
+        debug = true;
+        enable = true;
+        interactive = true;
+      };
+    };
+
+  testScript =
+    ''
+      $machine->waitForUnit('multi-user.target');
+      $machine->succeed('egrep "auth required .*/lib/security/pam_u2f.so.*debug.*interactive.*cue" /etc/pam.d/ -R');
+    '';
+})
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
new file mode 100644
index 00000000000..c50f77f8617
--- /dev/null
+++ b/nixos/tests/pantheon.nix
@@ -0,0 +1,55 @@
+import ./make-test.nix ({ pkgs, ...} :
+
+{
+  name = "pantheon";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ worldofpeace ];
+  };
+
+  machine = { ... }:
+
+  {
+    imports = [ ./common/user-account.nix ];
+
+    services.xserver.enable = true;
+    services.xserver.desktopManager.pantheon.enable = true;
+
+    virtualisation.memorySize = 1024;
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    startAll;
+
+    # Wait for display manager to start
+    $machine->waitForText(qr/${user.description}/);
+    $machine->screenshot("lightdm");
+
+    # Log in
+    $machine->sendChars("${user.password}\n");
+    $machine->waitForFile("/home/alice/.Xauthority");
+    $machine->succeed("xauth merge ~alice/.Xauthority");
+
+    # Check if "pantheon-shell" components actually start
+    $machine->waitUntilSucceeds("pgrep gala");
+    $machine->waitForWindow(qr/gala/);
+    $machine->waitUntilSucceeds("pgrep wingpanel");
+    $machine->waitForWindow("wingpanel");
+    $machine->waitUntilSucceeds("pgrep plank");
+    $machine->waitForWindow(qr/plank/);
+
+    # Check that logging in has given the user ownership of devices.
+    $machine->succeed("getfacl /dev/snd/timer | grep -q alice");
+
+    # Open elementary terminal
+    $machine->execute("su - alice -c 'DISPLAY=:0.0 io.elementary.terminal &'");
+    $machine->waitForWindow(qr/io.elementary.terminal/);
+
+    # Take a screenshot of the desktop
+    $machine->sleep(20);
+    $machine->screenshot("screen");
+  '';
+})
diff --git a/nixos/tests/phabricator.nix b/nixos/tests/phabricator.nix
index 20b3b838aba..db23331842c 100644
--- a/nixos/tests/phabricator.nix
+++ b/nixos/tests/phabricator.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ... }: {
   name = "phabricator";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ chaoflow ];
+    maintainers = [ ];
   };
 
   nodes = {
diff --git a/nixos/tests/postgis.nix b/nixos/tests/postgis.nix
index 49be0672a8e..294eb50b5fe 100644
--- a/nixos/tests/postgis.nix
+++ b/nixos/tests/postgis.nix
@@ -12,7 +12,9 @@ import ./make-test.nix ({ pkgs, ...} : {
         services.postgresql = let mypg = pkgs.postgresql_11; in {
             enable = true;
             package = mypg;
-            extraPlugins = [ (pkgs.postgis.override { postgresql = mypg; }) ];
+            extraPlugins = with mypg.pkgs; [
+              postgis
+            ];
         };
       };
   };
@@ -22,5 +24,6 @@ import ./make-test.nix ({ pkgs, ...} : {
     $master->waitForUnit("postgresql");
     $master->sleep(10); # Hopefully this is long enough!!
     $master->succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis;'");
+    $master->succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis_topology;'");
   '';
 })
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
index 975ba7f488e..ae5d6d095ea 100644
--- a/nixos/tests/postgresql.nix
+++ b/nixos/tests/postgresql.nix
@@ -7,7 +7,7 @@ with import ../lib/testing.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
-  postgresql-versions = pkgs.callPackages ../../pkgs/servers/sql/postgresql { };
+  postgresql-versions = import ../../pkgs/servers/sql/postgresql pkgs;
   test-sql = pkgs.writeText "postgresql-test" ''
     CREATE EXTENSION pgcrypto; -- just to check if lib loading works
     CREATE TABLE sth (
@@ -29,8 +29,8 @@ let
 
     machine = {...}:
       {
-        services.postgresql.package = postgresql-package;
         services.postgresql.enable = true;
+        services.postgresql.package = postgresql-package;
 
         services.postgresqlBackup.enable = true;
         services.postgresqlBackup.databases = optional (!backup-all) "postgres";
@@ -67,12 +67,7 @@ let
 
   };
 in
-  (mapAttrs' (name: package: { inherit name; value=make-postgresql-test name package false;}) postgresql-versions) // (
-    # just pick one version for the dump all test
-    let
-      first = head (attrNames postgresql-versions);
-      name = "${first}-backup-all";
-    in {
-      ${name} = make-postgresql-test name postgresql-versions.${first} true;
-    }
-  )
+  (mapAttrs' (name: package: { inherit name; value=make-postgresql-test name package false;}) postgresql-versions) // {
+    postgresql_11-backup-all = make-postgresql-test "postgresql_11-backup-all" postgresql-versions.postgresql_11 true;
+  }
+
diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix
index d85abf3c105..f009a7c706e 100644
--- a/nixos/tests/printing.nix
+++ b/nixos/tests/printing.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({pkgs, ... }: {
   name = "printing";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ domenkozar eelco chaoflow jgeerds ];
+    maintainers = [ domenkozar eelco ];
   };
 
   nodes = {
@@ -39,6 +39,8 @@ import ./make-test.nix ({pkgs, ... }: {
       $client->waitForUnit("cups.service");
       $client->sleep(10); # wait until cups is fully initialized
       $client->succeed("lpstat -r") =~ /scheduler is running/ or die;
+      # check local encrypted connections work without error
+      $client->succeed("lpstat -E -r") =~ /scheduler is running/ or die;
       # Test that UNIX socket is used for connections.
       $client->succeed("lpstat -H") =~ "/var/run/cups/cups.sock" or die;
       # Test that HTTP server is available too.
diff --git a/nixos/tests/proxy.nix b/nixos/tests/proxy.nix
index 18195312028..1f39e903cdd 100644
--- a/nixos/tests/proxy.nix
+++ b/nixos/tests/proxy.nix
@@ -16,7 +16,7 @@ in
 {
   name = "proxy";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes =
diff --git a/nixos/tests/quake3.nix b/nixos/tests/quake3.nix
index 75c82cca63f..fbb798515e1 100644
--- a/nixos/tests/quake3.nix
+++ b/nixos/tests/quake3.nix
@@ -22,7 +22,7 @@ in
 rec {
   name = "quake3";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ domenkozar eelco chaoflow ];
+    maintainers = [ domenkozar eelco ];
   };
 
   # TODO: lcov doesn't work atm
diff --git a/nixos/tests/rabbitmq.nix b/nixos/tests/rabbitmq.nix
index 34ab0578786..bb5932c3641 100644
--- a/nixos/tests/rabbitmq.nix
+++ b/nixos/tests/rabbitmq.nix
@@ -3,7 +3,7 @@
 import ./make-test.nix ({ pkgs, ... }: {
   name = "rabbitmq";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow offline ];
+    maintainers = [ eelco offline ];
   };
 
   nodes = {
diff --git a/nixos/tests/redmine.nix b/nixos/tests/redmine.nix
index 330f72854ca..ea72a0121d1 100644
--- a/nixos/tests/redmine.nix
+++ b/nixos/tests/redmine.nix
@@ -1,40 +1,58 @@
-import ./make-test.nix ({ pkgs, lib, ... }:
-{
-  name = "redmine";
-  meta.maintainers = [ lib.maintainers.aanderse ];
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing.nix { inherit system pkgs; };
+with pkgs.lib;
 
-  machine =
-    { config, pkgs, ... }:
-    { services.mysql.enable = true;
-      services.mysql.package = pkgs.mariadb;
-      services.mysql.ensureDatabases = [ "redmine" ];
-      services.mysql.ensureUsers = [
-        { name = "redmine";
-          ensurePermissions = { "redmine.*" = "ALL PRIVILEGES"; };
-        }
-      ];
+let
+  redmineTest = package: makeTest {
+    machine =
+      { config, pkgs, ... }:
+      { services.mysql.enable = true;
+        services.mysql.package = pkgs.mariadb;
+        services.mysql.ensureDatabases = [ "redmine" ];
+        services.mysql.ensureUsers = [
+          { name = "redmine";
+            ensurePermissions = { "redmine.*" = "ALL PRIVILEGES"; };
+          }
+        ];
 
-      services.redmine.enable = true;
-      services.redmine.database.socket = "/run/mysqld/mysqld.sock";
-      services.redmine.plugins = {
-        redmine_env_auth = pkgs.fetchurl {
-          url = https://github.com/Intera/redmine_env_auth/archive/0.6.zip;
-          sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak";
+        services.redmine.enable = true;
+        services.redmine.package = package;
+        services.redmine.database.socket = "/run/mysqld/mysqld.sock";
+        services.redmine.plugins = {
+          redmine_env_auth = pkgs.fetchurl {
+            url = https://github.com/Intera/redmine_env_auth/archive/0.7.zip;
+            sha256 = "1xb8lyarc7mpi86yflnlgyllh9hfwb9z304f19dx409gqpia99sc";
+          };
         };
-      };
-      services.redmine.themes = {
-        dkuk-redmine_alex_skin = pkgs.fetchurl {
-          url = https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip;
-          sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
+        services.redmine.themes = {
+          dkuk-redmine_alex_skin = pkgs.fetchurl {
+            url = https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip;
+            sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
+          };
         };
       };
-    };
 
-  testScript = ''
-    startAll;
+    testScript = ''
+      startAll;
+
+      $machine->waitForUnit('redmine.service');
+      $machine->waitForOpenPort('3000');
+      $machine->succeed("curl --fail http://localhost:3000/");
+    '';
+  };
+in
+{
+  redmine_3 = redmineTest pkgs.redmine // {
+    name = "redmine_3";
+    meta.maintainers = [ maintainers.aanderse ];
+  };
 
-    $machine->waitForUnit('redmine.service');
-    $machine->waitForOpenPort('3000');
-    $machine->succeed("curl --fail http://localhost:3000/");
-  '';
-})
+  redmine_4 = redmineTest pkgs.redmine_4 // {
+    name = "redmine_4";
+    meta.maintainers = [ maintainers.aanderse ];
+  };
+}
diff --git a/nixos/tests/roundcube.nix b/nixos/tests/roundcube.nix
index 178134fd9b3..ed0ebd7dd19 100644
--- a/nixos/tests/roundcube.nix
+++ b/nixos/tests/roundcube.nix
@@ -10,6 +10,8 @@ import ./make-test.nix ({ pkgs, ...} : {
         enable = true;
         hostName = "roundcube";
         database.password = "notproduction";
+        package = pkgs.roundcube.withPlugins (plugins: [ plugins.persistent_login ]);
+        plugins = [ "persistent_login" ];
       };
       services.nginx.virtualHosts.roundcube = {
         forceSSL = false;
@@ -23,6 +25,6 @@ import ./make-test.nix ({ pkgs, ...} : {
     $roundcube->waitForUnit("postgresql.service");
     $roundcube->waitForUnit("phpfpm-roundcube.service");
     $roundcube->waitForUnit("nginx.service");
-    $roundcube->succeed("curl -sSfL http://roundcube/");
+    $roundcube->succeed("curl -sSfL http://roundcube/ | grep 'Keep me logged in'");
   '';
 })
diff --git a/nixos/tests/rspamd.nix b/nixos/tests/rspamd.nix
index 396cd5b67d8..0cc94728f80 100644
--- a/nixos/tests/rspamd.nix
+++ b/nixos/tests/rspamd.nix
@@ -52,8 +52,18 @@ in
     machine = {
       services.rspamd = {
         enable = true;
-        bindSocket = [ "/run/rspamd.sock mode=0600 user=root group=root" ];
-        bindUISocket = [ "/run/rspamd-worker.sock mode=0666 user=root group=root" ];
+        workers.normal.bindSockets = [{
+          socket = "/run/rspamd.sock";
+          mode = "0600";
+          owner = "root";
+          group = "root";
+        }];
+        workers.controller.bindSockets = [{
+          socket = "/run/rspamd-worker.sock";
+          mode = "0666";
+          owner = "root";
+          group = "root";
+        }];
       };
     };
 
@@ -235,7 +245,7 @@ in
       services.rspamd = {
         enable = true;
         postfix.enable = true;
-        workers.rspamd_proxy.type = "proxy";
+        workers.rspamd_proxy.type = "rspamd_proxy";
       };
     };
     testScript = ''
diff --git a/nixos/tests/subversion.nix b/nixos/tests/subversion.nix
index 6175155cdfc..e7b99b10602 100644
--- a/nixos/tests/subversion.nix
+++ b/nixos/tests/subversion.nix
@@ -34,7 +34,7 @@ in
 {
   name = "subversion";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes =
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 32010838e67..0dba3697980 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -18,8 +18,17 @@ import ./make-test.nix ({ pkgs, ...} : {
   testScript = {nodes, ...}: let
     originalSystem = nodes.machine.config.system.build.toplevel;
     otherSystem = nodes.other.config.system.build.toplevel;
+
+    # Ensures failures pass through using pipefail, otherwise failing to
+    # switch-to-configuration is hidden by the success of `tee`.
+    stderrRunner = pkgs.writeScript "stderr-runner" ''
+      #! ${pkgs.stdenv.shell}
+      set -e
+      set -o pipefail
+      exec env -i "$@" | tee /dev/stderr
+    '';
   in ''
-    $machine->succeed("env -i ${originalSystem}/bin/switch-to-configuration test | tee /dev/stderr");
-    $machine->succeed("env -i ${otherSystem}/bin/switch-to-configuration test | tee /dev/stderr");
+    $machine->succeed("${stderrRunner} ${originalSystem}/bin/switch-to-configuration test");
+    $machine->succeed("${stderrRunner} ${otherSystem}/bin/switch-to-configuration test");
   '';
 })
diff --git a/nixos/tests/tomcat.nix b/nixos/tests/tomcat.nix
index af63c7ee8e0..8e7b886dd30 100644
--- a/nixos/tests/tomcat.nix
+++ b/nixos/tests/tomcat.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "tomcat";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes = {
diff --git a/nixos/tests/trac.nix b/nixos/tests/trac.nix
index 4599885acde..8ec11ebda2c 100644
--- a/nixos/tests/trac.nix
+++ b/nixos/tests/trac.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ... }: {
   name = "trac";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   nodes = {
diff --git a/nixos/tests/udisks2.nix b/nixos/tests/udisks2.nix
index 8bbbe286efc..dcf869908d8 100644
--- a/nixos/tests/udisks2.nix
+++ b/nixos/tests/udisks2.nix
@@ -12,7 +12,7 @@ in
 {
   name = "udisks2";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow ];
+    maintainers = [ eelco ];
   };
 
   machine =
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index 385e2939fe3..84d5f3e1530 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -379,7 +379,7 @@ let
     '';
 
     meta = with pkgs.stdenv.lib.maintainers; {
-      maintainers = [ aszlig wkennington cdepillabout ];
+      maintainers = [ aszlig cdepillabout ];
     };
   };
 
diff --git a/nixos/tests/xfce.nix b/nixos/tests/xfce.nix
index 47717e8cf7d..12d8a050d47 100644
--- a/nixos/tests/xfce.nix
+++ b/nixos/tests/xfce.nix
@@ -1,7 +1,7 @@
 import ./make-test.nix ({ pkgs, ...} : {
   name = "xfce";
   meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco chaoflow shlevy ];
+    maintainers = [ eelco shlevy ];
   };
 
   machine =