summary refs log tree commit diff
path: root/nixos/modules/services
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services')
-rw-r--r--nixos/modules/services/admin/oxidized.nix1
-rw-r--r--nixos/modules/services/amqp/activemq/default.nix8
-rw-r--r--nixos/modules/services/amqp/rabbitmq.nix14
-rw-r--r--nixos/modules/services/audio/alsa.nix9
-rw-r--r--nixos/modules/services/audio/mopidy.nix4
-rw-r--r--nixos/modules/services/audio/mpd.nix26
-rw-r--r--nixos/modules/services/audio/roon-server.nix3
-rw-r--r--nixos/modules/services/backup/bacula.nix166
-rw-r--r--nixos/modules/services/backup/borgbackup.nix55
-rw-r--r--nixos/modules/services/backup/borgbackup.xml227
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix14
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix16
-rw-r--r--nixos/modules/services/backup/restic.nix56
-rw-r--r--nixos/modules/services/backup/sanoid.nix213
-rw-r--r--nixos/modules/services/backup/syncoid.nix172
-rw-r--r--nixos/modules/services/backup/tarsnap.nix4
-rw-r--r--nixos/modules/services/cluster/hadoop/default.nix32
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix101
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dashboard.nix4
-rw-r--r--nixos/modules/services/cluster/kubernetes/apiserver.nix12
-rw-r--r--nixos/modules/services/cluster/kubernetes/controller-manager.nix5
-rw-r--r--nixos/modules/services/cluster/kubernetes/default.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix6
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix10
-rw-r--r--nixos/modules/services/cluster/kubernetes/proxy.nix3
-rw-r--r--nixos/modules/services/computing/boinc/client.nix6
-rw-r--r--nixos/modules/services/computing/foldingathome/client.nix81
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix1
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix23
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix23
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agents.nix (renamed from nixos/modules/services/continuous-integration/buildkite-agent.nix)165
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix11
-rw-r--r--nixos/modules/services/continuous-integration/gocd-agent/default.nix24
-rw-r--r--nixos/modules/services/continuous-integration/gocd-server/default.nix24
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix87
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/default.nix24
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/slave.nix22
-rw-r--r--nixos/modules/services/databases/4store-endpoint.nix74
-rw-r--r--nixos/modules/services/databases/4store.nix72
-rw-r--r--nixos/modules/services/databases/clickhouse.nix5
-rw-r--r--nixos/modules/services/databases/cockroachdb.nix16
-rw-r--r--nixos/modules/services/databases/firebird.nix7
-rw-r--r--nixos/modules/services/databases/foundationdb.nix14
-rw-r--r--nixos/modules/services/databases/influxdb.nix14
-rw-r--r--nixos/modules/services/databases/memcached.nix14
-rw-r--r--nixos/modules/services/databases/mongodb.nix7
-rw-r--r--nixos/modules/services/databases/mysql.nix126
-rw-r--r--nixos/modules/services/databases/neo4j.nix15
-rw-r--r--nixos/modules/services/databases/openldap.nix2
-rw-r--r--nixos/modules/services/databases/postgresql.nix39
-rw-r--r--nixos/modules/services/databases/postgresql.xml103
-rw-r--r--nixos/modules/services/databases/redis.nix54
-rw-r--r--nixos/modules/services/databases/victoriametrics.nix70
-rw-r--r--nixos/modules/services/databases/virtuoso.nix10
-rw-r--r--nixos/modules/services/desktops/accountsservice.nix4
-rw-r--r--nixos/modules/services/desktops/bamf.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/at-spi2-core.nix7
-rw-r--r--nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/evolution-data-server.nix6
-rw-r--r--nixos/modules/services/desktops/gnome3/glib-networking.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-keyring.nix6
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-online-miners.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix6
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-user-share.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/rygel.nix6
-rw-r--r--nixos/modules/services/desktops/gnome3/sushi.nix4
-rw-r--r--nixos/modules/services/desktops/gnome3/tracker-miners.nix5
-rw-r--r--nixos/modules/services/desktops/gnome3/tracker.nix4
-rw-r--r--nixos/modules/services/desktops/gvfs.nix4
-rw-r--r--nixos/modules/services/desktops/malcontent.nix37
-rw-r--r--nixos/modules/services/desktops/neard.nix23
-rw-r--r--nixos/modules/services/desktops/pantheon/contractor.nix41
-rw-r--r--nixos/modules/services/desktops/pantheon/files.nix31
-rw-r--r--nixos/modules/services/desktops/pipewire.nix6
-rw-r--r--nixos/modules/services/desktops/telepathy.nix4
-rw-r--r--nixos/modules/services/desktops/tumbler.nix4
-rw-r--r--nixos/modules/services/desktops/zeitgeist.nix5
-rw-r--r--nixos/modules/services/development/jupyter/default.nix6
-rw-r--r--nixos/modules/services/development/lorri.nix2
-rw-r--r--nixos/modules/services/editors/infinoted.nix15
-rw-r--r--nixos/modules/services/games/factorio.nix26
-rw-r--r--nixos/modules/services/hardware/actkbd.nix2
-rw-r--r--nixos/modules/services/hardware/bluetooth.nix34
-rw-r--r--nixos/modules/services/hardware/fwupd.nix5
-rw-r--r--nixos/modules/services/hardware/irqbalance.nix14
-rw-r--r--nixos/modules/services/hardware/ratbagd.nix7
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4.nix11
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix2
-rw-r--r--nixos/modules/services/hardware/tcsd.nix14
-rw-r--r--nixos/modules/services/hardware/thermald.nix7
-rw-r--r--nixos/modules/services/hardware/tlp.nix141
-rw-r--r--nixos/modules/services/hardware/udev.nix15
-rw-r--r--nixos/modules/services/hardware/upower.nix181
-rw-r--r--nixos/modules/services/hardware/usbmuxd.nix15
-rw-r--r--nixos/modules/services/hardware/xow.nix17
-rw-r--r--nixos/modules/services/logging/awstats.nix298
-rw-r--r--nixos/modules/services/logging/logcheck.nix7
-rw-r--r--nixos/modules/services/logging/logstash.nix5
-rw-r--r--nixos/modules/services/logging/syslog-ng.nix4
-rw-r--r--nixos/modules/services/mail/dovecot.nix279
-rw-r--r--nixos/modules/services/mail/dspam.nix14
-rw-r--r--nixos/modules/services/mail/exim.nix6
-rw-r--r--nixos/modules/services/mail/mailman.nix285
-rw-r--r--nixos/modules/services/mail/mlmmj.nix6
-rw-r--r--nixos/modules/services/mail/nullmailer.nix7
-rw-r--r--nixos/modules/services/mail/opendkim.nix17
-rw-r--r--nixos/modules/services/mail/postfix.nix26
-rw-r--r--nixos/modules/services/mail/postgrey.nix11
-rw-r--r--nixos/modules/services/mail/postsrsd.nix14
-rw-r--r--nixos/modules/services/mail/roundcube.nix98
-rw-r--r--nixos/modules/services/mail/rspamd.nix8
-rw-r--r--nixos/modules/services/mail/rss2email.nix2
-rw-r--r--nixos/modules/services/mail/spamassassin.nix38
-rw-r--r--nixos/modules/services/mail/sympa.nix594
-rw-r--r--nixos/modules/services/misc/airsonic.nix1
-rw-r--r--nixos/modules/services/misc/ankisyncd.nix79
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix5
-rw-r--r--nixos/modules/services/misc/autofs.nix1
-rw-r--r--nixos/modules/services/misc/autorandr.nix2
-rw-r--r--nixos/modules/services/misc/bepasty.nix18
-rw-r--r--nixos/modules/services/misc/cgminer.nix15
-rw-r--r--nixos/modules/services/misc/couchpotato.nix11
-rw-r--r--nixos/modules/services/misc/devmon.nix7
-rw-r--r--nixos/modules/services/misc/dictd.nix10
-rw-r--r--nixos/modules/services/misc/disnix.nix10
-rw-r--r--nixos/modules/services/misc/dysnomia.nix2
-rw-r--r--nixos/modules/services/misc/etcd.nix3
-rw-r--r--nixos/modules/services/misc/ethminer.nix4
-rw-r--r--nixos/modules/services/misc/exhibitor.nix3
-rw-r--r--nixos/modules/services/misc/felix.nix15
-rw-r--r--nixos/modules/services/misc/folding-at-home.nix68
-rw-r--r--nixos/modules/services/misc/freeswitch.nix103
-rw-r--r--nixos/modules/services/misc/gitea.nix23
-rw-r--r--nixos/modules/services/misc/gitlab.nix19
-rw-r--r--nixos/modules/services/misc/gpsd.nix10
-rw-r--r--nixos/modules/services/misc/headphones.nix24
-rw-r--r--nixos/modules/services/misc/home-assistant.nix25
-rw-r--r--nixos/modules/services/misc/ihaskell.nix1
-rw-r--r--nixos/modules/services/misc/mame.nix67
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix80
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml225
-rw-r--r--nixos/modules/services/misc/mediatomb.nix24
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix85
-rw-r--r--nixos/modules/services/misc/nixos-manual.nix73
-rw-r--r--nixos/modules/services/misc/nzbget.nix6
-rw-r--r--nixos/modules/services/misc/octoprint.nix14
-rw-r--r--nixos/modules/services/misc/paperless.nix14
-rw-r--r--nixos/modules/services/misc/parsoid.nix29
-rw-r--r--nixos/modules/services/misc/pykms.nix4
-rw-r--r--nixos/modules/services/misc/redmine.nix20
-rw-r--r--nixos/modules/services/misc/ripple-data-api.nix5
-rw-r--r--nixos/modules/services/misc/rippled.nix5
-rw-r--r--nixos/modules/services/misc/rogue.nix62
-rw-r--r--nixos/modules/services/misc/safeeyes.nix5
-rw-r--r--nixos/modules/services/misc/serviio.nix13
-rw-r--r--nixos/modules/services/misc/sickbeard.nix24
-rw-r--r--nixos/modules/services/misc/siproxd.nix3
-rw-r--r--nixos/modules/services/misc/sssd.nix6
-rw-r--r--nixos/modules/services/misc/svnserve.nix1
-rw-r--r--nixos/modules/services/misc/synergy.nix16
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix16
-rw-r--r--nixos/modules/services/misc/tautulli.nix4
-rw-r--r--nixos/modules/services/misc/uhub.nix24
-rw-r--r--nixos/modules/services/misc/zoneminder.nix4
-rw-r--r--nixos/modules/services/misc/zookeeper.nix3
-rw-r--r--nixos/modules/services/monitoring/cadvisor.nix1
-rw-r--r--nixos/modules/services/monitoring/collectd.nix7
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix21
-rw-r--r--nixos/modules/services/monitoring/dd-agent/dd-agent.nix60
-rw-r--r--nixos/modules/services/monitoring/fusion-inventory.nix3
-rw-r--r--nixos/modules/services/monitoring/graphite.nix140
-rw-r--r--nixos/modules/services/monitoring/heapster.nix3
-rw-r--r--nixos/modules/services/monitoring/munin.nix10
-rw-r--r--nixos/modules/services/monitoring/nagios.nix153
-rw-r--r--nixos/modules/services/monitoring/netdata.nix47
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix43
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix34
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix35
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/json.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mail.nix9
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix66
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/minio.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginx.nix13
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postfix.nix6
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/snmp.nix16
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unifi.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/varnish.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix47
-rw-r--r--nixos/modules/services/monitoring/statsd.nix3
-rw-r--r--nixos/modules/services/monitoring/sysstat.nix12
-rw-r--r--nixos/modules/services/monitoring/telegraf.nix5
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix84
-rw-r--r--nixos/modules/services/monitoring/ups.nix45
-rw-r--r--nixos/modules/services/monitoring/zabbix-server.nix5
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix7
-rw-r--r--nixos/modules/services/network-filesystems/davfs2.nix23
-rw-r--r--nixos/modules/services/network-filesystems/drbd.nix6
-rw-r--r--nixos/modules/services/network-filesystems/kbfs.nix90
-rw-r--r--nixos/modules/services/network-filesystems/netatalk.nix11
-rw-r--r--nixos/modules/services/network-filesystems/nfsd.nix4
-rw-r--r--nixos/modules/services/network-filesystems/openafs/client.nix2
-rw-r--r--nixos/modules/services/network-filesystems/rsyncd.nix10
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix8
-rw-r--r--nixos/modules/services/network-filesystems/xtreemfs.nix6
-rw-r--r--nixos/modules/services/network-filesystems/yandex-disk.nix1
-rw-r--r--nixos/modules/services/networking/3proxy.nix426
-rw-r--r--nixos/modules/services/networking/amuled.nix1
-rw-r--r--nixos/modules/services/networking/babeld.nix7
-rw-r--r--nixos/modules/services/networking/bind.nix12
-rw-r--r--nixos/modules/services/networking/bitlbee.nix7
-rw-r--r--nixos/modules/services/networking/charybdis.nix6
-rw-r--r--nixos/modules/services/networking/cjdns.nix52
-rw-r--r--nixos/modules/services/networking/cntlm.nix7
-rw-r--r--nixos/modules/services/networking/connman.nix51
-rw-r--r--nixos/modules/services/networking/corerad.nix46
-rw-r--r--nixos/modules/services/networking/coturn.nix14
-rw-r--r--nixos/modules/services/networking/ddclient.nix8
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix37
-rw-r--r--nixos/modules/services/networking/dhcpd.nix4
-rw-r--r--nixos/modules/services/networking/dnscache.nix10
-rw-r--r--nixos/modules/services/networking/dnschain.nix7
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy.nix328
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy.xml66
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix61
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix3
-rw-r--r--nixos/modules/services/networking/dnsmasq.nix3
-rw-r--r--nixos/modules/services/networking/ejabberd.nix14
-rw-r--r--nixos/modules/services/networking/fakeroute.nix2
-rw-r--r--nixos/modules/services/networking/firewall.nix25
-rw-r--r--nixos/modules/services/networking/flashpolicyd.nix1
-rw-r--r--nixos/modules/services/networking/freeradius.nix18
-rw-r--r--nixos/modules/services/networking/gale.nix5
-rw-r--r--nixos/modules/services/networking/git-daemon.nix12
-rw-r--r--nixos/modules/services/networking/gnunet.nix8
-rw-r--r--nixos/modules/services/networking/gvpe.nix8
-rw-r--r--nixos/modules/services/networking/hans.nix3
-rw-r--r--nixos/modules/services/networking/haproxy.nix26
-rw-r--r--nixos/modules/services/networking/helpers.nix11
-rw-r--r--nixos/modules/services/networking/hostapd.nix1
-rw-r--r--nixos/modules/services/networking/i2pd.nix10
-rw-r--r--nixos/modules/services/networking/iodine.nix173
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/default.nix12
-rw-r--r--nixos/modules/services/networking/iwd.nix7
-rw-r--r--nixos/modules/services/networking/keybase.nix11
-rw-r--r--nixos/modules/services/networking/kippo.nix5
-rw-r--r--nixos/modules/services/networking/knot.nix42
-rw-r--r--nixos/modules/services/networking/kresd.nix169
-rw-r--r--nixos/modules/services/networking/magic-wormhole-mailbox-server.nix28
-rw-r--r--nixos/modules/services/networking/mailpile.nix8
-rw-r--r--nixos/modules/services/networking/matterbridge.nix15
-rw-r--r--nixos/modules/services/networking/minidlna.nix17
-rw-r--r--nixos/modules/services/networking/mjpg-streamer.nix9
-rw-r--r--nixos/modules/services/networking/monero.nix20
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix43
-rw-r--r--nixos/modules/services/networking/murmur.nix5
-rw-r--r--nixos/modules/services/networking/mxisd.nix12
-rw-r--r--nixos/modules/services/networking/namecoind.nix8
-rw-r--r--nixos/modules/services/networking/nat.nix34
-rw-r--r--nixos/modules/services/networking/ndppd.nix30
-rw-r--r--nixos/modules/services/networking/networkmanager.nix107
-rw-r--r--nixos/modules/services/networking/nftables.nix2
-rw-r--r--nixos/modules/services/networking/nix-store-gcs-proxy.nix75
-rw-r--r--nixos/modules/services/networking/nntp-proxy.nix5
-rw-r--r--nixos/modules/services/networking/nsd.nix12
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix25
-rw-r--r--nixos/modules/services/networking/ntp/ntpd.nix19
-rw-r--r--nixos/modules/services/networking/ntp/openntpd.nix3
-rw-r--r--nixos/modules/services/networking/openfire.nix7
-rw-r--r--nixos/modules/services/networking/openvpn.nix3
-rw-r--r--nixos/modules/services/networking/owamp.nix7
-rw-r--r--nixos/modules/services/networking/pdns-recursor.nix15
-rw-r--r--nixos/modules/services/networking/pdnsd.nix6
-rw-r--r--nixos/modules/services/networking/pixiecore.nix134
-rw-r--r--nixos/modules/services/networking/polipo.nix10
-rw-r--r--nixos/modules/services/networking/pppd.nix14
-rw-r--r--nixos/modules/services/networking/prayer.nix18
-rw-r--r--nixos/modules/services/networking/privoxy.nix2
-rw-r--r--nixos/modules/services/networking/quassel.nix23
-rw-r--r--nixos/modules/services/networking/quorum.nix229
-rw-r--r--nixos/modules/services/networking/radicale.nix11
-rw-r--r--nixos/modules/services/networking/radvd.nix1
-rw-r--r--nixos/modules/services/networking/rdnssd.nix1
-rw-r--r--nixos/modules/services/networking/resilio.nix2
-rw-r--r--nixos/modules/services/networking/rxe.nix29
-rw-r--r--nixos/modules/services/networking/sabnzbd.nix6
-rw-r--r--nixos/modules/services/networking/searx.nix2
-rw-r--r--nixos/modules/services/networking/shairport-sync.nix6
-rw-r--r--nixos/modules/services/networking/shorewall.nix70
-rw-r--r--nixos/modules/services/networking/shorewall6.nix70
-rw-r--r--nixos/modules/services/networking/shout.nix3
-rw-r--r--nixos/modules/services/networking/smartdns.nix61
-rw-r--r--nixos/modules/services/networking/smokeping.nix3
-rw-r--r--nixos/modules/services/networking/spacecookie.nix83
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix1
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix34
-rw-r--r--nixos/modules/services/networking/sslh.nix9
-rw-r--r--nixos/modules/services/networking/strongswan.nix30
-rw-r--r--nixos/modules/services/networking/stubby.nix3
-rw-r--r--nixos/modules/services/networking/supplicant.nix2
-rw-r--r--nixos/modules/services/networking/supybot.nix113
-rw-r--r--nixos/modules/services/networking/syncthing.nix44
-rw-r--r--nixos/modules/services/networking/tailscale.nix49
-rw-r--r--nixos/modules/services/networking/tcpcrypt.nix3
-rw-r--r--nixos/modules/services/networking/tox-bootstrapd.nix5
-rw-r--r--nixos/modules/services/networking/unbound.nix13
-rw-r--r--nixos/modules/services/networking/unifi.nix15
-rw-r--r--nixos/modules/services/networking/v2ray.nix81
-rw-r--r--nixos/modules/services/networking/vsftpd.nix23
-rw-r--r--nixos/modules/services/networking/wg-quick.nix2
-rw-r--r--nixos/modules/services/networking/wireguard.nix6
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix7
-rw-r--r--nixos/modules/services/networking/xandikos.nix148
-rw-r--r--nixos/modules/services/networking/xinetd.nix7
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix6
-rw-r--r--nixos/modules/services/networking/zerotierone.nix21
-rw-r--r--nixos/modules/services/networking/znc/default.nix26
-rw-r--r--nixos/modules/services/printing/cupsd.nix14
-rw-r--r--nixos/modules/services/scheduling/atd.nix12
-rw-r--r--nixos/modules/services/scheduling/fcron.nix5
-rw-r--r--nixos/modules/services/search/hound.nix22
-rw-r--r--nixos/modules/services/search/kibana.nix3
-rw-r--r--nixos/modules/services/search/solr.nix26
-rw-r--r--nixos/modules/services/security/bitwarden_rs/default.nix46
-rw-r--r--nixos/modules/services/security/certmgr.nix4
-rw-r--r--nixos/modules/services/security/clamav.nix13
-rw-r--r--nixos/modules/services/security/fail2ban.nix308
-rw-r--r--nixos/modules/services/security/fprot.nix21
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix29
-rw-r--r--nixos/modules/services/security/sshguard.nix13
-rw-r--r--nixos/modules/services/security/tor.nix6
-rw-r--r--nixos/modules/services/security/torify.nix3
-rw-r--r--nixos/modules/services/security/torsocks.nix9
-rw-r--r--nixos/modules/services/security/vault.nix1
-rw-r--r--nixos/modules/services/system/dbus.nix5
-rw-r--r--nixos/modules/services/system/kerberos/default.nix7
-rw-r--r--nixos/modules/services/system/localtime.nix5
-rw-r--r--nixos/modules/services/system/nscd.nix12
-rw-r--r--nixos/modules/services/system/uptimed.nix1
-rw-r--r--nixos/modules/services/torrent/deluge.nix31
-rw-r--r--nixos/modules/services/torrent/magnetico.nix2
-rw-r--r--nixos/modules/services/torrent/transmission.nix38
-rw-r--r--nixos/modules/services/ttys/agetty.nix3
-rw-r--r--nixos/modules/services/wayland/cage.nix99
-rw-r--r--nixos/modules/services/web-apps/codimd.nix2
-rw-r--r--nixos/modules/services/web-apps/documize.nix2
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix388
-rw-r--r--nixos/modules/services/web-apps/frab.nix10
-rw-r--r--nixos/modules/services/web-apps/gerrit.nix218
-rw-r--r--nixos/modules/services/web-apps/grocy.nix172
-rw-r--r--nixos/modules/services/web-apps/grocy.xml77
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix141
-rw-r--r--nixos/modules/services/web-apps/jirafeau.nix169
-rw-r--r--nixos/modules/services/web-apps/limesurvey.nix88
-rw-r--r--nixos/modules/services/web-apps/matomo-doc.xml6
-rw-r--r--nixos/modules/services/web-apps/matomo.nix8
-rw-r--r--nixos/modules/services/web-apps/mattermost.nix38
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix59
-rw-r--r--nixos/modules/services/web-apps/moinmoin.nix2
-rw-r--r--nixos/modules/services/web-apps/moodle.nix54
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix106
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml48
-rw-r--r--nixos/modules/services/web-apps/restya-board.nix4
-rw-r--r--nixos/modules/services/web-apps/trilium.nix137
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix86
-rw-r--r--nixos/modules/services/web-apps/youtrack.nix8
-rw-r--r--nixos/modules/services/web-apps/zabbix.nix54
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix616
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/location-options.nix54
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/per-server-options.nix174
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix275
-rw-r--r--nixos/modules/services/web-servers/caddy.nix18
-rw-r--r--nixos/modules/services/web-servers/jboss/default.nix1
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix102
-rw-r--r--nixos/modules/services/web-servers/nginx/gitweb.nix53
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix1
-rw-r--r--nixos/modules/services/web-servers/phpfpm/default.nix9
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix10
-rw-r--r--nixos/modules/services/web-servers/traefik.nix125
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix46
-rw-r--r--nixos/modules/services/web-servers/uwsgi.nix35
-rw-r--r--nixos/modules/services/x11/desktop-managers/cde.nix55
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix51
-rw-r--r--nixos/modules/services/x11/desktop-managers/enlightenment.nix12
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix34
-rw-r--r--nixos/modules/services/x11/desktop-managers/kodi.nix3
-rw-r--r--nixos/modules/services/x11/desktop-managers/mate.nix46
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix330
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml114
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix186
-rw-r--r--nixos/modules/services/x11/desktop-managers/surf-display.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix6
-rw-r--r--nixos/modules/services/x11/display-managers/account-service-util.nix44
-rw-r--r--nixos/modules/services/x11/display-managers/auto.nix68
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix270
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix91
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix11
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix7
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix92
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix40
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix25
-rw-r--r--nixos/modules/services/x11/display-managers/startx.nix1
-rw-r--r--nixos/modules/services/x11/extra-layouts.nix8
-rw-r--r--nixos/modules/services/x11/gdk-pixbuf.nix4
-rw-r--r--nixos/modules/services/x11/hardware/libinput.nix19
-rw-r--r--nixos/modules/services/x11/hardware/multitouch.nix94
-rw-r--r--nixos/modules/services/x11/hardware/wacom.nix1
-rw-r--r--nixos/modules/services/x11/imwheel.nix68
-rw-r--r--nixos/modules/services/x11/picom.nix (renamed from nixos/modules/services/x11/compton.nix)34
-rw-r--r--nixos/modules/services/x11/unclutter.nix12
-rw-r--r--nixos/modules/services/x11/urxvtd.nix8
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix15
-rw-r--r--nixos/modules/services/x11/xserver.nix118
423 files changed, 12022 insertions, 4767 deletions
diff --git a/nixos/modules/services/admin/oxidized.nix b/nixos/modules/services/admin/oxidized.nix
index da81be3f23e..94b44630ba6 100644
--- a/nixos/modules/services/admin/oxidized.nix
+++ b/nixos/modules/services/admin/oxidized.nix
@@ -111,6 +111,7 @@ in
         Restart  = "always";
         WorkingDirectory = cfg.dataDir;
         KillSignal = "SIGKILL";
+        PIDFile = "${cfg.dataDir}/.config/oxidized/pid";
       };
     };
   };
diff --git a/nixos/modules/services/amqp/activemq/default.nix b/nixos/modules/services/amqp/activemq/default.nix
index 7729da27304..160dbddcd48 100644
--- a/nixos/modules/services/amqp/activemq/default.nix
+++ b/nixos/modules/services/amqp/activemq/default.nix
@@ -63,9 +63,11 @@ in {
       javaProperties = mkOption {
         type = types.attrs;
         default = { };
-        example = {
-          "java.net.preferIPv4Stack" = "true";
-        };
+        example = literalExample ''
+          {
+            "java.net.preferIPv4Stack" = "true";
+          }
+        '';
         apply = attrs: {
           "activemq.base" = "${cfg.baseDir}";
           "activemq.data" = "${cfg.baseDir}/data";
diff --git a/nixos/modules/services/amqp/rabbitmq.nix b/nixos/modules/services/amqp/rabbitmq.nix
index 697732426cc..646708e01c4 100644
--- a/nixos/modules/services/amqp/rabbitmq.nix
+++ b/nixos/modules/services/amqp/rabbitmq.nix
@@ -17,6 +17,7 @@ in {
   options = {
     services.rabbitmq = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to enable the RabbitMQ server, an Advanced Message
@@ -98,8 +99,8 @@ in {
           will be merged into these options by RabbitMQ at runtime to
           form the final configuration.
 
-          See http://www.rabbitmq.com/configure.html#config-items
-          For the distinct formats, see http://www.rabbitmq.com/configure.html#config-file-formats
+          See https://www.rabbitmq.com/configure.html#config-items
+          For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
         '';
       };
 
@@ -116,8 +117,8 @@ in {
           The contents of this option will be merged into the <literal>configItems</literal>
           by RabbitMQ at runtime to form the final configuration.
 
-          See the second table on http://www.rabbitmq.com/configure.html#config-items
-          For the distinct formats, see http://www.rabbitmq.com/configure.html#config-file-formats
+          See the second table on https://www.rabbitmq.com/configure.html#config-items
+          For the distinct formats, see https://www.rabbitmq.com/configure.html#config-file-formats
         '';
       };
 
@@ -165,7 +166,10 @@ in {
       after = [ "network.target" "epmd.socket" ];
       wants = [ "network.target" "epmd.socket" ];
 
-      path = [ cfg.package pkgs.procps ];
+      path = [
+        cfg.package
+        pkgs.coreutils # mkdir/chown/chmod for preStart
+      ];
 
       environment = {
         RABBITMQ_MNESIA_BASE = "${cfg.dataDir}/mnesia";
diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix
index f632644af09..3fe76a16540 100644
--- a/nixos/modules/services/audio/alsa.nix
+++ b/nixos/modules/services/audio/alsa.nix
@@ -12,6 +12,9 @@ let
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "sound" "enableMediaKeys" ] [ "sound" "mediaKeys" "enable" ])
+  ];
 
   ###### interface
 
@@ -88,11 +91,7 @@ in
     environment.systemPackages = [ alsaUtils ];
 
     environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "")
-      [
-        { source = pkgs.writeText "asound.conf" config.sound.extraConfig;
-          target = "asound.conf";
-        }
-      ];
+      { "asound.conf".text = config.sound.extraConfig; };
 
     # ALSA provides a udev rule for restoring volume settings.
     services.udev.packages = [ alsaUtils ];
diff --git a/nixos/modules/services/audio/mopidy.nix b/nixos/modules/services/audio/mopidy.nix
index a534b692f17..6fd7eae5b89 100644
--- a/nixos/modules/services/audio/mopidy.nix
+++ b/nixos/modules/services/audio/mopidy.nix
@@ -13,11 +13,11 @@ let
   mopidyEnv = buildEnv {
     name = "mopidy-with-extensions-${mopidy.version}";
     paths = closePropagation cfg.extensionPackages;
-    pathsToLink = [ "/${python.sitePackages}" ];
+    pathsToLink = [ "/${mopidyPackages.python.sitePackages}" ];
     buildInputs = [ makeWrapper ];
     postBuild = ''
       makeWrapper ${mopidy}/bin/mopidy $out/bin/mopidy \
-        --prefix PYTHONPATH : $out/${python.sitePackages}
+        --prefix PYTHONPATH : $out/${mopidyPackages.python.sitePackages}
     '';
   };
 in {
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index 7932d094197..e20591b5beb 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -184,19 +184,19 @@ in {
       };
     };
 
-    users.users = optionalAttrs (cfg.user == name) (singleton {
-      inherit uid;
-      inherit name;
-      group = cfg.group;
-      extraGroups = [ "audio" ];
-      description = "Music Player Daemon user";
-      home = "${cfg.dataDir}";
-    });
-
-    users.groups = optionalAttrs (cfg.group == name) (singleton {
-      inherit name;
-      gid = gid;
-    });
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        inherit uid;
+        group = cfg.group;
+        extraGroups = [ "audio" ];
+        description = "Music Player Daemon user";
+        home = "${cfg.dataDir}";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = gid;
+    };
   };
 
 }
diff --git a/nixos/modules/services/audio/roon-server.nix b/nixos/modules/services/audio/roon-server.nix
index 4eda3c5708d..6aed485638c 100644
--- a/nixos/modules/services/audio/roon-server.nix
+++ b/nixos/modules/services/audio/roon-server.nix
@@ -66,7 +66,8 @@ in {
       if cfg.user == "roon-server" then {
         isSystemUser = true;
         description = "Roon Server user";
-        groups = [ cfg.group "audio" ];
+        group = cfg.group;
+        extraGroups = [ "audio" ];
       }
       else {};
   };
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
index 41bda7893a7..cef304734ae 100644
--- a/nixos/modules/services/backup/bacula.nix
+++ b/nixos/modules/services/backup/bacula.nix
@@ -44,7 +44,17 @@ let
         Pid Directory = "/run";
         ${sd_cfg.extraStorageConfig}
       }
- 
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Autochanger {
+        Name = "${name}";
+        Device = ${concatStringsSep ", " (map (a: "\"${a}\"") value.devices)};
+        Changer Device =  "${value.changerDevice}";
+        Changer Command = "${value.changerCommand}";
+        ${value.extraAutochangerConfig}
+      }
+      '') sd_cfg.autochanger)}
+
       ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
       Device {
         Name = "${name}";
@@ -103,7 +113,19 @@ let
       password = mkOption {
         # TODO: required?
         description = ''
-           Specifies the password that must be supplied for a Director to b
+          Specifies the password that must be supplied for the default Bacula
+          Console to be authorized. The same password must appear in the
+          Director resource of the Console configuration file. For added
+          security, the password is never passed across the network but instead
+          a challenge response hash code created with the password. This
+          directive is required. If you have either /dev/random or bc on your
+          machine, Bacula will generate a random password during the
+          configuration process, otherwise it will be left blank and you must
+          manually supply it.
+
+          The password is plain text. It is not generated through any special
+          process but as noted above, it is better to use random text for
+          security reasons. 
         '';
       };
       
@@ -111,26 +133,133 @@ let
         default = "no";
         example = "yes";
         description = ''
-           If Monitor is set to no (default), this director will have full 
+          If Monitor is set to <literal>no</literal>, this director will have
+          full access to this Storage daemon. If Monitor is set to
+          <literal>yes</literal>, this director will only be able to fetch the
+          current status of this Storage daemon.
+
+          Please note that if this director is being used by a Monitor, we
+          highly recommend to set this directive to yes to avoid serious
+          security problems. 
+        '';
+      };
+    };
+  };
+
+  autochangerOptions = {...}:
+  {
+    options = {
+      changerDevice = mkOption {
+        description = ''
+          The specified name-string must be the generic SCSI device name of the
+          autochanger that corresponds to the normal read/write Archive Device
+          specified in the Device resource. This generic SCSI device name
+          should be specified if you have an autochanger or if you have a
+          standard tape drive and want to use the Alert Command (see below).
+          For example, on Linux systems, for an Archive Device name of
+          <literal>/dev/nst0</literal>, you would specify
+          <literal>/dev/sg0</literal> for the Changer Device name.  Depending
+          on your exact configuration, and the number of autochangers or the
+          type of autochanger, what you specify here can vary. This directive
+          is optional. See the Using AutochangersAutochangersChapter chapter of
+          this manual for more details of using this and the following
+          autochanger directives.         
+          '';
+      };
+
+      changerCommand = mkOption {
+        description = ''
+          The name-string specifies an external program to be called that will
+          automatically change volumes as required by Bacula. Normally, this
+          directive will be specified only in the AutoChanger resource, which
+          is then used for all devices. However, you may also specify the
+          different Changer Command in each Device resource. Most frequently,
+          you will specify the Bacula supplied mtx-changer script as follows:
+
+          <literal>"/path/mtx-changer %c %o %S %a %d"</literal>
+
+          and you will install the mtx on your system (found in the depkgs
+          release). An example of this command is in the default bacula-sd.conf
+          file. For more details on the substitution characters that may be
+          specified to configure your autochanger please see the
+          AutochangersAutochangersChapter chapter of this manual. For FreeBSD
+          users, you might want to see one of the several chio scripts in
+          examples/autochangers.
+          '';
+        default = "/etc/bacula/mtx-changer %c %o %S %a %d";
+      };
+
+      devices = mkOption {
+        description = ''
+        '';
+      };
+
+      extraAutochangerConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Autochanger directive.
+        '';
+        example = ''
+   
         '';
       };
     };
   };
 
+
   deviceOptions = {...}:
   {
     options = {
       archiveDevice = mkOption {
         # TODO: required?
         description = ''
-          The specified name-string gives the system file name of the storage device managed by this storage daemon. This will usually be the device file name of a removable storage device (tape drive), for example " /dev/nst0" or "/dev/rmt/0mbn". For a DVD-writer, it will be for example /dev/hdc. It may also be a directory name if you are archiving to disk storage.
+          The specified name-string gives the system file name of the storage
+          device managed by this storage daemon. This will usually be the
+          device file name of a removable storage device (tape drive), for
+          example <literal>/dev/nst0</literal> or
+          <literal>/dev/rmt/0mbn</literal>. For a DVD-writer, it will be for
+          example <literal>/dev/hdc</literal>. It may also be a directory name
+          if you are archiving to disk storage. In this case, you must supply
+          the full absolute path to the directory. When specifying a tape
+          device, it is preferable that the "non-rewind" variant of the device
+          file name be given. 
         '';
       };
 
       mediaType = mkOption {
         # TODO: required?
         description = ''
-          The specified name-string names the type of media supported by this device, for example, "DLT7000". Media type names are arbitrary in that you set them to anything you want, but they must be known to the volume database to keep track of which storage daemons can read which volumes. In general, each different storage type should have a unique Media Type associated with it. The same name-string must appear in the appropriate Storage resource definition in the Director's configuration file.
+          The specified name-string names the type of media supported by this
+          device, for example, <literal>DLT7000</literal>. Media type names are
+          arbitrary in that you set them to anything you want, but they must be
+          known to the volume database to keep track of which storage daemons
+          can read which volumes. In general, each different storage type
+          should have a unique Media Type associated with it. The same
+          name-string must appear in the appropriate Storage resource
+          definition in the Director's configuration file.
+
+          Even though the names you assign are arbitrary (i.e. you choose the
+          name you want), you should take care in specifying them because the
+          Media Type is used to determine which storage device Bacula will
+          select during restore. Thus you should probably use the same Media
+          Type specification for all drives where the Media can be freely
+          interchanged. This is not generally an issue if you have a single
+          Storage daemon, but it is with multiple Storage daemons, especially
+          if they have incompatible media.
+
+          For example, if you specify a Media Type of <literal>DDS-4</literal>
+          then during the restore, Bacula will be able to choose any Storage
+          Daemon that handles <literal>DDS-4</literal>. If you have an
+          autochanger, you might want to name the Media Type in a way that is
+          unique to the autochanger, unless you wish to possibly use the
+          Volumes in other drives. You should also ensure to have unique Media
+          Type names if the Media is not compatible between drives. This
+          specification is required for all devices.
+
+          In addition, if you are using disk storage, each Device resource will
+          generally have a different mount point or directory. In order for
+          Bacula to select the correct Device resource, each one must have a
+          unique Media Type.
         '';
       };
 
@@ -166,8 +295,8 @@ in {
         default = "${config.networking.hostName}-fd";
         description = ''
           The client name that must be used by the Director when connecting.
-          Generally, it is a good idea to use a name related to the machine
-          so that error messages can be easily identified if you have multiple
+          Generally, it is a good idea to use a name related to the machine so
+          that error messages can be easily identified if you have multiple
           Clients. This directive is required.
         '';
       };
@@ -232,7 +361,8 @@ in {
         default = 9103;
         type = types.int;
         description = ''
-          Specifies port number on which the Storage daemon listens for Director connections. The default is 9103.
+          Specifies port number on which the Storage daemon listens for
+          Director connections.
         '';
       };
 
@@ -251,7 +381,15 @@ in {
         '';
         type = with types; attrsOf (submodule deviceOptions);
       };
- 
+
+      autochanger = mkOption {
+        default = {};
+        description = ''
+          This option defines Autochanger resources in Bacula Storage Daemon.
+        '';
+        type = with types; attrsOf (submodule autochangerOptions);
+      };
+
       extraStorageConfig = mkOption {
         default = "";
         description = ''
@@ -287,7 +425,8 @@ in {
       name = mkOption {
         default = "${config.networking.hostName}-dir";
         description = ''
-          The director name used by the system administrator. This directive is required.
+          The director name used by the system administrator. This directive is
+          required.
         '';
       };
  
@@ -295,7 +434,12 @@ in {
         default = 9101;
         type = types.int;
         description = ''
-          Specify the port (a positive integer) on which the Director daemon will listen for Bacula Console connections. This same port number must be specified in the Director resource of the Console configuration file. The default is 9101, so normally this directive need not be specified. This directive should not be used if you specify DirAddresses (N.B plural) directive.
+          Specify the port (a positive integer) on which the Director daemon
+          will listen for Bacula Console connections. This same port number
+          must be specified in the Director resource of the Console
+          configuration file. The default is 9101, so normally this directive
+          need not be specified. This directive should not be used if you
+          specify DirAddresses (N.B plural) directive.
         '';
       };
  
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index 10d42325a6b..be661b201f0 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -68,7 +68,7 @@ let
       { BORG_PASSPHRASE = passphrase; }
     else { };
 
-  mkBackupService = name: cfg: 
+  mkBackupService = name: cfg:
     let
       userHome = config.users.users.${cfg.user}.home;
     in nameValuePair "borgbackup-job-${name}" {
@@ -98,6 +98,23 @@ let
       inherit (cfg) startAt;
     };
 
+  # utility function around makeWrapper
+  mkWrapperDrv = {
+      original, name, set ? {}
+    }:
+    pkgs.runCommandNoCC "${name}-wrapper" {
+      buildInputs = [ pkgs.makeWrapper ];
+    } (with lib; ''
+      makeWrapper "${original}" "$out/bin/${name}" \
+        ${concatStringsSep " \\\n " (mapAttrsToList (name: value: ''--set ${name} "${value}"'') set)}
+    '');
+
+  mkBorgWrapper = name: cfg: mkWrapperDrv {
+    original = "${pkgs.borgbackup}/bin/borg";
+    name = "borg-job-${name}";
+    set = { BORG_REPO = cfg.repo; } // (mkPassEnv cfg) // cfg.environment;
+  };
+
   # Paths listed in ReadWritePaths must exist before service is started
   mkActivationScript = name: cfg:
     let
@@ -172,14 +189,20 @@ let
 
 in {
   meta.maintainers = with maintainers; [ dotlambda ];
+  meta.doc = ./borgbackup.xml;
 
   ###### interface
 
   options.services.borgbackup.jobs = mkOption {
-    description = "Deduplicating backups using BorgBackup.";
+    description = ''
+      Deduplicating backups using BorgBackup.
+      Adding a job will cause a borg-job-NAME wrapper to be added
+      to your system path, so that you can perform maintenance easily.
+      See also the chapter about BorgBackup in the NixOS manual.
+    '';
     default = { };
     example = literalExample ''
-      {
+      { # for a local backup
         rootBackup = {
           paths = "/";
           exclude = [ "/nix" ];
@@ -192,6 +215,23 @@ in {
           startAt = "weekly";
         };
       }
+      { # Root backing each day up to a remote backup server. We assume that you have
+        #   * created a password less key: ssh-keygen -N "" -t ed25519 -f /path/to/ssh_key
+        #     best practices are: use -t ed25519, /path/to = /run/keys
+        #   * the passphrase is in the file /run/keys/borgbackup_passphrase
+        #   * you have initialized the repository manually
+        paths = [ "/etc" "/home" ];
+        exclude = [ "/nix" "'**/.cache'" ];
+        doInit = false;
+        repo =  "user3@arep.repo.borgbase.com:repo";
+        encryption = {
+          mode = "repokey-blake2";
+          passCommand = "cat /path/to/passphrase";
+        };
+        environment = { BORG_RSH = "ssh -i /path/to/ssh_key"; };
+        compression = "auto,lzma";
+        startAt = "daily";
+    };
     '';
     type = types.attrsOf (types.submodule (let globalConfig = config; in
       { name, config, ... }: {
@@ -247,6 +287,8 @@ in {
               <manvolnum>7</manvolnum></citerefentry>.
               If you do not want the backup to start
               automatically, use <literal>[ ]</literal>.
+              It will generate a systemd service borgbackup-job-NAME.
+              You may trigger it manually via systemctl restart borgbackup-job-NAME.
             '';
           };
 
@@ -282,6 +324,10 @@ in {
               you to specify a <option>passCommand</option>
               or a <option>passphrase</option>.
             '';
+            example = ''
+              encryption.mode = "repokey-blake2" ;
+              encryption.passphrase = "mySecretPassphrase" ;
+            '';
           };
 
           encryption.passCommand = mkOption {
@@ -517,6 +563,7 @@ in {
     description = ''
       Serve BorgBackup repositories to given public SSH keys,
       restricting their access to the repository only.
+      See also the chapter about BorgBackup in the NixOS manual.
       Also, clients do not need to specify the absolute path when accessing the repository,
       i.e. <literal>user@machine:.</literal> is enough. (Note colon and dot.)
     '';
@@ -623,6 +670,6 @@ in {
 
       users = mkMerge (mapAttrsToList mkUsersConfig repos);
 
-      environment.systemPackages = with pkgs; [ borgbackup ];
+      environment.systemPackages = with pkgs; [ borgbackup ] ++ (mapAttrsToList mkBorgWrapper jobs);
     });
 }
diff --git a/nixos/modules/services/backup/borgbackup.xml b/nixos/modules/services/backup/borgbackup.xml
new file mode 100644
index 00000000000..bef7db608f8
--- /dev/null
+++ b/nixos/modules/services/backup/borgbackup.xml
@@ -0,0 +1,227 @@
+<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-borgbase">
+ <title>BorgBackup</title>
+  <para>
+  <emphasis>Source:</emphasis>
+  <filename>modules/services/backup/borgbackup.nix</filename>
+ </para>
+ <para>
+  <emphasis>Upstream documentation:</emphasis>
+  <link xlink:href="https://borgbackup.readthedocs.io/"/>
+ </para>
+ <para>
+  <link xlink:href="https://www.borgbackup.org/">BorgBackup</link> (short: Borg)
+  is a deduplicating backup program. Optionally, it supports compression and
+  authenticated encryption.
+  </para>
+  <para>
+  The main goal of Borg is to provide an efficient and secure way to backup
+  data. The data deduplication technique used makes Borg suitable for daily
+  backups since only changes are stored. The authenticated encryption technique
+  makes it suitable for backups to not fully trusted targets.
+ </para>
+  <section xml:id="module-services-backup-borgbackup-configuring">
+  <title>Configuring</title>
+  <para>
+   A complete list of options for the Borgbase module may be found
+   <link linkend="opt-services.borgbackup.jobs">here</link>.
+  </para>
+</section>
+ <section xml:id="opt-services-backup-borgbackup-local-directory">
+  <title>Basic usage for a local backup</title>
+
+  <para>
+   A very basic configuration for backing up to a locally accessible directory
+   is:
+<programlisting>
+{
+    opt.services.borgbackup.jobs = {
+      { rootBackup = {
+          paths = "/";
+          exclude = [ "/nix" "/path/to/local/repo" ];
+          repo = "/path/to/local/repo";
+          doInit = true;
+          encryption = {
+            mode = "repokey";
+            passphrase = "secret";
+          };
+          compression = "auto,lzma";
+          startAt = "weekly";
+        };
+      }
+    };
+}</programlisting>
+  </para>
+  <warning>
+    <para>
+        If you do not want the passphrase to be stored in the world-readable
+        Nix store, use passCommand. You find an example below.
+    </para>
+  </warning>
+ </section>
+<section xml:id="opt-services-backup-create-server">
+  <title>Create a borg backup server</title>
+  <para>You should use a different SSH key for each repository you write to,
+    because the specified keys are restricted to running borg serve and can only
+    access this single repository. You need the output of the generate pub file.
+  </para>
+    <para>
+        <programlisting>
+# sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
+# cat /run/keys/id_ed25519_my_borg_repo
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos</programlisting>
+    </para>
+    <para>
+      Add the following snippet to your NixOS configuration:
+      <programlisting>
+{
+  services.borgbackup.repos = {
+    my_borg_repo = {
+      authorizedKeys = [
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos"
+      ] ;
+      path = "/var/lib/my_borg_repo" ;
+    };
+  };
+}</programlisting>
+    </para>
+</section>
+
+ <section xml:id="opt-services-backup-borgbackup-remote-server">
+  <title>Backup to the borg repository server</title>
+  <para>The following NixOS snippet creates an hourly backup to the service
+    (on the host nixos) as created in the section above. We assume
+    that you have stored a secret passphrasse in the file
+    <code>/run/keys/borgbackup_passphrase</code>, which should be only
+    accessible by root
+  </para>
+  <para>
+      <programlisting>
+{
+  services.borgbackup.jobs = {
+    backupToLocalServer = {
+      paths = [ "/etc/nixos" ];
+      doInit = true;
+      repo =  "borg@nixos:." ;
+      encryption = {
+        mode = "repokey-blake2";
+        passCommand = "cat /run/keys/borgbackup_passphrase";
+      };
+      environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_my_borg_repo"; };
+      compression = "auto,lzma";
+      startAt = "hourly";
+    };
+  };
+};</programlisting>
+  </para>
+  <para>The following few commands (run as root) let you test your backup.
+      <programlisting>
+> nixos-rebuild switch
+...restarting the following units: polkit.service
+> systemctl restart borgbackup-job-backupToLocalServer
+> sleep 10
+> systemctl restart borgbackup-job-backupToLocalServer
+> export BORG_PASSPHRASE=topSecrect
+> borg list --rsh='ssh -i /run/keys/id_ed25519_my_borg_repo' borg@nixos:.
+nixos-backupToLocalServer-2020-03-30T21:46:17 Mon, 2020-03-30 21:46:19 [84feb97710954931ca384182f5f3cb90665f35cef214760abd7350fb064786ac]
+nixos-backupToLocalServer-2020-03-30T21:46:30 Mon, 2020-03-30 21:46:32 [e77321694ecd160ca2228611747c6ad1be177d6e0d894538898de7a2621b6e68]</programlisting>
+    </para>
+</section>
+
+ <section xml:id="opt-services-backup-borgbackup-borgbase">
+  <title>Backup to a hosting service</title>
+
+  <para>
+    Several companies offer <link
+      xlink:href="https://www.borgbackup.org/support/commercial.html">(paid)
+      hosting services</link> for Borg repositories.
+  </para>
+  <para>
+    To backup your home directory to borgbase you have to:
+  </para>
+  <itemizedlist>
+  <listitem>
+    <para>
+      Generate a SSH key without a password, to access the remote server. E.g.
+    </para>
+    <para>
+        <programlisting>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_borgbase</programlisting>
+    </para>
+  </listitem>
+  <listitem>
+    <para>
+      Create the repository on the server by following the instructions for your
+      hosting server.
+    </para>
+  </listitem>
+  <listitem>
+    <para>
+      Initialize the repository on the server. Eg.
+      <programlisting>
+sudo borg init --encryption=repokey-blake2  \
+    -rsh "ssh -i /run/keys/id_ed25519_borgbase" \
+    zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo</programlisting>
+  </para>
+  </listitem>
+  <listitem>
+<para>Add it to your NixOS configuration, e.g.
+<programlisting>
+{
+    services.borgbackup.jobs = {
+    my_Remote_Backup = {
+        paths = [ "/" ];
+        exclude = [ "/nix" "'**/.cache'" ];
+        repo =  "zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo";
+          encryption = {
+          mode = "repokey-blake2";
+          passCommand = "cat /run/keys/borgbackup_passphrase";
+        };
+        BORG_RSH = "ssh -i /run/keys/id_ed25519_borgbase";
+        compression = "auto,lzma";
+        startAt = "daily";
+    };
+  };
+}}</programlisting>
+  </para>
+  </listitem>
+</itemizedlist>
+ </section>
+  <section xml:id="opt-services-backup-borgbackup-vorta">
+  <title>Vorta backup client for the desktop</title>
+  <para>
+    Vorta is a backup client for macOS and Linux desktops. It integrates the
+    mighty BorgBackup with your desktop environment to protect your data from
+    disk failure, ransomware and theft.
+  </para>
+  <para>
+    It is available as a flatpak package. To enable it you must set the
+    following two configuration items.
+  </para>
+  <para>
+    <programlisting>
+services.flatpak.enable = true ;
+# next line is needed to avoid the Error
+# Error deploying: GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown:
+services.accounts-daemon.enable = true;
+    </programlisting>
+  </para>
+  <para>As a normal user you must first install, then run vorta using the
+    following commands:
+    <programlisting>
+flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+flatpak install flathub com.borgbase.Vorta
+flatpak run --branch=stable --arch=x86_64 --command=vorta com.borgbase.Vorta
+</programlisting>
+    After running <code>flatpak install</code> you can start Vorta also via
+        the KDE application menu.
+  </para>
+  <para>
+    Details about using Vorta can be found under <link
+      xlink:href="https://vorta.borgbase.com/usage">https://vorta.borgbase.com
+      </link>.
+  </para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index dbd5605143f..31d606b141a 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -37,12 +37,7 @@ in
 
     services.mysqlBackup = {
 
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable MySQL backups.
-        '';
-      };
+      enable = mkEnableOption "MySQL backups";
 
       calendar = mkOption {
         type = types.str;
@@ -84,13 +79,14 @@ in
   };
 
   config = mkIf cfg.enable {
-    users.users = optionalAttrs (cfg.user == defaultUser) (singleton
-      { name = defaultUser;
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
         isSystemUser = true;
         createHome = false;
         home = cfg.location;
         group = "nogroup";
-      });
+      };
+    };
 
     services.mysql.ensureUsers = [{
       name = cfg.user;
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
index 13a36ae32ac..428861a7598 100644
--- a/nixos/modules/services/backup/postgresql-backup.nix
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -35,14 +35,16 @@ let
 
 in {
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "postgresqlBackup" "period" ] ''
+       A systemd timer is now used instead of cron.
+       The starting time can be configured via <literal>services.postgresqlBackup.startAt</literal>.
+    '')
+  ];
+
   options = {
     services.postgresqlBackup = {
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable PostgreSQL dumps.
-        '';
-      };
+      enable = mkEnableOption "PostgreSQL dumps";
 
       startAt = mkOption {
         default = "*-*-* 01:15:00";
@@ -82,7 +84,7 @@ in {
 
       pgdumpOptions = mkOption {
         type = types.separatedString " ";
-        default = "-Cbo";
+        default = "-C";
         description = ''
           Command line options for pg_dump. This options is not used
           if <literal>config.services.postgresqlBackup.backupAll</literal> is enabled.
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 7e8e91e4b9c..2388f1d6ca1 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -103,6 +103,34 @@ in
             Create the repository if it doesn't exist.
           '';
         };
+
+        pruneOpts = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          description = ''
+            A list of options (--keep-* et al.) for 'restic forget
+            --prune', to automatically prune old snapshots.  The
+            'forget' command is run *after* the 'backup' command, so
+            keep that in mind when constructing the --keep-* options.
+          '';
+          example = [
+            "--keep-daily 7"
+            "--keep-weekly 5"
+            "--keep-monthly 12"
+            "--keep-yearly 75"
+          ];
+        };
+
+        dynamicFilesFrom = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = ''
+            A script that produces a list of files to back up.  The
+            results of this command are given to the '--files-from'
+            option.
+          '';
+          example = "find /home/matt/git -type d -name .git";
+        };
       };
     }));
     default = {};
@@ -134,25 +162,41 @@ in
         let
           extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
           resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
+          filesFromTmpFile = "/run/restic-backups-${name}/includes";
+          backupPaths = if (backup.dynamicFilesFrom == null)
+                        then concatStringsSep " " backup.paths
+                        else "--files-from ${filesFromTmpFile}";
+          pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
+            ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) )
+            ( resticCmd + " check" )
+          ];
         in nameValuePair "restic-backups-${name}" ({
           environment = {
             RESTIC_PASSWORD_FILE = backup.passwordFile;
             RESTIC_REPOSITORY = backup.repository;
           };
-          path = with pkgs; [
-            openssh
-          ];
+          path = [ pkgs.openssh ];
           restartIfChanged = false;
           serviceConfig = {
             Type = "oneshot";
-            ExecStart = "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${concatStringsSep " " backup.paths}";
+            ExecStart = [ "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd;
             User = backup.user;
+            RuntimeDirectory = "restic-backups-${name}";
           } // optionalAttrs (backup.s3CredentialsFile != null) {
             EnvironmentFile = backup.s3CredentialsFile;
           };
-        } // optionalAttrs backup.initialize {
+        } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) {
           preStart = ''
-            ${resticCmd} snapshots || ${resticCmd} init
+            ${optionalString (backup.initialize) ''
+              ${resticCmd} snapshots || ${resticCmd} init
+            ''}
+            ${optionalString (backup.dynamicFilesFrom != null) ''
+              ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
+            ''}
+          '';
+        } // optionalAttrs (backup.dynamicFilesFrom != null) {
+          postStart = ''
+            rm ${filesFromTmpFile}
           '';
         })
       ) config.services.restic.backups;
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
new file mode 100644
index 00000000000..0472fb4ba1e
--- /dev/null
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -0,0 +1,213 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sanoid;
+
+  datasetSettingsType = with types;
+    (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
+      description = "dataset/template options";
+    };
+
+  # Default values from https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf
+
+  commonOptions = {
+    hourly = mkOption {
+      description = "Number of hourly snapshots.";
+      type = types.ints.unsigned;
+      default = 48;
+    };
+
+    daily = mkOption {
+      description = "Number of daily snapshots.";
+      type = types.ints.unsigned;
+      default = 90;
+    };
+
+    monthly = mkOption {
+      description = "Number of monthly snapshots.";
+      type = types.ints.unsigned;
+      default = 6;
+    };
+
+    yearly = mkOption {
+      description = "Number of yearly snapshots.";
+      type = types.ints.unsigned;
+      default = 0;
+    };
+
+    autoprune = mkOption {
+      description = "Whether to automatically prune old snapshots.";
+      type = types.bool;
+      default = true;
+    };
+
+    autosnap = mkOption {
+      description = "Whether to automatically take snapshots.";
+      type = types.bool;
+      default = true;
+    };
+
+    settings = mkOption {
+      description = ''
+        Free-form settings for this template/dataset. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
+        for allowed values.
+      '';
+      type = datasetSettingsType;
+    };
+  };
+
+  commonConfig = config: {
+    settings = {
+      hourly = mkDefault config.hourly;
+      daily = mkDefault config.daily;
+      monthly = mkDefault config.monthly;
+      yearly = mkDefault config.yearly;
+      autoprune = mkDefault config.autoprune;
+      autosnap = mkDefault config.autosnap;
+    };
+  };
+
+  datasetOptions = {
+    useTemplate = mkOption {
+      description = "Names of the templates to use for this dataset.";
+      type = (types.listOf (types.enum (attrNames cfg.templates))) // {
+        description = "list of template names";
+      };
+      default = [];
+    };
+
+    recursive = mkOption {
+      description = "Whether to recursively snapshot dataset children.";
+      type = types.bool;
+      default = false;
+    };
+
+    processChildrenOnly = mkOption {
+      description = "Whether to only snapshot child datasets if recursing.";
+      type = types.bool;
+      default = false;
+    };
+  };
+
+  datasetConfig = config: {
+    settings = {
+      use_template = mkDefault config.useTemplate;
+      recursive = mkDefault config.recursive;
+      process_children_only = mkDefault config.processChildrenOnly;
+    };
+  };
+
+  # Extract pool names from configured datasets
+  pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets));
+
+  configFile = let
+    mkValueString = v:
+      if builtins.isList v then concatStringsSep "," v
+      else generators.mkValueStringDefault {} v;
+
+    mkKeyValue = k: v: if v == null then ""
+      else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
+  in generators.toINI { inherit mkKeyValue; } cfg.settings;
+
+  configDir = pkgs.writeTextDir "sanoid.conf" configFile;
+
+in {
+
+    # Interface
+
+    options.services.sanoid = {
+      enable = mkEnableOption "Sanoid ZFS snapshotting service";
+
+      interval = mkOption {
+        type = types.str;
+        default = "hourly";
+        example = "daily";
+        description = ''
+          Run sanoid at this interval. The default is to run hourly.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      datasets = mkOption {
+        type = types.attrsOf (types.submodule ({ config, ... }: {
+          options = commonOptions // datasetOptions;
+          config = mkMerge [ (commonConfig config) (datasetConfig config) ];
+        }));
+        default = {};
+        description = "Datasets to snapshot.";
+      };
+
+      templates = mkOption {
+        type = types.attrsOf (types.submodule ({ config, ... }: {
+          options = commonOptions;
+          config = commonConfig config;
+        }));
+        default = {};
+        description = "Templates for datasets.";
+      };
+
+      settings = mkOption {
+        type = types.attrsOf datasetSettingsType;
+        description = ''
+          Free-form settings written directly to the config file. See
+          <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
+          for allowed values.
+        '';
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--verbose" "--readonly" "--debug" ];
+        description = ''
+          Extra arguments to pass to sanoid. See
+          <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
+          for allowed options.
+        '';
+      };
+    };
+
+    # Implementation
+
+    config = mkIf cfg.enable {
+      services.sanoid.settings = mkMerge [
+        (mapAttrs' (d: v: nameValuePair ("template_" + d) v.settings) cfg.templates)
+        (mapAttrs (d: v: v.settings) cfg.datasets)
+      ];
+
+      systemd.services.sanoid = {
+        description = "Sanoid snapshot service";
+        serviceConfig = {
+          ExecStartPre = map (pool: lib.escapeShellArgs [
+            "+/run/booted-system/sw/bin/zfs" "allow"
+            "sanoid" "snapshot,mount,destroy" pool
+          ]) pools;
+          ExecStart = lib.escapeShellArgs ([
+            "${pkgs.sanoid}/bin/sanoid"
+            "--cron"
+            "--configdir" configDir
+          ] ++ cfg.extraArgs);
+          ExecStopPost = map (pool: lib.escapeShellArgs [
+            "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" pool
+          ]) pools;
+          User = "sanoid";
+          Group = "sanoid";
+          DynamicUser = true;
+          RuntimeDirectory = "sanoid";
+          CacheDirectory = "sanoid";
+        };
+        # Prevents missing snapshots during DST changes
+        environment.TZ = "UTC";
+        after = [ "zfs.target" ];
+        startAt = cfg.interval;
+      };
+    };
+
+    meta.maintainers = with maintainers; [ lopsided98 ];
+  }
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
new file mode 100644
index 00000000000..fff119c2cf0
--- /dev/null
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.syncoid;
+in {
+
+    # Interface
+
+    options.services.syncoid = {
+      enable = mkEnableOption "Syncoid ZFS synchronization service";
+
+      interval = mkOption {
+        type = types.str;
+        default = "hourly";
+        example = "*-*-* *:15:00";
+        description = ''
+          Run syncoid at this interval. The default is to run hourly.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "root";
+        example = "backup";
+        description = ''
+          The user for the service. Sudo or ZFS privilege delegation must be
+          configured to use a user other than root.
+        '';
+      };
+
+      sshKey = mkOption {
+        type = types.nullOr types.path;
+        # Prevent key from being copied to store
+        apply = mapNullable toString;
+        default = null;
+        description = ''
+          SSH private key file to use to login to the remote system. Can be
+          overridden in individual commands.
+        '';
+      };
+
+      commonArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--no-sync-snap" ];
+        description = ''
+          Arguments to add to every syncoid command, unless disabled for that
+          command. See
+          <link xlink:href="https://github.com/jimsalterjrs/sanoid/#syncoid-command-line-options"/>
+          for available options.
+        '';
+      };
+
+      commands = mkOption {
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          options = {
+            source = mkOption {
+              type = types.str;
+              example = "pool/dataset";
+              description = ''
+                Source ZFS dataset. Can be either local or remote. Defaults to
+                the attribute name.
+              '';
+            };
+
+            target = mkOption {
+              type = types.str;
+              example = "user@server:pool/dataset";
+              description = ''
+                Target ZFS dataset. Can be either local
+                (<replaceable>pool/dataset</replaceable>) or remote
+                (<replaceable>user@server:pool/dataset</replaceable>).
+              '';
+            };
+
+            recursive = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Whether to also transfer child datasets.
+              '';
+            };
+
+            sshKey = mkOption {
+              type = types.nullOr types.path;
+              # Prevent key from being copied to store
+              apply = mapNullable toString;
+              description = ''
+                SSH private key file to use to login to the remote system.
+                Defaults to <option>services.syncoid.sshKey</option> option.
+              '';
+            };
+
+            sendOptions = mkOption {
+              type = types.separatedString " ";
+              default = "";
+              example = "Lc e";
+              description = ''
+                Advanced options to pass to zfs send. Options are specified
+                without their leading dashes and separated by spaces.
+              '';
+            };
+
+            recvOptions = mkOption {
+              type = types.separatedString " ";
+              default = "";
+              example = "ux recordsize o compression=lz4";
+              description = ''
+                Advanced options to pass to zfs recv. Options are specified
+                without their leading dashes and separated by spaces.
+              '';
+            };
+
+            useCommonArgs = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                Whether to add the configured common arguments to this command.
+              '';
+            };
+
+            extraArgs = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "--sshport 2222" ];
+              description = "Extra syncoid arguments for this command.";
+            };
+          };
+          config = {
+            source = mkDefault name;
+            sshKey = mkDefault cfg.sshKey;
+          };
+        }));
+        default = {};
+        example = literalExample ''
+          {
+            "pool/test".target = "root@target:pool/test";
+          }
+        '';
+        description = "Syncoid commands to run.";
+      };
+    };
+
+    # Implementation
+
+    config = mkIf cfg.enable {
+      systemd.services.syncoid = {
+        description = "Syncoid ZFS synchronization service";
+        script = concatMapStringsSep "\n" (c: lib.escapeShellArgs
+          ([ "${pkgs.sanoid}/bin/syncoid" ]
+            ++ (optionals c.useCommonArgs cfg.commonArgs)
+            ++ (optional c.recursive "-r")
+            ++ (optionals (c.sshKey != null) [ "--sshkey" c.sshKey ])
+            ++ c.extraArgs
+            ++ [ "--sendoptions" c.sendOptions
+                 "--recvoptions" c.recvOptions
+                 c.source c.target
+               ])) (attrValues cfg.commands);
+        after = [ "zfs.target" ];
+        serviceConfig.User = cfg.user;
+        startAt = cfg.interval;
+      };
+    };
+
+    meta.maintainers = with maintainers; [ lopsided98 ];
+  }
diff --git a/nixos/modules/services/backup/tarsnap.nix b/nixos/modules/services/backup/tarsnap.nix
index 4fc7c24813a..6d99a1efb61 100644
--- a/nixos/modules/services/backup/tarsnap.nix
+++ b/nixos/modules/services/backup/tarsnap.nix
@@ -23,6 +23,10 @@ let
   '';
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "tarsnap" "cachedir" ] "Use services.tarsnap.archives.<name>.cachedir")
+  ];
+
   options = {
     services.tarsnap = {
       enable = mkOption {
diff --git a/nixos/modules/services/cluster/hadoop/default.nix b/nixos/modules/services/cluster/hadoop/default.nix
index f0f5a6ecbfc..bfb73f68371 100644
--- a/nixos/modules/services/cluster/hadoop/default.nix
+++ b/nixos/modules/services/cluster/hadoop/default.nix
@@ -7,33 +7,41 @@ with lib;
   options.services.hadoop = {
     coreSite = mkOption {
       default = {};
-      example = {
-        "fs.defaultFS" = "hdfs://localhost";
-      };
+      example = literalExample ''
+        {
+          "fs.defaultFS" = "hdfs://localhost";
+        }
+      '';
       description = "Hadoop core-site.xml definition";
     };
 
     hdfsSite = mkOption {
       default = {};
-      example = {
-        "dfs.nameservices" = "namenode1";
-      };
+      example = literalExample ''
+        {
+          "dfs.nameservices" = "namenode1";
+        }
+      '';
       description = "Hadoop hdfs-site.xml definition";
     };
 
     mapredSite = mkOption {
       default = {};
-      example = {
-        "mapreduce.map.cpu.vcores" = "1";
-      };
+      example = literalExample ''
+        {
+          "mapreduce.map.cpu.vcores" = "1";
+        }
+      '';
       description = "Hadoop mapred-site.xml definition";
     };
 
     yarnSite = mkOption {
       default = {};
-      example = {
-        "yarn.resourcemanager.ha.id" = "resourcemanager1";
-      };
+      example = literalExample ''
+        {
+          "yarn.resourcemanager.ha.id" = "resourcemanager1";
+        }
+      '';
       description = "Hadoop yarn-site.xml definition";
     };
 
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
new file mode 100644
index 00000000000..2e8bf20a68f
--- /dev/null
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.k3s;
+in
+{
+  # interface
+  options.services.k3s = {
+    enable = mkEnableOption "k3s";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.k3s;
+      defaultText = "pkgs.k3s";
+      example = literalExample "pkgs.k3s";
+      description = "Package that should be used for k3s";
+    };
+
+    role = mkOption {
+      description = ''
+        Whether k3s should run as a server or agent.
+        Note that the server, by default, also runs as an agent.
+      '';
+      default = "server";
+      type = types.enum [ "server" "agent" ];
+    };
+
+    serverAddr = mkOption {
+      type = types.str;
+      description = "The k3s server to connect to. This option only makes sense for an agent.";
+      example = "https://10.0.0.10:6443";
+      default = "";
+    };
+
+    token = mkOption {
+      type = types.str;
+      description = "The k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      default = "";
+    };
+
+    docker = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Use docker to run containers rather than the built-in containerd.";
+    };
+
+    extraFlags = mkOption {
+      description = "Extra flags to pass to the k3s command.";
+      default = "";
+      example = "--no-deploy traefik --cluster-cidr 10.24.0.0/16";
+    };
+
+    disableAgent = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Only run the server. This option only makes sense for a server.";
+    };
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.role == "agent" -> cfg.serverAddr != "";
+        message = "serverAddr should be set if role is 'agent'";
+      }
+      {
+        assertion = cfg.role == "agent" -> cfg.token != "";
+        message = "token should be set if role is 'agent'";
+      }
+    ];
+
+    virtualisation.docker = mkIf cfg.docker {
+      enable = mkDefault true;
+    };
+
+    systemd.services.k3s = {
+      description = "k3s service";
+      after = mkIf cfg.docker [ "docker.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        # See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
+        Type = if cfg.role == "agent" then "exec" else "notify";
+        KillMode = "process";
+        Delegate = "yes";
+        Restart = "always";
+        RestartSec = "5s";
+        ExecStart = concatStringsSep " \\\n " (
+          [
+            "${cfg.package}/bin/k3s ${cfg.role}"
+          ] ++ (optional cfg.docker "--docker")
+          ++ (optional cfg.disableAgent "--disable-agent")
+          ++ (optional (cfg.role == "agent") "--server ${cfg.serverAddr} --token ${cfg.token}")
+          ++ [ cfg.extraFlags ]
+        );
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
index 17f2dde31a7..f55079300b1 100644
--- a/nixos/modules/services/cluster/kubernetes/addon-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -88,7 +88,7 @@ in
 
     services.kubernetes.addonManager.bootstrapAddons = mkIf isRBACEnabled
     (let
-      name = system:kube-addon-manager;
+      name = "system:kube-addon-manager";
       namespace = "kube-system";
     in
     {
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix b/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
index 70f96d75a46..2ed7742eda0 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dashboard.nix
@@ -5,6 +5,10 @@ with lib;
 let
   cfg = config.services.kubernetes.addons.dashboard;
 in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "kubernetes" "addons" "dashboard" "enableRBAC" ] [ "services" "kubernetes" "addons" "dashboard" "rbac" "enable" ])
+  ];
+
   options.services.kubernetes.addons.dashboard = {
     enable = mkEnableOption "kubernetes dashboard addon";
 
diff --git a/nixos/modules/services/cluster/kubernetes/apiserver.nix b/nixos/modules/services/cluster/kubernetes/apiserver.nix
index 33796bf2e08..95bdb4c0d14 100644
--- a/nixos/modules/services/cluster/kubernetes/apiserver.nix
+++ b/nixos/modules/services/cluster/kubernetes/apiserver.nix
@@ -13,6 +13,18 @@ let
   )) + ".1");
 in
 {
+
+  imports = [
+    (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" "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" ])
+  ];
+
   ###### interface
   options.services.kubernetes.apiserver = with lib.types; {
 
diff --git a/nixos/modules/services/cluster/kubernetes/controller-manager.nix b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
index 0b73d090f24..a99ef6640e9 100644
--- a/nixos/modules/services/cluster/kubernetes/controller-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
@@ -7,6 +7,11 @@ let
   cfg = top.controllerManager;
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "address" ] ["services" "kubernetes" "controllerManager" "bindAddress"])
+    (mkRenamedOptionModule [ "services" "kubernetes" "controllerManager" "port" ] ["services" "kubernetes" "controllerManager" "insecurePort"])
+  ];
+
   ###### interface
   options.services.kubernetes.controllerManager = with lib.types; {
 
diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix
index 3790ac9b691..3a11a6513a4 100644
--- a/nixos/modules/services/cluster/kubernetes/default.nix
+++ b/nixos/modules/services/cluster/kubernetes/default.nix
@@ -74,6 +74,10 @@ let
   };
 in {
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "kubernetes" "verbose" ] "")
+  ];
+
   ###### interface
 
   options.services.kubernetes = {
@@ -262,8 +266,7 @@ in {
         "d /var/lib/kubernetes 0755 kubernetes kubernetes -"
       ];
 
-      users.users = singleton {
-        name = "kubernetes";
+      users.users.kubernetes = {
         uid = config.ids.uids.kubernetes;
         description = "Kubernetes user";
         extraGroups = [ "docker" ];
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 62d893dfefc..c3d67552cc8 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -52,6 +52,12 @@ let
   taints = concatMapStringsSep "," (v: "${v.key}=${v.value}:${v.effect}") (mapAttrsToList (n: v: v) cfg.taints);
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "applyManifests" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "")
+  ];
+
   ###### interface
   options.services.kubernetes.kubelet = with lib.types; {
 
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 733479e24c9..4275563f1a3 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -20,6 +20,7 @@ let
         size = 2048;
     };
     CN = top.masterAddress;
+    hosts = cfg.cfsslAPIExtraSANs;
   });
 
   cfsslAPITokenBaseName = "apitoken.secret";
@@ -66,6 +67,15 @@ in
       type = bool;
     };
 
+    cfsslAPIExtraSANs = mkOption {
+      description = ''
+        Extra x509 Subject Alternative Names to be added to the cfssl API webserver TLS cert.
+      '';
+      default = [];
+      example = [ "subdomain.example.com" ];
+      type = listOf str;
+    };
+
     genCfsslAPIToken = mkOption {
       description = ''
         Whether to automatically generate cfssl API-token secret,
diff --git a/nixos/modules/services/cluster/kubernetes/proxy.nix b/nixos/modules/services/cluster/kubernetes/proxy.nix
index 3943c908840..86d1dc2439b 100644
--- a/nixos/modules/services/cluster/kubernetes/proxy.nix
+++ b/nixos/modules/services/cluster/kubernetes/proxy.nix
@@ -7,6 +7,9 @@ let
   cfg = top.proxy;
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "kubernetes" "proxy" "address" ] ["services" "kubernetes" "proxy" "bindAddress"])
+  ];
 
   ###### interface
   options.services.kubernetes.proxy = with lib.types; {
diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix
index a7edac02538..7becf624071 100644
--- a/nixos/modules/services/computing/boinc/client.nix
+++ b/nixos/modules/services/computing/boinc/client.nix
@@ -99,14 +99,16 @@ in
       environment.systemPackages = [cfg.package];
 
       users.users.boinc = {
+        group = "boinc";
         createHome = false;
         description = "BOINC Client";
         home = cfg.dataDir;
         isSystemUser = true;
       };
+      users.groups.boinc = {};
 
       systemd.tmpfiles.rules = [
-        "d '${cfg.dataDir}' - boinc - - -"
+        "d '${cfg.dataDir}' - boinc boinc - -"
       ];
 
       systemd.services.boinc = {
@@ -114,7 +116,7 @@ in
         after = ["network.target"];
         wantedBy = ["multi-user.target"];
         script = ''
-          ${fhsEnvExecutable} --dir ${cfg.dataDir} --redirectio ${allowRemoteGuiRpcFlag}
+          ${fhsEnvExecutable} --dir ${cfg.dataDir} ${allowRemoteGuiRpcFlag}
         '';
         serviceConfig = {
           User = "boinc";
diff --git a/nixos/modules/services/computing/foldingathome/client.nix b/nixos/modules/services/computing/foldingathome/client.nix
new file mode 100644
index 00000000000..9f99af48c48
--- /dev/null
+++ b/nixos/modules/services/computing/foldingathome/client.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.foldingathome;
+
+  args =
+    ["--team" "${toString cfg.team}"]
+    ++ lib.optionals (cfg.user != null) ["--user" cfg.user]
+    ++ cfg.extraArgs
+    ;
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "foldingAtHome" ] [ "services" "foldingathome" ])
+    (mkRenamedOptionModule [ "services" "foldingathome" "nickname" ] [ "services" "foldingathome" "user" ])
+    (mkRemovedOptionModule [ "services" "foldingathome" "config" ] ''
+      Use <literal>services.foldingathome.extraArgs instead<literal>
+    '')
+  ];
+  options.services.foldingathome = {
+    enable = mkEnableOption "Enable the Folding@home client";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.fahclient;
+      defaultText = "pkgs.fahclient";
+      description = ''
+        Which Folding@home client to use.
+      '';
+    };
+
+    user = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The user associated with the reported computation results. This will
+        be used in the ranking statistics.
+      '';
+    };
+
+    team = mkOption {
+      type = types.int;
+      default = 236565;
+      description = ''
+        The team ID associated with the reported computation results. This
+        will be used in the ranking statistics.
+
+        By default, use the NixOS folding@home team ID is being used.
+      '';
+    };
+
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra startup options for the FAHClient. Run
+        <literal>FAHClient --help</literal> to find all the available options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.foldingathome = {
+      description = "Folding@home client";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        exec ${cfg.package}/bin/FAHClient ${lib.escapeShellArgs args}
+      '';
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "foldingathome";
+        WorkingDirectory = "%S/foldingathome";
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ zimbatm ];
+  };
+}
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index c70d999ca96..050872e933f 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -355,6 +355,7 @@ in
         ExecStart = "${wrappedSlurm}/bin/slurmd";
         PIDFile = "/run/slurmd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitMEMLOCK = "infinity";
       };
 
       preStart = ''
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 9c615fbe885..e3da3092d45 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -222,19 +222,20 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.groups = optional (cfg.group == "buildbot") {
-      name = "buildbot";
+    users.groups = optionalAttrs (cfg.group == "buildbot") {
+      buildbot = { };
     };
 
-    users.users = optional (cfg.user == "buildbot") {
-      name = "buildbot";
-      description = "Buildbot User.";
-      isNormalUser = true;
-      createHome = true;
-      home = cfg.home;
-      group = cfg.group;
-      extraGroups = cfg.extraGroups;
-      useDefaultShell = true;
+    users.users = optionalAttrs (cfg.user == "buildbot") {
+      buildbot = {
+        description = "Buildbot User.";
+        isNormalUser = true;
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+      };
     };
 
     systemd.services.buildbot-master = {
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 49e04ca3622..52f24b8cee3 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -136,19 +136,20 @@ in {
   config = mkIf cfg.enable {
     services.buildbot-worker.workerPassFile = mkDefault (pkgs.writeText "buildbot-worker-password" cfg.workerPass);
 
-    users.groups = optional (cfg.group == "bbworker") {
-      name = "bbworker";
+    users.groups = optionalAttrs (cfg.group == "bbworker") {
+      bbworker = { };
     };
 
-    users.users = optional (cfg.user == "bbworker") {
-      name = "bbworker";
-      description = "Buildbot Worker User.";
-      isNormalUser = true;
-      createHome = true;
-      home = cfg.home;
-      group = cfg.group;
-      extraGroups = cfg.extraGroups;
-      useDefaultShell = true;
+    users.users = optionalAttrs (cfg.user == "bbworker") {
+      bbworker = {
+        description = "Buildbot Worker User.";
+        isNormalUser = true;
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+      };
     };
 
     systemd.services.buildbot-worker = {
diff --git a/nixos/modules/services/continuous-integration/buildkite-agent.nix b/nixos/modules/services/continuous-integration/buildkite-agents.nix
index 32f361454bc..b0045409ae6 100644
--- a/nixos/modules/services/continuous-integration/buildkite-agent.nix
+++ b/nixos/modules/services/continuous-integration/buildkite-agents.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  cfg = config.services.buildkite-agent;
+  cfg = config.services.buildkite-agents;
 
   mkHookOption = { name, description, example ? null }: {
     inherit name;
@@ -15,7 +15,7 @@ let
   };
   mkHookOptions = hooks: listToAttrs (map mkHookOption hooks);
 
-  hooksDir = let
+  hooksDir = cfg: let
     mkHookEntry = name: value: ''
       cat > $out/${name} <<'EOF'
       #! ${pkgs.runtimeShell}
@@ -29,12 +29,13 @@ let
     ${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))}
   '';
 
-in
-
-{
-  options = {
-    services.buildkite-agent = {
-      enable = mkEnableOption "buildkite-agent";
+  buildkiteOptions = { name ? "", config, ... }: {
+    options = {
+      enable = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Whether to enable this buildkite agent";
+      };
 
       package = mkOption {
         default = pkgs.buildkite-agent;
@@ -44,14 +45,14 @@ in
       };
 
       dataDir = mkOption {
-        default = "/var/lib/buildkite-agent";
+        default = "/var/lib/buildkite-agent-${name}";
         description = "The workdir for the agent";
         type = types.str;
       };
 
       runtimePackages = mkOption {
-        default = [ pkgs.bash pkgs.nix ];
-        defaultText = "[ pkgs.bash pkgs.nix ]";
+        default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ];
+        defaultText = "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]";
         description = "Add programs to the buildkite-agent environment";
         type = types.listOf types.package;
       };
@@ -68,19 +69,18 @@ in
 
       name = mkOption {
         type = types.str;
-        default = "%hostname-%n";
+        default = "%hostname-${name}-%n";
         description = ''
-          The name of the agent.
+          The name of the agent as seen in the buildkite dashboard.
         '';
       };
 
-      meta-data = mkOption {
-        type = types.str;
-        default = "";
-        example = "queue=default,docker=true,ruby2=true";
+      tags = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = { queue = "default"; docker = "true"; ruby2 ="true"; };
         description = ''
-          Meta data for the agent. This is a comma-separated list of
-          <code>key=value</code> pairs.
+          Tags for the agent.
         '';
       };
 
@@ -93,26 +93,20 @@ in
         '';
       };
 
-      openssh =
-        { privateKeyPath = mkOption {
-            type = types.path;
-            description = ''
-              Private agent key.
+      privateSshKeyPath = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        ## maximum care is taken so that secrets (ssh keys and the CI token)
+        ## don't end up in the Nix store.
+        apply = final: if final == null then null else toString final;
 
-              A run-time path to the key file, which is supposed to be provisioned
-              outside of Nix store.
-            '';
-          };
-          publicKeyPath = mkOption {
-            type = types.path;
-            description = ''
-              Public agent key.
-
-              A run-time path to the key file, which is supposed to be provisioned
-              outside of Nix store.
-            '';
-          };
-        };
+        description = ''
+          OpenSSH private key
+
+          A run-time path to the key file, which is supposed to be provisioned
+          outside of Nix store.
+        '';
+      };
 
       hooks = mkHookOptions [
         { name = "checkout";
@@ -173,34 +167,60 @@ in
 
       hooksPath = mkOption {
         type = types.path;
-        default = hooksDir;
-        defaultText = "generated from services.buildkite-agent.hooks";
+        default = hooksDir config;
+        defaultText = "generated from services.buildkite-agents.<name>.hooks";
         description = ''
           Path to the directory storing the hooks.
-          Consider using <option>services.buildkite-agent.hooks.&lt;name&gt;</option>
+          Consider using <option>services.buildkite-agents.&lt;name&gt;.hooks.&lt;name&gt;</option>
           instead.
         '';
       };
-    };
-  };
 
-  config = mkIf config.services.buildkite-agent.enable {
-    users.users.buildkite-agent =
-      { name = "buildkite-agent";
-        home = cfg.dataDir;
-        createHome = true;
-        description = "Buildkite agent user";
-        extraGroups = [ "keys" ];
-        isSystemUser = true;
+      shell = mkOption {
+        type = types.str;
+        default = "${pkgs.bash}/bin/bash -e -c";
+        description = ''
+          Command that buildkite-agent 3 will execute when it spawns a shell.
+        '';
       };
+    };
+  };
+  enabledAgents = lib.filterAttrs (n: v: v.enable) cfg;
+  mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents);
+in
+{
+  options.services.buildkite-agents = mkOption {
+    type = types.attrsOf (types.submodule buildkiteOptions);
+    default = {};
+    description = ''
+      Attribute set of buildkite agents.
+      The attribute key is combined with the hostname and a unique integer to
+      create the final agent name. This can be overridden by setting the `name`
+      attribute.
+    '';
+  };
 
-    environment.systemPackages = [ cfg.package ];
+  config.users.users = mapAgents (name: cfg: {
+    "buildkite-agent-${name}" = {
+      name = "buildkite-agent-${name}";
+      home = cfg.dataDir;
+      createHome = true;
+      description = "Buildkite agent user";
+      extraGroups = [ "keys" ];
+      isSystemUser = true;
+      group = "buildkite-agent-${name}";
+    };
+  });
+  config.users.groups = mapAgents (name: cfg: {
+    "buildkite-agent-${name}" = {};
+  });
 
-    systemd.services.buildkite-agent =
+  config.systemd.services = mapAgents (name: cfg: {
+    "buildkite-agent-${name}" =
       { description = "Buildkite Agent";
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ];
-        path = cfg.runtimePackages ++ [ pkgs.coreutils ];
+        path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ];
         environment = config.networking.proxy.envVars // {
           HOME = cfg.dataDir;
           NIX_REMOTE = "daemon";
@@ -210,17 +230,18 @@ in
         ##     don't end up in the Nix store.
         preStart = let
           sshDir = "${cfg.dataDir}/.ssh";
+          tagStr = lib.concatStringsSep "," (lib.mapAttrsToList (name: value: "${name}=${value}") cfg.tags);
         in
-          ''
+          optionalString (cfg.privateSshKeyPath != null) ''
             mkdir -m 0700 -p "${sshDir}"
-            cp -f "${toString cfg.openssh.privateKeyPath}" "${sshDir}/id_rsa"
-            cp -f "${toString cfg.openssh.publicKeyPath}"  "${sshDir}/id_rsa.pub"
-            chmod 600 "${sshDir}"/id_rsa*
-
+            cp -f "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
+            chmod 600 "${sshDir}"/id_rsa
+          '' + ''
             cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
             token="$(cat ${toString cfg.tokenPath})"
             name="${cfg.name}"
-            meta-data="${cfg.meta-data}"
+            shell="${cfg.shell}"
+            tags="${tagStr}"
             build-path="${cfg.dataDir}/builds"
             hooks-path="${cfg.hooksPath}"
             ${cfg.extraConfig}
@@ -228,26 +249,28 @@ in
           '';
 
         serviceConfig =
-          { ExecStart = "${pkgs.buildkite-agent}/bin/buildkite-agent start --config /var/lib/buildkite-agent/buildkite-agent.cfg";
-            User = "buildkite-agent";
+          { ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg";
+            User = "buildkite-agent-${name}";
             RestartSec = 5;
             Restart = "on-failure";
             TimeoutSec = 10;
+            # set a long timeout to give buildkite-agent a chance to finish current builds
+            TimeoutStopSec = "2 min";
+            KillMode = "mixed";
           };
       };
+  });
 
-    assertions = [
-      { assertion = cfg.hooksPath == hooksDir || all (v: v == null) (attrValues cfg.hooks);
+  config.assertions = mapAgents (name: cfg: [
+      { assertion = cfg.hooksPath == (hooksDir cfg) || all (v: v == null) (attrValues cfg.hooks);
         message = ''
-          Options `services.buildkite-agent.hooksPath' and
-          `services.buildkite-agent.hooks.<name>' are mutually exclusive.
+          Options `services.buildkite-agents.${name}.hooksPath' and
+          `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive.
         '';
       }
-    ];
-  };
+  ]);
+
   imports = [
-    (mkRenamedOptionModule [ "services" "buildkite-agent" "token" ]                [ "services" "buildkite-agent" "tokenPath" ])
-    (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "privateKey" ] [ "services" "buildkite-agent" "openssh" "privateKeyPath" ])
-    (mkRenamedOptionModule [ "services" "buildkite-agent" "openssh" "publicKey" ]  [ "services" "buildkite-agent" "openssh" "publicKeyPath" ])
+    (mkRemovedOptionModule [ "services" "buildkite-agent"] "services.buildkite-agent has been upgraded from version 2 to version 3 and moved to an attribute set at services.buildkite-agents. Please consult the 20.03 release notes for more information.")
   ];
 }
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index 3d307b1abcf..bd4cf6a37ba 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -120,10 +120,16 @@ in
         ++ optional hasDocker "docker.service";
       requires = optional hasDocker "docker.service";
       wantedBy = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      restartTriggers = [
+         config.environment.etc."gitlab-runner/config.toml".source
+      ];
       serviceConfig = {
+        StateDirectory = "gitlab-runner";
+        ExecReload= "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStart = ''${cfg.package.bin}/bin/gitlab-runner run \
           --working-directory ${cfg.workDir} \
-          --config ${configFile} \
+          --config /etc/gitlab-runner/config.toml \
           --service gitlab-runner \
           --user gitlab-runner \
         '';
@@ -138,6 +144,9 @@ in
     # Make the gitlab-runner command availabe so users can query the runner
     environment.systemPackages = [ cfg.package ];
 
+    # Make sure the config can be reloaded on change
+    environment.etc."gitlab-runner/config.toml".source = configFile;
+
     users.users.gitlab-runner = {
       group = "gitlab-runner";
       extraGroups = optional hasDocker "docker";
diff --git a/nixos/modules/services/continuous-integration/gocd-agent/default.nix b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
index 8126f27c2b0..2e9e1c94857 100644
--- a/nixos/modules/services/continuous-integration/gocd-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
@@ -135,20 +135,20 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.groups = optional (cfg.group == "gocd-agent") {
-      name = "gocd-agent";
-      gid = config.ids.gids.gocd-agent;
+    users.groups = optionalAttrs (cfg.group == "gocd-agent") {
+      gocd-agent.gid = config.ids.gids.gocd-agent;
     };
 
-    users.users = optional (cfg.user == "gocd-agent") {
-      name = "gocd-agent";
-      description = "gocd-agent user";
-      createHome = true;
-      home = cfg.workDir;
-      group = cfg.group;
-      extraGroups = cfg.extraGroups;
-      useDefaultShell = true;
-      uid = config.ids.uids.gocd-agent;
+    users.users = optionalAttrs (cfg.user == "gocd-agent") {
+      gocd-agent = {
+        description = "gocd-agent user";
+        createHome = true;
+        home = cfg.workDir;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+        uid = config.ids.uids.gocd-agent;
+      };
     };
 
     systemd.services.gocd-agent = {
diff --git a/nixos/modules/services/continuous-integration/gocd-server/default.nix b/nixos/modules/services/continuous-integration/gocd-server/default.nix
index 8f177da129e..4fa41ac49ed 100644
--- a/nixos/modules/services/continuous-integration/gocd-server/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-server/default.nix
@@ -143,20 +143,20 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.groups = optional (cfg.group == "gocd-server") {
-      name = "gocd-server";
-      gid = config.ids.gids.gocd-server;
+    users.groups = optionalAttrs (cfg.group == "gocd-server") {
+      gocd-server.gid = config.ids.gids.gocd-server;
     };
 
-    users.users = optional (cfg.user == "gocd-server") {
-      name = "gocd-server";
-      description = "gocd-server user";
-      createHome = true;
-      home = cfg.workDir;
-      group = cfg.group;
-      extraGroups = cfg.extraGroups;
-      useDefaultShell = true;
-      uid = config.ids.uids.gocd-server;
+    users.users = optionalAttrs (cfg.user == "gocd-server") {
+      gocd-server = {
+        description = "gocd-server user";
+        createHome = true;
+        home = cfg.workDir;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+        uid = config.ids.uids.gocd-server;
+      };
     };
 
     systemd.services.gocd-server = {
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 30c5550f71c..502a5898a5d 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -37,6 +37,38 @@ let
 
   haveLocalDB = cfg.dbi == localDB;
 
+  inherit (config.system) stateVersion;
+
+  hydra-package =
+  let
+    makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv);
+  in pkgs.buildEnv rec {
+    name = "hydra-env";
+    buildInputs = [ pkgs.makeWrapper ];
+    paths = [ cfg.package ];
+
+    postBuild = ''
+      if [ -L "$out/bin" ]; then
+          unlink "$out/bin"
+      fi
+      mkdir -p "$out/bin"
+
+      for path in ${concatStringsSep " " paths}; do
+        if [ -d "$path/bin" ]; then
+          cd "$path/bin"
+          for prg in *; do
+            if [ -f "$prg" ]; then
+              rm -f "$out/bin/$prg"
+              if [ -x "$prg" ]; then
+                makeWrapper "$path/bin/$prg" "$out/bin/$prg" ${makeWrapperArgs}
+              fi
+            fi
+          done
+        fi
+      done
+   '';
+  };
+
 in
 
 {
@@ -63,8 +95,7 @@ in
       };
 
       package = mkOption {
-        type = types.path;
-        default = pkgs.hydra;
+        type = types.package;
         defaultText = "pkgs.hydra";
         description = "The Hydra package.";
       };
@@ -167,7 +198,7 @@ in
 
       buildMachinesFiles = mkOption {
         type = types.listOf types.path;
-        default = [ "/etc/nix/machines" ];
+        default = optional (config.nix.buildMachines != []) "/etc/nix/machines";
         example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
         description = "List of files containing build machines.";
       };
@@ -194,6 +225,34 @@ in
 
   config = mkIf cfg.enable {
 
+    warnings = optional (cfg.package.migration or false) ''
+      You're currently deploying an older version of Hydra which is needed to
+      make some required database changes[1]. As soon as this is done, it's recommended
+      to run `hydra-backfill-ids` and set `services.hydra.package` to `pkgs.hydra-unstable`
+      after that.
+
+      [1] https://github.com/NixOS/hydra/pull/711
+    '';
+
+    services.hydra.package = with pkgs;
+      mkDefault (
+        if pkgs ? hydra
+          then throw ''
+            The Hydra package doesn't exist anymore in `nixpkgs`! It probably exists
+            due to an overlay. To upgrade Hydra, you need to take two steps as some
+            bigger changes in the database schema were implemented recently[1]. You first
+            need to deploy `pkgs.hydra-migration`, run `hydra-backfill-ids` on the server
+            and then deploy `pkgs.hydra-unstable`.
+
+            If you want to use `pkgs.hydra` from your overlay, please set `services.hydra.package`
+            explicitly to `pkgs.hydra` and make sure you know what you're doing.
+
+            [1] https://github.com/NixOS/hydra/pull/711
+          ''
+        else if versionOlder stateVersion "20.03" then hydra-migration
+        else hydra-unstable
+      );
+
     users.groups.hydra = {
       gid = config.ids.gids.hydra;
     };
@@ -237,7 +296,7 @@ in
         use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
       '';
 
-    environment.systemPackages = [ cfg.package ];
+    environment.systemPackages = [ hydra-package ];
 
     environment.variables = hydraEnv;
 
@@ -298,7 +357,7 @@ in
           chown hydra.hydra ${cfg.gcRootsDir}
           chmod 2775 ${cfg.gcRootsDir}
         '';
-        serviceConfig.ExecStart = "${cfg.package}/bin/hydra-init";
+        serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
         serviceConfig.PermissionsStartOnly = true;
         serviceConfig.User = "hydra";
         serviceConfig.Type = "oneshot";
@@ -313,7 +372,7 @@ in
         restartTriggers = [ hydraConf ];
         serviceConfig =
           { ExecStart =
-              "@${cfg.package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
+              "@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
               + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 "
               + "--max_requests 100 ${optionalString cfg.debugServer "-d"}";
             User = "hydra-www";
@@ -326,15 +385,15 @@ in
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" "network.target" ];
-        path = [ cfg.package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
+        path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
         restartTriggers = [ hydraConf ];
         environment = env // {
           PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
           IN_SYSTEMD = "1"; # to get log severity levels
         };
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner -v --option build-use-substitutes ${boolToString cfg.useSubstitutes}";
-            ExecStopPost = "${cfg.package}/bin/hydra-queue-runner --unlock";
+          { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
+            ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock";
             User = "hydra-queue-runner";
             Restart = "always";
 
@@ -348,11 +407,11 @@ in
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" "network.target" ];
-        path = with pkgs; [ cfg.package nettools jq ];
+        path = with pkgs; [ hydra-package nettools jq ];
         restartTriggers = [ hydraConf ];
         environment = env;
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/hydra-evaluator hydra-evaluator";
+          { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
             User = "hydra";
             Restart = "always";
             WorkingDirectory = baseDir;
@@ -364,7 +423,7 @@ in
         after = [ "hydra-init.service" ];
         environment = env;
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
+          { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
             User = "hydra";
           };
         startAt = "2,14:15";
@@ -375,7 +434,7 @@ in
         after = [ "hydra-init.service" ];
         environment = env;
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/hydra-send-stats hydra-send-stats";
+          { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
             User = "hydra";
           };
       };
@@ -389,7 +448,7 @@ in
           PGPASSFILE = "${baseDir}/pgpass-queue-runner";
         };
         serviceConfig =
-          { ExecStart = "@${cfg.package}/bin/hydra-notify hydra-notify";
+          { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
             # FIXME: run this under a less privileged user?
             User = "hydra-queue-runner";
             Restart = "always";
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix
index 0ec90671388..1477c471f8a 100644
--- a/nixos/modules/services/continuous-integration/jenkins/default.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/default.nix
@@ -150,20 +150,20 @@ in {
       pkgs.dejavu_fonts
     ];
 
-    users.groups = optional (cfg.group == "jenkins") {
-      name = "jenkins";
-      gid = config.ids.gids.jenkins;
+    users.groups = optionalAttrs (cfg.group == "jenkins") {
+      jenkins.gid = config.ids.gids.jenkins;
     };
 
-    users.users = optional (cfg.user == "jenkins") {
-      name = "jenkins";
-      description = "jenkins user";
-      createHome = true;
-      home = cfg.home;
-      group = cfg.group;
-      extraGroups = cfg.extraGroups;
-      useDefaultShell = true;
-      uid = config.ids.uids.jenkins;
+    users.users = optionalAttrs (cfg.user == "jenkins") {
+      jenkins = {
+        description = "jenkins user";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        useDefaultShell = true;
+        uid = config.ids.uids.jenkins;
+      };
     };
 
     systemd.services.jenkins = {
diff --git a/nixos/modules/services/continuous-integration/jenkins/slave.nix b/nixos/modules/services/continuous-integration/jenkins/slave.nix
index 92deabc3dd3..3c0e6f78e74 100644
--- a/nixos/modules/services/continuous-integration/jenkins/slave.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/slave.nix
@@ -50,19 +50,19 @@ in {
   };
 
   config = mkIf (cfg.enable && !masterCfg.enable) {
-    users.groups = optional (cfg.group == "jenkins") {
-      name = "jenkins";
-      gid = config.ids.gids.jenkins;
+    users.groups = optionalAttrs (cfg.group == "jenkins") {
+      jenkins.gid = config.ids.gids.jenkins;
     };
 
-    users.users = optional (cfg.user == "jenkins") {
-      name = "jenkins";
-      description = "jenkins user";
-      createHome = true;
-      home = cfg.home;
-      group = cfg.group;
-      useDefaultShell = true;
-      uid = config.ids.uids.jenkins;
+    users.users = optionalAttrs (cfg.user == "jenkins") {
+      jenkins = {
+        description = "jenkins user";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        useDefaultShell = true;
+        uid = config.ids.uids.jenkins;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/databases/4store-endpoint.nix b/nixos/modules/services/databases/4store-endpoint.nix
deleted file mode 100644
index 59ed0e5f0af..00000000000
--- a/nixos/modules/services/databases/4store-endpoint.nix
+++ /dev/null
@@ -1,74 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  cfg = config.services.fourStoreEndpoint;
-  endpointUser = "fourstorehttp";
-  run = "${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${endpointUser} -c";
-in
-with lib;
-{
-
-  ###### interface
-
-  options = {
-
-    services.fourStoreEndpoint = {
-
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable 4Store SPARQL endpoint.";
-      };
-
-      database = mkOption {
-        default = config.services.fourStore.database;
-        description = "RDF database name to expose via the endpoint. Defaults to local 4Store database name.";
-      };
-
-      listenAddress = mkOption {
-        default = null;
-        description = "IP address to listen on.";
-      };
-
-      port = mkOption {
-        default = 8080;
-        description = "port to listen on.";
-      };
-
-      options = mkOption {
-        default = "";
-        description = "Extra CLI options to pass to 4Store's 4s-httpd process.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    assertions = singleton
-      { assertion = cfg.enable -> cfg.database != "";
-        message = "Must specify 4Store database name";
-      };
-
-    users.users = singleton
-      { name = endpointUser;
-        uid = config.ids.uids.fourstorehttp;
-        description = "4Store SPARQL endpoint user";
-      };
-
-    services.avahi.enable = true;
-
-    systemd.services."4store-endpoint" = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      script = ''
-        ${run} '${pkgs.rdf4store}/bin/4s-httpd -D ${cfg.options} ${if cfg.listenAddress!=null then "-H ${cfg.listenAddress}" else "" } -p ${toString cfg.port} ${cfg.database}'
-      '';
-    };
-
-  };
-
-}
diff --git a/nixos/modules/services/databases/4store.nix b/nixos/modules/services/databases/4store.nix
deleted file mode 100644
index be4351c1c38..00000000000
--- a/nixos/modules/services/databases/4store.nix
+++ /dev/null
@@ -1,72 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  cfg = config.services.fourStore;
-  stateDir = "/var/lib/4store";
-  fourStoreUser = "fourstore";
-  run = "${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${fourStoreUser}";
-in
-with lib;
-{
-
-  ###### interface
-
-  options = {
-
-    services.fourStore = {
-
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable 4Store RDF database server.";
-      };
-
-      database = mkOption {
-        default = "";
-        description = "RDF database name. If it doesn't exist, it will be created. Databases are stored in ${stateDir}.";
-      };
-
-      options = mkOption {
-        default = "";
-        description = "Extra CLI options to pass to 4Store.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    assertions = singleton
-      { assertion = cfg.enable -> cfg.database != "";
-        message = "Must specify 4Store database name.";
-      };
-
-    users.users = singleton
-      { name = fourStoreUser;
-        uid = config.ids.uids.fourstore;
-        description = "4Store database user";
-        home = stateDir;
-      };
-
-    services.avahi.enable = true;
-
-    systemd.services."4store" = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart = ''
-        mkdir -p ${stateDir}/
-        chown ${fourStoreUser} ${stateDir}
-        if ! test -e "${stateDir}/${cfg.database}"; then
-          ${run} -c '${pkgs.rdf4store}/bin/4s-backend-setup ${cfg.database}'
-        fi
-      '';
-
-      script = ''
-        ${run} -c '${pkgs.rdf4store}/bin/4s-backend -D ${cfg.options} ${cfg.database}'
-      '';
-    };
-  };
-}
diff --git a/nixos/modules/services/databases/clickhouse.nix b/nixos/modules/services/databases/clickhouse.nix
index dbabcae43ee..27440fec4e1 100644
--- a/nixos/modules/services/databases/clickhouse.nix
+++ b/nixos/modules/services/databases/clickhouse.nix
@@ -11,10 +11,7 @@ with lib;
 
     services.clickhouse = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable ClickHouse database server.";
-      };
+      enable = mkEnableOption "ClickHouse database server";
 
     };
 
diff --git a/nixos/modules/services/databases/cockroachdb.nix b/nixos/modules/services/databases/cockroachdb.nix
index 268fdcc819f..35fb46d69d8 100644
--- a/nixos/modules/services/databases/cockroachdb.nix
+++ b/nixos/modules/services/databases/cockroachdb.nix
@@ -153,7 +153,7 @@ in
         defaultText = "pkgs.cockroachdb";
         description = ''
           The CockroachDB derivation to use for running the service.
-          
+
           This would primarily be useful to enable Enterprise Edition features
           in your own custom CockroachDB build (Nixpkgs CockroachDB binaries
           only contain open source features and open source code).
@@ -171,17 +171,17 @@ in
 
     environment.systemPackages = [ crdb ];
 
-    users.users = optionalAttrs (cfg.user == "cockroachdb") (singleton
-      { name        = "cockroachdb";
+    users.users = optionalAttrs (cfg.user == "cockroachdb") {
+      cockroachdb = {
         description = "CockroachDB Server User";
         uid         = config.ids.uids.cockroachdb;
         group       = cfg.group;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "cockroachdb") (singleton
-      { name = "cockroachdb";
-        gid  = config.ids.gids.cockroachdb;
-      });
+    users.groups = optionalAttrs (cfg.group == "cockroachdb") {
+      cockroachdb.gid = config.ids.gids.cockroachdb;
+    };
 
     networking.firewall.allowedTCPPorts = lib.optionals cfg.openPorts
       [ cfg.http.port cfg.listen.port ];
diff --git a/nixos/modules/services/databases/firebird.nix b/nixos/modules/services/databases/firebird.nix
index 042c9841df5..95837aa1cea 100644
--- a/nixos/modules/services/databases/firebird.nix
+++ b/nixos/modules/services/databases/firebird.nix
@@ -40,12 +40,7 @@ in
 
     services.firebird = {
 
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable the Firebird super server.
-        '';
-      };
+      enable = mkEnableOption "the Firebird super server";
 
       package = mkOption {
         default = pkgs.firebirdSuper;
diff --git a/nixos/modules/services/databases/foundationdb.nix b/nixos/modules/services/databases/foundationdb.nix
index 8f8d0da7c8d..18727acc7c7 100644
--- a/nixos/modules/services/databases/foundationdb.nix
+++ b/nixos/modules/services/databases/foundationdb.nix
@@ -341,17 +341,17 @@ in
 
     environment.systemPackages = [ pkg ];
 
-    users.users = optionalAttrs (cfg.user == "foundationdb") (singleton
-      { name        = "foundationdb";
+    users.users = optionalAttrs (cfg.user == "foundationdb") {
+      foundationdb = {
         description = "FoundationDB User";
         uid         = config.ids.uids.foundationdb;
         group       = cfg.group;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "foundationdb") (singleton
-      { name = "foundationdb";
-        gid  = config.ids.gids.foundationdb;
-      });
+    users.groups = optionalAttrs (cfg.group == "foundationdb") {
+      foundationdb.gid = config.ids.gids.foundationdb;
+    };
 
     networking.firewall.allowedTCPPortRanges = mkIf cfg.openFirewall
       [ { from = cfg.listenPortStart;
diff --git a/nixos/modules/services/databases/influxdb.nix b/nixos/modules/services/databases/influxdb.nix
index 2f176a03872..dd5d69b1147 100644
--- a/nixos/modules/services/databases/influxdb.nix
+++ b/nixos/modules/services/databases/influxdb.nix
@@ -182,15 +182,15 @@ in
         '';
     };
 
-    users.users = optional (cfg.user == "influxdb") {
-      name = "influxdb";
-      uid = config.ids.uids.influxdb;
-      description = "Influxdb daemon user";
+    users.users = optionalAttrs (cfg.user == "influxdb") {
+      influxdb = {
+        uid = config.ids.uids.influxdb;
+        description = "Influxdb daemon user";
+      };
     };
 
-    users.groups = optional (cfg.group == "influxdb") {
-      name = "influxdb";
-      gid = config.ids.gids.influxdb;
+    users.groups = optionalAttrs (cfg.group == "influxdb") {
+      influxdb.gid = config.ids.gids.influxdb;
     };
   };
 
diff --git a/nixos/modules/services/databases/memcached.nix b/nixos/modules/services/databases/memcached.nix
index d1dfdb41bf4..f54bb6cc9b1 100644
--- a/nixos/modules/services/databases/memcached.nix
+++ b/nixos/modules/services/databases/memcached.nix
@@ -18,12 +18,7 @@ in
 
     services.memcached = {
 
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to enable Memcached.
-        ";
-      };
+      enable = mkEnableOption "Memcached";
 
       user = mkOption {
         default = "memcached";
@@ -64,10 +59,9 @@ in
 
   config = mkIf config.services.memcached.enable {
 
-    users.users = optional (cfg.user == "memcached") {
-      name = "memcached";
-      description = "Memcached server user";
-      isSystemUser = true;
+    users.users = optionalAttrs (cfg.user == "memcached") {
+      memcached.description = "Memcached server user";
+      memcached.isSystemUser = true;
     };
 
     environment.systemPackages = [ memcached ];
diff --git a/nixos/modules/services/databases/mongodb.nix b/nixos/modules/services/databases/mongodb.nix
index 12879afed47..4453a182990 100644
--- a/nixos/modules/services/databases/mongodb.nix
+++ b/nixos/modules/services/databases/mongodb.nix
@@ -29,12 +29,7 @@ in
 
     services.mongodb = {
 
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to enable the MongoDB server.
-        ";
-      };
+      enable = mkEnableOption "the MongoDB server";
 
       package = mkOption {
         default = pkgs.mongodb;
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
index 5549cfa5cf4..f9e657f5774 100644
--- a/nixos/modules/services/databases/mysql.nix
+++ b/nixos/modules/services/databases/mysql.nix
@@ -10,20 +10,21 @@ let
 
   isMariaDB = lib.getName mysql == lib.getName pkgs.mariadb;
 
-  isMysqlAtLeast57 =
-    (lib.getName mysql == lib.getName pkgs.mysql57)
-     && (builtins.compareVersions mysql.version "5.7" >= 0);
-
   mysqldOptions =
     "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}";
-  # For MySQL 5.7+, --insecure creates the root user without password
-  # (earlier versions and MariaDB do this by default).
-  installOptions =
-    "${mysqldOptions} ${lib.optionalString isMysqlAtLeast57 "--insecure"}";
+
+  settingsFile = pkgs.writeText "my.cnf" (
+    generators.toINI { listsAsDuplicateKeys = true; } cfg.settings +
+    optionalString (cfg.extraOptions != null) "[mysqld]\n${cfg.extraOptions}"
+  );
 
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd")
+    (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.")
+  ];
 
   ###### interface
 
@@ -72,9 +73,64 @@ in
         description = "Location where MySQL stores its table files";
       };
 
+      configFile = mkOption {
+        type = types.path;
+        default = settingsFile;
+        defaultText = "settingsFile";
+        description = ''
+          Override the configuration file used by MySQL. By default,
+          NixOS generates one automatically from <option>services.mysql.settings</option>.
+        '';
+        example = literalExample ''
+          pkgs.writeText "my.cnf" '''
+            [mysqld]
+            datadir = /var/lib/mysql
+            bind-address = 127.0.0.1
+            port = 3336
+            plugin-load-add = auth_socket.so
+
+            !includedir /etc/mysql/conf.d/
+          ''';
+        '';
+      };
+
+      settings = mkOption {
+        type = with types; attrsOf (attrsOf (oneOf [ bool int str (listOf str) ]));
+        default = {};
+        description = ''
+          MySQL configuration. Refer to
+          <link xlink:href="https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html"/>,
+          <link xlink:href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html"/>,
+          and <link xlink:href="https://mariadb.com/kb/en/server-system-variables/"/>
+          for details on supported values.
+
+          <note>
+            <para>
+              MySQL configuration options such as <literal>--quick</literal> should be treated as
+              boolean options and provided values such as <literal>true</literal>, <literal>false</literal>,
+              <literal>1</literal>, or <literal>0</literal>. See the provided example below.
+            </para>
+          </note>
+        '';
+        example = literalExample ''
+          {
+            mysqld = {
+              key_buffer_size = "6G";
+              table_cache = 1600;
+              log-error = "/var/log/mysql_err.log";
+              plugin-load-add = [ "server_audit" "ed25519=auth_ed25519" ];
+            };
+            mysqldump = {
+              quick = true;
+              max_allowed_packet = "16M";
+            };
+          }
+        '';
+      };
+
       extraOptions = mkOption {
-        type = types.lines;
-        default = "";
+        type = with types; nullOr lines;
+        default = null;
         example = ''
           key_buffer_size = 6G
           table_cache = 1600
@@ -248,10 +304,27 @@ in
 
   config = mkIf config.services.mysql.enable {
 
+    warnings = optional (cfg.extraOptions != null) "services.mysql.`extraOptions` is deprecated, please use services.mysql.`settings`.";
+
     services.mysql.dataDir =
       mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
                  else "/var/mysql");
 
+    services.mysql.settings.mysqld = mkMerge [
+      {
+        datadir = cfg.dataDir;
+        bind-address = mkIf (cfg.bind != null) cfg.bind;
+        port = cfg.port;
+        plugin-load-add = optional (cfg.ensureUsers != []) "auth_socket.so";
+      }
+      (mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
+        log-bin = "mysql-bin-${toString cfg.replication.serverId}";
+        log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
+        relay-log = "mysql-relay-bin";
+        server-id = cfg.replication.serverId;
+      })
+    ];
+
     users.users.mysql = {
       description = "MySQL server user";
       group = "mysql";
@@ -262,25 +335,7 @@ in
 
     environment.systemPackages = [mysql];
 
-    environment.etc."my.cnf".text =
-    ''
-      [mysqld]
-      port = ${toString cfg.port}
-      datadir = ${cfg.dataDir}
-      ${optionalString (cfg.bind != null) "bind-address = ${cfg.bind}" }
-      ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave")
-      ''
-        log-bin=mysql-bin-${toString cfg.replication.serverId}
-        log-bin-index=mysql-bin-${toString cfg.replication.serverId}.index
-        relay-log=mysql-relay-bin
-        server-id = ${toString cfg.replication.serverId}
-      ''}
-      ${optionalString (cfg.ensureUsers != [])
-      ''
-        plugin-load-add = auth_socket.so
-      ''}
-      ${cfg.extraOptions}
-    '';
+    environment.etc."my.cnf".source = cfg.configFile;
 
     systemd.tmpfiles.rules = [
       "d '${cfg.dataDir}' 0700 ${cfg.user} mysql -"
@@ -293,7 +348,7 @@ in
 
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
-        restartTriggers = [ config.environment.etc."my.cnf".source ];
+        restartTriggers = [ cfg.configFile ];
 
         unitConfig.RequiresMountsFor = "${cfg.dataDir}";
 
@@ -303,9 +358,14 @@ in
           pkgs.nettools
         ];
 
-        preStart = ''
+        preStart = if isMariaDB then ''
+          if ! test -e ${cfg.dataDir}/mysql; then
+            ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
+            touch /tmp/mysql_init
+          fi
+        '' else ''
           if ! test -e ${cfg.dataDir}/mysql; then
-            ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${installOptions}
+            ${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
             touch /tmp/mysql_init
           fi
         '';
@@ -316,6 +376,8 @@ in
           Type = if hasNotify then "notify" else "simple";
           RuntimeDirectory = "mysqld";
           RuntimeDirectoryMode = "0755";
+          Restart = "on-abort";
+          RestartSec = "5s";
           # The last two environment variables are used for starting Galera clusters
           ExecStart = "${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
           ExecStartPost =
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
index 5533182c311..09b453e7584 100644
--- a/nixos/modules/services/databases/neo4j.nix
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -103,6 +103,18 @@ let
 
 in {
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "neo4j" "host" ] [ "services" "neo4j" "defaultListenAddress" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "listenAddress" ] [ "services" "neo4j" "defaultListenAddress" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "enableBolt" ] [ "services" "neo4j" "bolt" "enable" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "enableHttps" ] [ "services" "neo4j" "https" "enable" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "certDir" ] [ "services" "neo4j" "directories" "certificates" ])
+    (mkRenamedOptionModule [ "services" "neo4j" "dataDir" ] [ "services" "neo4j" "directories" "home" ])
+    (mkRemovedOptionModule [ "services" "neo4j" "port" ] "Use services.neo4j.http.listenAddress instead.")
+    (mkRemovedOptionModule [ "services" "neo4j" "boltPort" ] "Use services.neo4j.bolt.listenAddress instead.")
+    (mkRemovedOptionModule [ "services" "neo4j" "httpsPort" ] "Use services.neo4j.https.listenAddress instead.")
+  ];
+
   ###### interface
 
   options.services.neo4j = {
@@ -638,8 +650,7 @@ in {
 
       environment.systemPackages = [ cfg.package ];
 
-      users.users = singleton {
-        name = "neo4j";
+      users.users.neo4j = {
         uid = config.ids.uids.neo4j;
         description = "Neo4j daemon user";
         home = cfg.directories.home;
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 5bf57a1bf9c..809f61cfa81 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -259,6 +259,8 @@ in
           ${openldap.out}/bin/slapadd ${configOpts} -l ${dataFile}
         ''}
         chown -R "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
+
+        ${openldap}/bin/slaptest ${configOpts}
       '';
       serviceConfig.ExecStart =
         "${openldap.out}/libexec/slapd -d '${cfg.logLevel}' " +
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 3bedfe96a18..0b79a996dc7 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -20,7 +20,9 @@ let
       listen_addresses = '${if cfg.enableTCPIP then "*" else "localhost"}'
       port = ${toString cfg.port}
       ${cfg.extraConfig}
-    '';
+    ''; 
+
+  groupAccessAvailable = versionAtLeast postgresql.version "11.0";
 
 in
 
@@ -88,6 +90,16 @@ in
         '';
       };
 
+      initdbArgs = mkOption {
+        type = with types; listOf str;
+        default = [];
+        example = [ "--data-checksums" "--allow-group-access" ];
+        description = ''
+          Additional arguments passed to <literal>initdb</literal> during data dir
+          initialisation.
+        '';
+      };
+
       initialScript = mkOption {
         type = types.nullOr types.path;
         default = null;
@@ -220,7 +232,7 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.postgresql.enable {
+  config = mkIf cfg.enable {
 
     services.postgresql.package =
       # Note: when changing the default, make it conditional on
@@ -232,13 +244,14 @@ in
             else throw "postgresql_9_4 was removed, please upgrade your postgresql version.");
 
     services.postgresql.dataDir =
-      mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/postgresql/${config.services.postgresql.package.psqlSchema}"
-                 else "/var/db/postgresql");
+      mkDefault (if versionAtLeast config.system.stateVersion "17.09"
+                  then "/var/lib/postgresql/${cfg.package.psqlSchema}"
+                  else "/var/db/postgresql");
 
     services.postgresql.authentication = mkAfter
       ''
         # Generated file; do not edit!
-        local all all              ident
+        local all all              peer
         host  all all 127.0.0.1/32 md5
         host  all all ::1/128      md5
       '';
@@ -284,7 +297,7 @@ in
           ''
             # Initialise the database.
             if ! test -e ${cfg.dataDir}/PG_VERSION; then
-              initdb -U ${cfg.superUser}
+              initdb -U ${cfg.superUser} ${concatStringsSep " " cfg.initdbArgs}
               # See postStart!
               touch "${cfg.dataDir}/.first_startup"
             fi
@@ -293,8 +306,12 @@ in
               ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
                 "${cfg.dataDir}/recovery.conf"
             ''}
+            ${optionalString (!groupAccessAvailable) ''
+              # postgresql pre 11.0 doesn't start if state directory mode is group accessible
+              chmod 0700 "${cfg.dataDir}"
+            ''}
 
-             exec postgres
+            exec postgres
           '';
 
         serviceConfig =
@@ -303,7 +320,7 @@ in
             Group = "postgres";
             PermissionsStartOnly = true;
             RuntimeDirectory = "postgresql";
-            Type = if lib.versionAtLeast cfg.package.version "9.6"
+            Type = if versionAtLeast cfg.package.version "9.6"
                    then "notify"
                    else "simple";
 
@@ -339,9 +356,9 @@ in
             '') cfg.ensureDatabases}
           '' + ''
             ${concatMapStrings (user: ''
-              $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc "CREATE USER ${user.name}"
+              $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
               ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
-                $PSQL -tAc 'GRANT ${permission} ON ${database} TO ${user.name}'
+                $PSQL -tAc 'GRANT ${permission} ON ${database} TO "${user.name}"'
               '') user.ensurePermissions)}
             '') cfg.ensureUsers}
           '';
@@ -352,5 +369,5 @@ in
   };
 
   meta.doc = ./postgresql.xml;
-  meta.maintainers = with lib.maintainers; [ thoughtpolice ];
+  meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];
 }
diff --git a/nixos/modules/services/databases/postgresql.xml b/nixos/modules/services/databases/postgresql.xml
index 72d4a8249a3..07af4c937f0 100644
--- a/nixos/modules/services/databases/postgresql.xml
+++ b/nixos/modules/services/databases/postgresql.xml
@@ -7,12 +7,10 @@
 <!-- FIXME: render nicely -->
 <!-- FIXME: source can be added automatically -->
  <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/databases/postgresql.nix</filename>
+  <emphasis>Source:</emphasis> <filename>modules/services/databases/postgresql.nix</filename>
  </para>
  <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="http://www.postgresql.org/docs/"/>
+  <emphasis>Upstream documentation:</emphasis> <link xlink:href="http://www.postgresql.org/docs/"/>
  </para>
 <!-- FIXME: more stuff, like maintainer? -->
  <para>
@@ -23,18 +21,12 @@
   <title>Configuring</title>
 
   <para>
-   To enable PostgreSQL, add the following to your
-   <filename>configuration.nix</filename>:
+   To enable PostgreSQL, add the following to your <filename>configuration.nix</filename>:
 <programlisting>
 <xref linkend="opt-services.postgresql.enable"/> = true;
 <xref linkend="opt-services.postgresql.package"/> = pkgs.postgresql_11;
 </programlisting>
-   Note that you are required to specify the desired version of PostgreSQL
-   (e.g. <literal>pkgs.postgresql_11</literal>). Since upgrading your
-   PostgreSQL version requires a database dump and reload (see below), NixOS
-   cannot provide a default value for
-   <xref linkend="opt-services.postgresql.package"/> such as the most recent
-   release of PostgreSQL.
+   Note that you are required to specify the desired version of PostgreSQL (e.g. <literal>pkgs.postgresql_11</literal>). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for <xref linkend="opt-services.postgresql.package"/> such as the most recent release of PostgreSQL.
   </para>
 
 <!--
@@ -51,9 +43,7 @@ Type "help" for help.
 -->
 
   <para>
-   By default, PostgreSQL stores its databases in
-   <filename>/var/lib/postgresql/$psqlSchema</filename>. You can override this using
-   <xref linkend="opt-services.postgresql.dataDir"/>, e.g.
+   By default, PostgreSQL stores its databases in <filename>/var/lib/postgresql/$psqlSchema</filename>. You can override this using <xref linkend="opt-services.postgresql.dataDir"/>, e.g.
 <programlisting>
 <xref linkend="opt-services.postgresql.dataDir"/> = "/data/postgresql";
 </programlisting>
@@ -63,25 +53,83 @@ Type "help" for help.
   <title>Upgrading</title>
 
   <para>
-   FIXME: document dump/upgrade/load cycle.
+   Major PostgreSQL upgrade requires PostgreSQL downtime and a few imperative steps to be called. To simplify this process, use the following NixOS module:
+<programlisting>
+  containers.temp-pg.config.services.postgresql = {
+    enable = true;
+    package = pkgs.postgresql_12;
+    ## set a custom new dataDir
+    # dataDir = "/some/data/dir";
+  };
+  environment.systemPackages =
+    let newpg = config.containers.temp-pg.config.services.postgresql;
+    in [
+      (pkgs.writeScriptBin "upgrade-pg-cluster" ''
+        set -x
+        export OLDDATA="${config.services.postgresql.dataDir}"
+        export NEWDATA="${newpg.dataDir}"
+        export OLDBIN="${config.services.postgresql.package}/bin"
+        export NEWBIN="${newpg.package}/bin"
+
+        install -d -m 0700 -o postgres -g postgres "$NEWDATA"
+        cd "$NEWDATA"
+        sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
+
+        systemctl stop postgresql    # old one
+
+        sudo -u postgres $NEWBIN/pg_upgrade \
+          --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
+          --old-bindir $OLDBIN --new-bindir $NEWBIN \
+          "$@"
+      '')
+    ];
+</programlisting>
   </para>
+
+  <para>
+   The upgrade process is:
+  </para>
+
+  <orderedlist>
+   <listitem>
+    <para>
+     Rebuild nixos configuration with the configuration above added to your <filename>configuration.nix</filename>. Alternatively, add that into separate file and reference it in <literal>imports</literal> list.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Login as root (<literal>sudo su -</literal>)
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Run <literal>upgrade-pg-cluster</literal>. It will stop old postgresql, initialize new one and migrate old one to new one. You may supply arguments like <literal>--jobs 4</literal> and <literal>--link</literal> to speedup migration process. See <link xlink:href="https://www.postgresql.org/docs/current/pgupgrade.html" /> for details.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Change postgresql package in NixOS configuration to the one you were upgrading to, and change <literal>dataDir</literal> to the one you have migrated to. Rebuild NixOS. This should start new postgres using upgraded data directory.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     After upgrade you may want to <literal>ANALYZE</literal> new db.
+    </para>
+   </listitem>
+  </orderedlist>
  </section>
  <section xml:id="module-services-postgres-options">
   <title>Options</title>
 
   <para>
-   A complete list of options for the PostgreSQL module may be found
-   <link linkend="opt-services.postgresql.enable">here</link>.
+   A complete list of options for the PostgreSQL module may be found <link linkend="opt-services.postgresql.enable">here</link>.
   </para>
  </section>
  <section xml:id="module-services-postgres-plugins">
   <title>Plugins</title>
 
   <para>
-   Plugins collection for each PostgreSQL version can be accessed with
-   <literal>.pkgs</literal>. For example, for
-   <literal>pkgs.postgresql_11</literal> package, its plugin collection is
-   accessed by <literal>pkgs.postgresql_11.pkgs</literal>:
+   Plugins collection for each PostgreSQL version can be accessed with <literal>.pkgs</literal>. For example, for <literal>pkgs.postgresql_11</literal> package, its plugin collection is accessed by <literal>pkgs.postgresql_11.pkgs</literal>:
 <screen>
 <prompt>$ </prompt>nix repl '&lt;nixpkgs&gt;'
 
@@ -98,8 +146,9 @@ postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
 ...
 </screen>
   </para>
+
   <para>
-    To add plugins via NixOS configuration, set <literal>services.postgresql.extraPlugins</literal>:
+   To add plugins via NixOS configuration, set <literal>services.postgresql.extraPlugins</literal>:
 <programlisting>
 <xref linkend="opt-services.postgresql.package"/> = pkgs.postgresql_11;
 <xref linkend="opt-services.postgresql.extraPlugins"/> = with pkgs.postgresql_11.pkgs; [
@@ -108,10 +157,9 @@ postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
 ];
 </programlisting>
   </para>
+
   <para>
-   You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using
-   function <literal>.withPackages</literal>. For example, creating a custom
-   PostgreSQL package in an overlay can look like:
+   You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function <literal>.withPackages</literal>. For example, creating a custom PostgreSQL package in an overlay can look like:
 <programlisting>
 self: super: {
   postgresql_custom = self.postgresql_11.withPackages (ps: [
@@ -121,8 +169,9 @@ self: super: {
 }
 </programlisting>
   </para>
+
   <para>
-    Here's a recipe on how to override a particular plugin through an overlay:
+   Here's a recipe on how to override a particular plugin through an overlay:
 <programlisting>
 self: super: {
   postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 95128a641d9..5c817422aae 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -32,6 +32,13 @@ let
   '';
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "redis" "user" ] "The redis module now is hardcoded to the redis user.")
+    (mkRemovedOptionModule [ "services" "redis" "dbpath" ] "The redis module now uses /var/lib/redis as data directory.")
+    (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.")
+    (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.")
+    (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.")
+  ];
 
   ###### interface
 
@@ -143,10 +150,20 @@ in
       requirePass = mkOption {
         type = with types; nullOr str;
         default = null;
-        description = "Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)";
+        description = ''
+          Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE).
+          Use requirePassFile to store it outside of the nix store in a dedicated file.
+        '';
         example = "letmein!";
       };
 
+      requirePassFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        description = "File with password for the database.";
+        example = "/run/keys/redis-password";
+      };
+
       appendOnly = mkOption {
         type = types.bool;
         default = false;
@@ -185,6 +202,10 @@ in
   ###### implementation
 
   config = mkIf config.services.redis.enable {
+    assertions = [{
+      assertion = cfg.requirePass != null -> cfg.requirePassFile == null;
+      message = "You can only set one services.redis.requirePass or services.redis.requirePassFile";
+    }];
     boot.kernel.sysctl = (mkMerge [
       { "vm.nr_hugepages" = "0"; }
       ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
@@ -201,21 +222,26 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
-    systemd.services.redis =
-      { description = "Redis Server";
+    systemd.services.redis = {
+      description = "Redis Server";
 
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
 
-        serviceConfig = {
-          ExecStart = "${cfg.package}/bin/redis-server ${redisConfig}";
-          RuntimeDirectory = "redis";
-          StateDirectory = "redis";
-          Type = "notify";
-          User = "redis";
-        };
-      };
+      preStart = ''
+        install -m 600 ${redisConfig} /run/redis/redis.conf
+      '' + optionalString (cfg.requirePassFile != null) ''
+        password=$(cat ${escapeShellArg cfg.requirePassFile})
+        echo "requirePass $password" >> /run/redis/redis.conf
+      '';
 
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
+        RuntimeDirectory = "redis";
+        StateDirectory = "redis";
+        Type = "notify";
+        User = "redis";
+      };
+    };
   };
-
 }
diff --git a/nixos/modules/services/databases/victoriametrics.nix b/nixos/modules/services/databases/victoriametrics.nix
new file mode 100644
index 00000000000..cb6bf8508fb
--- /dev/null
+++ b/nixos/modules/services/databases/victoriametrics.nix
@@ -0,0 +1,70 @@
+{ config, pkgs, lib, ... }:
+let cfg = config.services.victoriametrics; in
+{
+  options.services.victoriametrics = with lib; {
+    enable = mkEnableOption "victoriametrics";
+    package = mkOption {
+      type = types.package;
+      default = pkgs.victoriametrics;
+      defaultText = "pkgs.victoriametrics";
+      description = ''
+        The VictoriaMetrics distribution to use.
+      '';
+    };
+    listenAddress = mkOption {
+      default = ":8428";
+      type = types.str;
+      description = ''
+        The listen address for the http interface.
+      '';
+    };
+    retentionPeriod = mkOption {
+      type = types.int;
+      default = 1;
+      description = ''
+        Retention period in months.
+      '';
+    };
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        Extra options to pass to VictoriaMetrics. See the README: <link
+        xlink:href="https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md" />
+        or <command>victoriametrics -help</command> for more
+        information.
+      '';
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    systemd.services.victoriametrics = {
+      description = "VictoriaMetrics time series database";
+      after = [ "network.target" ];
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = 1;
+        StartLimitBurst = 5;
+        StateDirectory = "victoriametrics";
+        DynamicUser = true;
+        ExecStart = ''
+          ${cfg.package}/bin/victoria-metrics \
+              -storageDataPath=/var/lib/victoriametrics \
+              -httpListenAddr ${cfg.listenAddress}
+              -retentionPeriod ${toString cfg.retentionPeriod}
+              ${lib.escapeShellArgs cfg.extraOptions}
+        '';
+      };
+      wantedBy = [ "multi-user.target" ];
+
+      postStart =
+        let
+          bindAddr = (lib.optionalString (lib.hasPrefix ":" cfg.listenAddress) "127.0.0.1") + cfg.listenAddress;
+        in
+        lib.mkBefore ''
+          until ${lib.getBin pkgs.curl}/bin/curl -s -o /dev/null http://${bindAddr}/ping; do
+            sleep 1;
+          done
+        '';
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/virtuoso.nix b/nixos/modules/services/databases/virtuoso.nix
index 6ffc44a5274..6eb09e0a58f 100644
--- a/nixos/modules/services/databases/virtuoso.nix
+++ b/nixos/modules/services/databases/virtuoso.nix
@@ -13,10 +13,7 @@ with lib;
 
     services.virtuoso = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable Virtuoso Opensource database server.";
-      };
+      enable = mkEnableOption "Virtuoso Opensource database server";
 
       config = mkOption {
         default = "";
@@ -54,9 +51,8 @@ with lib;
 
   config = mkIf cfg.enable {
 
-    users.users = singleton
-      { name = virtuosoUser;
-        uid = config.ids.uids.virtuoso;
+    users.users.${virtuosoUser} =
+      { uid = config.ids.uids.virtuoso;
         description = "virtuoso user";
         home = stateDir;
       };
diff --git a/nixos/modules/services/desktops/accountsservice.nix b/nixos/modules/services/desktops/accountsservice.nix
index c48036a99e8..ae2ecb5ffeb 100644
--- a/nixos/modules/services/desktops/accountsservice.nix
+++ b/nixos/modules/services/desktops/accountsservice.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/bamf.nix b/nixos/modules/services/desktops/bamf.nix
index 0928ee81a64..4b35146d084 100644
--- a/nixos/modules/services/desktops/bamf.nix
+++ b/nixos/modules/services/desktops/bamf.nix
@@ -5,6 +5,10 @@
 with lib;
 
 {
+  meta = {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/at-spi2-core.nix b/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
index cca98c43dc7..492242e3296 100644
--- a/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
+++ b/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
@@ -18,6 +22,9 @@ with lib;
         description = ''
           Whether to enable at-spi2-core, a service for the Assistive Technologies
           available on the GNOME platform.
+
+          Enable this if you get the error or warning
+          <literal>The name org.a11y.Bus was not provided by any .service files</literal>.
         '';
       };
 
diff --git a/nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix b/nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix
index 3d2b3ed85e3..3c7f217b18d 100644
--- a/nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix
+++ b/nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix
@@ -4,6 +4,10 @@
 with lib;
 
 {
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
   options = {
     services.gnome3.chrome-gnome-shell.enable = mkEnableOption ''
diff --git a/nixos/modules/services/desktops/gnome3/evolution-data-server.nix b/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
index 7e312a1b81e..bd62d16f61c 100644
--- a/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
+++ b/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
@@ -16,7 +20,7 @@ with lib;
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable Evolution Data Server, a collection of services for 
+          Whether to enable Evolution Data Server, a collection of services for
           storing addressbooks and calendars.
         '';
       };
diff --git a/nixos/modules/services/desktops/gnome3/glib-networking.nix b/nixos/modules/services/desktops/gnome3/glib-networking.nix
index fcd58509d6f..7e667b6b1f0 100644
--- a/nixos/modules/services/desktops/gnome3/glib-networking.nix
+++ b/nixos/modules/services/desktops/gnome3/glib-networking.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix b/nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix
index d715d52c2d0..c391ad9694c 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix
@@ -44,6 +44,10 @@ in
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/gnome-keyring.nix b/nixos/modules/services/desktops/gnome3/gnome-keyring.nix
index db60445ef77..2916a3c82b3 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-keyring.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-keyring.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
@@ -35,6 +39,8 @@ with lib;
 
     services.dbus.packages = [ pkgs.gnome3.gnome-keyring pkgs.gcr ];
 
+    xdg.portal.extraPortals = [ pkgs.gnome3.gnome-keyring ];
+
     security.pam.services.login.enableGnomeKeyring = true;
 
     security.wrappers.gnome-keyring-daemon = {
diff --git a/nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix b/nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix
index 748a025414a..3f9ced5e86b 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/gnome-online-miners.nix b/nixos/modules/services/desktops/gnome3/gnome-online-miners.nix
index d406bf6f5e3..39d669e8b30 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-online-miners.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-online-miners.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix b/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix
index 021f4f9534b..164a0a44f8c 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix
@@ -4,6 +4,10 @@
 with lib;
 
 {
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
   options = {
     services.gnome3.gnome-remote-desktop = {
@@ -13,6 +17,8 @@ with lib;
 
   ###### implementation
   config = mkIf config.services.gnome3.gnome-remote-desktop.enable {
+    services.pipewire.enable = true;
+
     systemd.packages = [ pkgs.gnome3.gnome-remote-desktop ];
   };
 }
diff --git a/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix b/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix
index 2f83fd653bd..1c33ed064a1 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix
@@ -12,6 +12,10 @@ in
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   imports = [
     (mkRemovedOptionModule
       ["services" "gnome3" "gnome-settings-daemon" "package"]
diff --git a/nixos/modules/services/desktops/gnome3/gnome-user-share.nix b/nixos/modules/services/desktops/gnome3/gnome-user-share.nix
index f8396287770..f2fe8b41a9e 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-user-share.nix
+++ b/nixos/modules/services/desktops/gnome3/gnome-user-share.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/rygel.nix b/nixos/modules/services/desktops/gnome3/rygel.nix
index 55d5e703aa1..917a1d6541e 100644
--- a/nixos/modules/services/desktops/gnome3/rygel.nix
+++ b/nixos/modules/services/desktops/gnome3/rygel.nix
@@ -4,6 +4,10 @@
 with lib;
 
 {
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
   options = {
     services.gnome3.rygel = {
@@ -26,5 +30,7 @@ with lib;
     services.dbus.packages = [ pkgs.gnome3.rygel ];
 
     systemd.packages = [ pkgs.gnome3.rygel ];
+
+    environment.etc."rygel.conf".source = "${pkgs.gnome3.rygel}/etc/rygel.conf";
   };
 }
diff --git a/nixos/modules/services/desktops/gnome3/sushi.nix b/nixos/modules/services/desktops/gnome3/sushi.nix
index 7a4389038b2..83b17365d5d 100644
--- a/nixos/modules/services/desktops/gnome3/sushi.nix
+++ b/nixos/modules/services/desktops/gnome3/sushi.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gnome3/tracker-miners.nix b/nixos/modules/services/desktops/gnome3/tracker-miners.nix
index b390d8368c6..f2af4024927 100644
--- a/nixos/modules/services/desktops/gnome3/tracker-miners.nix
+++ b/nixos/modules/services/desktops/gnome3/tracker-miners.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
@@ -25,7 +29,6 @@ with lib;
 
   };
 
-
   ###### implementation
 
   config = mkIf config.services.gnome3.tracker-miners.enable {
diff --git a/nixos/modules/services/desktops/gnome3/tracker.nix b/nixos/modules/services/desktops/gnome3/tracker.nix
index 2e829274226..cd196e38553 100644
--- a/nixos/modules/services/desktops/gnome3/tracker.nix
+++ b/nixos/modules/services/desktops/gnome3/tracker.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/gvfs.nix b/nixos/modules/services/desktops/gvfs.nix
index 1d002eac41d..250ea6d4575 100644
--- a/nixos/modules/services/desktops/gvfs.nix
+++ b/nixos/modules/services/desktops/gvfs.nix
@@ -12,6 +12,10 @@ in
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   # Added 2019-08-19
   imports = [
     (mkRenamedOptionModule
diff --git a/nixos/modules/services/desktops/malcontent.nix b/nixos/modules/services/desktops/malcontent.nix
new file mode 100644
index 00000000000..5d6912595b5
--- /dev/null
+++ b/nixos/modules/services/desktops/malcontent.nix
@@ -0,0 +1,37 @@
+# Malcontent daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.malcontent = {
+
+      enable = mkEnableOption "Malcontent, parental control support for applications";
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.malcontent.enable {
+
+    environment.systemPackages = with pkgs; [
+      malcontent
+      malcontent-ui
+    ];
+
+    services.dbus.packages = [ pkgs.malcontent ];
+
+    services.accounts-daemon.enable = true;
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/neard.nix b/nixos/modules/services/desktops/neard.nix
new file mode 100644
index 00000000000..9b0f8d1b3a7
--- /dev/null
+++ b/nixos/modules/services/desktops/neard.nix
@@ -0,0 +1,23 @@
+# neard service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  ###### interface
+  options = {
+    services.neard = {
+      enable = mkEnableOption "neard, NFC daemon";
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf config.services.neard.enable {
+    environment.systemPackages = [ pkgs.neard ];
+
+    services.dbus.packages = [ pkgs.neard ];
+
+    systemd.packages = [ pkgs.neard ];
+  };
+}
diff --git a/nixos/modules/services/desktops/pantheon/contractor.nix b/nixos/modules/services/desktops/pantheon/contractor.nix
deleted file mode 100644
index 2638a21df73..00000000000
--- a/nixos/modules/services/desktops/pantheon/contractor.nix
+++ /dev/null
@@ -1,41 +0,0 @@
-# Contractor
-
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-{
-
-  meta.maintainers = pkgs.pantheon.maintainers;
-
-  ###### 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
index 577aad6c298..8cee9f42b62 100644
--- a/nixos/modules/services/desktops/pantheon/files.nix
+++ b/nixos/modules/services/desktops/pantheon/files.nix
@@ -6,33 +6,8 @@ with lib;
 
 {
 
-  meta.maintainers = pkgs.pantheon.maintainers;
-
-  ###### 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
-    ];
-
-  };
+  imports = [
+    (mkRemovedOptionModule [ "services" "pantheon" "files" "enable" ] "Use `environment.systemPackages [ pkgs.pantheon.elementary-files ];`")
+  ];
 
 }
diff --git a/nixos/modules/services/desktops/pipewire.nix b/nixos/modules/services/desktops/pipewire.nix
index 13f3d61e84c..5aee59cfdcc 100644
--- a/nixos/modules/services/desktops/pipewire.nix
+++ b/nixos/modules/services/desktops/pipewire.nix
@@ -8,6 +8,11 @@ let
   packages = with pkgs; [ pipewire ];
 
 in {
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
   ###### interface
   options = {
     services.pipewire = {
@@ -33,5 +38,4 @@ in {
     systemd.user.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
   };
 
-  meta.maintainers = with lib.maintainers; [ jtojnar ];
 }
diff --git a/nixos/modules/services/desktops/telepathy.nix b/nixos/modules/services/desktops/telepathy.nix
index f5401c18098..34596bf7818 100644
--- a/nixos/modules/services/desktops/telepathy.nix
+++ b/nixos/modules/services/desktops/telepathy.nix
@@ -6,6 +6,10 @@ with lib;
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/tumbler.nix b/nixos/modules/services/desktops/tumbler.nix
index a833e99ff8c..a09079517f0 100644
--- a/nixos/modules/services/desktops/tumbler.nix
+++ b/nixos/modules/services/desktops/tumbler.nix
@@ -18,6 +18,10 @@ in
       "")
   ];
 
+  meta = {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/desktops/zeitgeist.nix b/nixos/modules/services/desktops/zeitgeist.nix
index 20c82ccdd56..cf7dd5fe3a1 100644
--- a/nixos/modules/services/desktops/zeitgeist.nix
+++ b/nixos/modules/services/desktops/zeitgeist.nix
@@ -5,6 +5,11 @@
 with lib;
 
 {
+
+  meta = {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/development/jupyter/default.nix b/nixos/modules/services/development/jupyter/default.nix
index f20860af6e1..e598b018645 100644
--- a/nixos/modules/services/development/jupyter/default.nix
+++ b/nixos/modules/services/development/jupyter/default.nix
@@ -118,15 +118,15 @@ in {
           in {
             displayName = "Python 3 for machine learning";
             argv = [
-              "$ {env.interpreter}"
+              "''${env.interpreter}"
               "-m"
               "ipykernel_launcher"
               "-f"
               "{connection_file}"
             ];
             language = "python";
-            logo32 = "$ {env.sitePackages}/ipykernel/resources/logo-32x32.png";
-            logo64 = "$ {env.sitePackages}/ipykernel/resources/logo-64x64.png";
+            logo32 = "''${env.sitePackages}/ipykernel/resources/logo-32x32.png";
+            logo64 = "''${env.sitePackages}/ipykernel/resources/logo-64x64.png";
           };
         }
       '';
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index 68264ee869d..c843aa56d13 100644
--- a/nixos/modules/services/development/lorri.nix
+++ b/nixos/modules/services/development/lorri.nix
@@ -32,7 +32,7 @@ in {
       description = "Lorri Daemon";
       requires = [ "lorri.socket" ];
       after = [ "lorri.socket" ];
-      path = with pkgs; [ config.nix.package gnutar gzip ];
+      path = with pkgs; [ config.nix.package git gnutar gzip ];
       serviceConfig = {
         ExecStart = "${pkgs.lorri}/bin/lorri daemon";
         PrivateTmp = true;
diff --git a/nixos/modules/services/editors/infinoted.nix b/nixos/modules/services/editors/infinoted.nix
index be366761694..8b997ccbf66 100644
--- a/nixos/modules/services/editors/infinoted.nix
+++ b/nixos/modules/services/editors/infinoted.nix
@@ -111,14 +111,15 @@ in {
   };
 
   config = mkIf (cfg.enable) {
-    users.users = optional (cfg.user == "infinoted")
-      { name = "infinoted";
-        description = "Infinoted user";
-        group = cfg.group;
-        isSystemUser = true;
+    users.users = optionalAttrs (cfg.user == "infinoted")
+      { infinoted = {
+          description = "Infinoted user";
+          group = cfg.group;
+          isSystemUser = true;
+        };
       };
-    users.groups = optional (cfg.group == "infinoted")
-      { name = "infinoted";
+    users.groups = optionalAttrs (cfg.group == "infinoted")
+      { infinoted = { };
       };
 
     systemd.services.infinoted =
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
index f3831156f45..4b2e1a3c07f 100644
--- a/nixos/modules/services/games/factorio.nix
+++ b/nixos/modules/services/games/factorio.nix
@@ -4,14 +4,13 @@ with lib;
 
 let
   cfg = config.services.factorio;
-  factorio = pkgs.factorio-headless;
   name = "Factorio";
   stateDir = "/var/lib/${cfg.stateDirName}";
   mkSavePath = name: "${stateDir}/saves/${name}.zip";
   configFile = pkgs.writeText "factorio.conf" ''
     use-system-read-write-data-directories=true
     [path]
-    read-data=${factorio}/share/factorio/data
+    read-data=${cfg.package}/share/factorio/data
     write-data=${stateDir}
   '';
   serverSettings = {
@@ -37,7 +36,7 @@ let
     only_admins_can_pause_the_game = true;
     autosave_only_on_server = true;
     admins = [];
-  };
+  } // cfg.extraSettings;
   serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
   modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
 in
@@ -115,6 +114,14 @@ in
           Description of the game that will appear in the listing.
         '';
       };
+      extraSettings = mkOption {
+        type = types.attrs;
+        default = {};
+        example = { admins = [ "username" ];};
+        description = ''
+          Extra game configuration that will go into server-settings.json
+        '';
+      };
       public = mkOption {
         type = types.bool;
         default = false;
@@ -136,6 +143,15 @@ in
           Your factorio.com login credentials. Required for games with visibility public.
         '';
       };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.factorio-headless;
+        defaultText = "pkgs.factorio-headless";
+        example = "pkgs.factorio-headless-experimental";
+        description = ''
+          Factorio version to use. This defaults to the stable channel.
+        '';
+      };
       password = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -184,7 +200,7 @@ in
       preStart = toString [
         "test -e ${stateDir}/saves/${cfg.saveName}.zip"
         "||"
-        "${factorio}/bin/factorio"
+        "${cfg.package}/bin/factorio"
           "--config=${cfg.configFile}"
           "--create=${mkSavePath cfg.saveName}"
           (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
@@ -197,7 +213,7 @@ in
         StateDirectory = cfg.stateDirName;
         UMask = "0007";
         ExecStart = toString [
-          "${factorio}/bin/factorio"
+          "${cfg.package}/bin/factorio"
           "--config=${cfg.configFile}"
           "--port=${toString cfg.port}"
           "--start-server=${mkSavePath cfg.saveName}"
diff --git a/nixos/modules/services/hardware/actkbd.nix b/nixos/modules/services/hardware/actkbd.nix
index 4168140b287..daa407ca1f0 100644
--- a/nixos/modules/services/hardware/actkbd.nix
+++ b/nixos/modules/services/hardware/actkbd.nix
@@ -83,7 +83,7 @@ in
 
           See <command>actkbd</command> <filename>README</filename> for documentation.
 
-          The example shows a piece of what <option>sound.enableMediaKeys</option> does when enabled.
+          The example shows a piece of what <option>sound.mediaKeys.enable</option> does when enabled.
         '';
       };
 
diff --git a/nixos/modules/services/hardware/bluetooth.nix b/nixos/modules/services/hardware/bluetooth.nix
index c5f9d1f9b72..dfa39e7f602 100644
--- a/nixos/modules/services/hardware/bluetooth.nix
+++ b/nixos/modules/services/hardware/bluetooth.nix
@@ -36,17 +36,25 @@ in {
         '';
       };
 
+      config = mkOption {
+        type = with types; attrsOf (attrsOf (oneOf [ bool int str ]));
+        example = {
+          General = {
+            ControllerMode = "bredr";
+          };
+        };
+        description = "Set configuration for system-wide bluetooth (/etc/bluetooth/main.conf).";
+      };
+
       extraConfig = mkOption {
-        type = types.lines;
-        default = "";
+        type = with types; nullOr lines;
+        default = null;
         example = ''
           [General]
           ControllerMode = bredr
         '';
         description = ''
           Set additional configuration for system-wide bluetooth (/etc/bluetooth/main.conf).
-
-          NOTE: We already include [Policy], so any configuration under the Policy group should come first.
         '';
       };
     };
@@ -56,17 +64,19 @@ in {
   ###### implementation
 
   config = mkIf cfg.enable {
+    warnings = optional (cfg.extraConfig != null) "hardware.bluetooth.`extraConfig` is deprecated, please use hardware.bluetooth.`config`.";
 
-    environment.systemPackages = [ bluez-bluetooth pkgs.openobex pkgs.obexftp ];
+    hardware.bluetooth.config = {
+      Policy = {
+        AutoEnable = mkDefault cfg.powerOnBoot;
+      };
+    };
 
-    environment.etc = singleton {
-      source = pkgs.writeText "main.conf" ''
-        [Policy]
-        AutoEnable=${lib.boolToString cfg.powerOnBoot}
+    environment.systemPackages = [ bluez-bluetooth ];
 
-        ${cfg.extraConfig}
-      '';
-      target = "bluetooth/main.conf";
+    environment.etc."bluetooth/main.conf"= {
+      source = pkgs.writeText "main.conf"
+        (generators.toINI { } cfg.config + optionalString (cfg.extraConfig != null) cfg.extraConfig);
     };
 
     services.udev.packages = [ bluez-bluetooth ];
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 51877970a8b..e586af25c2b 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -53,7 +53,7 @@ in {
 
       blacklistPlugins = mkOption {
         type = types.listOf types.str;
-        default = [ "test" ];
+        default = [];
         example = [ "udev" ];
         description = ''
           Allow blacklisting specific plugins
@@ -91,6 +91,9 @@ in {
 
   ###### implementation
   config = mkIf cfg.enable {
+    # Disable test related plug-ins implicitly so that users do not have to care about them.
+    services.fwupd.blacklistPlugins = cfg.package.defaultBlacklistedPlugins;
+
     environment.systemPackages = [ cfg.package ];
 
     environment.etc = {
diff --git a/nixos/modules/services/hardware/irqbalance.nix b/nixos/modules/services/hardware/irqbalance.nix
index b139154432c..c79e0eb83ec 100644
--- a/nixos/modules/services/hardware/irqbalance.nix
+++ b/nixos/modules/services/hardware/irqbalance.nix
@@ -13,18 +13,12 @@ in
 
   config = mkIf cfg.enable {
 
-    systemd.services = {
-      irqbalance = {
-        description = "irqbalance daemon";
-        path = [ pkgs.irqbalance ];
-        serviceConfig =
-          { ExecStart = "${pkgs.irqbalance}/bin/irqbalance --foreground"; };
-        wantedBy = [ "multi-user.target" ];
-      };
-    };
-
     environment.systemPackages = [ pkgs.irqbalance ];
 
+    systemd.services.irqbalance.wantedBy = ["multi-user.target"];
+
+    systemd.packages = [ pkgs.irqbalance ];
+
   };
 
 }
diff --git a/nixos/modules/services/hardware/ratbagd.nix b/nixos/modules/services/hardware/ratbagd.nix
index 103e1d2315a..01a8276750f 100644
--- a/nixos/modules/services/hardware/ratbagd.nix
+++ b/nixos/modules/services/hardware/ratbagd.nix
@@ -10,12 +10,7 @@ in
 
   options = {
     services.ratbagd = {
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable ratbagd for configuring gaming mice.
-        '';
-      };
+      enable = mkEnableOption "ratbagd for configuring gaming mice";
     };
   };
 
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
index f6ed4e25e9c..6f49a1ab6d4 100644
--- a/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
@@ -67,11 +67,11 @@ in
 {
   options = {
 
-    hardware.sane.brscan4.enable = 
+    hardware.sane.brscan4.enable =
       mkEnableOption "Brother's brscan4 scan backend" // {
       description = ''
         When enabled, will automatically register the "brscan4" sane
-        backend and bring configuration files to their expected location. 
+        backend and bring configuration files to their expected location.
       '';
     };
 
@@ -95,14 +95,11 @@ in
       pkgs.brscan4
     ];
 
-    environment.etc = singleton {
-      target = "opt/brother/scanner/brscan4";
-      source = "${etcFiles}/etc/opt/brother/scanner/brscan4";
-    };
+    environment.etc."opt/brother/scanner/brscan4" =
+      { source = "${etcFiles}/etc/opt/brother/scanner/brscan4"; };
 
     assertions = [
       { assertion = all (x: !(null != x.ip && null != x.nodename)) netDeviceList;
-          
         message = ''
           When describing a network device as part of the attribute list
           `hardware.sane.brscan4.netDevices`, only one of its `ip` or `nodename`
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
index 6bf31982b71..ec0457bbd58 100644
--- a/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix
@@ -63,7 +63,7 @@ stdenv.mkDerivation {
 
   meta = {
     description = "Brother brscan4 sane backend driver etc files";
-    homepage = http://www.brother.com;
+    homepage = "http://www.brother.com";
     platforms = stdenv.lib.platforms.linux;
     license = stdenv.lib.licenses.unfree;
     maintainers = with stdenv.lib.maintainers; [ jraygauthier ];
diff --git a/nixos/modules/services/hardware/tcsd.nix b/nixos/modules/services/hardware/tcsd.nix
index 3876280ee6b..68cb5d791aa 100644
--- a/nixos/modules/services/hardware/tcsd.nix
+++ b/nixos/modules/services/hardware/tcsd.nix
@@ -137,15 +137,15 @@ in
       serviceConfig.ExecStart = "${pkgs.trousers}/sbin/tcsd -f -c ${tcsdConf}";
     };
 
-    users.users = optionalAttrs (cfg.user == "tss") (singleton
-      { name = "tss";
+    users.users = optionalAttrs (cfg.user == "tss") {
+      tss = {
         group = "tss";
         uid = config.ids.uids.tss;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "tss") (singleton
-      { name = "tss";
-        gid = config.ids.gids.tss;
-      });
+    users.groups = optionalAttrs (cfg.group == "tss") {
+      tss.gid = config.ids.gids.tss;
+    };
   };
 }
diff --git a/nixos/modules/services/hardware/thermald.nix b/nixos/modules/services/hardware/thermald.nix
index 69577bbe018..ecb529e9bf0 100644
--- a/nixos/modules/services/hardware/thermald.nix
+++ b/nixos/modules/services/hardware/thermald.nix
@@ -8,12 +8,7 @@ in {
   ###### interface
   options = {
     services.thermald = {
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable thermald, the temperature management daemon.
-        '';
-      };
+      enable = mkEnableOption "thermald, the temperature management daemon";
 
       debug = mkOption {
         type = types.bool;
diff --git a/nixos/modules/services/hardware/tlp.nix b/nixos/modules/services/hardware/tlp.nix
index adc1881a525..3962d7b1598 100644
--- a/nixos/modules/services/hardware/tlp.nix
+++ b/nixos/modules/services/hardware/tlp.nix
@@ -1,39 +1,26 @@
 { config, lib, pkgs, ... }:
-
 with lib;
-
 let
-
-cfg = config.services.tlp;
-
-enableRDW = config.networking.networkmanager.enable;
-
-tlp = pkgs.tlp.override {
-  inherit enableRDW;
-};
-
-# XXX: We can't use writeTextFile + readFile here because it triggers
-# TLP build to get the .drv (even on --dry-run).
-confFile = pkgs.runCommand "tlp"
-  { config = cfg.extraConfig;
-    passAsFile = [ "config" ];
-    preferLocalBuild = true;
-  }
-  ''
-    cat ${tlp}/etc/default/tlp > $out
-    cat $configPath >> $out
-  '';
-
+  cfg = config.services.tlp;
+  enableRDW = config.networking.networkmanager.enable;
+  tlp = pkgs.tlp.override { inherit enableRDW; };
+  # TODO: Use this for having proper parameters in the future
+  mkTlpConfig = tlpConfig: generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {
+      mkValueString = val:
+        if isInt val then toString val
+        else if isString val then val
+        else if true == val then "1"
+        else if false == val then "0"
+        else if isList val then "\"" + (concatStringsSep " " val) + "\""
+        else err "invalid value provided to mkTlpConfig:" (toString val);
+    } "=";
+  } tlpConfig;
 in
-
 {
-
   ###### interface
-
   options = {
-
     services.tlp = {
-
       enable = mkOption {
         type = types.bool;
         default = false;
@@ -45,76 +32,64 @@ in
         default = "";
         description = "Additional configuration variables for TLP";
       };
-
     };
-
   };
 
-
   ###### implementation
-
   config = mkIf cfg.enable {
+    boot.kernelModules = [ "msr" ];
 
-    powerManagement.scsiLinkPolicy = null;
-    powerManagement.cpuFreqGovernor = null;
-    powerManagement.cpufreq.max = null;
-    powerManagement.cpufreq.min = null;
+    environment.etc = {
+      "tlp.conf".text = cfg.extraConfig;
+    } // optionalAttrs enableRDW {
+      "NetworkManager/dispatcher.d/99tlp-rdw-nm".source =
+        "${tlp}/etc/NetworkManager/dispatcher.d/99tlp-rdw-nm";
+    };
 
-    systemd.sockets.systemd-rfkill.enable = false;
+    environment.systemPackages = [ tlp ];
 
-    systemd.services = {
-      "systemd-rfkill@".enable = false;
-      systemd-rfkill.enable = false;
+    # FIXME: When the config is parametrized we need to move these into a
+    # conditional on the relevant options being enabled.
+    powerManagement = {
+      scsiLinkPolicy = null;
+      cpuFreqGovernor = null;
+      cpufreq.max = null;
+      cpufreq.min = null;
+    };
 
-      tlp = {
-        description = "TLP system startup/shutdown";
+    services.udev.packages = [ tlp ];
 
-        after = [ "multi-user.target" ];
+    systemd = {
+      packages = [ tlp ];
+      # XXX: These must always be disabled/masked according to [1].
+      #
+      # [1]: https://github.com/linrunner/TLP/blob/a9ada09e0821f275ce5f93dc80a4d81a7ff62ae4/tlp-stat.in#L319
+      sockets.systemd-rfkill.enable = false;
+      services.systemd-rfkill.enable = false;
+
+      services.tlp = {
+        # XXX: The service should reload whenever the configuration changes,
+        # otherwise newly set power options remain inactive until reboot (or
+        # manual unit restart.)
+        restartTriggers = [ config.environment.etc."tlp.conf".source ];
+        # XXX: When using systemd.packages (which we do above) the [Install]
+        # section of systemd units does not work (citation needed) so we manually
+        # enforce it here.
         wantedBy = [ "multi-user.target" ];
-        before = [ "shutdown.target" ];
-        restartTriggers = [ confFile ];
-
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-          ExecStart = "${tlp}/bin/tlp init start";
-          ExecStop = "${tlp}/bin/tlp init stop";
-        };
       };
 
-      tlp-sleep = {
-        description = "TLP suspend/resume";
-
-        wantedBy = [ "sleep.target" ];
+      services.tlp-sleep = {
+        # XXX: When using systemd.packages (which we do above) the [Install]
+        # section of systemd units does not work (citation needed) so we manually
+        # enforce it here.
         before = [ "sleep.target" ];
-
-        unitConfig = {
-          StopWhenUnneeded = true;
-        };
-
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-          ExecStart = "${tlp}/bin/tlp suspend";
-          ExecStop = "${tlp}/bin/tlp resume";
-        };
+        wantedBy = [ "sleep.target" ];
+        # XXX: `tlp suspend` requires /var/lib/tlp to exist in order to save
+        # some stuff in there. There is no way, that I know of, to do this in
+        # the package itself, so we do it here instead making sure the unit
+        # won't fail due to the save dir not existing.
+        serviceConfig.StateDirectory = "tlp";
       };
     };
-
-    services.udev.packages = [ tlp ];
-
-    environment.etc = [{ source = confFile;
-                         target = "default/tlp";
-                       }
-                      ] ++ optional enableRDW {
-                        source = "${tlp}/etc/NetworkManager/dispatcher.d/99tlp-rdw-nm";
-                        target = "NetworkManager/dispatcher.d/99tlp-rdw-nm";
-                      };
-
-    environment.systemPackages = [ tlp ];
-
-    boot.kernelModules = [ "msr" ];
-
   };
-
 }
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 83ab93bd7cf..168056a475e 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -221,8 +221,8 @@ in
         type = types.lines;
         description = ''
           Additional <command>hwdb</command> files. They'll be written
-          into file <filename>10-local.hwdb</filename>. Thus they are
-          read before all other files.
+          into file <filename>99-local.hwdb</filename>. Thus they are
+          read after all other files.
         '';
       };
 
@@ -281,13 +281,10 @@ in
     boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
 
     environment.etc =
-      [ { source = udevRules;
-          target = "udev/rules.d";
-        }
-        { source = hwdbBin;
-          target = "udev/hwdb.bin";
-        }
-      ];
+      {
+        "udev/rules.d".source = udevRules;
+        "udev/hwdb.bin".source = hwdbBin;
+      };
 
     system.requiredKernelConfig = with config.lib.kernelConfig; [
       (isEnabled "UNIX")
diff --git a/nixos/modules/services/hardware/upower.nix b/nixos/modules/services/hardware/upower.nix
index 5e7ac7a6e65..449810b5315 100644
--- a/nixos/modules/services/hardware/upower.nix
+++ b/nixos/modules/services/hardware/upower.nix
@@ -37,6 +37,172 @@ in
         '';
       };
 
+      enableWattsUpPro = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the Watts Up Pro device.
+
+          The Watts Up Pro contains a generic FTDI USB device without a specific
+          vendor and product ID. When we probe for WUP devices, we can cause
+          the user to get a perplexing "Device or resource busy" error when
+          attempting to use their non-WUP device.
+
+          The generic FTDI device is known to also be used on:
+
+          <itemizedlist>
+            <listitem><para>Sparkfun FT232 breakout board</para></listitem>
+            <listitem><para>Parallax Propeller</para></listitem>
+          </itemizedlist>
+        '';
+      };
+
+      noPollBatteries = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Don't poll the kernel for battery level changes.
+
+          Some hardware will send us battery level changes through
+          events, rather than us having to poll for it. This option
+          allows disabling polling for hardware that sends out events.
+        '';
+      };
+
+      ignoreLid = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Do we ignore the lid state
+
+          Some laptops are broken. The lid state is either inverted, or stuck
+          on or off. We can't do much to fix these problems, but this is a way
+          for users to make the laptop panel vanish, a state that might be used
+          by a couple of user-space daemons. On Linux systems, see also
+          logind.conf(5).
+        '';
+      };
+
+      usePercentageForPolicy = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Policy for warnings and action based on battery levels
+
+          Whether battery percentage based policy should be used. The default
+          is to use the percentage, which
+          should work around broken firmwares. It is also more reliable than
+          the time left (frantically saving all your files is going to use more
+          battery than letting it rest for example).
+        '';
+      };
+
+      percentageLow = mkOption {
+        type = types.ints.unsigned;
+        default = 10;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>true</literal>, the levels at which UPower will consider the
+          battery low.
+
+          This will also be used for batteries which don't have time information
+          such as that of peripherals.
+
+          If any value (of <literal>percentageLow</literal>,
+          <literal>percentageCritical</literal> and
+          <literal>percentageAction</literal>) is invalid, or not in descending
+          order, the defaults will be used.
+        '';
+      };
+
+      percentageCritical = mkOption {
+        type = types.ints.unsigned;
+        default = 3;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>true</literal>, the levels at which UPower will consider the
+          battery critical.
+
+          This will also be used for batteries which don't have time information
+          such as that of peripherals.
+
+          If any value (of <literal>percentageLow</literal>,
+          <literal>percentageCritical</literal> and
+          <literal>percentageAction</literal>) is invalid, or not in descending
+          order, the defaults will be used.
+        '';
+      };
+
+      percentageAction = mkOption {
+        type = types.ints.unsigned;
+        default = 2;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>true</literal>, the levels at which UPower will take action
+          for the critical battery level.
+
+          This will also be used for batteries which don't have time information
+          such as that of peripherals.
+
+          If any value (of <literal>percentageLow</literal>,
+          <literal>percentageCritical</literal> and
+          <literal>percentageAction</literal>) is invalid, or not in descending
+          order, the defaults will be used.
+        '';
+      };
+
+      timeLow = mkOption {
+        type = types.ints.unsigned;
+        default = 1200;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>false</literal>, the time remaining at which UPower will
+          consider the battery low.
+
+          If any value (of <literal>timeLow</literal>,
+          <literal>timeCritical</literal> and <literal>timeAction</literal>) is
+          invalid, or not in descending order, the defaults will be used.
+        '';
+      };
+
+      timeCritical = mkOption {
+        type = types.ints.unsigned;
+        default = 300;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>false</literal>, the time remaining at which UPower will
+          consider the battery critical.
+
+          If any value (of <literal>timeLow</literal>,
+          <literal>timeCritical</literal> and <literal>timeAction</literal>) is
+          invalid, or not in descending order, the defaults will be used.
+        '';
+      };
+
+      timeAction = mkOption {
+        type = types.ints.unsigned;
+        default = 120;
+        description = ''
+          When <literal>usePercentageForPolicy</literal> is
+          <literal>false</literal>, the time remaining at which UPower will
+          take action for the critical battery level.
+
+          If any value (of <literal>timeLow</literal>,
+          <literal>timeCritical</literal> and <literal>timeAction</literal>) is
+          invalid, or not in descending order, the defaults will be used.
+        '';
+      };
+
+      criticalPowerAction = mkOption {
+        type = types.enum [ "PowerOff" "Hibernate" "HybridSleep" ];
+        default = "HybridSleep";
+        description = ''
+          The action to take when <literal>timeAction</literal> or
+          <literal>percentageAction</literal> has been reached for the batteries
+          (UPS or laptop batteries) supplying the computer
+        '';
+      };
+
     };
 
   };
@@ -54,6 +220,21 @@ in
 
     systemd.packages = [ cfg.package ];
 
+    environment.etc."UPower/UPower.conf".text = generators.toINI {} {
+      UPower = {
+        EnableWattsUpPro = cfg.enableWattsUpPro;
+        NoPollBatteries = cfg.noPollBatteries;
+        IgnoreLid = cfg.ignoreLid;
+        UsePercentageForPolicy = cfg.usePercentageForPolicy;
+        PercentageLow = cfg.percentageLow;
+        PercentageCritical = cfg.percentageCritical;
+        PercentageAction = cfg.percentageAction;
+        TimeLow = cfg.timeLow;
+        TimeCritical = cfg.timeCritical;
+        TimeAction = cfg.timeAction;
+        CriticalPowerAction = cfg.criticalPowerAction;
+      };
+    };
   };
 
 }
diff --git a/nixos/modules/services/hardware/usbmuxd.nix b/nixos/modules/services/hardware/usbmuxd.nix
index 39bbcaf4627..11a4b0a858f 100644
--- a/nixos/modules/services/hardware/usbmuxd.nix
+++ b/nixos/modules/services/hardware/usbmuxd.nix
@@ -43,15 +43,16 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = optional (cfg.user == defaultUserGroup) {
-      name = cfg.user;
-      description = "usbmuxd user";
-      group = cfg.group;
-      isSystemUser = true;
+    users.users = optionalAttrs (cfg.user == defaultUserGroup) {
+      ${cfg.user} = {
+        description = "usbmuxd user";
+        group = cfg.group;
+        isSystemUser = true;
+      };
     };
 
-    users.groups = optional (cfg.group == defaultUserGroup) {
-      name = cfg.group;
+    users.groups = optionalAttrs (cfg.group == defaultUserGroup) {
+      ${cfg.group} = { };
     };
 
     # Give usbmuxd permission for Apple devices
diff --git a/nixos/modules/services/hardware/xow.nix b/nixos/modules/services/hardware/xow.nix
new file mode 100644
index 00000000000..a18d60ad83b
--- /dev/null
+++ b/nixos/modules/services/hardware/xow.nix
@@ -0,0 +1,17 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.hardware.xow;
+in {
+  options.services.hardware.xow = {
+    enable = lib.mkEnableOption "xow as a systemd service";
+  };
+
+  config = lib.mkIf cfg.enable {
+    hardware.uinput.enable = true;
+
+    systemd.packages = [ pkgs.xow ];
+
+    services.udev.packages = [ pkgs.xow ];
+  };
+}
diff --git a/nixos/modules/services/logging/awstats.nix b/nixos/modules/services/logging/awstats.nix
index a92ff3bee49..5939d7808f7 100644
--- a/nixos/modules/services/logging/awstats.nix
+++ b/nixos/modules/services/logging/awstats.nix
@@ -4,31 +4,117 @@ with lib;
 
 let
   cfg = config.services.awstats;
-  httpd = config.services.httpd;
   package = pkgs.awstats;
-in
+  configOpts = {name, config, ...}: {
+    options = {
+      type = mkOption{
+        type = types.enum [ "mail" "web" ];
+        default = "web";
+        example = "mail";
+        description = ''
+          The type of log being collected.
+        '';
+      };
+      domain = mkOption {
+        type = types.str;
+        default = name;
+        description = "The domain name to collect stats for.";
+        example = "example.com";
+      };
+
+      logFile = mkOption {
+        type = types.str;
+        example = "/var/spool/nginx/logs/access.log";
+        description = ''
+          The log file to be scanned.
 
+          For mail, set this to
+          <literal>
+          journalctl $OLD_CURSOR -u postfix.service | ''${pkgs.perl}/bin/perl ''${pkgs.awstats.out}/share/awstats/tools/maillogconvert.pl standard |
+          </literal>
+        '';
+      };
+
+      logFormat = mkOption {
+        type = types.str;
+        default = "1";
+        description = ''
+          The log format being used.
+
+          For mail, set this to
+          <literal>
+          %time2 %email %email_r %host %host_r %method %url %code %bytesd
+          </literal>
+        '';
+      };
+
+      hostAliases = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = "[ \"www.example.org\" ]";
+        description = ''
+          List of aliases the site has.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = literalExample ''
+          {
+            "ValidHTTPCodes" = "404";
+          }
+        '';
+        description = "Extra configuration to be appendend to awstats.\${name}.conf.";
+      };
+
+      webService = {
+        enable = mkEnableOption "awstats web service";
+
+        hostname = mkOption {
+          type = types.str;
+          default = config.domain;
+          description = "The hostname the web service appears under.";
+        };
+
+        urlPrefix = mkOption {
+          type = types.str;
+          default = "/awstats";
+          description = "The URL prefix under which the awstats pages appear.";
+        };
+      };
+    };
+  };
+  webServices = filterAttrs (name: value: value.webService.enable) cfg.configs;
+in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "awstats" "service" "enable" ] "Please enable per domain with `services.awstats.configs.<name>.webService.enable`")
+    (mkRemovedOptionModule [ "services" "awstats" "service" "urlPrefix" ] "Please set per domain with `services.awstats.configs.<name>.webService.urlPrefix`")
+    (mkRenamedOptionModule [ "services" "awstats" "vardir" ] [ "services" "awstats" "dataDir" ])
+  ];
+
   options.services.awstats = {
-    enable = mkOption {
-      type = types.bool;
-      default = cfg.service.enable;
-      description = ''
-        Enable the awstats program (but not service).
-        Currently only simple httpd (Apache) configs are supported,
-        and awstats plugins may not work correctly.
-      '';
-    };
-    vardir = mkOption {
+    enable = mkEnableOption "awstats";
+
+    dataDir = mkOption {
       type = types.path;
       default = "/var/lib/awstats";
-      description = "The directory where variable awstats data will be stored.";
+      description = "The directory where awstats data will be stored.";
     };
 
-    extraConfig = mkOption {
-      type = types.lines;
-      default = "";
-      description = "Extra configuration to be appendend to awstats.conf.";
+    configs = mkOption {
+      type = types.attrsOf (types.submodule configOpts);
+      default = {};
+      example = literalExample ''
+        {
+          "mysite" = {
+            domain = "example.com";
+            logFile = "/var/spool/nginx/logs/access.log";
+          };
+        }
+      '';
+      description = "Attribute set of domains to collect stats for.";
     };
 
     updateAt = mkOption {
@@ -42,75 +128,129 @@ in
           <manvolnum>7</manvolnum></citerefentry>)
       '';
     };
-
-    service = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''Enable the awstats web service. This switches on httpd.'';
-      };
-      urlPrefix = mkOption {
-        type = types.str;
-        default = "/awstats";
-        description = "The URL prefix under which the awstats service appears.";
-      };
-    };
   };
 
 
   config = mkIf cfg.enable {
     environment.systemPackages = [ package.bin ];
-    /* TODO:
-      - heed config.services.httpd.logPerVirtualHost, etc.
-      - Can't AllowToUpdateStatsFromBrowser, as CGI scripts don't have permission
-        to read the logs, and our httpd config apparently doesn't an option for that.
-    */
-    environment.etc."awstats/awstats.conf".source = pkgs.runCommand "awstats.conf"
+
+    environment.etc = mapAttrs' (name: opts:
+    nameValuePair "awstats/awstats.${name}.conf" {
+      source = pkgs.runCommand "awstats.${name}.conf"
       { preferLocalBuild = true; }
-      ( let
-          logFormat =
-            if httpd.logFormat == "combined" then "1" else
-            if httpd.logFormat == "common" then "4" else
-            throw "awstats service doesn't support Apache log format `${httpd.logFormat}`";
-        in
-        ''
-          sed \
-            -e 's|^\(DirData\)=.*$|\1="${cfg.vardir}"|' \
-            -e 's|^\(DirIcons\)=.*$|\1="icons"|' \
-            -e 's|^\(CreateDirDataIfNotExists\)=.*$|\1=1|' \
-            -e 's|^\(SiteDomain\)=.*$|\1="${httpd.hostName}"|' \
-            -e 's|^\(LogFile\)=.*$|\1="${httpd.logDir}/access_log"|' \
-            -e 's|^\(LogFormat\)=.*$|\1=${logFormat}|' \
-            < '${package.out}/wwwroot/cgi-bin/awstats.model.conf' > "$out"
-          echo '${cfg.extraConfig}' >> "$out"
-        '');
-
-    systemd.tmpfiles.rules = optionals cfg.service.enable [
-      "d '${cfg.vardir}' - ${httpd.user} ${httpd.group} - -"
-      "Z '${cfg.vardir}' - ${httpd.user} ${httpd.group} - -"
-    ];
-
-    # The httpd sub-service showing awstats.
-    services.httpd = optionalAttrs cfg.service.enable {
-      enable = true;
-      extraConfig = ''
-        Alias ${cfg.service.urlPrefix}/classes "${package.out}/wwwroot/classes/"
-        Alias ${cfg.service.urlPrefix}/css "${package.out}/wwwroot/css/"
-        Alias ${cfg.service.urlPrefix}/icons "${package.out}/wwwroot/icon/"
-        ScriptAlias ${cfg.service.urlPrefix}/ "${package.out}/wwwroot/cgi-bin/"
-
-        <Directory "${package.out}/wwwroot">
-          Options None
-          Require all granted
-        </Directory>
-      '';
-    };
+      (''
+        sed \
+      ''
+      # set up mail stats
+      + optionalString (opts.type == "mail")
+      ''
+        -e 's|^\(LogType\)=.*$|\1=M|' \
+        -e 's|^\(LevelForBrowsersDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForOSDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForRefererAnalyze\)=.*$|\1=0|' \
+        -e 's|^\(LevelForRobotsDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForSearchEnginesDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForFileTypesDetection\)=.*$|\1=0|' \
+        -e 's|^\(LevelForWormsDetection\)=.*$|\1=0|' \
+        -e 's|^\(ShowMenu\)=.*$|\1=1|' \
+        -e 's|^\(ShowSummary\)=.*$|\1=HB|' \
+        -e 's|^\(ShowMonthStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowDaysOfMonthStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowDaysOfWeekStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowHoursStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowDomainsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowHostsStats\)=.*$|\1=HB|' \
+        -e 's|^\(ShowAuthenticatedUsers\)=.*$|\1=0|' \
+        -e 's|^\(ShowRobotsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowEMailSenders\)=.*$|\1=HBML|' \
+        -e 's|^\(ShowEMailReceivers\)=.*$|\1=HBML|' \
+        -e 's|^\(ShowSessionsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowPagesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowFileTypesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowFileSizesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowBrowsersStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowOSStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowOriginStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowKeyphrasesStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowKeywordsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowMiscStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowHTTPErrorsStats\)=.*$|\1=0|' \
+        -e 's|^\(ShowSMTPErrorsStats\)=.*$|\1=1|' \
+      ''
+      +
+      # common options
+      ''
+        -e 's|^\(DirData\)=.*$|\1="${cfg.dataDir}/${name}"|' \
+        -e 's|^\(DirIcons\)=.*$|\1="icons"|' \
+        -e 's|^\(CreateDirDataIfNotExists\)=.*$|\1=1|' \
+        -e 's|^\(SiteDomain\)=.*$|\1="${name}"|' \
+        -e 's|^\(LogFile\)=.*$|\1="${opts.logFile}"|' \
+        -e 's|^\(LogFormat\)=.*$|\1="${opts.logFormat}"|' \
+      ''
+      +
+      # extra config
+      concatStringsSep "\n" (mapAttrsToList (n: v: ''
+        -e 's|^\(${n}\)=.*$|\1="${v}"|' \
+      '') opts.extraConfig)
+      +
+      ''
+        < '${package.out}/wwwroot/cgi-bin/awstats.model.conf' > "$out"
+      '');
+    }) cfg.configs;
 
-    systemd.services.awstats-update = mkIf (cfg.updateAt != null) {
-      description = "awstats log collector";
-      script = "exec '${package.bin}/bin/awstats' -update -config=awstats.conf";
-      startAt = cfg.updateAt;
-    };
+    # create data directory with the correct permissions
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 755 root root - -" ] ++
+      mapAttrsToList (name: opts: "d '${cfg.dataDir}/${name}' 755 root root - -") cfg.configs ++
+      [ "Z '${cfg.dataDir}' 755 root root - -" ];
+
+    # nginx options
+    services.nginx.virtualHosts = mapAttrs'(name: opts: {
+      name = opts.webService.hostname;
+      value = {
+        locations = {
+          "${opts.webService.urlPrefix}/css/" = {
+            alias = "${package.out}/wwwroot/css/";
+          };
+          "${opts.webService.urlPrefix}/icons/" = {
+            alias = "${package.out}/wwwroot/icon/";
+          };
+          "${opts.webService.urlPrefix}/" = {
+            alias = "${cfg.dataDir}/${name}/";
+            extraConfig = ''
+              autoindex on;
+            '';
+          };
+        };
+      };
+    }) webServices;
+
+    # update awstats
+    systemd.services = mkIf (cfg.updateAt != null) (mapAttrs' (name: opts:
+      nameValuePair "awstats-${name}-update" {
+        description = "update awstats for ${name}";
+        script = optionalString (opts.type == "mail")
+        ''
+          if [[ -f "${cfg.dataDir}/${name}-cursor" ]]; then
+            CURSOR="$(cat "${cfg.dataDir}/${name}-cursor" | tr -d '\n')"
+            if [[ -n "$CURSOR" ]]; then
+              echo "Using cursor: $CURSOR"
+              export OLD_CURSOR="--cursor $CURSOR"
+            fi
+          fi
+          NEW_CURSOR="$(journalctl $OLD_CURSOR -u postfix.service --show-cursor | tail -n 1 | tr -d '\n' | sed -e 's#^-- cursor: \(.*\)#\1#')"
+          echo "New cursor: $NEW_CURSOR"
+          ${package.bin}/bin/awstats -update -config=${name}
+          if [ -n "$NEW_CURSOR" ]; then
+            echo -n "$NEW_CURSOR" > ${cfg.dataDir}/${name}-cursor
+          fi
+        '' + ''
+          ${package.out}/share/awstats/tools/awstats_buildstaticpages.pl \
+            -config=${name} -update -dir=${cfg.dataDir}/${name} \
+            -awstatsprog=${package.bin}/bin/awstats
+        '';
+        startAt = cfg.updateAt;
+    }) cfg.configs);
   };
 
 }
diff --git a/nixos/modules/services/logging/logcheck.nix b/nixos/modules/services/logging/logcheck.nix
index 6d8be5b926d..4296b2270c2 100644
--- a/nixos/modules/services/logging/logcheck.nix
+++ b/nixos/modules/services/logging/logcheck.nix
@@ -213,13 +213,14 @@ in
         mapAttrsToList writeIgnoreRule cfg.ignore
         ++ mapAttrsToList writeIgnoreCronRule cfg.ignoreCron;
 
-    users.users = optionalAttrs (cfg.user == "logcheck") (singleton
-      { name = "logcheck";
+    users.users = optionalAttrs (cfg.user == "logcheck") {
+      logcheck = {
         uid = config.ids.uids.logcheck;
         shell = "/bin/sh";
         description = "Logcheck user account";
         extraGroups = cfg.extraGroups;
-      });
+      };
+    };
 
     system.activationScripts.logcheck = ''
       mkdir -m 700 -p /var/{lib,lock}/logcheck
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index 4943e8d7db3..21a83803fd8 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -37,6 +37,11 @@ let
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "logstash" "address" ] [ "services" "logstash" "listenAddress" ])
+    (mkRemovedOptionModule [ "services" "logstash" "enableWeb" ] "The web interface was removed from logstash")
+  ];
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/logging/syslog-ng.nix b/nixos/modules/services/logging/syslog-ng.nix
index 65e103ac2ba..35055311680 100644
--- a/nixos/modules/services/logging/syslog-ng.nix
+++ b/nixos/modules/services/logging/syslog-ng.nix
@@ -25,6 +25,10 @@ let
   ];
 
 in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "syslog-ng" "serviceName" ] "")
+    (mkRemovedOptionModule [ "services" "syslog-ng" "listenToJournal" ] "")
+  ];
 
   options = {
 
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index 3fd06812c67..230a2ae3f82 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -14,18 +14,34 @@ let
       base_dir = ${baseDir}
       protocols = ${concatStringsSep " " cfg.protocols}
       sendmail_path = /run/wrappers/bin/sendmail
+      # defining mail_plugins must be done before the first protocol {} filter because of https://doc.dovecot.org/configuration_manual/config_file/config_file_syntax/#variable-expansion
+      mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
     ''
 
-    (if cfg.sslServerCert == null then ''
-      ssl = no
-      disable_plaintext_auth = no
-    '' else ''
-      ssl_cert = <${cfg.sslServerCert}
-      ssl_key = <${cfg.sslServerKey}
-      ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
-      ssl_dh = <${config.security.dhparams.params.dovecot2.path}
-      disable_plaintext_auth = yes
-    '')
+    (
+      concatStringsSep "\n" (
+        mapAttrsToList (
+          protocol: plugins: ''
+            protocol ${protocol} {
+              mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
+            }
+          ''
+        ) cfg.mailPlugins.perProtocol
+      )
+    )
+
+    (
+      if cfg.sslServerCert == null then ''
+        ssl = no
+        disable_plaintext_auth = no
+      '' else ''
+        ssl_cert = <${cfg.sslServerCert}
+        ssl_key = <${cfg.sslServerKey}
+        ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
+        ssl_dh = <${config.security.dhparams.params.dovecot2.path}
+        disable_plaintext_auth = yes
+      ''
+    )
 
     ''
       default_internal_user = ${cfg.user}
@@ -45,55 +61,58 @@ let
       }
     ''
 
-    (optionalString cfg.enablePAM ''
-      userdb {
-        driver = passwd
-      }
-
-      passdb {
-        driver = pam
-        args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
-      }
-    '')
+    (
+      optionalString cfg.enablePAM ''
+        userdb {
+          driver = passwd
+        }
 
-    (optionalString (cfg.sieveScripts != {}) ''
-      plugin {
-        ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
-      }
-    '')
+        passdb {
+          driver = pam
+          args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
+        }
+      ''
+    )
 
-    (optionalString (cfg.mailboxes != []) ''
-      protocol imap {
-        namespace inbox {
-          inbox=yes
-          ${concatStringsSep "\n" (map mailboxConfig cfg.mailboxes)}
+    (
+      optionalString (cfg.sieveScripts != {}) ''
+        plugin {
+          ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
         }
-      }
-    '')
-
-    (optionalString cfg.enableQuota ''
-      mail_plugins = $mail_plugins quota
-      service quota-status {
-        executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
-        inet_listener {
-          port = ${cfg.quotaPort}
+      ''
+    )
+
+    (
+      optionalString (cfg.mailboxes != []) ''
+        protocol imap {
+          namespace inbox {
+            inbox=yes
+            ${concatStringsSep "\n" (map mailboxConfig cfg.mailboxes)}
+          }
+        }
+      ''
+    )
+
+    (
+      optionalString cfg.enableQuota ''
+        service quota-status {
+          executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
+          inet_listener {
+            port = ${cfg.quotaPort}
+          }
+          client_limit = 1
         }
-        client_limit = 1
-      }
-
-      protocol imap {
-        mail_plugins = $mail_plugins imap_quota
-      }
 
-      plugin {
-        quota_rule = *:storage=${cfg.quotaGlobalPerUser} 
-        quota = maildir:User quota # per virtual mail user quota # BUG/FIXME broken, we couldn't get this working
-        quota_status_success = DUNNO
-        quota_status_nouser = DUNNO
-        quota_status_overquota = "552 5.2.2 Mailbox is full"
-        quota_grace = 10%%
-      }
-    '')
+        plugin {
+          quota_rule = *:storage=${cfg.quotaGlobalPerUser}
+          quota = maildir:User quota # per virtual mail user quota # BUG/FIXME broken, we couldn't get this working
+          quota_status_success = DUNNO
+          quota_status_nouser = DUNNO
+          quota_status_overquota = "552 5.2.2 Mailbox is full"
+          quota_grace = 10%%
+        }
+      ''
+    )
 
     cfg.extraConfig
   ];
@@ -107,7 +126,7 @@ let
     mailbox "${mailbox.name}" {
       auto = ${toString mailbox.auto}
   '' + optionalString (mailbox.specialUse != null) ''
-      special_use = \${toString mailbox.specialUse}
+    special_use = \${toString mailbox.specialUse}
   '' + "}";
 
   mailboxes = { ... }: {
@@ -133,6 +152,9 @@ let
   };
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
+  ];
 
   options.services.dovecot2 = {
     enable = mkEnableOption "Dovecot 2.x POP3/IMAP server";
@@ -157,7 +179,7 @@ in
 
     protocols = mkOption {
       type = types.listOf types.str;
-      default = [ ];
+      default = [];
       description = "Additional listeners to start when Dovecot is enabled.";
     };
 
@@ -180,6 +202,43 @@ in
       description = "Additional entries to put verbatim into Dovecot's config file.";
     };
 
+    mailPlugins =
+      let
+        plugins = hint: types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = "mail plugins to enable as a list of strings to append to the ${hint} <literal>$mail_plugins</literal> configuration variable";
+            };
+          };
+        };
+      in
+        mkOption {
+          type = with types; submodule {
+            options = {
+              globally = mkOption {
+                description = "Additional entries to add to the mail_plugins variable for all protocols";
+                type = plugins "top-level";
+                example = { enable = [ "virtual" ]; };
+                default = { enable = []; };
+              };
+              perProtocol = mkOption {
+                description = "Additional entries to add to the mail_plugins variable, per protocol";
+                type = attrsOf (plugins "corresponding per-protocol");
+                default = {};
+                example = { imap = [ "imap_acl" ]; };
+              };
+            };
+          };
+          description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
+          example = {
+            globally.enable = [ "acl" ];
+            perProtocol.imap.enable = [ "imap_acl" ];
+          };
+          default = { globally.enable = []; perProtocol = {}; };
+        };
+
     configFile = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -302,41 +361,43 @@ in
       enable = true;
       params.dovecot2 = {};
     };
-   services.dovecot2.protocols =
-     optional cfg.enableImap "imap"
-     ++ optional cfg.enablePop3 "pop3"
-     ++ optional cfg.enableLmtp "lmtp";
-
-    users.users = [
-      { name = "dovenull";
-        uid = config.ids.uids.dovenull2;
-        description = "Dovecot user for untrusted logins";
-        group = "dovenull";
-      }
-    ] ++ optional (cfg.user == "dovecot2")
-         { name = "dovecot2";
-           uid = config.ids.uids.dovecot2;
-           description = "Dovecot user";
-           group = cfg.group;
-         }
-      ++ optional (cfg.createMailUser && cfg.mailUser != null)
-         ({ name = cfg.mailUser;
-            description = "Virtual Mail User";
-         } // optionalAttrs (cfg.mailGroup != null) {
-           group = cfg.mailGroup;
-         });
-
-    users.groups = optional (cfg.group == "dovecot2")
-      { name = "dovecot2";
-        gid = config.ids.gids.dovecot2;
-      }
-    ++ optional (cfg.createMailUser && cfg.mailGroup != null)
-      { name = cfg.mailGroup;
-      }
-    ++ singleton
-      { name = "dovenull";
-        gid = config.ids.gids.dovenull2;
-      };
+    services.dovecot2.protocols =
+      optional cfg.enableImap "imap"
+      ++ optional cfg.enablePop3 "pop3"
+      ++ optional cfg.enableLmtp "lmtp";
+
+    services.dovecot2.mailPlugins = mkIf cfg.enableQuota {
+      globally.enable = [ "quota" ];
+      perProtocol.imap.enable = [ "imap_quota" ];
+    };
+
+    users.users = {
+      dovenull =
+        {
+          uid = config.ids.uids.dovenull2;
+          description = "Dovecot user for untrusted logins";
+          group = "dovenull";
+        };
+    } // optionalAttrs (cfg.user == "dovecot2") {
+      dovecot2 =
+        {
+          uid = config.ids.uids.dovecot2;
+          description = "Dovecot user";
+          group = cfg.group;
+        };
+    } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
+      ${cfg.mailUser} =
+        { description = "Virtual Mail User"; } // optionalAttrs (cfg.mailGroup != null)
+          { group = cfg.mailGroup; };
+    };
+
+    users.groups = {
+      dovenull.gid = config.ids.gids.dovenull2;
+    } // optionalAttrs (cfg.group == "dovecot2") {
+      dovecot2.gid = config.ids.gids.dovecot2;
+    } // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
+      ${cfg.mailGroup} = {};
+    };
 
     environment.etc."dovecot/modules".source = modulesDir;
     environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
@@ -364,15 +425,19 @@ in
         rm -rf ${stateDir}/sieve
       '' + optionalString (cfg.sieveScripts != {}) ''
         mkdir -p ${stateDir}/sieve
-        ${concatStringsSep "\n" (mapAttrsToList (to: from: ''
-          if [ -d '${from}' ]; then
-            mkdir '${stateDir}/sieve/${to}'
-            cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
-          else
-            cp -p '${from}' '${stateDir}/sieve/${to}'
-          fi
-          ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
-        '') cfg.sieveScripts)}
+        ${concatStringsSep "\n" (
+        mapAttrsToList (
+          to: from: ''
+            if [ -d '${from}' ]; then
+              mkdir '${stateDir}/sieve/${to}'
+              cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
+            else
+              cp -p '${from}' '${stateDir}/sieve/${to}'
+            fi
+            ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
+          ''
+        ) cfg.sieveScripts
+      )}
         chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
       '';
     };
@@ -380,17 +445,21 @@ in
     environment.systemPackages = [ dovecotPkg ];
 
     assertions = [
-      { assertion = intersectLists cfg.protocols [ "pop3" "imap" ] != [];
+      {
+        assertion = intersectLists cfg.protocols [ "pop3" "imap" ] != [];
         message = "dovecot needs at least one of the IMAP or POP3 listeners enabled";
       }
-      { assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
-          && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
+      {
+        assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
+        && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
         message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
       }
-      { assertion = cfg.showPAMFailure -> cfg.enablePAM;
+      {
+        assertion = cfg.showPAMFailure -> cfg.enablePAM;
         message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
       }
-      { assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
+      {
+        assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
         message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
       }
     ];
diff --git a/nixos/modules/services/mail/dspam.nix b/nixos/modules/services/mail/dspam.nix
index 72b8c4c08b9..766ebc8095a 100644
--- a/nixos/modules/services/mail/dspam.nix
+++ b/nixos/modules/services/mail/dspam.nix
@@ -86,16 +86,16 @@ in {
 
   config = mkIf cfg.enable (mkMerge [
     {
-      users.users = optionalAttrs (cfg.user == "dspam") (singleton
-        { name = "dspam";
+      users.users = optionalAttrs (cfg.user == "dspam") {
+        dspam = {
           group = cfg.group;
           uid = config.ids.uids.dspam;
-        });
+        };
+      };
 
-      users.groups = optionalAttrs (cfg.group == "dspam") (singleton
-        { name = "dspam";
-          gid = config.ids.gids.dspam;
-        });
+      users.groups = optionalAttrs (cfg.group == "dspam") {
+        dspam.gid = config.ids.gids.dspam;
+      };
 
       environment.systemPackages = [ dspam ];
 
diff --git a/nixos/modules/services/mail/exim.nix b/nixos/modules/services/mail/exim.nix
index 47812dd1e40..892fbd33214 100644
--- a/nixos/modules/services/mail/exim.nix
+++ b/nixos/modules/services/mail/exim.nix
@@ -87,15 +87,13 @@ in
       systemPackages = [ cfg.package ];
     };
 
-    users.users = singleton {
-      name = cfg.user;
+    users.users.${cfg.user} = {
       description = "Exim mail transfer agent user";
       uid = config.ids.uids.exim;
       group = cfg.group;
     };
 
-    users.groups = singleton {
-      name = cfg.group;
+    users.groups.${cfg.group} = {
       gid = config.ids.gids.exim;
     };
 
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index e917209f3d1..f5e78b18293 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -6,37 +6,18 @@ let
 
   cfg = config.services.mailman;
 
-  mailmanPyEnv = pkgs.python3.withPackages (ps: with ps; [mailman mailman-hyperkitty]);
-
-  mailmanExe = with pkgs; stdenv.mkDerivation {
-    name = "mailman-" + python3Packages.mailman.version;
-    buildInputs = [makeWrapper];
-    unpackPhase = ":";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${mailmanPyEnv}/bin/mailman $out/bin/mailman \
-        --set MAILMAN_CONFIG_FILE /etc/mailman.cfg
-   '';
-  };
-
-  mailmanWeb = pkgs.python3Packages.mailman-web.override {
-    serverEMail = cfg.siteOwner;
-    archiverKey = cfg.hyperkittyApiKey;
-    allowedHosts = cfg.webHosts;
-  };
-
-  mailmanWebPyEnv = pkgs.python3.withPackages (x: with x; [mailman-web]);
-
-  mailmanWebExe = with pkgs; stdenv.mkDerivation {
-    inherit (mailmanWeb) name;
-    buildInputs = [makeWrapper];
-    unpackPhase = ":";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${mailmanWebPyEnv}/bin/django-admin $out/bin/mailman-web \
-        --set DJANGO_SETTINGS_MODULE settings
-    '';
-  };
+  # This deliberately doesn't use recursiveUpdate so users can
+  # override the defaults.
+  settings = {
+    DEFAULT_FROM_EMAIL = cfg.siteOwner;
+    SERVER_EMAIL = cfg.siteOwner;
+    ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
+    COMPRESS_OFFLINE = true;
+    STATIC_ROOT = "/var/lib/mailman-web/static";
+    MEDIA_ROOT = "/var/lib/mailman-web/media";
+  } // cfg.webSettings;
+
+  settingsJSON = pkgs.writeText "settings.json" (builtins.toJSON settings);
 
   mailmanCfg = ''
     [mailman]
@@ -53,30 +34,42 @@ let
     etc_dir: /etc
     ext_dir: $etc_dir/mailman.d
     pid_file: /run/mailman/master.pid
-  '' + optionalString (cfg.hyperkittyApiKey != null) ''
+  '' + optionalString cfg.hyperkitty.enable ''
+
     [archiver.hyperkitty]
     class: mailman_hyperkitty.Archiver
     enable: yes
-    configuration: ${pkgs.writeText "mailman-hyperkitty.cfg" mailmanHyperkittyCfg}
+    configuration: /var/lib/mailman/mailman-hyperkitty.cfg
   '';
 
-  mailmanHyperkittyCfg = ''
+  mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
     [general]
     # This is your HyperKitty installation, preferably on the localhost. This
     # address will be used by Mailman to forward incoming emails to HyperKitty
     # for archiving. It does not need to be publicly available, in fact it's
     # better if it is not.
-    base_url: ${cfg.hyperkittyBaseUrl}
+    base_url: ${cfg.hyperkitty.baseUrl}
 
     # Shared API key, must be the identical to the value in HyperKitty's
     # settings.
-    api_key: ${cfg.hyperkittyApiKey}
+    api_key: @API_KEY@
   '';
 
 in {
 
   ###### interface
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
+      [ "services" "mailman" "hyperkitty" "baseUrl" ])
+
+    (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
+      The Hyperkitty API key is now generated on first run, and not
+      stored in the world-readable Nix store.  To continue using
+      Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
+    '')
+  ];
+
   options = {
 
     services.mailman = {
@@ -87,9 +80,17 @@ in {
         description = "Enable Mailman on this host. Requires an active Postfix installation.";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mailman;
+        defaultText = "pkgs.mailman";
+        example = "pkgs.mailman.override { archivers = []; }";
+        description = "Mailman package to use";
+      };
+
       siteOwner = mkOption {
         type = types.str;
-        default = "postmaster@example.org";
+        example = "postmaster@example.org";
         description = ''
           Certain messages that must be delivered to a human, but which can't
           be delivered to a list owner (e.g. a bounce from a list owner), will
@@ -99,12 +100,13 @@ in {
 
       webRoot = mkOption {
         type = types.path;
-        default = "${mailmanWeb}/${pkgs.python3.sitePackages}";
-        defaultText = "pkgs.python3Packages.mailman-web";
+        default = "${pkgs.mailman-web}/${pkgs.python3.sitePackages}";
+        defaultText = "\${pkgs.mailman-web}/\${pkgs.python3.sitePackages}";
         description = ''
           The web root for the Hyperkity + Postorius apps provided by Mailman.
           This variable can be set, of course, but it mainly exists so that site
-          admins can refer to it in their own hand-written httpd configuration files.
+          admins can refer to it in their own hand-written web server
+          configuration files.
         '';
       };
 
@@ -120,26 +122,35 @@ in {
         '';
       };
 
-      hyperkittyBaseUrl = mkOption {
+      webUser = mkOption {
         type = types.str;
-        default = "http://localhost/hyperkitty/";
+        default = config.services.httpd.user;
         description = ''
-          Where can Mailman connect to Hyperkitty's internal API, preferably on
-          localhost?
+          User to run mailman-web as
         '';
       };
 
-      hyperkittyApiKey = mkOption {
-        type = types.nullOr types.str;
-        default = null;
+      webSettings = mkOption {
+        type = types.attrs;
+        default = {};
         description = ''
-          The shared secret used to authenticate Mailman's internal
-          communication with Hyperkitty. Must be set to enable support for the
-          Hyperkitty archiver. Note that this secret is going to be visible to
-          all local users in the Nix store.
+          Overrides for the default mailman-web Django settings.
         '';
       };
 
+      hyperkitty = {
+        enable = mkEnableOption "the Hyperkitty archiver for Mailman";
+
+        baseUrl = mkOption {
+          type = types.str;
+          default = "http://localhost/hyperkitty/";
+          description = ''
+            Where can Mailman connect to Hyperkitty's internal API, preferably on
+            localhost?
+          '';
+        };
+      };
+
     };
   };
 
@@ -147,25 +158,58 @@ in {
 
   config = mkIf cfg.enable {
 
-    assertions = [
-      { assertion = cfg.enable -> config.services.postfix.enable;
+    assertions = let
+      inherit (config.services) postfix;
+
+      requirePostfixHash = optionPath: dataFile:
+        with lib;
+        let
+          expected = "hash:/var/lib/mailman/data/${dataFile}";
+          value = attrByPath optionPath [] postfix;
+        in
+          { assertion = postfix.enable -> isList value && elem expected value;
+            message = ''
+              services.postfix.${concatStringsSep "." optionPath} must contain
+              "${expected}".
+              See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
+            '';
+          };
+    in [
+      { assertion = postfix.enable;
         message = "Mailman requires Postfix";
       }
+      (requirePostfixHash [ "relayDomains" ] "postfix_domains")
+      (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
+      (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
     ];
 
     users.users.mailman = { description = "GNU Mailman"; isSystemUser = true; };
 
-    environment = {
-      systemPackages = [ mailmanExe mailmanWebExe pkgs.sassc ];
-      etc."mailman.cfg".text = mailmanCfg;
-    };
+    environment.etc."mailman.cfg".text = mailmanCfg;
+
+    environment.etc."mailman3/settings.py".text = ''
+      import os
+
+      # Required by mailman_web.settings, but will be overridden when
+      # settings_local.json is loaded.
+      os.environ["SECRET_KEY"] = ""
+
+      from mailman_web.settings import *
+
+      import json
+
+      with open('${settingsJSON}') as f:
+          globals().update(json.load(f))
+
+      with open('/var/lib/mailman-web/settings_local.json') as f:
+          globals().update(json.load(f))
+    '';
+
+    environment.systemPackages = [ cfg.package ] ++ (with pkgs; [ mailman-web ]);
 
     services.postfix = {
-      relayDomains = [ "hash:/var/lib/mailman/data/postfix_domains" ];
       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
       config = {
-        transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
-        local_recipient_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
         owner_request_special = "no";   # Mailman handles -owner addresses on its own
       };
     };
@@ -173,34 +217,79 @@ in {
     systemd.services.mailman = {
       description = "GNU Mailman Master Process";
       after = [ "network.target" ];
+      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${mailmanExe}/bin/mailman start";
-        ExecStop = "${mailmanExe}/bin/mailman stop";
+        ExecStart = "${cfg.package}/bin/mailman start";
+        ExecStop = "${cfg.package}/bin/mailman stop";
         User = "mailman";
         Type = "forking";
-        StateDirectory = "mailman";
-        StateDirectoryMode = "0700";
         RuntimeDirectory = "mailman";
         PIDFile = "/run/mailman/master.pid";
       };
     };
 
+    systemd.services.mailman-settings = {
+      description = "Generate settings files (including secrets) for Mailman";
+      before = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
+      requiredBy = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
+      path = with pkgs; [ jq ];
+      script = ''
+        mailmanDir=/var/lib/mailman
+        mailmanWebDir=/var/lib/mailman-web
+
+        mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
+        mailmanWebCfg=$mailmanWebDir/settings_local.json
+
+        install -m 0700 -o mailman -g nogroup -d $mailmanDir
+        install -m 0700 -o ${cfg.webUser} -g nogroup -d $mailmanWebDir
+
+        if [ ! -e $mailmanWebCfg ]; then
+            hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+            secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+
+            mailmanWebCfgTmp=$(mktemp)
+            jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
+                --arg archiver_key "$hyperkittyApiKey" \
+                --arg secret_key "$secretKey" \
+                >"$mailmanWebCfgTmp"
+            chown ${cfg.webUser} "$mailmanWebCfgTmp"
+            mv -n "$mailmanWebCfgTmp" $mailmanWebCfg
+        fi
+
+        hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY $mailmanWebCfg)"
+        mailmanCfgTmp=$(mktemp)
+        sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
+        chown mailman "$mailmanCfgTmp"
+        mv "$mailmanCfgTmp" $mailmanCfg
+      '';
+      serviceConfig = {
+        Type = "oneshot";
+        # RemainAfterExit makes restartIfChanged work for this service, so
+        # downstream services will get updated automatically when things like
+        # services.mailman.hyperkitty.baseUrl change.  Otherwise users have to
+        # restart things manually, which is confusing.
+        RemainAfterExit = "yes";
+      };
+    };
+
     systemd.services.mailman-web = {
       description = "Init Postorius DB";
-      before = [ "httpd.service" ];
-      requiredBy = [ "httpd.service" ];
+      before = [ "httpd.service" "uwsgi.service" ];
+      requiredBy = [ "httpd.service" "uwsgi.service" ];
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       script = ''
-        ${mailmanWebExe}/bin/mailman-web migrate
+        ${pkgs.mailman-web}/bin/mailman-web migrate
         rm -rf static
-        ${mailmanWebExe}/bin/mailman-web collectstatic
-        ${mailmanWebExe}/bin/mailman-web compress
+        ${pkgs.mailman-web}/bin/mailman-web collectstatic
+        ${pkgs.mailman-web}/bin/mailman-web compress
       '';
       serviceConfig = {
-        User = config.services.httpd.user;
+        User = cfg.webUser;
         Type = "oneshot";
-        StateDirectory = "mailman-web";
-        StateDirectoryMode = "0700";
+        # Similar to mailman-settings.service, this makes restartTriggers work
+        # properly for this service.
+        RemainAfterExit = "yes";
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
@@ -208,86 +297,94 @@ in {
     systemd.services.mailman-daily = {
       description = "Trigger daily Mailman events";
       startAt = "daily";
+      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
       serviceConfig = {
-        ExecStart = "${mailmanExe}/bin/mailman digests --send";
+        ExecStart = "${cfg.package}/bin/mailman digests --send";
         User = "mailman";
       };
     };
 
     systemd.services.hyperkitty = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "GNU Hyperkitty QCluster Process";
       after = [ "network.target" ];
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       wantedBy = [ "mailman.service" "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web qcluster";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web qcluster";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-minutely = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger minutely Hyperkitty events";
       startAt = "minutely";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs minutely";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs minutely";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-quarter-hourly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger quarter-hourly Hyperkitty events";
       startAt = "*:00/15";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs quarter_hourly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs quarter_hourly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-hourly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger hourly Hyperkitty events";
       startAt = "hourly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs hourly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs hourly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-daily = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger daily Hyperkitty events";
       startAt = "daily";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs daily";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs daily";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-weekly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger weekly Hyperkitty events";
       startAt = "weekly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs weekly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs weekly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-yearly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger yearly Hyperkitty events";
       startAt = "yearly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs yearly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs yearly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
diff --git a/nixos/modules/services/mail/mlmmj.nix b/nixos/modules/services/mail/mlmmj.nix
index 7ae00f3e501..d58d93c4214 100644
--- a/nixos/modules/services/mail/mlmmj.nix
+++ b/nixos/modules/services/mail/mlmmj.nix
@@ -94,8 +94,7 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton {
-      name = cfg.user;
+    users.users.${cfg.user} = {
       description = "mlmmj user";
       home = stateDir;
       createHome = true;
@@ -104,8 +103,7 @@ in
       useDefaultShell = true;
     };
 
-    users.groups = singleton {
-      name = cfg.group;
+    users.groups.${cfg.group} = {
       gid = config.ids.gids.mlmmj;
     };
 
diff --git a/nixos/modules/services/mail/nullmailer.nix b/nixos/modules/services/mail/nullmailer.nix
index 2c2910e0aa9..fe3f8ef9b39 100644
--- a/nixos/modules/services/mail/nullmailer.nix
+++ b/nixos/modules/services/mail/nullmailer.nix
@@ -201,15 +201,12 @@ with lib;
     };
 
     users = {
-      users = singleton {
-        name = cfg.user;
+      users.${cfg.user} = {
         description = "Nullmailer relay-only mta user";
         group = cfg.group;
       };
 
-      groups = singleton {
-        name = cfg.group;
-      };
+      groups.${cfg.group} = { };
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/mail/opendkim.nix b/nixos/modules/services/mail/opendkim.nix
index 253823cbaf9..eb6a426684d 100644
--- a/nixos/modules/services/mail/opendkim.nix
+++ b/nixos/modules/services/mail/opendkim.nix
@@ -18,6 +18,9 @@ let
          ] ++ optionals (cfg.configFile != null) [ "-x" cfg.configFile ];
 
 in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "opendkim" "keyFile" ] [ "services" "opendkim" "keyPath" ])
+  ];
 
   ###### interface
 
@@ -88,16 +91,16 @@ in {
 
   config = mkIf cfg.enable {
 
-    users.users = optionalAttrs (cfg.user == "opendkim") (singleton
-      { name = "opendkim";
+    users.users = optionalAttrs (cfg.user == "opendkim") {
+      opendkim = {
         group = cfg.group;
         uid = config.ids.uids.opendkim;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "opendkim") (singleton
-      { name = "opendkim";
-        gid = config.ids.gids.opendkim;
-      });
+    users.groups = optionalAttrs (cfg.group == "opendkim") {
+      opendkim.gid = config.ids.gids.opendkim;
+    };
 
     environment.systemPackages = [ pkgs.opendkim ];
 
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index df438a0c69d..19e11b31d9c 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -612,10 +612,7 @@ in
     {
 
       environment = {
-        etc = singleton
-          { source = "/var/lib/postfix/conf";
-            target = "postfix";
-          };
+        etc.postfix.source = "/var/lib/postfix/conf";
 
         # This makes it comfortable to run 'postqueue/postdrop' for example.
         systemPackages = [ pkgs.postfix ];
@@ -655,21 +652,20 @@ in
         setgid = true;
       };
 
-      users.users = optional (user == "postfix")
-        { name = "postfix";
-          description = "Postfix mail server user";
-          uid = config.ids.uids.postfix;
-          group = group;
+      users.users = optionalAttrs (user == "postfix")
+        { postfix = {
+            description = "Postfix mail server user";
+            uid = config.ids.uids.postfix;
+            group = group;
+          };
         };
 
       users.groups =
-        optional (group == "postfix")
-        { name = group;
-          gid = config.ids.gids.postfix;
+        optionalAttrs (group == "postfix")
+        { ${group}.gid = config.ids.gids.postfix;
         }
-        ++ optional (setgidGroup == "postdrop")
-        { name = setgidGroup;
-          gid = config.ids.gids.postdrop;
+        // optionalAttrs (setgidGroup == "postdrop")
+        { ${setgidGroup}.gid = config.ids.gids.postdrop;
         };
 
       systemd.services.postfix =
diff --git a/nixos/modules/services/mail/postgrey.nix b/nixos/modules/services/mail/postgrey.nix
index 88fb7f0b4ad..709f6b21aa0 100644
--- a/nixos/modules/services/mail/postgrey.nix
+++ b/nixos/modules/services/mail/postgrey.nix
@@ -42,6 +42,17 @@ with lib; let
   };
 
 in {
+  imports = [
+    (mkMergedOptionModule [ [ "services" "postgrey" "inetAddr" ] [ "services" "postgrey" "inetPort" ] ] [ "services" "postgrey" "socket" ] (config: let
+        value = p: getAttrFromPath p config;
+        inetAddr = [ "services" "postgrey" "inetAddr" ];
+        inetPort = [ "services" "postgrey" "inetPort" ];
+      in
+        if value inetAddr == null
+        then { path = "/run/postgrey.sock"; }
+        else { addr = value inetAddr; port = value inetPort; }
+    ))
+  ];
 
   options = {
     services.postgrey = with types; {
diff --git a/nixos/modules/services/mail/postsrsd.nix b/nixos/modules/services/mail/postsrsd.nix
index 8f12a16906c..2ebc675ab10 100644
--- a/nixos/modules/services/mail/postsrsd.nix
+++ b/nixos/modules/services/mail/postsrsd.nix
@@ -90,16 +90,16 @@ in {
 
     services.postsrsd.domain = mkDefault config.networking.hostName;
 
-    users.users = optionalAttrs (cfg.user == "postsrsd") (singleton
-      { name = "postsrsd";
+    users.users = optionalAttrs (cfg.user == "postsrsd") {
+      postsrsd = {
         group = cfg.group;
         uid = config.ids.uids.postsrsd;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "postsrsd") (singleton
-      { name = "postsrsd";
-        gid = config.ids.gids.postsrsd;
-      });
+    users.groups = optionalAttrs (cfg.group == "postsrsd") {
+      postsrsd.gid = config.ids.gids.postsrsd;
+    };
 
     systemd.services.postsrsd = {
       description = "PostSRSd SRS rewriting server";
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index 36dda619ad0..21e92cfee01 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -5,6 +5,9 @@ with lib;
 let
   cfg = config.services.roundcube;
   fpm = config.services.phpfpm.pools.roundcube;
+  localDB = cfg.database.host == "localhost";
+  user = cfg.database.username;
+  phpWithPspell = pkgs.php.withExtensions (e: [ e.pspell ] ++ pkgs.php.enabledExtensions);
 in
 {
   options.services.roundcube = {
@@ -44,7 +47,10 @@ in
       username = mkOption {
         type = types.str;
         default = "roundcube";
-        description = "Username for the postgresql connection";
+        description = ''
+          Username for the postgresql connection.
+          If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
+        '';
       };
       host = mkOption {
         type = types.str;
@@ -58,7 +64,12 @@ in
       };
       password = mkOption {
         type = types.str;
-        description = "Password for the postgresql connection";
+        description = "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use <literal>passwordFile</literal> instead.";
+        default = "";
+      };
+      passwordFile = mkOption {
+        type = types.str;
+        description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>. Ignored if <literal>database.host</literal> is set to <literal>localhost</literal>, as peer authentication will be used.";
       };
       dbname = mkOption {
         type = types.str;
@@ -75,6 +86,15 @@ in
       '';
     };
 
+    dicts = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExample "with pkgs.aspellDicts; [ en fr de ]";
+      description = ''
+        List of aspell dictionnaries for spell checking. If empty, spell checking is disabled.
+      '';
+    };
+
     extraConfig = mkOption {
       type = types.lines;
       default = "";
@@ -83,14 +103,27 @@ in
   };
 
   config = mkIf cfg.enable {
+    # backward compatibility: if password is set but not passwordFile, make one.
+    services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
+    warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
+
     environment.etc."roundcube/config.inc.php".text = ''
       <?php
 
+      ${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
+
       $config = array();
-      $config['db_dsnw'] = 'pgsql://${cfg.database.username}:${cfg.database.password}@${cfg.database.host}/${cfg.database.dbname}';
+      $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
       $config['log_driver'] = 'syslog';
       $config['max_message_size'] = '25M';
       $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
+      $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
+      $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
+      $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
+      # by default, spellchecking uses a third-party cloud services
+      $config['spellcheck_engine'] = 'pspell';
+      $config['spellcheck_languages'] = array(${lib.concatMapStringsSep ", " (dict: let p = builtins.parseDrvName dict.shortName; in "'${p.name}' => '${dict.fullName}'") cfg.dicts});
+
       ${cfg.extraConfig}
     '';
 
@@ -116,12 +149,26 @@ in
       };
     };
 
-    services.postgresql = mkIf (cfg.database.host == "localhost") {
+    services.postgresql = mkIf localDB {
       enable = true;
+      ensureDatabases = [ cfg.database.dbname ];
+      ensureUsers = [ {
+        name = cfg.database.username;
+        ensurePermissions = {
+          "DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
+        };
+      } ];
     };
 
+    users.users.${user} = mkIf localDB {
+      group = user;
+      isSystemUser = true;
+      createHome = false;
+    };
+    users.groups.${user} = mkIf localDB {};
+
     services.phpfpm.pools.roundcube = {
-      user = "nginx";
+      user = if localDB then user else "nginx";
       phpOptions = ''
         error_log = 'stderr'
         log_errors = on
@@ -140,12 +187,12 @@ in
         "pm.max_requests" = 500;
         "catch_workers_output" = true;
       };
+      phpPackage = phpWithPspell;
+      phpEnv.ASPELL_CONF = "dict-dir ${pkgs.aspellWithDicts (_: cfg.dicts)}/lib/aspell";
     };
     systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
 
-    systemd.services.roundcube-setup = let
-      pgSuperUser = config.services.postgresql.superUser;
-    in mkMerge [
+    systemd.services.roundcube-setup = mkMerge [
       (mkIf (cfg.database.host == "localhost") {
         requires = [ "postgresql.service" ];
         after = [ "postgresql.service" ];
@@ -153,22 +200,31 @@ in
       })
       {
         wantedBy = [ "multi-user.target" ];
-        script = ''
-          mkdir -p /var/lib/roundcube
-          if [ ! -f /var/lib/roundcube/db-created ]; then
-            if [ "${cfg.database.host}" = "localhost" ]; then
-              ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create role ${cfg.database.username} with login password '${cfg.database.password}'";
-              ${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 ${cfg.package}/SQL/postgres.initial.sql \
-              -h ${cfg.database.host} ${cfg.database.dbname}
-            touch /var/lib/roundcube/db-created
+        script = let
+          psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
+        in
+        ''
+          version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
+          if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
+            ${psql} -f ${cfg.package}/SQL/postgres.initial.sql
+          fi
+
+          if [ ! -f /var/lib/roundcube/des_key ]; then
+            base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
+            # we need to log out everyone in case change the des_key
+            # from the default when upgrading from nixos 19.09
+            ${psql} <<< 'TRUNCATE TABLE session;'
           fi
 
-          ${pkgs.php}/bin/php ${cfg.package}/bin/update.sh
+          ${phpWithPspell}/bin/php ${cfg.package}/bin/update.sh
         '';
-        serviceConfig.Type = "oneshot";
+        serviceConfig = {
+          Type = "oneshot";
+          StateDirectory = "roundcube";
+          User = if localDB then user else "nginx";
+          # so that the des_key is not world readable
+          StateDirectoryMode = "0700";
+        };
       }
     ];
   };
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
index 7ef23ad1726..aacdbe2aeed 100644
--- a/nixos/modules/services/mail/rspamd.nix
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -220,7 +220,6 @@ let
 in
 
 {
-
   ###### interface
 
   options = {
@@ -375,15 +374,13 @@ in
     # Allow users to run 'rspamc' and 'rspamadm'.
     environment.systemPackages = [ pkgs.rspamd ];
 
-    users.users = singleton {
-      name = cfg.user;
+    users.users.${cfg.user} = {
       description = "rspamd daemon";
       uid = config.ids.uids.rspamd;
       group = cfg.group;
     };
 
-    users.groups = singleton {
-      name = cfg.group;
+    users.groups.${cfg.group} = {
       gid = config.ids.gids.rspamd;
     };
 
@@ -414,5 +411,6 @@ in
 	     "Socket activation never worked correctly and could at this time not be fixed and so was removed")
     (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
     (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
+    (mkRemovedOptionModule [ "services" "rmilter" ] "Use services.rspamd.* instead to set up milter service")
   ];
 }
diff --git a/nixos/modules/services/mail/rss2email.nix b/nixos/modules/services/mail/rss2email.nix
index c1e5964c453..7f8d2adac64 100644
--- a/nixos/modules/services/mail/rss2email.nix
+++ b/nixos/modules/services/mail/rss2email.nix
@@ -91,6 +91,8 @@ in {
       };
     };
 
+    environment.systemPackages = with pkgs; [ rss2email ];
+
     services.rss2email.config.to = cfg.to;
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/mail/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
index 1fe77ce5a0c..f6c0b9c794c 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -5,16 +5,6 @@ with lib;
 let
   cfg = config.services.spamassassin;
   spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config;
-  spamassassin-init-pre = pkgs.writeText "init.pre" cfg.initPreConf;
-
-  spamdEnv = pkgs.buildEnv {
-    name = "spamd-env";
-    paths = [];
-    postBuild = ''
-      ln -sf ${spamassassin-init-pre} $out/init.pre
-      ln -sf ${spamassassin-local-cf} $out/local.cf
-    '';
-  };
 
 in
 
@@ -22,10 +12,7 @@ in
   options = {
 
     services.spamassassin = {
-      enable = mkOption {
-        default = false;
-        description = "Whether to run the SpamAssassin daemon";
-      };
+      enable = mkEnableOption "the SpamAssassin daemon";
 
       debug = mkOption {
         default = false;
@@ -65,8 +52,9 @@ in
       };
 
       initPreConf = mkOption {
-        type = types.str;
+        type = with types; either str path;
         description = "The SpamAssassin init.pre config.";
+        apply = val: if builtins.isPath val then val else pkgs.writeText "init.pre" val;
         default =
         ''
           #
@@ -120,30 +108,26 @@ in
   };
 
   config = mkIf cfg.enable {
+    environment.etc."mail/spamassassin/init.pre".source = cfg.initPreConf;
+    environment.etc."mail/spamassassin/local.cf".source = spamassassin-local-cf;
 
     # Allow users to run 'spamc'.
+    environment.systemPackages = [ pkgs.spamassassin ];
 
-    environment = {
-      etc = singleton { source = spamdEnv; target = "spamassassin"; };
-      systemPackages = [ pkgs.spamassassin ];
-    };
-
-    users.users = singleton {
-      name = "spamd";
+    users.users.spamd = {
       description = "Spam Assassin Daemon";
       uid = config.ids.uids.spamd;
       group = "spamd";
     };
 
-    users.groups = singleton {
-      name = "spamd";
+    users.groups.spamd = {
       gid = config.ids.gids.spamd;
     };
 
     systemd.services.sa-update = {
       script = ''
         set +e
-        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd
+        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/" spamd
 
         v=$?
         set -e
@@ -174,7 +158,7 @@ in
       after = [ "network.target" ];
 
       serviceConfig = {
-        ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --siteconfigpath=${spamdEnv} --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
+        ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
       };
 
@@ -185,7 +169,7 @@ in
         mkdir -p /var/lib/spamassassin
         chown spamd:spamd /var/lib/spamassassin -R
         set +e
-        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd
+        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/" spamd
         v=$?
         set -e
         if [ $v -gt 1 ]; then
diff --git a/nixos/modules/services/mail/sympa.nix b/nixos/modules/services/mail/sympa.nix
new file mode 100644
index 00000000000..0cad09927b2
--- /dev/null
+++ b/nixos/modules/services/mail/sympa.nix
@@ -0,0 +1,594 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sympa;
+  dataDir = "/var/lib/sympa";
+  user = "sympa";
+  group = "sympa";
+  pkg = pkgs.sympa;
+  fqdns = attrNames cfg.domains;
+  usingNginx = cfg.web.enable && cfg.web.server == "nginx";
+  mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL";
+  pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL";
+
+  sympaSubServices = [
+    "sympa-archive.service"
+    "sympa-bounce.service"
+    "sympa-bulk.service"
+    "sympa-task.service"
+  ];
+
+  # common for all services including wwsympa
+  commonServiceConfig = {
+    StateDirectory = "sympa";
+    ProtectHome = true;
+    ProtectSystem = "full";
+    ProtectControlGroups = true;
+  };
+
+  # wwsympa has its own service config
+  sympaServiceConfig = srv: {
+    Type = "simple";
+    Restart = "always";
+    ExecStart = "${pkg}/bin/${srv}.pl --foreground";
+    PIDFile = "/run/sympa/${srv}.pid";
+    User = user;
+    Group = group;
+
+    # avoid duplicating log messageges in journal
+    StandardError = "null";
+  } // commonServiceConfig;
+
+  configVal = value:
+    if isBool value then
+      if value then "on" else "off"
+    else toString value;
+  configGenerator = c: concatStrings (flip mapAttrsToList c (key: val: "${key}\t${configVal val}\n"));
+
+  mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings);
+  robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings);
+
+  transport = pkgs.writeText "transport.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
+    ${domain}                        error:User unknown in recipient table
+    sympa@${domain}                  sympa:sympa@${domain}
+    listmaster@${domain}             sympa:listmaster@${domain}
+    bounce@${domain}                 sympabounce:sympa@${domain}
+    abuse-feedback-report@${domain}  sympabounce:sympa@${domain}
+  '')));
+
+  virtual = pkgs.writeText "virtual.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
+    sympa-request@${domain}  postmaster@localhost
+    sympa-owner@${domain}    postmaster@localhost
+  '')));
+
+  listAliases = pkgs.writeText "list_aliases.tt2" ''
+    #--- [% list.name %]@[% list.domain %]: list transport map created at [% date %]
+    [% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %]
+    [% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %]
+    [% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %]
+    #[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %]
+    [% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %]
+    [% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %]
+  '';
+
+  enabledFiles = filterAttrs (n: v: v.enable) cfg.settingsFile;
+in
+{
+
+  ###### interface
+  options.services.sympa = with types; {
+
+    enable = mkEnableOption "Sympa mailing list manager";
+
+    lang = mkOption {
+      type = str;
+      default = "en_US";
+      example = "cs";
+      description = ''
+        Default Sympa language.
+        See <link xlink:href='https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa' />
+        for available options.
+      '';
+    };
+
+    listMasters = mkOption {
+      type = listOf str;
+      example = [ "postmaster@sympa.example.org" ];
+      description = ''
+        The list of the email addresses of the listmasters
+        (users authorized to perform global server commands).
+      '';
+    };
+
+    mainDomain = mkOption {
+      type = nullOr str;
+      default = null;
+      example = "lists.example.org";
+      description = ''
+        Main domain to be used in <filename>sympa.conf</filename>.
+        If <literal>null</literal>, one of the <option>services.sympa.domains</option> is chosen for you.
+      '';
+    };
+
+    domains = mkOption {
+      type = attrsOf (submodule ({ name, config, ... }: {
+        options = {
+          webHost = mkOption {
+            type = nullOr str;
+            default = null;
+            example = "archive.example.org";
+            description = ''
+              Domain part of the web interface URL (no web interface for this domain if <literal>null</literal>).
+              DNS record of type A (or AAAA or CNAME) has to exist with this value.
+            '';
+          };
+          webLocation = mkOption {
+            type = str;
+            default = "/";
+            example = "/sympa";
+            description = "URL path part of the web interface.";
+          };
+          settings = mkOption {
+            type = attrsOf (oneOf [ str int bool ]);
+            default = {};
+            example = {
+              default_max_list_members = 3;
+            };
+            description = ''
+              The <filename>robot.conf</filename> configuration file as key value set.
+              See <link xlink:href='https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html' />
+              for list of configuration parameters.
+            '';
+          };
+        };
+
+        config.settings = mkIf (cfg.web.enable && config.webHost != null) {
+          wwsympa_url = mkDefault "https://${config.webHost}${strings.removeSuffix "/" config.webLocation}";
+        };
+      }));
+
+      description = ''
+        Email domains handled by this instance. There have
+        to be MX records for keys of this attribute set.
+      '';
+      example = literalExample ''
+        {
+          "lists.example.org" = {
+            webHost = "lists.example.org";
+            webLocation = "/";
+          };
+          "sympa.example.com" = {
+            webHost = "example.com";
+            webLocation = "/sympa";
+          };
+        }
+      '';
+    };
+
+    database = {
+      type = mkOption {
+        type = enum [ "SQLite" "PostgreSQL" "MySQL" ];
+        default = "SQLite";
+        example = "MySQL";
+        description = "Database engine to use.";
+      };
+
+      host = mkOption {
+        type = nullOr str;
+        default = null;
+        description = ''
+          Database host address.
+
+          For MySQL, use <literal>localhost</literal> to connect using Unix domain socket.
+
+          For PostgreSQL, use path to directory (e.g. <filename>/run/postgresql</filename>)
+          to connect using Unix domain socket located in this directory.
+
+          Use <literal>null</literal> to fall back on Sympa default, or when using
+          <option>services.sympa.database.createLocally</option>.
+        '';
+      };
+
+      port = mkOption {
+        type = nullOr port;
+        default = null;
+        description = "Database port. Use <literal>null</literal> for default port.";
+      };
+
+      name = mkOption {
+        type = str;
+        default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa";
+        defaultText = ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"'';
+        description = ''
+          Database name. When using SQLite this must be an absolute
+          path to the database file.
+        '';
+      };
+
+      user = mkOption {
+        type = nullOr str;
+        default = user;
+        description = "Database user. The system user name is used as a default.";
+      };
+
+      passwordFile = mkOption {
+        type = nullOr path;
+        default = null;
+        example = "/run/keys/sympa-dbpassword";
+        description = ''
+          A file containing the password for <option>services.sympa.database.user</option>.
+        '';
+      };
+
+      createLocally = mkOption {
+        type = bool;
+        default = true;
+        description = "Whether to create a local database automatically.";
+      };
+    };
+
+    web = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = "Whether to enable Sympa web interface.";
+      };
+
+      server = mkOption {
+        type = enum [ "nginx" "none" ];
+        default = "nginx";
+        description = ''
+          The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself.
+          Further nginx configuration can be done by adapting
+          <option>services.nginx.virtualHosts.<replaceable>name</replaceable></option>.
+        '';
+      };
+
+      https = mkOption {
+        type = bool;
+        default = true;
+        description = ''
+          Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME.
+          Please note that Sympa web interface always uses https links even when this option is disabled.
+        '';
+      };
+
+      fcgiProcs = mkOption {
+        type = ints.positive;
+        default = 2;
+        description = "Number of FastCGI processes to fork.";
+      };
+    };
+
+    mta = {
+      type = mkOption {
+        type = enum [ "postfix" "none" ];
+        default = "postfix";
+        description = ''
+          Mail transfer agent (MTA) integration. Use <literal>none</literal> if you want to configure it yourself.
+
+          The <literal>postfix</literal> integration sets up local Postfix instance that will pass incoming
+          messages from configured domains to Sympa. You still need to configure at least outgoing message
+          handling using e.g. <option>services.postfix.relayHost</option>.
+        '';
+      };
+    };
+
+    settings = mkOption {
+      type = attrsOf (oneOf [ str int bool ]);
+      default = {};
+      example = literalExample ''
+        {
+          default_home = "lists";
+          viewlogs_page_size = 50;
+        }
+      '';
+      description = ''
+        The <filename>sympa.conf</filename> configuration file as key value set.
+        See <link xlink:href='https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html' />
+        for list of configuration parameters.
+      '';
+    };
+
+    settingsFile = mkOption {
+      type = attrsOf (submodule ({ name, config, ... }: {
+        options = {
+          enable = mkOption {
+            type = bool;
+            default = true;
+            description = "Whether this file should be generated. This option allows specific files to be disabled.";
+          };
+          text = mkOption {
+            default = null;
+            type = nullOr lines;
+            description = "Text of the file.";
+          };
+          source = mkOption {
+            type = path;
+            description = "Path of the source file.";
+          };
+        };
+
+        config.source = mkIf (config.text != null) (mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text));
+      }));
+      default = {};
+      example = literalExample ''
+        {
+          "list_data/lists.example.org/help" = {
+            text = "subject This list provides help to users";
+          };
+        }
+      '';
+      description = "Set of files to be linked in <filename>${dataDir}</filename>.";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.sympa.settings = (mapAttrs (_: v: mkDefault v) {
+      domain     = if cfg.mainDomain != null then cfg.mainDomain else head fqdns;
+      listmaster = concatStringsSep "," cfg.listMasters;
+      lang       = cfg.lang;
+
+      home        = "${dataDir}/list_data";
+      arc_path    = "${dataDir}/arc";
+      bounce_path = "${dataDir}/bounce";
+
+      sendmail = "${pkgs.system-sendmail}/bin/sendmail";
+
+      db_type = cfg.database.type;
+      db_name = cfg.database.name;
+    }
+    // (optionalAttrs (cfg.database.host != null) {
+      db_host = cfg.database.host;
+    })
+    // (optionalAttrs mysqlLocal {
+      db_host = "localhost"; # use unix domain socket
+    })
+    // (optionalAttrs pgsqlLocal {
+      db_host = "/run/postgresql"; # use unix domain socket
+    })
+    // (optionalAttrs (cfg.database.port != null) {
+      db_port = cfg.database.port;
+    })
+    // (optionalAttrs (cfg.database.user != null) {
+      db_user = cfg.database.user;
+    })
+    // (optionalAttrs (cfg.mta.type == "postfix") {
+      sendmail_aliases = "${dataDir}/sympa_transport";
+      aliases_program  = "${pkgs.postfix}/bin/postmap";
+      aliases_db_type  = "hash";
+    })
+    // (optionalAttrs cfg.web.enable {
+      static_content_path = "${dataDir}/static_content";
+      css_path            = "${dataDir}/static_content/css";
+      pictures_path       = "${dataDir}/static_content/pictures";
+      mhonarc             = "${pkgs.perlPackages.MHonArc}/bin/mhonarc";
+    }));
+
+    services.sympa.settingsFile = {
+      "virtual.sympa"        = mkDefault { source = virtual; };
+      "transport.sympa"      = mkDefault { source = transport; };
+      "etc/list_aliases.tt2" = mkDefault { source = listAliases; };
+    }
+    // (flip mapAttrs' cfg.domains (fqdn: domain:
+          nameValuePair "etc/${fqdn}/robot.conf" (mkDefault { source = robotConfig fqdn domain; })));
+
+    environment = {
+      systemPackages = [ pkg ];
+    };
+
+    users.users.${user} = {
+      description = "Sympa mailing list manager user";
+      group = group;
+      home = dataDir;
+      createHome = false;
+      isSystemUser = true;
+    };
+
+    users.groups.${group} = {};
+
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.user == user;
+        message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true";
+      }
+      { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "a password cannot be specified if services.sympa.database.createLocally is set to true";
+      }
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d  ${dataDir}                   0711 ${user} ${group} - -"
+      "d  ${dataDir}/etc               0700 ${user} ${group} - -"
+      "d  ${dataDir}/spool             0700 ${user} ${group} - -"
+      "d  ${dataDir}/list_data         0700 ${user} ${group} - -"
+      "d  ${dataDir}/arc               0700 ${user} ${group} - -"
+      "d  ${dataDir}/bounce            0700 ${user} ${group} - -"
+      "f  ${dataDir}/sympa_transport   0600 ${user} ${group} - -"
+
+      # force-copy static_content so it's up to date with package
+      # set permissions for wwsympa which needs write access (...)
+      "R  ${dataDir}/static_content    -    -       -        - -"
+      "C  ${dataDir}/static_content    0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content"
+      "e  ${dataDir}/static_content/*  0711 ${user} ${group} - -"
+
+      "d  /run/sympa                   0755 ${user} ${group} - -"
+    ]
+    ++ (flip concatMap fqdns (fqdn: [
+      "d  ${dataDir}/etc/${fqdn}       0700 ${user} ${group} - -"
+      "d  ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -"
+    ]))
+    #++ (flip mapAttrsToList enabledFiles (k: v:
+    #  "L+ ${dataDir}/${k}              -    -       -        - ${v.source}"
+    #))
+    ++ (concatLists (flip mapAttrsToList enabledFiles (k: v: [
+      # sympa doesn't handle symlinks well (e.g. fails to create locks)
+      # force-copy instead
+      "R ${dataDir}/${k}              -    -       -        - -"
+      "C ${dataDir}/${k}              0700 ${user}  ${group} - ${v.source}"
+    ])));
+
+    systemd.services.sympa = {
+      description = "Sympa mailing list manager";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = sympaSubServices;
+      before = sympaSubServices;
+      serviceConfig = sympaServiceConfig "sympa_msg";
+
+      preStart = ''
+        umask 0077
+
+        cp -f ${mainConfig} ${dataDir}/etc/sympa.conf
+        ${optionalString (cfg.database.passwordFile != null) ''
+          chmod u+w ${dataDir}/etc/sympa.conf
+          echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf
+          cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf
+        ''}
+
+        ${optionalString (cfg.mta.type == "postfix") ''
+          ${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa
+          ${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa
+        ''}
+        ${pkg}/bin/sympa_newaliases.pl
+        ${pkg}/bin/sympa.pl --health_check
+      '';
+    };
+    systemd.services.sympa-archive = {
+      description = "Sympa mailing list manager (archiving)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "archived";
+    };
+    systemd.services.sympa-bounce = {
+      description = "Sympa mailing list manager (bounce processing)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "bounced";
+    };
+    systemd.services.sympa-bulk = {
+      description = "Sympa mailing list manager (message distribution)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "bulk";
+    };
+    systemd.services.sympa-task = {
+      description = "Sympa mailing list manager (task management)";
+      bindsTo = [ "sympa.service" ];
+      serviceConfig = sympaServiceConfig "task_manager";
+    };
+
+    systemd.services.wwsympa = mkIf usingNginx {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "sympa.service" ];
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/sympa/wwsympa.pid";
+        Restart = "always";
+        ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
+          -u ${user} \
+          -g ${group} \
+          -U nginx \
+          -M 0600 \
+          -F ${toString cfg.web.fcgiProcs} \
+          -P /run/sympa/wwsympa.pid \
+          -s /run/sympa/wwsympa.socket \
+          -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi
+        '';
+
+      } // commonServiceConfig;
+    };
+
+    services.nginx.enable = mkIf usingNginx true;
+    services.nginx.virtualHosts = mkIf usingNginx (let
+      vHosts = unique (remove null (mapAttrsToList (_k: v: v.webHost) cfg.domains));
+      hostLocations = host: map (v: v.webLocation) (filter (v: v.webHost == host) (attrValues cfg.domains));
+      httpsOpts = optionalAttrs cfg.web.https { forceSSL = mkDefault true; enableACME = mkDefault true; };
+    in
+    genAttrs vHosts (host: {
+      locations = genAttrs (hostLocations host) (loc: {
+        extraConfig = ''
+          include ${config.services.nginx.package}/conf/fastcgi_params;
+
+          fastcgi_pass unix:/run/sympa/wwsympa.socket;
+          fastcgi_split_path_info ^(${loc})(.*)$;
+
+          fastcgi_param PATH_INFO       $fastcgi_path_info;
+          fastcgi_param SCRIPT_FILENAME ${pkg}/lib/sympa/cgi/wwsympa.fcgi;
+        '';
+      }) // {
+        "/static-sympa/".alias = "${dataDir}/static_content/";
+      };
+    } // httpsOpts));
+
+    services.postfix = mkIf (cfg.mta.type == "postfix") {
+      enable = true;
+      recipientDelimiter = "+";
+      config = {
+        virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ];
+        virtual_mailbox_maps = [
+          "hash:${dataDir}/transport.sympa"
+          "hash:${dataDir}/sympa_transport"
+          "hash:${dataDir}/virtual.sympa"
+        ];
+        virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ];
+        transport_maps = [
+          "hash:${dataDir}/transport.sympa"
+          "hash:${dataDir}/sympa_transport"
+        ];
+      };
+      masterConfig = {
+        "sympa" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = [
+            "flags=hqRu"
+            "user=${user}"
+            "argv=${pkg}/libexec/queue"
+            "\${nexthop}"
+          ];
+        };
+        "sympabounce" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = [
+            "flags=hqRu"
+            "user=${user}"
+            "argv=${pkg}/libexec/bouncequeue"
+            "\${nexthop}"
+          ];
+        };
+      };
+    };
+
+    services.mysql = optionalAttrs mysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.postgresql = optionalAttrs pgsqlLocal {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [
+        { name = cfg.database.user;
+          ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ mmilata sorki ];
+}
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index c296e048cea..5cc2ff7f4bd 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -138,6 +138,7 @@ in {
 
     services.nginx = mkIf (cfg.virtualHost != null) {
       enable = true;
+      recommendedProxySettings = true;
       virtualHosts.${cfg.virtualHost} = {
         locations.${cfg.contextPath}.proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
       };
diff --git a/nixos/modules/services/misc/ankisyncd.nix b/nixos/modules/services/misc/ankisyncd.nix
new file mode 100644
index 00000000000..5fc19649d3d
--- /dev/null
+++ b/nixos/modules/services/misc/ankisyncd.nix
@@ -0,0 +1,79 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.ankisyncd;
+
+  name = "ankisyncd";
+
+  stateDir = "/var/lib/${name}";
+
+  authDbPath = "${stateDir}/auth.db";
+
+  sessionDbPath = "${stateDir}/session.db";
+
+  configFile = pkgs.writeText "ankisyncd.conf" (lib.generators.toINI {} {
+    sync_app = {
+      host = cfg.host;
+      port = cfg.port;
+      data_root = stateDir;
+      auth_db_path = authDbPath;
+      session_db_path = sessionDbPath;
+
+      base_url = "/sync/";
+      base_media_url = "/msync/";
+    };
+  });
+in
+  {
+    options.services.ankisyncd = {
+      enable = mkEnableOption "ankisyncd";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ankisyncd;
+        defaultText = literalExample "pkgs.ankisyncd";
+        description = "The package to use for the ankisyncd command.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "ankisyncd host";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 27701;
+        description = "ankisyncd port";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      environment.etc."ankisyncd/ankisyncd.conf".source = configFile;
+
+      systemd.services.ankisyncd = {
+        description = "ankisyncd - Anki sync server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ cfg.package ];
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = name;
+          ExecStart = "${cfg.package}/bin/ankisyncd";
+          Restart = "always";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
index 798e902ccae..f3a650a260f 100644
--- a/nixos/modules/services/misc/apache-kafka.nix
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -124,14 +124,13 @@ in {
 
     environment.systemPackages = [cfg.package];
 
-    users.users = singleton {
-      name = "apache-kafka";
+    users.users.apache-kafka = {
       uid = config.ids.uids.apache-kafka;
       description = "Apache Kafka daemon user";
       home = head cfg.logDirs;
     };
 
-    systemd.tmpfiles.rules = map (logDir: "d '${logDir} 0700 apache-kafka - - -") cfg.logDirs;
+    systemd.tmpfiles.rules = map (logDir: "d '${logDir}' 0700 apache-kafka - - -") cfg.logDirs;
 
     systemd.services.apache-kafka = {
       description = "Apache Kafka Daemon";
diff --git a/nixos/modules/services/misc/autofs.nix b/nixos/modules/services/misc/autofs.nix
index f1742177326..84f49f964b1 100644
--- a/nixos/modules/services/misc/autofs.nix
+++ b/nixos/modules/services/misc/autofs.nix
@@ -19,6 +19,7 @@ in
     services.autofs = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Mount filesystems on demand. Unmount them automatically.
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index 4708e16e2a6..cf7fb5f78d3 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -48,5 +48,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ma27 ];
+  meta.maintainers = with maintainers; [ gnidorah ];
 }
diff --git a/nixos/modules/services/misc/bepasty.nix b/nixos/modules/services/misc/bepasty.nix
index 87d36068144..f69832e5b2b 100644
--- a/nixos/modules/services/misc/bepasty.nix
+++ b/nixos/modules/services/misc/bepasty.nix
@@ -168,16 +168,12 @@ in
         })
     ) cfg.servers;
 
-    users.users = [{
-      uid = config.ids.uids.bepasty;
-      name = user;
-      group = group;
-      home = default_home;
-    }];
-
-    users.groups = [{
-      name = group;
-      gid = config.ids.gids.bepasty;
-    }];
+    users.users.${user} =
+      { uid = config.ids.uids.bepasty;
+        group = group;
+        home = default_home;
+      };
+
+    users.groups.${group}.gid = config.ids.gids.bepasty;
   };
 }
diff --git a/nixos/modules/services/misc/cgminer.nix b/nixos/modules/services/misc/cgminer.nix
index b1cf5a7d110..7635c2a0f4e 100644
--- a/nixos/modules/services/misc/cgminer.nix
+++ b/nixos/modules/services/misc/cgminer.nix
@@ -31,13 +31,7 @@ in
 
     services.cgminer = {
 
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable cgminer, an ASIC/FPGA/GPU miner for bitcoin and
-          litecoin.
-        '';
-      };
+      enable = mkEnableOption "cgminer, an ASIC/FPGA/GPU miner for bitcoin and litecoin";
 
       package = mkOption {
         default = pkgs.cgminer;
@@ -110,11 +104,12 @@ in
 
   config = mkIf config.services.cgminer.enable {
 
-    users.users = optionalAttrs (cfg.user == "cgminer") (singleton
-      { name = "cgminer";
+    users.users = optionalAttrs (cfg.user == "cgminer") {
+      cgminer = {
         uid = config.ids.uids.cgminer;
         description = "Cgminer user";
-      });
+      };
+    };
 
     environment.systemPackages = [ cfg.package ];
 
diff --git a/nixos/modules/services/misc/couchpotato.nix b/nixos/modules/services/misc/couchpotato.nix
index 528af486b41..f5163cf86cf 100644
--- a/nixos/modules/services/misc/couchpotato.nix
+++ b/nixos/modules/services/misc/couchpotato.nix
@@ -29,17 +29,14 @@ in
       };
     };
 
-    users.users = singleton
-      { name = "couchpotato";
-        group = "couchpotato";
+    users.users.couchpotato =
+      { group = "couchpotato";
         home = "/var/lib/couchpotato/";
         description = "CouchPotato daemon user";
         uid = config.ids.uids.couchpotato;
       };
 
-    users.groups = singleton
-      { name = "couchpotato";
-        gid = config.ids.gids.couchpotato;
-      };
+    users.groups.couchpotato =
+      { gid = config.ids.gids.couchpotato; };
   };
 }
diff --git a/nixos/modules/services/misc/devmon.nix b/nixos/modules/services/misc/devmon.nix
index 9dc8fee2964..e4a3348646b 100644
--- a/nixos/modules/services/misc/devmon.nix
+++ b/nixos/modules/services/misc/devmon.nix
@@ -8,12 +8,7 @@ let
 in {
   options = {
     services.devmon = {
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable devmon, an automatic device mounting daemon.
-        '';
-      };
+      enable = mkEnableOption "devmon, an automatic device mounting daemon";
     };
   };
 
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
index 8d3e294622d..d175854d2d1 100644
--- a/nixos/modules/services/misc/dictd.nix
+++ b/nixos/modules/services/misc/dictd.nix
@@ -45,18 +45,14 @@ in
     # get the command line client on system path to make some use of the service
     environment.systemPackages = [ pkgs.dict ];
 
-    users.users = singleton
-      { name = "dictd";
-        group = "dictd";
+    users.users.dictd =
+      { group = "dictd";
         description = "DICT.org dictd server";
         home = "${dictdb}/share/dictd";
         uid = config.ids.uids.dictd;
       };
 
-    users.groups = singleton
-      { name = "dictd";
-        gid = config.ids.gids.dictd;
-      };
+    users.groups.dictd.gid = config.ids.gids.dictd;
 
     systemd.services.dictd = {
       description = "DICT.org Dictionary Server";
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
index c21cb2afc3c..0776ac67908 100644
--- a/nixos/modules/services/misc/disnix.nix
+++ b/nixos/modules/services/misc/disnix.nix
@@ -17,10 +17,7 @@ in
 
     services.disnix = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable Disnix";
-      };
+      enable = mkEnableOption "Disnix";
 
       enableMultiUser = mkOption {
         type = types.bool;
@@ -61,10 +58,7 @@ in
       ++ optional cfg.useWebServiceInterface "${pkgs.dbus_java}/share/java/dbus.jar";
     services.tomcat.webapps = optional cfg.useWebServiceInterface pkgs.DisnixWebService;
 
-    users.groups = singleton
-      { name = "disnix";
-        gid = config.ids.gids.disnix;
-      };
+    users.groups.disnix.gid = config.ids.gids.disnix;
 
     systemd.services = {
       disnix = mkIf cfg.enableMultiUser {
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
index 33a6fb15264..4b52963500d 100644
--- a/nixos/modules/services/misc/dysnomia.nix
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -177,7 +177,7 @@ in
       wrapper = {};
     }
     // lib.optionalAttrs (config.services.httpd.enable) { apache-webapplication = {
-      documentRoot = config.services.httpd.documentRoot;
+      documentRoot = config.services.httpd.virtualHosts.localhost.documentRoot;
     }; }
     // lib.optionalAttrs (config.services.tomcat.axis2.enable) { axis2-webservice = {}; }
     // lib.optionalAttrs (config.services.ejabberd.enable) { ejabberd-dump = {
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
index e4d5322f9b5..7322e1c080b 100644
--- a/nixos/modules/services/misc/etcd.nix
+++ b/nixos/modules/services/misc/etcd.nix
@@ -186,8 +186,7 @@ in {
 
     environment.systemPackages = [ pkgs.etcdctl ];
 
-    users.users = singleton {
-      name = "etcd";
+    users.users.etcd = {
       uid = config.ids.uids.etcd;
       description = "Etcd daemon user";
       home = cfg.dataDir;
diff --git a/nixos/modules/services/misc/ethminer.nix b/nixos/modules/services/misc/ethminer.nix
index 2958cf21447..95afb0460fb 100644
--- a/nixos/modules/services/misc/ethminer.nix
+++ b/nixos/modules/services/misc/ethminer.nix
@@ -71,7 +71,7 @@ in
 
       maxPower = mkOption {
         type = types.int;
-        default = 115;
+        default = 113;
         description = "Miner max watt usage.";
       };
 
@@ -92,7 +92,9 @@ in
 
       serviceConfig = {
         DynamicUser = true;
+        ExecStartPre = "${pkgs.ethminer}/bin/.ethminer-wrapped --list-devices";
         ExecStartPost = optional (cfg.toolkit == "cuda") "+${getBin config.boot.kernelPackages.nvidia_x11}/bin/nvidia-smi -pl ${toString cfg.maxPower}";
+        Restart = "always";
       };
 
       environment = {
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
index 74f4f671f46..f8c79f892da 100644
--- a/nixos/modules/services/misc/exhibitor.nix
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -410,8 +410,7 @@ in
         sed -i 's/'"$replace_what"'/'"$replace_with"'/g' ${cfg.baseDir}/zookeeper/bin/zk*.sh
       '';
     };
-    users.users = singleton {
-      name = "zookeeper";
+    users.users.zookeeper = {
       uid = config.ids.uids.zookeeper;
       description = "Zookeeper daemon user";
       home = cfg.baseDir;
diff --git a/nixos/modules/services/misc/felix.nix b/nixos/modules/services/misc/felix.nix
index 1c5ece86825..21740c8c0b7 100644
--- a/nixos/modules/services/misc/felix.nix
+++ b/nixos/modules/services/misc/felix.nix
@@ -17,10 +17,7 @@ in
 
     services.felix = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable the Apache Felix OSGi service";
-      };
+      enable = mkEnableOption "the Apache Felix OSGi service";
 
       bundles = mkOption {
         type = types.listOf types.package;
@@ -47,14 +44,10 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-    users.groups = singleton
-      { name = "osgi";
-        gid = config.ids.gids.osgi;
-      };
+    users.groups.osgi.gid = config.ids.gids.osgi;
 
-    users.users = singleton
-      { name = "osgi";
-        uid = config.ids.uids.osgi;
+    users.users.osgi =
+      { uid = config.ids.uids.osgi;
         description = "OSGi user";
         home = "/homeless-shelter";
       };
diff --git a/nixos/modules/services/misc/folding-at-home.nix b/nixos/modules/services/misc/folding-at-home.nix
deleted file mode 100644
index 122c89ce068..00000000000
--- a/nixos/modules/services/misc/folding-at-home.nix
+++ /dev/null
@@ -1,68 +0,0 @@
-{ config, lib, pkgs, ... }:
-with lib;
-let
-  stateDir = "/var/lib/foldingathome";
-  cfg = config.services.foldingAtHome;
-  fahUser = "foldingathome";
-in {
-
-  ###### interface
-
-  options = {
-
-    services.foldingAtHome = {
-
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable the Folding@Home to use idle CPU time.
-        '';
-      };
-
-      nickname = mkOption {
-        default = "Anonymous";
-        description = ''
-          A unique handle for statistics.
-        '';
-      };
-
-      config = mkOption {
-        default = "";
-        description = ''
-          Extra configuration. Contents will be added verbatim to the
-          configuration file.
-        '';
-      };
-
-    };
-
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    users.users = singleton
-      { name = fahUser;
-        uid = config.ids.uids.foldingathome;
-        description = "Folding@Home user";
-        home = stateDir;
-      };
-
-    systemd.services.foldingathome = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -m 0755 -p ${stateDir}
-        chown ${fahUser} ${stateDir}
-        cp -f ${pkgs.writeText "client.cfg" cfg.config} ${stateDir}/client.cfg
-      '';
-      script = "${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${fahUser} -c 'cd ${stateDir}; ${pkgs.foldingathome}/bin/fah6'";
-    };
-
-    services.foldingAtHome.config = ''
-        [settings]
-        username=${cfg.nickname}
-    '';
-  };
-}
diff --git a/nixos/modules/services/misc/freeswitch.nix b/nixos/modules/services/misc/freeswitch.nix
new file mode 100644
index 00000000000..0de5ba42811
--- /dev/null
+++ b/nixos/modules/services/misc/freeswitch.nix
@@ -0,0 +1,103 @@
+{ config, lib, pkgs, ...}:
+with lib;
+let
+  cfg = config.services.freeswitch;
+  pkg = cfg.package;
+  configDirectory = pkgs.runCommand "freeswitch-config-d" { } ''
+    mkdir -p $out
+    cp -rT ${cfg.configTemplate} $out
+    chmod -R +w $out
+    ${concatStringsSep "\n" (mapAttrsToList (fileName: filePath: ''
+      mkdir -p $out/$(dirname ${fileName})
+      cp ${filePath} $out/${fileName}
+    '') cfg.configDir)}
+  '';
+  configPath = if cfg.enableReload
+    then "/etc/freeswitch"
+    else configDirectory;
+in {
+  options = {
+    services.freeswitch = {
+      enable = mkEnableOption "FreeSWITCH";
+      enableReload = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Issue the <literal>reloadxml</literal> command to FreeSWITCH when configuration directory changes (instead of restart).
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Reloading">FreeSWITCH documentation</link> for more info.
+          The configuration directory is exposed at <filename>/etc/freeswitch</filename>.
+          See also <literal>systemd.services.*.restartIfChanged</literal>.
+        '';
+      };
+      configTemplate = mkOption {
+        type = types.path;
+        default = "${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
+        defaultText = literalExample "\${config.services.freeswitch.package}/share/freeswitch/conf/vanilla";
+        example = literalExample "\${config.services.freeswitch.package}/share/freeswitch/conf/minimal";
+        description = ''
+          Configuration template to use.
+          See available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+          You can also set your own configuration directory.
+        '';
+      };
+      configDir = mkOption {
+        type = with types; attrsOf path;
+        default = { };
+        example = literalExample ''
+          {
+            "freeswitch.xml" = ./freeswitch.xml;
+            "dialplan/default.xml" = pkgs.writeText "dialplan-default.xml" '''
+              [xml lines]
+            ''';
+          }
+        '';
+        description = ''
+          Override file in FreeSWITCH config template directory.
+          Each top-level attribute denotes a file path in the configuration directory, its value is the file path.
+          See <link xlink:href="https://freeswitch.org/confluence/display/FREESWITCH/Default+Configuration">FreeSWITCH documentation</link> for more info.
+          Also check available templates in <link xlink:href="https://github.com/signalwire/freeswitch/tree/master/conf">FreeSWITCH repository</link>.
+        '';
+      };
+      package = mkOption {
+        type = types.package;
+        default = pkgs.freeswitch;
+        defaultText = literalExample "pkgs.freeswitch";
+        example = literalExample "pkgs.freeswitch";
+        description = ''
+          FreeSWITCH package.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    environment.etc.freeswitch = mkIf cfg.enableReload {
+      source = configDirectory;
+    };
+    systemd.services.freeswitch-config-reload = mkIf cfg.enableReload {
+      before = [ "freeswitch.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configDirectory ];
+      serviceConfig = {
+        ExecStart = "${pkgs.systemd}/bin/systemctl try-reload-or-restart freeswitch.service";
+        RemainAfterExit = true;
+        Type = "oneshot";
+      };
+    };
+    systemd.services.freeswitch = {
+      description = "Free and open-source application server for real-time communication";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "freeswitch";
+        ExecStart = "${pkg}/bin/freeswitch -nf \\
+          -mod ${pkg}/lib/freeswitch/mod \\
+          -conf ${configPath} \\
+          -base /var/lib/freeswitch";
+        ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
+        Restart = "always";
+        RestartSec = "5s";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index c8c59fb256e..38910a5a005 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -364,7 +364,7 @@ in
           ''}
           sed -e "s,#secretkey#,$KEY,g" \
               -e "s,#dbpass#,$DBPASS,g" \
-              -e "s,#jwtsecet#,$JWTSECET,g" \
+              -e "s,#jwtsecret#,$JWTSECRET,g" \
               -e "s,#mailerpass#,$MAILERPASSWORD,g" \
               -i ${runConfig}
           chmod 640 ${runConfig} ${secretKey} ${jwtSecret}
@@ -394,6 +394,26 @@ in
         WorkingDirectory = cfg.stateDir;
         ExecStart = "${gitea.bin}/bin/gitea web";
         Restart = "always";
+
+        # Filesystem
+        ProtectHome = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        ReadWritePaths = cfg.stateDir;
+        # Caps
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        # Misc.
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
       };
 
       environment = {
@@ -453,4 +473,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
+  meta.maintainers = with lib.maintainers; [ srhb ];
 }
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 07ea9c45843..aa958985379 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -189,6 +189,11 @@ let
 
 in {
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
+    (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
+  ];
+
   options = {
     services.gitlab = {
       enable = mkOption {
@@ -628,20 +633,14 @@ in {
     # Use postfix to send out mails.
     services.postfix.enable = mkDefault true;
 
-    users.users = [
-      { name = cfg.user;
-        group = cfg.group;
+    users.users.${cfg.user} =
+      { group = cfg.group;
         home = "${cfg.statePath}/home";
         shell = "${pkgs.bash}/bin/bash";
         uid = config.ids.uids.gitlab;
-      }
-    ];
+      };
 
-    users.groups = [
-      { name = cfg.group;
-        gid = config.ids.gids.gitlab;
-      }
-    ];
+    users.groups.${cfg.group}.gid = config.ids.gids.gitlab;
 
     systemd.tmpfiles.rules = [
       "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -"
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index 3bfcb636a3c..f954249942a 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -86,17 +86,13 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton
-      { name = "gpsd";
-        inherit uid;
+    users.users.gpsd =
+      { inherit uid;
         description = "gpsd daemon user";
         home = "/var/empty";
       };
 
-    users.groups = singleton
-      { name = "gpsd";
-        inherit gid;
-      };
+    users.groups.gpsd = { inherit gid; };
 
     systemd.services.gpsd = {
       description = "GPSD daemon";
diff --git a/nixos/modules/services/misc/headphones.nix b/nixos/modules/services/misc/headphones.nix
index 4a77045be28..3ee0a4458bd 100644
--- a/nixos/modules/services/misc/headphones.nix
+++ b/nixos/modules/services/misc/headphones.nix
@@ -59,19 +59,19 @@ in
 
   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.users = optionalAttrs (cfg.user == 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;
-    });
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = config.ids.gids.headphones;
+    };
 
     systemd.services.headphones = {
         description = "Headphones Server";
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 74702c97f55..86033d02bf3 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -11,6 +11,9 @@ let
     (recursiveUpdate defaultConfig cfg.config) else cfg.config));
   configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
     ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
+    # Hack to support secrets, that are encoded as custom yaml objects,
+    # https://www.home-assistant.io/docs/configuration/secrets/
+    sed -i -e "s/'\!secret \(.*\)'/\!secret \1/" $out
   '';
 
   lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
@@ -93,11 +96,28 @@ in {
 
     config = mkOption {
       default = null;
-      type = with types; nullOr attrs;
+      # Migrate to new option types later: https://github.com/NixOS/nixpkgs/pull/75584
+      type =  with lib.types; let
+          valueType = nullOr (oneOf [
+            bool
+            int
+            float
+            str
+            (lazyAttrsOf valueType)
+            (listOf valueType)
+          ]) // {
+            description = "Yaml value";
+            emptyValue.value = {};
+          };
+        in valueType;
       example = literalExample ''
         {
           homeassistant = {
             name = "Home";
+            latitude = "!secret latitude";
+            longitude = "!secret longitude";
+            elevation = "!secret elevation";
+            unit_system = "metric";
             time_zone = "UTC";
           };
           frontend = { };
@@ -108,6 +128,8 @@ in {
       description = ''
         Your <filename>configuration.yaml</filename> as a Nix attribute set.
         Beware that setting this option will delete your previous <filename>configuration.yaml</filename>.
+        <link xlink:href="https://www.home-assistant.io/docs/configuration/secrets/">Secrets</link>
+        are encoded as strings as shown in the example.
       '';
     };
 
@@ -242,6 +264,7 @@ in {
       home = cfg.configDir;
       createHome = true;
       group = "hass";
+      extraGroups = [ "dialout" ];
       uid = config.ids.uids.hass;
     };
 
diff --git a/nixos/modules/services/misc/ihaskell.nix b/nixos/modules/services/misc/ihaskell.nix
index 11597706d0d..684a242d738 100644
--- a/nixos/modules/services/misc/ihaskell.nix
+++ b/nixos/modules/services/misc/ihaskell.nix
@@ -15,6 +15,7 @@ in
   options = {
     services.ihaskell = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = "Autostart an IHaskell notebook service.";
       };
diff --git a/nixos/modules/services/misc/mame.nix b/nixos/modules/services/misc/mame.nix
new file mode 100644
index 00000000000..c5d5e9e4837
--- /dev/null
+++ b/nixos/modules/services/misc/mame.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mame;
+  mame = "mame${lib.optionalString pkgs.stdenv.is64bit "64"}";
+in
+{
+  options = {
+    services.mame = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to setup TUN/TAP Ethernet interface for MAME emulator.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        description = ''
+          User from which you run MAME binary.
+        '';
+      };
+      hostAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the host system. Usually an address of the main network
+          adapter or the adapter through which you get an internet connection.
+        '';
+        example = "192.168.31.156";
+      };
+      emuAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the guest system. The same you set inside guest OS under
+          MAME. Should be on the same subnet as <option>services.mame.hostAddr</option>.
+        '';
+        example = "192.168.31.155";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mame ];
+
+    security.wrappers."${mame}" = {
+      source = "${pkgs.mame}/bin/${mame}";
+      capabilities = "cap_net_admin,cap_net_raw+eip";
+    };
+
+    systemd.services.mame = {
+      description = "MAME TUN/TAP Ethernet interface";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.iproute ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.mame}/bin/taputil.sh -c ${cfg.user} ${cfg.emuAddr} ${cfg.hostAddr} -";
+        ExecStop = "${pkgs.mame}/bin/taputil.sh -d ${cfg.user}";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ gnidorah ];
+}
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 50661b873f6..703bc9416f8 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -31,7 +31,6 @@ bind_host: "${cfg.bind_host}"
 ''}
 server_name: "${cfg.server_name}"
 pid_file: "/run/matrix-synapse.pid"
-web_client: ${boolToString cfg.web_client}
 ${optionalString (cfg.public_baseurl != null) ''
 public_baseurl: "${cfg.public_baseurl}"
 ''}
@@ -111,6 +110,9 @@ app_service_config_files: ${builtins.toJSON cfg.app_service_config_files}
 
 ${cfg.extraConfig}
 '';
+
+  hasLocalPostgresDB = let args = cfg.database_args; in
+    usePostgresql && (!(args ? host) || (elem args.host [ "localhost" "127.0.0.1" "::1" ]));
 in {
   options = {
     services.matrix-synapse = {
@@ -199,13 +201,6 @@ in {
           This is also the last part of your UserID.
         '';
       };
-      web_client = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to serve a web client from the HTTP/HTTPS root resource.
-        '';
-      };
       public_baseurl = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -354,13 +349,6 @@ in {
           The database engine name. Can be sqlite or psycopg2.
         '';
       };
-      create_local_database = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether to create a local database automatically.
-        '';
-      };
       database_name = mkOption {
         type = types.str;
         default = "matrix-synapse";
@@ -657,57 +645,52 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.users = [
-      { name = "matrix-synapse";
+    assertions = [
+      { assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
+        message = ''
+          Cannot deploy matrix-synapse with a configuration for a local postgresql database
+            and a missing postgresql service. Since 20.03 it's mandatory to manually configure the
+            database (please read the thread in https://github.com/NixOS/nixpkgs/pull/80447 for
+            further reference).
+
+            If you
+            - try to deploy a fresh synapse, you need to configure the database yourself. An example
+              for this can be found in <nixpkgs/nixos/tests/matrix-synapse.nix>
+            - update your existing matrix-synapse instance, you simply need to add `services.postgresql.enable = true`
+              to your configuration.
+
+          For further information about this update, please read the release-notes of 20.03 carefully.
+        '';
+      }
+    ];
+
+    users.users.matrix-synapse = { 
         group = "matrix-synapse";
         home = cfg.dataDir;
         createHome = true;
         shell = "${pkgs.bash}/bin/bash";
         uid = config.ids.uids.matrix-synapse;
-      } ];
-
-    users.groups = [
-      { name = "matrix-synapse";
-        gid = config.ids.gids.matrix-synapse;
-      } ];
+      };
 
-    services.postgresql.enable = mkIf usePostgresql (mkDefault true);
+    users.groups.matrix-synapse = {
+      gid = config.ids.gids.matrix-synapse;
+    };
 
     systemd.services.matrix-synapse = {
       description = "Synapse Matrix homeserver";
-      after = [ "network.target" "postgresql.service" ];
+      after = [ "network.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
       wantedBy = [ "multi-user.target" ];
       preStart = ''
         ${cfg.package}/bin/homeserver \
           --config-path ${configFile} \
           --keys-directory ${cfg.dataDir} \
           --generate-keys
-      '' + optionalString (usePostgresql && cfg.create_local_database) ''
-        if ! test -e "${cfg.dataDir}/db-created"; then
-          ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
-            ${pg.package}/bin/createuser \
-            --login \
-            --no-createdb \
-            --no-createrole \
-            --encrypted \
-            ${cfg.database_user}
-          ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
-            ${pg.package}/bin/createdb \
-            --owner=${cfg.database_user} \
-            --encoding=UTF8 \
-            --lc-collate=C \
-            --lc-ctype=C \
-            --template=template0 \
-            ${cfg.database_name}
-          touch "${cfg.dataDir}/db-created"
-        fi
       '';
       serviceConfig = {
         Type = "notify";
         User = "matrix-synapse";
         Group = "matrix-synapse";
         WorkingDirectory = cfg.dataDir;
-        PermissionsStartOnly = true;
         ExecStart = ''
           ${cfg.package}/bin/homeserver \
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
@@ -724,6 +707,13 @@ in {
       The `trusted_third_party_id_servers` option as been removed in `matrix-synapse` v1.4.0
       as the behavior is now obsolete.
     '')
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "create_local_database" ] ''
+      Database configuration must be done manually. An exemplary setup is demonstrated in
+      <nixpkgs/nixos/tests/matrix-synapse.nix>
+    '')
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
   ];
 
+  meta.doc = ./matrix-synapse.xml;
+
 }
diff --git a/nixos/modules/services/misc/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
new file mode 100644
index 00000000000..2f2ac27eeb9
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-synapse.xml
@@ -0,0 +1,225 @@
+<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>
+{ pkgs, ... }:
+let
+  fqdn =
+    let
+      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+    in join config.networking.hostName config.networking.domain;
+in {
+  networking = {
+    <link linkend="opt-networking.hostName">hostName</link> = "myhostname";
+    <link linkend="opt-networking.domain">domain</link> = "example.org";
+  };
+  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
+
+  <link linkend="opt-services.postgresql.enable">services.postgresql.enable</link> = true;
+  <link linkend="opt-services.postgresql.initialScript">services.postgresql.initialScript</link> = pkgs.writeText "synapse-init.sql" ''
+    CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
+    CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
+      TEMPLATE template0
+      LC_COLLATE = "C"
+      LC_CTYPE = "C";
+  '';
+
+  services.nginx = {
+    <link linkend="opt-services.nginx.enable">enable</link> = true;
+    # only recommendedProxySettings and recommendedGzipSettings are strictly required,
+    # but the rest make sense as well
+    <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
+    <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
+    <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
+
+    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
+      # 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}" = {
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/server".extraConfig</link> =
+          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}';
+          '';
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/client".extraConfig</link> =
+          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} = {
+        <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = 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.
+        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = ''
+          return 404;
+        '';
+
+        # forward all Matrix API calls to the synapse Matrix homeserver
+        locations."/_matrix" = {
+          <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">proxyPass</link> = "http://[::1]:8008"; # without a trailing /
+        };
+      };
+    };
+  };
+  services.matrix-synapse = {
+    <link linkend="opt-services.matrix-synapse.enable">enable</link> = true;
+    <link linkend="opt-services.matrix-synapse.server_name">server_name</link> = config.networking.domain;
+    <link linkend="opt-services.matrix-synapse.listeners">listeners</link> = [
+      {
+        <link linkend="opt-services.matrix-synapse.listeners._.port">port</link> = 8008;
+        <link linkend="opt-services.matrix-synapse.listeners._.bind_address">bind_address</link> = "::1";
+        <link linkend="opt-services.matrix-synapse.listeners._.type">type</link> = "http";
+        <link linkend="opt-services.matrix-synapse.listeners._.tls">tls</link> = false;
+        <link linkend="opt-services.matrix-synapse.listeners._.x_forwarded">x_forwarded</link> = true;
+        <link linkend="opt-services.matrix-synapse.listeners._.resources">resources</link> = [
+          {
+            <link linkend="opt-services.matrix-synapse.listeners._.resources._.names">names</link> = [ "client" "federation" ];
+            <link linkend="opt-services.matrix-synapse.listeners._.resources._.compress">compress</link> = 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 <literal><link linkend="opt-services.matrix-synapse.enable_registration">services.matrix-synapse.enable_registration</link> =
+   true;</literal>. Otherwise, or you can generate a registration secret with
+   <command>pwgen -s 64 1</command> and set it with
+   <option><link linkend="opt-services.matrix-synapse.registration_shared_secret">services.matrix-synapse.registration_shared_secret</link></option>. To
+   create a new user or admin, run the following after you have set the secret
+   and have rebuilt NixOS:
+<screen>
+<prompt>$ </prompt>nix run nixpkgs.matrix-synapse
+<prompt>$ </prompt>register_new_matrix_user -k <replaceable>your-registration-shared-secret</replaceable> http://localhost:8008
+<prompt>New user localpart: </prompt><replaceable>your-username</replaceable>
+<prompt>Password:</prompt>
+<prompt>Confirm password:</prompt>
+<prompt>Make admin [no]:</prompt>
+Success!
+</screen>
+   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
+   <link linkend="opt-services.matrix-synapse.registration_shared_secret">registration_shared_secret</link>
+   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}" = {
+    <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+    <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [
+      "riot.${config.networking.domain}"
+    ];
+
+    <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.riot-web.override {
+      conf = {
+        default_server_config."m.homeserver" = {
+          "base_url" = "${config.networking.domain}";
+          "server_name" = "${fqdn}";
+        };
+      };
+    };
+  };
+}
+</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/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index 107fb57fe1c..529f584a201 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -266,19 +266,19 @@ in {
       serviceConfig.User = "${cfg.user}";
     };
 
-    users.groups = optionalAttrs (cfg.group == "mediatomb") (singleton {
-      name = "mediatomb";
-      gid = gid;
-    });
+    users.groups = optionalAttrs (cfg.group == "mediatomb") {
+      mediatomb.gid = gid;
+    };
 
-    users.users = optionalAttrs (cfg.user == "mediatomb") (singleton {
-      name = "mediatomb";
-      isSystemUser = true;
-      group = cfg.group;
-      home = "${cfg.dataDir}";
-      createHome = true;
-      description = "Mediatomb DLNA Server User";
-    });
+    users.users = optionalAttrs (cfg.user == "mediatomb") {
+      mediatomb = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = "${cfg.dataDir}";
+        createHome = true;
+        description = "Mediatomb DLNA Server User";
+      };
+    };
 
     networking.firewall = {
       allowedUDPPorts = [ 1900 cfg.port ];
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index dcec4d4fc6c..0c2407e1dd2 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -12,8 +12,9 @@ let
 
   isNix23 = versionAtLeast nixVersion "2.3pre";
 
-  makeNixBuildUser = nr:
-    { name = "nixbld${toString nr}";
+  makeNixBuildUser = nr: {
+    name  = "nixbld${toString nr}";
+    value = {
       description = "Nix build user ${toString nr}";
 
       /* For consistency with the setgid(2), setuid(2), and setgroups(2)
@@ -23,8 +24,9 @@ let
       group = "nixbld";
       extraGroups = [ "nixbld" ];
     };
+  };
 
-  nixbldUsers = map makeNixBuildUser (range 1 cfg.nrBuildUsers);
+  nixbldUsers = listToAttrs (map makeNixBuildUser (range 1 cfg.nrBuildUsers));
 
   nixConf =
     assert versionAtLeast nixVersion "2.2";
@@ -68,6 +70,10 @@ let
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "nix" "useChroot" ] [ "nix" "useSandbox" ])
+    (mkRenamedOptionModule [ "nix" "chrootDirs" ] [ "nix" "sandboxPaths" ])
+  ];
 
   ###### interface
 
@@ -86,13 +92,14 @@ in
 
       maxJobs = mkOption {
         type = types.either types.int (types.enum ["auto"]);
-        default = 1;
+        default = "auto";
         example = 64;
         description = ''
-          This option defines the maximum number of jobs that Nix will try
-          to build in parallel.  The default is 1.  You should generally
-          set it to the total number of logical cores in your system (e.g., 16
-          for two CPUs with 4 cores each and hyper-threading).
+          This option defines the maximum number of jobs that Nix will try to
+          build in parallel. The default is auto, which means it will use all
+          available logical cores. It is recommend to set it to the total
+          number of logical cores in your system (e.g., 16 for two CPUs with 4
+          cores each and hyper-threading).
         '';
       };
 
@@ -276,7 +283,7 @@ in
       trustedBinaryCaches = mkOption {
         type = types.listOf types.str;
         default = [ ];
-        example = [ http://hydra.nixos.org/ ];
+        example = [ "http://hydra.nixos.org/" ];
         description = ''
           List of binary cache URLs that non-root users can use (in
           addition to those specified using
@@ -370,6 +377,59 @@ in
           If enabled (the default), checks that Nix can parse the generated nix.conf.
         '';
       };
+
+      registry = mkOption {
+        type = types.attrsOf (types.submodule (
+          let
+            inputAttrs = types.attrsOf (types.oneOf [types.str types.int types.bool types.package]);
+          in
+          { config, name, ... }:
+          { options = {
+              from = mkOption {
+                type = inputAttrs;
+                example = { type = "indirect"; id = "nixpkgs"; };
+                description = "The flake reference to be rewritten.";
+              };
+              to = mkOption {
+                type = inputAttrs;
+                example = { type = "github"; owner = "my-org"; repo = "my-nixpkgs"; };
+                description = "The flake reference to which <option>from></option> is to be rewritten.";
+              };
+              flake = mkOption {
+                type = types.unspecified;
+                default = null;
+                example = literalExample "nixpkgs";
+                description = ''
+                  The flake input to which <option>from></option> is to be rewritten.
+                '';
+              };
+              exact = mkOption {
+                type = types.bool;
+                default = true;
+                description = ''
+                  Whether the <option>from</option> reference needs to match exactly. If set,
+                  a <option>from</option> reference like <literal>nixpkgs</literal> does not
+                  match with a reference like <literal>nixpkgs/nixos-20.03</literal>.
+                '';
+              };
+            };
+            config = {
+              from = mkDefault { type = "indirect"; id = name; };
+              to = mkIf (config.flake != null)
+                ({ type = "path";
+                   path = config.flake.outPath;
+                 } // lib.filterAttrs
+                   (n: v: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash")
+                   config.flake);
+            };
+          }
+        ));
+        default = {};
+        description = ''
+          A system-wide flake registry.
+        '';
+      };
+
     };
 
   };
@@ -384,6 +444,11 @@ in
 
     environment.etc."nix/nix.conf".source = nixConf;
 
+    environment.etc."nix/registry.json".text = builtins.toJSON {
+      version = 2;
+      flakes = mapAttrsToList (n: v: { inherit (v) from to exact; }) cfg.registry;
+    };
+
     # List of machines for distributed Nix builds in the format
     # expected by build-remote.pl.
     environment.etc."nix/machines" =
@@ -441,7 +506,7 @@ in
 
     users.users = nixbldUsers;
 
-    services.xserver.displayManager.hiddenUsers = map ({ name, ... }: name) nixbldUsers;
+    services.xserver.displayManager.hiddenUsers = attrNames nixbldUsers;
 
     system.activationScripts.nix = stringAfter [ "etc" "users" ]
       ''
diff --git a/nixos/modules/services/misc/nixos-manual.nix b/nixos/modules/services/misc/nixos-manual.nix
deleted file mode 100644
index 20ba3d8ef0b..00000000000
--- a/nixos/modules/services/misc/nixos-manual.nix
+++ /dev/null
@@ -1,73 +0,0 @@
-# This module optionally starts a browser that shows the NixOS manual
-# on one of the virtual consoles which is useful for the installation
-# CD.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.nixosManual;
-  cfgd = config.documentation;
-in
-
-{
-
-  options = {
-
-    # TODO(@oxij): rename this to `.enable` eventually.
-    services.nixosManual.showManual = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether to show the NixOS manual on one of the virtual
-        consoles.
-      '';
-    };
-
-    services.nixosManual.ttyNumber = mkOption {
-      type = types.int;
-      default = 8;
-      description = ''
-        Virtual console on which to show the manual.
-      '';
-    };
-
-    services.nixosManual.browser = mkOption {
-      type = types.path;
-      default = "${pkgs.w3m-nographics}/bin/w3m";
-      description = ''
-        Browser used to show the manual.
-      '';
-    };
-
-  };
-
-
-  config = mkMerge [
-    (mkIf cfg.showManual {
-      assertions = singleton {
-        assertion = cfgd.enable && cfgd.nixos.enable;
-        message   = "Can't enable `services.nixosManual.showManual` without `documentation.nixos.enable`";
-      };
-    })
-    (mkIf (cfg.showManual && cfgd.enable && cfgd.nixos.enable) {
-      boot.extraTTYs = [ "tty${toString cfg.ttyNumber}" ];
-
-      systemd.services.nixos-manual = {
-        description = "NixOS Manual";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          ExecStart = "${cfg.browser} ${config.system.build.manual.manualHTMLIndex}";
-          StandardInput = "tty";
-          StandardOutput = "tty";
-          TTYPath = "/dev/tty${toString cfg.ttyNumber}";
-          TTYReset = true;
-          TTYVTDisallocate = true;
-          Restart = "always";
-        };
-      };
-    })
-  ];
-
-}
diff --git a/nixos/modules/services/misc/nzbget.nix b/nixos/modules/services/misc/nzbget.nix
index eb7b4c05d82..715ec891cd6 100644
--- a/nixos/modules/services/misc/nzbget.nix
+++ b/nixos/modules/services/misc/nzbget.nix
@@ -27,6 +27,12 @@ let
 
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "configFile" ] "The configuration of nzbget is now managed by users through the web interface.")
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "dataDir" ] "The data directory for nzbget is now /var/lib/nzbget.")
+    (mkRemovedOptionModule [ "services" "misc" "nzbget" "openFirewall" ] "The port used by nzbget is managed through the web interface so you should adjust your firewall rules accordingly.")
+  ];
+
   # interface
 
   options = {
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index 8950010773c..651ed374388 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -86,16 +86,16 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = optionalAttrs (cfg.user == "octoprint") (singleton
-      { name = "octoprint";
+    users.users = optionalAttrs (cfg.user == "octoprint") {
+      octoprint = {
         group = cfg.group;
         uid = config.ids.uids.octoprint;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "octoprint") (singleton
-      { name = "octoprint";
-        gid = config.ids.gids.octoprint;
-      });
+    users.groups = optionalAttrs (cfg.group == "octoprint") {
+      octoprint.gid = config.ids.gids.octoprint;
+    };
 
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 3985dc0b303..bfaf760fb83 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -123,9 +123,9 @@ in
   config = mkIf cfg.enable {
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.user} - -"
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
     ] ++ (optional cfg.consumptionDirIsPublic
-      "d '${cfg.consumptionDir}' 777 ${cfg.user} ${cfg.user} - -"
+      "d '${cfg.consumptionDir}' 777 - - - -"
       # If the consumption dir is not created here, it's automatically created by
       # 'manage' with the default permissions.
     );
@@ -169,17 +169,15 @@ in
     };
 
     users = optionalAttrs (cfg.user == defaultUser) {
-      users = [{
-        name = defaultUser;
+      users.${defaultUser} = {
         group = defaultUser;
         uid = config.ids.uids.paperless;
         home = cfg.dataDir;
-      }];
+      };
 
-      groups = [{
-        name = defaultUser;
+      groups.${defaultUser} = {
         gid = config.ids.gids.paperless;
-      }];
+      };
     };
   };
 }
diff --git a/nixos/modules/services/misc/parsoid.nix b/nixos/modules/services/misc/parsoid.nix
index c757093e5c1..09b7f977bfb 100644
--- a/nixos/modules/services/misc/parsoid.nix
+++ b/nixos/modules/services/misc/parsoid.nix
@@ -6,7 +6,7 @@ let
 
   cfg = config.services.parsoid;
 
-  parsoid = pkgs.nodePackages."parsoid-git://github.com/abbradar/parsoid#stable";
+  parsoid = pkgs.nodePackages.parsoid;
 
   confTree = {
     worker_heartbeat_timeout = 300000;
@@ -26,6 +26,10 @@ let
 
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "parsoid" "interwikis" ] "Use services.parsoid.wikis instead")
+  ];
+
   ##### interface
 
   options = {
@@ -94,8 +98,29 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       serviceConfig = {
-        User = "nobody";
         ExecStart = "${parsoid}/lib/node_modules/parsoid/bin/server.js -c ${confFile} -n ${toString cfg.workers}";
+
+        DynamicUser = true;
+        User = "parsoid";
+        Group = "parsoid";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        #MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
       };
     };
 
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index e2d1254602b..25aa27ae767 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -9,6 +9,10 @@ let
 in {
   meta.maintainers = with lib.maintainers; [ peterhoeg ];
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "pykms" "verbose" ] "Use services.pykms.logLevel instead")
+  ];
+
   options = {
     services.pykms = {
       enable = mkOption {
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index bf9a6914a48..1febdba0c8f 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -66,7 +66,7 @@ in
         type = types.package;
         default = pkgs.redmine;
         description = "Which Redmine package to use.";
-        example = "pkgs.redmine.override { ruby = pkgs.ruby_2_4; }";
+        example = "pkgs.redmine.override { ruby = pkgs.ruby_2_7; }";
       };
 
       user = mkOption {
@@ -132,7 +132,7 @@ in
         example = literalExample ''
           {
             dkuk-redmine_alex_skin = builtins.fetchurl {
-              url = https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip;
+              url = "https://bitbucket.org/dkuk/redmine_alex_skin/get/1842ef675ef3.zip";
               sha256 = "0hrin9lzyi50k4w2bd2b30vrf1i4fi1c0gyas5801wn8i7kpm9yl";
             };
           }
@@ -146,7 +146,7 @@ in
         example = literalExample ''
           {
             redmine_env_auth = builtins.fetchurl {
-              url = https://github.com/Intera/redmine_env_auth/archive/0.6.zip;
+              url = "https://github.com/Intera/redmine_env_auth/archive/0.6.zip";
               sha256 = "0yyr1yjd8gvvh832wdc8m3xfnhhxzk2pk3gm2psg5w9jdvd6skak";
             };
           }
@@ -367,17 +367,17 @@ in
 
     };
 
-    users.users = optionalAttrs (cfg.user == "redmine") (singleton
-      { name = "redmine";
+    users.users = optionalAttrs (cfg.user == "redmine") {
+      redmine = {
         group = cfg.group;
         home = cfg.stateDir;
         uid = config.ids.uids.redmine;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "redmine") (singleton
-      { name = "redmine";
-        gid = config.ids.gids.redmine;
-      });
+    users.groups = optionalAttrs (cfg.group == "redmine") {
+      redmine.gid = config.ids.gids.redmine;
+    };
 
     warnings = optional (cfg.database.password != "")
       ''config.services.redmine.database.password will be stored as plaintext
diff --git a/nixos/modules/services/misc/ripple-data-api.nix b/nixos/modules/services/misc/ripple-data-api.nix
index 042b496d35e..9fab462f7e3 100644
--- a/nixos/modules/services/misc/ripple-data-api.nix
+++ b/nixos/modules/services/misc/ripple-data-api.nix
@@ -185,9 +185,8 @@ in {
       ];
     };
 
-    users.users = singleton
-      { name = "ripple-data-api";
-        description = "Ripple data api user";
+    users.users.ripple-data-api =
+      { description = "Ripple data api user";
         uid = config.ids.uids.ripple-data-api;
       };
   };
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index cdf61730de3..ef34e3a779f 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -406,9 +406,8 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton
-      { name = "rippled";
-        description = "Ripple server user";
+    users.users.rippled =
+      { description = "Ripple server user";
         uid = config.ids.uids.rippled;
         home = cfg.databasePath;
         createHome = true;
diff --git a/nixos/modules/services/misc/rogue.nix b/nixos/modules/services/misc/rogue.nix
deleted file mode 100644
index aae02e384c9..00000000000
--- a/nixos/modules/services/misc/rogue.nix
+++ /dev/null
@@ -1,62 +0,0 @@
-# Execute the game `rogue' on tty 9.  Mostly used by the NixOS
-# installation CD.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.rogue;
-
-in
-
-{
-  ###### interface
-
-  options = {
-
-    services.rogue.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether to enable the Rogue game on one of the virtual
-        consoles.
-      '';
-    };
-
-    services.rogue.tty = mkOption {
-      type = types.str;
-      default = "tty9";
-      description = ''
-        Virtual console on which to run Rogue.
-      '';
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    boot.extraTTYs = [ cfg.tty ];
-
-    systemd.services.rogue =
-      { description = "Rogue dungeon crawling game";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig =
-          { ExecStart = "${pkgs.rogue}/bin/rogue";
-            StandardInput = "tty";
-            StandardOutput = "tty";
-            TTYPath = "/dev/${cfg.tty}";
-            TTYReset = true;
-            TTYVTDisallocate = true;
-            WorkingDirectory = "/tmp";
-            Restart = "always";
-          };
-      };
-
-  };
-
-}
diff --git a/nixos/modules/services/misc/safeeyes.nix b/nixos/modules/services/misc/safeeyes.nix
index 1a33971d922..6ecb0d13187 100644
--- a/nixos/modules/services/misc/safeeyes.nix
+++ b/nixos/modules/services/misc/safeeyes.nix
@@ -16,10 +16,7 @@ in
 
     services.safeeyes = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable the safeeyes OSGi service";
-      };
+      enable = mkEnableOption "the safeeyes OSGi service";
 
     };
 
diff --git a/nixos/modules/services/misc/serviio.nix b/nixos/modules/services/misc/serviio.nix
index 9868192724b..0ead6a81691 100644
--- a/nixos/modules/services/misc/serviio.nix
+++ b/nixos/modules/services/misc/serviio.nix
@@ -63,20 +63,15 @@ in {
       };
     };
 
-    users.users = [
-      {
-        name = "serviio";
-        group = "serviio";
+    users.users.serviio =
+      { group = "serviio";
         home = cfg.dataDir;
         description = "Serviio Media Server User";
         createHome = true;
         isSystemUser = true;
-      }
-    ];
+      };
 
-    users.groups = [
-      { name = "serviio";}
-    ];
+    users.groups.serviio = { };
 
     networking.firewall = {
       allowedTCPPorts = [
diff --git a/nixos/modules/services/misc/sickbeard.nix b/nixos/modules/services/misc/sickbeard.nix
index 5cfbbe516ae..a32dbfa3108 100644
--- a/nixos/modules/services/misc/sickbeard.nix
+++ b/nixos/modules/services/misc/sickbeard.nix
@@ -63,19 +63,19 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = optionalAttrs (cfg.user == name) (singleton {
-      name = name;
-      uid = config.ids.uids.sickbeard;
-      group = cfg.group;
-      description = "sickbeard user";
-      home = cfg.dataDir;
-      createHome = true;
-    });
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        uid = config.ids.uids.sickbeard;
+        group = cfg.group;
+        description = "sickbeard user";
+        home = cfg.dataDir;
+        createHome = true;
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == name) (singleton {
-      name = name;
-      gid = config.ids.gids.sickbeard;
-    });
+    users.groups = optionalAttrs (cfg.group == name) {
+      ${name}.gid = config.ids.gids.sickbeard;
+    };
 
     systemd.services.sickbeard = {
       description = "Sickbeard Server";
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index dcaf73aca44..ae7b27de8e7 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -161,8 +161,7 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton {
-      name = "siproxyd";
+    users.users.siproxyd = {
       uid = config.ids.uids.siproxd;
     };
 
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
index 6b64045dde8..36008d25741 100644
--- a/nixos/modules/services/misc/sssd.nix
+++ b/nixos/modules/services/misc/sssd.nix
@@ -88,9 +88,7 @@ in {
         exec ${pkgs.sssd}/bin/sss_ssh_authorizedkeys "$@"
       '';
     };
-    services.openssh.extraConfig = ''
-      AuthorizedKeysCommand /etc/ssh/authorized_keys_command
-      AuthorizedKeysCommandUser nobody
-    '';
+    services.openssh.authorizedKeysCommand = "/etc/ssh/authorized_keys_command";
+    services.openssh.authorizedKeysCommandUser = "nobody";
   })];
 }
diff --git a/nixos/modules/services/misc/svnserve.nix b/nixos/modules/services/misc/svnserve.nix
index 6292bc52b1e..3335ed09d40 100644
--- a/nixos/modules/services/misc/svnserve.nix
+++ b/nixos/modules/services/misc/svnserve.nix
@@ -18,6 +18,7 @@ in
     services.svnserve = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = "Whether to enable svnserve to serve Subversion repositories through the SVN protocol.";
       };
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
index bfab8c534d8..5b7cf3ac46c 100644
--- a/nixos/modules/services/misc/synergy.nix
+++ b/nixos/modules/services/misc/synergy.nix
@@ -19,12 +19,8 @@ in
       # !!! All these option descriptions needs to be cleaned up.
 
       client = {
-        enable = mkOption {
-          default = false;
-          description = "
-            Whether to enable the Synergy client (receive keyboard and mouse events from a Synergy server).
-          ";
-        };
+        enable = mkEnableOption "the Synergy client (receive keyboard and mouse events from a Synergy server)";
+
         screenName = mkOption {
           default = "";
           description = ''
@@ -47,12 +43,8 @@ in
       };
 
       server = {
-        enable = mkOption {
-          default = false;
-          description = ''
-            Whether to enable the Synergy server (send keyboard and mouse events).
-          '';
-        };
+        enable = mkEnableOption "the Synergy server (send keyboard and mouse events)";
+
         configFile = mkOption {
           default = "/etc/synergy-server.conf";
           description = "The Synergy server configuration file.";
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
index 8a57277fafe..a894caed1a3 100644
--- a/nixos/modules/services/misc/taskserver/default.nix
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -368,16 +368,16 @@ in {
     (mkIf cfg.enable {
       environment.systemPackages = [ nixos-taskserver ];
 
-      users.users = optional (cfg.user == "taskd") {
-        name = "taskd";
-        uid = config.ids.uids.taskd;
-        description = "Taskserver user";
-        group = cfg.group;
+      users.users = optionalAttrs (cfg.user == "taskd") {
+        taskd = {
+          uid = config.ids.uids.taskd;
+          description = "Taskserver user";
+          group = cfg.group;
+        };
       };
 
-      users.groups = optional (cfg.group == "taskd") {
-        name = "taskd";
-        gid = config.ids.gids.taskd;
+      users.groups = optionalAttrs (cfg.group == "taskd") {
+        taskd.gid = config.ids.gids.taskd;
       };
 
       services.taskserver.config = {
diff --git a/nixos/modules/services/misc/tautulli.nix b/nixos/modules/services/misc/tautulli.nix
index 50e45036647..aded33629f1 100644
--- a/nixos/modules/services/misc/tautulli.nix
+++ b/nixos/modules/services/misc/tautulli.nix
@@ -6,6 +6,10 @@ let
   cfg = config.services.tautulli;
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "plexpy" ] [ "services" "tautulli" ])
+  ];
+
   options = {
     services.tautulli = {
       enable = mkEnableOption "Tautulli Plex Monitor";
diff --git a/nixos/modules/services/misc/uhub.nix b/nixos/modules/services/misc/uhub.nix
index 753580c3e40..d1b38831028 100644
--- a/nixos/modules/services/misc/uhub.nix
+++ b/nixos/modules/services/misc/uhub.nix
@@ -41,31 +41,31 @@ in
       enable = mkOption {
         type = types.bool;
         default = false;
-	description = "Whether to enable the uhub ADC hub.";
+        description = "Whether to enable the uhub ADC hub.";
       };
 
       port = mkOption {
         type = types.int;
         default = 1511;
-	description = "TCP port to bind the hub to.";
+        description = "TCP port to bind the hub to.";
       };
 
       address = mkOption {
         type = types.str;
         default = "any";
-	description = "Address to bind the hub to.";
+        description = "Address to bind the hub to.";
       };
 
       enableTLS = mkOption {
         type = types.bool;
         default = false;
-	description = "Whether to enable TLS support.";
+        description = "Whether to enable TLS support.";
       };
 
       hubConfig = mkOption {
         type = types.lines;
         default = "";
-	description = "Contents of uhub configuration file.";
+        description = "Contents of uhub configuration file.";
       };
 
       aclConfig = mkOption {
@@ -77,11 +77,11 @@ in
       plugins = {
 
         authSqlite = {
-	  enable = mkOption {
+          enable = mkOption {
             type = types.bool;
             default = false;
             description = "Whether to enable the Sqlite authentication database plugin";
-	  };
+          };
           file = mkOption {
             type = types.path;
             example = "/var/db/uhub-users";
@@ -161,14 +161,8 @@ in
   config = mkIf cfg.enable {
 
     users = {
-      users = singleton {
-        name = "uhub";
-        uid = config.ids.uids.uhub;
-      };
-      groups = singleton {
-        name = "uhub";
-        gid = config.ids.gids.uhub;
-      };
+      users.uhub.uid = config.ids.uids.uhub;
+      groups.uhub.gid = config.ids.gids.uhub;
     };
 
     systemd.services.uhub = {
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index d7f7324580c..d5b3537068d 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -77,6 +77,8 @@ in {
         `config.services.zoneminder.database.createLocally` to true. Otherwise,
         when set to `false` (the default), you will have to create the database
         and database user as well as populate the database yourself.
+        Additionally, you will need to run `zmupdate.pl` yourself when
+        upgrading to a newer version.
       '';
 
       webserver = mkOption {
@@ -330,6 +332,8 @@ in {
             ${config.services.mysql.package}/bin/mysql < ${pkg}/share/zoneminder/db/zm_create.sql
             touch "/var/lib/${dirName}/db-created"
           fi
+
+          ${zoneminder}/bin/zmupdate.pl -nointeractive
         '';
         serviceConfig = {
           User = user;
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
index 5d91e44a199..f6af7c75eba 100644
--- a/nixos/modules/services/misc/zookeeper.nix
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -146,8 +146,7 @@ in {
       '';
     };
 
-    users.users = singleton {
-      name = "zookeeper";
+    users.users.zookeeper = {
       uid = config.ids.uids.zookeeper;
       description = "Zookeeper daemon user";
       home = cfg.dataDir;
diff --git a/nixos/modules/services/monitoring/cadvisor.nix b/nixos/modules/services/monitoring/cadvisor.nix
index 695a8c42e85..655a6934a26 100644
--- a/nixos/modules/services/monitoring/cadvisor.nix
+++ b/nixos/modules/services/monitoring/cadvisor.nix
@@ -135,7 +135,6 @@ in {
 
         serviceConfig.TimeoutStartSec=300;
       };
-      virtualisation.docker.enable = mkDefault true;
     })
   ];
 }
diff --git a/nixos/modules/services/monitoring/collectd.nix b/nixos/modules/services/monitoring/collectd.nix
index 731ac743b7c..ef3663c62e0 100644
--- a/nixos/modules/services/monitoring/collectd.nix
+++ b/nixos/modules/services/monitoring/collectd.nix
@@ -129,9 +129,10 @@ in {
       };
     };
 
-    users.users = optional (cfg.user == "collectd") {
-      name = "collectd";
-      isSystemUser = true;
+    users.users = optionalAttrs (cfg.user == "collectd") {
+      collectd = {
+        isSystemUser = true;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index 02a9f316fc3..f1cb890794e 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -22,9 +22,9 @@ let
   # Generate Datadog configuration files for each configured checks.
   # This works because check configurations have predictable paths,
   # and because JSON is a valid subset of YAML.
-  makeCheckConfigs = entries: mapAttrsToList (name: conf: {
-    source = pkgs.writeText "${name}-check-conf.yaml" (builtins.toJSON conf);
-    target = "datadog-agent/conf.d/${name}.d/conf.yaml";
+  makeCheckConfigs = entries: mapAttrs' (name: conf: {
+    name = "datadog-agent/conf.d/${name}.d/conf.yaml";
+    value.source = pkgs.writeText "${name}-check-conf.yaml" (builtins.toJSON conf);
   }) entries;
 
   defaultChecks = {
@@ -34,10 +34,11 @@ let
 
   # Assemble all check configurations and the top-level agent
   # configuration.
-  etcfiles = with pkgs; with builtins; [{
-    source = writeText "datadog.yaml" (toJSON ddConf);
-    target = "datadog-agent/datadog.yaml";
-  }] ++ makeCheckConfigs (cfg.checks // defaultChecks);
+  etcfiles = with pkgs; with builtins;
+  { "datadog-agent/datadog.yaml" = {
+      source = writeText "datadog.yaml" (toJSON ddConf);
+    };
+  } // makeCheckConfigs (cfg.checks // defaultChecks);
 
   # Apply the configured extraIntegrations to the provided agent
   # package. See the documentation of `dd-agent/integrations-core.nix`
@@ -204,7 +205,7 @@ in {
   config = mkIf cfg.enable {
     environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute ];
 
-    users.extraUsers.datadog = {
+    users.users.datadog = {
       description = "Datadog Agent User";
       uid = config.ids.uids.datadog;
       group = "datadog";
@@ -212,7 +213,7 @@ in {
       createHome = true;
     };
 
-    users.extraGroups.datadog.gid = config.ids.gids.datadog;
+    users.groups.datadog.gid = config.ids.gids.datadog;
 
     systemd.services = let
       makeService = attrs: recursiveUpdate {
@@ -224,7 +225,7 @@ in {
           Restart = "always";
           RestartSec = 2;
         };
-        restartTriggers = [ datadogPkg ] ++ map (etc: etc.source) etcfiles;
+        restartTriggers = [ datadogPkg ] ++  map (x: x.source) (attrValues etcfiles);
       } attrs;
     in {
       datadog-agent = makeService {
diff --git a/nixos/modules/services/monitoring/dd-agent/dd-agent.nix b/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
index 5ee6b092a6a..e91717fb205 100644
--- a/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
+++ b/nixos/modules/services/monitoring/dd-agent/dd-agent.nix
@@ -78,37 +78,35 @@ let
   etcfiles =
     let
       defaultConfd = import ./dd-agent-defaults.nix;
-    in (map (f: { source = "${pkgs.dd-agent}/agent/conf.d-system/${f}";
-                  target = "dd-agent/conf.d/${f}";
-                }) defaultConfd) ++ [
-      { source = ddConf;
-        target = "dd-agent/datadog.conf";
-      }
-      { source = diskConfig;
-        target = "dd-agent/conf.d/disk.yaml";
-      }
-      { source = networkConfig;
-        target = "dd-agent/conf.d/network.yaml";
-      } ] ++
-    (optional (cfg.postgresqlConfig != null)
-      { source = postgresqlConfig;
-        target = "dd-agent/conf.d/postgres.yaml";
-      }) ++
-    (optional (cfg.nginxConfig != null)
-      { source = nginxConfig;
-        target = "dd-agent/conf.d/nginx.yaml";
-      }) ++
-    (optional (cfg.mongoConfig != null)
-      { source = mongoConfig;
-        target = "dd-agent/conf.d/mongo.yaml";
-      }) ++
-    (optional (cfg.processConfig != null)
-      { source = processConfig;
-        target = "dd-agent/conf.d/process.yaml";
-      }) ++
-    (optional (cfg.jmxConfig != null)
-      { source = jmxConfig;
-        target = "dd-agent/conf.d/jmx.yaml";
+    in
+      listToAttrs (map (f: {
+        name = "dd-agent/conf.d/${f}";
+        value.source = "${pkgs.dd-agent}/agent/conf.d-system/${f}";
+      }) defaultConfd) //
+      {
+        "dd-agent/datadog.conf".source = ddConf;
+        "dd-agent/conf.d/disk.yaml".source = diskConfig;
+        "dd-agent/conf.d/network.yaml".source = networkConfig;
+      } //
+      (optionalAttrs (cfg.postgresqlConfig != null)
+      {
+        "dd-agent/conf.d/postgres.yaml".source = postgresqlConfig;
+      }) //
+      (optionalAttrs (cfg.nginxConfig != null)
+      {
+        "dd-agent/conf.d/nginx.yaml".source = nginxConfig;
+      }) //
+      (optionalAttrs (cfg.mongoConfig != null)
+      { 
+        "dd-agent/conf.d/mongo.yaml".source = mongoConfig;
+      }) //
+      (optionalAttrs (cfg.processConfig != null)
+      { 
+        "dd-agent/conf.d/process.yaml".source = processConfig;
+      }) //
+      (optionalAttrs (cfg.jmxConfig != null)
+      {
+        "dd-agent/conf.d/jmx.yaml".source = jmxConfig;
       });
 
 in {
diff --git a/nixos/modules/services/monitoring/fusion-inventory.nix b/nixos/modules/services/monitoring/fusion-inventory.nix
index fe19ed56195..9b65c76ce02 100644
--- a/nixos/modules/services/monitoring/fusion-inventory.nix
+++ b/nixos/modules/services/monitoring/fusion-inventory.nix
@@ -46,8 +46,7 @@ in {
 
   config = mkIf cfg.enable {
 
-    users.users = singleton {
-      name = "fusion-inventory";
+    users.users.fusion-inventory = {
       description = "FusionInventory user";
       isSystemUser = true;
     };
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
index f7874af3df2..64d9d61950d 100644
--- a/nixos/modules/services/monitoring/graphite.nix
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -39,8 +39,6 @@ let
     GRAPHITE_URL = cfg.seyren.graphiteUrl;
   } // cfg.seyren.extraConfig;
 
-  pagerConfig = pkgs.writeText "alarms.yaml" cfg.pager.alerts;
-
   configDir = pkgs.buildEnv {
     name = "graphite-config";
     paths = lists.filter (el: el != null) [
@@ -61,12 +59,10 @@ let
 
   carbonEnv = {
     PYTHONPATH = let
-      cenv = pkgs.python.buildEnv.override {
-        extraLibs = [ pkgs.python27Packages.carbon ];
+      cenv = pkgs.python3.buildEnv.override {
+        extraLibs = [ pkgs.python3Packages.carbon ];
       };
-      cenvPack =  "${cenv}/${pkgs.python.sitePackages}";
-    # opt/graphite/lib contains twisted.plugins.carbon-cache
-    in "${cenvPack}/opt/graphite/lib:${cenvPack}";
+    in "${cenv}/${pkgs.python3.sitePackages}";
     GRAPHITE_ROOT = dataDir;
     GRAPHITE_CONF_DIR = configDir;
     GRAPHITE_STORAGE_DIR = dataDir;
@@ -74,6 +70,10 @@ let
 
 in {
 
+  imports = [
+    (mkRemovedOptionModule ["services" "graphite" "pager"] "")
+  ];
+
   ###### interface
 
   options.services.graphite = {
@@ -132,7 +132,7 @@ in {
       finders = mkOption {
         description = "List of finder plugins to load.";
         default = [];
-        example = literalExample "[ pkgs.python27Packages.influxgraph ]";
+        example = literalExample "[ pkgs.python3Packages.influxgraph ]";
         type = types.listOf types.package;
       };
 
@@ -159,8 +159,8 @@ in {
 
       package = mkOption {
         description = "Package to use for graphite api.";
-        default = pkgs.python27Packages.graphite_api;
-        defaultText = "pkgs.python27Packages.graphite_api";
+        default = pkgs.python3Packages.graphite_api;
+        defaultText = "pkgs.python3Packages.graphite_api";
         type = types.package;
       };
 
@@ -344,49 +344,6 @@ in {
       };
     };
 
-    pager = {
-      enable = mkOption {
-        description = ''
-          Whether to enable graphite-pager service. For more information visit
-          <link xlink:href="https://github.com/seatgeek/graphite-pager"/>
-        '';
-        default = false;
-        type = types.bool;
-      };
-
-      redisUrl = mkOption {
-        description = "Redis connection string.";
-        default = "redis://localhost:${toString config.services.redis.port}/";
-        type = types.str;
-      };
-
-      graphiteUrl = mkOption {
-        description = "URL to your graphite service.";
-        default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}";
-        type = types.str;
-      };
-
-      alerts = mkOption {
-        description = "Alerts configuration for graphite-pager.";
-        default = ''
-          alerts:
-            - target: constantLine(100)
-              warning: 90
-              critical: 200
-              name: Test
-        '';
-        example = ''
-          pushbullet_key: pushbullet_api_key
-          alerts:
-            - target: stats.seatgeek.app.deal_quality.venue_info_cache.hit
-              warning: .5
-              critical: 1
-              name: Deal quality venue cache hits
-        '';
-        type = types.lines;
-      };
-    };
-
     beacon = {
       enable = mkEnableOption "graphite beacon";
 
@@ -409,7 +366,7 @@ in {
         environment = carbonEnv;
         serviceConfig = {
           RuntimeDirectory = name;
-          ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
+          ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}";
           User = "graphite";
           Group = "graphite";
           PermissionsStartOnly = true;
@@ -431,7 +388,7 @@ in {
         environment = carbonEnv;
         serviceConfig = {
           RuntimeDirectory = name;
-          ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
+          ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}";
           User = "graphite";
           Group = "graphite";
           PIDFile="/run/${name}/${name}.pid";
@@ -447,7 +404,7 @@ in {
         environment = carbonEnv;
         serviceConfig = {
           RuntimeDirectory = name;
-          ExecStart = "${pkgs.pythonPackages.twisted}/bin/twistd ${carbonOpts name}";
+          ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}";
           User = "graphite";
           Group = "graphite";
           PIDFile="/run/${name}/${name}.pid";
@@ -457,19 +414,11 @@ in {
 
     (mkIf (cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay) {
       environment.systemPackages = [
-        pkgs.pythonPackages.carbon
+        pkgs.python3Packages.carbon
       ];
     })
 
-    (mkIf cfg.web.enable (let
-      python27' = pkgs.python27.override {
-        packageOverrides = self: super: {
-          django = self.django_1_8;
-          django_tagging = self.django_tagging_0_4_3;
-        };
-      };
-      pythonPackages = python27'.pkgs;
-    in {
+    (mkIf cfg.web.enable ({
       systemd.services.graphiteWeb = {
         description = "Graphite Web Interface";
         wantedBy = [ "multi-user.target" ];
@@ -477,28 +426,27 @@ in {
         path = [ pkgs.perl ];
         environment = {
           PYTHONPATH = let
-              penv = pkgs.python.buildEnv.override {
+              penv = pkgs.python3.buildEnv.override {
                 extraLibs = [
-                  pythonPackages.graphite-web
-                  pythonPackages.pysqlite
+                  pkgs.python3Packages.graphite-web
                 ];
               };
-              penvPack = "${penv}/${pkgs.python.sitePackages}";
+              penvPack = "${penv}/${pkgs.python3.sitePackages}";
             in concatStringsSep ":" [
                  "${graphiteLocalSettingsDir}"
-                 "${penvPack}/opt/graphite/webapp"
                  "${penvPack}"
                  # explicitly adding pycairo in path because it cannot be imported via buildEnv
-                 "${pkgs.pythonPackages.pycairo}/${pkgs.python.sitePackages}"
+                 "${pkgs.python3Packages.pycairo}/${pkgs.python3.sitePackages}"
                ];
           DJANGO_SETTINGS_MODULE = "graphite.settings";
+          GRAPHITE_SETTINGS_MODULE = "graphite_local_settings";
           GRAPHITE_CONF_DIR = configDir;
           GRAPHITE_STORAGE_DIR = dataDir;
           LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
         };
         serviceConfig = {
           ExecStart = ''
-            ${pkgs.python27Packages.waitress-django}/bin/waitress-serve-django \
+            ${pkgs.python3Packages.waitress-django}/bin/waitress-serve-django \
               --host=${cfg.web.listenAddress} --port=${toString cfg.web.port}
           '';
           User = "graphite";
@@ -510,7 +458,7 @@ in {
             mkdir -p ${dataDir}/{whisper/,log/webapp/}
             chmod 0700 ${dataDir}/{whisper/,log/webapp/}
 
-            ${pkgs.pythonPackages.django_1_8}/bin/django-admin.py migrate --noinput
+            ${pkgs.python3Packages.django}/bin/django-admin.py migrate --noinput
 
             chown -R graphite:graphite ${dataDir}
 
@@ -518,16 +466,16 @@ in {
           fi
 
           # Only collect static files when graphite_web changes.
-          if ! [ "${dataDir}/current_graphite_web" -ef "${pythonPackages.graphite-web}" ]; then
+          if ! [ "${dataDir}/current_graphite_web" -ef "${pkgs.python3Packages.graphite-web}" ]; then
             mkdir -p ${staticDir}
-            ${pkgs.pythonPackages.django_1_8}/bin/django-admin.py collectstatic  --noinput --clear
+            ${pkgs.python3Packages.django}/bin/django-admin.py collectstatic  --noinput --clear
             chown -R graphite:graphite ${staticDir}
-            ln -sfT "${pythonPackages.graphite-web}" "${dataDir}/current_graphite_web"
+            ln -sfT "${pkgs.python3Packages.graphite-web}" "${dataDir}/current_graphite_web"
           fi
         '';
       };
 
-      environment.systemPackages = [ pythonPackages.graphite-web ];
+      environment.systemPackages = [ pkgs.python3Packages.graphite-web ];
     }))
 
     (mkIf cfg.api.enable {
@@ -537,16 +485,16 @@ in {
         after = [ "network.target" ];
         environment = {
           PYTHONPATH = let
-              aenv = pkgs.python.buildEnv.override {
-                extraLibs = [ cfg.api.package pkgs.cairo pkgs.pythonPackages.cffi ] ++ cfg.api.finders;
+              aenv = pkgs.python3.buildEnv.override {
+                extraLibs = [ cfg.api.package pkgs.cairo pkgs.python3Packages.cffi ] ++ cfg.api.finders;
               };
-            in "${aenv}/${pkgs.python.sitePackages}";
+            in "${aenv}/${pkgs.python3.sitePackages}";
           GRAPHITE_API_CONFIG = graphiteApiConfig;
           LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib";
         };
         serviceConfig = {
           ExecStart = ''
-            ${pkgs.python27Packages.waitress}/bin/waitress-serve \
+            ${pkgs.python3Packages.waitress}/bin/waitress-serve \
             --host=${cfg.api.listenAddress} --port=${toString cfg.api.port} \
             graphite_api.app:app
           '';
@@ -591,34 +539,13 @@ in {
       services.mongodb.enable = mkDefault true;
     })
 
-    (mkIf cfg.pager.enable {
-      systemd.services.graphitePager = {
-        description = "Graphite Pager Alerting Daemon";
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" "redis.service" ];
-        environment = {
-          REDIS_URL = cfg.pager.redisUrl;
-          GRAPHITE_URL = cfg.pager.graphiteUrl;
-        };
-        serviceConfig = {
-          ExecStart = "${pkgs.pythonPackages.graphitepager}/bin/graphite-pager --config ${pagerConfig}";
-          User = "graphite";
-          Group = "graphite";
-        };
-      };
-
-      services.redis.enable = mkDefault true;
-
-      environment.systemPackages = [ pkgs.pythonPackages.graphitepager ];
-    })
-
     (mkIf cfg.beacon.enable {
       systemd.services.graphite-beacon = {
         description = "Grpahite Beacon Alerting Daemon";
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           ExecStart = ''
-            ${pkgs.pythonPackages.graphite_beacon}/bin/graphite-beacon \
+            ${pkgs.python3Packages.graphite_beacon}/bin/graphite-beacon \
               --config=${pkgs.writeText "graphite-beacon.json" (builtins.toJSON cfg.beacon.config)}
           '';
           User = "graphite";
@@ -630,10 +557,9 @@ in {
     (mkIf (
       cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay ||
       cfg.web.enable || cfg.api.enable ||
-      cfg.seyren.enable || cfg.pager.enable || cfg.beacon.enable
+      cfg.seyren.enable || cfg.beacon.enable
      ) {
-      users.users = singleton {
-        name = "graphite";
+      users.users.graphite = {
         uid = config.ids.uids.graphite;
         description = "Graphite daemon user";
         home = dataDir;
diff --git a/nixos/modules/services/monitoring/heapster.nix b/nixos/modules/services/monitoring/heapster.nix
index 6da0831b4c5..0a9dfa12eaa 100644
--- a/nixos/modules/services/monitoring/heapster.nix
+++ b/nixos/modules/services/monitoring/heapster.nix
@@ -49,8 +49,7 @@ in {
       };
     };
 
-    users.users = singleton {
-      name = "heapster";
+    users.users.heapster = {
       uid = config.ids.uids.heapster;
       description = "Heapster user";
     };
diff --git a/nixos/modules/services/monitoring/munin.nix b/nixos/modules/services/monitoring/munin.nix
index 8af0650c738..1ebf7ee6a76 100644
--- a/nixos/modules/services/monitoring/munin.nix
+++ b/nixos/modules/services/monitoring/munin.nix
@@ -317,18 +317,16 @@ in
 
     environment.systemPackages = [ pkgs.munin ];
 
-    users.users = [{
-      name = "munin";
+    users.users.munin = {
       description = "Munin monitoring user";
       group = "munin";
       uid = config.ids.uids.munin;
       home = "/var/lib/munin";
-    }];
+    };
 
-    users.groups = [{
-      name = "munin";
+    users.groups.munin = {
       gid = config.ids.gids.munin;
-    }];
+    };
 
   }) (mkIf nodeCfg.enable {
 
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 6a3b9776946..9ac6869068f 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -8,6 +8,7 @@ let
 
   nagiosState = "/var/lib/nagios";
   nagiosLogDir = "/var/log/nagios";
+  urlPath = "/nagios";
 
   nagiosObjectDefs = cfg.objectDefs;
 
@@ -16,32 +17,39 @@ let
       preferLocalBuild = true;
     } "mkdir -p $out; ln -s $nagiosObjectDefs $out/";
 
-  nagiosCfgFile = pkgs.writeText "nagios.cfg"
-    ''
-      # Paths for state and logs.
-      log_file=${nagiosLogDir}/current
-      log_archive_path=${nagiosLogDir}/archive
-      status_file=${nagiosState}/status.dat
-      object_cache_file=${nagiosState}/objects.cache
-      temp_file=${nagiosState}/nagios.tmp
-      lock_file=/run/nagios.lock # Not used I think.
-      state_retention_file=${nagiosState}/retention.dat
-      query_socket=${nagiosState}/nagios.qh
-      check_result_path=${nagiosState}
-      command_file=${nagiosState}/nagios.cmd
-
-      # Configuration files.
-      #resource_file=resource.cfg
-      cfg_dir=${nagiosObjectDefsDir}
-
-      # Uid/gid that the daemon runs under.
-      nagios_user=nagios
-      nagios_group=nagios
-
-      # Misc. options.
-      illegal_macro_output_chars=`~$&|'"<>
-      retain_state_information=1
-    ''; # "
+  nagiosCfgFile = let
+    default = {
+      log_file="${nagiosLogDir}/current";
+      log_archive_path="${nagiosLogDir}/archive";
+      status_file="${nagiosState}/status.dat";
+      object_cache_file="${nagiosState}/objects.cache";
+      temp_file="${nagiosState}/nagios.tmp";
+      lock_file="/run/nagios.lock";
+      state_retention_file="${nagiosState}/retention.dat";
+      query_socket="${nagiosState}/nagios.qh";
+      check_result_path="${nagiosState}";
+      command_file="${nagiosState}/nagios.cmd";
+      cfg_dir="${nagiosObjectDefsDir}";
+      nagios_user="nagios";
+      nagios_group="nagios";
+      illegal_macro_output_chars="`~$&|'\"<>";
+      retain_state_information="1";
+    };
+    lines = mapAttrsToList (key: value: "${key}=${value}") (default // cfg.extraConfig);
+    content = concatStringsSep "\n" lines;
+    file = pkgs.writeText "nagios.cfg" content;
+    validated =  pkgs.runCommand "nagios-checked.cfg" {preferLocalBuild=true;} ''
+      cp ${file} nagios.cfg
+      # nagios checks the existence of /var/lib/nagios, but
+      # it does not exists in the build sandbox, so we fake it
+      mkdir lib
+      lib=$(readlink -f lib)
+      sed -i s@=${nagiosState}@=$lib@ nagios.cfg
+      ${pkgs.nagios}/bin/nagios -v nagios.cfg && cp ${file} $out
+    '';
+    defaultCfgFile = if cfg.validateConfig then validated else file;
+  in
+  if cfg.mainConfigFile == null then defaultCfgFile else cfg.mainConfigFile;
 
   # Plain configuration for the Nagios web-interface with no
   # authentication.
@@ -49,12 +57,12 @@ let
     ''
       main_config_file=${cfg.mainConfigFile}
       use_authentication=0
-      url_html_path=${cfg.urlPath}
+      url_html_path=${urlPath}
     '';
 
   extraHttpdConfig =
     ''
-      ScriptAlias ${cfg.urlPath}/cgi-bin ${pkgs.nagios}/sbin
+      ScriptAlias ${urlPath}/cgi-bin ${pkgs.nagios}/sbin
 
       <Directory "${pkgs.nagios}/sbin">
         Options ExecCGI
@@ -62,7 +70,7 @@ let
         SetEnv NAGIOS_CGI_CONFIG ${cfg.cgiConfigFile}
       </Directory>
 
-      Alias ${cfg.urlPath} ${pkgs.nagios}/share
+      Alias ${urlPath} ${pkgs.nagios}/share
 
       <Directory "${pkgs.nagios}/share">
         Options None
@@ -72,16 +80,15 @@ let
 
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "nagios" "urlPath" ] "The urlPath option has been removed as it is hard coded to /nagios in the nagios package.")
+  ];
+
+  meta.maintainers = with lib.maintainers; [ symphorien ];
+
   options = {
     services.nagios = {
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to use <link
-          xlink:href='http://www.nagios.org/'>Nagios</link> to monitor
-          your system or network.
-        ";
-      };
+      enable = mkEnableOption "<link xlink:href='http://www.nagios.org/'>Nagios</link> to monitor your system or network.";
 
       objectDefs = mkOption {
         description = "
@@ -89,12 +96,14 @@ in
           the hosts, host groups, services and contacts for the
           network that you want Nagios to monitor.
         ";
+        type = types.listOf types.path;
+        example = literalExample "[ ./objects.cfg ]";
       };
 
       plugins = mkOption {
         type = types.listOf types.package;
-        default = [pkgs.nagiosPluginsOfficial pkgs.ssmtp];
-        defaultText = "[pkgs.nagiosPluginsOfficial pkgs.ssmtp]";
+        default = with pkgs; [ nagiosPluginsOfficial ssmtp mailutils ];
+        defaultText = "[pkgs.nagiosPluginsOfficial pkgs.ssmtp pkgs.mailutils]";
         description = "
           Packages to be added to the Nagios <envar>PATH</envar>.
           Typically used to add plugins, but can be anything.
@@ -102,14 +111,29 @@ in
       };
 
       mainConfigFile = mkOption {
-        type = types.package;
-        default = nagiosCfgFile;
-        defaultText = "nagiosCfgFile";
+        type = types.nullOr types.package;
+        default = null;
         description = "
-          Derivation for the main configuration file of Nagios.
+          If non-null, overrides the main configuration file of Nagios.
         ";
       };
 
+      extraConfig = mkOption {
+        type = types.attrsOf types.str;
+        example = {
+          debug_level = "-1";
+          debug_file = "/var/log/nagios/debug.log";
+        };
+        default = {};
+        description = "Configuration to add to /etc/nagios.cfg";
+      };
+
+      validateConfig = mkOption {
+        type = types.bool;
+        default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform;
+        description = "if true, the syntax of the nagios configuration file is checked at build time";
+      };
+
       cgiConfigFile = mkOption {
         type = types.package;
         default = nagiosCGICfgFile;
@@ -121,6 +145,7 @@ in
       };
 
       enableWebInterface = mkOption {
+        type = types.bool;
         default = false;
         description = "
           Whether to enable the Nagios web interface.  You should also
@@ -128,13 +153,20 @@ in
         ";
       };
 
-      urlPath = mkOption {
-        default = "/nagios";
-        description = "
-          The URL path under which the Nagios web interface appears.
-          That is, you can access the Nagios web interface through
-          <literal>http://<replaceable>server</replaceable>/<replaceable>urlPath</replaceable></literal>.
-        ";
+      virtualHost = mkOption {
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+        example = literalExample ''
+          { hostName = "example.org";
+            adminAddr = "webmaster@example.org";
+            enableSSL = true;
+            sslServerCert = "/var/lib/acme/example.org/full.pem";
+            sslServerKey = "/var/lib/acme/example.org/key.pem";
+          }
+        '';
+        description = ''
+          Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
+          See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+        '';
       };
     };
   };
@@ -152,16 +184,12 @@ in
 
     # This isn't needed, it's just so that the user can type "nagiostats
     # -c /etc/nagios.cfg".
-    environment.etc = [
-      { source = cfg.mainConfigFile;
-        target = "nagios.cfg";
-      }
-    ];
+    environment.etc."nagios.cfg".source = nagiosCfgFile;
 
     environment.systemPackages = [ pkgs.nagios ];
     systemd.services.nagios = {
       description = "Nagios monitoring daemon";
-      path     = [ pkgs.nagios ];
+      path     = [ pkgs.nagios ] ++ cfg.plugins;
       wantedBy = [ "multi-user.target" ];
       after    = [ "network.target" ];
 
@@ -172,16 +200,13 @@ in
         RestartSec = 2;
         LogsDirectory = "nagios";
         StateDirectory = "nagios";
+        ExecStart = "${pkgs.nagios}/bin/nagios /etc/nagios.cfg";
+        X-ReloadIfChanged = nagiosCfgFile;
       };
-
-      script = ''
-        for i in ${toString cfg.plugins}; do
-          export PATH=$i/bin:$i/sbin:$i/libexec:$PATH
-        done
-        exec ${pkgs.nagios}/bin/nagios ${cfg.mainConfigFile}
-      '';
     };
 
-    services.httpd.extraConfig = optionalString cfg.enableWebInterface extraHttpdConfig;
+    services.httpd.virtualHosts = optionalAttrs cfg.enableWebInterface {
+      ${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost { extraConfig = extraHttpdConfig; } ];
+    };
   };
 }
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index 3ffde8e9bce..a5233a46e34 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -9,10 +9,12 @@ let
     mkdir -p $out/libexec/netdata/plugins.d
     ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin
     ln -s /run/wrappers/bin/freeipmi.plugin $out/libexec/netdata/plugins.d/freeipmi.plugin
+    ln -s /run/wrappers/bin/perf.plugin $out/libexec/netdata/plugins.d/perf.plugin
+    ln -s /run/wrappers/bin/slabinfo.plugin $out/libexec/netdata/plugins.d/slabinfo.plugin
   '';
 
   plugins = [
-    "${pkgs.netdata}/libexec/netdata/plugins.d"
+    "${cfg.package}/libexec/netdata/plugins.d"
     "${wrappedPlugins}/libexec/netdata/plugins.d"
   ] ++ cfg.extraPluginPaths;
 
@@ -35,6 +37,13 @@ in {
     services.netdata = {
       enable = mkEnableOption "netdata";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.netdata;
+        defaultText = "pkgs.netdata";
+        description = "Netdata package to use.";
+      };
+
       user = mkOption {
         type = types.str;
         default = "netdata";
@@ -141,10 +150,11 @@ in {
       path = (with pkgs; [ curl gawk which ]) ++ lib.optional cfg.python.enable
         (pkgs.python3.withPackages cfg.python.extraPackages);
       serviceConfig = {
-        Environment="PYTHONPATH=${pkgs.netdata}/libexec/netdata/python.d/python_modules";
-        ExecStart = "${pkgs.netdata}/bin/netdata -P /run/netdata/netdata.pid -D -c ${configFile}";
+        Environment="PYTHONPATH=${cfg.package}/libexec/netdata/python.d/python_modules";
+        ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c ${configFile}";
         ExecReload = "${pkgs.utillinux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
         TimeoutStopSec = 60;
+        Restart = "on-failure";
         # User and group
         User = cfg.user;
         Group = cfg.group;
@@ -159,7 +169,7 @@ in {
     systemd.enableCgroupAccounting = true;
 
     security.wrappers."apps.plugin" = {
-      source = "${pkgs.netdata}/libexec/netdata/plugins.d/apps.plugin.org";
+      source = "${cfg.package}/libexec/netdata/plugins.d/apps.plugin.org";
       capabilities = "cap_dac_read_search,cap_sys_ptrace+ep";
       owner = cfg.user;
       group = cfg.group;
@@ -167,25 +177,42 @@ in {
     };
 
     security.wrappers."freeipmi.plugin" = {
-      source = "${pkgs.netdata}/libexec/netdata/plugins.d/freeipmi.plugin.org";
+      source = "${cfg.package}/libexec/netdata/plugins.d/freeipmi.plugin.org";
       capabilities = "cap_dac_override,cap_fowner+ep";
       owner = cfg.user;
       group = cfg.group;
       permissions = "u+rx,g+rx,o-rwx";
     };
 
+    security.wrappers."perf.plugin" = {
+      source = "${cfg.package}/libexec/netdata/plugins.d/perf.plugin.org";
+      capabilities = "cap_sys_admin+ep";
+      owner = cfg.user;
+      group = cfg.group;
+      permissions = "u+rx,g+rx,o-rx";
+    };
+
+    security.wrappers."slabinfo.plugin" = {
+      source = "${cfg.package}/libexec/netdata/plugins.d/slabinfo.plugin.org";
+      capabilities = "cap_dac_override+ep";
+      owner = cfg.user;
+      group = cfg.group;
+      permissions = "u+rx,g+rx,o-rx";
+    };
+
     security.pam.loginLimits = [
       { domain = "netdata"; type = "soft"; item = "nofile"; value = "10000"; }
       { domain = "netdata"; type = "hard"; item = "nofile"; value = "30000"; }
     ];
 
-    users.users = optional (cfg.user == defaultUser) {
-      name = defaultUser;
-      isSystemUser = true;
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} = {
+        isSystemUser = true;
+      };
     };
 
-    users.groups = optional (cfg.group == defaultUser) {
-      name = defaultUser;
+    users.groups = optionalAttrs (cfg.group == defaultUser) {
+      ${defaultUser} = { };
     };
 
   };
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index 11d85e9c4fc..1b02ebf3704 100644
--- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -18,15 +18,26 @@ let
     in checkedConfig yml;
 
   cmdlineArgs = cfg.extraFlags ++ [
-    "--config.file ${alertmanagerYml}"
+    "--config.file /tmp/alert-manager-substituted.yaml"
     "--web.listen-address ${cfg.listenAddress}:${toString cfg.port}"
     "--log.level ${cfg.logLevel}"
+    "--storage.path /var/lib/alertmanager"
+    (toString (map (peer: "--cluster.peer ${peer}:9094") cfg.clusterPeers))
     ] ++ (optional (cfg.webExternalUrl != null)
       "--web.external-url ${cfg.webExternalUrl}"
     ) ++ (optional (cfg.logFormat != null)
       "--log.format ${cfg.logFormat}"
   );
 in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanager" "user" ] "The alertmanager service is now using systemd's DynamicUser mechanism which obviates a user setting.")
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanager" "group" ] "The alertmanager service is now using systemd's DynamicUser mechanism which obviates a group setting.")
+    (mkRemovedOptionModule [ "services" "prometheus" "alertmanagerURL" ] ''
+      Due to incompatibility, the alertmanagerURL option has been removed,
+      please use 'services.prometheus2.alertmanagers' instead.
+    '')
+  ];
+
   options = {
     services.prometheus.alertmanager = {
       enable = mkEnableOption "Prometheus Alertmanager";
@@ -111,6 +122,14 @@ in {
         '';
       };
 
+      clusterPeers = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Initial peers for HA cluster.
+        '';
+      };
+
       extraFlags = mkOption {
         type = types.listOf types.str;
         default = [];
@@ -118,6 +137,18 @@ in {
           Extra commandline options when launching the Alertmanager.
         '';
       };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/root/alertmanager.env";
+        description = ''
+          File to load as environment file. Environment variables
+          from this file will be interpolated into the config file
+          using envsubst with this syntax:
+          <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+        '';
+      };
     };
   };
 
@@ -134,10 +165,16 @@ in {
 
       systemd.services.alertmanager = {
         wantedBy = [ "multi-user.target" ];
-        after    = [ "network.target" ];
+        after    = [ "network-online.target" ];
+        preStart = ''
+           ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/tmp/alert-manager-substituted.yaml" \
+                                                    -i "${alertmanagerYml}"
+        '';
         serviceConfig = {
           Restart  = "always";
-          DynamicUser = true;
+          StateDirectory = "alertmanager";
+          DynamicUser = true; # implies PrivateTmp
+          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
           WorkingDirectory = "/tmp";
           ExecStart = "${cfg.package}/bin/alertmanager" +
             optionalString (length cmdlineArgs != 0) (" \\\n  " +
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index 191c0bff9c8..84a72afac2f 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -9,16 +9,17 @@ let
 
   # a wrapper that verifies that the configuration is valid
   promtoolCheck = what: name: file:
-    pkgs.runCommand
-      "${name}-${replaceStrings [" "] [""] what}-checked"
-      { buildInputs = [ cfg.package ]; } ''
-    ln -s ${file} $out
-    promtool ${what} $out
-  '';
+    if cfg.checkConfig then
+      pkgs.runCommandNoCCLocal
+        "${name}-${replaceStrings [" "] [""] what}-checked"
+        { buildInputs = [ cfg.package ]; } ''
+      ln -s ${file} $out
+      promtool ${what} $out
+    '' else file;
 
   # Pretty-print JSON to a file
   writePrettyJSON = name: x:
-    pkgs.runCommand name { preferLocalBuild = true; } ''
+    pkgs.runCommandNoCCLocal name {} ''
       echo '${builtins.toJSON x}' | ${pkgs.jq}/bin/jq . > $out
     '';
 
@@ -464,6 +465,11 @@ let
   };
 
 in {
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "prometheus2" ] [ "services" "prometheus" ])
+  ];
+
   options.services.prometheus = {
 
     enable = mkOption {
@@ -596,6 +602,20 @@ in {
         if Prometheus is served via a reverse proxy).
       '';
     };
+
+    checkConfig = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Check configuration with <literal>promtool
+        check</literal>. The call to <literal>promtool</literal> is
+        subject to sandboxing by Nix. When credentials are stored in
+        external files (<literal>password_file</literal>,
+        <literal>bearer_token_file</literal>, etc), they will not be
+        visible to <literal>promtool</literal> and it will report
+        errors, despite a correct configuration.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index 53f32b8fadc..f9ad1457fc8 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -29,6 +29,7 @@ let
     "fritzbox"
     "json"
     "mail"
+    "mikrotik"
     "minio"
     "nextcloud"
     "nginx"
@@ -160,6 +161,24 @@ let
   };
 in
 {
+
+  imports = (lib.forEach [ "blackboxExporter" "collectdExporter" "fritzboxExporter"
+                   "jsonExporter" "minioExporter" "nginxExporter" "nodeExporter"
+                   "snmpExporter" "unifiExporter" "varnishExporter" ]
+       (opt: lib.mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] ''
+         The prometheus exporters are now configured using `services.prometheus.exporters'.
+         See the 18.03 release notes for more information.
+       '' ))
+
+    ++ (lib.forEach [ "enable" "substitutions" "preset" ]
+       (opt: lib.mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
+         The fonts.fontconfig.ultimate module and configuration is obsolete.
+         The repository has since been archived and activity has ceased.
+         https://github.com/bohoomil/fontconfig-ultimate/issues/171.
+         No action should be needed for font configuration, as the fonts.fontconfig
+         module is already used by default.
+       '' ));
+
   options.services.prometheus.exporters = mkOption {
     type = types.submodule {
       options = (mkSubModules);
@@ -179,13 +198,25 @@ in
 
   config = mkMerge ([{
     assertions = [ {
-      assertion = (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null);
+      assertion = cfg.snmp.enable -> (
+        (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null)
+      );
       message = ''
         Please ensure you have either `services.prometheus.exporters.snmp.configuration'
           or `services.prometheus.exporters.snmp.configurationPath' set!
       '';
     } {
-      assertion = (cfg.mail.configFile == null) != (cfg.mail.configuration == {});
+      assertion = cfg.mikrotik.enable -> (
+        (cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null)
+      );
+      message = ''
+        Please specify either `services.prometheus.exporters.mikrotik.configuration'
+          or `services.prometheus.exporters.mikrotik.configFile'.
+      '';
+    } {
+      assertion = cfg.mail.enable -> (
+        (cfg.mail.configFile == null) != (cfg.mail.configuration == null)
+      );
       message = ''
         Please specify either 'services.prometheus.exporters.mail.configuration'
           or 'services.prometheus.exporters.mail.configFile'.
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix b/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix
index 8a90afa9984..fe8d905da3f 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix
@@ -61,7 +61,7 @@ in {
       ExecStart = ''
         ${pkgs.prometheus-blackbox-exporter}/bin/blackbox_exporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
-          --config.file ${adjustedConfigFile} \
+          --config.file ${escapeShellArg adjustedConfigFile} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
       ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
index 1cc34641809..97210463027 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -66,7 +66,7 @@ in
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-collectd-exporter}/bin/collectd_exporter \
-          -log.format ${cfg.logFormat} \
+          -log.format ${escapeShellArg cfg.logFormat} \
           -log.level ${cfg.logLevel} \
           -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           ${collectSettingsArgs} \
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix b/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix
index e9fa26cb1f5..68afba21d64 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix
@@ -30,7 +30,7 @@ in
         ${pkgs.prometheus-dnsmasq-exporter}/bin/dnsmasq_exporter \
           --listen ${cfg.listenAddress}:${toString cfg.port} \
           --dnsmasq ${cfg.dnsmasqListenAddress} \
-          --leases_path ${cfg.leasesPath} \
+          --leases_path ${escapeShellArg cfg.leasesPath} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
index a01074758ff..aba3533e439 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
@@ -64,7 +64,7 @@ in
         ${pkgs.prometheus-dovecot-exporter}/bin/dovecot_exporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           --web.telemetry-path ${cfg.telemetryPath} \
-          --dovecot.socket-path ${cfg.socketPath} \
+          --dovecot.socket-path ${escapeShellArg cfg.socketPath} \
           --dovecot.scopes ${concatStringsSep "," cfg.scopes} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/json.nix b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
index 82a55bafc98..bd0026b55f7 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/json.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
@@ -27,7 +27,7 @@ in
       ExecStart = ''
         ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \
           --port ${toString cfg.port} \
-          ${cfg.url} ${cfg.configFile} \
+          ${cfg.url} ${escapeShellArg cfg.configFile} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
index 7d8c6fb6140..18c5c4dd162 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
@@ -90,7 +90,7 @@ let
         Timeout until mails are considered "didn't make it".
       '';
     };
-    disableFileDelition = mkOption {
+    disableFileDeletion = mkOption {
       type = types.bool;
       default = false;
       description = ''
@@ -127,8 +127,8 @@ in
       '';
     };
     configuration = mkOption {
-      type = types.submodule exporterOptions;
-      default = {};
+      type = types.nullOr (types.submodule exporterOptions);
+      default = null;
       description = ''
         Specify the mailexporter configuration file to use.
       '';
@@ -147,8 +147,9 @@ in
       ExecStart = ''
         ${pkgs.prometheus-mail-exporter}/bin/mailexporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
           --config.file ${
-            if cfg.configuration != {} then configurationFile else cfg.configFile
+            if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configFile)
           } \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix b/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix
new file mode 100644
index 00000000000..62c2cc56847
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.mikrotik;
+in
+{
+  port = 9436;
+  extraOpts = {
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to a mikrotik exporter configuration file. Mutually exclusive with
+        <option>configuration</option> option.
+      '';
+      example = literalExample "./mikrotik.yml";
+    };
+
+    configuration = mkOption {
+      type = types.nullOr types.attrs;
+      default = null;
+      description = ''
+        Mikrotik exporter configuration as nix attribute set. Mutually exclusive with
+        <option>configFile</option> option.
+
+        See <link xlink:href="https://github.com/nshttpd/mikrotik-exporter/blob/master/README.md"/>
+        for the description of the configuration file format.
+      '';
+      example = literalExample ''
+        {
+          devices = [
+            {
+              name = "my_router";
+              address = "10.10.0.1";
+              user = "prometheus";
+              password = "changeme";
+            }
+          ];
+          features = {
+            bgp = true;
+            dhcp = true;
+            routes = true;
+            optics = true;
+          };
+        }
+      '';
+    };
+  };
+  serviceOpts = let
+    configFile = if cfg.configFile != null
+                 then cfg.configFile
+                 else "${pkgs.writeText "mikrotik-exporter.yml" (builtins.toJSON cfg.configuration)}";
+    in {
+    serviceConfig = {
+      # -port is misleading name, it actually accepts address too
+      ExecStart = ''
+        ${pkgs.prometheus-mikrotik-exporter}/bin/mikrotik-exporter \
+          -config-file=${escapeShellArg configFile} \
+          -port=${cfg.listenAddress}:${toString cfg.port} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/minio.nix b/nixos/modules/services/monitoring/prometheus/exporters/minio.nix
index ab3e3d7d5d5..d6dd62f871b 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/minio.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/minio.nix
@@ -54,8 +54,8 @@ in
         ${pkgs.prometheus-minio-exporter}/bin/minio-exporter \
           -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           -minio.server ${cfg.minioAddress} \
-          -minio.access-key ${cfg.minioAccessKey} \
-          -minio.access-secret ${cfg.minioAccessSecret} \
+          -minio.access-key ${escapeShellArg cfg.minioAccessKey} \
+          -minio.access-secret ${escapeShellArg cfg.minioAccessSecret} \
           ${optionalString cfg.minioBucketStats "-minio.bucket-stats"} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
index 5f9a52053f7..aee6bd5e66c 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
@@ -50,7 +50,7 @@ in
           -u ${cfg.username} \
           -t ${cfg.timeout} \
           -l ${cfg.url} \
-          -p @${cfg.passwordFile} \
+          -p ${escapeShellArg "@${cfg.passwordFile}"} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
index ba852fea433..56cddfc55b7 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
@@ -30,7 +30,17 @@ in
         Whether to perform certificate verification for https.
       '';
     };
-
+    constLabels = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "label1=value1"
+        "label2=value2"
+      ];
+      description = ''
+        A list of constant labels that will be used in every metric.
+      '';
+    };
   };
   serviceOpts = {
     serviceConfig = {
@@ -40,6 +50,7 @@ in
           --nginx.ssl-verify ${toString cfg.sslVerify} \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           --web.telemetry-path ${cfg.telemetryPath} \
+          --prometheus.const-labels ${concatStringsSep "," cfg.constLabels} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
index f40819e826b..3b6ef1631f8 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
@@ -67,15 +67,15 @@ in
         ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           --web.telemetry-path ${cfg.telemetryPath} \
-          --postfix.showq_path ${cfg.showqPath} \
+          --postfix.showq_path ${escapeShellArg cfg.showqPath} \
           ${concatStringsSep " \\\n  " (cfg.extraFlags
           ++ optional cfg.systemd.enable "--systemd.enable"
           ++ optional cfg.systemd.enable (if cfg.systemd.slice != null
                                           then "--systemd.slice ${cfg.systemd.slice}"
                                           else "--systemd.unit ${cfg.systemd.unit}")
           ++ optional (cfg.systemd.enable && (cfg.systemd.journalPath != null))
-                       "--systemd.jounal_path ${cfg.systemd.journalPath}"
-          ++ optional (!cfg.systemd.enable) "--postfix.logfile_path ${cfg.logfilePath}")}
+                       "--systemd.journal_path ${escapeShellArg cfg.systemd.journalPath}"
+          ++ optional (!cfg.systemd.enable) "--postfix.logfile_path ${escapeShellArg cfg.logfilePath}")}
       '';
     };
   };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix b/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix
index fe7ae8a8ac9..045e48a3d0f 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix
@@ -19,7 +19,7 @@ in
 
     configuration = mkOption {
       type = types.nullOr types.attrs;
-      default = {};
+      default = null;
       description = ''
         Snmp exporter configuration as nix attribute set. Mutually exclusive with 'configurationPath' option.
       '';
@@ -36,15 +36,15 @@ in
     };
 
     logFormat = mkOption {
-      type = types.str;
-      default = "logger:stderr";
+      type = types.enum ["logfmt" "json"];
+      default = "logfmt";
       description = ''
-        Set the log target and format.
+        Output format of log messages.
       '';
     };
 
     logLevel = mkOption {
-      type = types.enum ["debug" "info" "warn" "error" "fatal"];
+      type = types.enum ["debug" "info" "warn" "error"];
       default = "info";
       description = ''
         Only log messages with the given severity or above.
@@ -54,13 +54,13 @@ in
   serviceOpts = let
     configFile = if cfg.configurationPath != null
                  then cfg.configurationPath
-                 else "${pkgs.writeText "snmp-eporter-conf.yml" (builtins.toJSON cfg.configuration)}";
+                 else "${pkgs.writeText "snmp-exporter-conf.yml" (builtins.toJSON cfg.configuration)}";
     in {
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-snmp-exporter.bin}/bin/snmp_exporter \
-          --config.file=${configFile} \
-          --log.format=${cfg.logFormat} \
+          --config.file=${escapeShellArg configFile} \
+          --log.format=${escapeShellArg cfg.logFormat} \
           --log.level=${cfg.logLevel} \
           --web.listen-address=${cfg.listenAddress}:${toString cfg.port} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix
index 9aa0f1b85aa..8d0e8764001 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix
@@ -55,8 +55,8 @@ in
         ${pkgs.prometheus-unifi-exporter}/bin/unifi_exporter \
           -telemetry.addr ${cfg.listenAddress}:${toString cfg.port} \
           -unifi.addr ${cfg.unifiAddress} \
-          -unifi.username ${cfg.unifiUsername} \
-          -unifi.password ${cfg.unifiPassword} \
+          -unifi.username ${escapeShellArg cfg.unifiUsername} \
+          -unifi.password ${escapeShellArg cfg.unifiPassword} \
           -unifi.timeout ${cfg.unifiTimeout} \
           ${optionalString cfg.unifiInsecure "-unifi.insecure" } \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix b/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
index 12153fa021e..5b5a6e18fcd 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix
@@ -74,10 +74,10 @@ in
         ${pkgs.prometheus-varnish-exporter}/bin/prometheus_varnish_exporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           --web.telemetry-path ${cfg.telemetryPath} \
-          --varnishstat-path ${cfg.varnishStatPath} \
+          --varnishstat-path ${escapeShellArg cfg.varnishStatPath} \
           ${concatStringsSep " \\\n  " (cfg.extraFlags
             ++ optional (cfg.healthPath != null) "--web.health-path ${cfg.healthPath}"
-            ++ optional (cfg.instance != null) "-n ${cfg.instance}"
+            ++ optional (cfg.instance != null) "-n ${escapeShellArg cfg.instance}"
             ++ optional cfg.noExit "--no-exit"
             ++ optional cfg.withGoMetrics "--with-go-metrics"
             ++ optional cfg.verbose "--verbose"
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
index 374f83a2939..04421fc2d25 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix
@@ -59,7 +59,7 @@ in {
           ${optionalString cfg.verbose "-v"} \
           ${optionalString cfg.singleSubnetPerField "-s"} \
           ${optionalString cfg.withRemoteIp "-r"} \
-          ${optionalString (cfg.wireguardConfig != null) "-n ${cfg.wireguardConfig}"}
+          ${optionalString (cfg.wireguardConfig != null) "-n ${escapeShellArg cfg.wireguardConfig}"}
       '';
     };
   };
diff --git a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
new file mode 100644
index 00000000000..44b15cb2034
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.xmpp-alerts;
+
+  configFile = pkgs.writeText "prometheus-xmpp-alerts.yml" (builtins.toJSON cfg.configuration);
+
+in
+
+{
+  options.services.prometheus.xmpp-alerts = {
+
+    enable = mkEnableOption "XMPP Web hook service for Alertmanager";
+
+    configuration = mkOption {
+      type = types.attrs;
+      description = "Configuration as attribute set which will be converted to YAML";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.prometheus-xmpp-alerts = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.prometheus-xmpp-alerts}/bin/prometheus-xmpp-alerts --config ${configFile}";
+        Restart = "on-failure";
+        DynamicUser = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        NoNewPrivileges = true;
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        SystemCallFilter = [ "@system-service" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/statsd.nix b/nixos/modules/services/monitoring/statsd.nix
index ea155821ecc..30b2916a992 100644
--- a/nixos/modules/services/monitoring/statsd.nix
+++ b/nixos/modules/services/monitoring/statsd.nix
@@ -125,8 +125,7 @@ in
       message = "Only builtin backends (graphite, console, repeater) or backends enumerated in `pkgs.nodePackages` are allowed!";
     }) cfg.backends;
 
-    users.users = singleton {
-      name = "statsd";
+    users.users.statsd = {
       uid = config.ids.uids.statsd;
       description = "Statsd daemon user";
     };
diff --git a/nixos/modules/services/monitoring/sysstat.nix b/nixos/modules/services/monitoring/sysstat.nix
index d668faa53cc..ca2cff82723 100644
--- a/nixos/modules/services/monitoring/sysstat.nix
+++ b/nixos/modules/services/monitoring/sysstat.nix
@@ -5,15 +5,10 @@ let
 in {
   options = {
     services.sysstat = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable sar system activity collection.
-        '';
-      };
+      enable = mkEnableOption "sar system activity collection";
 
       collect-frequency = mkOption {
+        type = types.str;
         default = "*:00/10";
         description = ''
           OnCalendar specification for sysstat-collect
@@ -21,6 +16,7 @@ in {
       };
 
       collect-args = mkOption {
+        type = types.str;
         default = "1 1";
         description = ''
           Arguments to pass sa1 when collecting statistics
@@ -33,13 +29,13 @@ in {
     systemd.services.sysstat = {
       description = "Resets System Activity Logs";
       wantedBy = [ "multi-user.target" ];
-      preStart = "test -d /var/log/sa || mkdir -p /var/log/sa";
 
       serviceConfig = {
         User = "root";
         RemainAfterExit = true;
         Type = "oneshot";
         ExecStart = "${pkgs.sysstat}/lib/sa/sa1 --boot";
+        LogsDirectory = "sa";
       };
     };
 
diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix
index d8786732668..5d131557e8b 100644
--- a/nixos/modules/services/monitoring/telegraf.nix
+++ b/nixos/modules/services/monitoring/telegraf.nix
@@ -63,10 +63,9 @@ in {
       };
     };
 
-    users.users = [{
-      name = "telegraf";
+    users.users.telegraf = {
       uid = config.ids.uids.telegraf;
       description = "telegraf daemon user";
-    }];
+    };
   };
 }
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
new file mode 100644
index 00000000000..731260a5c20
--- /dev/null
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.tuptime;
+
+in {
+
+  options.services.tuptime = {
+
+    enable = mkEnableOption "the total uptime service";
+
+    timer = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to regularly log uptime to detect bad shutdowns.";
+      };
+
+      period = mkOption {
+        type = types.str;
+        default = "*:0/5";
+        description = "systemd calendar event";
+      };
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.tuptime ];
+
+    users.users.tuptime.description = "tuptime database owner";
+
+    systemd = {
+      services = {
+
+        tuptime = {
+          description = "the total uptime service";
+          documentation = [ "man:tuptime(1)" ];
+          after = [ "time-sync.target" ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            StateDirectory = "tuptime";
+            Type = "oneshot";
+            User = "tuptime";
+            RemainAfterExit = true;
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+            ExecStop = "${pkgs.tuptime}/bin/tuptime -xg";
+          };
+        };
+
+        tuptime-oneshot = mkIf cfg.timer.enable {
+          description = "the tuptime scheduled execution unit";
+          serviceConfig = {
+            StateDirectory = "tuptime";
+            Type = "oneshot";
+            User = "tuptime";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+          };
+        };
+      };
+
+      timers.tuptime = mkIf cfg.timer.enable {
+        description = "the tuptime scheduled execution timer";
+        # this timer should be started if the service is started
+        # even if the timer was previously stopped
+        wantedBy = [ "tuptime.service" "timers.target" ];
+        # this timer should be stopped if the service is stopped
+        partOf = [ "tuptime.service" ];
+        timerConfig = {
+          OnBootSec = "1min";
+          OnCalendar = cfg.timer.period;
+          Unit = "tuptime-oneshot.service";
+        };
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.evils ];
+
+}
diff --git a/nixos/modules/services/monitoring/ups.nix b/nixos/modules/services/monitoring/ups.nix
index 1bdc4e4410f..a45e806d4ad 100644
--- a/nixos/modules/services/monitoring/ups.nix
+++ b/nixos/modules/services/monitoring/ups.nix
@@ -214,14 +214,12 @@ in
       environment.NUT_STATEPATH = "/var/lib/nut/";
     };
 
-    environment.etc = [
-      { source = pkgs.writeText "nut.conf"
+    environment.etc = {
+      "nut/nut.conf".source = pkgs.writeText "nut.conf"
         ''
           MODE = ${cfg.mode}
         '';
-        target = "nut/nut.conf";
-      }
-      { source = pkgs.writeText "ups.conf"
+      "nut/ups.conf".source = pkgs.writeText "ups.conf"
         ''
           maxstartdelay = ${toString cfg.maxStartDelay}
 
@@ -229,25 +227,15 @@ in
 
           "}
         '';
-        target = "nut/ups.conf";
-      }
-      { source = cfg.schedulerRules;
-        target = "nut/upssched.conf";
-      }
+      "nut/upssched.conf".source = cfg.schedulerRules;
       # These file are containing private informations and thus should not
       # be stored inside the Nix store.
       /*
-      { source = ;
-        target = "nut/upsd.conf";
-      }
-      { source = ;
-        target = "nut/upsd.users";
-      }
-      { source = ;
-        target = "nut/upsmon.conf;
-      }
+      "nut/upsd.conf".source = "";
+      "nut/upsd.users".source = "";
+      "nut/upsmon.conf".source = "";
       */
-    ];
+    };
 
     power.ups.schedulerRules = mkDefault "${pkgs.nut}/etc/upssched.conf.sample";
 
@@ -259,21 +247,16 @@ in
 
 
 /*
-    users.users = [
-      { name = "nut";
-        uid = 84;
+    users.users.nut =
+      { uid = 84;
         home = "/var/lib/nut";
         createHome = true;
         group = "nut";
         description = "UPnP A/V Media Server user";
-      }
-    ];
-
-    users.groups = [
-      { name = "nut";
-        gid = 84;
-      }
-    ];
+      };
+
+    users.groups."nut" =
+      { gid = 84; };
 */
 
   };
diff --git a/nixos/modules/services/monitoring/zabbix-server.nix b/nixos/modules/services/monitoring/zabbix-server.nix
index e9f1590760a..b4e4378ce1e 100644
--- a/nixos/modules/services/monitoring/zabbix-server.nix
+++ b/nixos/modules/services/monitoring/zabbix-server.nix
@@ -44,6 +44,11 @@ let
 in
 
 {
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "zabbixServer" "dbServer" ] [ "services" "zabbixServer" "database" "host" ])
+    (lib.mkRemovedOptionModule [ "services" "zabbixServer" "dbPassword" ] "Use services.zabbixServer.database.passwordFile instead.")
+  ];
+
   # interface
 
   options = {
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index 543a7b25d5d..d17959a6a30 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -371,15 +371,14 @@ in
       in
         generators.toINI {} totalConfig;
 
-    users.users = singleton {
-      name = "ceph";
+    users.users.ceph = {
       uid = config.ids.uids.ceph;
       description = "Ceph daemon user";
       group = "ceph";
       extraGroups = [ "disk" ];
     };
-    users.groups = singleton {
-      name = "ceph";
+
+    users.groups.ceph = {
       gid = config.ids.gids.ceph;
     };
 
diff --git a/nixos/modules/services/network-filesystems/davfs2.nix b/nixos/modules/services/network-filesystems/davfs2.nix
index 100d458d536..4b6f85e4a2c 100644
--- a/nixos/modules/services/network-filesystems/davfs2.nix
+++ b/nixos/modules/services/network-filesystems/davfs2.nix
@@ -57,18 +57,19 @@ in
     environment.systemPackages = [ pkgs.davfs2 ];
     environment.etc."davfs2/davfs2.conf".source = cfgFile;
 
-    users.groups = optionalAttrs (cfg.davGroup == "davfs2") (singleton {
-      name = "davfs2";
-      gid = config.ids.gids.davfs2;
-    });
+    users.groups = optionalAttrs (cfg.davGroup == "davfs2") {
+      davfs2.gid = config.ids.gids.davfs2;
+    };
+
+    users.users = optionalAttrs (cfg.davUser == "davfs2") {
+      davfs2 = {
+        createHome = false;
+        group = cfg.davGroup;
+        uid = config.ids.uids.davfs2;
+        description = "davfs2 user";
+      };
+    };
 
-    users.users = optionalAttrs (cfg.davUser == "davfs2") (singleton {
-      name = "davfs2";
-      createHome = false;
-      group = cfg.davGroup;
-      uid = config.ids.uids.davfs2;
-      description = "davfs2 user";
-    });
   };
 
 }
diff --git a/nixos/modules/services/network-filesystems/drbd.nix b/nixos/modules/services/network-filesystems/drbd.nix
index 4ab74ed8e1c..916e7eaaaa9 100644
--- a/nixos/modules/services/network-filesystems/drbd.nix
+++ b/nixos/modules/services/network-filesystems/drbd.nix
@@ -47,10 +47,8 @@ let cfg = config.services.drbd; in
         options drbd usermode_helper=/run/current-system/sw/bin/drbdadm
       '';
 
-    environment.etc = singleton
-      { source = pkgs.writeText "drbd.conf" cfg.config;
-        target = "drbd.conf";
-      };
+    environment.etc.drbd.conf =
+      { source = pkgs.writeText "drbd.conf" cfg.config; };
 
     systemd.services.drbd = {
       after = [ "systemd-udev.settle.service" "network.target" ];
diff --git a/nixos/modules/services/network-filesystems/kbfs.nix b/nixos/modules/services/network-filesystems/kbfs.nix
index 263b70d04a5..a43ac656f66 100644
--- a/nixos/modules/services/network-filesystems/kbfs.nix
+++ b/nixos/modules/services/network-filesystems/kbfs.nix
@@ -1,6 +1,7 @@
 { config, lib, pkgs, ... }:
 with lib;
 let
+  inherit (config.security) wrapperDir;
   cfg = config.services.kbfs;
 
 in {
@@ -17,6 +18,16 @@ in {
         description = "Whether to mount the Keybase filesystem.";
       };
 
+      enableRedirector = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the Keybase root redirector service, allowing
+          any user to access KBFS files via <literal>/keybase</literal>,
+          which will show different contents depending on the requester.
+        '';
+      };
+
       mountPoint = mkOption {
         type = types.str;
         default = "%h/keybase";
@@ -41,26 +52,67 @@ in {
 
   ###### implementation
 
-  config = mkIf cfg.enable {
-
-    systemd.user.services.kbfs = {
-      description = "Keybase File System";
-      requires = [ "keybase.service" ];
-      after = [ "keybase.service" ];
-      path = [ "/run/wrappers" ];
-      unitConfig.ConditionUser = "!@system";
-      serviceConfig = {
-        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${cfg.mountPoint}";
-        ExecStart = "${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} ${cfg.mountPoint}";
-        ExecStopPost = "/run/wrappers/bin/fusermount -u ${cfg.mountPoint}";
-        Restart = "on-failure";
-        PrivateTmp = true;
+  config = mkIf cfg.enable (mkMerge [
+    {
+      # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/kbfs.service
+      systemd.user.services.kbfs = {
+        description = "Keybase File System";
+
+        # Note that the "Requires" directive will cause a unit to be restarted whenever its dependency is restarted.
+        # Do not issue a hard dependency on keybase, because kbfs can reconnect to a restarted service.
+        # Do not issue a hard dependency on keybase-redirector, because it's ok if it fails (e.g., if it is disabled).
+        wants = [ "keybase.service" ] ++ optional cfg.enableRedirector "keybase-redirector.service";
+        path = [ "/run/wrappers" ];
+        unitConfig.ConditionUser = "!@system";
+
+        serviceConfig = {
+          Type = "notify";
+          # Keybase notifies from a forked process
+          EnvironmentFile = [
+            "-%E/keybase/keybase.autogen.env"
+            "-%E/keybase/keybase.env"
+          ];
+          ExecStartPre = [
+            "${pkgs.coreutils}/bin/mkdir -p \"${cfg.mountPoint}\""
+            "-${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\""
+          ];
+          ExecStart = "${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} \"${cfg.mountPoint}\"";
+          ExecStop = "${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\"";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+        wantedBy = [ "default.target" ];
       };
-      wantedBy = [ "default.target" ];
-    };
 
-    services.keybase.enable = true;
+      services.keybase.enable = true;
 
-    environment.systemPackages = [ pkgs.kbfs ];
-  };
+      environment.systemPackages = [ pkgs.kbfs ];
+    }
+
+    (mkIf cfg.enableRedirector {
+      security.wrappers."keybase-redirector".source = "${pkgs.kbfs}/bin/redirector";
+
+      systemd.tmpfiles.rules = [ "d /keybase 0755 root root 0" ];
+
+      # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase-redirector.service
+      systemd.user.services.keybase-redirector = {
+        description = "Keybase Root Redirector for KBFS";
+        wants = [ "keybase.service" ];
+        unitConfig.ConditionUser = "!@system";
+
+        serviceConfig = {
+          EnvironmentFile = [
+            "-%E/keybase/keybase.autogen.env"
+            "-%E/keybase/keybase.env"
+          ];
+          # Note: The /keybase mount point is not currently configurable upstream.
+          ExecStart = "${wrapperDir}/keybase-redirector /keybase";
+          Restart = "on-failure";
+          PrivateTmp = true;
+        };
+
+        wantedBy = [ "default.target" ];
+      };
+    })
+  ]);
 }
diff --git a/nixos/modules/services/network-filesystems/netatalk.nix b/nixos/modules/services/network-filesystems/netatalk.nix
index 1dd869043f0..7674c8f7fa8 100644
--- a/nixos/modules/services/network-filesystems/netatalk.nix
+++ b/nixos/modules/services/network-filesystems/netatalk.nix
@@ -43,10 +43,7 @@ in
   options = {
     services.netatalk = {
 
-      enable = mkOption {
-          default = false;
-          description = "Whether to enable the Netatalk AFP fileserver.";
-        };
+      enable = mkEnableOption "the Netatalk AFP fileserver";
 
       port = mkOption {
         default = 548;
@@ -65,6 +62,7 @@ in
 
       homes = {
         enable = mkOption {
+          type = types.bool;
           default = false;
           description = "Enable sharing of the UNIX server user home directories.";
         };
@@ -98,13 +96,14 @@ in
             Set of AFP volumes to export.
             See <literal>man apf.conf</literal> for more information.
           '';
-        example =
+        example = literalExample ''
           { srv =
              { path = "/srv";
                "read only" = true;
                "hosts allow" = "10.1.0.0/16 10.2.1.100 2001:0db8:1234::/48";
              };
-          };
+          }
+        '';
       };
 
       extmap = mkOption {
diff --git a/nixos/modules/services/network-filesystems/nfsd.nix b/nixos/modules/services/network-filesystems/nfsd.nix
index 1a78f9a76a3..1b62bfa8203 100644
--- a/nixos/modules/services/network-filesystems/nfsd.nix
+++ b/nixos/modules/services/network-filesystems/nfsd.nix
@@ -11,6 +11,10 @@ let
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "nfs" "lockdPort" ] [ "services" "nfs" "server" "lockdPort" ])
+    (mkRenamedOptionModule [ "services" "nfs" "statdPort" ] [ "services" "nfs" "server" "statdPort" ])
+  ];
 
   ###### interface
 
diff --git a/nixos/modules/services/network-filesystems/openafs/client.nix b/nixos/modules/services/network-filesystems/openafs/client.nix
index 79c4b7aee06..677111814a0 100644
--- a/nixos/modules/services/network-filesystems/openafs/client.nix
+++ b/nixos/modules/services/network-filesystems/openafs/client.nix
@@ -9,7 +9,7 @@ let
   cfg = config.services.openafsClient;
 
   cellServDB = pkgs.fetchurl {
-    url = http://dl.central.org/dl/cellservdb/CellServDB.2018-05-14;
+    url = "http://dl.central.org/dl/cellservdb/CellServDB.2018-05-14";
     sha256 = "1wmjn6mmyy2r8p10nlbdzs4nrqxy8a9pjyrdciy5nmppg4053rk2";
   };
 
diff --git a/nixos/modules/services/network-filesystems/rsyncd.nix b/nixos/modules/services/network-filesystems/rsyncd.nix
index b17ec3aa930..fa29e18a939 100644
--- a/nixos/modules/services/network-filesystems/rsyncd.nix
+++ b/nixos/modules/services/network-filesystems/rsyncd.nix
@@ -29,10 +29,7 @@ in
   options = {
     services.rsyncd = {
 
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable the rsync daemon.";
-      };
+      enable = mkEnableOption "the rsync daemon";
 
       motd = mkOption {
         type = types.str;
@@ -74,13 +71,14 @@ in
             See <command>man rsyncd.conf</command> for options.
           '';
         type = types.attrsOf (types.attrsOf types.str);
-        example =
+        example = literalExample ''
           { srv =
              { path = "/srv";
                "read only" = "yes";
                comment = "Public rsync share.";
              };
-          };
+          }
+        '';
       };
 
       user = mkOption {
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index 875ab70bfc7..a115590ccaa 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -65,6 +65,9 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "samba" "defaultShare" ] "")
+  ];
 
   ###### interface
 
@@ -186,7 +189,7 @@ in
           See <command>man smb.conf</command> for options.
         '';
         type = types.attrsOf (types.attrsOf types.unspecified);
-        example =
+        example = literalExample ''
           { public =
             { path = "/srv/public";
               "read only" = true;
@@ -194,7 +197,8 @@ in
               "guest ok" = "yes";
               comment = "Public samba share.";
             };
-          };
+          }
+        '';
       };
 
     };
diff --git a/nixos/modules/services/network-filesystems/xtreemfs.nix b/nixos/modules/services/network-filesystems/xtreemfs.nix
index c93e201da56..b8f8c1d7117 100644
--- a/nixos/modules/services/network-filesystems/xtreemfs.nix
+++ b/nixos/modules/services/network-filesystems/xtreemfs.nix
@@ -100,11 +100,13 @@ in
 
       dir = {
         enable = mkOption {
+          type = types.bool;
           default = true;
           description = ''
             Whether to enable XtreemFS DIR service.
           '';
         };
+
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e40";
           description = ''
@@ -218,11 +220,13 @@ in
 
       mrc = {
         enable = mkOption {
+          type = types.bool;
           default = true;
           description = ''
             Whether to enable XtreemFS MRC service.
           '';
         };
+
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e41";
           description = ''
@@ -354,11 +358,13 @@ in
 
       osd = {
         enable = mkOption {
+          type = types.bool;
           default = true;
           description = ''
             Whether to enable XtreemFS OSD service.
           '';
         };
+
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e42";
           description = ''
diff --git a/nixos/modules/services/network-filesystems/yandex-disk.nix b/nixos/modules/services/network-filesystems/yandex-disk.nix
index 0aa01ef9e6d..cc73f13bf77 100644
--- a/nixos/modules/services/network-filesystems/yandex-disk.nix
+++ b/nixos/modules/services/network-filesystems/yandex-disk.nix
@@ -21,6 +21,7 @@ in
     services.yandex-disk = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = "
           Whether to enable Yandex-disk client. See https://disk.yandex.ru/
diff --git a/nixos/modules/services/networking/3proxy.nix b/nixos/modules/services/networking/3proxy.nix
new file mode 100644
index 00000000000..ae8a4958ca9
--- /dev/null
+++ b/nixos/modules/services/networking/3proxy.nix
@@ -0,0 +1,426 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  pkg = pkgs._3proxy;
+  cfg = config.services._3proxy;
+  optionalList = list: if list == [ ] then "*" else concatMapStringsSep "," toString list;
+in {
+  options.services._3proxy = {
+    enable = mkEnableOption "3proxy";
+    confFile = mkOption {
+      type = types.path;
+      example = "/var/lib/3proxy/3proxy.conf";
+      description = ''
+        Ignore all other 3proxy options and load configuration from this file.
+      '';
+    };
+    usersFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/lib/3proxy/3proxy.passwd";
+      description = ''
+        Load users and passwords from this file.
+
+        Example users file with plain-text passwords:
+
+        <literal>
+          test1:CL:password1
+          test2:CL:password2
+        </literal>
+
+        Example users file with md5-crypted passwords:
+
+        <literal>
+          test1:CR:$1$tFkisVd2$1GA8JXkRmTXdLDytM/i3a1
+          test2:CR:$1$rkpibm5J$Aq1.9VtYAn0JrqZ8M.1ME.
+        </literal>
+
+        You can generate md5-crypted passwords via https://unix4lyfe.org/crypt/
+        Note that htpasswd tool generates incompatible md5-crypted passwords.
+        Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/How-To-(incomplete)#USERS">documentation</link> for more information.
+      '';
+    };
+    services = mkOption {
+      type = types.listOf (types.submodule {
+        options = {
+          type = mkOption {
+            type = types.enum [
+              "proxy"
+              "socks"
+              "pop3p"
+              "ftppr"
+              "admin"
+              "dnspr"
+              "tcppm"
+              "udppm"
+            ];
+            example = "proxy";
+            description = ''
+              Service type. The following values are valid:
+
+              <itemizedlist>
+                <listitem><para>
+                  <literal>"proxy"</literal>: HTTP/HTTPS proxy (default port 3128).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"socks"</literal>: SOCKS 4/4.5/5 proxy (default port 1080).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"pop3p"</literal>: POP3 proxy (default port 110).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"ftppr"</literal>: FTP proxy (default port 21).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"admin"</literal>: Web interface (default port 80).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"dnspr"</literal>: Caching DNS proxy (default port 53).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"tcppm"</literal>: TCP portmapper.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"udppm"</literal>: UDP portmapper.
+                </para></listitem>
+              </itemizedlist>
+            '';
+          };
+          bindAddress = mkOption {
+            type = types.str;
+            default = "[::]";
+            example = "127.0.0.1";
+            description = ''
+              Address used for service.
+            '';
+          };
+          bindPort = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            example = 3128;
+            description = ''
+              Override default port used for service.
+            '';
+          };
+          maxConnections = mkOption {
+            type = types.int;
+            default = 100;
+            example = 1000;
+            description = ''
+              Maximum number of simulationeous connections to this service.
+            '';
+          };
+          auth = mkOption {
+            type = types.listOf (types.enum [ "none" "iponly" "strong" ]);
+            example = [ "iponly" "strong" ];
+            description = ''
+              Authentication type. The following values are valid:
+
+              <itemizedlist>
+                <listitem><para>
+                  <literal>"none"</literal>: disables both authentication and authorization. You can not use ACLs.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"iponly"</literal>: specifies no authentication. ACLs authorization is used.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"strong"</literal>: authentication by username/password. If user is not registered his access is denied regardless of ACLs.
+                </para></listitem>
+              </itemizedlist>
+
+              Double authentication is possible, e.g.
+
+              <literal>
+                {
+                  auth = [ "iponly" "strong" ];
+                  acl = [
+                    {
+                      rule = "allow";
+                      targets = [ "192.168.0.0/16" ];
+                    }
+                    {
+                      rule = "allow"
+                      users = [ "user1" "user2" ];
+                    }
+                  ];
+                }
+              </literal>
+              In this example strong username authentication is not required to access 192.168.0.0/16.
+            '';
+          };
+          acl = mkOption {
+            type = types.listOf (types.submodule {
+              options = {
+                rule = mkOption {
+                  type = types.enum [ "allow" "deny" ];
+                  example = "allow";
+                  description = ''
+                    ACL rule. The following values are valid:
+
+                    <itemizedlist>
+                      <listitem><para>
+                        <literal>"allow"</literal>: connections allowed.
+                      </para></listitem>
+                      <listitem><para>
+                        <literal>"deny"</literal>: connections not allowed.
+                      </para></listitem>
+                    </itemizedlist>
+                  '';
+                };
+                users = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "user1" "user2" "user3" ];
+                  description = ''
+                    List of users, use empty list for any.
+                  '';
+                };
+                sources = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "127.0.0.1" "192.168.1.0/24" ];
+                  description = ''
+                    List of source IP range, use empty list for any.
+                  '';
+                };
+                targets = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "127.0.0.1" "192.168.1.0/24" ];
+                  description = ''
+                    List of target IP ranges, use empty list for any.
+                    May also contain host names instead of addresses.
+                    It's possible to use wildmask in the begginning and in the the end of hostname, e.g. *badsite.com or *badcontent*.
+                    Hostname is only checked if hostname presents in request.
+                  '';
+                };
+                targetPorts = mkOption {
+                  type = types.listOf types.int;
+                  default = [ ];
+                  example = [ 80 443 ];
+                  description = ''
+                    List of target ports, use empty list for any.
+                  '';
+                };
+              };
+            });
+            default = [ ];
+            example = literalExample ''
+              [
+                {
+                  rule = "allow";
+                  users = [ "user1" ];
+                }
+                {
+                  rule = "allow";
+                  sources = [ "192.168.1.0/24" ];
+                }
+                {
+                  rule = "deny";
+                }
+              ]
+            '';
+            description = ''
+              Use this option to limit user access to resources.
+            '';
+          };
+          extraArguments = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "-46";
+            description = ''
+              Extra arguments for service.
+              Consult "Options" section in <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available arguments.
+            '';
+          };
+          extraConfig = mkOption {
+            type = types.nullOr types.lines;
+            default = null;
+            description = ''
+              Extra configuration for service. Use this to configure things like bandwidth limiter or ACL-based redirection.
+              Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available options.
+            '';
+          };
+        };
+      });
+      default = [ ];
+      example = literalExample ''
+        [
+          {
+            type = "proxy";
+            bindAddress = "192.168.1.24";
+            bindPort = 3128;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindAddress = "10.10.1.20";
+            bindPort = 3128;
+            auth = [ "iponly" ];
+          }
+          {
+            type = "socks";
+            bindAddress = "172.17.0.1";
+            bindPort = 1080;
+            auth = [ "strong" ];
+          }
+        ]
+      '';
+      description = ''
+        Use this option to define 3proxy services.
+      '';
+    };
+    denyPrivate = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to deny access to private IP ranges including loopback.
+      '';
+    };
+    privateRanges = mkOption {
+      type = types.listOf types.str;
+      default = [
+        "0.0.0.0/8"
+        "127.0.0.0/8"
+        "10.0.0.0/8"
+        "100.64.0.0/10"
+        "172.16.0.0/12"
+        "192.168.0.0/16"
+        "::"
+        "::1"
+        "fc00::/7"
+      ];
+      example = [
+        "0.0.0.0/8"
+        "127.0.0.0/8"
+        "10.0.0.0/8"
+        "100.64.0.0/10"
+        "172.16.0.0/12"
+        "192.168.0.0/16"
+        "::"
+        "::1"
+        "fc00::/7"
+      ];
+      description = ''
+        What IP ranges to deny access when denyPrivate is set tu true.
+      '';
+    };
+    resolution = mkOption {
+      type = types.submodule {
+        options = {
+          nserver = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "127.0.0.53" "192.168.1.3:5353/tcp" ];
+            description = ''
+              List of nameservers to use.
+
+              Up to 5 nservers may be specified. If no nserver is configured,
+              default system name resolution functions are used.
+            '';
+          };
+          nscache = mkOption {
+            type = types.int;
+            default = 65535;
+            example = 65535;
+            description = "Set name cache size for IPv4.";
+          };
+          nscache6 = mkOption {
+            type = types.int;
+            default = 65535;
+            example = 65535;
+            description = "Set name cache size for IPv6.";
+          };
+          nsrecord = mkOption {
+            type = types.attrsOf types.str;
+            default = { };
+            example = literalExample ''
+              {
+                "files.local" = "192.168.1.12";
+                "site.local" = "192.168.1.43";
+              }
+            '';
+            description = "Adds static nsrecords.";
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Use this option to configure name resolution and DNS caching.
+      '';
+    };
+    extraConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Extra configuration, appended to the 3proxy configuration file.
+        Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services._3proxy.confFile = mkDefault (pkgs.writeText "3proxy.conf" ''
+      # log to stdout
+      log
+
+      ${concatMapStringsSep "\n" (x: "nserver " + x) cfg.resolution.nserver}
+
+      nscache ${toString cfg.resolution.nscache}
+      nscache6 ${toString cfg.resolution.nscache6}
+
+      ${concatMapStringsSep "\n" (x: "nsrecord " + x)
+      (mapAttrsToList (name: value: "${name} ${value}")
+        cfg.resolution.nsrecord)}
+
+      ${optionalString (cfg.usersFile != null)
+        ''users $"${cfg.usersFile}"''
+      }
+
+      ${concatMapStringsSep "\n" (service: ''
+        auth ${concatStringsSep " " service.auth}
+
+        ${optionalString (cfg.denyPrivate)
+        "deny * * ${optionalList cfg.privateRanges}"}
+
+        ${concatMapStringsSep "\n" (acl:
+          "${acl.rule} ${
+            concatMapStringsSep " " optionalList [
+              acl.users
+              acl.sources
+              acl.targets
+              acl.targetPorts
+            ]
+          }") service.acl}
+
+        maxconn ${toString service.maxConnections}
+
+        ${optionalString (service.extraConfig != null) service.extraConfig}
+
+        ${service.type} -i${toString service.bindAddress} ${
+          optionalString (service.bindPort != null)
+          "-p${toString service.bindPort}"
+        } ${
+          optionalString (service.extraArguments != null) service.extraArguments
+        }
+
+        flush
+      '') cfg.services}
+      ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+    '');
+    systemd.services."3proxy" = {
+      description = "Tiny free proxy server";
+      documentation = [ "https://github.com/z3APA3A/3proxy/wiki" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "3proxy";
+        ExecStart = "${pkg}/bin/3proxy ${cfg.confFile}";
+        Restart = "on-failure";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ misuzu ];
+}
diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix
index 57f02542eaf..1128ee2c3e6 100644
--- a/nixos/modules/services/networking/amuled.nix
+++ b/nixos/modules/services/networking/amuled.nix
@@ -16,6 +16,7 @@ in
     services.amule = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to run the AMule daemon. You need to manually run "amuled --ec-config" to configure the service for the first time.
diff --git a/nixos/modules/services/networking/babeld.nix b/nixos/modules/services/networking/babeld.nix
index de863461eab..e62c74d0069 100644
--- a/nixos/modules/services/networking/babeld.nix
+++ b/nixos/modules/services/networking/babeld.nix
@@ -35,12 +35,7 @@ in
 
     services.babeld = {
 
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to run the babeld network routing daemon.
-        '';
-      };
+      enable = mkEnableOption "the babeld network routing daemon";
 
       interfaceDefaults = mkOption {
         default = null;
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index d09c6735e12..faad8863575 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -68,12 +68,7 @@ in
 
     services.bind = {
 
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to enable BIND domain name server.
-        ";
-      };
+      enable = mkEnableOption "BIND domain name server";
 
       cacheNetworks = mkOption {
         default = ["127.0.0.0/24"];
@@ -178,9 +173,8 @@ in
 
     networking.resolvconf.useLocalResolver = mkDefault true;
 
-    users.users = singleton
-      { name = bindUser;
-        uid = config.ids.uids.bind;
+    users.users.${bindUser} =
+      { uid = config.ids.uids.bind;
         description = "BIND daemon user";
       };
 
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index 274b3617160..9ebf382fce4 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -48,6 +48,7 @@ in
     services.bitlbee = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to run the BitlBee IRC to other chat network gateway.
@@ -161,16 +162,14 @@ in
 
   config =  mkMerge [
     (mkIf config.services.bitlbee.enable {
-      users.users = singleton {
-        name = "bitlbee";
+      users.users.bitlbee = {
         uid = bitlbeeUid;
         description = "BitlBee user";
         home = "/var/lib/bitlbee";
         createHome = true;
       };
 
-      users.groups = singleton {
-        name = "bitlbee";
+      users.groups.bitlbee = {
         gid = config.ids.gids.bitlbee;
       };
 
diff --git a/nixos/modules/services/networking/charybdis.nix b/nixos/modules/services/networking/charybdis.nix
index da26246e703..43829d36e41 100644
--- a/nixos/modules/services/networking/charybdis.nix
+++ b/nixos/modules/services/networking/charybdis.nix
@@ -71,15 +71,13 @@ in
 
   config = mkIf cfg.enable (lib.mkMerge [
     {
-      users.users = singleton {
-        name = cfg.user;
+      users.users.${cfg.user} = {
         description = "Charybdis IRC daemon user";
         uid = config.ids.uids.ircd;
         group = cfg.group;
       };
 
-      users.groups = singleton {
-        name = cfg.group;
+      users.groups.${cfg.group} = {
         gid = config.ids.gids.ircd;
       };
 
diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix
index 3fb85b16cbe..5f8ac96b229 100644
--- a/nixos/modules/services/networking/cjdns.nix
+++ b/nixos/modules/services/networking/cjdns.nix
@@ -29,17 +29,13 @@ let
   };
 
   # Additional /etc/hosts entries for peers with an associated hostname
-  cjdnsExtraHosts = import (pkgs.runCommand "cjdns-hosts" {}
-    # Generate a builder that produces an output usable as a Nix string value
-    ''
-      exec >$out
-      echo \'\'
-      ${concatStringsSep "\n" (mapAttrsToList (k: v:
-          optionalString (v.hostname != "")
-            "echo $(${pkgs.cjdns}/bin/publictoip6 ${v.publicKey}) ${v.hostname}")
-          (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo))}
-      echo \'\'
-    '');
+  cjdnsExtraHosts = pkgs.runCommandNoCC "cjdns-hosts" {} ''
+    exec >$out
+    ${concatStringsSep "\n" (mapAttrsToList (k: v:
+        optionalString (v.hostname != "")
+          "echo $(${pkgs.cjdns}/bin/publictoip6 ${v.publicKey}) ${v.hostname}")
+        (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo))}
+  '';
 
   parseModules = x:
     x // { connectTo = mapAttrs (name: value: { inherit (value) password publicKey; }) x.connectTo; };
@@ -144,13 +140,15 @@ in
         connectTo = mkOption {
           type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
           default = { };
-          example = {
-            "192.168.1.1:27313" = {
-              hostname = "homer.hype";
-              password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
-              publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
-            };
-          };
+          example = literalExample ''
+            {
+              "192.168.1.1:27313" = {
+                hostname = "homer.hype";
+                password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
+                publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
+              };
+            }
+          '';
           description = ''
             Credentials for making UDP tunnels.
           '';
@@ -189,13 +187,15 @@ in
         connectTo = mkOption {
           type = types.attrsOf ( types.submodule ( connectToSubmodule ) );
           default = { };
-          example = {
-            "01:02:03:04:05:06" = {
-              hostname = "homer.hype";
-              password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
-              publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
-            };
-          };
+          example = literalExample ''
+            {
+              "01:02:03:04:05:06" = {
+                hostname = "homer.hype";
+                password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM";
+                publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k";
+              };
+            }
+          '';
           description = ''
             Credentials for connecting look similar to UDP credientials
             except they begin with the mac address.
@@ -278,7 +278,7 @@ in
       };
     };
 
-    networking.extraHosts = mkIf cfg.addExtraHosts cjdnsExtraHosts;
+    networking.hostFiles = mkIf cfg.addExtraHosts [ cjdnsExtraHosts ];
 
     assertions = [
       { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile != null );
diff --git a/nixos/modules/services/networking/cntlm.nix b/nixos/modules/services/networking/cntlm.nix
index 4e4e3104c3a..5b5068e43d7 100644
--- a/nixos/modules/services/networking/cntlm.nix
+++ b/nixos/modules/services/networking/cntlm.nix
@@ -33,12 +33,7 @@ in
 
   options.services.cntlm = {
 
-    enable = mkOption {
-      default = false;
-      description = ''
-        Whether to enable the cntlm, which start a local proxy.
-      '';
-    };
+    enable = mkEnableOption "cntlm, which starts a local proxy";
 
     username = mkOption {
       description = ''
diff --git a/nixos/modules/services/networking/connman.nix b/nixos/modules/services/networking/connman.nix
index cac517f410e..6ccc2dffb26 100644
--- a/nixos/modules/services/networking/connman.nix
+++ b/nixos/modules/services/networking/connman.nix
@@ -11,8 +11,13 @@ let
 
     ${cfg.extraConfig}
   '';
+  enableIwd = cfg.wifi.backend == "iwd";
 in {
 
+  imports = [
+    (mkRenamedOptionModule [ "networking" "connman" ] [ "services" "connman" ])
+  ];
+
   ###### interface
 
   options = {
@@ -52,6 +57,17 @@ in {
         '';
       };
 
+      wifi = {
+        backend = mkOption {
+          type = types.enum [ "wpa_supplicant" "iwd" ];
+          default = "wpa_supplicant";
+          description = ''
+            Specify the Wi-Fi backend used.
+            Currently supported are <option>wpa_supplicant</option> or <option>iwd</option>.
+          '';
+        };
+      };
+
       extraFlags = mkOption {
         type = with types; listOf str;
         default = [ ];
@@ -61,6 +77,13 @@ in {
         '';
       };
 
+      package = mkOption {
+        type = types.path;
+        description = "The connman package / build flavor";
+        default = connman;
+        example = literalExample "pkgs.connmanFull";
+      };
+
     };
 
   };
@@ -73,24 +96,29 @@ in {
       assertion = !config.networking.useDHCP;
       message = "You can not use services.connman with networking.useDHCP";
     }{
-      assertion = config.networking.wireless.enable;
-      message = "You must use services.connman with networking.wireless";
-    }{
+      # TODO: connman seemingly can be used along network manager and
+      # connmanFull supports this - so this should be worked out somehow
       assertion = !config.networking.networkmanager.enable;
       message = "You can not use services.connman with networking.networkmanager";
     }];
 
-    environment.systemPackages = [ connman ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.services.connman = {
       description = "Connection service";
       wantedBy = [ "multi-user.target" ];
-      after = [ "syslog.target" ];
+      after = [ "syslog.target" ] ++ optional enableIwd "iwd.service";
+      requires = optional enableIwd "iwd.service";
       serviceConfig = {
         Type = "dbus";
         BusName = "net.connman";
         Restart = "on-failure";
-        ExecStart = "${pkgs.connman}/sbin/connmand --config=${configFile} --nodaemon ${toString cfg.extraFlags}";
+        ExecStart = toString ([
+          "${cfg.package}/sbin/connmand"
+          "--config=${configFile}"
+          "--nodaemon"
+        ] ++ optional enableIwd "--wifi=iwd_agent"
+          ++ cfg.extraFlags);
         StandardOutput = "null";
       };
     };
@@ -103,7 +131,7 @@ in {
       serviceConfig = {
         Type = "dbus";
         BusName = "net.connman.vpn";
-        ExecStart = "${pkgs.connman}/sbin/connman-vpnd -n";
+        ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
         StandardOutput = "null";
       };
     };
@@ -113,7 +141,7 @@ in {
       serviceConfig = {
         Name = "net.connman.vpn";
         before = [ "connman" ];
-        ExecStart = "${pkgs.connman}/sbin/connman-vpnd -n";
+        ExecStart = "${cfg.package}/sbin/connman-vpnd -n";
         User = "root";
         SystemdService = "connman-vpn.service";
       };
@@ -121,7 +149,12 @@ in {
 
     networking = {
       useDHCP = false;
-      wireless.enable = true;
+      wireless = {
+        enable = mkIf (!enableIwd) true;
+        iwd = mkIf enableIwd {
+          enable = true;
+        };
+      };
       networkmanager.enable = false;
     };
   };
diff --git a/nixos/modules/services/networking/corerad.nix b/nixos/modules/services/networking/corerad.nix
new file mode 100644
index 00000000000..1a2c4aec665
--- /dev/null
+++ b/nixos/modules/services/networking/corerad.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.corerad;
+in {
+  meta = {
+    maintainers = with maintainers; [ mdlayher ];
+  };
+
+  options.services.corerad = {
+    enable = mkEnableOption "CoreRAD IPv6 NDP RA daemon";
+
+    configFile = mkOption {
+      type = types.path;
+      example = literalExample "\"\${pkgs.corerad}/etc/corerad/corerad.toml\"";
+      description = "Path to CoreRAD TOML configuration file.";
+    };
+
+    package = mkOption {
+      default = pkgs.corerad;
+      defaultText = literalExample "pkgs.corerad";
+      type = types.package;
+      description = "CoreRAD package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.corerad = {
+      description = "CoreRAD IPv6 NDP RA daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        LimitNPROC = 512;
+        LimitNOFILE = 1048576;
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
+        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_RAW";
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        ExecStart = "${getBin cfg.package}/bin/corerad -c=${cfg.configFile}";
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/coturn.nix b/nixos/modules/services/networking/coturn.nix
index c430ce5af92..1bfbc307c59 100644
--- a/nixos/modules/services/networking/coturn.nix
+++ b/nixos/modules/services/networking/coturn.nix
@@ -294,16 +294,14 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.users = [
-      { name = "turnserver";
-        uid = config.ids.uids.turnserver;
+    users.users.turnserver =
+      { uid = config.ids.uids.turnserver;
         description = "coturn TURN server user";
-      } ];
-    users.groups = [
-      { name = "turnserver";
-        gid = config.ids.gids.turnserver;
+      };
+    users.groups.turnserver =
+      { gid = config.ids.gids.turnserver;
         members = [ "turnserver" ];
-      } ];
+      };
 
     systemd.services.coturn = {
       description = "coturn TURN server";
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index 04ce5ca3a87..053efe71270 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -30,6 +30,14 @@ with lib;
 
 {
 
+  imports = [
+    (mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ]
+      (config:
+        let value = getAttrFromPath [ "services" "ddclient" "domain" ] config;
+        in if value != "" then [ value ] else []))
+    (mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
+  ];
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 7b278603455..c0619211c2f 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -19,7 +19,7 @@ let
     map (i: i.name) (filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces)
     ++ mapAttrsToList (i: _: i) config.networking.sits
     ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bridges))
-    ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.vswitches))
+    ++ flatten (concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues config.networking.vswitches))
     ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bonds))
     ++ config.networking.dhcpcd.denyInterfaces;
 
@@ -59,6 +59,16 @@ let
       # Use the list of allowed interfaces if specified
       ${optionalString (allowInterfaces != null) "allowinterfaces ${toString allowInterfaces}"}
 
+      # Immediately fork to background if specified, otherwise wait for IP address to be assigned
+      ${{
+        background = "background";
+        any = "waitip";
+        ipv4 = "waitip 4";
+        ipv6 = "waitip 6";
+        both = "waitip 4\nwaitip 6";
+        if-carrier-up = "";
+      }.${cfg.wait}}
+
       ${cfg.extraConfig}
     '';
 
@@ -146,6 +156,21 @@ in
       '';
     };
 
+    networking.dhcpcd.wait = mkOption {
+      type = types.enum [ "background" "any" "ipv4" "ipv6" "both" "if-carrier-up" ];
+      default = "any";
+      description = ''
+        This option specifies when the dhcpcd service will fork to background.
+        If set to "background", dhcpcd will fork to background immediately.
+        If set to "ipv4" or "ipv6", dhcpcd will wait for the corresponding IP
+        address to be assigned. If set to "any", dhcpcd will wait for any type
+        (IPv4 or IPv6) to be assigned. If set to "both", dhcpcd will wait for
+        both an IPv4 and an IPv6 address before forking.
+        The option "if-carrier-up" is equivalent to "any" if either ethernet
+        is plugged nor WiFi is powered, and to "background" otherwise.
+      '';
+    };
+
   };
 
 
@@ -165,6 +190,8 @@ in
         before = [ "network-online.target" ];
         after = [ "systemd-udev-settle.service" ];
 
+        restartTriggers = [ exitHook ];
+
         # Stopping dhcpcd during a reconfiguration is undesirable
         # because it brings down the network interfaces configured by
         # dhcpcd.  So do a "systemctl restart" instead.
@@ -177,7 +204,7 @@ in
         serviceConfig =
           { Type = "forking";
             PIDFile = "/run/dhcpcd.pid";
-            ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd -w --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
+            ExecStart = "@${dhcpcd}/sbin/dhcpcd dhcpcd --quiet ${optionalString cfg.persistent "--persistent"} --config ${dhcpcdConf}";
             ExecReload = "${dhcpcd}/sbin/dhcpcd --rebind";
             Restart = "always";
           };
@@ -185,11 +212,7 @@ in
 
     environment.systemPackages = [ dhcpcd ];
 
-    environment.etc =
-      [ { source = exitHook;
-          target = "dhcpcd.exit-hook";
-        }
-      ];
+    environment.etc."dhcpcd.exit-hook".source = exitHook;
 
     powerManagement.resumeCommands = mkIf config.systemd.services.dhcpcd.enable
       ''
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 0b2063bc424..67f7d811887 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -182,6 +182,10 @@ in
 
 {
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "dhcpd" ] [ "services" "dhcpd4" ])
+  ];
+
   ###### interface
 
   options = {
diff --git a/nixos/modules/services/networking/dnscache.nix b/nixos/modules/services/networking/dnscache.nix
index d123bca9321..d06032daecc 100644
--- a/nixos/modules/services/networking/dnscache.nix
+++ b/nixos/modules/services/networking/dnscache.nix
@@ -61,10 +61,12 @@ in {
           Table of {hostname: server} pairs to use as authoritative servers for hosts (and subhosts).
           If entry for @ is not specified predefined list of root servers is used.
         '';
-        example = {
-          "@" = ["8.8.8.8" "8.8.4.4"];
-          "example.com" = ["192.168.100.100"];
-        };
+        example = literalExample ''
+          {
+            "@" = ["8.8.8.8" "8.8.4.4"];
+            "example.com" = ["192.168.100.100"];
+          }
+        '';
       };
 
       forwardOnly = mkOption {
diff --git a/nixos/modules/services/networking/dnschain.nix b/nixos/modules/services/networking/dnschain.nix
index 5b58ea9b0c9..003609ea705 100644
--- a/nixos/modules/services/networking/dnschain.nix
+++ b/nixos/modules/services/networking/dnschain.nix
@@ -137,7 +137,7 @@ in
       ];
 
     services.pdns-recursor = mkIf cfgs.pdns-recursor.resolveDNSChainQueries {
-      forwardZones =
+      forwardZonesRecurse =
         { bit = "127.0.0.1:${toString cfg.dns.port}";
           dns = "127.0.0.1:${toString cfg.dns.port}";
         };
@@ -147,8 +147,7 @@ in
       '';
     };
 
-    users.users = singleton {
-      name = username;
+    users.users.${username} = {
       description = "DNSChain daemon user";
       home = dataDir;
       createHome = true;
@@ -180,4 +179,6 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/dnscrypt-proxy.nix b/nixos/modules/services/networking/dnscrypt-proxy.nix
deleted file mode 100644
index 8edcf925dbf..00000000000
--- a/nixos/modules/services/networking/dnscrypt-proxy.nix
+++ /dev/null
@@ -1,328 +0,0 @@
-{ config, lib, pkgs, ... }:
-with lib;
-
-let
-  cfg = config.services.dnscrypt-proxy;
-
-  stateDirectory = "/var/lib/dnscrypt-proxy";
-
-  # The minisign public key used to sign the upstream resolver list.
-  # This is somewhat more flexible than preloading the key as an
-  # embedded string.
-  upstreamResolverListPubKey = pkgs.fetchurl {
-    url = https://raw.githubusercontent.com/dyne/dnscrypt-proxy/master/minisign.pub;
-    sha256 = "18lnp8qr6ghfc2sd46nn1rhcpr324fqlvgsp4zaigw396cd7vnnh";
-  };
-
-  # Internal flag indicating whether the upstream resolver list is used.
-  useUpstreamResolverList = cfg.customResolver == null;
-
-  # The final local address.
-  localAddress = "${cfg.localAddress}:${toString cfg.localPort}";
-
-  # The final resolvers list path.
-  resolverList = "${stateDirectory}/dnscrypt-resolvers.csv";
-
-  # Build daemon command line
-
-  resolverArgs =
-    if (cfg.customResolver == null)
-      then
-        [ "-L ${resolverList}"
-          "-R ${cfg.resolverName}"
-        ]
-      else with cfg.customResolver;
-        [ "-N ${name}"
-          "-k ${key}"
-          "-r ${address}:${toString port}"
-        ];
-
-  daemonArgs =
-       [ "-a ${localAddress}" ]
-    ++ resolverArgs
-    ++ cfg.extraArgs;
-in
-
-{
-  meta = {
-    maintainers = with maintainers; [ joachifm ];
-    doc = ./dnscrypt-proxy.xml;
-  };
-
-  options = {
-    # Before adding another option, consider whether it could
-    # equally well be passed via extraArgs.
-
-    services.dnscrypt-proxy = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = "Whether to enable the DNSCrypt client proxy";
-      };
-
-      localAddress = mkOption {
-        default = "127.0.0.1";
-        type = types.str;
-        description = ''
-          Listen for DNS queries to relay on this address. The only reason to
-          change this from its default value is to proxy queries on behalf
-          of other machines (typically on the local network).
-        '';
-      };
-
-      localPort = mkOption {
-        default = 53;
-        type = types.int;
-        description = ''
-          Listen for DNS queries to relay on this port. The default value
-          assumes that the DNSCrypt proxy should relay DNS queries directly.
-          When running as a forwarder for another DNS client, set this option
-          to a different value; otherwise leave the default.
-        '';
-      };
-
-      resolverName = mkOption {
-        default = "random";
-        example = "dnscrypt.eu-nl";
-        type = types.nullOr types.str;
-        description = ''
-          The name of the DNSCrypt resolver to use, taken from
-          <filename>${resolverList}</filename>.  The default is to
-          pick a random non-logging resolver that supports DNSSEC.
-        '';
-      };
-
-      customResolver = mkOption {
-        default = null;
-        description = ''
-          Use an unlisted resolver (e.g., a private DNSCrypt provider). For
-          advanced users only. If specified, this option takes precedence.
-        '';
-        type = types.nullOr (types.submodule ({ ... }: { options = {
-          address = mkOption {
-            type = types.str;
-            description = "IP address";
-            example = "208.67.220.220";
-          };
-
-          port = mkOption {
-            type = types.int;
-            description = "Port";
-            default = 443;
-          };
-
-          name = mkOption {
-            type = types.str;
-            description = "Fully qualified domain name";
-            example = "2.dnscrypt-cert.example.com";
-          };
-
-          key = mkOption {
-            type = types.str;
-            description = "Public key";
-            example = "B735:1140:206F:225D:3E2B:D822:D7FD:691E:A1C3:3CC8:D666:8D0C:BE04:BFAB:CA43:FB79";
-          };
-        }; }));
-      };
-
-      extraArgs = mkOption {
-        default = [];
-        type = types.listOf types.str;
-        description = ''
-          Additional command-line arguments passed verbatim to the daemon.
-          See <citerefentry><refentrytitle>dnscrypt-proxy</refentrytitle>
-          <manvolnum>8</manvolnum></citerefentry> for details.
-        '';
-        example = [ "-X libdcplugin_example_cache.so,--min-ttl=60" ];
-      };
-    };
-  };
-
-  config = mkIf cfg.enable (mkMerge [{
-    assertions = [
-      { assertion = (cfg.customResolver != null) || (cfg.resolverName != null);
-        message   = "please configure upstream DNSCrypt resolver";
-      }
-    ];
-
-    # make man 8 dnscrypt-proxy work
-    environment.systemPackages = [ pkgs.dnscrypt-proxy ];
-
-    users.users.dnscrypt-proxy = {
-      description = "dnscrypt-proxy daemon user";
-      isSystemUser = true;
-      group = "dnscrypt-proxy";
-    };
-    users.groups.dnscrypt-proxy = {};
-
-    systemd.sockets.dnscrypt-proxy = {
-      description = "dnscrypt-proxy listening socket";
-      documentation = [ "man:dnscrypt-proxy(8)" ];
-
-      wantedBy = [ "sockets.target" ];
-
-      socketConfig = {
-        ListenStream = localAddress;
-        ListenDatagram = localAddress;
-      };
-    };
-
-    systemd.services.dnscrypt-proxy = {
-      description = "dnscrypt-proxy daemon";
-      documentation = [ "man:dnscrypt-proxy(8)" ];
-
-      before = [ "nss-lookup.target" ];
-      after = [ "network.target" ];
-      requires = [ "dnscrypt-proxy.socket "];
-
-      serviceConfig = {
-        NonBlocking = "true";
-        ExecStart = "${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy ${toString daemonArgs}";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-
-        User = "dnscrypt-proxy";
-
-        PrivateTmp = true;
-        PrivateDevices = true;
-        ProtectHome = true;
-      };
-    };
-    }
-
-    (mkIf config.security.apparmor.enable {
-    systemd.services.dnscrypt-proxy.after = [ "apparmor.service" ];
-
-    security.apparmor.profiles = singleton (pkgs.writeText "apparmor-dnscrypt-proxy" ''
-      ${pkgs.dnscrypt-proxy}/bin/dnscrypt-proxy {
-        /dev/null rw,
-        /dev/random r,
-        /dev/urandom r,
-
-        /etc/passwd r,
-        /etc/group r,
-        ${config.environment.etc."nsswitch.conf".source} r,
-
-        ${getLib pkgs.glibc}/lib/*.so mr,
-        ${pkgs.tzdata}/share/zoneinfo/** r,
-
-        network inet stream,
-        network inet6 stream,
-        network inet dgram,
-        network inet6 dgram,
-
-        ${getLib pkgs.dnscrypt-proxy}/lib/dnscrypt-proxy/libdcplugin*.so mr,
-
-        ${getLib pkgs.gcc.cc}/lib/libssp.so.* mr,
-        ${getLib pkgs.libsodium}/lib/libsodium.so.* mr,
-        ${getLib pkgs.systemd}/lib/libsystemd.so.* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so.* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so.* mr,
-        ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
-        ${getLib pkgs.xz}/lib/liblzma.so.* mr,
-        ${getLib pkgs.libgcrypt}/lib/libgcrypt.so.* mr,
-        ${getLib pkgs.libgpgerror}/lib/libgpg-error.so.* mr,
-        ${getLib pkgs.libcap}/lib/libcap.so.* mr,
-        ${getLib pkgs.lz4}/lib/liblz4.so.* mr,
-        ${getLib pkgs.attr}/lib/libattr.so.* mr, # */
-
-        ${resolverList} r,
-
-        /run/systemd/notify rw,
-      }
-    '');
-    })
-
-    (mkIf useUpstreamResolverList {
-    systemd.services.init-dnscrypt-proxy-statedir = {
-      description = "Initialize dnscrypt-proxy state directory";
-
-      wantedBy = [ "dnscrypt-proxy.service" ];
-      before = [ "dnscrypt-proxy.service" ];
-
-      script = ''
-        mkdir -pv ${stateDirectory}
-        chown -c dnscrypt-proxy:dnscrypt-proxy ${stateDirectory}
-        cp -uv \
-          ${pkgs.dnscrypt-proxy}/share/dnscrypt-proxy/dnscrypt-resolvers.csv \
-          ${stateDirectory}
-      '';
-
-      serviceConfig = {
-        Type = "oneshot";
-        RemainAfterExit = true;
-      };
-    };
-
-    systemd.services.update-dnscrypt-resolvers = {
-      description = "Update list of DNSCrypt resolvers";
-
-      requires = [ "init-dnscrypt-proxy-statedir.service" ];
-      after = [ "init-dnscrypt-proxy-statedir.service" ];
-
-      path = with pkgs; [ curl diffutils dnscrypt-proxy minisign ];
-      script = ''
-        cd ${stateDirectory}
-        domain=raw.githubusercontent.com
-        get="curl -fSs --resolve $domain:443:$(hostip -r 8.8.8.8 $domain | head -1)"
-        $get -o dnscrypt-resolvers.csv.tmp \
-          https://$domain/dyne/dnscrypt-proxy/master/dnscrypt-resolvers.csv
-        $get -o dnscrypt-resolvers.csv.minisig.tmp \
-          https://$domain/dyne/dnscrypt-proxy/master/dnscrypt-resolvers.csv.minisig
-        mv dnscrypt-resolvers.csv.minisig{.tmp,}
-        if ! minisign -q -V -p ${upstreamResolverListPubKey} \
-          -m dnscrypt-resolvers.csv.tmp -x dnscrypt-resolvers.csv.minisig ; then
-          echo "failed to verify resolver list!" >&2
-          exit 1
-        fi
-        [[ -f dnscrypt-resolvers.csv ]] && mv dnscrypt-resolvers.csv{,.old}
-        mv dnscrypt-resolvers.csv{.tmp,}
-        if cmp dnscrypt-resolvers.csv{,.old} ; then
-          echo "no change"
-        else
-          echo "resolver list updated"
-        fi
-      '';
-
-      serviceConfig = {
-        PrivateTmp = true;
-        PrivateDevices = true;
-        ProtectHome = true;
-        ProtectSystem = "strict";
-        ReadWritePaths = "${dirOf stateDirectory} ${stateDirectory}";
-        SystemCallFilter = "~@mount";
-      };
-    };
-
-    systemd.timers.update-dnscrypt-resolvers = {
-      wantedBy = [ "timers.target" ];
-      timerConfig = {
-        OnBootSec = "5min";
-        OnUnitActiveSec = "6h";
-      };
-    };
-    })
-    ]);
-
-  imports = [
-    (mkRenamedOptionModule [ "services" "dnscrypt-proxy" "port" ] [ "services" "dnscrypt-proxy" "localPort" ])
-
-    (mkChangedOptionModule
-      [ "services" "dnscrypt-proxy" "tcpOnly" ]
-      [ "services" "dnscrypt-proxy" "extraArgs" ]
-      (config:
-        let val = getAttrFromPath [ "services" "dnscrypt-proxy" "tcpOnly" ] config; in
-        optional val "-T"))
-
-    (mkChangedOptionModule
-      [ "services" "dnscrypt-proxy" "ephemeralKeys" ]
-      [ "services" "dnscrypt-proxy" "extraArgs" ]
-      (config:
-        let val = getAttrFromPath [ "services" "dnscrypt-proxy" "ephemeralKeys" ] config; in
-        optional val "-E"))
-
-    (mkRemovedOptionModule [ "services" "dnscrypt-proxy" "resolverList" ] ''
-      The current resolver listing from upstream is always used
-      unless a custom resolver is specified.
-    '')
-  ];
-}
diff --git a/nixos/modules/services/networking/dnscrypt-proxy.xml b/nixos/modules/services/networking/dnscrypt-proxy.xml
deleted file mode 100644
index afc7880392a..00000000000
--- a/nixos/modules/services/networking/dnscrypt-proxy.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<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-dnscrypt-proxy">
- <title>DNSCrypt client proxy</title>
- <para>
-  The DNSCrypt client proxy relays DNS queries to a DNSCrypt enabled upstream
-  resolver. The traffic between the client and the upstream resolver is
-  encrypted and authenticated, mitigating the risk of MITM attacks, DNS
-  poisoning attacks, and third-party snooping (assuming the upstream is
-  trustworthy).
- </para>
- <sect1 xml:id="sec-dnscrypt-proxy-configuration">
-  <title>Basic configuration</title>
-
-  <para>
-   To enable the client proxy, set
-<programlisting>
-<xref linkend="opt-services.dnscrypt-proxy.enable"/> = true;
-</programlisting>
-  </para>
-
-  <para>
-   Enabling the client proxy does not alter the system nameserver; to relay
-   local queries, prepend <literal>127.0.0.1</literal> to
-   <option>networking.nameservers</option>.
-  </para>
- </sect1>
- <sect1 xml:id="sec-dnscrypt-proxy-forwarder">
-  <title>As a forwarder for another DNS client</title>
-
-  <para>
-   To run the DNSCrypt proxy client as a forwarder for another DNS client,
-   change the default proxy listening port to a non-standard value and point
-   the other client to it:
-<programlisting>
-<xref linkend="opt-services.dnscrypt-proxy.localPort"/> = 43;
-</programlisting>
-  </para>
-
-  <sect2 xml:id="sec-dnscrypt-proxy-forwarder-dsnmasq">
-   <title>dnsmasq</title>
-   <para>
-<programlisting>
-{
-  <xref linkend="opt-services.dnsmasq.enable"/> = true;
-  <xref linkend="opt-services.dnsmasq.servers"/> = [ "127.0.0.1#43" ];
-}
-</programlisting>
-   </para>
-  </sect2>
-
-  <sect2 xml:id="sec-dnscrypt-proxy-forwarder-unbound">
-   <title>unbound</title>
-   <para>
-<programlisting>
-{
-  <xref linkend="opt-services.unbound.enable"/> = true;
-  <xref linkend="opt-services.unbound.forwardAddresses"/> = [ "127.0.0.1@43" ];
-}
-</programlisting>
-   </para>
-  </sect2>
- </sect1>
-</chapter>
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
new file mode 100644
index 00000000000..e48eb729103
--- /dev/null
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }: with lib;
+
+let
+  cfg = config.services.dnscrypt-proxy2;
+in
+
+{
+  options.services.dnscrypt-proxy2 = {
+    enable = mkEnableOption "dnscrypt-proxy2";
+
+    settings = mkOption {
+      description = ''
+        Attrset that is converted and passed as TOML config file.
+        For available params, see: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
+      '';
+      example = literalExample ''
+        {
+          sources.public-resolvers = {
+            urls = [ "https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md" ];
+            cache_file = "public-resolvers.md";
+            minisign_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3";
+            refresh_delay = 72;
+          };
+        }
+      '';
+      type = types.attrs;
+      default = {};
+    };
+
+    configFile = mkOption {
+      description = ''
+        Path to TOML config file. See: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
+        If this option is set, it will override any configuration done in options.services.dnscrypt-proxy2.settings.
+      '';
+      example = "/etc/dnscrypt-proxy/dnscrypt-proxy.toml";
+      type = types.path;
+      default = pkgs.runCommand "dnscrypt-proxy.toml" {
+        json = builtins.toJSON cfg.settings;
+        passAsFile = [ "json" ];
+      } ''
+        ${pkgs.remarshal}/bin/json2toml < $jsonPath > $out
+      '';
+      defaultText = literalExample "TOML file generated from services.dnscrypt-proxy2.settings";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    networking.nameservers = lib.mkDefault [ "127.0.0.1" ];
+
+    systemd.services.dnscrypt-proxy2 = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        DynamicUser = true;
+        ExecStart = "${pkgs.dnscrypt-proxy2}/bin/dnscrypt-proxy -config ${cfg.configFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
index 79f9e1a4308..e53fb7a1578 100644
--- a/nixos/modules/services/networking/dnscrypt-wrapper.nix
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -197,4 +197,7 @@ in {
     };
 
   };
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/dnsmasq.nix b/nixos/modules/services/networking/dnsmasq.nix
index 714a5903bff..377d7bc5705 100644
--- a/nixos/modules/services/networking/dnsmasq.nix
+++ b/nixos/modules/services/networking/dnsmasq.nix
@@ -86,8 +86,7 @@ in
 
     services.dbus.packages = [ dnsmasq ];
 
-    users.users = singleton {
-      name = "dnsmasq";
+    users.users.dnsmasq = {
       uid = config.ids.uids.dnsmasq;
       description = "Dnsmasq daemon user";
     };
diff --git a/nixos/modules/services/networking/ejabberd.nix b/nixos/modules/services/networking/ejabberd.nix
index 6a38f85c48a..a5af25b983b 100644
--- a/nixos/modules/services/networking/ejabberd.nix
+++ b/nixos/modules/services/networking/ejabberd.nix
@@ -94,18 +94,18 @@ in {
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
-    users.users = optionalAttrs (cfg.user == "ejabberd") (singleton
-      { name = "ejabberd";
+    users.users = optionalAttrs (cfg.user == "ejabberd") {
+      ejabberd = {
         group = cfg.group;
         home = cfg.spoolDir;
         createHome = true;
         uid = config.ids.uids.ejabberd;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "ejabberd") (singleton
-      { name = "ejabberd";
-        gid = config.ids.gids.ejabberd;
-      });
+    users.groups = optionalAttrs (cfg.group == "ejabberd") {
+      ejabberd.gid = config.ids.gids.ejabberd;
+    };
 
     systemd.services.ejabberd = {
       description = "ejabberd server";
diff --git a/nixos/modules/services/networking/fakeroute.nix b/nixos/modules/services/networking/fakeroute.nix
index 82a9fb729d8..7916ad4098a 100644
--- a/nixos/modules/services/networking/fakeroute.nix
+++ b/nixos/modules/services/networking/fakeroute.nix
@@ -60,4 +60,6 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 5919962837a..cdc3a172ea7 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -42,16 +42,7 @@ let
 
   kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
 
-  helpers =
-    ''
-      # Helper command to manipulate both the IPv4 and IPv6 tables.
-      ip46tables() {
-        iptables -w "$@"
-        ${optionalString config.networking.enableIPv6 ''
-          ip6tables -w "$@"
-        ''}
-      }
-    '';
+  helpers = import ./helpers.nix { inherit config lib; };
 
   writeShScript = name: text: let dir = pkgs.writeScriptBin name ''
     #! ${pkgs.runtimeShell} -e
@@ -271,7 +262,7 @@ let
       apply = canonicalizePortList;
       example = [ 22 80 ];
       description =
-        '' 
+        ''
           List of TCP ports on which incoming connections are
           accepted.
         '';
@@ -282,7 +273,7 @@ let
       default = [ ];
       example = [ { from = 8999; to = 9003; } ];
       description =
-        '' 
+        ''
           A range of TCP ports on which incoming connections are
           accepted.
         '';
@@ -555,9 +546,13 @@ in
       options nf_conntrack nf_conntrack_helper=1
     '';
 
-    assertions = [ { assertion = (cfg.checkReversePath != false) || kernelHasRPFilter;
-                     message = "This kernel does not support rpfilter"; }
-                 ];
+    assertions = [
+      # This is approximately "checkReversePath -> kernelHasRPFilter",
+      # but the checkReversePath option can include non-boolean
+      # values.
+      { assertion = cfg.checkReversePath == false || kernelHasRPFilter;
+        message = "This kernel does not support rpfilter"; }
+    ];
 
     systemd.services.firewall = {
       description = "Firewall";
diff --git a/nixos/modules/services/networking/flashpolicyd.nix b/nixos/modules/services/networking/flashpolicyd.nix
index 9c51b88ef67..7f25083307c 100644
--- a/nixos/modules/services/networking/flashpolicyd.nix
+++ b/nixos/modules/services/networking/flashpolicyd.nix
@@ -39,6 +39,7 @@ in
     services.flashpolicyd = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description =
           ''
diff --git a/nixos/modules/services/networking/freeradius.nix b/nixos/modules/services/networking/freeradius.nix
index e192b70c129..f3fdd576b65 100644
--- a/nixos/modules/services/networking/freeradius.nix
+++ b/nixos/modules/services/networking/freeradius.nix
@@ -10,14 +10,15 @@ let
   {
     description = "FreeRadius server";
     wantedBy = ["multi-user.target"];
-    after = ["network-online.target"];
-    wants = ["network-online.target"];
+    after = ["network.target"];
+    wants = ["network.target"];
     preStart = ''
       ${pkgs.freeradius}/bin/radiusd -C -d ${cfg.configDir} -l stdout
     '';
 
     serviceConfig = {
-        ExecStart = "${pkgs.freeradius}/bin/radiusd -f -d ${cfg.configDir} -l stdout -xx";
+        ExecStart = "${pkgs.freeradius}/bin/radiusd -f -d ${cfg.configDir} -l stdout" +
+                    optionalString cfg.debug " -xx";
         ExecReload = [
           "${pkgs.freeradius}/bin/radiusd -C -d ${cfg.configDir} -l stdout"
           "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
@@ -41,6 +42,16 @@ let
       '';
     };
 
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable debug logging for freeradius (-xx
+        option). This should not be left on, since it includes
+        sensitive data such as passwords in the logs.
+      '';
+    };
+
   };
 
 in
@@ -66,6 +77,7 @@ in
     };
 
     systemd.services.freeradius = freeradiusService cfg;
+    warnings = optional cfg.debug "Freeradius debug logging is enabled. This will log passwords in plaintext to the journal!";
 
   };
 
diff --git a/nixos/modules/services/networking/gale.nix b/nixos/modules/services/networking/gale.nix
index 7083d87c407..cb954fd836b 100644
--- a/nixos/modules/services/networking/gale.nix
+++ b/nixos/modules/services/networking/gale.nix
@@ -104,14 +104,13 @@ in
          systemPackages = [ pkgs.gale ];
        };
 
-       users.users = [{
-         name = cfg.user;
+       users.users.${cfg.user} = {
          description = "Gale daemon";
          uid = config.ids.uids.gale;
          group = cfg.group;
          home = home;
          createHome = true;
-       }];
+       };
 
        users.groups = [{
          name = cfg.group;
diff --git a/nixos/modules/services/networking/git-daemon.nix b/nixos/modules/services/networking/git-daemon.nix
index a638a3083fb..52c895215fb 100644
--- a/nixos/modules/services/networking/git-daemon.nix
+++ b/nixos/modules/services/networking/git-daemon.nix
@@ -104,16 +104,16 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = if cfg.user != "git" then {} else singleton
-      { name = "git";
+    users.users = optionalAttrs (cfg.user == "git") {
+      git = {
         uid = config.ids.uids.git;
         description = "Git daemon user";
       };
+    };
 
-    users.groups = if cfg.group != "git" then {} else singleton
-      { name = "git";
-        gid = config.ids.gids.git;
-      };
+    users.groups = optionalAttrs (cfg.group == "git") {
+      git.gid = config.ids.gids.git;
+    };
 
     systemd.services.git-daemon = {
       after = [ "network.target" ];
diff --git a/nixos/modules/services/networking/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
index 178a832c166..69d4ed04775 100644
--- a/nixos/modules/services/networking/gnunet.nix
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -42,6 +42,7 @@ in
     services.gnunet = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to run the GNUnet daemon.  GNUnet is GNU's anonymous
@@ -51,6 +52,7 @@ in
 
       fileSharing = {
         quota = mkOption {
+          type = types.int;
           default = 1024;
           description = ''
             Maximum file system usage (in MiB) for file sharing.
@@ -60,6 +62,7 @@ in
 
       udp = {
         port = mkOption {
+          type = types.port;
           default = 2086;  # assigned by IANA
           description = ''
             The UDP port for use by GNUnet.
@@ -69,6 +72,7 @@ in
 
       tcp = {
         port = mkOption {
+          type = types.port;
           default = 2086;  # assigned by IANA
           description = ''
             The TCP port for use by GNUnet.
@@ -78,6 +82,7 @@ in
 
       load = {
         maxNetDownBandwidth = mkOption {
+          type = types.int;
           default = 50000;
           description = ''
             Maximum bandwidth usage (in bits per second) for GNUnet
@@ -86,6 +91,7 @@ in
         };
 
         maxNetUpBandwidth = mkOption {
+          type = types.int;
           default = 50000;
           description = ''
             Maximum bandwidth usage (in bits per second) for GNUnet
@@ -94,6 +100,7 @@ in
         };
 
         hardNetUpBandwidth = mkOption {
+          type = types.int;
           default = 0;
           description = ''
             Hard bandwidth limit (in bits per second) when uploading
@@ -111,6 +118,7 @@ in
       };
 
       extraOptions = mkOption {
+        type = types.lines;
         default = "";
         description = ''
           Additional options that will be copied verbatim in `gnunet.conf'.
diff --git a/nixos/modules/services/networking/gvpe.nix b/nixos/modules/services/networking/gvpe.nix
index 3ef3548e0a0..92e87cd4640 100644
--- a/nixos/modules/services/networking/gvpe.nix
+++ b/nixos/modules/services/networking/gvpe.nix
@@ -42,12 +42,8 @@ in
 {
   options = {
     services.gvpe = {
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to run gvpe
-        '';
-      };
+      enable = lib.mkEnableOption "gvpe";
+
       nodename = mkOption {
         default = null;
         description =''
diff --git a/nixos/modules/services/networking/hans.nix b/nixos/modules/services/networking/hans.nix
index 4f60300f5ff..8334dc68d62 100644
--- a/nixos/modules/services/networking/hans.nix
+++ b/nixos/modules/services/networking/hans.nix
@@ -135,8 +135,7 @@ in
       };
     };
 
-    users.users = singleton {
-      name = hansUser;
+    users.users.${hansUser} = {
       description = "Hans daemon user";
       isSystemUser = true;
     };
diff --git a/nixos/modules/services/networking/haproxy.nix b/nixos/modules/services/networking/haproxy.nix
index aff71e5e97d..4678829986c 100644
--- a/nixos/modules/services/networking/haproxy.nix
+++ b/nixos/modules/services/networking/haproxy.nix
@@ -26,6 +26,18 @@ with lib;
         '';
       };
 
+      user = mkOption {
+        type = types.str;
+        default = "haproxy";
+        description = "User account under which haproxy runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "haproxy";
+        description = "Group account under which haproxy runs.";
+      };
+
       config = mkOption {
         type = types.nullOr types.lines;
         default = null;
@@ -49,7 +61,8 @@ with lib;
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        DynamicUser = true;
+        User = cfg.user;
+        Group = cfg.group;
         Type = "notify";
         # when running the config test, don't be quiet so we can see what goes wrong
         ExecStartPre = "${pkgs.haproxy}/sbin/haproxy -c -f ${haproxyCfg}";
@@ -60,5 +73,16 @@ with lib;
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
       };
     };
+
+    users.users = optionalAttrs (cfg.user == "haproxy") {
+      haproxy = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "haproxy") {
+      haproxy = {};
+    };
   };
 }
diff --git a/nixos/modules/services/networking/helpers.nix b/nixos/modules/services/networking/helpers.nix
new file mode 100644
index 00000000000..d7d42de0e3a
--- /dev/null
+++ b/nixos/modules/services/networking/helpers.nix
@@ -0,0 +1,11 @@
+{ config, lib, ... }: ''
+  # Helper command to manipulate both the IPv4 and IPv6 tables.
+  ip46tables() {
+    iptables -w "$@"
+    ${
+      lib.optionalString config.networking.enableIPv6 ''
+        ip6tables -w "$@"
+      ''
+    }
+  }
+''
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 2915b54f05b..12c0626a941 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -49,6 +49,7 @@ in
     services.hostapd = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Enable putting a wireless interface into infrastructure mode,
diff --git a/nixos/modules/services/networking/i2pd.nix b/nixos/modules/services/networking/i2pd.nix
index f2be417738e..93a21fd4c97 100644
--- a/nixos/modules/services/networking/i2pd.nix
+++ b/nixos/modules/services/networking/i2pd.nix
@@ -159,7 +159,7 @@ let
       (strOpt "defaulturl" cfg.addressbook.defaulturl)
     ] ++ (optionalEmptyList "subscriptions" cfg.addressbook.subscriptions)
       ++ (flip map
-      (collect (proto: proto ? port && proto ? address && proto ? name) cfg.proto)
+      (collect (proto: proto ? port && proto ? address) cfg.proto)
       (proto: let protoOpts = [
         (sec proto.name)
         (boolOpt "enabled" proto.enable)
@@ -235,6 +235,10 @@ in
 
 {
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "i2pd" "extIp" ] [ "services" "i2pd" "address" ])
+  ];
+
   ###### interface
 
   options = {
@@ -602,7 +606,7 @@ in
 
       outTunnels = mkOption {
         default = {};
-        type = with types; loaOf (submodule (
+        type = with types; attrsOf (submodule (
           { name, ... }: {
             options = {
               destinationPort = mkOption {
@@ -623,7 +627,7 @@ in
 
       inTunnels = mkOption {
         default = {};
-        type = with types; loaOf (submodule (
+        type = with types; attrsOf (submodule (
           { name, ... }: {
             options = {
               inPort = mkOption {
diff --git a/nixos/modules/services/networking/iodine.nix b/nixos/modules/services/networking/iodine.nix
index 344f84374bb..46051d7044b 100644
--- a/nixos/modules/services/networking/iodine.nix
+++ b/nixos/modules/services/networking/iodine.nix
@@ -9,8 +9,17 @@ let
 
   iodinedUser = "iodined";
 
+  /* is this path made unreadable by ProtectHome = true ? */
+  isProtected = x: hasPrefix "/root" x || hasPrefix "/home" x;
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "iodined" "enable" ] [ "services" "iodine" "server" "enable" ])
+    (mkRenamedOptionModule [ "services" "iodined" "domain" ] [ "services" "iodine" "server" "domain" ])
+    (mkRenamedOptionModule [ "services" "iodined" "ip" ] [ "services" "iodine" "server" "ip" ])
+    (mkRenamedOptionModule [ "services" "iodined" "extraConfig" ] [ "services" "iodine" "server" "extraConfig" ])
+    (mkRemovedOptionModule [ "services" "iodined" "client" ] "")
+  ];
 
   ### configuration
 
@@ -28,45 +37,48 @@ in
           corresponding attribute name.
         '';
         example = literalExample ''
-        {
-          foo = {
-            server = "tunnel.mdomain.com";
-            relay = "8.8.8.8";
-            extraConfig = "-v";
+          {
+            foo = {
+              server = "tunnel.mdomain.com";
+              relay = "8.8.8.8";
+              extraConfig = "-v";
+            }
           }
-        }
         '';
-        type = types.attrsOf (types.submodule (
-        {
-          options = {
-            server = mkOption {
-              type = types.str;
-              default = "";
-              description = "Domain or Subdomain of server running iodined";
-              example = "tunnel.mydomain.com";
-            };
-
-            relay = mkOption {
-              type = types.str;
-              default = "";
-              description = "DNS server to use as a intermediate relay to the iodined server";
-              example = "8.8.8.8";
-            };
-
-            extraConfig = mkOption {
-              type = types.str;
-              default = "";
-              description = "Additional command line parameters";
-              example = "-l 192.168.1.10 -p 23";
-            };
-
-            passwordFile = mkOption {
-              type = types.str;
-              default = "";
-              description = "File that contains password";
-            };
-          };
-        }));
+        type = types.attrsOf (
+          types.submodule (
+            {
+              options = {
+                server = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "Hostname of server running iodined";
+                  example = "tunnel.mydomain.com";
+                };
+
+                relay = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "DNS server to use as an intermediate relay to the iodined server";
+                  example = "8.8.8.8";
+                };
+
+                extraConfig = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "Additional command line parameters";
+                  example = "-l 192.168.1.10 -p 23";
+                };
+
+                passwordFile = mkOption {
+                  type = types.str;
+                  default = "";
+                  description = "Path to a file containing the password.";
+                };
+              };
+            }
+          )
+        );
       };
 
       server = {
@@ -114,34 +126,69 @@ in
     boot.kernelModules = [ "tun" ];
 
     systemd.services =
-    let
-      createIodineClientService = name: cfg:
-      {
-        description = "iodine client - ${name}";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        script = "exec ${pkgs.iodine}/bin/iodine -f -u ${iodinedUser} ${cfg.extraConfig} ${optionalString (cfg.passwordFile != "") "< \"${cfg.passwordFile}\""} ${cfg.relay} ${cfg.server}";
-        serviceConfig = {
-          RestartSec = "30s";
-          Restart = "always";
+      let
+        createIodineClientService = name: cfg:
+          {
+            description = "iodine client - ${name}";
+            after = [ "network.target" ];
+            wantedBy = [ "multi-user.target" ];
+            script = "exec ${pkgs.iodine}/bin/iodine -f -u ${iodinedUser} ${cfg.extraConfig} ${optionalString (cfg.passwordFile != "") "< \"${builtins.toString cfg.passwordFile}\""} ${cfg.relay} ${cfg.server}";
+            serviceConfig = {
+              RestartSec = "30s";
+              Restart = "always";
+
+              # hardening :
+              # Filesystem access
+              ProtectSystem = "strict";
+              ProtectHome = if isProtected cfg.passwordFile then "read-only" else "true" ;
+              PrivateTmp = true;
+              ReadWritePaths = "/dev/net/tun";
+              PrivateDevices = false;
+              ProtectKernelTunables = true;
+              ProtectKernelModules = true;
+              ProtectControlGroups = true;
+              # Caps
+              NoNewPrivileges = true;
+              # Misc.
+              LockPersonality = true;
+              RestrictRealtime = true;
+              PrivateMounts = true;
+              MemoryDenyWriteExecute = true;
+            };
+          };
+      in
+        listToAttrs (
+          mapAttrsToList
+            (name: value: nameValuePair "iodine-${name}" (createIodineClientService name value))
+            cfg.clients
+        ) // {
+          iodined = mkIf (cfg.server.enable) {
+            description = "iodine, ip over dns server daemon";
+            after = [ "network.target" ];
+            wantedBy = [ "multi-user.target" ];
+            script = "exec ${pkgs.iodine}/bin/iodined -f -u ${iodinedUser} ${cfg.server.extraConfig} ${optionalString (cfg.server.passwordFile != "") "< \"${builtins.toString cfg.server.passwordFile}\""} ${cfg.server.ip} ${cfg.server.domain}";
+            serviceConfig = {
+              # Filesystem access
+              ProtectSystem = "strict";
+              ProtectHome = if isProtected cfg.server.passwordFile then "read-only" else "true" ;
+              PrivateTmp = true;
+              ReadWritePaths = "/dev/net/tun";
+              PrivateDevices = false;
+              ProtectKernelTunables = true;
+              ProtectKernelModules = true;
+              ProtectControlGroups = true;
+              # Caps
+              NoNewPrivileges = true;
+              # Misc.
+              LockPersonality = true;
+              RestrictRealtime = true;
+              PrivateMounts = true;
+              MemoryDenyWriteExecute = true;
+            };
+          };
         };
-      };
-    in
-    listToAttrs (
-      mapAttrsToList
-        (name: value: nameValuePair "iodine-${name}" (createIodineClientService name value))
-        cfg.clients
-    ) // {
-      iodined = mkIf (cfg.server.enable) {
-        description = "iodine, ip over dns server daemon";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        script = "exec ${pkgs.iodine}/bin/iodined -f -u ${iodinedUser} ${cfg.server.extraConfig} ${optionalString (cfg.server.passwordFile != "") "< \"${cfg.server.passwordFile}\""} ${cfg.server.ip} ${cfg.server.domain}";
-      };
-    };
 
-    users.users = singleton {
-      name = iodinedUser;
+    users.users.${iodinedUser} = {
       uid = config.ids.uids.iodined;
       description = "Iodine daemon user";
     };
diff --git a/nixos/modules/services/networking/ircd-hybrid/default.nix b/nixos/modules/services/networking/ircd-hybrid/default.nix
index f5abe61a1ba..91d0bf437d6 100644
--- a/nixos/modules/services/networking/ircd-hybrid/default.nix
+++ b/nixos/modules/services/networking/ircd-hybrid/default.nix
@@ -36,12 +36,7 @@ in
 
     services.ircdHybrid = {
 
-      enable = mkOption {
-        default = false;
-        description = "
-          Enable IRCD.
-        ";
-      };
+      enable = mkEnableOption "IRCD";
 
       serverName = mkOption {
         default = "hades.arpa";
@@ -112,9 +107,8 @@ in
 
   config = mkIf config.services.ircdHybrid.enable {
 
-    users.users = singleton
-      { name = "ircd";
-        description = "IRCD owner";
+    users.users.ircd =
+      { description = "IRCD owner";
         group = "ircd";
         uid = config.ids.uids.ircd;
       };
diff --git a/nixos/modules/services/networking/iwd.nix b/nixos/modules/services/networking/iwd.nix
index 839fa48d9a4..6be67a8b96f 100644
--- a/nixos/modules/services/networking/iwd.nix
+++ b/nixos/modules/services/networking/iwd.nix
@@ -23,12 +23,7 @@ in {
     systemd.packages = [ pkgs.iwd ];
 
     systemd.services.iwd.wantedBy = [ "multi-user.target" ];
-
-    systemd.tmpfiles.rules = [
-      "d /var/lib/iwd 0700 root root -"
-      "d /var/lib/ead 0700 root root -"
-    ];
   };
 
-  meta.maintainers = with lib.maintainers; [ mic92 ];
+  meta.maintainers = with lib.maintainers; [ mic92 dtzWill ];
 }
diff --git a/nixos/modules/services/networking/keybase.nix b/nixos/modules/services/networking/keybase.nix
index 85f52be8a6a..495102cb7ee 100644
--- a/nixos/modules/services/networking/keybase.nix
+++ b/nixos/modules/services/networking/keybase.nix
@@ -24,13 +24,18 @@ in {
 
   config = mkIf cfg.enable {
 
+    # Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase.service
     systemd.user.services.keybase = {
       description = "Keybase service";
       unitConfig.ConditionUser = "!@system";
+      environment.KEYBASE_SERVICE_TYPE = "systemd";
       serviceConfig = {
-        ExecStart = ''
-          ${pkgs.keybase}/bin/keybase service --auto-forked
-        '';
+        Type = "notify";
+        EnvironmentFile = [
+          "-%E/keybase/keybase.autogen.env"
+          "-%E/keybase/keybase.env"
+        ];
+        ExecStart = "${pkgs.keybase}/bin/keybase service";
         Restart = "on-failure";
         PrivateTmp = true;
       };
diff --git a/nixos/modules/services/networking/kippo.nix b/nixos/modules/services/networking/kippo.nix
index bdea6a1d1ca..553415a2f32 100644
--- a/nixos/modules/services/networking/kippo.nix
+++ b/nixos/modules/services/networking/kippo.nix
@@ -73,12 +73,11 @@ in
         ${cfg.extraConfig}
     '';
 
-    users.users = singleton {
-      name = "kippo";
+    users.users.kippo = {
       description = "kippo web server privilege separation user";
       uid = 108; # why does config.ids.uids.kippo give an error?
     };
-    users.groups = singleton { name = "kippo";gid=108; };
+    users.groups.kippo.gid = 108;
 
     systemd.services.kippo = with pkgs; {
       description = "Kippo Web Server";
diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix
index 1cc1dd3f2f6..12ff89fe849 100644
--- a/nixos/modules/services/networking/knot.nix
+++ b/nixos/modules/services/networking/knot.nix
@@ -5,14 +5,16 @@ with lib;
 let
   cfg = config.services.knot;
 
-  configFile = pkgs.writeText "knot.conf" cfg.extraConfig;
-  socketFile = "/run/knot/knot.sock";
+  configFile = pkgs.writeTextFile {
+    name = "knot.conf";
+    text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" +
+           cfg.extraConfig;
+    checkPhase = lib.optionalString (cfg.keyFiles == []) ''
+      ${cfg.package}/bin/knotc --config=$out conf-check
+    '';
+  };
 
-  knotConfCheck = file: pkgs.runCommand "knot-config-checked"
-    { buildInputs = [ cfg.package ]; } ''
-    ln -s ${configFile} $out
-    knotc --config=${configFile} conf-check
-  '';
+  socketFile = "/run/knot/knot.sock";
 
   knot-cli-wrappers = pkgs.stdenv.mkDerivation {
     name = "knot-cli-wrappers";
@@ -45,6 +47,19 @@ in {
         '';
       };
 
+      keyFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          A list of files containing additional configuration
+          to be included using the include directive. This option
+          allows to include configuration like TSIG keys without
+          exposing them to the nix store readable to any process.
+          Note that using this option will also disable configuration
+          checks at build time.
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "";
@@ -56,6 +71,7 @@ in {
       package = mkOption {
         type = types.package;
         default = pkgs.knot-dns;
+        defaultText = "pkgs.knot-dns";
         description = ''
           Which Knot DNS package to use
         '';
@@ -64,6 +80,13 @@ in {
   };
 
   config = mkIf config.services.knot.enable {
+    users.users.knot = {
+      isSystemUser = true;
+      group = "knot";
+      description = "Knot daemon user";
+    };
+
+    users.groups.knot.gid = null;
     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;
@@ -73,12 +96,12 @@ in {
 
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${cfg.package}/bin/knotd --config=${knotConfCheck configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}";
+        ExecStart = "${cfg.package}/bin/knotd --config=${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";
+        User = "knot";
         RuntimeDirectory = "knot";
         StateDirectory = "knot";
         StateDirectoryMode = "0700";
@@ -92,4 +115,3 @@ in {
     environment.systemPackages = [ knot-cli-wrappers ];
   };
 }
-
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index fc516c01230..c5a84eebd46 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -3,16 +3,53 @@
 with lib;
 
 let
-
   cfg = config.services.kresd;
-  package = pkgs.knot-resolver;
 
-  configFile = pkgs.writeText "kresd.conf" cfg.extraConfig;
-in
+  # Convert systemd-style address specification to kresd config line(s).
+  # On Nix level we don't attempt to precisely validate the address specifications.
+  mkListen = kind: addr: let
+    al_v4 = builtins.match "([0-9.]\+):([0-9]\+)" addr;
+    al_v6 = builtins.match "\\[(.\+)]:([0-9]\+)" addr;
+    al_portOnly = builtins.match "()([0-9]\+)" addr;
+    al = findFirst (a: a != null)
+      (throw "services.kresd.*: incorrect address specification '${addr}'")
+      [ al_v4 al_v6 al_portOnly ];
+    port = last al;
+    addrSpec = if al_portOnly == null then "'${head al}'" else "{'::', '127.0.0.1'}";
+    in # freebind is set for compatibility with earlier kresd services;
+       # it could be configurable, for example.
+      ''
+        net.listen(${addrSpec}, ${port}, { kind = '${kind}', freebind = true })
+      '';
 
-{
+  configFile = pkgs.writeText "kresd.conf" (
+    optionalString (cfg.listenDoH != []) ''
+      modules.load('http')
+    ''
+    + concatMapStrings (mkListen "dns") cfg.listenPlain
+    + concatMapStrings (mkListen "tls") cfg.listenTLS
+    + concatMapStrings (mkListen "doh") cfg.listenDoH
+    + cfg.extraConfig
+  );
+
+  package = if cfg.listenDoH == []
+    then pkgs.knot-resolver # never force `extraFeatures = false`
+    else pkgs.knot-resolver.override { extraFeatures = true; };
+in {
   meta.maintainers = [ maintainers.vcunat /* upstream developer */ ];
 
+  imports = [
+    (mkChangedOptionModule [ "services" "kresd" "interfaces" ] [ "services" "kresd" "listenPlain" ]
+      (config:
+        let value = getAttrFromPath [ "services" "kresd" "interfaces" ] config;
+        in map
+          (iface: if elem ":" (stringToCharacters iface) then "[${iface}]:53" else "${iface}:53") # Syntax depends on being IPv6 or IPv4.
+          value
+      )
+    )
+    (mkRemovedOptionModule [ "services" "kresd" "cacheDir" ] "Please use (bind-)mounting instead.")
+  ];
+
   ###### interface
   options.services.kresd = {
     enable = mkOption {
@@ -21,8 +58,8 @@ in
       description = ''
         Whether to enable knot-resolver domain name server.
         DNSSEC validation is turned on by default.
-        You can run <literal>sudo nc -U /run/kresd/control</literal>
-        and give commands interactively to kresd.
+        You can run <literal>sudo nc -U /run/knot-resolver/control/1</literal>
+        and give commands interactively to kresd@1.service.
       '';
     };
     extraConfig = mkOption {
@@ -32,105 +69,77 @@ in
         Extra lines to be added verbatim to the generated configuration file.
       '';
     };
-    cacheDir = mkOption {
-      type = types.path;
-      default = "/var/cache/kresd";
+    listenPlain = mkOption {
+      type = with types; listOf str;
+      default = [ "[::1]:53" "127.0.0.1:53" ];
+      example = [ "53" ];
       description = ''
-        Directory for caches.  They are intended to survive reboots.
+        What addresses and ports the server should listen on.
+        For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
-    interfaces = mkOption {
+    listenTLS = mkOption {
       type = with types; listOf str;
-      default = [ "::1" "127.0.0.1" ];
+      default = [];
+      example = [ "198.51.100.1:853" "[2001:db8::1]:853" "853" ];
       description = ''
-        What addresses the server should listen on. (UDP+TCP 53)
+        Addresses and ports on which kresd should provide DNS over TLS (see RFC 7858).
+        For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
-    listenTLS = mkOption {
+    listenDoH = mkOption {
       type = with types; listOf str;
       default = [];
-      example = [ "198.51.100.1:853" "[2001:db8::1]:853" "853" ];
+      example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
       description = ''
-        Addresses on which kresd should provide DNS over TLS (see RFC 7858).
+        Addresses and ports on which kresd should provide DNS over HTTPS (see RFC 8484).
         For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
+    instances = mkOption {
+      type = types.ints.unsigned;
+      default = 1;
+      description = ''
+        The number of instances to start.  They will be called kresd@{1,2,...}.service.
+        Knot Resolver uses no threads, so this is the way to scale.
+        You can dynamically start/stop them at will, so this is just system default.
+      '';
+    };
     # TODO: perhaps options for more common stuff like cache size or forwarding
   };
 
   ###### implementation
   config = mkIf cfg.enable {
-    environment.etc."kresd.conf".source = configFile; # not required
+    environment.etc."knot-resolver/kresd.conf".source = configFile; # not required
 
-    users.users = singleton
-      { name = "kresd";
-        uid = config.ids.uids.kresd;
-        group = "kresd";
+    users.users.knot-resolver =
+      { isSystemUser = true;
+        group = "knot-resolver";
         description = "Knot-resolver daemon user";
       };
-    users.groups = singleton
-      { name = "kresd";
-        gid = config.ids.gids.kresd;
-      };
+    users.groups.knot-resolver.gid = null;
 
-    systemd.sockets.kresd = rec {
-      wantedBy = [ "sockets.target" ];
-      before = wantedBy;
-      listenStreams = map
-        # Syntax depends on being IPv6 or IPv4.
-        (iface: if elem ":" (stringToCharacters iface) then "[${iface}]:53" else "${iface}:53")
-        cfg.interfaces;
-      socketConfig = {
-        ListenDatagram = listenStreams;
-        FreeBind = true;
-        FileDescriptorName = "dns";
-      };
-    };
+    systemd.packages = [ package ]; # the units are patched inside the package a bit
 
-    systemd.sockets.kresd-tls = mkIf (cfg.listenTLS != []) rec {
-      wantedBy = [ "sockets.target" ];
-      before = wantedBy;
-      partOf = [ "kresd.socket" ];
-      listenStreams = cfg.listenTLS;
-      socketConfig = {
-        FileDescriptorName = "tls";
-        FreeBind = true;
-        Service = "kresd.service";
-      };
+    systemd.targets.kresd = { # configure units started by default
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "kres-cache-gc.service" ]
+        ++ map (i: "kresd@${toString i}.service") (range 1 cfg.instances);
     };
-
-    systemd.sockets.kresd-control = rec {
-      wantedBy = [ "sockets.target" ];
-      before = wantedBy;
-      partOf = [ "kresd.socket" ];
-      listenStreams = [ "/run/kresd/control" ];
-      socketConfig = {
-        FileDescriptorName = "control";
-        Service = "kresd.service";
-        SocketMode = "0660"; # only root user/group may connect and control kresd
-      };
+    systemd.services."kresd@".serviceConfig = {
+      ExecStart = "${package}/bin/kresd --noninteractive "
+        + "-c ${package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
+      # Ensure correct ownership in case UID or GID changes.
+      CacheDirectory = "knot-resolver";
+      CacheDirectoryMode = "0750";
     };
 
-    systemd.tmpfiles.rules = [ "d '${cfg.cacheDir}' 0770 kresd kresd - -" ];
+    environment.etc."tmpfiles.d/knot-resolver.conf".source =
+      "${package}/lib/tmpfiles.d/knot-resolver.conf";
 
-    systemd.services.kresd = {
-      description = "Knot-resolver daemon";
-
-      serviceConfig = {
-        User = "kresd";
-        Type = "notify";
-        WorkingDirectory = cfg.cacheDir;
-        Restart = "on-failure";
-        Sockets = [ "kresd.socket" "kresd-control.socket" ]
-          ++ optional (cfg.listenTLS != []) "kresd-tls.socket";
-      };
-
-      # Trust anchor goes from dns-root-data by default.
-      script = ''
-        exec '${package}/bin/kresd' --config '${configFile}' --forks=1
-      '';
-
-      requires = [ "kresd.socket" ];
-    };
+    # Try cleaning up the previously default location of cache file.
+    # Note that /var/cache/* should always be safe to remove.
+    # TODO: remove later, probably between 20.09 and 21.03
+    systemd.tmpfiles.rules = [ "R /var/cache/kresd" ];
   };
 }
diff --git a/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix b/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
new file mode 100644
index 00000000000..09d357cd2b6
--- /dev/null
+++ b/nixos/modules/services/networking/magic-wormhole-mailbox-server.nix
@@ -0,0 +1,28 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.magic-wormhole-mailbox-server;
+  dataDir = "/var/lib/magic-wormhole-mailbox-server;";
+  python = pkgs.python3.withPackages (py: [ py.magic-wormhole-mailbox-server py.twisted ]);
+in
+{
+  options.services.magic-wormhole-mailbox-server = {
+    enable = mkEnableOption "Enable Magic Wormhole Mailbox Server";
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.magic-wormhole-mailbox-server = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${python}/bin/twistd --nodaemon wormhole-mailbox";
+        WorkingDirectory = dataDir;
+        StateDirectory = baseNameOf dataDir;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/networking/mailpile.nix b/nixos/modules/services/networking/mailpile.nix
index c42d3d5a44c..b79ee11d17d 100644
--- a/nixos/modules/services/networking/mailpile.nix
+++ b/nixos/modules/services/networking/mailpile.nix
@@ -18,12 +18,8 @@ in
   options = {
 
     services.mailpile = {
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to enable Mailpile the mail client.
-        ";
-      };
+      enable = mkEnableOption "Mailpile the mail client";
+
       hostname = mkOption {
         default = "localhost";
         description = "Listen to this hostname or ip.";
diff --git a/nixos/modules/services/networking/matterbridge.nix b/nixos/modules/services/networking/matterbridge.nix
index 682eaa6eb29..b8b4f37c84a 100644
--- a/nixos/modules/services/networking/matterbridge.nix
+++ b/nixos/modules/services/networking/matterbridge.nix
@@ -92,14 +92,15 @@ in
     warnings = optional options.services.matterbridge.configFile.isDefined
       "The option services.matterbridge.configFile is insecure and should be replaced with services.matterbridge.configPath";
 
-    users.users = optional (cfg.user == "matterbridge")
-      { name = "matterbridge";
-        group = "matterbridge";
-        isSystemUser = true;
+    users.users = optionalAttrs (cfg.user == "matterbridge")
+      { matterbridge = {
+          group = "matterbridge";
+          isSystemUser = true;
+        };
       };
 
-    users.groups = optional (cfg.group == "matterbridge")
-      { name = "matterbridge";
+    users.groups = optionalAttrs (cfg.group == "matterbridge")
+      { matterbridge = { };
       };
 
     systemd.services.matterbridge = {
@@ -110,7 +111,7 @@ in
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
-        ExecStart = "${pkgs.matterbridge.bin}/bin/matterbridge -conf ${matterbridgeConfToml}";
+        ExecStart = "${pkgs.matterbridge}/bin/matterbridge -conf ${matterbridgeConfToml}";
         Restart = "always";
         RestartSec = "10";
       };
diff --git a/nixos/modules/services/networking/minidlna.nix b/nixos/modules/services/networking/minidlna.nix
index 3ddea3c9757..c580ba47dad 100644
--- a/nixos/modules/services/networking/minidlna.nix
+++ b/nixos/modules/services/networking/minidlna.nix
@@ -95,6 +95,22 @@ in
         '';
     };
 
+    services.minidlna.announceInterval = mkOption {
+      type = types.int;
+      default = 895;
+      description =
+        ''
+          The interval between announces (in seconds).
+
+          By default miniDLNA will announce its presence on the network
+          approximately every 15 minutes.
+
+          Many people prefer shorter announce intervals (e.g. 60 seconds)
+          on their home networks, especially when DLNA clients are
+          started on demand.
+        '';
+    };
+
     services.minidlna.config = mkOption {
       type = types.lines;
       description =
@@ -144,6 +160,7 @@ in
         ${concatMapStrings (dir: ''
           media_dir=${dir}
         '') cfg.mediaDirs}
+        notify_interval=${toString cfg.announceInterval}
         ${cfg.extraConfig}
       '';
 
diff --git a/nixos/modules/services/networking/mjpg-streamer.nix b/nixos/modules/services/networking/mjpg-streamer.nix
index e0a6c112e3c..dbc35e2e71c 100644
--- a/nixos/modules/services/networking/mjpg-streamer.nix
+++ b/nixos/modules/services/networking/mjpg-streamer.nix
@@ -49,10 +49,11 @@ in {
 
   config = mkIf cfg.enable {
 
-    users.users = optional (cfg.user == "mjpg-streamer") {
-      name = "mjpg-streamer";
-      uid = config.ids.uids.mjpg-streamer;
-      group = cfg.group;
+    users.users = optionalAttrs (cfg.user == "mjpg-streamer") {
+      mjpg-streamer = {
+        uid = config.ids.uids.mjpg-streamer;
+        group = cfg.group;
+      };
     };
 
     systemd.services.mjpg-streamer = {
diff --git a/nixos/modules/services/networking/monero.nix b/nixos/modules/services/networking/monero.nix
index 831e4d60d8d..b9536430868 100644
--- a/nixos/modules/services/networking/monero.nix
+++ b/nixos/modules/services/networking/monero.nix
@@ -197,17 +197,15 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton {
-      name = "monero";
+    users.users.monero = {
       uid  = config.ids.uids.monero;
       description = "Monero daemon user";
       home = dataDir;
       createHome = true;
     };
 
-    users.groups = singleton {
-      name = "monero";
-      gid  = config.ids.gids.monero;
+    users.groups.monero = {
+      gid = config.ids.gids.monero;
     };
 
     systemd.services.monero = {
@@ -224,15 +222,17 @@ in
       };
     };
 
-   assertions = singleton {
-     assertion = cfg.mining.enable -> cfg.mining.address != "";
-     message   = ''
+    assertions = singleton {
+      assertion = cfg.mining.enable -> cfg.mining.address != "";
+      message   = ''
        You need a Monero address to receive mining rewards:
        specify one using option monero.mining.address.
-    '';
-   };
+      '';
+    };
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
 
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
new file mode 100644
index 00000000000..cc98414257c
--- /dev/null
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.mullvad-vpn;
+in
+with lib;
+{
+  options.services.mullvad-vpn.enable = mkOption {
+    type = types.bool;
+    default = false;
+    description = ''
+      This option enables Mullvad VPN daemon.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "tun" ];
+
+    systemd.services.mullvad-daemon = {
+      description = "Mullvad VPN daemon";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      after = [
+        "network-online.target"
+        "NetworkManager.service"
+        "systemd-resolved.service"
+      ];
+      path = [
+        pkgs.iproute
+        # Needed for ping
+        "/run/wrappers"
+      ];
+      serviceConfig = {
+        StartLimitBurst = 5;
+        StartLimitIntervalSec = 20;
+        ExecStart = "${pkgs.mullvad-vpn}/bin/mullvad-daemon -v --disable-stdout-timestamps";
+        Restart = "always";
+        RestartSec = 1;
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.xfix ];
+}
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index 082953d2f6a..3054ae1b201 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -46,6 +46,11 @@ let
   '';
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "murmur" "welcome" ] [ "services" "murmur" "welcometext" ])
+    (mkRemovedOptionModule [ "services" "murmur" "pidfile" ] "Hardcoded to /run/murmur/murmurd.pid now")
+  ];
+
   options = {
     services.murmur = {
       enable = mkOption {
diff --git a/nixos/modules/services/networking/mxisd.nix b/nixos/modules/services/networking/mxisd.nix
index a3d61922e57..482d6ff456b 100644
--- a/nixos/modules/services/networking/mxisd.nix
+++ b/nixos/modules/services/networking/mxisd.nix
@@ -93,23 +93,19 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.users = [
+    users.users.mxisd =
       {
-        name = "mxisd";
         group = "mxisd";
         home = cfg.dataDir;
         createHome = true;
         shell = "${pkgs.bash}/bin/bash";
         uid = config.ids.uids.mxisd;
-      }
-    ];
+      };
 
-    users.groups = [
+    users.groups.mxisd =
       {
-        name = "mxisd";
         gid = config.ids.gids.mxisd;
-      }
-    ];
+      };
 
     systemd.services.mxisd = {
       description = "a federated identity server for the matrix ecosystem";
diff --git a/nixos/modules/services/networking/namecoind.nix b/nixos/modules/services/networking/namecoind.nix
index c8ee0a2f564..ead7f085943 100644
--- a/nixos/modules/services/networking/namecoind.nix
+++ b/nixos/modules/services/networking/namecoind.nix
@@ -154,16 +154,14 @@ in
       config = ${configFile}
     '';
 
-    users.users = singleton {
-      name = "namecoin";
+    users.users.namecoin = {
       uid  = config.ids.uids.namecoin;
       description = "Namecoin daemon user";
       home = dataDir;
       createHome = true;
     };
 
-    users.groups = singleton {
-      name = "namecoin";
+    users.groups.namecoin = {
       gid  = config.ids.gids.namecoin;
     };
 
@@ -201,4 +199,6 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index 5681bda51cb..21ae9eb8b6d 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -7,26 +7,33 @@
 with lib;
 
 let
-
   cfg = config.networking.nat;
 
   dest = if cfg.externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${cfg.externalIP}";
 
+  helpers = import ./helpers.nix { inherit config lib; };
+
   flushNat = ''
-    iptables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
-    iptables -w -t nat -F nixos-nat-pre 2>/dev/null || true
-    iptables -w -t nat -X nixos-nat-pre 2>/dev/null || true
-    iptables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
-    iptables -w -t nat -F nixos-nat-post 2>/dev/null || true
-    iptables -w -t nat -X nixos-nat-post 2>/dev/null || true
+    ${helpers}
+    ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
+    ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
+    ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
+    ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
 
     ${cfg.extraStopCommands}
   '';
 
   setupNat = ''
+    ${helpers}
     # Create subchain where we store rules
-    iptables -w -t nat -N nixos-nat-pre
-    iptables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-pre
+    ip46tables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-out
 
     # We can't match on incoming interface in POSTROUTING, so
     # mark packets coming from the internal interfaces.
@@ -58,10 +65,10 @@ let
         let
           m                = builtins.match "([0-9.]+):([0-9-]+)" fwd.destination;
           destinationIP    = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
-          destinationPorts = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 1;
+          destinationPorts = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
         in ''
           # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
-          iptables -w -t nat -A OUTPUT \
+          iptables -w -t nat -A nixos-nat-out \
             -d ${loopbackip} -p ${fwd.proto} \
             --dport ${builtins.toString fwd.sourcePort} \
             -j DNAT --to-destination ${fwd.destination}
@@ -88,8 +95,9 @@ let
     ${cfg.extraCommands}
 
     # Append our chains to the nat tables
-    iptables -w -t nat -A PREROUTING -j nixos-nat-pre
-    iptables -w -t nat -A POSTROUTING -j nixos-nat-post
+    ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
+    ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
+    ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
   '';
 
 in
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 92088623517..77e979a8a42 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -43,7 +43,7 @@ let
       timeout = mkOption {
         type = types.int;
         description = ''
-          Controls how long to wait for a Neighbor Advertisment Message before 
+          Controls how long to wait for a Neighbor Advertisment Message before
           invalidating the entry, in milliseconds.
         '';
         default = 500;
@@ -51,7 +51,7 @@ let
       ttl = mkOption {
         type = types.int;
         description = ''
-          Controls how long a valid or invalid entry remains in the cache, in 
+          Controls how long a valid or invalid entry remains in the cache, in
           milliseconds.
         '';
         default = 30000;
@@ -142,7 +142,11 @@ in {
         messages, and respond to them according to a set of rules.
       '';
       default = {};
-      example = { eth0.rules."1111::/64" = {}; };
+      example = literalExample ''
+        {
+          eth0.rules."1111::/64" = {};
+        }
+      '';
     };
   };
 
@@ -161,7 +165,25 @@ in {
       documentation = [ "man:ndppd(1)" "man:ndppd.conf(5)" ];
       after = [ "network-pre.target" ];
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
+      serviceConfig = {
+        ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
+
+        # Sandboxing
+        CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = "AF_INET6 AF_PACKET AF_NETLINK";
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 90d1032c41b..6f24141b33c 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -131,6 +131,10 @@ let
 
 in {
 
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
   ###### interface
 
   options = {
@@ -308,6 +312,7 @@ in {
 
                 if [ "$2" != "up" ]; then
                     logger "exit: event $2 != up"
+                    exit
                 fi
 
                 # coreutils and iproute are in PATH too
@@ -336,6 +341,7 @@ in {
   };
 
   imports = [
+    (mkRenamedOptionModule [ "networking" "networkmanager" "useDnsmasq" ] [ "networking" "networkmanager" "dns" ])
     (mkRemovedOptionModule ["networking" "networkmanager" "dynamicHosts"] ''
       This option was removed because allowing (multiple) regular users to
       override host entries affecting the whole system opens up a huge attack
@@ -360,62 +366,59 @@ in {
       }
     ];
 
-    environment.etc = with pkgs; [
-      { source = configFile;
-        target = "NetworkManager/NetworkManager.conf";
-      }
-      { source = "${networkmanager-openvpn}/lib/NetworkManager/VPN/nm-openvpn-service.name";
-        target = "NetworkManager/VPN/nm-openvpn-service.name";
-      }
-      { source = "${networkmanager-vpnc}/lib/NetworkManager/VPN/nm-vpnc-service.name";
-        target = "NetworkManager/VPN/nm-vpnc-service.name";
-      }
-      { source = "${networkmanager-openconnect}/lib/NetworkManager/VPN/nm-openconnect-service.name";
-        target = "NetworkManager/VPN/nm-openconnect-service.name";
-      }
-      { source = "${networkmanager-fortisslvpn}/lib/NetworkManager/VPN/nm-fortisslvpn-service.name";
-        target = "NetworkManager/VPN/nm-fortisslvpn-service.name";
-      }
-      { source = "${networkmanager-l2tp}/lib/NetworkManager/VPN/nm-l2tp-service.name";
-        target = "NetworkManager/VPN/nm-l2tp-service.name";
-      }
-      { source = "${networkmanager-iodine}/lib/NetworkManager/VPN/nm-iodine-service.name";
-        target = "NetworkManager/VPN/nm-iodine-service.name";
+    environment.etc = with pkgs; {
+      "NetworkManager/NetworkManager.conf".source = configFile;
+
+      "NetworkManager/VPN/nm-openvpn-service.name".source =
+        "${networkmanager-openvpn}/lib/NetworkManager/VPN/nm-openvpn-service.name";
+
+      "NetworkManager/VPN/nm-vpnc-service.name".source =
+        "${networkmanager-vpnc}/lib/NetworkManager/VPN/nm-vpnc-service.name";
+
+      "NetworkManager/VPN/nm-openconnect-service.name".source =
+        "${networkmanager-openconnect}/lib/NetworkManager/VPN/nm-openconnect-service.name";
+
+      "NetworkManager/VPN/nm-fortisslvpn-service.name".source =
+        "${networkmanager-fortisslvpn}/lib/NetworkManager/VPN/nm-fortisslvpn-service.name";
+
+      "NetworkManager/VPN/nm-l2tp-service.name".source =
+        "${networkmanager-l2tp}/lib/NetworkManager/VPN/nm-l2tp-service.name";
+
+      "NetworkManager/VPN/nm-iodine-service.name".source =
+        "${networkmanager-iodine}/lib/NetworkManager/VPN/nm-iodine-service.name";
       }
-    ] ++ optional (cfg.appendNameservers != [] || cfg.insertNameservers != [])
-           { source = overrideNameserversScript;
-             target = "NetworkManager/dispatcher.d/02overridedns";
-           }
-      ++ lib.imap1 (i: s: {
-        inherit (s) source;
-        target = "NetworkManager/dispatcher.d/${dispatcherTypesSubdirMap.${s.type}}03userscript${lib.fixedWidthNumber 4 i}";
-        mode = "0544";
-      }) cfg.dispatcherScripts
-      ++ optional cfg.enableStrongSwan
-           { source = "${pkgs.networkmanager_strongswan}/lib/NetworkManager/VPN/nm-strongswan-service.name";
-             target = "NetworkManager/VPN/nm-strongswan-service.name";
-           };
+      // optionalAttrs (cfg.appendNameservers != [] || cfg.insertNameservers != [])
+         {
+           "NetworkManager/dispatcher.d/02overridedns".source = overrideNameserversScript;
+         }
+      // optionalAttrs cfg.enableStrongSwan
+         {
+           "NetworkManager/VPN/nm-strongswan-service.name".source =
+             "${pkgs.networkmanager_strongswan}/lib/NetworkManager/VPN/nm-strongswan-service.name";
+         }
+      // listToAttrs (lib.imap1 (i: s:
+         {
+            name = "NetworkManager/dispatcher.d/${dispatcherTypesSubdirMap.${s.type}}03userscript${lib.fixedWidthNumber 4 i}";
+            value = { mode = "0544"; inherit (s) source; };
+         }) cfg.dispatcherScripts);
 
     environment.systemPackages = cfg.packages;
 
-    users.groups = [{
-      name = "networkmanager";
-      gid = config.ids.gids.networkmanager;
-    }
-    {
-      name = "nm-openvpn";
-      gid = config.ids.gids.nm-openvpn;
-    }];
-    users.users = [{
-      name = "nm-openvpn";
-      uid = config.ids.uids.nm-openvpn;
-      extraGroups = [ "networkmanager" ];
-    }
-    {
-      name = "nm-iodine";
-      isSystemUser = true;
-      group = "networkmanager";
-    }];
+    users.groups = {
+      networkmanager.gid = config.ids.gids.networkmanager;
+      nm-openvpn.gid = config.ids.gids.nm-openvpn;
+    };
+
+    users.users = {
+      nm-openvpn = {
+        uid = config.ids.uids.nm-openvpn;
+        extraGroups = [ "networkmanager" ];
+      };
+      nm-iodine = {
+        isSystemUser = true;
+        group = "networkmanager";
+      };
+    };
 
     systemd.packages = cfg.packages;
 
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index ad7c013a544..ec9d9753cfe 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -52,7 +52,7 @@ in
             ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
 
             # allow "ping"
-            ip6 nexthdr icmp icmpv6 type echo-request accept
+            ip6 nexthdr icmpv6 icmpv6 type echo-request accept
             ip protocol icmp icmp type echo-request accept
 
             # accept SSH connections (required for a server)
diff --git a/nixos/modules/services/networking/nix-store-gcs-proxy.nix b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
new file mode 100644
index 00000000000..3f2ce5bca4d
--- /dev/null
+++ b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
@@ -0,0 +1,75 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  opts = { name, config, ... }: {
+    options = {
+      enable = mkOption {
+        default = true;
+        type = types.bool;
+        example = true;
+        description = "Whether to enable proxy for this bucket";
+      };
+      bucketName = mkOption {
+        type = types.str;
+        default = name;
+        example = "my-bucket-name";
+        description = "Name of Google storage bucket";
+      };
+      address = mkOption {
+        type = types.str;
+        example = "localhost:3000";
+        description = "The address of the proxy.";
+      };
+    };
+  };
+  enabledProxies = lib.filterAttrs (n: v: v.enable) config.services.nix-store-gcs-proxy;
+  mapProxies = function: lib.mkMerge (lib.mapAttrsToList function enabledProxies);
+in
+{
+  options.services.nix-store-gcs-proxy = mkOption {
+    type = types.attrsOf (types.submodule opts);
+    default = {};
+    description = ''
+      An attribute set describing an HTTP to GCS proxy that allows us to use GCS
+      bucket via HTTP protocol.
+    '';
+  };
+
+  config.systemd.services = mapProxies (name: cfg: {
+    "nix-store-gcs-proxy-${name}" = {
+      description = "A HTTP nix store that proxies requests to Google Storage";
+      wantedBy = ["multi-user.target"];
+
+      serviceConfig = {
+        RestartSec = 5;
+        StartLimitInterval = 10;
+        ExecStart = ''
+          ${pkgs.nix-store-gcs-proxy}/bin/nix-store-gcs-proxy \
+            --bucket-name ${cfg.bucketName} \
+            --addr ${cfg.address}
+        '';
+
+        DynamicUser = true;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+      };
+    };
+  });
+
+  meta.maintainers = [ maintainers.mrkkrp ];
+}
diff --git a/nixos/modules/services/networking/nntp-proxy.nix b/nixos/modules/services/networking/nntp-proxy.nix
index d24d6f77a49..cc061bf6e3b 100644
--- a/nixos/modules/services/networking/nntp-proxy.nix
+++ b/nixos/modules/services/networking/nntp-proxy.nix
@@ -210,9 +210,8 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton
-      { name = proxyUser;
-        uid = config.ids.uids.nntp-proxy;
+    users.users.${proxyUser} =
+      { uid = config.ids.uids.nntp-proxy;
         description = "NNTP-Proxy daemon user";
       };
 
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index bc0966e6b8e..429580e5c6c 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -244,7 +244,7 @@ let
       };
 
       data = mkOption {
-        type = types.str;
+        type = types.lines;
         default = "";
         example = "";
         description = ''
@@ -484,7 +484,7 @@ in
     };
 
     extraConfig = mkOption {
-      type = types.str;
+      type = types.lines;
       default = "";
       description = ''
         Extra nsd config.
@@ -899,13 +899,9 @@ in
 
     environment.systemPackages = [ nsdPkg ];
 
-    users.groups = singleton {
-      name = username;
-      gid = config.ids.gids.nsd;
-    };
+    users.groups.${username}.gid = config.ids.gids.nsd;
 
-    users.users = singleton {
-      name = username;
+    users.users.${username} = {
       description = "NSD service user";
       home = stateDir;
       createHome  = true;
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
index c74476c7a15..b7e4c89a155 100644
--- a/nixos/modules/services/networking/ntp/chrony.nix
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -30,6 +30,7 @@ in
   options = {
     services.chrony = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to synchronise your machine's time using chrony.
@@ -79,14 +80,10 @@ in
 
     environment.systemPackages = [ pkgs.chrony ];
 
-    users.groups = singleton
-      { name = "chrony";
-        gid = config.ids.gids.chrony;
-      };
+    users.groups.chrony.gid = config.ids.gids.chrony;
 
-    users.users = singleton
-      { name = "chrony";
-        uid = config.ids.uids.chrony;
+    users.users.chrony =
+      { uid = config.ids.uids.chrony;
         group = "chrony";
         description = "chrony daemon user";
         home = stateDir;
@@ -96,6 +93,11 @@ in
 
     systemd.services.systemd-timedated.environment = { SYSTEMD_TIMEDATED_NTP_SERVICES = "chronyd.service"; };
 
+    systemd.tmpfiles.rules = [
+      "d ${stateDir} 0755 chrony chrony - -"
+      "f ${keyFile} 0640 chrony chrony -"
+    ];
+
     systemd.services.chronyd =
       { description = "chrony NTP daemon";
 
@@ -107,13 +109,6 @@ in
 
         path = [ pkgs.chrony ];
 
-        preStart = ''
-          mkdir -m 0755 -p ${stateDir}
-          touch ${keyFile}
-          chmod 0640 ${keyFile}
-          chown chrony:chrony ${stateDir} ${keyFile}
-        '';
-
         unitConfig.ConditionCapability = "CAP_SYS_TIME";
         serviceConfig =
           { Type = "simple";
@@ -122,7 +117,7 @@ in
             ProtectHome = "yes";
             ProtectSystem = "full";
             PrivateTmp = "yes";
-
+            StateDirectory = "chrony";
           };
 
       };
diff --git a/nixos/modules/services/networking/ntp/ntpd.nix b/nixos/modules/services/networking/ntp/ntpd.nix
index 1197c84f045..51398851adc 100644
--- a/nixos/modules/services/networking/ntp/ntpd.nix
+++ b/nixos/modules/services/networking/ntp/ntpd.nix
@@ -23,6 +23,8 @@ let
     restrict -6 ::1
 
     ${toString (map (server: "server " + server + " iburst\n") cfg.servers)}
+
+    ${cfg.extraConfig}
   '';
 
   ntpFlags = "-c ${configFile} -u ${ntpUser}:nogroup ${toString cfg.extraFlags}";
@@ -38,6 +40,7 @@ in
     services.ntp = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to synchronise your machine's time using ntpd, as a peer in
@@ -81,6 +84,17 @@ in
         '';
       };
 
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          fudge 127.127.1.0 stratum 10
+        '';
+        description = ''
+          Additional text appended to <filename>ntp.conf</filename>.
+        '';
+      };
+
       extraFlags = mkOption {
         type = types.listOf types.str;
         description = "Extra flags passed to the ntpd command.";
@@ -104,9 +118,8 @@ in
 
     systemd.services.systemd-timedated.environment = { SYSTEMD_TIMEDATED_NTP_SERVICES = "ntpd.service"; };
 
-    users.users = singleton
-      { name = ntpUser;
-        uid = config.ids.uids.ntp;
+    users.users.${ntpUser} =
+      { uid = config.ids.uids.ntp;
         description = "NTP daemon user";
         home = stateDir;
       };
diff --git a/nixos/modules/services/networking/ntp/openntpd.nix b/nixos/modules/services/networking/ntp/openntpd.nix
index 471d15b1687..67a04d48d30 100644
--- a/nixos/modules/services/networking/ntp/openntpd.nix
+++ b/nixos/modules/services/networking/ntp/openntpd.nix
@@ -60,8 +60,7 @@ in
 
     environment.etc."ntpd.conf".text = configFile;
 
-    users.users = singleton {
-      name = "ntp";
+    users.users.ntp = {
       uid = config.ids.uids.ntp;
       description = "OpenNTP daemon user";
       home = "/var/empty";
diff --git a/nixos/modules/services/networking/openfire.nix b/nixos/modules/services/networking/openfire.nix
index 4059eb3db83..c74f3611f79 100644
--- a/nixos/modules/services/networking/openfire.nix
+++ b/nixos/modules/services/networking/openfire.nix
@@ -9,12 +9,7 @@ with lib;
 
     services.openfire = {
 
-      enable = mkOption {
-        default = false;
-        description = "
-          Whether to enable OpenFire XMPP server.
-        ";
-      };
+      enable = mkEnableOption "OpenFire XMPP server";
 
       usePostgreSQL = mkOption {
         default = true;
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
index 05be97e66a3..dcd7e9e5fa4 100644
--- a/nixos/modules/services/networking/openvpn.nix
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -73,6 +73,9 @@ let
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "openvpn" "enable" ] "")
+  ];
 
   ###### interface
 
diff --git a/nixos/modules/services/networking/owamp.nix b/nixos/modules/services/networking/owamp.nix
index dbb2e3b4c40..637ed618b89 100644
--- a/nixos/modules/services/networking/owamp.nix
+++ b/nixos/modules/services/networking/owamp.nix
@@ -17,16 +17,13 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-    users.users = singleton {
-      name = "owamp";
+    users.users.owamp = {
       group = "owamp";
       description = "Owamp daemon";
       isSystemUser = true;
     };
 
-    users.groups = singleton {
-      name = "owamp";
-    };
+    users.groups.owamp = { };
 
     systemd.services.owamp = {
       description = "Owamp server";
diff --git a/nixos/modules/services/networking/pdns-recursor.nix b/nixos/modules/services/networking/pdns-recursor.nix
index ebfdd9f35b7..6ff181377fc 100644
--- a/nixos/modules/services/networking/pdns-recursor.nix
+++ b/nixos/modules/services/networking/pdns-recursor.nix
@@ -91,10 +91,18 @@ in {
 
     forwardZones = mkOption {
       type = types.attrs;
+      default = {};
+      description = ''
+        DNS zones to be forwarded to other authoritative servers.
+      '';
+    };
+
+    forwardZonesRecurse = mkOption {
+      type = types.attrs;
       example = { eth = "127.0.0.1:5353"; };
       default = {};
       description = ''
-        DNS zones to be forwarded to other servers.
+        DNS zones to be forwarded to other recursive servers.
       '';
     };
 
@@ -158,7 +166,8 @@ in {
       webserver-port       = cfg.api.port;
       webserver-allow-from = cfg.api.allowFrom;
 
-      forward-zones    = mapAttrsToList (zone: uri: "${zone}.=${uri}") cfg.forwardZones;
+      forward-zones         = mapAttrsToList (zone: uri: "${zone}.=${uri}") cfg.forwardZones;
+      forward-zones-recurse = mapAttrsToList (zone: uri: "${zone}.=${uri}") cfg.forwardZonesRecurse;
       export-etc-hosts = cfg.exportHosts;
       dnssec           = cfg.dnssecValidation;
       serve-rfc1918    = cfg.serveRFC1918;
@@ -210,4 +219,6 @@ in {
      "To change extra Recursor settings use services.pdns-recursor.settings instead.")
   ];
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/pdnsd.nix b/nixos/modules/services/networking/pdnsd.nix
index f5b174dd7b7..24b5bbc5104 100644
--- a/nixos/modules/services/networking/pdnsd.nix
+++ b/nixos/modules/services/networking/pdnsd.nix
@@ -62,15 +62,13 @@ in
     };
 
   config = mkIf cfg.enable {
-    users.users = singleton {
-      name = pdnsdUser;
+    users.users.${pdnsdUser} = {
       uid = config.ids.uids.pdnsd;
       group = pdnsdGroup;
       description = "pdnsd user";
     };
 
-    users.groups = singleton {
-      name = pdnsdGroup;
+    users.groups.${pdnsdGroup} = {
       gid = config.ids.gids.pdnsd;
     };
 
diff --git a/nixos/modules/services/networking/pixiecore.nix b/nixos/modules/services/networking/pixiecore.nix
new file mode 100644
index 00000000000..0e32f182e2a
--- /dev/null
+++ b/nixos/modules/services/networking/pixiecore.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.pixiecore;
+in
+{
+  meta.maintainers = with maintainers; [ bbigras danderson ];
+
+  options = {
+    services.pixiecore = {
+      enable = mkEnableOption "Pixiecore";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports (67, 69 UDP and 4011, 'port', 'statusPort' TCP) in the firewall for Pixiecore.
+        '';
+      };
+
+      mode = mkOption {
+        description = "Which mode to use";
+        default = "boot";
+        type = types.enum [ "api" "boot" ];
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Log more things that aren't directly related to booting a recognized client";
+      };
+
+      dhcpNoBind = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Handle DHCP traffic without binding to the DHCP server port";
+      };
+
+      kernel = mkOption {
+        type = types.str or types.path;
+        default = "";
+        description = "Kernel path. Ignored unless mode is set to 'boot'";
+      };
+
+      initrd = mkOption {
+        type = types.str or types.path;
+        default = "";
+        description = "Initrd path. Ignored unless mode is set to 'boot'";
+      };
+
+      cmdLine = mkOption {
+        type = types.str;
+        default = "";
+        description = "Kernel commandline arguments. Ignored unless mode is set to 'boot'";
+      };
+
+      listen = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IPv4 address to listen on";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 80;
+        description = "Port to listen on for HTTP";
+      };
+
+      statusPort = mkOption {
+        type = types.port;
+        default = 80;
+        description = "HTTP port for status information (can be the same as --port)";
+      };
+
+      apiServer = mkOption {
+        type = types.str;
+        example = "localhost:8080";
+        description = "host:port to connect to the API. Ignored unless mode is set to 'api'";
+      };
+
+      extraArguments = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line arguments to pass to Pixiecore";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.pixiecore = {};
+    users.users.pixiecore = {
+      description = "Pixiecore daemon user";
+      group = "pixiecore";
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 4011 cfg.port cfg.statusPort ];
+      allowedUDPPorts = [ 67 69 ];
+    };
+
+    systemd.services.pixiecore = {
+      description = "Pixiecore server";
+      after = [ "network.target"];
+      wants = [ "network.target"];
+      wantedBy = [ "multi-user.target"];
+      serviceConfig = {
+        User = "pixiecore";
+        Restart = "always";
+        AmbientCapabilities = [ "cap_net_bind_service" ] ++ optional cfg.dhcpNoBind "cap_net_raw";
+        ExecStart =
+          let
+            argString =
+              if cfg.mode == "boot"
+              then [ "boot" cfg.kernel ]
+                   ++ optional (cfg.initrd != "") cfg.initrd
+                   ++ optional (cfg.cmdLine != "") "--cmdline=${lib.escapeShellArg cfg.cmdLine}"
+              else [ "api" cfg.apiServer ];
+          in
+            ''
+              ${pkgs.pixiecore}/bin/pixiecore \
+                ${lib.escapeShellArgs argString} \
+                ${optionalString cfg.debug "--debug"} \
+                ${optionalString cfg.dhcpNoBind "--dhcp-no-bind"} \
+                --listen-addr ${lib.escapeShellArg cfg.listen} \
+                --port ${toString cfg.port} \
+                --status-port ${toString cfg.statusPort} \
+                ${escapeShellArgs cfg.extraArguments}
+              '';
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/polipo.nix b/nixos/modules/services/networking/polipo.nix
index dbe3b738097..1ff9388346b 100644
--- a/nixos/modules/services/networking/polipo.nix
+++ b/nixos/modules/services/networking/polipo.nix
@@ -85,17 +85,15 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton
-      { name = "polipo";
-        uid = config.ids.uids.polipo;
+    users.users.polipo =
+      { uid = config.ids.uids.polipo;
         description = "Polipo caching proxy user";
         home = "/var/cache/polipo";
         createHome = true;
       };
 
-    users.groups = singleton
-      { name = "polipo";
-        gid = config.ids.gids.polipo;
+    users.groups.polipo =
+      { gid = config.ids.gids.polipo;
         members = [ "polipo" ];
       };
 
diff --git a/nixos/modules/services/networking/pppd.nix b/nixos/modules/services/networking/pppd.nix
index e96c27bd84b..c1cbdb46176 100644
--- a/nixos/modules/services/networking/pppd.nix
+++ b/nixos/modules/services/networking/pppd.nix
@@ -64,11 +64,13 @@ in
     enabledConfigs = filter (f: f.enable) (attrValues cfg.peers);
 
     mkEtc = peerCfg: {
-      "ppp/peers/${peerCfg.name}".text = peerCfg.config;
+      name = "ppp/peers/${peerCfg.name}";
+      value.text = peerCfg.config;
     };
 
     mkSystemd = peerCfg: {
-      "pppd-${peerCfg.name}" = {
+      name = "pppd-${peerCfg.name}";
+      value = {
         restartTriggers = [ config.environment.etc."ppp/peers/${peerCfg.name}".source ];
         before = [ "network.target" ];
         wants = [ "network.target" ];
@@ -124,11 +126,11 @@ in
       };
     };
 
-    etcFiles = map mkEtc enabledConfigs;
-    systemdConfigs = map mkSystemd enabledConfigs;
+    etcFiles = listToAttrs (map mkEtc enabledConfigs);
+    systemdConfigs = listToAttrs (map mkSystemd enabledConfigs);
 
   in mkIf cfg.enable {
-    environment.etc = mkMerge etcFiles;
-    systemd.services = mkMerge systemdConfigs;
+    environment.etc = etcFiles;
+    systemd.services = systemdConfigs;
   };
 }
diff --git a/nixos/modules/services/networking/prayer.nix b/nixos/modules/services/networking/prayer.nix
index c936417e68c..f04dac01d9b 100644
--- a/nixos/modules/services/networking/prayer.nix
+++ b/nixos/modules/services/networking/prayer.nix
@@ -41,12 +41,7 @@ in
 
     services.prayer = {
 
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to run the prayer webmail http server.
-        '';
-      };
+      enable = mkEnableOption "the prayer webmail http server";
 
       port = mkOption {
         default = "2080";
@@ -72,17 +67,14 @@ in
   config = mkIf config.services.prayer.enable {
     environment.systemPackages = [ prayer ];
 
-    users.users = singleton
-      { name = prayerUser;
-        uid = config.ids.uids.prayer;
+    users.users.${prayerUser} =
+      { uid = config.ids.uids.prayer;
         description = "Prayer daemon user";
         home = stateDir;
       };
 
-    users.groups = singleton
-      { name = prayerGroup;
-        gid = config.ids.gids.prayer;
-      };
+    users.groups.${prayerGroup} =
+      { gid = config.ids.gids.prayer; };
 
     systemd.services.prayer = {
       wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
index 49ca839a2c3..1f41c720adf 100644
--- a/nixos/modules/services/networking/privoxy.nix
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -109,4 +109,6 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/quassel.nix b/nixos/modules/services/networking/quassel.nix
index b495b3948fb..da723ec86ad 100644
--- a/nixos/modules/services/networking/quassel.nix
+++ b/nixos/modules/services/networking/quassel.nix
@@ -16,12 +16,7 @@ in
 
     services.quassel = {
 
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to run the Quassel IRC client daemon.
-        '';
-      };
+      enable = mkEnableOption "the Quassel IRC client daemon";
 
       certificateFile = mkOption {
         type = types.nullOr types.str;
@@ -92,17 +87,21 @@ in
         message = "Quassel needs a certificate file in order to require SSL";
       }];
 
-    users.users = mkIf (cfg.user == null) [
-      { name = "quassel";
+    users.users = optionalAttrs (cfg.user == null) {
+      quassel = {
+        name = "quassel";
         description = "Quassel IRC client daemon";
         group = "quassel";
         uid = config.ids.uids.quassel;
-      }];
+      };
+    };
 
-    users.groups = mkIf (cfg.user == null) [
-      { name = "quassel";
+    users.groups = optionalAttrs (cfg.user == null) {
+      quassel = {
+        name = "quassel";
         gid = config.ids.gids.quassel;
-      }];
+      };
+    };
 
     systemd.tmpfiles.rules = [
       "d '${cfg.dataDir}' - ${user} - - -"
diff --git a/nixos/modules/services/networking/quorum.nix b/nixos/modules/services/networking/quorum.nix
new file mode 100644
index 00000000000..2f612c9db68
--- /dev/null
+++ b/nixos/modules/services/networking/quorum.nix
@@ -0,0 +1,229 @@
+{ config, pkgs, lib, ... }:
+let
+
+  inherit (lib) mkEnableOption mkIf mkOption literalExample types optionalString;
+
+  cfg = config.services.quorum;
+  dataDir = "/var/lib/quorum";
+  genesisFile = pkgs.writeText "genesis.json" (builtins.toJSON cfg.genesis);
+  staticNodesFile = pkgs.writeText "static-nodes.json" (builtins.toJSON cfg.staticNodes);
+
+in {
+  options = {
+
+    services.quorum = {
+      enable = mkEnableOption "Quorum blockchain daemon";
+
+      user = mkOption {
+        type = types.str;
+        default = "quorum";
+        description = "The user as which to run quorum.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = cfg.user;
+        description = "The group as which to run quorum.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 21000;
+        description = "Override the default port on which to listen for connections.";
+      };
+
+      nodekeyFile = mkOption {
+        type = types.path;
+        default = "${dataDir}/nodekey";
+        description = "Path to the nodekey.";
+      };
+
+      staticNodes = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "enode://dd333ec28f0a8910c92eb4d336461eea1c20803eed9cf2c056557f986e720f8e693605bba2f4e8f289b1162e5ac7c80c914c7178130711e393ca76abc1d92f57@0.0.0.0:30303?discport=0" ];
+        description = "List of validator nodes.";
+      };
+
+      privateconfig = mkOption {
+        type = types.str;
+        default = "ignore";
+        description = "Configuration of privacy transaction manager.";
+      };
+
+      syncmode = mkOption {
+        type = types.enum [ "fast" "full" "light" ];
+        default = "full";
+        description = "Blockchain sync mode.";
+      };
+
+      blockperiod = mkOption {
+        type = types.int;
+        default = 5;
+        description = "Default minimum difference between two consecutive block's timestamps in seconds.";
+      };
+
+      permissioned = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Allow only a defined list of nodes to connect.";
+      };
+
+      rpc = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable RPC interface.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "Listening address for RPC connections.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 22004;
+          description = "Override the default port on which to listen for RPC connections.";
+        };
+
+        api = mkOption {
+          type = types.str;
+          default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul";
+          description = "API's offered over the HTTP-RPC interface.";
+        };
+      };
+
+     ws = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = "Enable WS-RPC interface.";
+        };
+
+        address = mkOption {
+          type = types.str;
+          default = "0.0.0.0";
+          description = "Listening address for WS-RPC connections.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8546;
+          description = "Override the default port on which to listen for WS-RPC connections.";
+        };
+
+        api = mkOption {
+          type = types.str;
+          default = "admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul";
+          description = "API's offered over the WS-RPC interface.";
+        };
+
+       origins = mkOption {
+          type = types.str;
+          default = "*";
+          description = "Origins from which to accept websockets requests";
+       };
+     };
+
+      genesis = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        example = literalExample '' {
+          alloc = {
+            a47385db68718bdcbddc2d2bb7c54018066ec111 = {
+              balance = "1000000000000000000000000000";
+            };
+          };
+          coinbase = "0x0000000000000000000000000000000000000000";
+          config = {
+            byzantiumBlock = 4;
+            chainId = 494702925;
+            eip150Block = 2;
+            eip155Block = 3;
+            eip158Block = 3;
+            homesteadBlock = 1;
+            isQuorum = true;
+            istanbul = {
+              epoch = 30000;
+              policy = 0;
+            };
+          };
+          difficulty = "0x1";
+          extraData = "0x0000000000000000000000000000000000000000000000000000000000000000f85ad59438f0508111273d8e482f49410ca4078afc86a961b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0";
+          gasLimit = "0x2FEFD800";
+          mixHash = "0x63746963616c2062797a616e74696e65201111756c7420746f6c6572616e6365";
+          nonce = "0x0";
+          parentHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
+          timestamp = "0x00";
+          }'';
+        description = "Blockchain genesis settings.";
+      };
+     };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.quorum ];
+    systemd.tmpfiles.rules = [
+      "d '${dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
+    ];
+    systemd.services.quorum = {
+      description = "Quorum daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        PRIVATE_CONFIG = "${cfg.privateconfig}";
+      };
+      preStart = ''
+        if [ ! -d ${dataDir}/geth ]; then
+          if [ ! -d ${dataDir}/keystore ]; then
+            echo ERROR: You need to create a wallet before initializing your genesis file, run:
+            echo   # su -s /bin/sh - quorum
+            echo   $ geth --datadir ${dataDir} account new
+            echo and configure your genesis file accordingly.
+            exit 1;
+          fi
+          ln -s ${staticNodesFile} ${dataDir}/static-nodes.json
+          ${pkgs.quorum}/bin/geth --datadir ${dataDir} init ${genesisFile}
+        fi
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = ''${pkgs.quorum}/bin/geth \
+            --nodiscover \
+            --verbosity 5 \
+            --nodekey ${cfg.nodekeyFile} \
+            --istanbul.blockperiod ${toString cfg.blockperiod} \
+            --syncmode ${cfg.syncmode} \
+            ${optionalString (cfg.permissioned)
+            "--permissioned"} \
+            --mine --minerthreads 1 \
+            ${optionalString (cfg.rpc.enable)
+            "--rpc --rpcaddr ${cfg.rpc.address} --rpcport ${toString cfg.rpc.port} --rpcapi ${cfg.rpc.api}"} \
+            ${optionalString (cfg.ws.enable)
+            "--ws --wsaddr ${cfg.ws.address} --wsport ${toString cfg.ws.port} --wsapi ${cfg.ws.api} --wsorigins ${cfg.ws.origins}"} \
+            --emitcheckpoints \
+            --datadir ${dataDir} \
+            --port ${toString cfg.port}'';
+        Restart = "on-failure";
+
+        # Hardening measures
+        PrivateTmp = "true";
+        ProtectSystem = "full";
+        NoNewPrivileges = "true";
+        PrivateDevices = "true";
+        MemoryDenyWriteExecute = "true";
+      };
+    };
+    users.users.${cfg.user} = {
+      name = cfg.user;
+      group = cfg.group;
+      description = "Quorum daemon user";
+      home = dataDir;
+      isSystemUser = true;
+    };
+    users.groups.${cfg.group} = {};
+  };
+}
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 1daced4a6c7..30bf22586f8 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -59,18 +59,15 @@ in
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package ];
 
-    users.users = singleton
-      { name = "radicale";
-        uid = config.ids.uids.radicale;
+    users.users.radicale =
+      { uid = config.ids.uids.radicale;
         description = "radicale user";
         home = "/var/lib/radicale";
         createHome = true;
       };
 
-    users.groups = singleton
-      { name = "radicale";
-        gid = config.ids.gids.radicale;
-      };
+    users.groups.radicale =
+      { gid = config.ids.gids.radicale; };
 
     systemd.services.radicale = {
       description = "A Simple Calendar and Contact Server";
diff --git a/nixos/modules/services/networking/radvd.nix b/nixos/modules/services/networking/radvd.nix
index 020faa34922..f4b00c9b356 100644
--- a/nixos/modules/services/networking/radvd.nix
+++ b/nixos/modules/services/networking/radvd.nix
@@ -19,6 +19,7 @@ in
   options = {
 
     services.radvd.enable = mkOption {
+      type = types.bool;
       default = false;
       description =
         ''
diff --git a/nixos/modules/services/networking/rdnssd.nix b/nixos/modules/services/networking/rdnssd.nix
index bccab805bee..469504c4317 100644
--- a/nixos/modules/services/networking/rdnssd.nix
+++ b/nixos/modules/services/networking/rdnssd.nix
@@ -17,6 +17,7 @@ in
   options = {
 
     services.rdnssd.enable = mkOption {
+      type = types.bool;
       default = false;
       #default = config.networking.enableIPv6;
       description =
diff --git a/nixos/modules/services/networking/resilio.nix b/nixos/modules/services/networking/resilio.nix
index 9b25aa57583..e74e03fc0b0 100644
--- a/nixos/modules/services/networking/resilio.nix
+++ b/nixos/modules/services/networking/resilio.nix
@@ -244,7 +244,7 @@ in
       group           = "rslsync";
     };
 
-    users.groups = [ { name = "rslsync"; } ];
+    users.groups.rslsync = {};
 
     systemd.services.resilio = with pkgs; {
       description = "Resilio Sync Service";
diff --git a/nixos/modules/services/networking/rxe.nix b/nixos/modules/services/networking/rxe.nix
index a6a069ec50c..c7d174a00de 100644
--- a/nixos/modules/services/networking/rxe.nix
+++ b/nixos/modules/services/networking/rxe.nix
@@ -5,20 +5,6 @@ with lib;
 let
   cfg = config.networking.rxe;
 
-  runRxeCmd = cmd: ifcs:
-    concatStrings ( map (x: "${pkgs.rdma-core}/bin/rxe_cfg -n ${cmd} ${x};") ifcs);
-
-  startScript = pkgs.writeShellScriptBin "rxe-start" ''
-    ${pkgs.rdma-core}/bin/rxe_cfg -n start
-    ${runRxeCmd "add" cfg.interfaces}
-    ${pkgs.rdma-core}/bin/rxe_cfg
-  '';
-
-  stopScript = pkgs.writeShellScriptBin "rxe-stop" ''
-    ${runRxeCmd "remove" cfg.interfaces }
-    ${pkgs.rdma-core}/bin/rxe_cfg -n stop
-  '';
-
 in {
   ###### interface
 
@@ -31,9 +17,8 @@ in {
         example = [ "eth0" ];
         description = ''
           Enable RDMA on the listed interfaces. The corresponding virtual
-          RDMA interfaces will be named rxe0 ... rxeN where the ordering
-          will be as they are named in the list. UDP port 4791 must be
-          open on the respective ethernet interfaces.
+          RDMA interfaces will be named rxe_&lt;interface&gt;.
+          UDP port 4791 must be open on the respective ethernet interfaces.
         '';
       };
     };
@@ -44,7 +29,6 @@ in {
   config = mkIf cfg.enable {
 
     systemd.services.rxe = {
-      path = with pkgs; [ kmod rdma-core ];
       description = "RoCE interfaces";
 
       wantedBy = [ "multi-user.target" ];
@@ -54,8 +38,13 @@ in {
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
-        ExecStart = "${startScript}/bin/rxe-start";
-        ExecStop = "${stopScript}/bin/rxe-stop";
+        ExecStart = map ( x:
+          "${pkgs.iproute}/bin/rdma link add rxe_${x} type rxe netdev ${x}"
+          ) cfg.interfaces;
+
+        ExecStop = map ( x:
+          "${pkgs.iproute}/bin/rdma link delete rxe_${x}"
+          ) cfg.interfaces;
       };
     };
   };
diff --git a/nixos/modules/services/networking/sabnzbd.nix b/nixos/modules/services/networking/sabnzbd.nix
index 62b24d4377f..ff5aef7d1cb 100644
--- a/nixos/modules/services/networking/sabnzbd.nix
+++ b/nixos/modules/services/networking/sabnzbd.nix
@@ -15,10 +15,8 @@ in
 
   options = {
     services.sabnzbd = {
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable the sabnzbd server.";
-      };
+      enable = mkEnableOption "the sabnzbd server";
+
       configFile = mkOption {
         default = "/var/lib/sabnzbd/sabnzbd.ini";
         description = "Path to config file.";
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index 9412d0ef8a6..60fb3d5d6d4 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -75,4 +75,6 @@ in
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/networking/shairport-sync.nix b/nixos/modules/services/networking/shairport-sync.nix
index 68e005ab81d..b4b86a2d55b 100644
--- a/nixos/modules/services/networking/shairport-sync.nix
+++ b/nixos/modules/services/networking/shairport-sync.nix
@@ -17,6 +17,7 @@ in
     services.shairport-sync = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Enable the shairport-sync daemon.
@@ -55,9 +56,8 @@ in
     services.avahi.publish.enable = true;
     services.avahi.publish.userServices = true;
 
-    users.users = singleton
-      { name = cfg.user;
-        description = "Shairport user";
+    users.users.${cfg.user} =
+      { description = "Shairport user";
         isSystemUser = true;
         createHome = true;
         home = "/var/lib/shairport-sync";
diff --git a/nixos/modules/services/networking/shorewall.nix b/nixos/modules/services/networking/shorewall.nix
new file mode 100644
index 00000000000..16383be2530
--- /dev/null
+++ b/nixos/modules/services/networking/shorewall.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+let
+  types = lib.types;
+  cfg = config.services.shorewall;
+in {
+  options = {
+    services.shorewall = {
+      enable = lib.mkOption {
+        type        = types.bool;
+        default     = false;
+        description = ''
+          Whether to enable Shorewall IPv4 Firewall.
+          <warning>
+            <para>
+            Enabling this service WILL disable the existing NixOS
+            firewall! Default firewall rules provided by packages are not
+            considered at the moment.
+            </para>
+          </warning>
+        '';
+      };
+      package = lib.mkOption {
+        type        = types.package;
+        default     = pkgs.shorewall;
+        defaultText = "pkgs.shorewall";
+        description = "The shorewall package to use.";
+      };
+      configs = lib.mkOption {
+        type        = types.attrsOf types.lines;
+        default     = {};
+        description = ''
+          This option defines the Shorewall configs.
+          The attribute name defines the name of the config,
+          and the attribute value defines the content of the config.
+        '';
+        apply = lib.mapAttrs (name: text: pkgs.writeText "${name}" text);
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.firewall.enable = false;
+    systemd.services.shorewall = {
+      description     = "Shorewall IPv4 Firewall";
+      after           = [ "ipset.target" ];
+      before          = [ "network-pre.target" ];
+      wants           = [ "network-pre.target" ];
+      wantedBy        = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      restartTriggers = lib.attrValues cfg.configs;
+      serviceConfig = {
+        Type            = "oneshot";
+        RemainAfterExit = "yes";
+        ExecStart       = "${cfg.package}/bin/shorewall start";
+        ExecReload      = "${cfg.package}/bin/shorewall reload";
+        ExecStop        = "${cfg.package}/bin/shorewall stop";
+      };
+      preStart = ''
+        install -D -d -m 750 /var/lib/shorewall
+        install -D -d -m 755 /var/lock/subsys
+        touch                /var/log/shorewall.log
+        chown 750            /var/log/shorewall.log
+      '';
+    };
+    environment = {
+      etc = lib.mapAttrs' (name: conf: lib.nameValuePair "shorewall/${name}" {source=conf;}) cfg.configs;
+      systemPackages = [ cfg.package ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/shorewall6.nix b/nixos/modules/services/networking/shorewall6.nix
new file mode 100644
index 00000000000..e081aedc6c3
--- /dev/null
+++ b/nixos/modules/services/networking/shorewall6.nix
@@ -0,0 +1,70 @@
+{ config, lib, pkgs, ... }:
+let
+  types = lib.types;
+  cfg = config.services.shorewall6;
+in {
+  options = {
+    services.shorewall6 = {
+      enable = lib.mkOption {
+        type        = types.bool;
+        default     = false;
+        description = ''
+          Whether to enable Shorewall IPv6 Firewall.
+          <warning>
+            <para>
+            Enabling this service WILL disable the existing NixOS
+            firewall! Default firewall rules provided by packages are not
+            considered at the moment.
+            </para>
+          </warning>
+        '';
+      };
+      package = lib.mkOption {
+        type        = types.package;
+        default     = pkgs.shorewall;
+        defaultText = "pkgs.shorewall";
+        description = "The shorewall package to use.";
+      };
+      configs = lib.mkOption {
+        type        = types.attrsOf types.lines;
+        default     = {};
+        description = ''
+          This option defines the Shorewall configs.
+          The attribute name defines the name of the config,
+          and the attribute value defines the content of the config.
+        '';
+        apply = lib.mapAttrs (name: text: pkgs.writeText "${name}" text);
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.firewall.enable = false;
+    systemd.services.shorewall6 = {
+      description     = "Shorewall IPv6 Firewall";
+      after           = [ "ipset.target" ];
+      before          = [ "network-pre.target" ];
+      wants           = [ "network-pre.target" ];
+      wantedBy        = [ "multi-user.target" ];
+      reloadIfChanged = true;
+      restartTriggers = lib.attrValues cfg.configs;
+      serviceConfig = {
+        Type            = "oneshot";
+        RemainAfterExit = "yes";
+        ExecStart       = "${cfg.package}/bin/shorewall6 start";
+        ExecReload      = "${cfg.package}/bin/shorewall6 reload";
+        ExecStop        = "${cfg.package}/bin/shorewall6 stop";
+      };
+      preStart = ''
+        install -D -d -m 750 /var/lib/shorewall6
+        install -D -d -m 755 /var/lock/subsys
+        touch                /var/log/shorewall6.log
+        chown 750            /var/log/shorewall6.log
+      '';
+    };
+    environment = {
+      etc = lib.mapAttrs' (name: conf: lib.nameValuePair "shorewall6/${name}" {source=conf;}) cfg.configs;
+      systemPackages = [ cfg.package ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/shout.nix b/nixos/modules/services/networking/shout.nix
index e548ec66962..a808a7f39d0 100644
--- a/nixos/modules/services/networking/shout.nix
+++ b/nixos/modules/services/networking/shout.nix
@@ -82,8 +82,7 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.users = singleton {
-      name = "shout";
+    users.users.shout = {
       uid = config.ids.uids.shout;
       description = "Shout daemon user";
       home = shoutHome;
diff --git a/nixos/modules/services/networking/smartdns.nix b/nixos/modules/services/networking/smartdns.nix
new file mode 100644
index 00000000000..f1888af7041
--- /dev/null
+++ b/nixos/modules/services/networking/smartdns.nix
@@ -0,0 +1,61 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  inherit (lib.types) attrsOf coercedTo listOf oneOf str int bool;
+  cfg = config.services.smartdns;
+
+  confFile = pkgs.writeText "smartdns.conf" (with generators;
+    toKeyValue {
+      mkKeyValue = mkKeyValueDefault {
+        mkValueString = v:
+          if isBool v then
+            if v then "yes" else "no"
+          else
+            mkValueStringDefault { } v;
+      } " ";
+      listsAsDuplicateKeys =
+        true; # Allowing duplications because we need to deal with multiple entries with the same key.
+    } cfg.settings);
+in {
+  options.services.smartdns = {
+    enable = mkEnableOption "SmartDNS DNS server";
+
+    bindPort = mkOption {
+      type = types.port;
+      default = 53;
+      description = "DNS listening port number.";
+    };
+
+    settings = mkOption {
+      type =
+      let atom = oneOf [ str int bool ];
+      in attrsOf (coercedTo atom toList (listOf atom));
+      example = literalExample ''
+        {
+          bind = ":5353 -no-rule -group example";
+          cache-size = 4096;
+          server-tls = [ "8.8.8.8:853" "1.1.1.1:853" ];
+          server-https = "https://cloudflare-dns.com/dns-query -exclude-default-group";
+          prefetch-domain = true;
+          speed-check-mode = "ping,tcp:80";
+        };
+      '';
+      description = ''
+        A set that will be generated into configuration file, see the <link xlink:href="https://github.com/pymumu/smartdns/blob/master/ReadMe_en.md#configuration-parameter">SmartDNS README</link> for details of configuration parameters.
+        You could override the options here like <option>services.smartdns.bindPort</option> by writing <literal>settings.bind = ":5353 -no-rule -group example";</literal>.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.smartdns.settings.bind = mkDefault ":${toString cfg.bindPort}";
+
+    systemd.packages = [ pkgs.smartdns ];
+    systemd.services.smartdns.wantedBy = [ "multi-user.target" ];
+    environment.etc."smartdns/smartdns.conf".source = confFile;
+    environment.etc."default/smartdns".source =
+      "${pkgs.smartdns}/etc/default/smartdns";
+  };
+}
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index b48b0b3a9d6..37ee2a80389 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -280,8 +280,7 @@ in
       fping6.source = "${pkgs.fping}/bin/fping6";
     };
     environment.systemPackages = [ pkgs.fping ];
-    users.users = singleton {
-      name = cfg.user;
+    users.users.${cfg.user} = {
       isNormalUser = false;
       isSystemUser = true;
       uid = config.ids.uids.smokeping;
diff --git a/nixos/modules/services/networking/spacecookie.nix b/nixos/modules/services/networking/spacecookie.nix
new file mode 100644
index 00000000000..c4d06df6ad4
--- /dev/null
+++ b/nixos/modules/services/networking/spacecookie.nix
@@ -0,0 +1,83 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.spacecookie;
+  configFile = pkgs.writeText "spacecookie.json" (lib.generators.toJSON {} {
+    inherit (cfg) hostname port root;
+  });
+in {
+
+  options = {
+
+    services.spacecookie = {
+
+      enable = mkEnableOption "spacecookie";
+
+      hostname = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "The hostname the service is reachable via. Clients will use this hostname for further requests after loading the initial gopher menu.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 70;
+        description = "Port the gopher service should be exposed on.";
+      };
+
+      root = mkOption {
+        type = types.path;
+        default = "/srv/gopher";
+        description = "The root directory spacecookie serves via gopher.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.sockets.spacecookie = {
+      description = "Socket for the Spacecookie Gopher Server";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ "[::]:${toString cfg.port}" ];
+      socketConfig = {
+        BindIPv6Only = "both";
+      };
+    };
+
+    systemd.services.spacecookie = {
+      description = "Spacecookie Gopher Server";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "spacecookie.socket" ];
+
+      serviceConfig = {
+        Type = "notify";
+        ExecStart = "${pkgs.haskellPackages.spacecookie}/bin/spacecookie ${configFile}";
+        FileDescriptorStoreMax = 1;
+
+        DynamicUser = true;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+
+        # AF_UNIX for communication with systemd
+        # AF_INET replaced by BindIPv6Only=both
+        RestrictAddressFamilies = "AF_UNIX AF_INET6";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index eca599afb33..892e59778c3 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -19,6 +19,7 @@ in
     services.lshd = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to enable the GNU lshd SSH2 daemon, which allows
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 91fc7d72bc6..17f31e3a488 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -9,15 +9,15 @@ let
   # This middle-ground solution ensures *an* sshd can do their basic validation
   # on the configuration.
   validationPackage = if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
-    then [ cfgc.package ]
-    else [ pkgs.buildPackages.openssh ];
+    then cfgc.package
+    else pkgs.buildPackages.openssh;
 
   sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } ''
     cat >$out <<EOL
     ${cfg.extraConfig}
     EOL
 
-    ssh-keygen -f mock-hostkey -N ""
+    ssh-keygen -q -f mock-hostkey -N ""
     sshd -t -f $out -h mock-hostkey
   '';
 
@@ -74,6 +74,10 @@ let
 in
 
 {
+  imports = [
+    (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
+    (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
+  ];
 
   ###### interface
 
@@ -234,6 +238,26 @@ in
         description = "Files from which authorized keys are read.";
       };
 
+      authorizedKeysCommand = mkOption {
+        type = types.str;
+        default = "none";
+        description = ''
+          Specifies a program to be used to look up the user's public
+          keys. The program must be owned by root, not writable by group
+          or others and specified by an absolute path.
+        '';
+      };
+
+      authorizedKeysCommandUser = mkOption {
+        type = types.str;
+        default = "nobody";
+        description = ''
+          Specifies the user under whose account the AuthorizedKeysCommand
+          is run. It is recommended to use a dedicated user that has no
+          other role on the host than running authorized keys commands.
+        '';
+      };
+
       kexAlgorithms = mkOption {
         type = types.listOf types.str;
         default = [
@@ -481,6 +505,10 @@ in
         PrintMotd no # handled by pam_motd
 
         AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
+        ${optionalString (cfg.authorizedKeysCommand != "none") ''
+          AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
+          AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser}
+        ''}
 
         ${flip concatMapStrings cfg.hostKeys (k: ''
           HostKey ${k.path}
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
index 0222e8ce8b5..c4fa370a5fe 100644
--- a/nixos/modules/services/networking/sslh.nix
+++ b/nixos/modules/services/networking/sslh.nix
@@ -77,19 +77,14 @@ in
 
   config = mkMerge [
     (mkIf cfg.enable {
-      users.users.${user} = {
-        description = "sslh daemon user";
-        isSystemUser = true;
-      };
-
       systemd.services.sslh = {
         description = "Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)";
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
 
         serviceConfig = {
-          User                 = user;
-          Group                = "nogroup";
+          DynamicUser          = true;
+          User                 = "sslh";
           PermissionsStartOnly = true;
           Restart              = "always";
           RestartSec           = "1s";
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 4ff9c486059..13a1a897c5e 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -4,7 +4,7 @@ let
 
   inherit (builtins) toFile;
   inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList
-                mkIf mkEnableOption mkOption types;
+                mkIf mkEnableOption mkOption types literalExample;
 
   cfg = config.services.strongswan;
 
@@ -79,19 +79,21 @@ in
     connections = mkOption {
       type = types.attrsOf (types.attrsOf types.str);
       default = {};
-      example = {
-        "%default" = {
-          keyexchange = "ikev2";
-          keyingtries = "1";
-        };
-        roadwarrior = {
-          auto       = "add";
-          leftcert   = "/run/keys/moonCert.pem";
-          leftid     = "@moon.strongswan.org";
-          leftsubnet = "10.1.0.0/16";
-          right      = "%any";
-        };
-      };
+      example = literalExample ''
+        {
+          "%default" = {
+            keyexchange = "ikev2";
+            keyingtries = "1";
+          };
+          roadwarrior = {
+            auto       = "add";
+            leftcert   = "/run/keys/moonCert.pem";
+            leftid     = "@moon.strongswan.org";
+            leftsubnet = "10.1.0.0/16";
+            right      = "%any";
+          };
+        }
+      '';
       description = ''
         A set of connections and their options for the ‘conn xxx’
         sections of the <filename>ipsec.conf</filename> file.
diff --git a/nixos/modules/services/networking/stubby.nix b/nixos/modules/services/networking/stubby.nix
index b38bcd4cec0..c5e0f929a12 100644
--- a/nixos/modules/services/networking/stubby.nix
+++ b/nixos/modules/services/networking/stubby.nix
@@ -72,6 +72,7 @@ let
     resolution_type: GETDNS_RESOLUTION_STUB
     dns_transport_list:
       ${fallbacks}
+    appdata_dir: "/var/cache/stubby"
     tls_authentication: ${cfg.authenticationMode}
     tls_query_padding_blocksize: ${toString cfg.queryPaddingBlocksize}
     edns_client_subnet_private: ${if cfg.subnetPrivate then "1" else "0"}
@@ -204,10 +205,12 @@ in
       wantedBy = [ "multi-user.target" ];
 
       serviceConfig = {
+        Type = "notify";
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
         CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
         ExecStart = "${pkgs.stubby}/bin/stubby -C ${confFile} ${optionalString cfg.debugLogging "-l"}";
         DynamicUser = true;
+        CacheDirectory = "stubby";
       };
     };
   };
diff --git a/nixos/modules/services/networking/supplicant.nix b/nixos/modules/services/networking/supplicant.nix
index 35c1e649e2e..b5b9989ce18 100644
--- a/nixos/modules/services/networking/supplicant.nix
+++ b/nixos/modules/services/networking/supplicant.nix
@@ -39,8 +39,6 @@ let
         bindsTo = deps;
         after = deps;
         before = [ "network.target" ];
-        # Receive restart event after resume
-        partOf = [ "post-resume.target" ];
 
         path = [ pkgs.coreutils ];
 
diff --git a/nixos/modules/services/networking/supybot.nix b/nixos/modules/services/networking/supybot.nix
index 64eb1106832..dc9fb31ffd0 100644
--- a/nixos/modules/services/networking/supybot.nix
+++ b/nixos/modules/services/networking/supybot.nix
@@ -3,32 +3,35 @@
 with lib;
 
 let
-
   cfg  = config.services.supybot;
-
+  isStateDirHome = hasPrefix "/home/" cfg.stateDir;
+  isStateDirVar = cfg.stateDir == "/var/lib/supybot";
+  pyEnv = pkgs.python3.withPackages (p: [ p.limnoria ] ++ (cfg.extraPackages p));
 in
-
 {
-
   options = {
 
     services.supybot = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
-        description = "Enable Supybot, an IRC bot";
+        description = "Enable Supybot, an IRC bot (also known as Limnoria).";
       };
 
       stateDir = mkOption {
-        # Setting this to /var/lib/supybot caused useradd to fail
-        default = "/home/supybot";
+        type = types.path;
+        default = if versionAtLeast config.system.stateVersion "20.09"
+          then "/var/lib/supybot"
+          else "/home/supybot";
+        defaultText = "/var/lib/supybot";
         description = "The root directory, logs and plugins are stored here";
       };
 
       configFile = mkOption {
         type = types.path;
         description = ''
-          Path to a supybot config file. This can be generated by
+          Path to initial supybot config file. This can be generated by
           running supybot-wizard.
 
           Note: all paths should include the full path to the stateDir
@@ -36,44 +39,72 @@ in
         '';
       };
 
+      plugins = mkOption {
+        type = types.attrsOf types.path;
+        default = {};
+        description = ''
+          Attribute set of additional plugins that will be symlinked to the
+          <filename>plugin</filename> subdirectory.
+
+          Please note that you still need to add the plugins to the config
+          file (or with <literal>!load</literal>) using their attribute name.
+        '';
+        example = literalExample ''
+          let
+            plugins = pkgs.fetchzip {
+              url = "https://github.com/ProgVal/Supybot-plugins/archive/57c2450c.zip";
+              sha256 = "077snf84ibnva3sbpzdfpfma6hcdw7dflwnhg6pw7mgnf0nd84qd";
+            };
+          in
+          {
+            Wikipedia = "''${plugins}/Wikipedia";
+            Decide = ./supy-decide;
+          }
+        '';
+      };
+
+      extraPackages = mkOption {
+        default = p: [];
+        description = ''
+          Extra Python packages available to supybot plugins. The
+          value must be a function which receives the attrset defined
+          in <varname>python3Packages</varname> as the sole argument.
+        '';
+        example = literalExample ''p: [ p.lxml p.requests ]'';
+      };
+
     };
 
   };
 
-
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.pythonPackages.limnoria ];
+    environment.systemPackages = [ pkgs.python3Packages.limnoria ];
 
-    users.users = singleton {
-      name = "supybot";
+    users.users.supybot = {
       uid = config.ids.uids.supybot;
       group = "supybot";
       description = "Supybot IRC bot user";
       home = cfg.stateDir;
-      createHome = true;
+      isSystemUser = true;
     };
 
     users.groups.supybot = {
-      name = "supybot";
       gid = config.ids.gids.supybot;
     };
 
     systemd.services.supybot = {
       description = "Supybot, an IRC bot";
+      documentation = [ "https://limnoria.readthedocs.io/" ];
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.pythonPackages.limnoria ];
       preStart = ''
-        cd ${cfg.stateDir}
-        mkdir -p backup conf data plugins logs/plugins tmp web
-        ln -sf ${cfg.configFile} supybot.cfg
         # This needs to be created afresh every time
-        rm -f supybot.cfg.bak
+        rm -f '${cfg.stateDir}/supybot.cfg.bak'
       '';
 
       serviceConfig = {
-        ExecStart = "${pkgs.pythonPackages.limnoria}/bin/supybot ${cfg.stateDir}/supybot.cfg";
+        ExecStart = "${pyEnv}/bin/supybot ${cfg.stateDir}/supybot.cfg";
         PIDFile = "/run/supybot.pid";
         User = "supybot";
         Group = "supybot";
@@ -81,8 +112,50 @@ in
         Restart = "on-abort";
         StartLimitInterval = "5m";
         StartLimitBurst = "1";
+
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RemoveIPC = true;
+        ProtectHostname = true;
+        CapabilityBoundingSet = "";
+        ProtectSystem = "full";
+      }
+      // optionalAttrs isStateDirVar {
+        StateDirectory = "supybot";
+        ProtectSystem = "strict";
+      }
+      // optionalAttrs (!isStateDirHome) {
+        ProtectHome = true;
       };
     };
 
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}'              0700 supybot supybot - -"
+      "d '${cfg.stateDir}/backup'       0750 supybot supybot - -"
+      "d '${cfg.stateDir}/conf'         0750 supybot supybot - -"
+      "d '${cfg.stateDir}/data'         0750 supybot supybot - -"
+      "d '${cfg.stateDir}/plugins'      0750 supybot supybot - -"
+      "d '${cfg.stateDir}/logs'         0750 supybot supybot - -"
+      "d '${cfg.stateDir}/logs/plugins' 0750 supybot supybot - -"
+      "d '${cfg.stateDir}/tmp'          0750 supybot supybot - -"
+      "d '${cfg.stateDir}/web'          0750 supybot supybot - -"
+      "L '${cfg.stateDir}/supybot.cfg'  -    -       -       - ${cfg.configFile}"
+    ]
+    ++ (flip mapAttrsToList cfg.plugins (name: dest:
+      "L+ '${cfg.stateDir}/plugins/${name}' - - - - ${dest}"
+    ));
+
   };
 }
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index b3f2af5b179..e717d78feed 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -112,12 +112,12 @@ in {
               addresses = [ "tcp://192.168.0.10:51820" ];
             };
           };
-          type = types.attrsOf (types.submodule ({ config, ... }: {
+          type = types.attrsOf (types.submodule ({ name, ... }: {
             options = {
 
               name = mkOption {
                 type = types.str;
-                default = config._module.args.name;
+                default = name;
                 description = ''
                   Name of the device
                 '';
@@ -169,13 +169,15 @@ in {
           description = ''
             folders which should be shared by syncthing.
           '';
-          example = {
-            "/home/user/sync" = {
-              id = "syncme";
-              devices = [ "bigbox" ];
-            };
-          };
-          type = types.attrsOf (types.submodule ({ config, ... }: {
+          example = literalExample ''
+            {
+              "/home/user/sync" = {
+                id = "syncme";
+                devices = [ "bigbox" ];
+              };
+            }
+          '';
+          type = types.attrsOf (types.submodule ({ name, ... }: {
             options = {
 
               enable = mkOption {
@@ -190,7 +192,7 @@ in {
 
               path = mkOption {
                 type = types.str;
-                default = config._module.args.name;
+                default = name;
                 description = ''
                   The path to the folder which should be shared.
                 '';
@@ -198,7 +200,7 @@ in {
 
               id = mkOption {
                 type = types.str;
-                default = config._module.args.name;
+                default = name;
                 description = ''
                   The id of the folder. Must be the same on all devices.
                 '';
@@ -206,7 +208,7 @@ in {
 
               label = mkOption {
                 type = types.str;
-                default = config._module.args.name;
+                default = name;
                 description = ''
                   The label of the folder.
                 '';
@@ -484,6 +486,24 @@ in {
               -gui-address=${cfg.guiAddress} \
               -home=${cfg.configDir}
           '';
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateTmp = true;
+          PrivateUsers = true;
+          ProtectControlGroups = true;
+          ProtectHostname = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          CapabilityBoundingSet = [
+            "~CAP_SYS_PTRACE" "~CAP_SYS_ADMIN"
+            "~CAP_SETGID" "~CAP_SETUID" "~CAP_SETPCAP"
+            "~CAP_SYS_TIME" "~CAP_KILL"
+          ];
         };
       };
       syncthing-init = mkIf (
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
new file mode 100644
index 00000000000..4d6aeb75ebd
--- /dev/null
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.services.tailscale;
+in {
+  meta.maintainers = with maintainers; [ danderson mbaillie ];
+
+  options.services.tailscale = {
+    enable = mkEnableOption "Tailscale client daemon";
+
+    port = mkOption {
+      type = types.port;
+      default = 41641;
+      description = "The port to listen on for tunnel traffic (0=autoselect).";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.tailscale = {
+      description = "Tailscale client daemon";
+
+      after = [ "network-pre.target" ];
+      wants = [ "network-pre.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      unitConfig = {
+        StartLimitIntervalSec = 0;
+        StartLimitBurst = 0;
+      };
+
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.tailscale}/bin/tailscaled --port ${toString cfg.port}";
+
+        RuntimeDirectory = "tailscale";
+        RuntimeDirectoryMode = 755;
+
+        StateDirectory = "tailscale";
+        StateDirectoryMode = 750;
+
+        CacheDirectory = "tailscale";
+        CacheDirectoryMode = 750;
+
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/tcpcrypt.nix b/nixos/modules/services/networking/tcpcrypt.nix
index a0ccb995009..18f2e135124 100644
--- a/nixos/modules/services/networking/tcpcrypt.nix
+++ b/nixos/modules/services/networking/tcpcrypt.nix
@@ -29,8 +29,7 @@ in
 
   config = mkIf cfg.enable {
 
-    users.users = singleton {
-      name = "tcpcryptd";
+    users.users.tcpcryptd = {
       uid = config.ids.uids.tcpcryptd;
       description = "tcpcrypt daemon user";
     };
diff --git a/nixos/modules/services/networking/tox-bootstrapd.nix b/nixos/modules/services/networking/tox-bootstrapd.nix
index 1d349215169..f88e34827d0 100644
--- a/nixos/modules/services/networking/tox-bootstrapd.nix
+++ b/nixos/modules/services/networking/tox-bootstrapd.nix
@@ -56,9 +56,8 @@ in
 
   config = mkIf config.services.toxBootstrapd.enable {
 
-    users.users = singleton
-      { name = "tox-bootstrapd";
-        uid = config.ids.uids.tox-bootstrapd;
+    users.users.tox-bootstrapd =
+      { uid = config.ids.uids.tox-bootstrapd;
         description = "Tox bootstrap daemon user";
         inherit home;
         createHome = true;
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index 3cf82e8839b..baed83591e1 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -53,6 +53,13 @@ in
 
       enable = mkEnableOption "Unbound domain name server";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.unbound;
+        defaultText = "pkgs.unbound";
+        description = "The unbound package to use";
+      };
+
       allowedAccess = mkOption {
         default = [ "127.0.0.0/24" ];
         type = types.listOf types.str;
@@ -94,7 +101,7 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.unbound ];
+    environment.systemPackages = [ cfg.package ];
 
     users.users.unbound = {
       description = "unbound daemon user";
@@ -114,7 +121,7 @@ in
         mkdir -m 0755 -p ${stateDir}/dev/
         cp ${confFile} ${stateDir}/unbound.conf
         ${optionalString cfg.enableRootTrustAnchor ''
-          ${pkgs.unbound}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
+          ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
           chown unbound ${stateDir} ${rootTrustAnchorFile}
         ''}
         touch ${stateDir}/dev/random
@@ -122,7 +129,7 @@ in
       '';
 
       serviceConfig = {
-        ExecStart = "${pkgs.unbound}/bin/unbound -d -c ${stateDir}/unbound.conf";
+        ExecStart = "${cfg.package}/bin/unbound -d -c ${stateDir}/unbound.conf";
         ExecStopPost="${pkgs.utillinux}/bin/umount ${stateDir}/dev/random";
 
         ProtectSystem = true;
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index c922ba15960..4bdfa8143dc 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -147,8 +147,10 @@ in
       }) mountPoints;
 
     systemd.tmpfiles.rules = [
-      "e '${stateDir}' 0700 unifi - - -"
+      "d '${stateDir}' 0700 unifi - - -"
       "d '${stateDir}/data' 0700 unifi - - -"
+      "d '${stateDir}/webapps' 0700 unifi - - -"
+      "L+ '${stateDir}/webapps/ROOT' - - - - ${cfg.unifiPackage}/webapps/ROOT"
     ];
 
     systemd.services.unifi = {
@@ -161,17 +163,6 @@ in
       # This a HACK to fix missing dependencies of dynamic libs extracted from jars
       environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
 
-      preStart = ''
-        # Create the volatile webapps
-        rm -rf "${stateDir}/webapps"
-        mkdir -p "${stateDir}/webapps"
-        ln -s "${cfg.unifiPackage}/webapps/ROOT" "${stateDir}/webapps/ROOT"
-      '';
-
-      postStop = ''
-        rm -rf "${stateDir}/webapps"
-      '';
-
       serviceConfig = {
         Type = "simple";
         ExecStart = "${(removeSuffix "\n" cmd)} start";
diff --git a/nixos/modules/services/networking/v2ray.nix b/nixos/modules/services/networking/v2ray.nix
new file mode 100644
index 00000000000..a1774cdffbb
--- /dev/null
+++ b/nixos/modules/services/networking/v2ray.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  options = {
+
+    services.v2ray = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run v2ray server.
+
+          Either <literal>configFile</literal> or <literal>config</literal> must be specified.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/etc/v2ray/config.json";
+        description = ''
+          The absolute path to the configuration file.
+
+          Either <literal>configFile</literal> or <literal>config</literal> must be specified.
+
+          See <link xlink:href="https://v2ray.com/en/configuration/overview.html"/>.
+        '';
+      };
+
+      config = mkOption {
+        type = types.nullOr (types.attrsOf types.unspecified);
+        default = null;
+        example = {
+          inbounds = [{
+            port = 1080;
+            listen = "127.0.0.1";
+            protocol = "http";
+          }];
+          outbounds = [{
+            protocol = "freedom";
+          }];
+        };
+        description = ''
+          The configuration object.
+
+          Either `configFile` or `config` must be specified.
+
+          See <link xlink:href="https://v2ray.com/en/configuration/overview.html"/>.
+        '';
+      };
+    };
+
+  };
+
+  config = let
+    cfg = config.services.v2ray;
+    configFile = if cfg.configFile != null
+      then cfg.configFile
+      else (pkgs.writeText "v2ray.json" (builtins.toJSON cfg.config));
+
+  in mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.configFile == null) != (cfg.config == null);
+        message = "Either but not both `configFile` and `config` should be specified for v2ray.";
+      }
+    ];
+
+    systemd.services.v2ray = {
+      description = "v2ray Daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.v2ray ];
+      script = ''
+        exec v2ray -config ${configFile}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/vsftpd.nix b/nixos/modules/services/networking/vsftpd.nix
index 90093d9a78d..b3e20184423 100644
--- a/nixos/modules/services/networking/vsftpd.nix
+++ b/nixos/modules/services/networking/vsftpd.nix
@@ -133,8 +133,8 @@ let
       ${optionalString cfg.enableVirtualUsers ''
         guest_enable=YES
         guest_username=vsftpd
-        pam_service_name=vsftpd
       ''}
+      pam_service_name=vsftpd
       ${cfg.extraConfig}
     '';
 
@@ -279,21 +279,22 @@ in
         message = "vsftpd: If enableVirtualUsers is true, you need to setup both the userDbPath and localUsers options.";
       }];
 
-    users.users =
-      [ { name = "vsftpd";
-          uid = config.ids.uids.vsftpd;
-          description = "VSFTPD user";
-          home = if cfg.localRoot != null
-                   then cfg.localRoot # <= Necessary for virtual users.
-                   else "/homeless-shelter";
-        }
-      ] ++ optional cfg.anonymousUser
-        { name = "ftp";
+    users.users = {
+      "vsftpd" = {
+        uid = config.ids.uids.vsftpd;
+        description = "VSFTPD user";
+        home = if cfg.localRoot != null
+               then cfg.localRoot # <= Necessary for virtual users.
+               else "/homeless-shelter";
+      };
+    } // optionalAttrs cfg.anonymousUser {
+      "ftp" = { name = "ftp";
           uid = config.ids.uids.ftp;
           group = "ftp";
           description = "Anonymous FTP user";
           home = cfg.anonymousUserHome;
         };
+    };
 
     users.groups.ftp.gid = config.ids.gids.ftp;
 
diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
index b770d47d269..ff1bdeed9f4 100644
--- a/nixos/modules/services/networking/wg-quick.nix
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -302,7 +302,7 @@ in {
   ###### implementation
 
   config = mkIf (cfg.interfaces != {}) {
-    boot.extraModulePackages = [ kernel.wireguard ];
+    boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
     environment.systemPackages = [ pkgs.wireguard-tools ];
     # This is forced to false for now because the default "--validmark" rpfilter we apply on reverse path filtering
     # breaks the wg-quick routing because wireguard packets leave with a fwmark from wireguard.
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 980961225c9..e8f83f6dd8b 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -151,7 +151,7 @@ let
       publicKey = mkOption {
         example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
         type = types.str;
-        description = "The base64 public key the peer.";
+        description = "The base64 public key of the peer.";
       };
 
       presharedKey = mkOption {
@@ -428,14 +428,14 @@ in
       ++ (attrValues (
         mapAttrs (name: value: {
           assertion = value.generatePrivateKeyFile -> (value.privateKey == null);
-          message = "networking.wireguard.interfaces.${name}.generatePrivateKey must not be set if networking.wireguard.interfaces.${name}.privateKey is set.";
+          message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile must not be set if networking.wireguard.interfaces.${name}.privateKey is set.";
         }) cfg.interfaces))
         ++ map ({ interfaceName, peer, ... }: {
           assertion = (peer.presharedKey == null) || (peer.presharedKeyFile == null);
           message = "networking.wireguard.interfaces.${interfaceName} peer «${peer.publicKey}» has both presharedKey and presharedKeyFile set, but only one can be used.";
         }) all_peers;
 
-    boot.extraModulePackages = [ kernel.wireguard ];
+    boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
     environment.systemPackages = [ pkgs.wireguard-tools ];
 
     systemd.services =
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 8f05c3949fb..de0f11595a9 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -233,6 +233,7 @@ in {
       path = [ pkgs.wpa_supplicant ];
 
       script = ''
+        iface_args="-s -u -D${cfg.driver} -c ${configFile}"
         ${if ifaces == [] then ''
           for i in $(cd /sys/class/net && echo *); do
             DEVTYPE=
@@ -240,14 +241,14 @@ in {
             if [ -e "$UEVENT_PATH" ]; then
               source "$UEVENT_PATH"
               if [ "$DEVTYPE" = "wlan" -o -e /sys/class/net/$i/wireless ]; then
-                ifaces="$ifaces''${ifaces:+ -N} -i$i"
+                args+="''${args:+ -N} -i$i $iface_args"
               fi
             fi
           done
         '' else ''
-          ifaces="${concatStringsSep " -N " (map (i: "-i${i}") ifaces)}"
+          args="${concatMapStringsSep " -N " (i: "-i${i} $iface_args") ifaces}"
         ''}
-        exec wpa_supplicant -s -u -D${cfg.driver} -c ${configFile} $ifaces
+        exec wpa_supplicant $args
       '';
     };
 
diff --git a/nixos/modules/services/networking/xandikos.nix b/nixos/modules/services/networking/xandikos.nix
new file mode 100644
index 00000000000..87c029156b9
--- /dev/null
+++ b/nixos/modules/services/networking/xandikos.nix
@@ -0,0 +1,148 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xandikos;
+in
+{
+
+  options = {
+    services.xandikos = {
+      enable = mkEnableOption "Xandikos CalDAV and CardDAV server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.xandikos;
+        defaultText = "pkgs.xandikos";
+        description = "The Xandikos package to use.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          The IP address on which Xandikos will listen.
+          By default listens on localhost.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = "The port of the Xandikos web application";
+      };
+
+      routePrefix = mkOption {
+        type = types.str;
+        default = "/";
+        description = ''
+          Path to Xandikos.
+          Useful when Xandikos is behind a reverse proxy.
+        '';
+      };
+
+      extraOptions = mkOption {
+        default = [];
+        type = types.listOf types.str;
+        example = literalExample ''
+          [ "--autocreate"
+            "--defaults"
+            "--current-user-principal user"
+            "--dump-dav-xml"
+          ]
+        '';
+        description = ''
+          Extra command line arguments to pass to xandikos.
+        '';
+      };
+
+      nginx = mkOption {
+        default = {};
+        description = ''
+          Configuration for nginx reverse proxy.
+        '';
+
+        type = types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                Configure the nginx reverse proxy settings.
+              '';
+            };
+
+            hostName = mkOption {
+              type = types.str;
+              description = ''
+                The hostname use to setup the virtualhost configuration
+              '';
+            };
+          };
+        };
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable (
+    mkMerge [
+      {
+        meta.maintainers = [ lib.maintainers."0x4A6F" ];
+
+        systemd.services.xandikos = {
+          description = "A Simple Calendar and Contact Server";
+          after = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+
+          serviceConfig = {
+            User = "xandikos";
+            Group = "xandikos";
+            DynamicUser = "yes";
+            RuntimeDirectory = "xandikos";
+            StateDirectory = "xandikos";
+            StateDirectoryMode = "0700";
+            PrivateDevices = true;
+            # Sandboxing
+            CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN";
+            ProtectSystem = "strict";
+            ProtectHome = true;
+            PrivateTmp = true;
+            ProtectKernelTunables = true;
+            ProtectKernelModules = true;
+            ProtectControlGroups = true;
+            RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_PACKET AF_NETLINK";
+            RestrictNamespaces = true;
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            ExecStart = ''
+              ${cfg.package}/bin/xandikos \
+                --directory /var/lib/xandikos \
+                --listen_address ${cfg.address} \
+                --port ${toString cfg.port} \
+                --route-prefix ${cfg.routePrefix} \
+                ${lib.concatStringsSep " " cfg.extraOptions}
+            '';
+          };
+        };
+      }
+
+      (
+        mkIf cfg.nginx.enable {
+          services.nginx = {
+            enable = true;
+            virtualHosts."${cfg.nginx.hostName}" = {
+              locations."/" = {
+                proxyPass = "http://${cfg.address}:${toString cfg.port}/";
+              };
+            };
+          };
+        }
+      )
+    ]
+  );
+}
diff --git a/nixos/modules/services/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix
index 8dc6f845ed8..2f527ab156a 100644
--- a/nixos/modules/services/networking/xinetd.nix
+++ b/nixos/modules/services/networking/xinetd.nix
@@ -44,12 +44,7 @@ in
 
   options = {
 
-    services.xinetd.enable = mkOption {
-      default = false;
-      description = ''
-        Whether to enable the xinetd super-server daemon.
-      '';
-    };
+    services.xinetd.enable = mkEnableOption "the xinetd super-server daemon";
 
     services.xinetd.extraDefaults = mkOption {
       default = "";
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index 5d65f8e3413..9e675ecd6f4 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -12,11 +12,11 @@ let
   configFileProvided = (cfg.configFile != null);
   generateConfig = (
     if configProvided && configFileProvided then
-      "${pkgs.jq}/bin/jq -s add /run/yggdrasil/configFile.json ${configAsFile}"
+      "${pkgs.jq}/bin/jq -s add ${configAsFile} ${cfg.configFile}"
     else if configProvided then
       "cat ${configAsFile}"
     else if configFileProvided then
-      "cat /run/yggdrasil/configFile.json"
+      "cat ${cfg.configFile}"
     else
       "${cfg.package}/bin/yggdrasil -genconf"
   );
@@ -147,7 +147,7 @@ in {
         RuntimeDirectory = "yggdrasil";
         RuntimeDirectoryMode = "0700";
         BindReadOnlyPaths = mkIf configFileProvided
-          [ "${cfg.configFile}:/run/yggdrasil/configFile.json" ];
+          [ "${cfg.configFile}" ];
 
         # TODO: as of yggdrasil 0.3.8 and systemd 243, yggdrasil fails
         # to set up the network adapter when DynamicUser is set.  See
diff --git a/nixos/modules/services/networking/zerotierone.nix b/nixos/modules/services/networking/zerotierone.nix
index 764af3846fe..cf39ed065a7 100644
--- a/nixos/modules/services/networking/zerotierone.nix
+++ b/nixos/modules/services/networking/zerotierone.nix
@@ -38,10 +38,13 @@ in
   config = mkIf cfg.enable {
     systemd.services.zerotierone = {
       description = "ZeroTierOne";
-      path = [ cfg.package ];
-      bindsTo = [ "network-online.target" ];
-      after = [ "network-online.target" ];
+
       wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      wants = [ "network-online.target" ];
+
+      path = [ cfg.package ];
+
       preStart = ''
         mkdir -p /var/lib/zerotier-one/networks.d
         chmod 700 /var/lib/zerotier-one
@@ -53,6 +56,7 @@ in
         ExecStart = "${cfg.package}/bin/zerotier-one -p${toString cfg.port}";
         Restart = "always";
         KillMode = "process";
+        TimeoutStopSec = 5;
       };
     };
 
@@ -63,5 +67,16 @@ in
     networking.firewall.allowedUDPPorts = [ cfg.port ];
 
     environment.systemPackages = [ cfg.package ];
+
+    # Prevent systemd from potentially changing the MAC address
+    systemd.network.links."50-zerotier" = {
+      matchConfig = {
+        OriginalName = "zt*";
+      };
+      linkConfig = {
+        AutoNegotiation = false;
+        MACAddressPolicy = "none";
+      };
+    };
   };
 }
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
index 0a9848a4934..a7315896c50 100644
--- a/nixos/modules/services/networking/znc/default.nix
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -287,20 +287,22 @@ in
       '';
     };
 
-    users.users = optional (cfg.user == defaultUser)
-      { name = defaultUser;
-        description = "ZNC server daemon owner";
-        group = defaultUser;
-        uid = config.ids.uids.znc;
-        home = cfg.dataDir;
-        createHome = true;
+    users.users = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        { description = "ZNC server daemon owner";
+          group = defaultUser;
+          uid = config.ids.uids.znc;
+          home = cfg.dataDir;
+          createHome = true;
+        };
       };
 
-    users.groups = optional (cfg.user == defaultUser)
-      { name = defaultUser;
-        gid = config.ids.gids.znc;
-        members = [ defaultUser ];
-      };
+    users.groups = optionalAttrs (cfg.user == defaultUser) {
+      ${defaultUser} =
+        { gid = config.ids.gids.znc;
+          members = [ defaultUser ];
+        };
+    };
 
   };
 }
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index 1071c05d514..59306d625e6 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -112,6 +112,15 @@ in
 
 {
 
+  imports = [
+    (mkChangedOptionModule [ "services" "printing" "gutenprint" ] [ "services" "printing" "drivers" ]
+      (config:
+        let enabled = getAttrFromPath [ "services" "printing" "gutenprint" ] config;
+        in if enabled then [ pkgs.gutenprint ] else [ ]))
+    (mkRemovedOptionModule [ "services" "printing" "cupsFilesConf" ] "")
+    (mkRemovedOptionModule [ "services" "printing" "cupsdConf" ] "")
+  ];
+
   ###### interface
 
   options = {
@@ -279,9 +288,8 @@ in
 
   config = mkIf config.services.printing.enable {
 
-    users.users = singleton
-      { name = "cups";
-        uid = config.ids.uids.cups;
+    users.users.cups =
+      { uid = config.ids.uids.cups;
         group = "lp";
         description = "CUPS printing services";
       };
diff --git a/nixos/modules/services/scheduling/atd.nix b/nixos/modules/services/scheduling/atd.nix
index a32907647a0..cefe72b0e99 100644
--- a/nixos/modules/services/scheduling/atd.nix
+++ b/nixos/modules/services/scheduling/atd.nix
@@ -57,22 +57,16 @@ in
 
     security.pam.services.atd = {};
 
-    users.users = singleton
-      { name = "atd";
-        uid = config.ids.uids.atd;
+    users.users.atd =
+      { uid = config.ids.uids.atd;
         description = "atd user";
         home = "/var/empty";
       };
 
-    users.groups = singleton
-      { name = "atd";
-        gid = config.ids.gids.atd;
-      };
+    users.groups.atd.gid = config.ids.gids.atd;
 
     systemd.services.atd = {
       description = "Job Execution Daemon (atd)";
-      after = [ "systemd-udev-settle.service" ];
-      wants = [ "systemd-udev-settle.service" ];
       wantedBy = [ "multi-user.target" ];
 
       path = [ at ];
diff --git a/nixos/modules/services/scheduling/fcron.nix b/nixos/modules/services/scheduling/fcron.nix
index e43ca014e14..42bed21bf25 100644
--- a/nixos/modules/services/scheduling/fcron.nix
+++ b/nixos/modules/services/scheduling/fcron.nix
@@ -86,7 +86,8 @@ in
 
     services.fcron.systab = systemCronJobs;
 
-    environment.etc =
+    environment.etc = listToAttrs
+      (map (x: { name = x.target; value = x; })
       [ (allowdeny "allow" (cfg.allow))
         (allowdeny "deny" cfg.deny)
         # see man 5 fcron.conf
@@ -112,7 +113,7 @@ in
           gid = config.ids.gids.fcron;
           mode = "0644";
         }
-      ];
+      ]);
 
     environment.systemPackages = [ pkgs.fcron ];
     users.users.fcron = {
diff --git a/nixos/modules/services/search/hound.nix b/nixos/modules/services/search/hound.nix
index 6740928db9a..7a44489efe6 100644
--- a/nixos/modules/services/search/hound.nix
+++ b/nixos/modules/services/search/hound.nix
@@ -88,19 +88,19 @@ in {
   };
 
   config = mkIf cfg.enable {
-    users.groups = optional (cfg.group == "hound") {
-      name = "hound";
-      gid = config.ids.gids.hound;
+    users.groups = optionalAttrs (cfg.group == "hound") {
+      hound.gid = config.ids.gids.hound;
     };
 
-    users.users = optional (cfg.user == "hound") {
-      name = "hound";
-      description = "hound code search";
-      createHome = true;
-      home = cfg.home;
-      group = cfg.group;
-      extraGroups = cfg.extraGroups;
-      uid = config.ids.uids.hound;
+    users.users = optionalAttrs (cfg.user == "hound") {
+      hound = {
+        description = "hound code search";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        uid = config.ids.uids.hound;
+      };
     };
 
     systemd.services.hound = {
diff --git a/nixos/modules/services/search/kibana.nix b/nixos/modules/services/search/kibana.nix
index 43a63aa8fdc..2beb265ee5d 100644
--- a/nixos/modules/services/search/kibana.nix
+++ b/nixos/modules/services/search/kibana.nix
@@ -198,8 +198,7 @@ in {
 
     environment.systemPackages = [ cfg.package ];
 
-    users.users = singleton {
-      name = "kibana";
+    users.users.kibana = {
       uid = config.ids.uids.kibana;
       description = "Kibana service user";
       home = cfg.dataDir;
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
index 5ef7d9893a4..a8615a20a1c 100644
--- a/nixos/modules/services/search/solr.nix
+++ b/nixos/modules/services/search/solr.nix
@@ -13,19 +13,11 @@ in
     services.solr = {
       enable = mkEnableOption "Solr";
 
-      # default to the 8.x series not forcing major version upgrade of those on the 7.x series
       package = mkOption {
         type = types.package;
-        default = if versionAtLeast config.system.stateVersion "19.09"
-          then pkgs.solr_8
-          else pkgs.solr_7
-        ;
+        default = pkgs.solr;
         defaultText = "pkgs.solr";
-        description = ''
-          Which Solr package to use. This defaults to version 7.x if
-          <literal>system.stateVersion &lt; 19.09</literal> and version 8.x
-          otherwise.
-        '';
+        description = "Which Solr package to use.";
       };
 
       port = mkOption {
@@ -100,18 +92,18 @@ in
       };
     };
 
-    users.users = optionalAttrs (cfg.user == "solr") (singleton
-      { name = "solr";
+    users.users = optionalAttrs (cfg.user == "solr") {
+      solr = {
         group = cfg.group;
         home = cfg.stateDir;
         createHome = true;
         uid = config.ids.uids.solr;
-      });
+      };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "solr") (singleton
-      { name = "solr";
-        gid = config.ids.gids.solr;
-      });
+    users.groups = optionalAttrs (cfg.group == "solr") {
+      solr.gid = config.ids.gids.solr;
+    };
 
   };
 
diff --git a/nixos/modules/services/security/bitwarden_rs/default.nix b/nixos/modules/services/security/bitwarden_rs/default.nix
index d1817db0755..903a5327037 100644
--- a/nixos/modules/services/security/bitwarden_rs/default.nix
+++ b/nixos/modules/services/security/bitwarden_rs/default.nix
@@ -18,15 +18,33 @@ let
         else key + toUpper x) "" parts;
     in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
 
-  configFile = pkgs.writeText "bitwarden_rs.env" (concatMapStrings (s: s + "\n") (
-    (concatLists (mapAttrsToList (name: value:
-      if value != null then [ "${nameToEnvVar name}=${if isBool value then boolToString value else toString value}" ] else []
-    ) cfg.config))));
+  # Due to the different naming schemes allowed for config keys,
+  # we can only check for values consistently after converting them to their corresponding environment variable name.
+  configEnv =
+    let
+      configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
+        if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
+      ) cfg.config));
+    in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
+      WEB_VAULT_FOLDER = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault";
+    } // configEnv;
+
+  configFile = pkgs.writeText "bitwarden_rs.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
+
+  bitwarden_rs = pkgs.bitwarden_rs.override { inherit (cfg) dbBackend; };
 
 in {
   options.services.bitwarden_rs = with types; {
     enable = mkEnableOption "bitwarden_rs";
 
+    dbBackend = mkOption {
+      type = enum [ "sqlite" "mysql" "postgresql" ];
+      default = "sqlite";
+      description = ''
+        Which database backend bitwarden_rs will be using.
+      '';
+    };
+
     backupDir = mkOption {
       type = nullOr str;
       default = null;
@@ -40,7 +58,7 @@ in {
       default = {};
       example = literalExample ''
         {
-          domain = https://bw.domain.tld:8443;
+          domain = "https://bw.domain.tld:8443";
           signupsAllowed = true;
           rocketPort = 8222;
           rocketLog = "critical";
@@ -56,23 +74,20 @@ in {
         even though foo2 would have been converted to FOO_2.
         This allows working around any potential future conflicting naming conventions.
 
-        Based on the attributes passed to this config option a environment file will be generated
+        Based on the attributes passed to this config option an environment file will be generated
         that is passed to bitwarden_rs's systemd service.
 
         The available configuration options can be found in
-        <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/1.8.0/.env.template">the environment template file</link>.
+        <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/${bitwarden_rs.version}/.env.template">the environment template file</link>.
       '';
-      apply = config: optionalAttrs config.webVaultEnabled {
-        webVaultFolder = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault";
-      } // config;
     };
   };
 
   config = mkIf cfg.enable {
-    services.bitwarden_rs.config = {
-      dataFolder = "/var/lib/bitwarden_rs";
-      webVaultEnabled = mkDefault true;
-    };
+    assertions = [ {
+      assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
+      message = "Backups for database backends other than sqlite will need customization";
+    } ];
 
     users.users.bitwarden_rs = {
       inherit group;
@@ -87,7 +102,7 @@ in {
         User = user;
         Group = group;
         EnvironmentFile = configFile;
-        ExecStart = "${pkgs.bitwarden_rs}/bin/bitwarden_rs";
+        ExecStart = "${bitwarden_rs}/bin/bitwarden_rs";
         LimitNOFILE = "1048576";
         LimitNPROC = "64";
         PrivateTmp = "true";
@@ -109,6 +124,7 @@ in {
       path = with pkgs; [ sqlite ];
       serviceConfig = {
         SyslogIdentifier = "backup-bitwarden_rs";
+        Type = "oneshot";
         User = mkDefault user;
         Group = mkDefault group;
         ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
diff --git a/nixos/modules/services/security/certmgr.nix b/nixos/modules/services/security/certmgr.nix
index e89078883eb..94c0ba14117 100644
--- a/nixos/modules/services/security/certmgr.nix
+++ b/nixos/modules/services/security/certmgr.nix
@@ -113,7 +113,7 @@ in
         otherCert = "/var/certmgr/specs/other-cert.json";
       }
       '';
-      type = with types; attrsOf (either (submodule {
+      type = with types; attrsOf (either path (submodule {
         options = {
           service = mkOption {
             type = nullOr str;
@@ -148,7 +148,7 @@ in
             description = "certmgr spec request object.";
           };
         };
-    }) path);
+    }));
       description = ''
         Certificate specs as described by:
         <link xlink:href="https://github.com/cloudflare/certmgr#certificate-specs" />
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
index 04b433f8f2b..aaf6fb0479b 100644
--- a/nixos/modules/services/security/clamav.nix
+++ b/nixos/modules/services/security/clamav.nix
@@ -30,6 +30,10 @@ let
   '';
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "clamav" "updater" "config" ] [ "services" "clamav" "updater" "extraConfig" ])
+  ];
+
   options = {
     services.clamav = {
       daemon = {
@@ -79,18 +83,15 @@ in
   config = mkIf (cfg.updater.enable || cfg.daemon.enable) {
     environment.systemPackages = [ pkg ];
 
-    users.users = singleton {
-      name = clamavUser;
+    users.users.${clamavUser} = {
       uid = config.ids.uids.clamav;
       group = clamavGroup;
       description = "ClamAV daemon user";
       home = stateDir;
     };
 
-    users.groups = singleton {
-      name = clamavGroup;
-      gid = config.ids.gids.clamav;
-    };
+    users.groups.${clamavGroup} =
+      { gid = config.ids.gids.clamav; };
 
     environment.etc."clamav/freshclam.conf".source = freshclamConfigFile;
     environment.etc."clamav/clamd.conf".source = clamdConfigFile;
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index 716ae7a2d2f..3f84f9c2560 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -6,15 +6,32 @@ let
 
   cfg = config.services.fail2ban;
 
-  fail2banConf = pkgs.writeText "fail2ban.conf" cfg.daemonConfig;
+  fail2banConf = pkgs.writeText "fail2ban.local" cfg.daemonConfig;
 
-  jailConf = pkgs.writeText "jail.conf"
-    (concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
+  jailConf = pkgs.writeText "jail.local" ''
+    [INCLUDES]
+
+    before = paths-nixos.conf
+
+    ${concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
       optionalString (def != "")
         ''
           [${name}]
           ${def}
-        ''))));
+        '')))}
+  '';
+
+  pathsConf = pkgs.writeText "paths-nixos.conf" ''
+    # NixOS
+
+    [INCLUDES]
+
+    before = paths-common.conf
+
+    after  = paths-overrides.local
+
+    [DEFAULT]
+  '';
 
 in
 
@@ -31,21 +48,135 @@ in
         description = "Whether to enable the fail2ban service.";
       };
 
+      package = mkOption {
+        default = pkgs.fail2ban;
+        type = types.package;
+        example = "pkgs.fail2ban_0_11";
+        description = "The fail2ban package to use for running the fail2ban service.";
+      };
+
+      packageFirewall = mkOption {
+        default = pkgs.iptables;
+        type = types.package;
+        example = "pkgs.nftables";
+        description = "The firewall package used by fail2ban service.";
+      };
+
+      banaction = mkOption {
+        default = "iptables-multiport";
+        type = types.str;
+        example = "nftables-multiport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      banaction-allports = mkOption {
+        default = "iptables-allport";
+        type = types.str;
+        example = "nftables-allport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      bantime-increment.enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Allows to use database for searching of previously banned ip's to increase
+          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
+        '';
+      };
+
+      bantime-increment.rndtime = mkOption {
+        default = "4m";
+        type = types.str;
+        example = "8m";
+        description = ''
+          "bantime-increment.rndtime" is the max number of seconds using for mixing with random time
+          to prevent "clever" botnets calculate exact time IP can be unbanned again
+        '';
+      };
+
+      bantime-increment.maxtime = mkOption {
+        default = "10h";
+        type = types.str;
+        example = "48h";
+        description = ''
+          "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
+        '';
+      };
+
+      bantime-increment.factor = mkOption {
+        default = "1";
+        type = types.str;
+        example = "4";
+        description = ''
+          "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
+          default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
+        '';
+      };
+
+      bantime-increment.formula = mkOption {
+        default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor";
+        type = types.str;
+        example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
+        description = ''
+          "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow,
+          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
+        '';
+      };
+
+      bantime-increment.multipliers = mkOption {
+        default = "1 2 4 8 16 32 64";
+        type = types.str;
+        example = "2 4 16 128";
+        description = ''
+          "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, coresponding
+          previously ban count and given "bantime.factor" (for multipliers default is 1);
+          following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
+          always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
+        '';
+      };
+
+      bantime-increment.overalljails = mkOption {
+        default = false;
+        type = types.bool;
+        example = true;
+        description = ''
+          "bantime-increment.overalljails"  (if true) specifies the search of IP in the database will be executed
+          cross over all jails, if false (dafault), only current jail of the ban IP will be searched
+        '';
+      };
+
+      ignoreIP = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [ "192.168.0.0/16" "2001:DB8::42" ];
+        description = ''
+          "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
+          matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
+        '';
+      };
+
       daemonConfig = mkOption {
-        default =
-          ''
-            [Definition]
-            loglevel  = INFO
-            logtarget = SYSLOG
-            socket    = /run/fail2ban/fail2ban.sock
-            pidfile   = /run/fail2ban/fail2ban.pid
-          '';
+        default = ''
+          [Definition]
+          logtarget = SYSLOG
+          socket    = /run/fail2ban/fail2ban.sock
+          pidfile   = /run/fail2ban/fail2ban.pid
+          dbfile    = /var/lib/fail2ban/fail2ban.sqlite3
+        '';
         type = types.lines;
-        description =
-          ''
-            The contents of Fail2ban's main configuration file.  It's
-            generally not necessary to change it.
-          '';
+        description = ''
+          The contents of Fail2ban's main configuration file.  It's
+          generally not necessary to change it.
+       '';
       };
 
       jails = mkOption {
@@ -65,88 +196,111 @@ in
           }
         '';
         type = types.attrsOf types.lines;
-        description =
-          ''
-            The configuration of each Fail2ban “jail”.  A jail
-            consists of an action (such as blocking a port using
-            <command>iptables</command>) that is triggered when a
-            filter applied to a log file triggers more than a certain
-            number of times in a certain time period.  Actions are
-            defined in <filename>/etc/fail2ban/action.d</filename>,
-            while filters are defined in
-            <filename>/etc/fail2ban/filter.d</filename>.
-          '';
+        description = ''
+          The configuration of each Fail2ban “jail”.  A jail
+          consists of an action (such as blocking a port using
+          <command>iptables</command>) that is triggered when a
+          filter applied to a log file triggers more than a certain
+          number of times in a certain time period.  Actions are
+          defined in <filename>/etc/fail2ban/action.d</filename>,
+          while filters are defined in
+          <filename>/etc/fail2ban/filter.d</filename>.
+        '';
       };
 
     };
 
   };
 
-
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.fail2ban ];
+    warnings = mkIf (config.networking.firewall.enable == false && config.networking.nftables.enable == false) [
+      "fail2ban can not be used without a firewall"
+    ];
 
-    environment.etc."fail2ban/fail2ban.conf".source = fail2banConf;
-    environment.etc."fail2ban/jail.conf".source = jailConf;
-    environment.etc."fail2ban/action.d".source = "${pkgs.fail2ban}/etc/fail2ban/action.d/*.conf";
-    environment.etc."fail2ban/filter.d".source = "${pkgs.fail2ban}/etc/fail2ban/filter.d/*.conf";
+    environment.systemPackages = [ cfg.package ];
 
-    systemd.services.fail2ban =
-      { description = "Fail2ban Intrusion Prevention System";
+    environment.etc = {
+      "fail2ban/fail2ban.local".source = fail2banConf;
+      "fail2ban/jail.local".source = jailConf;
+      "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
+      "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
+      "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
+      "fail2ban/paths-nixos.conf".source = pathsConf;
+      "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
+      "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
+    };
 
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
-        partOf = optional config.networking.firewall.enable "firewall.service";
+    systemd.services.fail2ban = {
+      description = "Fail2ban Intrusion Prevention System";
 
-        restartTriggers = [ fail2banConf jailConf ];
-        path = [ pkgs.fail2ban pkgs.iptables pkgs.iproute ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      partOf = optional config.networking.firewall.enable "firewall.service";
 
-        preStart =
-          ''
-            mkdir -p /var/lib/fail2ban
-          '';
+      restartTriggers = [ fail2banConf jailConf pathsConf ];
+      reloadIfChanged = true;
 
-        unitConfig.Documentation = "man:fail2ban(1)";
+      path = [ cfg.package cfg.packageFirewall pkgs.iproute ];
 
-        serviceConfig =
-          { Type = "forking";
-            ExecStart = "${pkgs.fail2ban}/bin/fail2ban-client -x start";
-            ExecStop = "${pkgs.fail2ban}/bin/fail2ban-client stop";
-            ExecReload = "${pkgs.fail2ban}/bin/fail2ban-client reload";
-            PIDFile = "/run/fail2ban/fail2ban.pid";
-            Restart = "always";
+      unitConfig.Documentation = "man:fail2ban(1)";
 
-            ReadOnlyDirectories = "/";
-            ReadWriteDirectories = "/run/fail2ban /var/tmp /var/lib";
-            PrivateTmp = "true";
-            RuntimeDirectory = "fail2ban";
-            CapabilityBoundingSet = "CAP_DAC_READ_SEARCH CAP_NET_ADMIN CAP_NET_RAW";
-          };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/fail2ban-server -xf start";
+        ExecStop = "${cfg.package}/bin/fail2ban-server stop";
+        ExecReload = "${cfg.package}/bin/fail2ban-server reload";
+        Type = "simple";
+        Restart = "on-failure";
+        PIDFile = "/run/fail2ban/fail2ban.pid";
+        # Capabilities
+        CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
+        # Security
+        NoNewPrivileges = true;
+        # Directory
+        RuntimeDirectory = "fail2ban";
+        RuntimeDirectoryMode = "0750";
+        StateDirectory = "fail2ban";
+        StateDirectoryMode = "0750";
+        LogsDirectory = "fail2ban";
+        LogsDirectoryMode = "0750";
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
       };
+    };
 
     # Add some reasonable default jails.  The special "DEFAULT" jail
     # sets default values for all other jails.
-    services.fail2ban.jails.DEFAULT =
-      ''
-        ignoreip = 127.0.0.1/8
-        bantime  = 600
-        findtime = 600
-        maxretry = 3
-        backend  = systemd
-        enabled  = true
-       '';
-
+    services.fail2ban.jails.DEFAULT = ''
+      ${optionalString cfg.bantime-increment.enable ''
+        # Bantime incremental
+        bantime.increment    = ${if cfg.bantime-increment.enable then "true" else "false"}
+        bantime.maxtime      = ${cfg.bantime-increment.maxtime}
+        bantime.factor       = ${cfg.bantime-increment.factor}
+        bantime.formula      = ${cfg.bantime-increment.formula}
+        bantime.multipliers  = ${cfg.bantime-increment.multipliers}
+        bantime.overalljails = ${if cfg.bantime-increment.overalljails then "true" else "false"}
+      ''}
+      # Miscellaneous options
+      ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
+      maxretry    = 3
+      backend     = systemd
+      # Actions
+      banaction   = ${cfg.banaction}
+      banaction_allports = ${cfg.banaction-allports}
+    '';
     # Block SSH if there are too many failing connection attempts.
-    services.fail2ban.jails.ssh-iptables =
-      ''
-        filter   = sshd
-        action   = iptables-multiport[name=SSH, port="${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}", protocol=tcp]
-        maxretry = 5
-      '';
-
+    services.fail2ban.jails.sshd = mkDefault ''
+      enabled = true
+      port    = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
+    '';
   };
-
 }
diff --git a/nixos/modules/services/security/fprot.nix b/nixos/modules/services/security/fprot.nix
index 47449039146..3a0b08b3c6d 100644
--- a/nixos/modules/services/security/fprot.nix
+++ b/nixos/modules/services/security/fprot.nix
@@ -10,12 +10,7 @@ in {
 
     services.fprot = {
       updater = {
-        enable = mkOption {
-          default = false;
-          description = ''
-            Whether to enable automatic F-Prot virus definitions database updates.
-          '';
-        };
+        enable = mkEnableOption "automatic F-Prot virus definitions database updates";
 
         productData = mkOption {
           description = ''
@@ -48,22 +43,18 @@ in {
     services.fprot.updater.licenseKeyfile = mkDefault "${pkgs.fprot}/opt/f-prot/license.key";
 
     environment.systemPackages = [ pkgs.fprot ];
-    environment.etc = singleton {
+    environment.etc."f-prot.conf" = {
       source = "${pkgs.fprot}/opt/f-prot/f-prot.conf";
-      target = "f-prot.conf";
     };
 
-    users.users = singleton
-      { name = fprotUser;
-        uid = config.ids.uids.fprot;
+    users.users.${fprotUser} =
+      { uid = config.ids.uids.fprot;
         description = "F-Prot daemon user";
         home = stateDir;
       };
 
-    users.groups = singleton
-      { name = fprotGroup;
-        gid = config.ids.gids.fprot;
-      };
+    users.groups.${fprotGroup} =
+      { gid = config.ids.gids.fprot; };
 
     services.cron.systemCronJobs = [ "*/${toString cfg.updater.frequency} * * * * root start fprot-updater" ];
 
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index 2abb9ec32ac..46caadee204 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -12,7 +12,7 @@ let
   # command-line to launch oauth2_proxy.
   providerSpecificOptions = {
     azure = cfg: {
-      azure.tenant = cfg.azure.tenant;
+      azure-tenant = cfg.azure.tenant;
       resource = cfg.azure.resource;
     };
 
@@ -44,6 +44,7 @@ let
     pass-access-token = passAccessToken;
     pass-basic-auth = passBasicAuth;
     pass-host-header = passHostHeader;
+    reverse-proxy = reverseProxy;
     proxy-prefix = proxyPrefix;
     profile-url = profileURL;
     redeem-url = redeemURL;
@@ -65,8 +66,8 @@ let
   } // lib.optionalAttrs (cfg.htpasswd.file != null) {
     display-htpasswd-file = cfg.htpasswd.displayForm;
   } // lib.optionalAttrs tls.enable {
-    tls-cert = tls.certificate;
-    tls-key = tls.key;
+    tls-cert-file = tls.certificate;
+    tls-key-file = tls.key;
     https-address = tls.httpsAddress;
   } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
 
@@ -98,14 +99,21 @@ in
 
     ##############################################
     # PROVIDER configuration
+    # Taken from: https://github.com/pusher/oauth2_proxy/blob/master/providers/providers.go
     provider = mkOption {
       type = types.enum [
         "google"
-        "github"
         "azure"
+        "facebook"
+        "github"
+        "keycloak"
         "gitlab"
         "linkedin"
-        "myusa"
+        "login.gov"
+        "bitbucket"
+        "nextcloud"
+        "digitalocean"
+        "oidc"
       ];
       default = "google";
       description = ''
@@ -433,6 +441,17 @@ in
       '';
     };
 
+    reverseProxy = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        In case when running behind a reverse proxy, controls whether headers
+	like <literal>X-Real-Ip</literal> are accepted. Usage behind a reverse
+        proxy will require this flag to be set to avoid logging the reverse
+        proxy IP address.
+      '';
+    };
+
     proxyPrefix = mkOption {
       type = types.str;
       default = "/oauth2";
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index 4a174564dd2..e7a9cefdef3 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -92,8 +92,11 @@ in {
         "-o cat"
         "-n1"
       ] ++ (map (name: "-t ${escapeShellArg name}") cfg.services));
+      backend = if config.networking.nftables.enable
+        then "sshg-fw-nft-sets"
+        else "sshg-fw-ipset";
     in ''
-      BACKEND="${pkgs.sshguard}/libexec/sshg-fw-ipset"
+      BACKEND="${pkgs.sshguard}/libexec/${backend}"
       LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
     '';
 
@@ -104,7 +107,9 @@ in {
       after = [ "network.target" ];
       partOf = optional config.networking.firewall.enable "firewall.service";
 
-      path = with pkgs; [ iptables ipset iproute systemd ];
+      path = with pkgs; if config.networking.nftables.enable
+        then [ nftables iproute systemd ]
+        else [ iptables ipset iproute systemd ];
 
       # The sshguard ipsets must exist before we invoke
       # iptables. sshguard creates the ipsets after startup if
@@ -112,14 +117,14 @@ in {
       # the iptables rules because postStart races with the creation
       # of the ipsets. So instead, we create both the ipsets and
       # firewall rules before sshguard starts.
-      preStart = ''
+      preStart = optionalString config.networking.firewall.enable ''
         ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:net family inet
         ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net 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
       '';
 
-      postStop = ''
+      postStop = optionalString config.networking.firewall.enable ''
         ${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
         ${pkgs.ipset}/bin/ipset -quiet destroy sshguard4
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index ed862387cce..18c105b2f57 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -106,6 +106,12 @@ let
 
 in
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "relay" "port" ])
+    (mkRemovedOptionModule [ "services" "tor" "relay" "isBridge" ] "Use services.tor.relay.role instead.")
+    (mkRemovedOptionModule [ "services" "tor" "relay" "isExit" ] "Use services.tor.relay.role instead.")
+  ];
+
   options = {
     services.tor = {
       enable = mkOption {
diff --git a/nixos/modules/services/security/torify.nix b/nixos/modules/services/security/torify.nix
index 08da726437e..39551190dd3 100644
--- a/nixos/modules/services/security/torify.nix
+++ b/nixos/modules/services/security/torify.nix
@@ -25,6 +25,7 @@ in
     services.tor.tsocks = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to build tsocks wrapper script to relay application traffic via Tor.
@@ -40,6 +41,7 @@ in
       };
 
       server = mkOption {
+        type = types.str;
         default = "localhost:9050";
         example = "192.168.0.20";
         description = ''
@@ -48,6 +50,7 @@ in
       };
 
       config = mkOption {
+        type = types.lines;
         default = "";
         description = ''
           Extra configuration. Contents will be added verbatim to TSocks
diff --git a/nixos/modules/services/security/torsocks.nix b/nixos/modules/services/security/torsocks.nix
index c60c745443b..47ac95c4626 100644
--- a/nixos/modules/services/security/torsocks.nix
+++ b/nixos/modules/services/security/torsocks.nix
@@ -112,10 +112,9 @@ in
   config = mkIf cfg.enable {
     environment.systemPackages = [ pkgs.torsocks (wrapTorsocks "torsocks-faster" cfg.fasterServer) ];
 
-    environment.etc =
-      [ { source = pkgs.writeText "torsocks.conf" (configFile cfg.server);
-          target = "tor/torsocks.conf";
-        }
-      ];
+    environment.etc."tor/torsocks.conf" =
+      {
+        source = pkgs.writeText "torsocks.conf" (configFile cfg.server);
+      };
   };
 }
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index b0ab8fadcbe..6a8a3a93327 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -135,6 +135,7 @@ in
         User = "vault";
         Group = "vault";
         ExecStart = "${cfg.package}/bin/vault server -config ${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
         PrivateDevices = true;
         PrivateTmp = true;
         ProtectSystem = "full";
diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix
index 936646a5fd7..4a60fec1ca8 100644
--- a/nixos/modules/services/system/dbus.nix
+++ b/nixos/modules/services/system/dbus.nix
@@ -68,10 +68,7 @@ in
 
     environment.systemPackages = [ pkgs.dbus.daemon pkgs.dbus ];
 
-    environment.etc = singleton
-      { source = configDir;
-        target = "dbus-1";
-      };
+    environment.etc."dbus-1".source = configDir;
 
     users.users.messagebus = {
       uid = config.ids.uids.messagebus;
diff --git a/nixos/modules/services/system/kerberos/default.nix b/nixos/modules/services/system/kerberos/default.nix
index c55241c4cff..9a1e6739901 100644
--- a/nixos/modules/services/system/kerberos/default.nix
+++ b/nixos/modules/services/system/kerberos/default.nix
@@ -51,12 +51,7 @@ in
   ###### interface
   options = {
     services.kerberos_server = {
-      enable = mkOption {
-        default = false;
-        description = ''
-          Enable the kerberos authentification server.
-        '';
-      };
+      enable = lib.mkEnableOption "the kerberos authentification server";
 
       realms = mkOption {
         type = types.attrsOf (types.submodule realm);
diff --git a/nixos/modules/services/system/localtime.nix b/nixos/modules/services/system/localtime.nix
index c3c0b432b49..8f8e2e2e933 100644
--- a/nixos/modules/services/system/localtime.nix
+++ b/nixos/modules/services/system/localtime.nix
@@ -8,6 +8,7 @@ in {
   options = {
     services.localtime = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Enable <literal>localtime</literal>, simple daemon for keeping the system
@@ -35,6 +36,10 @@ in {
     # Install the systemd unit.
     systemd.packages = [ pkgs.localtime.out ];
 
+    users.users.localtimed = {
+      description = "Taskserver user";
+    };
+
     systemd.services.localtime = {
       wantedBy = [ "multi-user.target" ];
       serviceConfig.Restart = "on-failure";
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
index e11f7e049d8..af26dd84707 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -7,6 +7,10 @@ let
   nssModulesPath = config.system.nssModules.path;
   cfg = config.services.nscd;
 
+  nscd = if pkgs.stdenv.hostPlatform.libc == "glibc"
+         then pkgs.stdenv.cc.libc.bin
+         else pkgs.glibc.bin;
+
 in
 
 {
@@ -59,16 +63,16 @@ in
         # files. So prefix the ExecStart command with "!" to prevent systemd
         # from dropping privileges early. See ExecStart in systemd.service(5).
         serviceConfig =
-          { ExecStart = "!@${pkgs.glibc.bin}/sbin/nscd nscd";
+          { ExecStart = "!@${nscd}/sbin/nscd nscd";
             Type = "forking";
             DynamicUser = true;
             RuntimeDirectory = "nscd";
             PIDFile = "/run/nscd/nscd.pid";
             Restart = "always";
             ExecReload =
-              [ "${pkgs.glibc.bin}/sbin/nscd --invalidate passwd"
-                "${pkgs.glibc.bin}/sbin/nscd --invalidate group"
-                "${pkgs.glibc.bin}/sbin/nscd --invalidate hosts"
+              [ "${nscd}/sbin/nscd --invalidate passwd"
+                "${nscd}/sbin/nscd --invalidate group"
+                "${nscd}/sbin/nscd --invalidate hosts"
               ];
           };
       };
diff --git a/nixos/modules/services/system/uptimed.nix b/nixos/modules/services/system/uptimed.nix
index 3c9978ab226..1e256c51408 100644
--- a/nixos/modules/services/system/uptimed.nix
+++ b/nixos/modules/services/system/uptimed.nix
@@ -10,6 +10,7 @@ in
   options = {
     services.uptimed = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Enable <literal>uptimed</literal>, allowing you to track
diff --git a/nixos/modules/services/torrent/deluge.nix b/nixos/modules/services/torrent/deluge.nix
index 0c72505395d..2f1e7300ca3 100644
--- a/nixos/modules/services/torrent/deluge.nix
+++ b/nixos/modules/services/torrent/deluge.nix
@@ -5,6 +5,7 @@ with lib;
 let
   cfg = config.services.deluge;
   cfg_web = config.services.deluge.web;
+  isDeluge1 = versionOlder cfg.package.version "2.0.0";
 
   openFilesLimit = 4096;
   listenPortsDefault = [ 6881 6889 ];
@@ -18,11 +19,11 @@ let
   preStart = if cfg.declarative then ''
     if [ -e ${declarativeLockFile} ]; then
       # Was declarative before, no need to back up anything
-      ln -sf ${configFile} ${configDir}/core.conf
+      ${if isDeluge1 then "ln -sf" else "cp"} ${configFile} ${configDir}/core.conf
       ln -sf ${cfg.authFile} ${configDir}/auth
     else
       # Declarative for the first time, backup stateful files
-      ln -sb --suffix=.stateful ${configFile} ${configDir}/core.conf
+      ${if isDeluge1 then "ln -s" else "cp"} -b --suffix=.stateful ${configFile} ${configDir}/core.conf
       ln -sb --suffix=.stateful ${cfg.authFile} ${configDir}/auth
       echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
         > ${declarativeLockFile}
@@ -144,6 +145,14 @@ in {
             This always contains unzip, gnutar, xz, p7zip and bzip2.
           '';
         };
+
+        package = mkOption {
+          type = types.package;
+          example = literalExample "pkgs.deluge-1_x";
+          description = ''
+            Deluge package to use.
+          '';
+        };
       };
 
       deluge.web = {
@@ -170,6 +179,13 @@ in {
 
   config = mkIf cfg.enable {
 
+    services.deluge.package = mkDefault (
+      if versionAtLeast config.system.stateVersion "20.09" then
+        pkgs.deluge-2_x
+      else
+        pkgs.deluge-1_x
+    );
+
     # Provide a default set of `extraPackages`.
     services.deluge.extraPackages = with pkgs; [ unzip gnutar xz p7zip bzip2 ];
 
@@ -189,10 +205,10 @@ in {
       after = [ "network.target" ];
       description = "Deluge BitTorrent Daemon";
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.deluge ] ++ cfg.extraPackages;
+      path = [ cfg.package ] ++ cfg.extraPackages;
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.deluge}/bin/deluged \
+          ${cfg.package}/bin/deluged \
             --do-not-daemonize \
             --config ${configDir}
         '';
@@ -212,10 +228,11 @@ in {
       requires = [ "deluged.service" ];
       description = "Deluge BitTorrent WebUI";
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.deluge ];
+      path = [ cfg.package ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.deluge}/bin/deluge-web \
+          ${cfg.package}/bin/deluge-web \
+            ${optionalString (!isDeluge1) "--do-not-daemonize"} \
             --config ${configDir} \
             --port ${toString cfg.web.port}
         '';
@@ -234,7 +251,7 @@ in {
       })
     ];
 
-    environment.systemPackages = [ pkgs.deluge ];
+    environment.systemPackages = [ cfg.package ];
 
     users.users = mkIf (cfg.user == "deluge") {
       deluge = {
diff --git a/nixos/modules/services/torrent/magnetico.nix b/nixos/modules/services/torrent/magnetico.nix
index 719827713ff..7465c10e002 100644
--- a/nixos/modules/services/torrent/magnetico.nix
+++ b/nixos/modules/services/torrent/magnetico.nix
@@ -213,4 +213,6 @@ in {
 
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 7409eb8cdcb..fd28b94f7be 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -7,6 +7,7 @@ let
   apparmor = config.security.apparmor.enable;
 
   homeDir = cfg.home;
+  downloadDirPermissions = cfg.downloadDirPermissions;
   downloadDir = "${homeDir}/Downloads";
   incompleteDir = "${homeDir}/.incomplete";
 
@@ -16,16 +17,15 @@ let
   # for users in group "transmission" to have access to torrents
   fullSettings = { umask = 2; download-dir = downloadDir; incomplete-dir = incompleteDir; } // cfg.settings;
 
-  # Directories transmission expects to exist and be ug+rwx.
-  directoriesToManage = [ homeDir settingsDir fullSettings.download-dir fullSettings.incomplete-dir ];
-
   preStart = pkgs.writeScript "transmission-pre-start" ''
     #!${pkgs.runtimeShell}
     set -ex
-    for DIR in ${escapeShellArgs directoriesToManage}; do
+    for DIR in "${homeDir}" "${settingsDir}" "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"; do
       mkdir -p "$DIR"
-      chmod 770 "$DIR"
     done
+    chmod 755 "${homeDir}"
+    chmod 700 "${settingsDir}"
+    chmod ${downloadDirPermissions} "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"
     cp -f ${settingsFile} ${settingsDir}/settings.json
   '';
 in
@@ -71,6 +71,16 @@ in
         '';
       };
 
+      downloadDirPermissions = mkOption {
+        type = types.str;
+        default = "770";
+        example = "775";
+        description = ''
+          The permissions to set for download-dir and incomplete-dir.
+          They will be applied on every service start.
+        '';
+      };
+
       port = mkOption {
         type = types.int;
         default = 9091;
@@ -109,7 +119,7 @@ in
       # 1) Only the "transmission" user and group have access to torrents.
       # 2) Optionally update/force specific fields into the configuration file.
       serviceConfig.ExecStartPre = preStart;
-      serviceConfig.ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f --port ${toString config.services.transmission.port}";
+      serviceConfig.ExecStart = "${pkgs.transmission}/bin/transmission-daemon -f --port ${toString config.services.transmission.port} --config-dir ${settingsDir}";
       serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
       serviceConfig.User = cfg.user;
       serviceConfig.Group = cfg.group;
@@ -120,19 +130,23 @@ in
     # It's useful to have transmission in path, e.g. for remote control
     environment.systemPackages = [ pkgs.transmission ];
 
-    users.users = optionalAttrs (cfg.user == "transmission") (singleton
-      { name = "transmission";
+    users.users = optionalAttrs (cfg.user == "transmission") ({
+      transmission = {
+        name = "transmission";
         group = cfg.group;
         uid = config.ids.uids.transmission;
         description = "Transmission BitTorrent user";
         home = homeDir;
         createHome = true;
-      });
+      };
+    });
 
-    users.groups = optionalAttrs (cfg.group == "transmission") (singleton
-      { name = "transmission";
+    users.groups = optionalAttrs (cfg.group == "transmission") ({
+      transmission = {
+        name = "transmission";
         gid = config.ids.gids.transmission;
-      });
+      };
+    });
 
     # AppArmor profile
     security.apparmor.profiles = mkIf apparmor [
diff --git a/nixos/modules/services/ttys/agetty.nix b/nixos/modules/services/ttys/agetty.nix
index f127d8a0276..f3a629f7af7 100644
--- a/nixos/modules/services/ttys/agetty.nix
+++ b/nixos/modules/services/ttys/agetty.nix
@@ -102,7 +102,7 @@ in
         enable = mkDefault config.boot.isContainer;
       };
 
-    environment.etc = singleton
+    environment.etc.issue =
       { # Friendly greeting on the virtual consoles.
         source = pkgs.writeText "issue" ''
 
@@ -110,7 +110,6 @@ in
           ${config.services.mingetty.helpLine}
 
         '';
-        target = "issue";
       };
 
   };
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
new file mode 100644
index 00000000000..c59ca9983a6
--- /dev/null
+++ b/nixos/modules/services/wayland/cage.nix
@@ -0,0 +1,99 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cage;
+in {
+  options.services.cage.enable = mkEnableOption "cage kiosk service";
+
+  options.services.cage.user = mkOption {
+    type = types.str;
+    default = "demo";
+    description = ''
+      User to log-in as.
+    '';
+  };
+
+  options.services.cage.extraArguments = mkOption {
+    type = types.listOf types.str;
+    default = [];
+    defaultText = "[]";
+    description = "Additional command line arguments to pass to Cage.";
+    example = ["-d"];
+  };
+
+  options.services.cage.program = mkOption {
+    type = types.path;
+    default = "${pkgs.xterm}/bin/xterm";
+    description = ''
+      Program to run in cage.
+    '';
+  };
+
+  config = mkIf cfg.enable {
+
+    # The service is partially based off of the one provided in the
+    # cage wiki at
+    # https://github.com/Hjdskes/cage/wiki/Starting-Cage-on-boot-with-systemd.
+    systemd.services."cage-tty1" = {
+      enable = true;
+      after = [
+        "systemd-user-sessions.service"
+        "plymouth-start.service"
+        "plymouth-quit.service"
+        "systemd-logind.service"
+        "getty@tty1.service"
+      ];
+      before = [ "graphical.target" ];
+      wants = [ "dbus.socket" "systemd-logind.service" "plymouth-quit.service"];
+      wantedBy = [ "graphical.target" ];
+      conflicts = [ "getty@tty1.service" ];
+
+      restartIfChanged = false;
+      unitConfig.ConditionPathExists = "/dev/tty1";
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.cage}/bin/cage \
+            ${escapeShellArgs cfg.extraArguments} \
+            -- ${cfg.program}
+        '';
+        User = cfg.user;
+
+        IgnoreSIGPIPE = "no";
+
+        # Log this user with utmp, letting it show up with commands 'w' and
+        # 'who'. This is needed since we replace (a)getty.
+        UtmpIdentifier = "%n";
+        UtmpMode = "user";
+        # A virtual terminal is needed.
+        TTYPath = "/dev/tty1";
+        TTYReset = "yes";
+        TTYVHangup = "yes";
+        TTYVTDisallocate = "yes";
+        # Fail to start if not controlling the virtual terminal.
+        StandardInput = "tty-fail";
+        StandardOutput = "syslog";
+        StandardError = "syslog";
+        # Set up a full (custom) user session for the user, required by Cage.
+        PAMName = "cage";
+      };
+    };
+
+    security.pam.services.cage.text = ''
+      auth    required pam_unix.so nullok
+      account required pam_unix.so
+      session required pam_unix.so
+      session required ${pkgs.systemd}/lib/security/pam_systemd.so
+    '';
+
+    hardware.opengl.enable = mkDefault true;
+
+    systemd.targets.graphical.wants = [ "cage-tty1.service" ];
+
+    systemd.defaultUnit = "graphical.target";
+  };
+
+  meta.maintainers = with lib.maintainers; [ matthewbauer flokli ];
+
+}
diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/codimd.nix
index 5f56f8ed5a0..751f81649dd 100644
--- a/nixos/modules/services/web-apps/codimd.nix
+++ b/nixos/modules/services/web-apps/codimd.nix
@@ -156,7 +156,7 @@ in
       };
       useCDN = mkOption {
         type = types.bool;
-        default = true;
+        default = false;
         description = ''
           Whether to use CDN resources or not.
         '';
diff --git a/nixos/modules/services/web-apps/documize.nix b/nixos/modules/services/web-apps/documize.nix
index 1b90299aa23..a5f48e744fd 100644
--- a/nixos/modules/services/web-apps/documize.nix
+++ b/nixos/modules/services/web-apps/documize.nix
@@ -130,7 +130,7 @@ in {
   config = mkIf cfg.enable {
     systemd.services.documize-server = {
       description = "Documize Wiki";
-      documentation = [ https://documize.com/ ];
+      documentation = [ "https://documize.com/" ];
       wantedBy = [ "multi-user.target" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
new file mode 100644
index 00000000000..76e18266a27
--- /dev/null
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -0,0 +1,388 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  inherit (lib) mkEnableOption mkForce mkIf mkMerge mkOption optionalAttrs recursiveUpdate types;
+  inherit (lib) concatMapStringsSep flatten mapAttrs mapAttrs' mapAttrsToList nameValuePair concatMapStringSep;
+
+  eachSite = config.services.dokuwiki;
+
+  user = "dokuwiki";
+  group = config.services.nginx.group;
+
+  dokuwikiAclAuthConfig = cfg: pkgs.writeText "acl.auth.php" ''
+    # acl.auth.php
+    # <?php exit()?>
+    #
+    # Access Control Lists
+    #
+    ${toString cfg.acl}
+  '';
+
+  dokuwikiLocalConfig = cfg: pkgs.writeText "local.php" ''
+    <?php
+    $conf['savedir'] = '${cfg.stateDir}';
+    $conf['superuser'] = '${toString cfg.superUser}';
+    $conf['useacl'] = '${toString cfg.aclUse}';
+    $conf['disableactions'] = '${cfg.disableActions}';
+    ${toString cfg.extraConfig}
+  '';
+
+  dokuwikiPluginsLocalConfig = cfg: pkgs.writeText "plugins.local.php" ''
+    <?php
+    ${cfg.pluginsConfig}
+  '';
+
+  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
+    pname = "dokuwiki-${hostName}";
+    version = src.version;
+    src = cfg.package;
+
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      # symlink the dokuwiki config
+      ln -s ${dokuwikiLocalConfig cfg} $out/share/dokuwiki/local.php
+
+      # symlink plugins config
+      ln -s ${dokuwikiPluginsLocalConfig cfg} $out/share/dokuwiki/plugins.local.php
+
+      # symlink acl
+      ln -s ${dokuwikiAclAuthConfig cfg} $out/share/dokuwiki/acl.auth.php
+
+      # symlink additional plugin(s) and templates(s)
+      ${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
+      ${concatMapStringsSep "\n" (plugin: "ln -s ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
+    '';
+  };
+
+  siteOpts = { config, lib, name, ...}: {
+    options = {
+      enable = mkEnableOption "DokuWiki web application.";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.dokuwiki;
+        description = "Which dokuwiki package to use.";
+      };
+
+      hostName = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "FQDN for the instance.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/dokuwiki/${name}/data";
+        description = "Location of the dokuwiki state directory.";
+      };
+
+      acl = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        example = "*               @ALL               8";
+        description = ''
+          Access Control Lists: see <link xlink:href="https://www.dokuwiki.org/acl"/>
+          Mutually exclusive with services.dokuwiki.aclFile
+          Set this to a value other than null to take precedence over aclFile option.
+
+          Warning: Consider using aclFile instead if you do not
+          want to store the ACL in the world-readable Nix store.
+        '';
+      };
+
+      aclFile = mkOption {
+        type = with types; nullOr str;
+        default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
+        description = ''
+          Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
+          Mutually exclusive with services.dokuwiki.acl which is preferred.
+          Consult documentation <link xlink:href="https://www.dokuwiki.org/acl"/> for further instructions.
+          Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist"/>
+        '';
+        example = "/var/lib/dokuwiki/${name}/acl.auth.php";
+      };
+
+      aclUse = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Necessary for users to log in into the system.
+          Also limits anonymous users. When disabled,
+          everyone is able to create and edit content.
+        '';
+      };
+
+      pluginsConfig = mkOption {
+        type = types.lines;
+        default = ''
+          $plugins['authad'] = 0;
+          $plugins['authldap'] = 0;
+          $plugins['authmysql'] = 0;
+          $plugins['authpgsql'] = 0;
+        '';
+        description = ''
+          List of the dokuwiki (un)loaded plugins.
+        '';
+      };
+
+      superUser = mkOption {
+        type = types.nullOr types.str;
+        default = "@admin";
+        description = ''
+          You can set either a username, a list of usernames (“admin1,admin2”),
+          or the name of a group by prepending an @ char to the groupname
+          Consult documentation <link xlink:href="https://www.dokuwiki.org/config:superuser"/> for further instructions.
+        '';
+      };
+
+      usersFile = mkOption {
+        type = with types; nullOr str;
+        default = if config.aclUse then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
+        description = ''
+          Location of the dokuwiki users file. List of users. Format:
+          login:passwordhash:Real Name:email:groups,comma,separated
+          Create passwordHash easily by using:$ mkpasswd -5 password `pwgen 8 1`
+          Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist"/>
+          '';
+        example = "/var/lib/dokuwiki/${name}/users.auth.php";
+      };
+
+      disableActions = mkOption {
+        type = types.nullOr types.str;
+        default = "";
+        example = "search,register";
+        description = ''
+          Disable individual action modes. Refer to
+          <link xlink:href="https://www.dokuwiki.org/config:action_modes"/>
+          for details on supported values.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        example = ''
+          $conf['title'] = 'My Wiki';
+          $conf['userewrite'] = 1;
+        '';
+        description = ''
+          DokuWiki configuration. Refer to
+          <link xlink:href="https://www.dokuwiki.org/config"/>
+          for details on supported values.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+              List of path(s) to respective plugin(s) which are copied from the 'plugin' directory.
+              <note><para>These plugins need to be packaged before use, see example.</para></note>
+        '';
+        example = ''
+              # Let's package the icalevents plugin
+              plugin-icalevents = pkgs.stdenv.mkDerivation {
+                name = "icalevents";
+                # Download the plugin from the dokuwiki site
+                src = pkgs.fetchurl {
+                  url = https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/2017-06-16/dokuwiki-plugin-icalevents-2017-06-16.zip;
+                  sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
+                };
+                sourceRoot = ".";
+                # We need unzip to build this package
+                buildInputs = [ pkgs.unzip ];
+                # Installing simply means copying all files to the output directory
+                installPhase = "mkdir -p $out; cp -R * $out/";
+              };
+
+              # And then pass this theme to the plugin list like this:
+              plugins = [ plugin-icalevents ];
+        '';
+      };
+
+      templates = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+              List of path(s) to respective template(s) which are copied from the 'tpl' directory.
+              <note><para>These templates need to be packaged before use, see example.</para></note>
+        '';
+        example = ''
+              # Let's package the bootstrap3 theme
+              template-bootstrap3 = pkgs.stdenv.mkDerivation {
+                name = "bootstrap3";
+                # Download the theme from the dokuwiki site
+                src = pkgs.fetchurl {
+                  url = https://github.com/giterlizzi/dokuwiki-template-bootstrap3/archive/v2019-05-22.zip;
+                  sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
+                };
+                # We need unzip to build this package
+                buildInputs = [ pkgs.unzip ];
+                # Installing simply means copying all files to the output directory
+                installPhase = "mkdir -p $out; cp -R * $out/";
+              };
+
+              # And then pass this theme to the template list like this:
+              templates = [ template-bootstrap3 ];
+        '';
+      };
+
+      poolConfig = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool ]);
+        default = {
+          "pm" = "dynamic";
+          "pm.max_children" = 32;
+          "pm.start_servers" = 2;
+          "pm.min_spare_servers" = 2;
+          "pm.max_spare_servers" = 4;
+          "pm.max_requests" = 500;
+        };
+        description = ''
+          Options for the dokuwiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
+          for details on configuration directives.
+        '';
+      };
+
+      nginx = mkOption {
+        type = types.submodule (
+          recursiveUpdate
+            (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
+            {
+              # Enable encryption by default,
+              options.forceSSL.default = true;
+              options.enableACME.default = true;
+            }
+        );
+        default = {forceSSL = true; enableACME = true;};
+        example = {
+          serverAliases = [
+            "wiki.\${config.networking.domain}"
+          ];
+          enableACME = false;
+        };
+        description = ''
+          With this option, you can customize the nginx virtualHost which already has sensible defaults for DokuWiki.
+        '';
+      };
+    };
+  };
+in
+{
+  # interface
+  options = {
+    services.dokuwiki = mkOption {
+      type = types.attrsOf (types.submodule siteOpts);
+      default = {};
+      description = "Sepcification of one or more dokuwiki sites to service.";
+    };
+  };
+
+  # implementation
+
+  config = mkIf (eachSite != {}) {
+
+    warnings = mapAttrsToList (hostName: cfg: mkIf (cfg.superUser == null) "Not setting services.dokuwiki.${hostName} superUser will impair your ability to administer DokuWiki") eachSite;
+
+    assertions = flatten (mapAttrsToList (hostName: cfg:
+    [{
+      assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
+      message = "Either services.dokuwiki.${hostName}.acl or services.dokuwiki.${hostName}.aclFile is mandatory if aclUse true";
+    }
+    {
+      assertion = cfg.usersFile != null -> cfg.aclUse != false;
+      message = "services.dokuwiki.${hostName}.aclUse must must be true if usersFile is not null";
+    }
+    ]) eachSite);
+
+    services.phpfpm.pools = mapAttrs' (hostName: cfg: (
+      nameValuePair "dokuwiki-${hostName}" {
+        inherit user;
+        inherit group;
+        phpEnv = {
+          DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig cfg}";
+          DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig cfg}";
+        } // optionalAttrs (cfg.usersFile != null) {
+          DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
+        } //optionalAttrs (cfg.aclUse) {
+          DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig cfg}" else "${toString cfg.aclFile}";
+        };
+
+        settings = {
+          "listen.mode" = "0660";
+          "listen.owner" = user;
+          "listen.group" = group;
+        } // cfg.poolConfig;
+      })) eachSite;
+
+    services.nginx = {
+      enable = true;
+      virtualHosts = mapAttrs (hostName: cfg:  mkMerge [ cfg.nginx {
+        root = mkForce "${pkg hostName cfg}/share/dokuwiki";
+        extraConfig = "fastcgi_param HTTPS on;";
+
+        locations."~ /(conf/|bin/|inc/|install.php)" = {
+          extraConfig = "deny all;";
+        };
+
+        locations."~ ^/data/" = {
+          root = "${cfg.stateDir}";
+          extraConfig = "internal;";
+        };
+
+        locations."~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
+          extraConfig = "expires 365d;";
+        };
+
+        locations."/" = {
+          priority = 1;
+          index = "doku.php";
+          extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
+        };
+
+        locations."@dokuwiki" = {
+          extraConfig = ''
+              # rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page
+              rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
+              rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
+              rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
+              rewrite ^/(.*) /doku.php?id=$1&$args last;
+          '';
+        };
+
+        locations."~ \.php$" = {
+          extraConfig = ''
+              try_files $uri $uri/ /doku.php;
+              include ${pkgs.nginx}/conf/fastcgi_params;
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              fastcgi_param REDIRECT_STATUS 200;
+              fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
+              fastcgi_param HTTPS on;
+          '';
+        };
+      }]) eachSite;
+    };
+
+    systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
+      "d ${cfg.stateDir}/attic 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/cache 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/index 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/locks 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/media 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/media_attic 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/media_meta 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/meta 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/pages 0750 ${user} ${group} - -"
+      "d ${cfg.stateDir}/tmp 0750 ${user} ${group} - -"
+    ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist"
+    ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist"
+    ) eachSite);
+
+    users.users.${user} = {
+      group = group;
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/frab.nix b/nixos/modules/services/web-apps/frab.nix
index a9a30b40922..1b5890d6b0c 100644
--- a/nixos/modules/services/web-apps/frab.nix
+++ b/nixos/modules/services/web-apps/frab.nix
@@ -173,15 +173,13 @@ in
   config = mkIf cfg.enable {
     environment.systemPackages = [ frab-rake ];
 
-    users.users = [
-      { name = cfg.user;
-        group = cfg.group;
+    users.users.${cfg.user} =
+      { group = cfg.group;
         home = "${cfg.statePath}";
         isSystemUser = true;
-      }
-    ];
+      };
 
-    users.groups = [ { name = cfg.group; } ];
+    users.groups.${cfg.group} = { };
 
     systemd.tmpfiles.rules = [
       "d '${cfg.statePath}/system/attachments' - ${cfg.user} ${cfg.group} - -"
diff --git a/nixos/modules/services/web-apps/gerrit.nix b/nixos/modules/services/web-apps/gerrit.nix
new file mode 100644
index 00000000000..b184c0754d4
--- /dev/null
+++ b/nixos/modules/services/web-apps/gerrit.nix
@@ -0,0 +1,218 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.gerrit;
+
+  # NixOS option type for git-like configs
+  gitIniType = with types;
+    let
+      primitiveType = either str (either bool int);
+      multipleType = either primitiveType (listOf primitiveType);
+      sectionType = lazyAttrsOf multipleType;
+      supersectionType = lazyAttrsOf (either multipleType sectionType);
+    in lazyAttrsOf supersectionType;
+
+  gerritConfig = pkgs.writeText "gerrit.conf" (
+    lib.generators.toGitINI cfg.settings
+  );
+
+  # Wrap the gerrit java with all the java options so it can be called
+  # like a normal CLI app
+  gerrit-cli = pkgs.writeShellScriptBin "gerrit" ''
+    set -euo pipefail
+    jvmOpts=(
+      ${lib.escapeShellArgs cfg.jvmOpts}
+      -Xmx${cfg.jvmHeapLimit}
+    )
+    exec ${cfg.jvmPackage}/bin/java \
+      "''${jvmOpts[@]}" \
+      -jar ${cfg.package}/webapps/${cfg.package.name}.war \
+      "$@"
+  '';
+
+  gerrit-plugins = pkgs.runCommand
+    "gerrit-plugins"
+    {
+      buildInputs = [ gerrit-cli ];
+    }
+    ''
+      shopt -s nullglob
+      mkdir $out
+
+      for name in ${toString cfg.builtinPlugins}; do
+        echo "Installing builtin plugin $name.jar"
+        gerrit cat plugins/$name.jar > $out/$name.jar
+      done
+
+      for file in ${toString cfg.plugins}; do
+        name=$(echo "$file" | cut -d - -f 2-)
+        echo "Installing plugin $name"
+        ln -sf "$file" $out/$name
+      done
+    '';
+in
+{
+  options = {
+    services.gerrit = {
+      enable = mkEnableOption "Gerrit service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.gerrit;
+        description = "Gerrit package to use";
+      };
+
+      jvmPackage = mkOption {
+        type = types.package;
+        default = pkgs.jre_headless;
+        defaultText = "pkgs.jre_headless";
+        description = "Java Runtime Environment package to use";
+      };
+
+      jvmOpts = mkOption {
+        type = types.listOf types.str;
+        default = [
+          "-Dflogger.backend_factory=com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance"
+          "-Dflogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance"
+        ];
+        description = "A list of JVM options to start gerrit with.";
+      };
+
+      jvmHeapLimit = mkOption {
+        type = types.str;
+        default = "1024m";
+        description = ''
+          How much memory to allocate to the JVM heap
+        '';
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "[::]:8080";
+        description = ''
+          <literal>hostname:port</literal> to listen for HTTP traffic.
+
+          This is bound using the systemd socket activation.
+        '';
+      };
+
+      settings = mkOption {
+        type = gitIniType;
+        default = {};
+        description = ''
+          Gerrit configuration. This will be generated to the
+          <literal>etc/gerrit.config</literal> file.
+        '';
+      };
+
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          List of plugins to add to Gerrit. Each derivation is a jar file
+          itself where the name of the derivation is the name of plugin.
+        '';
+      };
+
+      builtinPlugins = mkOption {
+        type = types.listOf (types.enum cfg.package.passthru.plugins);
+        default = [];
+        description = ''
+          List of builtins plugins to install. Those are shipped in the
+          <literal>gerrit.war</literal> file.
+        '';
+      };
+
+      serverId = mkOption {
+        type = types.str;
+        description = ''
+          Set a UUID that uniquely identifies the server.
+
+          This can be generated with
+          <literal>nix-shell -p utillinux --run uuidgen</literal>.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    services.gerrit.settings = {
+      cache.directory = "/var/cache/gerrit";
+      container.heapLimit = cfg.jvmHeapLimit;
+      gerrit.basePath = lib.mkDefault "git";
+      gerrit.serverId = cfg.serverId;
+      httpd.inheritChannel = "true";
+      httpd.listenUrl = lib.mkDefault "http://${cfg.listenAddress}";
+      index.type = lib.mkDefault "lucene";
+    };
+
+    # Add the gerrit CLI to the system to run `gerrit init` and friends.
+    environment.systemPackages = [ gerrit-cli ];
+
+    systemd.sockets.gerrit = {
+      unitConfig.Description = "Gerrit HTTP socket";
+      wantedBy = [ "sockets.target" ];
+      listenStreams = [ cfg.listenAddress ];
+    };
+
+    systemd.services.gerrit = {
+      description = "Gerrit";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "gerrit.socket" ];
+      after = [ "gerrit.socket" "network.target" ];
+
+      path = [
+        gerrit-cli
+        pkgs.bash
+        pkgs.coreutils
+        pkgs.git
+        pkgs.openssh
+      ];
+
+      environment = {
+        GERRIT_HOME = "%S/gerrit";
+        GERRIT_TMP = "%T";
+        HOME = "%S/gerrit";
+        XDG_CONFIG_HOME = "%S/gerrit/.config";
+      };
+
+      preStart = ''
+        set -euo pipefail
+
+        # bootstrap if nothing exists
+        if [[ ! -d git ]]; then
+          gerrit init --batch --no-auto-start
+        fi
+
+        # install gerrit.war for the plugin manager
+        rm -rf bin
+        mkdir bin
+        ln -sfv ${cfg.package}/webapps/${cfg.package.name}.war bin/gerrit.war
+
+        # copy the config, keep it mutable because Gerrit
+        ln -sfv ${gerritConfig} etc/gerrit.config
+
+        # install the plugins
+        rm -rf plugins
+        ln -sv ${gerrit-plugins} plugins
+      ''
+      ;
+
+      serviceConfig = {
+        CacheDirectory = "gerrit";
+        DynamicUser = true;
+        ExecStart = "${gerrit-cli}/bin/gerrit daemon --console-log";
+        LimitNOFILE = 4096;
+        StandardInput = "socket";
+        StandardOutput = "journal";
+        StateDirectory = "gerrit";
+        WorkingDirectory = "%S/gerrit";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ edef zimbatm ];
+}
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
new file mode 100644
index 00000000000..568bdfd0c42
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grocy;
+in {
+  options.services.grocy = {
+    enable = mkEnableOption "grocy";
+
+    hostName = mkOption {
+      type = types.str;
+      description = ''
+        FQDN for the grocy instance.
+      '';
+    };
+
+    nginx.enableSSL = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether or not to enable SSL (with ACME and let's encrypt)
+        for the grocy vhost.
+      '';
+    };
+
+    phpfpm.settings = mkOption {
+      type = with types; attrsOf (oneOf [ int str bool ]);
+      default = {
+        "pm" = "dynamic";
+        "php_admin_value[error_log]" = "stderr";
+        "php_admin_flag[log_errors]" = true;
+        "listen.owner" = "nginx";
+        "catch_workers_output" = true;
+        "pm.max_children" = "32";
+        "pm.start_servers" = "2";
+        "pm.min_spare_servers" = "2";
+        "pm.max_spare_servers" = "4";
+        "pm.max_requests" = "500";
+      };
+
+      description = ''
+        Options for grocy's PHPFPM pool.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/grocy";
+      description = ''
+        Home directory of the <literal>grocy</literal> user which contains
+        the application's state.
+      '';
+    };
+
+    settings = {
+      currency = mkOption {
+        type = types.str;
+        default = "USD";
+        example = "EUR";
+        description = ''
+          ISO 4217 code for the currency to display.
+        '';
+      };
+
+      culture = mkOption {
+        type = types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
+        default = "en";
+        description = ''
+          Display language of the frontend.
+        '';
+      };
+
+      calendar = {
+        showWeekNumber = mkOption {
+          default = true;
+          type = types.bool;
+          description = ''
+            Show the number of the weeks in the calendar views.
+          '';
+        };
+        firstDayOfWeek = mkOption {
+          default = null;
+          type = types.nullOr (types.enum (range 0 6));
+          description = ''
+            Which day of the week (0=Sunday, 1=Monday etc.) should be the
+            first day.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."grocy/config.php".text = ''
+      <?php
+      Setting('CULTURE', '${cfg.settings.culture}');
+      Setting('CURRENCY', '${cfg.settings.currency}');
+      Setting('CALENDAR_FIRST_DAY_OF_WEEK', '${toString cfg.settings.calendar.firstDayOfWeek}');
+      Setting('CALENDAR_SHOW_WEEK_OF_YEAR', ${boolToString cfg.settings.calendar.showWeekNumber});
+    '';
+
+    users.users.grocy = {
+      isSystemUser = true;
+      createHome = true;
+      home = cfg.dataDir;
+      group = "nginx";
+    };
+
+    systemd.tmpfiles.rules = map (
+      dirName: "d '${cfg.dataDir}/${dirName}' - grocy nginx - -"
+    ) [ "viewcache" "plugins" "settingoverrides" "storage" ];
+
+    services.phpfpm.pools.grocy = {
+      user = "grocy";
+      group = "nginx";
+
+      # PHP 7.3 is the only version which is supported/tested by upstream:
+      # https://github.com/grocy/grocy/blob/v2.6.0/README.md#how-to-install
+      phpPackage = pkgs.php73;
+
+      inherit (cfg.phpfpm) settings;
+
+      phpEnv = {
+        GROCY_CONFIG_FILE = "/etc/grocy/config.php";
+        GROCY_DB_FILE = "${cfg.dataDir}/grocy.db";
+        GROCY_STORAGE_DIR = "${cfg.dataDir}/storage";
+        GROCY_PLUGIN_DIR = "${cfg.dataDir}/plugins";
+        GROCY_CACHE_DIR = "${cfg.dataDir}/viewcache";
+      };
+    };
+
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.hostName}" = mkMerge [
+        { root = "${pkgs.grocy}/public";
+          locations."/".extraConfig = ''
+            rewrite ^ /index.php;
+          '';
+          locations."~ \\.php$".extraConfig = ''
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass unix:${config.services.phpfpm.pools.grocy.socket};
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+          '';
+          locations."~ \\.(js|css|ttf|woff2?|png|jpe?g|svg)$".extraConfig = ''
+            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;
+          '';
+          extraConfig = ''
+            try_files $uri /index.php;
+          '';
+        }
+        (mkIf cfg.nginx.enableSSL {
+          enableACME = true;
+          forceSSL = true;
+        })
+      ];
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ ma27 ];
+    doc = ./grocy.xml;
+  };
+}
diff --git a/nixos/modules/services/web-apps/grocy.xml b/nixos/modules/services/web-apps/grocy.xml
new file mode 100644
index 00000000000..fdf6d00f4b1
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.xml
@@ -0,0 +1,77 @@
+<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-grocy">
+
+  <title>Grocy</title>
+  <para>
+    <link xlink:href="https://grocy.info/">Grocy</link> is a web-based self-hosted groceries
+    &amp; household management solution for your home.
+  </para>
+
+  <section xml:id="module-services-grocy-basic-usage">
+   <title>Basic usage</title>
+   <para>
+    A very basic configuration may look like this:
+<programlisting>{ pkgs, ... }:
+{
+  services.grocy = {
+    <link linkend="opt-services.grocy.enable">enable</link> = true;
+    <link linkend="opt-services.grocy.hostName">hostName</link> = "grocy.tld";
+  };
+}</programlisting>
+    This configures a simple vhost using <link linkend="opt-services.nginx.enable">nginx</link>
+    which listens to <literal>grocy.tld</literal> with fully configured ACME/LE (this can be
+    disabled by setting <link linkend="opt-services.grocy.nginx.enableSSL">services.grocy.nginx.enableSSL</link>
+    to <literal>false</literal>). After the initial setup the credentials <literal>admin:admin</literal>
+    can be used to login.
+   </para>
+   <para>
+    The application's state is persisted at <literal>/var/lib/grocy/grocy.db</literal> in a
+    <package>sqlite3</package> database. The migration is applied when requesting the <literal>/</literal>-route
+    of the application.
+   </para>
+  </section>
+
+  <section xml:id="module-services-grocy-settings">
+   <title>Settings</title>
+   <para>
+    The configuration for <literal>grocy</literal> is located at <literal>/etc/grocy/config.php</literal>.
+    By default, the following settings can be defined in the NixOS-configuration:
+<programlisting>{ pkgs, ... }:
+{
+  services.grocy.settings = {
+    # The default currency in the system for invoices etc.
+    # Please note that exchange rates aren't taken into account, this
+    # is just the setting for what's shown in the frontend.
+    <link linkend="opt-services.grocy.settings.currency">currency</link> = "EUR";
+
+    # The display language (and locale configuration) for grocy.
+    <link linkend="opt-services.grocy.settings.currency">culture</link> = "de";
+
+    calendar = {
+      # Whether or not to show the week-numbers
+      # in the calendar.
+      <link linkend="opt-services.grocy.settings.calendar.showWeekNumber">showWeekNumber</link> = true;
+
+      # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
+      # 2=Tuesday and so on).
+      <link linkend="opt-services.grocy.settings.calendar.firstDayOfWeek">firstDayOfWeek</link> = 2;
+    };
+  };
+}</programlisting>
+   </para>
+   <para>
+    If you want to alter the configuration file on your own, you can do this manually with
+    an expression like this:
+<programlisting>{ lib, ... }:
+{
+  environment.etc."grocy/config.php".text = lib.mkAfter ''
+    // Arbitrary PHP code in grocy's configuration file
+  '';
+}</programlisting>
+   </para>
+  </section>
+
+</chapter>
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
new file mode 100644
index 00000000000..68769ac8c03
--- /dev/null
+++ b/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -0,0 +1,141 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.ihatemoney;
+  user = "ihatemoney";
+  group = "ihatemoney";
+  db = "ihatemoney";
+  python3 = config.services.uwsgi.package.python3;
+  pkg = python3.pkgs.ihatemoney;
+  toBool = x: if x then "True" else "False";
+  configFile = pkgs.writeText "ihatemoney.cfg" ''
+        from secrets import token_hex
+        # load a persistent secret key
+        SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
+        SECRET_KEY = ""
+        try:
+          with open(SECRET_KEY_FILE) as f:
+            SECRET_KEY = f.read()
+        except FileNotFoundError:
+          pass
+        if not SECRET_KEY:
+          print("ihatemoney: generating a new secret key")
+          SECRET_KEY = token_hex(50)
+          with open(SECRET_KEY_FILE, "w") as f:
+            f.write(SECRET_KEY)
+        del token_hex
+        del SECRET_KEY_FILE
+
+        # "normal" configuration
+        DEBUG = False
+        SQLALCHEMY_DATABASE_URI = '${
+          if cfg.backend == "sqlite"
+          then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
+          else "postgresql:///${db}"}'
+        SQLALCHEMY_TRACK_MODIFICATIONS = False
+        MAIL_DEFAULT_SENDER = ("${cfg.defaultSender.name}", "${cfg.defaultSender.email}")
+        ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
+        ADMIN_PASSWORD = "${toString cfg.adminHashedPassword /*toString null == ""*/}"
+        ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
+        ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
+
+        ${cfg.extraConfig}
+  '';
+in
+  {
+    options.services.ihatemoney = {
+      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root";
+      backend = mkOption {
+        type = types.enum [ "sqlite" "postgresql" ];
+        default = "sqlite";
+        description = ''
+          The database engine to use for ihatemoney.
+          If <literal>postgresql</literal> is selected, then a database called
+          <literal>${db}</literal> will be created. If you disable this option,
+          it will however not be removed.
+        '';
+      };
+      adminHashedPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "The hashed password of the administrator. To obtain it, run <literal>ihatemoney generate_password_hash</literal>";
+      };
+      uwsgiConfig = mkOption {
+        type = types.attrs;
+        example = {
+          http = ":8000";
+        };
+        description = "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
+      };
+      defaultSender = {
+        name = mkOption {
+          type = types.str;
+          default = "Budget manager";
+          description = "The display name of the sender of ihatemoney emails";
+        };
+        email = mkOption {
+          type = types.str;
+          default = "ihatemoney@${config.networking.hostName}";
+          description = "The email of the sender of ihatemoney emails";
+        };
+      };
+      enableDemoProject = mkEnableOption "access to the demo project in ihatemoney";
+      enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone";
+      enableAdminDashboard = mkEnableOption "ihatemoney admin dashboard";
+      extraConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
+      };
+    };
+    config = mkIf cfg.enable {
+      services.postgresql = mkIf (cfg.backend == "postgresql") {
+        enable = true;
+        ensureDatabases = [ db ];
+        ensureUsers = [ {
+          name = user;
+          ensurePermissions = {
+            "DATABASE ${db}" = "ALL PRIVILEGES";
+          };
+        } ];
+      };
+      systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
+        wantedBy = [ "uwsgi.service" ];
+        before = [ "uwsgi.service" ];
+      };
+      systemd.tmpfiles.rules = [
+        "d /var/lib/ihatemoney 770 ${user} ${group}"
+      ];
+      users = {
+        users.${user} = {
+          isSystemUser = true;
+          inherit group;
+        };
+        groups.${group} = {};
+      };
+      services.uwsgi = {
+        enable = true;
+        plugins = [ "python3" ];
+        # the vassal needs to be able to setuid
+        user = "root";
+        group = "root";
+        instance = {
+          type = "emperor";
+          vassals.ihatemoney = {
+            type = "normal";
+            strict = true;
+            uid = user;
+            gid = group;
+            # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
+            enable-threads = true;
+            module = "wsgi:application";
+            chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
+            env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
+            pythonPackages = self: [ self.ihatemoney ];
+          } // cfg.uwsgiConfig;
+        };
+      };
+    };
+  }
+
+
diff --git a/nixos/modules/services/web-apps/jirafeau.nix b/nixos/modules/services/web-apps/jirafeau.nix
new file mode 100644
index 00000000000..4f181257ef7
--- /dev/null
+++ b/nixos/modules/services/web-apps/jirafeau.nix
@@ -0,0 +1,169 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.jirafeau;
+
+  group = config.services.nginx.group;
+  user = config.services.nginx.user;
+
+  withTrailingSlash = str: if hasSuffix "/" str then str else "${str}/";
+
+  localConfig = pkgs.writeText "config.local.php" ''
+    <?php
+      $cfg['admin_password'] = '${cfg.adminPasswordSha256}';
+      $cfg['web_root'] = 'http://${withTrailingSlash cfg.hostName}';
+      $cfg['var_root'] = '${withTrailingSlash cfg.dataDir}';
+      $cfg['maximal_upload_size'] = ${builtins.toString cfg.maxUploadSizeMegabytes};
+      $cfg['installation_done'] = true;
+
+      ${cfg.extraConfig}
+  '';
+in
+{
+  options.services.jirafeau = {
+    adminPasswordSha256 = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        SHA-256 of the desired administration password. Leave blank/unset for no password.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/jirafeau/data/";
+      description = "Location of Jirafeau storage directory.";
+    };
+
+    enable = mkEnableOption "Jirafeau file upload application.";
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        $cfg['style'] = 'courgette';
+        $cfg['organisation'] = 'ACME';
+      '';
+      description = let
+        documentationLink =
+          "https://gitlab.com/mojo42/Jirafeau/-/blob/${cfg.package.version}/lib/config.original.php";
+      in
+        ''
+          Jirefeau configuration. Refer to <link xlink:href="${documentationLink}"/> for supported
+          values.
+        '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "URL of instance. Must have trailing slash.";
+    };
+
+    maxUploadSizeMegabytes = mkOption {
+      type = types.int;
+      default = 0;
+      description = "Maximum upload size of accepted files.";
+    };
+
+    maxUploadTimeout = mkOption {
+      type = types.str;
+      default = "30m";
+      description = let
+        nginxCoreDocumentation = "http://nginx.org/en/docs/http/ngx_http_core_module.html";
+      in
+        ''
+          Timeout for reading client request bodies and headers. Refer to
+          <link xlink:href="${nginxCoreDocumentation}#client_body_timeout"/> and
+          <link xlink:href="${nginxCoreDocumentation}#client_header_timeout"/> for accepted values.
+        '';
+    };
+
+    nginxConfig = mkOption {
+      type = types.submodule
+        (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
+      default = {};
+      example = {
+        serverAliases = [ "wiki.\${config.networking.domain}" ];
+      };
+      description = "Extra configuration for the nginx virtual host of Jirafeau.";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.jirafeau;
+      defaultText = "pkgs.jirafeau";
+      description = "Jirafeau package to use";
+      example = "pkgs.jirafeau";
+    };
+
+    poolConfig = mkOption {
+      type = with types; attrsOf (oneOf [ str int bool ]);
+      default = {
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 4;
+        "pm.max_requests" = 500;
+      };
+      description = ''
+        Options for Jirafeau PHP pool. See documentation on <literal>php-fpm.conf</literal> for
+        details on configuration directives.
+      '';
+    };
+  };
+
+
+  config = mkIf cfg.enable {
+    services = {
+      nginx = {
+        enable = true;
+        virtualHosts."${cfg.hostName}" = mkMerge [
+          cfg.nginxConfig
+          {
+            extraConfig = let
+              clientMaxBodySize =
+                if cfg.maxUploadSizeMegabytes == 0 then "0" else "${cfg.maxUploadSizeMegabytes}m";
+            in
+              ''
+                index index.php;
+                client_max_body_size ${clientMaxBodySize};
+                client_body_timeout ${cfg.maxUploadTimeout};
+                client_header_timeout ${cfg.maxUploadTimeout};
+              '';
+            locations = {
+              "~ \\.php$".extraConfig = ''
+                include ${pkgs.nginx}/conf/fastcgi_params;
+                fastcgi_split_path_info ^(.+\.php)(/.+)$;
+                fastcgi_index index.php;
+                fastcgi_pass unix:${config.services.phpfpm.pools.jirafeau.socket};
+                fastcgi_param PATH_INFO $fastcgi_path_info;
+                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+              '';
+            };
+            root = mkForce "${cfg.package}";
+          }
+        ];
+      };
+
+      phpfpm.pools.jirafeau = {
+        inherit group user;
+        phpEnv."JIRAFEAU_CONFIG" = "${localConfig}";
+        settings = {
+          "listen.mode" = "0660";
+          "listen.owner" = user;
+          "listen.group" = group;
+        } // cfg.poolConfig;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.dataDir} 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/files/ 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/links/ 0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/async/ 0750 ${user} ${group} - -"
+    ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/limesurvey.nix b/nixos/modules/services/web-apps/limesurvey.nix
index bd524524130..56265e80957 100644
--- a/nixos/modules/services/web-apps/limesurvey.nix
+++ b/nixos/modules/services/web-apps/limesurvey.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
-  inherit (lib) mapAttrs optional optionalString types;
+  inherit (lib) literalExample mapAttrs optional optionalString types;
 
   cfg = config.services.limesurvey;
   fpm = config.services.phpfpm.pools.limesurvey;
@@ -100,19 +100,15 @@ in
     };
 
     virtualHost = mkOption {
-      type = types.submodule ({
-        options = import ../web-servers/apache-httpd/per-server-options.nix {
-          inherit lib;
-          forMainServer = false;
-        };
-      });
-      example = {
-        hostName = "survey.example.org";
-        enableSSL = true;
-        adminAddr = "webmaster@example.org";
-        sslServerCert = "/var/lib/acme/survey.example.org/full.pem";
-        sslServerKey = "/var/lib/acme/survey.example.org/key.pem";
-      };
+      type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+      example = literalExample ''
+        {
+          hostName = "survey.example.org";
+          adminAddr = "webmaster@example.org";
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
       description = ''
         Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
         See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
@@ -184,7 +180,7 @@ in
       config = {
         tempdir = "${stateDir}/tmp";
         uploaddir = "${stateDir}/upload";
-        force_ssl = mkIf cfg.virtualHost.enableSSL "on";
+        force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
         config.defaultlang = "en";
       };
     };
@@ -215,38 +211,36 @@ in
       enable = true;
       adminAddr = mkDefault cfg.virtualHost.adminAddr;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts = [ (mkMerge [
-        cfg.virtualHost {
-          documentRoot = mkForce "${pkg}/share/limesurvey";
-          extraConfig = ''
-            Alias "/tmp" "${stateDir}/tmp"
-            <Directory "${stateDir}">
-              AllowOverride all
-              Require all granted
-              Options -Indexes +FollowSymlinks
-            </Directory>
-
-            Alias "/upload" "${stateDir}/upload"
-            <Directory "${stateDir}/upload">
-              AllowOverride all
-              Require all granted
-              Options -Indexes
-            </Directory>
-
-            <Directory "${pkg}/share/limesurvey">
-              <FilesMatch "\.php$">
-                <If "-f %{REQUEST_FILENAME}">
-                  SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
-                </If>
-              </FilesMatch>
-
-              AllowOverride all
-              Options -Indexes
-              DirectoryIndex index.php
-            </Directory>
-          '';
-        }
-      ]) ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${pkg}/share/limesurvey";
+        extraConfig = ''
+          Alias "/tmp" "${stateDir}/tmp"
+          <Directory "${stateDir}">
+            AllowOverride all
+            Require all granted
+            Options -Indexes +FollowSymlinks
+          </Directory>
+
+          Alias "/upload" "${stateDir}/upload"
+          <Directory "${stateDir}/upload">
+            AllowOverride all
+            Require all granted
+            Options -Indexes
+          </Directory>
+
+          <Directory "${pkg}/share/limesurvey">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+
+            AllowOverride all
+            Options -Indexes
+            DirectoryIndex index.php
+          </Directory>
+        '';
+      } ];
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/web-apps/matomo-doc.xml b/nixos/modules/services/web-apps/matomo-doc.xml
index 79cece551d3..69d1170e452 100644
--- a/nixos/modules/services/web-apps/matomo-doc.xml
+++ b/nixos/modules/services/web-apps/matomo-doc.xml
@@ -86,12 +86,6 @@ GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
   <itemizedlist>
    <listitem>
     <para>
-     Matomo's file integrity check will warn you. This is due to the patches
-     necessary for NixOS, you can safely ignore this.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
      Matomo will warn you that the JavaScript tracker is not writable. This is
      because it's located in the read-only nix store. You can safely ignore
      this, unless you need a plugin that needs JavaScript tracker access.
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 352cc4c647b..75da474dc44 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -18,6 +18,14 @@ let
      in join config.networking.hostName config.networking.domain;
 
 in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "piwik" "enable" ] [ "services" "matomo" "enable" ])
+    (mkRenamedOptionModule [ "services" "piwik" "webServerUser" ] [ "services" "matomo" "webServerUser" ])
+    (mkRemovedOptionModule [ "services" "piwik" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
+    (mkRemovedOptionModule [ "services" "matomo" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
+    (mkRenamedOptionModule [ "services" "piwik" "nginx" ] [ "services" "matomo" "nginx" ])
+  ];
+
   options = {
     services.matomo = {
       # NixOS PR for database setup: https://github.com/NixOS/nixpkgs/pull/6963
diff --git a/nixos/modules/services/web-apps/mattermost.nix b/nixos/modules/services/web-apps/mattermost.nix
index 8c7fc4056ad..853347bf86e 100644
--- a/nixos/modules/services/web-apps/mattermost.nix
+++ b/nixos/modules/services/web-apps/mattermost.nix
@@ -6,14 +6,18 @@ let
 
   cfg = config.services.mattermost;
 
-  defaultConfig = builtins.fromJSON (readFile "${pkgs.mattermost}/config/config.json");
+  defaultConfig = builtins.fromJSON (builtins.replaceStrings [ "\\u0026" ] [ "&" ]
+    (readFile "${pkgs.mattermost}/config/config.json")
+  );
+
+  database = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10";
 
   mattermostConf = foldl recursiveUpdate defaultConfig
     [ { ServiceSettings.SiteURL = cfg.siteUrl;
         ServiceSettings.ListenAddress = cfg.listenAddress;
         TeamSettings.SiteName = cfg.siteName;
         SqlSettings.DriverName = "postgres";
-        SqlSettings.DataSource = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10";
+        SqlSettings.DataSource = database;
       }
       cfg.extraConfig
     ];
@@ -146,17 +150,17 @@ in
 
   config = mkMerge [
     (mkIf cfg.enable {
-      users.users = optionalAttrs (cfg.user == "mattermost") (singleton {
-        name = "mattermost";
-        group = cfg.group;
-        uid = config.ids.uids.mattermost;
-        home = cfg.statePath;
-      });
-
-      users.groups = optionalAttrs (cfg.group == "mattermost") (singleton {
-        name = "mattermost";
-        gid = config.ids.gids.mattermost;
-      });
+      users.users = optionalAttrs (cfg.user == "mattermost") {
+        mattermost = {
+          group = cfg.group;
+          uid = config.ids.uids.mattermost;
+          home = cfg.statePath;
+        };
+      };
+
+      users.groups = optionalAttrs (cfg.group == "mattermost") {
+        mattermost.gid = config.ids.gids.mattermost;
+      };
 
       services.postgresql.enable = cfg.localDatabaseCreate;
 
@@ -175,7 +179,9 @@ in
           mkdir -p ${cfg.statePath}/{data,config,logs}
           ln -sf ${pkgs.mattermost}/{bin,fonts,i18n,templates,client} ${cfg.statePath}
         '' + lib.optionalString (!cfg.mutableConfig) ''
-          ln -sf ${mattermostConfJSON} ${cfg.statePath}/config/config.json
+          rm -f ${cfg.statePath}/config/config.json
+          cp ${mattermostConfJSON} ${cfg.statePath}/config/config.json
+          ${pkgs.mattermost}/bin/mattermost config migrate ${cfg.statePath}/config/config.json ${database}
         '' + lib.optionalString cfg.mutableConfig ''
           if ! test -e "${cfg.statePath}/config/.initial-created"; then
             rm -f ${cfg.statePath}/config/config.json
@@ -201,7 +207,8 @@ in
           PermissionsStartOnly = true;
           User = cfg.user;
           Group = cfg.group;
-          ExecStart = "${pkgs.mattermost}/bin/mattermost";
+          ExecStart = "${pkgs.mattermost}/bin/mattermost" +
+            (lib.optionalString (!cfg.mutableConfig) " -c ${database}");
           WorkingDirectory = "${cfg.statePath}";
           Restart = "always";
           RestartSec = "10";
@@ -227,4 +234,3 @@ in
     })
   ];
 }
-
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
index 43edc04e1a4..e9ed53857d8 100644
--- a/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -64,7 +64,7 @@ let
       $wgScriptPath = "";
 
       ## The protocol and server name to use in fully-qualified URLs
-      $wgServer = "${if cfg.virtualHost.enableSSL then "https" else "http"}://${cfg.virtualHost.hostName}";
+      $wgServer = "${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}";
 
       ## The URL path to static resources (images, scripts, etc.)
       $wgResourceBasePath = $wgScriptPath;
@@ -290,19 +290,13 @@ in
       };
 
       virtualHost = mkOption {
-        type = types.submodule ({
-          options = import ../web-servers/apache-httpd/per-server-options.nix {
-            inherit lib;
-            forMainServer = false;
-          };
-        });
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
         example = literalExample ''
           {
             hostName = "mediawiki.example.org";
-            enableSSL = true;
             adminAddr = "webmaster@example.org";
-            sslServerCert = "/var/lib/acme/mediawiki.example.org/full.pem";
-            sslServerKey = "/var/lib/acme/mediawiki.example.org/key.pem";
+            forceSSL = true;
+            enableACME = true;
           }
         '';
         description = ''
@@ -389,31 +383,28 @@ in
 
     services.httpd = {
       enable = true;
-      adminAddr = mkDefault cfg.virtualHost.adminAddr;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts = [ (mkMerge [
-        cfg.virtualHost {
-          documentRoot = mkForce "${pkg}/share/mediawiki";
-          extraConfig = ''
-            <Directory "${pkg}/share/mediawiki">
-              <FilesMatch "\.php$">
-                <If "-f %{REQUEST_FILENAME}">
-                  SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
-                </If>
-              </FilesMatch>
-
-              Require all granted
-              DirectoryIndex index.php
-              AllowOverride All
-            </Directory>
-          '' + optionalString (cfg.uploadsDir != null) ''
-            Alias "/images" "${cfg.uploadsDir}"
-            <Directory "${cfg.uploadsDir}">
-              Require all granted
-            </Directory>
-          '';
-        }
-      ]) ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${pkg}/share/mediawiki";
+        extraConfig = ''
+          <Directory "${pkg}/share/mediawiki">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+
+            Require all granted
+            DirectoryIndex index.php
+            AllowOverride All
+          </Directory>
+        '' + optionalString (cfg.uploadsDir != null) ''
+          Alias "/images" "${cfg.uploadsDir}"
+          <Directory "${cfg.uploadsDir}">
+            Require all granted
+          </Directory>
+        '';
+      } ];
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/web-apps/moinmoin.nix b/nixos/modules/services/web-apps/moinmoin.nix
index 0fee64be0bb..dc7abce2a5c 100644
--- a/nixos/modules/services/web-apps/moinmoin.nix
+++ b/nixos/modules/services/web-apps/moinmoin.nix
@@ -299,5 +299,5 @@ in
     ])));
   };
 
-  meta.maintainers = with lib.maintainers; [ b42 ];
+  meta.maintainers = with lib.maintainers; [ mmilata ];
 }
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index ac59f9e0012..1196780cf6e 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -32,7 +32,7 @@ let
     'dbcollation' => 'utf8mb4_unicode_ci',
   );
 
-  $CFG->wwwroot   = '${if cfg.virtualHost.enableSSL then "https" else "http"}://${cfg.virtualHost.hostName}';
+  $CFG->wwwroot   = '${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}';
   $CFG->dataroot  = '${stateDir}';
   $CFG->admin     = 'admin';
 
@@ -140,19 +140,15 @@ in
     };
 
     virtualHost = mkOption {
-      type = types.submodule ({
-        options = import ../web-servers/apache-httpd/per-server-options.nix {
-          inherit lib;
-          forMainServer = false;
-        };
-      });
-      example = {
-        hostName = "moodle.example.org";
-        enableSSL = true;
-        adminAddr = "webmaster@example.org";
-        sslServerCert = "/var/lib/acme/moodle.example.org/full.pem";
-        sslServerKey = "/var/lib/acme/moodle.example.org/key.pem";
-      };
+      type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+      example = literalExample ''
+        {
+          hostName = "moodle.example.org";
+          adminAddr = "webmaster@example.org";
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
       description = ''
         Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
         See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
@@ -241,22 +237,20 @@ in
       enable = true;
       adminAddr = mkDefault cfg.virtualHost.adminAddr;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts = [ (mkMerge [
-        cfg.virtualHost {
-          documentRoot = mkForce "${cfg.package}/share/moodle";
-          extraConfig = ''
-            <Directory "${cfg.package}/share/moodle">
-              <FilesMatch "\.php$">
-                <If "-f %{REQUEST_FILENAME}">
-                  SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
-                </If>
-              </FilesMatch>
-              Options -Indexes
-              DirectoryIndex index.php
-            </Directory>
-          '';
-        }
-      ]) ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${cfg.package}/share/moodle";
+        extraConfig = ''
+          <Directory "${cfg.package}/share/moodle">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+            Options -Indexes
+            DirectoryIndex index.php
+          </Directory>
+        '';
+      } ];
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index b67f0880878..5f6f2bc7a16 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -6,38 +6,44 @@ let
   cfg = config.services.nextcloud;
   fpm = config.services.phpfpm.pools.nextcloud;
 
-  phpPackage = pkgs.php73;
-  phpPackages = pkgs.php73Packages;
+  phpPackage =
+    let
+      base = pkgs.php74;
+    in
+      base.buildEnv {
+        extensions = e: with e;
+          base.enabledExtensions ++ [
+            apcu redis memcached imagick
+          ];
+        extraConfig = phpOptionsStr;
+      };
 
   toKeyValue = generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {} " = ";
   };
 
-  phpOptionsExtensions = ''
-    ${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
-  '';
   phpOptions = {
     upload_max_filesize = cfg.maxUploadSize;
     post_max_size = cfg.maxUploadSize;
     memory_limit = cfg.maxUploadSize;
   } // cfg.phpOptions;
-  phpOptionsStr = phpOptionsExtensions + (toKeyValue phpOptions);
+  phpOptionsStr = toKeyValue phpOptions;
 
   occ = pkgs.writeScriptBin "nextcloud-occ" ''
-    #! ${pkgs.stdenv.shell}
-    cd ${pkgs.nextcloud}
-    exec /run/wrappers/bin/sudo -u nextcloud \
-      NEXTCLOUD_CONFIG_DIR="${cfg.home}/config" \
+    #! ${pkgs.runtimeShell}
+    cd ${cfg.package}
+    sudo=exec
+    if [[ "$USER" != nextcloud ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR'
+    fi
+    export NEXTCLOUD_CONFIG_DIR="${cfg.home}/config"
+    $sudo \
       ${phpPackage}/bin/php \
-      -c ${pkgs.writeText "php.ini" phpOptionsStr}\
       occ $*
   '';
 
+  inherit (config.system) stateVersion;
+
 in {
   options.services.nextcloud = {
     enable = mkEnableOption "nextcloud";
@@ -58,7 +64,12 @@ in {
     https = mkOption {
       type = types.bool;
       default = false;
-      description = "Enable if there is a TLS terminating proxy in front of nextcloud.";
+      description = "Use https for generated links.";
+    };
+    package = mkOption {
+      type = types.package;
+      description = "Which package to use for the Nextcloud instance.";
+      relatedPackages = [ "nextcloud17" "nextcloud18" ];
     };
 
     maxUploadSize = mkOption {
@@ -225,6 +236,15 @@ in {
         '';
       };
 
+      trustedProxies = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Trusted proxies, to provide if the nextcloud installation is being
+          proxied to secure against e.g. spoofing.
+        '';
+      };
+
       overwriteProtocol = mkOption {
         type = types.nullOr (types.enum [ "http" "https" ]);
         default = null;
@@ -296,10 +316,40 @@ in {
         }
       ];
 
-      warnings = optional (cfg.poolConfig != null) ''
-        Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
-        Please migrate your configuration to config.services.nextcloud.poolSettings.
-      '';
+      warnings = []
+        ++ (optional (cfg.poolConfig != null) ''
+          Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
+          Please migrate your configuration to config.services.nextcloud.poolSettings.
+        '')
+        ++ (optional (versionOlder cfg.package.version "18") ''
+          A legacy Nextcloud install (from before NixOS 20.03) may be installed.
+
+          You're currently deploying an older version of Nextcloud. This may be needed
+          since Nextcloud doesn't allow major version upgrades that skip multiple
+          versions (i.e. an upgrade from 16 is possible to 17, but not 16 to 18).
+
+          It is assumed that Nextcloud will be upgraded from version 16 to 17.
+
+           * If this is a fresh install, there will be no upgrade to do now.
+
+           * If this server already had Nextcloud installed, first deploy this to your
+             server, and wait until the upgrade to 17 is finished.
+
+          Then, set `services.nextcloud.package` to `pkgs.nextcloud18` to upgrade to
+          Nextcloud version 18.
+        '');
+
+      services.nextcloud.package = with pkgs;
+        mkDefault (
+          if pkgs ? nextcloud
+            then throw ''
+              The `pkgs.nextcloud`-attribute has been removed. If it's supposed to be the default
+              nextcloud defined in an overlay, please set `services.nextcloud.package` to
+              `pkgs.nextcloud`.
+            ''
+          else if versionOlder stateVersion "20.03" then nextcloud17
+          else nextcloud18
+        );
     }
 
     { systemd.timers.nextcloud-cron = {
@@ -348,6 +398,7 @@ in {
               ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_pwd(),"}
               'dbtype' => '${c.dbtype}',
               'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
+              'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
             ];
           '';
           occInstallCmd = let
@@ -393,7 +444,7 @@ in {
           path = [ occ ];
           script = ''
             chmod og+x ${cfg.home}
-            ln -sf ${pkgs.nextcloud}/apps ${cfg.home}/
+            ln -sf ${cfg.package}/apps ${cfg.home}/
             mkdir -p ${cfg.home}/config ${cfg.home}/data ${cfg.home}/store-apps
             ln -sf ${overrideConfig} ${cfg.home}/config/override.config.php
 
@@ -415,11 +466,12 @@ in {
           environment.NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config";
           serviceConfig.Type = "oneshot";
           serviceConfig.User = "nextcloud";
-          serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${pkgs.nextcloud}/cron.php";
+          serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${cfg.package}/cron.php";
         };
         nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
           serviceConfig.Type = "oneshot";
           serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
+          serviceConfig.User = "nextcloud";
           startAt = cfg.autoUpdateApps.startAt;
         };
       };
@@ -428,7 +480,7 @@ in {
         pools.nextcloud = {
           user = "nextcloud";
           group = "nginx";
-          phpOptions = phpOptionsExtensions + phpOptionsStr;
+          phpOptions = phpOptionsStr;
           phpPackage = phpPackage;
           phpEnv = {
             NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config";
@@ -456,7 +508,7 @@ in {
         enable = true;
         virtualHosts = {
           ${cfg.hostName} = {
-            root = pkgs.nextcloud;
+            root = cfg.package;
             locations = {
               "= /robots.txt" = {
                 priority = 100;
@@ -518,10 +570,11 @@ in {
                 add_header X-Robots-Tag none;
                 add_header X-Download-Options noopen;
                 add_header X-Permitted-Cross-Domain-Policies none;
+                add_header X-Frame-Options sameorigin;
                 add_header Referrer-Policy no-referrer;
                 access_log off;
               '';
-              "~ \\.(?:png|html|ttf|ico|jpg|jpeg)$".extraConfig = ''
+              "~ \\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$".extraConfig = ''
                 try_files $uri /index.php$request_uri;
                 access_log off;
               '';
@@ -532,6 +585,7 @@ in {
               add_header X-Robots-Tag none;
               add_header X-Download-Options noopen;
               add_header X-Permitted-Cross-Domain-Policies none;
+              add_header X-Frame-Options sameorigin;
               add_header Referrer-Policy no-referrer;
               add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
               error_page 403 /core/templates/403.php;
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index d66e0f0c299..fc454f8ba25 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -113,5 +113,53 @@
    maintenance:install</literal>! This command tries to install the application
    and can cause unwanted side-effects!</para>
   </warning>
+
+  <para>
+   Nextcloud doesn't allow to move more than one major-version forward. If you're e.g. on
+   <literal>v16</literal>, you cannot upgrade to <literal>v18</literal>, you need to upgrade to
+   <literal>v17</literal> first. This is ensured automatically as long as the
+   <link linkend="opt-system.stateVersion">stateVersion</link> is declared properly. In that case
+   the oldest version available (one major behind the one from the previous NixOS
+   release) will be selected by default and the module will generate a warning that reminds
+   the user to upgrade to latest Nextcloud <emphasis>after</emphasis> that deploy.
+  </para>
+ </section>
+
+ <section xml:id="module-services-nextcloud-maintainer-info">
+  <title>Maintainer information</title>
+
+  <para>
+   As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
+   since it cannot move more than one major version forward on a single upgrade. This chapter
+   adds some notes how Nextcloud updates should be rolled out in the future.
+  </para>
+
+  <para>
+   While minor and patch-level updates are no problem and can be done directly in the
+   package-expression (and should be backported to supported stable branches after that),
+   major-releases should be added in a new attribute (e.g. Nextcloud <literal>v19.0.0</literal>
+   should be available in <literal>nixpkgs</literal> as <literal>pkgs.nextcloud19</literal>).
+   To provide simple upgrade paths it's generally useful to backport those as well to stable
+   branches. As long as the package-default isn't altered, this won't break existing setups.
+   After that, the versioning-warning in the <literal>nextcloud</literal>-module should be
+   updated to make sure that the
+   <link linkend="opt-services.nextcloud.package">package</link>-option selects the latest version
+   on fresh setups.
+  </para>
+
+  <para>
+   If major-releases will be abandoned by upstream, we should check first if those are needed
+   in NixOS for a safe upgrade-path before removing those. In that case we shold keep those
+   packages, but mark them as insecure in an expression like this (in
+   <literal>&lt;nixpkgs/pkgs/servers/nextcloud/default.nix&gt;</literal>):
+<programlisting>/* ... */
+{
+  nextcloud17 = generic {
+    version = "17.0.x";
+    sha256 = "0000000000000000000000000000000000000000000000000000";
+    insecure = true;
+  };
+}</programlisting>
+  </para>
  </section>
 </chapter>
diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix
index 2c2f36ac598..9d0a3f65253 100644
--- a/nixos/modules/services/web-apps/restya-board.nix
+++ b/nixos/modules/services/web-apps/restya-board.nix
@@ -116,7 +116,7 @@ in
         };
 
         passwordFile = mkOption {
-          type = types.nullOr types.str;
+          type = types.nullOr types.path;
           default = null;
           description = ''
             The database user's password. 'null' if no password is set.
@@ -285,7 +285,7 @@ in
           sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php"
         '' else ''
           sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php"
-          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', '$(<${cfg.database.dbPassFile})');/g" "${runDir}/server/php/config.inc.php"
+          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'file_get_contents(${cfg.database.passwordFile})'"});/g" "${runDir}/server/php/config.inc.php
         ''}
         sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php"
         sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php"
diff --git a/nixos/modules/services/web-apps/trilium.nix b/nixos/modules/services/web-apps/trilium.nix
new file mode 100644
index 00000000000..6f47193c62b
--- /dev/null
+++ b/nixos/modules/services/web-apps/trilium.nix
@@ -0,0 +1,137 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.trilium-server;
+  configIni = pkgs.writeText "trilium-config.ini" ''
+    [General]
+    # Instance name can be used to distinguish between different instances
+    instanceName=${cfg.instanceName}
+
+    # Disable automatically generating desktop icon
+    noDesktopIcon=true
+
+    [Network]
+    # host setting is relevant only for web deployments - set the host on which the server will listen
+    host=${cfg.host}
+    # port setting is relevant only for web deployments, desktop builds run on random free port
+    port=${toString cfg.port}
+    # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
+    https=false
+  '';
+in
+{
+
+  options.services.trilium-server = with lib; {
+    enable = mkEnableOption "trilium-server";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/trilium";
+      description = ''
+        The directory storing the nodes database and the configuration.
+      '';
+    };
+
+    instanceName = mkOption {
+      type = types.str;
+      default = "Trilium";
+      description = ''
+        Instance name used to distinguish between different instances
+      '';
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        The host address to bind to (defaults to localhost).
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 8080;
+      description = ''
+        The port number to bind to.
+      '';
+    };
+
+    nginx = mkOption {
+      default = {};
+      description = ''
+        Configuration for nginx reverse proxy.
+      '';
+
+      type = types.submodule {
+        options = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Configure the nginx reverse proxy settings.
+            '';
+          };
+
+          hostName = mkOption {
+            type = types.str;
+            description = ''
+              The hostname use to setup the virtualhost configuration
+            '';
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable (lib.mkMerge [ 
+  {
+    meta.maintainers = with lib.maintainers; [ kampka ];
+
+    users.groups.trilium = {};
+    users.users.trilium = {
+      description = "Trilium User";
+      group = "trilium";
+      home = cfg.dataDir;
+      isSystemUser = true;
+    };
+
+    systemd.services.trilium-server = {
+      wantedBy = [ "multi-user.target" ];
+      environment.TRILIUM_DATA_DIR = cfg.dataDir;
+      serviceConfig = {
+        ExecStart = "${pkgs.trilium-server}/bin/trilium-server";
+        User = "trilium";
+        Group = "trilium";
+        PrivateTmp = "true";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d  ${cfg.dataDir}            0750 trilium trilium - -"
+      "L+ ${cfg.dataDir}/config.ini -    -       -       - ${configIni}"
+    ];
+
+  }
+
+  (lib.mkIf cfg.nginx.enable {
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.nginx.hostName}" = {
+        locations."/" = {
+          proxyPass = "http://${cfg.host}:${toString cfg.port}/";
+          extraConfig = ''
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection 'upgrade';
+            proxy_set_header Host $host;
+            proxy_cache_bypass $http_upgrade;
+          '';
+        };
+        extraConfig = ''
+          client_max_body_size 0;
+        '';
+      };
+    };
+  })
+  ]);
+}
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index f1370c2854b..5fbe53221ae 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -3,7 +3,7 @@
 let
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
   inherit (lib) any attrValues concatMapStringsSep flatten literalExample;
-  inherit (lib) mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
+  inherit (lib) mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
 
   eachSite = config.services.wordpress;
   user = "wordpress";
@@ -105,7 +105,7 @@ let
               name = "embed-pdf-viewer-plugin";
               # Download the theme from the wordpress site
               src = pkgs.fetchurl {
-                url = https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip;
+                url = "https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip";
                 sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
               };
               # We need unzip to build this package
@@ -127,12 +127,12 @@ let
             <note><para>These themes need to be packaged before use, see example.</para></note>
           '';
           example = ''
-            # For shits and giggles, let's package the responsive theme
+            # Let's package the responsive theme
             responsiveTheme = pkgs.stdenv.mkDerivation {
               name = "responsive-theme";
               # Download the theme from the wordpress site
               src = pkgs.fetchurl {
-                url = https://downloads.wordpress.org/theme/responsive.3.14.zip;
+                url = "https://downloads.wordpress.org/theme/responsive.3.14.zip";
                 sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
               };
               # We need unzip to build this package
@@ -209,18 +209,12 @@ let
         };
 
         virtualHost = mkOption {
-          type = types.submodule ({
-            options = import ../web-servers/apache-httpd/per-server-options.nix {
-              inherit lib;
-              forMainServer = false;
-            };
-          });
+          type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
           example = literalExample ''
             {
-              enableSSL = true;
               adminAddr = "webmaster@example.org";
-              sslServerCert = "/var/lib/acme/wordpress.example.org/full.pem";
-              sslServerKey = "/var/lib/acme/wordpress.example.org/key.pem";
+              forceSSL = true;
+              enableACME = true;
             }
           '';
           description = ''
@@ -304,41 +298,37 @@ in
     services.httpd = {
       enable = true;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts = mapAttrsToList (hostName: cfg:
-        (mkMerge [
-          cfg.virtualHost {
-            documentRoot = mkForce "${pkg hostName cfg}/share/wordpress";
-            extraConfig = ''
-              <Directory "${pkg hostName cfg}/share/wordpress">
-                <FilesMatch "\.php$">
-                  <If "-f %{REQUEST_FILENAME}">
-                    SetHandler "proxy:unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}|fcgi://localhost/"
-                  </If>
-                </FilesMatch>
-
-                # standard wordpress .htaccess contents
-                <IfModule mod_rewrite.c>
-                  RewriteEngine On
-                  RewriteBase /
-                  RewriteRule ^index\.php$ - [L]
-                  RewriteCond %{REQUEST_FILENAME} !-f
-                  RewriteCond %{REQUEST_FILENAME} !-d
-                  RewriteRule . /index.php [L]
-                </IfModule>
-
-                DirectoryIndex index.php
-                Require all granted
-                Options +FollowSymLinks
-              </Directory>
-
-              # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php
-              <Files wp-config.php>
-                Require all denied
-              </Files>
-            '';
-          }
-        ])
-      ) eachSite;
+      virtualHosts = mapAttrs (hostName: cfg: mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${pkg hostName cfg}/share/wordpress";
+        extraConfig = ''
+          <Directory "${pkg hostName cfg}/share/wordpress">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+
+            # standard wordpress .htaccess contents
+            <IfModule mod_rewrite.c>
+              RewriteEngine On
+              RewriteBase /
+              RewriteRule ^index\.php$ - [L]
+              RewriteCond %{REQUEST_FILENAME} !-f
+              RewriteCond %{REQUEST_FILENAME} !-d
+              RewriteRule . /index.php [L]
+            </IfModule>
+
+            DirectoryIndex index.php
+            Require all granted
+            Options +FollowSymLinks
+          </Directory>
+
+          # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php
+          <Files wp-config.php>
+            Require all denied
+          </Files>
+        '';
+      } ]) eachSite;
     };
 
     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
diff --git a/nixos/modules/services/web-apps/youtrack.nix b/nixos/modules/services/web-apps/youtrack.nix
index 830edac20ba..b4d653d2d77 100644
--- a/nixos/modules/services/web-apps/youtrack.nix
+++ b/nixos/modules/services/web-apps/youtrack.nix
@@ -46,9 +46,11 @@ in
         https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Java-Start-Parameters.html
         for more information.
       '';
-      example = {
-        "jetbrains.youtrack.overrideRootPassword" = "tortuga";
-      };
+      example = literalExample ''
+        {
+          "jetbrains.youtrack.overrideRootPassword" = "tortuga";
+        }
+      '';
       type = types.attrsOf types.str;
     };
 
diff --git a/nixos/modules/services/web-apps/zabbix.nix b/nixos/modules/services/web-apps/zabbix.nix
index 09538726b7c..00719512834 100644
--- a/nixos/modules/services/web-apps/zabbix.nix
+++ b/nixos/modules/services/web-apps/zabbix.nix
@@ -113,19 +113,15 @@ in
       };
 
       virtualHost = mkOption {
-        type = types.submodule ({
-          options = import ../web-servers/apache-httpd/per-server-options.nix {
-            inherit lib;
-            forMainServer = false;
-          };
-        });
-        example = {
-          hostName = "zabbix.example.org";
-          enableSSL = true;
-          adminAddr = "webmaster@example.org";
-          sslServerCert = "/var/lib/acme/zabbix.example.org/full.pem";
-          sslServerKey = "/var/lib/acme/zabbix.example.org/key.pem";
-        };
+        type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
+        example = literalExample ''
+          {
+            hostName = "zabbix.example.org";
+            adminAddr = "webmaster@example.org";
+            forceSSL = true;
+            enableACME = true;
+          }
+        '';
         description = ''
           Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
           See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
@@ -190,23 +186,21 @@ in
       enable = true;
       adminAddr = mkDefault cfg.virtualHost.adminAddr;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts = [ (mkMerge [
-        cfg.virtualHost {
-          documentRoot = mkForce "${cfg.package}/share/zabbix";
-          extraConfig = ''
-            <Directory "${cfg.package}/share/zabbix">
-              <FilesMatch "\.php$">
-                <If "-f %{REQUEST_FILENAME}">
-                  SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
-                </If>
-              </FilesMatch>
-              AllowOverride all
-              Options -Indexes
-              DirectoryIndex index.php
-            </Directory>
-          '';
-        }
-      ]) ];
+      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
+        documentRoot = mkForce "${cfg.package}/share/zabbix";
+        extraConfig = ''
+          <Directory "${cfg.package}/share/zabbix">
+            <FilesMatch "\.php$">
+              <If "-f %{REQUEST_FILENAME}">
+                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+              </If>
+            </FilesMatch>
+            AllowOverride all
+            Options -Indexes
+            DirectoryIndex index.php
+          </Directory>
+        '';
+      } ];
     };
 
     users.users.${user} = mapAttrs (name: mkDefault) {
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index f5a6051b4b5..5e55baa203a 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -4,36 +4,34 @@ with lib;
 
 let
 
-  mainCfg = config.services.httpd;
+  cfg = config.services.httpd;
 
   runtimeDir = "/run/httpd";
 
-  httpd = mainCfg.package.out;
+  pkg = cfg.package.out;
 
-  httpdConf = mainCfg.configFile;
+  httpdConf = cfg.configFile;
 
-  php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ };
+  php = cfg.phpPackage.override { apacheHttpd = pkg; };
 
   phpMajorVersion = lib.versions.major (lib.getVersion php);
 
-  mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; };
+  mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
 
-  defaultListen = cfg: if cfg.enableSSL
-    then [{ip = "*"; port = 443;}]
-    else [{ip = "*"; port = 80;}];
+  vhosts = attrValues cfg.virtualHosts;
 
-  getListen = cfg:
-    if cfg.listen == []
-      then defaultListen cfg
-      else cfg.listen;
+  mkListenInfo = hostOpts:
+    if hostOpts.listen != [] then hostOpts.listen
+    else (
+      optional (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) { ip = "*"; port = 443; ssl = true; } ++
+      optional (!hostOpts.onlySSL) { ip = "*"; port = 80; ssl = false; }
+    );
 
-  listenToString = l: "${l.ip}:${toString l.port}";
+  listenInfo = unique (concatMap mkListenInfo vhosts);
 
-  allHosts = [mainCfg] ++ mainCfg.virtualHosts;
-
-  enableSSL = any (vhost: vhost.enableSSL) allHosts;
-
-  enableUserDir = any (vhost: vhost.enableUserDir) allHosts;
+  enableHttp2 = any (vhost: vhost.http2) vhosts;
+  enableSSL = any (listen: listen.ssl) listenInfo;
+  enableUserDir = any (vhost: vhost.enableUserDir) vhosts;
 
   # NOTE: generally speaking order of modules is very important
   modules =
@@ -43,23 +41,19 @@ let
       "mime" "autoindex" "negotiation" "dir"
       "alias" "rewrite"
       "unixd" "slotmem_shm" "socache_shmcb"
-      "mpm_${mainCfg.multiProcessingModule}"
+      "mpm_${cfg.multiProcessingModule}"
     ]
-    ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
+    ++ (if cfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
+    ++ optional enableHttp2 "http2"
     ++ optional enableSSL "ssl"
     ++ optional enableUserDir "userdir"
-    ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
-    ++ optional mainCfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
-    ++ optional mainCfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
-    ++ mainCfg.extraModules;
-
+    ++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
+    ++ optional cfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
+    ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
+    ++ cfg.extraModules;
 
-  allDenied = "Require all denied";
-  allGranted = "Require all granted";
-
-
-  loggingConf = (if mainCfg.logFormat != "none" then ''
-    ErrorLog ${mainCfg.logDir}/error.log
+  loggingConf = (if cfg.logFormat != "none" then ''
+    ErrorLog ${cfg.logDir}/error.log
 
     LogLevel notice
 
@@ -68,7 +62,7 @@ let
     LogFormat "%{Referer}i -> %U" referer
     LogFormat "%{User-agent}i" agent
 
-    CustomLog ${mainCfg.logDir}/access.log ${mainCfg.logFormat}
+    CustomLog ${cfg.logDir}/access.log ${cfg.logFormat}
   '' else ''
     ErrorLog /dev/null
   '');
@@ -90,174 +84,208 @@ let
 
 
   sslConf = ''
-    SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
+    <IfModule mod_ssl.c>
+        SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
 
-    Mutex posixsem
+        Mutex posixsem
 
-    SSLRandomSeed startup builtin
-    SSLRandomSeed connect builtin
+        SSLRandomSeed startup builtin
+        SSLRandomSeed connect builtin
 
-    SSLProtocol ${mainCfg.sslProtocols}
-    SSLCipherSuite ${mainCfg.sslCiphers}
-    SSLHonorCipherOrder on
+        SSLProtocol ${cfg.sslProtocols}
+        SSLCipherSuite ${cfg.sslCiphers}
+        SSLHonorCipherOrder on
+    </IfModule>
   '';
 
 
   mimeConf = ''
-    TypesConfig ${httpd}/conf/mime.types
+    TypesConfig ${pkg}/conf/mime.types
 
     AddType application/x-x509-ca-cert .crt
     AddType application/x-pkcs7-crl    .crl
     AddType application/x-httpd-php    .php .phtml
 
     <IfModule mod_mime_magic.c>
-        MIMEMagicFile ${httpd}/conf/magic
+        MIMEMagicFile ${pkg}/conf/magic
     </IfModule>
   '';
 
-
-  perServerConf = isMainServer: cfg: let
-
-    # Canonical name must not include a trailing slash.
-    canonicalNames =
-      let defaultPort = (head (defaultListen cfg)).port; in
-      map (port:
-        (if cfg.enableSSL then "https" else "http") + "://" +
-        cfg.hostName +
-        (if port != defaultPort then ":${toString port}" else "")
-        ) (map (x: x.port) (getListen cfg));
-
-    maybeDocumentRoot = fold (svc: acc:
-      if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc
-    ) null ([ cfg ]);
-
-    documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else
-      pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out";
-
-    documentRootConf = ''
-      DocumentRoot "${documentRoot}"
-
-      <Directory "${documentRoot}">
-          Options Indexes FollowSymLinks
-          AllowOverride None
-          ${allGranted}
-      </Directory>
-    '';
-
-    # If this is a vhost, the include the entries for the main server as well.
-    robotsTxt = concatStringsSep "\n" (filter (x: x != "") ([ cfg.robotsEntries ] ++ lib.optional (!isMainServer) mainCfg.robotsEntries));
-
-  in ''
-    ${concatStringsSep "\n" (map (n: "ServerName ${n}") canonicalNames)}
-
-    ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases}
-
-    ${if cfg.sslServerCert != null then ''
-      SSLCertificateFile ${cfg.sslServerCert}
-      SSLCertificateKeyFile ${cfg.sslServerKey}
-      ${if cfg.sslServerChain != null then ''
-        SSLCertificateChainFile ${cfg.sslServerChain}
-      '' else ""}
-    '' else ""}
-
-    ${if cfg.enableSSL then ''
-      SSLEngine on
-    '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */
-    ''
-      SSLEngine off
-    '' else ""}
-
-    ${if isMainServer || cfg.adminAddr != null then ''
-      ServerAdmin ${cfg.adminAddr}
-    '' else ""}
-
-    ${if !isMainServer && mainCfg.logPerVirtualHost then ''
-      ErrorLog ${mainCfg.logDir}/error-${cfg.hostName}.log
-      CustomLog ${mainCfg.logDir}/access-${cfg.hostName}.log ${cfg.logFormat}
-    '' else ""}
-
-    ${optionalString (robotsTxt != "") ''
-      Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt}
-    ''}
-
-    ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""}
-
-    ${if cfg.enableUserDir then ''
-
-      UserDir public_html
-      UserDir disabled root
-
-      <Directory "/home/*/public_html">
-          AllowOverride FileInfo AuthConfig Limit Indexes
-          Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
-          <Limit GET POST OPTIONS>
-              ${allGranted}
-          </Limit>
-          <LimitExcept GET POST OPTIONS>
-              ${allDenied}
-          </LimitExcept>
-      </Directory>
-
-    '' else ""}
-
-    ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then ''
-      RedirectPermanent / ${cfg.globalRedirect}
-    '' else ""}
-
-    ${
-      let makeFileConf = elem: ''
-            Alias ${elem.urlPath} ${elem.file}
-          '';
-      in concatMapStrings makeFileConf cfg.servedFiles
-    }
-
-    ${
-      let makeDirConf = elem: ''
-            Alias ${elem.urlPath} ${elem.dir}/
-            <Directory ${elem.dir}>
-                Options +Indexes
-                ${allGranted}
-                AllowOverride All
-            </Directory>
-          '';
-      in concatMapStrings makeDirConf cfg.servedDirs
-    }
-
-    ${cfg.extraConfig}
-  '';
+  mkVHostConf = hostOpts:
+    let
+      adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
+      listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts);
+      listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts);
+
+      useACME = hostOpts.enableACME || hostOpts.useACMEHost != null;
+      sslCertDir =
+        if hostOpts.enableACME then config.security.acme.certs.${hostOpts.hostName}.directory
+        else if hostOpts.useACMEHost != null then config.security.acme.certs.${hostOpts.useACMEHost}.directory
+        else abort "This case should never happen.";
+
+      sslServerCert = if useACME then "${sslCertDir}/full.pem" else hostOpts.sslServerCert;
+      sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
+      sslServerChain = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerChain;
+
+      acmeChallenge = optionalString useACME ''
+        Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
+        <Directory "${hostOpts.acmeRoot}">
+            AllowOverride None
+            Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+            Require method GET POST OPTIONS
+            Require all granted
+        </Directory>
+      '';
+    in
+      optionalString (listen != []) ''
+        <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}>
+            ServerName ${hostOpts.hostName}
+            ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
+            ServerAdmin ${adminAddr}
+            <IfModule mod_ssl.c>
+                SSLEngine off
+            </IfModule>
+            ${acmeChallenge}
+            ${if hostOpts.forceSSL then ''
+              <IfModule mod_rewrite.c>
+                  RewriteEngine on
+                  RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC]
+                  RewriteCond %{HTTPS} off
+                  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
+              </IfModule>
+            '' else mkVHostCommonConf hostOpts}
+        </VirtualHost>
+      '' +
+      optionalString (listenSSL != []) ''
+        <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listenSSL}>
+            ServerName ${hostOpts.hostName}
+            ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
+            ServerAdmin ${adminAddr}
+            SSLEngine on
+            SSLCertificateFile ${sslServerCert}
+            SSLCertificateKeyFile ${sslServerKey}
+            ${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"}
+            ${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"}
+            ${acmeChallenge}
+            ${mkVHostCommonConf hostOpts}
+        </VirtualHost>
+      ''
+  ;
+
+  mkVHostCommonConf = hostOpts:
+    let
+      documentRoot = if hostOpts.documentRoot != null
+        then hostOpts.documentRoot
+        else pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"
+      ;
+
+      mkLocations = locations: concatStringsSep "\n" (map (config: ''
+        <Location ${config.location}>
+          ${optionalString (config.proxyPass != null) ''
+            <IfModule mod_proxy.c>
+                ProxyPass ${config.proxyPass}
+                ProxyPassReverse ${config.proxyPass}
+            </IfModule>
+          ''}
+          ${optionalString (config.index != null) ''
+            <IfModule mod_dir.c>
+                DirectoryIndex ${config.index}
+            </IfModule>
+          ''}
+          ${optionalString (config.alias != null) ''
+            <IfModule mod_alias.c>
+                Alias "${config.alias}"
+            </IfModule>
+          ''}
+          ${config.extraConfig}
+        </Location>
+      '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
+    in
+      ''
+        ${optionalString cfg.logPerVirtualHost ''
+          ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log
+          CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat}
+        ''}
+
+        ${optionalString (hostOpts.robotsEntries != "") ''
+          Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries}
+        ''}
+
+        DocumentRoot "${documentRoot}"
+
+        <Directory "${documentRoot}">
+            Options Indexes FollowSymLinks
+            AllowOverride None
+            Require all granted
+        </Directory>
+
+        ${optionalString hostOpts.enableUserDir ''
+          UserDir public_html
+          UserDir disabled root
+          <Directory "/home/*/public_html">
+              AllowOverride FileInfo AuthConfig Limit Indexes
+              Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+              <Limit GET POST OPTIONS>
+                  Require all granted
+              </Limit>
+              <LimitExcept GET POST OPTIONS>
+                  Require all denied
+              </LimitExcept>
+          </Directory>
+        ''}
+
+        ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") ''
+          RedirectPermanent / ${hostOpts.globalRedirect}
+        ''}
+
+        ${
+          let makeDirConf = elem: ''
+                Alias ${elem.urlPath} ${elem.dir}/
+                <Directory ${elem.dir}>
+                    Options +Indexes
+                    Require all granted
+                    AllowOverride All
+                </Directory>
+              '';
+          in concatMapStrings makeDirConf hostOpts.servedDirs
+        }
+
+        ${mkLocations hostOpts.locations}
+        ${hostOpts.extraConfig}
+      ''
+  ;
 
 
   confFile = pkgs.writeText "httpd.conf" ''
 
-    ServerRoot ${httpd}
-
+    ServerRoot ${pkg}
+    ServerName ${config.networking.hostName}
     DefaultRuntimeDir ${runtimeDir}/runtime
 
     PidFile ${runtimeDir}/httpd.pid
 
-    ${optionalString (mainCfg.multiProcessingModule != "prefork") ''
+    ${optionalString (cfg.multiProcessingModule != "prefork") ''
       # mod_cgid requires this.
       ScriptSock ${runtimeDir}/cgisock
     ''}
 
     <IfModule prefork.c>
-        MaxClients           ${toString mainCfg.maxClients}
-        MaxRequestsPerChild  ${toString mainCfg.maxRequestsPerChild}
+        MaxClients           ${toString cfg.maxClients}
+        MaxRequestsPerChild  ${toString cfg.maxRequestsPerChild}
     </IfModule>
 
     ${let
-        listen = concatMap getListen allHosts;
-        toStr = listen: "Listen ${listenToString listen}\n";
-        uniqueListen = uniqList {inputList = map toStr listen;};
-      in concatStrings uniqueListen
+        toStr = listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}";
+        uniqueListen = uniqList {inputList = map toStr listenInfo;};
+      in concatStringsSep "\n" uniqueListen
     }
 
-    User ${mainCfg.user}
-    Group ${mainCfg.group}
+    User ${cfg.user}
+    Group ${cfg.group}
 
     ${let
         mkModule = module:
-          if isString module then { name = module; path = "${httpd}/modules/mod_${module}.so"; }
+          if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; }
           else if isAttrs module then { inherit (module) name path; }
           else throw "Expecting either a string or attribute set including a name and path.";
       in
@@ -267,57 +295,50 @@ let
     AddHandler type-map var
 
     <Files ~ "^\.ht">
-        ${allDenied}
+        Require all denied
     </Files>
 
     ${mimeConf}
     ${loggingConf}
     ${browserHacks}
 
-    Include ${httpd}/conf/extra/httpd-default.conf
-    Include ${httpd}/conf/extra/httpd-autoindex.conf
-    Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf
-    Include ${httpd}/conf/extra/httpd-languages.conf
+    Include ${pkg}/conf/extra/httpd-default.conf
+    Include ${pkg}/conf/extra/httpd-autoindex.conf
+    Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf
+    Include ${pkg}/conf/extra/httpd-languages.conf
 
     TraceEnable off
 
-    ${if enableSSL then sslConf else ""}
+    ${sslConf}
 
     # Fascist default - deny access to everything.
     <Directory />
         Options FollowSymLinks
         AllowOverride None
-        ${allDenied}
+        Require all denied
     </Directory>
 
     # But do allow access to files in the store so that we don't have
     # to generate <Directory> clauses for every generated file that we
     # want to serve.
     <Directory /nix/store>
-        ${allGranted}
+        Require all granted
     </Directory>
 
-    # Generate directives for the main server.
-    ${perServerConf true mainCfg}
+    ${cfg.extraConfig}
 
-    ${let
-        makeVirtualHost = vhost: ''
-          <VirtualHost ${concatStringsSep " " (map listenToString (getListen vhost))}>
-              ${perServerConf false vhost}
-          </VirtualHost>
-        '';
-      in concatMapStrings makeVirtualHost mainCfg.virtualHosts
-    }
+    ${concatMapStringsSep "\n" mkVHostConf vhosts}
   '';
 
   # Generate the PHP configuration file.  Should probably be factored
   # out into a separate module.
   phpIni = pkgs.runCommand "php.ini"
-    { options = mainCfg.phpOptions;
+    { options = cfg.phpOptions;
       preferLocalBuild = true;
     }
     ''
       cat ${php}/etc/php.ini > $out
+      cat ${php}/lib/custom-php.ini > $out
       echo "$options" >> $out
     '';
 
@@ -329,19 +350,30 @@ in
   imports = [
     (mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.")
     (mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.")
+
+    # virtualHosts options
+    (mkRemovedOptionModule [ "services" "httpd" "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
+    (mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
   ];
 
-  ###### interface
+  # interface
 
   options = {
 
     services.httpd = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable the Apache HTTP Server.";
-      };
+      enable = mkEnableOption "the Apache HTTP Server";
 
       package = mkOption {
         type = types.package;
@@ -367,8 +399,8 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Cnfiguration lines appended to the generated Apache
-          configuration file. Note that this mechanism may not work
+          Configuration lines appended to the generated Apache
+          configuration file. Note that this mechanism will not work
           when <option>configFile</option> is overridden.
         '';
       };
@@ -383,7 +415,7 @@ in
           ]
         '';
         description = ''
-          Additional Apache modules to be used.  These can be
+          Additional Apache modules to be used. These can be
           specified as a string in the case of modules distributed
           with Apache, or as an attribute set specifying the
           <varname>name</varname> and <varname>path</varname> of the
@@ -391,9 +423,25 @@ in
         '';
       };
 
+      adminAddr = mkOption {
+        type = types.str;
+        example = "admin@example.org";
+        description = "E-mail address of the server administrator.";
+      };
+
+      logFormat = mkOption {
+        type = types.str;
+        default = "common";
+        example = "combined";
+        description = ''
+          Log format for log files. Possible values are: combined, common, referer, agent.
+          See <link xlink:href="https://httpd.apache.org/docs/2.4/logs.html"/> for more details.
+        '';
+      };
+
       logPerVirtualHost = mkOption {
         type = types.bool;
-        default = false;
+        default = true;
         description = ''
           If enabled, each virtual host gets its own
           <filename>access.log</filename> and
@@ -406,8 +454,7 @@ in
         type = types.str;
         default = "wwwrun";
         description = ''
-          User account under which httpd runs.  The account is created
-          automatically if it doesn't exist.
+          User account under which httpd runs.
         '';
       };
 
@@ -415,8 +462,7 @@ in
         type = types.str;
         default = "wwwrun";
         description = ''
-          Group under which httpd runs.  The account is created
-          automatically if it doesn't exist.
+          Group under which httpd runs.
         '';
       };
 
@@ -424,31 +470,33 @@ in
         type = types.path;
         default = "/var/log/httpd";
         description = ''
-          Directory for Apache's log files.  It is created automatically.
+          Directory for Apache's log files. It is created automatically.
         '';
       };
 
       virtualHosts = mkOption {
-        type = types.listOf (types.submodule (
-          { options = import ./per-server-options.nix {
-              inherit lib;
-              forMainServer = false;
+        type = with types; attrsOf (submodule (import ./vhost-options.nix));
+        default = {
+          localhost = {
+            documentRoot = "${pkg}/htdocs";
+          };
+        };
+        example = literalExample ''
+          {
+            "foo.example.com" = {
+              forceSSL = true;
+              documentRoot = "/var/www/foo.example.com"
+            };
+            "bar.example.com" = {
+              addSSL = true;
+              documentRoot = "/var/www/bar.example.com";
             };
-          }));
-        default = [];
-        example = [
-          { hostName = "foo";
-            documentRoot = "/data/webroot-foo";
-          }
-          { hostName = "bar";
-            documentRoot = "/data/webroot-bar";
           }
-        ];
+        '';
         description = ''
-          Specification of the virtual hosts served by Apache.  Each
+          Specification of the virtual hosts served by Apache. Each
           element should be an attribute set specifying the
-          configuration of the virtual host.  The available options
-          are the non-global options permissible for the main host.
+          configuration of the virtual host.
         '';
       };
 
@@ -486,17 +534,18 @@ in
           ''
             date.timezone = "CET"
           '';
-        description =
-          "Options appended to the PHP configuration file <filename>php.ini</filename>.";
+        description = ''
+          Options appended to the PHP configuration file <filename>php.ini</filename>.
+        '';
       };
 
       multiProcessingModule = mkOption {
-        type = types.str;
+        type = types.enum [ "event" "prefork" "worker" ];
         default = "prefork";
         example = "worker";
         description =
           ''
-            Multi-processing module to be used by Apache.  Available
+            Multi-processing module to be used by Apache. Available
             modules are <literal>prefork</literal> (the default;
             handles each request in a separate child process),
             <literal>worker</literal> (hybrid approach that starts a
@@ -518,8 +567,9 @@ in
         type = types.int;
         default = 0;
         example = 500;
-        description =
-          "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited";
+        description = ''
+          Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited.
+        '';
       };
 
       sslCiphers = mkOption {
@@ -530,44 +580,74 @@ in
 
       sslProtocols = mkOption {
         type = types.str;
-        default = "All -SSLv2 -SSLv3 -TLSv1";
+        default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1";
         example = "All -SSLv2 -SSLv3";
         description = "Allowed SSL/TLS protocol versions.";
       };
-    }
-
-    # Include the options shared between the main server and virtual hosts.
-    // (import ./per-server-options.nix {
-      inherit lib;
-      forMainServer = true;
-    });
+    };
 
   };
 
+  # implementation
 
-  ###### implementation
+  config = mkIf cfg.enable {
 
-  config = mkIf config.services.httpd.enable {
+    assertions = [
+      {
+        assertion = all (hostOpts: !hostOpts.enableSSL) vhosts;
+        message = ''
+          The option `services.httpd.virtualHosts.<name>.enableSSL` no longer has any effect; please remove it.
+          Select one of `services.httpd.virtualHosts.<name>.addSSL`, `services.httpd.virtualHosts.<name>.forceSSL`,
+          or `services.httpd.virtualHosts.<name>.onlySSL`.
+        '';
+      }
+      {
+        assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts;
+        message = ''
+          Options `services.httpd.virtualHosts.<name>.addSSL`,
+          `services.httpd.virtualHosts.<name>.onlySSL` and `services.httpd.virtualHosts.<name>.forceSSL`
+          are mutually exclusive.
+        '';
+      }
+      {
+        assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts;
+        message = ''
+          Options `services.httpd.virtualHosts.<name>.enableACME` and
+          `services.httpd.virtualHosts.<name>.useACMEHost` are mutually exclusive.
+        '';
+      }
+    ];
 
-    assertions = [ { assertion = mainCfg.enableSSL == true
-                               -> mainCfg.sslServerCert != null
-                                    && mainCfg.sslServerKey != null;
-                     message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; }
-                 ];
+    warnings =
+      mapAttrsToList (name: hostOpts: ''
+        Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS.
+      '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts);
 
-    users.users = optionalAttrs (mainCfg.user == "wwwrun") (singleton
-      { name = "wwwrun";
-        group = mainCfg.group;
+    users.users = optionalAttrs (cfg.user == "wwwrun") {
+      wwwrun = {
+        group = cfg.group;
         description = "Apache httpd user";
         uid = config.ids.uids.wwwrun;
-      });
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "wwwrun") {
+      wwwrun.gid = config.ids.gids.wwwrun;
+    };
+
+    security.acme.certs = mapAttrs (name: hostOpts: {
+      user = cfg.user;
+      group = mkDefault cfg.group;
+      email = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
+      webroot = hostOpts.acmeRoot;
+      extraDomains = genAttrs hostOpts.serverAliases (alias: null);
+      postRun = "systemctl reload httpd.service";
+    }) (filterAttrs (name: hostOpts: hostOpts.enableACME) cfg.virtualHosts);
 
-    users.groups = optionalAttrs (mainCfg.group == "wwwrun") (singleton
-      { name = "wwwrun";
-        gid = config.ids.gids.wwwrun;
-      });
+    environment.systemPackages = [ pkg ];
 
-    environment.systemPackages = [httpd];
+    # required for "apachectl configtest"
+    environment.etc."httpd/httpd.conf".source = httpdConf;
 
     services.httpd.phpOptions =
       ''
@@ -604,42 +684,56 @@ in
       "access_compat"
     ];
 
+    systemd.tmpfiles.rules =
+      let
+        svc = config.systemd.services.httpd.serviceConfig;
+      in
+        [
+          "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}"
+          "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
+        ];
+
     systemd.services.httpd =
+      let
+        vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts;
+      in
       { description = "Apache HTTPD";
 
         wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" "fs.target" ];
+        wants = concatLists (map (hostOpts: [ "acme-${hostOpts.hostName}.service" "acme-selfsigned-${hostOpts.hostName}.service" ]) vhostsACME);
+        after = [ "network.target" "fs.target" ] ++ map (hostOpts: "acme-selfsigned-${hostOpts.hostName}.service") vhostsACME;
 
         path =
-          [ httpd pkgs.coreutils pkgs.gnugrep ]
-          ++ optional mainCfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function.
+          [ pkg pkgs.coreutils pkgs.gnugrep ]
+          ++ optional cfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function.
 
         environment =
-          optionalAttrs mainCfg.enablePHP { PHPRC = phpIni; }
-          // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };
+          optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
+          // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };
 
         preStart =
           ''
-            mkdir -m 0700 -p ${mainCfg.logDir}
-
             # Get rid of old semaphores.  These tend to accumulate across
             # server restarts, eventually preventing it from restarting
             # successfully.
-            for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do
+            for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
                 ${pkgs.utillinux}/bin/ipcrm -s $i
             done
           '';
 
-        serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}";
-        serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop";
-        serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful";
-        serviceConfig.Group = mainCfg.group;
-        serviceConfig.Type = "forking";
-        serviceConfig.PIDFile = "${runtimeDir}/httpd.pid";
-        serviceConfig.Restart = "always";
-        serviceConfig.RestartSec = "5s";
-        serviceConfig.RuntimeDirectory = "httpd httpd/runtime";
-        serviceConfig.RuntimeDirectoryMode = "0750";
+        serviceConfig = {
+          ExecStart = "@${pkg}/bin/httpd httpd -f ${httpdConf}";
+          ExecStop = "${pkg}/bin/httpd -f ${httpdConf} -k graceful-stop";
+          ExecReload = "${pkg}/bin/httpd -f ${httpdConf} -k graceful";
+          User = "root";
+          Group = cfg.group;
+          Type = "forking";
+          PIDFile = "${runtimeDir}/httpd.pid";
+          Restart = "always";
+          RestartSec = "5s";
+          RuntimeDirectory = "httpd httpd/runtime";
+          RuntimeDirectoryMode = "0750";
+        };
       };
 
   };
diff --git a/nixos/modules/services/web-servers/apache-httpd/location-options.nix b/nixos/modules/services/web-servers/apache-httpd/location-options.nix
new file mode 100644
index 00000000000..8ea88f94f97
--- /dev/null
+++ b/nixos/modules/services/web-servers/apache-httpd/location-options.nix
@@ -0,0 +1,54 @@
+{ config, lib, name, ... }:
+let
+  inherit (lib) mkOption types;
+in
+{
+  options = {
+
+    proxyPass = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "http://www.example.org/";
+      description = ''
+        Sets up a simple reverse proxy as described by <link xlink:href="https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html#simple" />.
+      '';
+    };
+
+    index = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "index.php index.html";
+      description = ''
+        Adds DirectoryIndex directive. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_dir.html#directoryindex" />.
+      '';
+    };
+
+    alias = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/your/alias/directory";
+      description = ''
+        Alias directory for requests. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_alias.html#alias" />.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        These lines go to the end of the location verbatim.
+      '';
+    };
+
+    priority = mkOption {
+      type = types.int;
+      default = 1000;
+      description = ''
+        Order of this location block in relation to the others in the vhost.
+        The semantics are the same as with `lib.mkOrder`. Smaller values have
+        a greater priority.
+      '';
+    };
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix b/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix
deleted file mode 100644
index c36207d5460..00000000000
--- a/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix
+++ /dev/null
@@ -1,174 +0,0 @@
-# This file defines the options that can be used both for the Apache
-# main server configuration, and for the virtual hosts.  (The latter
-# has additional options that affect the web server as a whole, like
-# the user/group to run under.)
-
-{ forMainServer, lib }:
-
-with lib;
-
-{
-
-  hostName = mkOption {
-    type = types.str;
-    default = "localhost";
-    description = "Canonical hostname for the server.";
-  };
-
-  serverAliases = mkOption {
-    type = types.listOf types.str;
-    default = [];
-    example = ["www.example.org" "www.example.org:8080" "example.org"];
-    description = ''
-      Additional names of virtual hosts served by this virtual host configuration.
-    '';
-  };
-
-  listen = mkOption {
-     type = types.listOf (types.submodule (
-          {
-            options = {
-              port = mkOption {
-                type = types.int;
-                description = "port to listen on";
-              };
-              ip = mkOption {
-                type = types.str;
-                default = "*";
-                description = "Ip to listen on. 0.0.0.0 for ipv4 only, * for all.";
-              };
-            };
-          } ));
-    description = ''
-      List of { /* ip: "*"; */ port = 80;} to listen on
-    '';
-
-    default = [];
-  };
-
-  enableSSL = mkOption {
-    type = types.bool;
-    default = false;
-    description = "Whether to enable SSL (https) support.";
-  };
-
-  # Note: sslServerCert and sslServerKey can be left empty, but this
-  # only makes sense for virtual hosts (they will inherit from the
-  # main server).
-
-  sslServerCert = mkOption {
-    type = types.nullOr types.path;
-    default = null;
-    example = "/var/host.cert";
-    description = "Path to server SSL certificate.";
-  };
-
-  sslServerKey = mkOption {
-    type = types.path;
-    example = "/var/host.key";
-    description = "Path to server SSL certificate key.";
-  };
-
-  sslServerChain = mkOption {
-    type = types.nullOr types.path;
-    default = null;
-    example = "/var/ca.pem";
-    description = "Path to server SSL chain file.";
-  };
-
-  adminAddr = mkOption ({
-    type = types.nullOr types.str;
-    example = "admin@example.org";
-    description = "E-mail address of the server administrator.";
-  } // (if forMainServer then {} else {default = null;}));
-
-  documentRoot = mkOption {
-    type = types.nullOr types.path;
-    default = null;
-    example = "/data/webserver/docs";
-    description = ''
-      The path of Apache's document root directory.  If left undefined,
-      an empty directory in the Nix store will be used as root.
-    '';
-  };
-
-  servedDirs = mkOption {
-    type = types.listOf types.attrs;
-    default = [];
-    example = [
-      { urlPath = "/nix";
-        dir = "/home/eelco/Dev/nix-homepage";
-      }
-    ];
-    description = ''
-      This option provides a simple way to serve static directories.
-    '';
-  };
-
-  servedFiles = mkOption {
-    type = types.listOf types.attrs;
-    default = [];
-    example = [
-      { urlPath = "/foo/bar.png";
-        file = "/home/eelco/some-file.png";
-      }
-    ];
-    description = ''
-      This option provides a simple way to serve individual, static files.
-    '';
-  };
-
-  extraConfig = mkOption {
-    type = types.lines;
-    default = "";
-    example = ''
-      <Directory /home>
-        Options FollowSymlinks
-        AllowOverride All
-      </Directory>
-    '';
-    description = ''
-      These lines go to httpd.conf verbatim. They will go after
-      directories and directory aliases defined by default.
-    '';
-  };
-
-  enableUserDir = mkOption {
-    type = types.bool;
-    default = false;
-    description = ''
-      Whether to enable serving <filename>~/public_html</filename> as
-      <literal>/~<replaceable>username</replaceable></literal>.
-    '';
-  };
-
-  globalRedirect = mkOption {
-    type = types.nullOr types.str;
-    default = null;
-    example = http://newserver.example.org/;
-    description = ''
-      If set, all requests for this host are redirected permanently to
-      the given URL.
-    '';
-  };
-
-  logFormat = mkOption {
-    type = types.str;
-    default = "common";
-    example = "combined";
-    description = ''
-      Log format for Apache's log files. Possible values are: combined, common, referer, agent.
-    '';
-  };
-
-  robotsEntries = mkOption {
-    type = types.lines;
-    default = "";
-    example = "Disallow: /foo/";
-    description = ''
-      Specification of pages to be ignored by web crawlers. See <link
-      xlink:href='http://www.robotstxt.org/'/> for details.
-    '';
-  };
-
-}
diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
new file mode 100644
index 00000000000..2e806afb42c
--- /dev/null
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -0,0 +1,275 @@
+{ config, lib, name, ... }:
+let
+  inherit (lib) literalExample mkOption nameValuePair types;
+in
+{
+  options = {
+
+    hostName = mkOption {
+      type = types.str;
+      default = name;
+      description = "Canonical hostname for the server.";
+    };
+
+    serverAliases = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = ["www.example.org" "www.example.org:8080" "example.org"];
+      description = ''
+        Additional names of virtual hosts served by this virtual host configuration.
+      '';
+    };
+
+    listen = mkOption {
+      type = with types; listOf (submodule ({
+        options = {
+          port = mkOption {
+            type = types.port;
+            description = "Port to listen on";
+          };
+          ip = mkOption {
+            type = types.str;
+            default = "*";
+            description = "IP to listen on. 0.0.0.0 for IPv4 only, * for all.";
+          };
+          ssl = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Whether to enable SSL (https) support.";
+          };
+        };
+      }));
+      default = [];
+      example = [
+        { ip = "195.154.1.1"; port = 443; ssl = true;}
+        { ip = "192.154.1.1"; port = 80; }
+        { ip = "*"; port = 8080; }
+      ];
+      description = ''
+        Listen addresses and ports for this virtual host.
+        <note><para>
+          This option overrides <literal>addSSL</literal>, <literal>forceSSL</literal> and <literal>onlySSL</literal>.
+        </para></note>
+      '';
+    };
+
+    enableSSL = mkOption {
+      type = types.bool;
+      visible = false;
+      default = false;
+    };
+
+    addSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTPS in addition to plain HTTP. This will set defaults for
+        <literal>listen</literal> to listen on all interfaces on the respective default
+        ports (80, 443).
+      '';
+    };
+
+    onlySSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTPS and reject plain HTTP connections. This will set
+        defaults for <literal>listen</literal> to listen on all interfaces on port 443.
+      '';
+    };
+
+    forceSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to add a separate nginx server block that permanently redirects (301)
+        all plain HTTP traffic to HTTPS. This will set defaults for
+        <literal>listen</literal> to listen on all interfaces on the respective default
+        ports (80, 443), where the non-SSL listens are used for the redirect vhosts.
+      '';
+    };
+
+    enableACME = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to ask Let's Encrypt to sign a certificate for this vhost.
+        Alternately, you can use an existing certificate through <option>useACMEHost</option>.
+      '';
+    };
+
+    useACMEHost = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        A host of an existing Let's Encrypt certificate to use.
+        This is useful if you have many subdomains and want to avoid hitting the
+        <link xlink:href="https://letsencrypt.org/docs/rate-limits/">rate limit</link>.
+        Alternately, you can generate a certificate through <option>enableACME</option>.
+        <emphasis>Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using  <xref linkend="opt-security.acme.certs"/>.</emphasis>
+      '';
+    };
+
+    acmeRoot = mkOption {
+      type = types.str;
+      default = "/var/lib/acme/acme-challenges";
+      description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here";
+    };
+
+    sslServerCert = mkOption {
+      type = types.path;
+      example = "/var/host.cert";
+      description = "Path to server SSL certificate.";
+    };
+
+    sslServerKey = mkOption {
+      type = types.path;
+      example = "/var/host.key";
+      description = "Path to server SSL certificate key.";
+    };
+
+    sslServerChain = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/ca.pem";
+      description = "Path to server SSL chain file.";
+    };
+
+    http2 = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTP 2. HTTP/2 is supported in all multi-processing modules that come with httpd. <emphasis>However, if you use the prefork mpm, there will
+        be severe restrictions.</emphasis> Refer to <link xlink:href="https://httpd.apache.org/docs/2.4/howto/http2.html#mpm-config"/> for details.
+      '';
+    };
+
+    adminAddr = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "admin@example.org";
+      description = "E-mail address of the server administrator.";
+    };
+
+    documentRoot = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/data/webserver/docs";
+      description = ''
+        The path of Apache's document root directory.  If left undefined,
+        an empty directory in the Nix store will be used as root.
+      '';
+    };
+
+    servedDirs = mkOption {
+      type = types.listOf types.attrs;
+      default = [];
+      example = [
+        { urlPath = "/nix";
+          dir = "/home/eelco/Dev/nix-homepage";
+        }
+      ];
+      description = ''
+        This option provides a simple way to serve static directories.
+      '';
+    };
+
+    servedFiles = mkOption {
+      type = types.listOf types.attrs;
+      default = [];
+      example = [
+        { urlPath = "/foo/bar.png";
+          file = "/home/eelco/some-file.png";
+        }
+      ];
+      description = ''
+        This option provides a simple way to serve individual, static files.
+
+        <note><para>
+          This option has been deprecated and will be removed in a future
+          version of NixOS. You can achieve the same result by making use of
+          the <literal>locations.&lt;name&gt;.alias</literal> option.
+        </para></note>
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        <Directory /home>
+          Options FollowSymlinks
+          AllowOverride All
+        </Directory>
+      '';
+      description = ''
+        These lines go to httpd.conf verbatim. They will go after
+        directories and directory aliases defined by default.
+      '';
+    };
+
+    enableUserDir = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable serving <filename>~/public_html</filename> as
+        <literal>/~<replaceable>username</replaceable></literal>.
+      '';
+    };
+
+    globalRedirect = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "http://newserver.example.org/";
+      description = ''
+        If set, all requests for this host are redirected permanently to
+        the given URL.
+      '';
+    };
+
+    logFormat = mkOption {
+      type = types.str;
+      default = "common";
+      example = "combined";
+      description = ''
+        Log format for Apache's log files. Possible values are: combined, common, referer, agent.
+      '';
+    };
+
+    robotsEntries = mkOption {
+      type = types.lines;
+      default = "";
+      example = "Disallow: /foo/";
+      description = ''
+        Specification of pages to be ignored by web crawlers. See <link
+        xlink:href='http://www.robotstxt.org/'/> for details.
+      '';
+    };
+
+    locations = mkOption {
+      type = with types; attrsOf (submodule (import ./location-options.nix));
+      default = {};
+      example = literalExample ''
+        {
+          "/" = {
+            proxyPass = "http://localhost:3000";
+          };
+          "/foo/bar.png" = {
+            alias = "/home/eelco/some-file.png";
+          };
+        };
+      '';
+      description = ''
+        Declarative location config. See <link
+        xlink:href="https://httpd.apache.org/docs/2.4/mod/core.html#location"/> for details.
+      '';
+    };
+
+  };
+
+  config = {
+
+    locations = builtins.listToAttrs (map (elem: nameValuePair elem.urlPath { alias = elem.file; }) config.servedFiles);
+
+  };
+}
diff --git a/nixos/modules/services/web-servers/caddy.nix b/nixos/modules/services/web-servers/caddy.nix
index 132c50735d9..0e6e10a5f47 100644
--- a/nixos/modules/services/web-servers/caddy.nix
+++ b/nixos/modules/services/web-servers/caddy.nix
@@ -64,32 +64,38 @@ in {
   config = mkIf cfg.enable {
     systemd.services.caddy = {
       description = "Caddy web server";
+      # upstream unit: https://github.com/caddyserver/caddy/blob/master/dist/init/linux-systemd/caddy.service
       after = [ "network-online.target" ];
+      wants = [ "network-online.target" ]; # systemd-networkd-wait-online.service
       wantedBy = [ "multi-user.target" ];
       environment = mkIf (versionAtLeast config.system.stateVersion "17.09")
         { CADDYPATH = cfg.dataDir; };
       serviceConfig = {
         ExecStart = ''
-          ${cfg.package}/bin/caddy -root=/var/tmp -conf=${configFile} \
+          ${cfg.package}/bin/caddy -log stdout -log-timestamps=false \
+            -root=/var/tmp -conf=${configFile} \
             -ca=${cfg.ca} -email=${cfg.email} ${optionalString cfg.agree "-agree"}
         '';
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
         Type = "simple";
         User = "caddy";
         Group = "caddy";
-        Restart = "on-failure";
-        StartLimitInterval = 86400;
-        StartLimitBurst = 5;
+        Restart = "on-abnormal";
+        StartLimitIntervalSec = 14400;
+        StartLimitBurst = 10;
         AmbientCapabilities = "cap_net_bind_service";
         CapabilityBoundingSet = "cap_net_bind_service";
         NoNewPrivileges = true;
-        LimitNPROC = 64;
+        LimitNPROC = 512;
         LimitNOFILE = 1048576;
         PrivateTmp = true;
         PrivateDevices = true;
         ProtectHome = true;
         ProtectSystem = "full";
         ReadWriteDirectories = cfg.dataDir;
+        KillMode = "mixed";
+        KillSignal = "SIGQUIT";
+        TimeoutStopSec = "5s";
       };
     };
 
diff --git a/nixos/modules/services/web-servers/jboss/default.nix b/nixos/modules/services/web-servers/jboss/default.nix
index d28724281a8..3a125982831 100644
--- a/nixos/modules/services/web-servers/jboss/default.nix
+++ b/nixos/modules/services/web-servers/jboss/default.nix
@@ -24,6 +24,7 @@ in
     services.jboss = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = "Whether to enable JBoss. WARNING : this package is outdated and is known to have vulnerabilities.";
       };
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index eb90dae94df..8d49dc66eb1 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -46,8 +46,17 @@ let
     }
   ''));
 
+  commonHttpConfig = ''
+      # The mime type definitions included with nginx are very incomplete, so
+      # we use a list of mime types from the mailcap package, which is also
+      # used by most other Linux distributions by default.
+      include ${pkgs.mailcap}/etc/nginx/mime.types;
+      include ${cfg.package}/conf/fastcgi.conf;
+      include ${cfg.package}/conf/uwsgi_params;
+  '';
+
   configFile = pkgs.writers.writeNginxConfig "nginx.conf" ''
-    user ${cfg.user} ${cfg.group};
+    pid /run/nginx/nginx.pid;
     error_log ${cfg.logError};
     daemon off;
 
@@ -61,12 +70,7 @@ let
 
     ${optionalString (cfg.httpConfig == "" && cfg.config == "") ''
     http {
-      # The mime type definitions included with nginx are very incomplete, so
-      # we use a list of mime types from the mailcap package, which is also
-      # used by most other Linux distributions by default.
-      include ${pkgs.mailcap}/etc/nginx/mime.types;
-      include ${cfg.package}/conf/fastcgi.conf;
-      include ${cfg.package}/conf/uwsgi_params;
+      ${commonHttpConfig}
 
       ${optionalString (cfg.resolver.addresses != []) ''
         resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"} ${optionalString (!cfg.resolver.ipv6) "ipv6=off"};
@@ -79,7 +83,7 @@ let
         tcp_nopush on;
         tcp_nodelay on;
         keepalive_timeout 65;
-        types_hash_max_size 2048;
+        types_hash_max_size 4096;
       ''}
 
       ssl_protocols ${cfg.sslProtocols};
@@ -87,10 +91,17 @@ let
       ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
 
       ${optionalString (cfg.recommendedTlsSettings) ''
-        ssl_session_cache shared:SSL:42m;
-        ssl_session_timeout 23m;
-        ssl_ecdh_curve secp384r1;
-        ssl_prefer_server_ciphers on;
+        # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
+
+        ssl_session_timeout 1d;
+        ssl_session_cache shared:SSL:10m;
+        # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135
+        ssl_session_tickets off;
+        # We don't enable insecure ciphers by default, so this allows
+        # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260
+        ssl_prefer_server_ciphers off;
+
+        # OCSP stapling
         ssl_stapling on;
         ssl_stapling_verify on;
       ''}
@@ -165,9 +176,7 @@ let
 
     ${optionalString (cfg.httpConfig != "") ''
     http {
-      include ${cfg.package}/conf/mime.types;
-      include ${cfg.package}/conf/fastcgi.conf;
-      include ${cfg.package}/conf/uwsgi_params;
+      ${commonHttpConfig}
       ${cfg.httpConfig}
     }''}
 
@@ -178,6 +187,8 @@ let
     then "/etc/nginx/nginx.conf"
     else configFile;
 
+  execCommand = "${cfg.package}/bin/nginx -c '${configPath}' -p '${cfg.stateDir}'";
+
   vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost:
     let
         onlySSL = vhost.onlySSL || vhost.enableSSL;
@@ -366,12 +377,7 @@ in
 
       preStart =  mkOption {
         type = types.lines;
-        default = ''
-          test -d ${cfg.stateDir}/logs || mkdir -m 750 -p ${cfg.stateDir}/logs
-          test `stat -c %a ${cfg.stateDir}` = "750" || chmod 750 ${cfg.stateDir}
-          test `stat -c %a ${cfg.stateDir}/logs` = "750" || chmod 750 ${cfg.stateDir}/logs
-          chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
-        '';
+        default = "";
         description = "
           Shell commands executed before the service's nginx is started.
         ";
@@ -490,8 +496,9 @@ in
 
       sslCiphers = mkOption {
         type = types.str;
-        default = "EECDH+aRSA+AESGCM:EDH+aRSA:EECDH+aRSA:+AES256:+AES128:+SHA1:!CAMELLIA:!SEED:!3DES:!DES:!RC4:!eNULL";
-        description = "Ciphers to choose from when negotiating tls handshakes.";
+        # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
+        default = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
+        description = "Ciphers to choose from when negotiating TLS handshakes.";
       };
 
       sslProtocols = mkOption {
@@ -673,23 +680,36 @@ in
       }
     ];
 
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/logs' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
     systemd.services.nginx = {
       description = "Nginx Web Server";
       wantedBy = [ "multi-user.target" ];
       wants = concatLists (map (vhostConfig: ["acme-${vhostConfig.serverName}.service" "acme-selfsigned-${vhostConfig.serverName}.service"]) acmeEnabledVhosts);
       after = [ "network.target" ] ++ map (vhostConfig: "acme-selfsigned-${vhostConfig.serverName}.service") acmeEnabledVhosts;
       stopIfChanged = false;
-      preStart =
-        ''
+      preStart = ''
         ${cfg.preStart}
-        ${cfg.package}/bin/nginx -c ${configPath} -p ${cfg.stateDir} -t
-        '';
+        ${execCommand} -t
+      '';
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/nginx -c ${configPath} -p ${cfg.stateDir}";
+        ExecStart = execCommand;
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "always";
         RestartSec = "10s";
         StartLimitInterval = "1min";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Runtime directory and mode
+        RuntimeDirectory = "nginx";
+        RuntimeDirectoryMode = "0750";
+        # Capabilities
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
       };
     };
 
@@ -698,11 +718,18 @@ in
     };
 
     systemd.services.nginx-config-reload = mkIf cfg.enableReload {
-      wantedBy = [ "nginx.service" ];
+      wants = [ "nginx.service" ];
+      wantedBy = [ "multi-user.target" ];
       restartTriggers = [ configFile ];
+      # commented, because can cause extra delays during activate for this config:
+      #      services.nginx.virtualHosts."_".locations."/".proxyPass = "http://blabla:3000";
+      # stopIfChanged = false;
+      serviceConfig.Type = "oneshot";
+      serviceConfig.TimeoutSec = 60;
       script = ''
         if ${pkgs.systemd}/bin/systemctl -q is-active nginx.service ; then
-          ${pkgs.systemd}/bin/systemctl reload nginx.service
+          ${execCommand} -t && \
+            ${pkgs.systemd}/bin/systemctl reload nginx.service
         fi
       '';
       serviceConfig.RemainAfterExit = true;
@@ -723,15 +750,16 @@ in
         listToAttrs acmePairs
     );
 
-    users.users = optionalAttrs (cfg.user == "nginx") (singleton
-      { name = "nginx";
+    users.users = optionalAttrs (cfg.user == "nginx") {
+      nginx = {
         group = cfg.group;
         uid = config.ids.uids.nginx;
-      });
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "nginx") {
+      nginx.gid = config.ids.gids.nginx;
+    };
 
-    users.groups = optionalAttrs (cfg.group == "nginx") (singleton
-      { name = "nginx";
-        gid = config.ids.gids.nginx;
-      });
   };
 }
diff --git a/nixos/modules/services/web-servers/nginx/gitweb.nix b/nixos/modules/services/web-servers/nginx/gitweb.nix
index 272fd148018..f7fb07bb797 100644
--- a/nixos/modules/services/web-servers/nginx/gitweb.nix
+++ b/nixos/modules/services/web-servers/nginx/gitweb.nix
@@ -3,8 +3,9 @@
 with lib;
 
 let
-  cfg = config.services.gitweb;
-  package = pkgs.gitweb.override (optionalAttrs cfg.gitwebTheme {
+  cfg = config.services.nginx.gitweb;
+  gitwebConfig = config.services.gitweb;
+  package = pkgs.gitweb.override (optionalAttrs gitwebConfig.gitwebTheme {
     gitwebTheme = true;
   });
 
@@ -17,13 +18,45 @@ in
       default = false;
       type = types.bool;
       description = ''
-        If true, enable gitweb in nginx. Access it at http://yourserver/gitweb
+        If true, enable gitweb in nginx.
+      '';
+    };
+
+    location = mkOption {
+      default = "/gitweb";
+      type = types.str;
+      description = ''
+        Location to serve gitweb on.
+      '';
+    };
+
+    user = mkOption {
+      default = "nginx";
+      type = types.str;
+      description = ''
+        Existing user that the CGI process will belong to. (Default almost surely will do.)
+      '';
+    };
+
+    group = mkOption {
+      default = "nginx";
+      type = types.str;
+      description = ''
+        Group that the CGI process will belong to. (Set to <literal>config.services.gitolite.group</literal> if you are using gitolite.)
+      '';
+    };
+
+    virtualHost = mkOption {
+      default = "_";
+      type = types.str;
+      description = ''
+        VirtualHost to serve gitweb on. Default is catch-all.
       '';
     };
 
   };
 
-  config = mkIf config.services.nginx.gitweb.enable {
+  config = mkIf cfg.enable {
 
     systemd.services.gitweb = {
       description = "GitWeb service";
@@ -32,22 +65,22 @@ in
         FCGI_SOCKET_PATH = "/run/gitweb/gitweb.sock";
       };
       serviceConfig = {
-        User = "nginx";
-        Group = "nginx";
+        User = cfg.user;
+        Group = cfg.group;
         RuntimeDirectory = [ "gitweb" ];
       };
       wantedBy = [ "multi-user.target" ];
     };
 
     services.nginx = {
-      virtualHosts.default = {
-        locations."/gitweb/static/" = {
+      virtualHosts.${cfg.virtualHost} = {
+        locations."${cfg.location}/static/" = {
           alias = "${package}/static/";
         };
-        locations."/gitweb/" = {
+        locations."${cfg.location}/" = {
           extraConfig = ''
             include ${pkgs.nginx}/conf/fastcgi_params;
-            fastcgi_param GITWEB_CONFIG ${cfg.gitwebConfigFile};
+            fastcgi_param GITWEB_CONFIG ${gitwebConfig.gitwebConfigFile};
             fastcgi_pass unix:/run/gitweb/gitweb.sock;
           '';
         };
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
index 2b3749d8a74..3d9e391ecf2 100644
--- a/nixos/modules/services/web-servers/nginx/location-options.nix
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -67,7 +67,7 @@ with lib;
     return = mkOption {
       type = types.nullOr types.str;
       default = null;
-      example = "301 http://example.com$request_uri;";
+      example = "301 http://example.com$request_uri";
       description = ''
         Adds a return directive, for e.g. redirections.
       '';
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 7e488f33a41..455854e2a96 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -207,6 +207,7 @@ with lib;
       default = null;
       description = ''
         Basic Auth password file for a vhost.
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>
       '';
     };
 
diff --git a/nixos/modules/services/web-servers/phpfpm/default.nix b/nixos/modules/services/web-servers/phpfpm/default.nix
index 7698f8c3a26..d090885a8ca 100644
--- a/nixos/modules/services/web-servers/phpfpm/default.nix
+++ b/nixos/modules/services/web-servers/phpfpm/default.nix
@@ -47,6 +47,7 @@ let
             Path to the unix socket file on which to accept FastCGI requests.
             <note><para>This option is read-only and managed by NixOS.</para></note>
           '';
+          example = "${runtimeDir}/<name>.sock";
         };
 
         listen = mkOption {
@@ -146,6 +147,10 @@ let
     };
 
 in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "phpfpm" "poolConfigs" ] "Use services.phpfpm.pools instead.")
+    (mkRemovedOptionModule [ "services" "phpfpm" "phpIni" ] "")
+  ];
 
   options = {
     services.phpfpm = {
@@ -204,14 +209,14 @@ in {
              user = "php";
              group = "php";
              phpPackage = pkgs.php;
-             settings = '''
+             settings = {
                "pm" = "dynamic";
                "pm.max_children" = 75;
                "pm.start_servers" = 10;
                "pm.min_spare_servers" = 5;
                "pm.max_spare_servers" = 20;
                "pm.max_requests" = 500;
-             ''';
+             };
            }
          }'';
         description = ''
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index 68261c50324..6d12925829f 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -194,14 +194,10 @@ in
 
   config = mkIf config.services.tomcat.enable {
 
-    users.groups = singleton
-      { name = "tomcat";
-        gid = config.ids.gids.tomcat;
-      };
+    users.groups.tomcat.gid = config.ids.gids.tomcat;
 
-    users.users = singleton
-      { name = "tomcat";
-        uid = config.ids.uids.tomcat;
+    users.users.tomcat =
+      { uid = config.ids.uids.tomcat;
         description = "Tomcat user";
         home = "/homeless-shelter";
         extraGroups = cfg.extraGroups;
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
index 5b0fc467ea4..4ab7307c3b6 100644
--- a/nixos/modules/services/web-servers/traefik.nix
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -4,56 +4,102 @@ with lib;
 
 let
   cfg = config.services.traefik;
-  configFile =
-    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)} \
-          > $out
-      ''
-    else cfg.configFile;
-
+  jsonValue = with types;
+    let
+      valueType = nullOr (oneOf [
+        bool
+        int
+        float
+        str
+        (lazyAttrsOf valueType)
+        (listOf valueType)
+      ]) // {
+        description = "JSON value";
+        emptyValue.value = { };
+      };
+    in valueType;
+  dynamicConfigFile = if cfg.dynamicConfigFile == null then
+    pkgs.runCommand "config.toml" {
+      buildInputs = [ pkgs.remarshal ];
+      preferLocalBuild = true;
+    } ''
+      remarshal -if json -of toml \
+        < ${
+          pkgs.writeText "dynamic_config.json"
+          (builtins.toJSON cfg.dynamicConfigOptions)
+        } \
+        > $out
+    ''
+  else
+    cfg.dynamicConfigFile;
+  staticConfigFile = if cfg.staticConfigFile == null then
+    pkgs.runCommand "config.toml" {
+      buildInputs = [ pkgs.yj ];
+      preferLocalBuild = true;
+    } ''
+      yj -jt -i \
+        < ${
+          pkgs.writeText "static_config.json" (builtins.toJSON
+            (recursiveUpdate cfg.staticConfigOptions {
+              providers.file.filename = "${dynamicConfigFile}";
+            }))
+        } \
+        > $out
+    ''
+  else
+    cfg.staticConfigFile;
 in {
   options.services.traefik = {
     enable = mkEnableOption "Traefik web server";
 
-    configFile = mkOption {
+    staticConfigFile = mkOption {
       default = null;
-      example = literalExample "/path/to/config.toml";
+      example = literalExample "/path/to/static_config.toml";
       type = types.nullOr types.path;
       description = ''
-        Path to verbatim traefik.toml to use.
-        (Using that option has precedence over <literal>configOptions</literal>)
+        Path to traefik's static configuration to use.
+        (Using that option has precedence over <literal>staticConfigOptions</literal> and <literal>dynamicConfigOptions</literal>)
       '';
     };
 
-    configOptions = mkOption {
+    staticConfigOptions = mkOption {
       description = ''
-        Config for Traefik.
+        Static configuration for Traefik.
       '';
-      type = types.attrs;
-      default = {
-        defaultEntryPoints = ["http"];
-        entryPoints.http.address = ":80";
-      };
+      type = jsonValue;
+      default = { entryPoints.http.address = ":80"; };
       example = {
-        defaultEntrypoints = [ "http" ];
-        web.address = ":8080";
+        entryPoints.web.address = ":8080";
         entryPoints.http.address = ":80";
 
-        file = {};
-        frontends = {
-          frontend1 = {
-            backend = "backend1";
-            routes.test_1.rule = "Host:localhost";
-          };
-        };
-        backends.backend1 = {
-          servers.server1.url = "http://localhost:8000";
+        api = { };
+      };
+    };
+
+    dynamicConfigFile = mkOption {
+      default = null;
+      example = literalExample "/path/to/dynamic_config.toml";
+      type = types.nullOr types.path;
+      description = ''
+        Path to traefik's dynamic configuration to use.
+        (Using that option has precedence over <literal>dynamicConfigOptions</literal>)
+      '';
+    };
+
+    dynamicConfigOptions = mkOption {
+      description = ''
+        Dynamic configuration for Traefik.
+      '';
+      type = jsonValue;
+      default = { };
+      example = {
+        http.routers.router1 = {
+          rule = "Host(`localhost`)";
+          service = "service1";
         };
+
+        http.services.service1.loadBalancer.servers =
+          [{ url = "http://localhost:8080"; }];
       };
     };
 
@@ -61,7 +107,7 @@ in {
       default = "/var/lib/traefik";
       type = types.path;
       description = ''
-      Location for any persistent data traefik creates, ie. acme
+        Location for any persistent data traefik creates, ie. acme
       '';
     };
 
@@ -84,16 +130,15 @@ in {
   };
 
   config = mkIf cfg.enable {
-    systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' 0700 traefik traefik - -"
-    ];
+    systemd.tmpfiles.rules = [ "d '${cfg.dataDir}' 0700 traefik traefik - -" ];
 
     systemd.services.traefik = {
       description = "Traefik web server";
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = ''${cfg.package.bin}/bin/traefik --configfile=${configFile}'';
+        ExecStart =
+          "${cfg.package}/bin/traefik --configfile=${staticConfigFile}";
         Type = "simple";
         User = "traefik";
         Group = cfg.group;
@@ -120,6 +165,6 @@ in {
       isSystemUser = true;
     };
 
-    users.groups.traefik = {};
+    users.groups.traefik = { };
   };
 }
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 32f6d475b34..f8a18954fc9 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -85,7 +85,7 @@ in {
     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";
@@ -93,34 +93,52 @@ in {
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [ curl ];
       preStart = ''
-        test -f '/run/unit/control.unit.sock' || rm -f '/run/unit/control.unit.sock'
+        test -f '${cfg.stateDir}/conf.json' || rm -f '${cfg.stateDir}/conf.json'
       '';
       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}
         '';
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Capabilities
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SETGID" "CAP_SETUID" ];
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "full";
+        ProtectHome = true;
         RuntimeDirectory = "unit";
         RuntimeDirectoryMode = "0750";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
       };
     };
 
-    users.users = optionalAttrs (cfg.user == "unit") (singleton {
-      name = "unit";
-      group = cfg.group;
-      isSystemUser = true;
-    });
+    users.users = optionalAttrs (cfg.user == "unit") {
+      unit = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "unit") {
+      unit = { };
+    };
 
-    users.groups = optionalAttrs (cfg.group == "unit") (singleton {
-      name = "unit";
-    });
   };
 }
diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
index af70f32f32d..4b74c329e3d 100644
--- a/nixos/modules/services/web-servers/uwsgi.nix
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -5,10 +5,6 @@ with lib;
 let
   cfg = config.services.uwsgi;
 
-  uwsgi = pkgs.uwsgi.override {
-    plugins = cfg.plugins;
-  };
-
   buildCfg = name: c:
     let
       plugins =
@@ -23,8 +19,8 @@ let
       python =
         if hasPython2 && hasPython3 then
           throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
-        else if hasPython2 then uwsgi.python2
-        else if hasPython3 then uwsgi.python3
+        else if hasPython2 then cfg.package.python2
+        else if hasPython3 then cfg.package.python3
         else null;
 
       pythonEnv = python.withPackages (c.pythonPackages or (self: []));
@@ -36,7 +32,7 @@ let
               inherit plugins;
             } // removeAttrs c [ "type" "pythonPackages" ]
               // optionalAttrs (python != null) {
-                pythonpath = "${pythonEnv}/${python.sitePackages}";
+                pyhome = "${pythonEnv}";
                 env =
                   # Argh, uwsgi expects list of key-values there instead of a dictionary.
                   let env' = c.env or [];
@@ -77,6 +73,11 @@ in {
         description = "Where uWSGI communication sockets can live";
       };
 
+      package = mkOption {
+        type = types.package;
+        internal = true;
+      };
+
       instance = mkOption {
         type = types.attrs;
         default = {
@@ -138,7 +139,7 @@ in {
       '';
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${uwsgi}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
+        ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
         NotifyAccess = "main";
@@ -146,15 +147,19 @@ in {
       };
     };
 
-    users.users = optionalAttrs (cfg.user == "uwsgi") (singleton
-      { name = "uwsgi";
+    users.users = optionalAttrs (cfg.user == "uwsgi") {
+      uwsgi = {
         group = cfg.group;
         uid = config.ids.uids.uwsgi;
-      });
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "uwsgi") {
+      uwsgi.gid = config.ids.gids.uwsgi;
+    };
 
-    users.groups = optionalAttrs (cfg.group == "uwsgi") (singleton
-      { name = "uwsgi";
-        gid = config.ids.gids.uwsgi;
-      });
+    services.uwsgi.package = pkgs.uwsgi.override {
+      inherit (cfg) plugins;
+    };
   };
 }
diff --git a/nixos/modules/services/x11/desktop-managers/cde.nix b/nixos/modules/services/x11/desktop-managers/cde.nix
new file mode 100644
index 00000000000..c1b6d3bf064
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/cde.nix
@@ -0,0 +1,55 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.cde;
+in {
+  options.services.xserver.desktopManager.cde = {
+    enable = mkEnableOption "Common Desktop Environment";
+  };
+
+  config = mkIf (xcfg.enable && cfg.enable) {
+    services.rpcbind.enable = true;
+
+    services.xinetd.enable = true;
+    services.xinetd.services = [
+      {
+        name = "cmsd";
+        protocol = "udp";
+        user = "root";
+        server = "${pkgs.cdesktopenv}/opt/dt/bin/rpc.cmsd";
+        extraConfig = ''
+          type  = RPC UNLISTED
+          rpc_number  = 100068
+          rpc_version = 2-5
+          only_from   = 127.0.0.1/0
+        '';
+      }
+    ];
+
+    users.groups.mail = {};
+    security.wrappers = {
+      dtmail = {
+        source = "${pkgs.cdesktopenv}/bin/dtmail";
+        group = "mail";
+        setgid = true;
+      };
+    };
+
+    system.activationScripts.setup-cde = ''
+      mkdir -p /var/dt/{tmp,appconfig/appmanager}
+      chmod a+w+t /var/dt/{tmp,appconfig/appmanager}
+    '';
+
+    services.xserver.desktopManager.session = [
+    { name = "CDE";
+      start = ''
+        exec ${pkgs.cdesktopenv}/opt/dt/bin/Xsession
+      '';
+    }];
+  };
+
+  meta.maintainers = [ maintainers.gnidorah ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 671a959cdde..ea6aac9f6c9 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 ./pantheon.nix ./surf-display.nix
+    ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
   ];
 
   options = {
@@ -68,46 +68,31 @@ in
           scripts before forwarding the value to the
           <varname>displayManager</varname>.
         '';
-        apply = list: {
-          list = map (d: d // {
-            manage = "desktop";
-            start = d.start
-            + optionalString (needBGCond d) ''
-              if [ -e $HOME/.background-image ]; then
-                ${pkgs.feh}/bin/feh --bg-${cfg.wallpaper.mode} ${optionalString cfg.wallpaper.combineScreens "--no-xinerama"} $HOME/.background-image
-              else
-                # Use a solid black background as fallback
-                ${pkgs.xorg.xsetroot}/bin/xsetroot -solid black
-              fi
-            '';
-          }) list;
-          needBGPackages = [] != filter needBGCond list;
-        };
+        apply = map (d: d // {
+          manage = "desktop";
+          start = d.start
+          + optionalString (needBGCond d) ''
+            if [ -e $HOME/.background-image ]; then
+              ${pkgs.feh}/bin/feh --bg-${cfg.wallpaper.mode} ${optionalString cfg.wallpaper.combineScreens "--no-xinerama"} $HOME/.background-image
+            fi
+          '';
+        });
       };
 
       default = mkOption {
-        type = types.str;
-        default = "";
+        type = types.nullOr types.str;
+        default = null;
         example = "none";
-        description = "Default desktop manager loaded if none have been chosen.";
-        apply = defaultDM:
-          if defaultDM == "" && cfg.session.list != [] then
-            (head cfg.session.list).name
-          else if any (w: w.name == defaultDM) cfg.session.list then
-            defaultDM
-          else
-            builtins.trace ''
-              Default desktop manager (${defaultDM}) not found at evaluation time.
-              These are the known valid session names:
-                ${concatMapStringsSep "\n  " (w: "services.xserver.desktopManager.default = \"${w.name}\";") cfg.session.list}
-              It's also possible the default can be found in one of these packages:
-                ${concatMapStringsSep "\n  " (p: p.name) config.services.xserver.displayManager.extraSessionFilePackages}
-            '' defaultDM;
+        description = ''
+          <emphasis role="strong">Deprecated</emphasis>, please use <xref linkend="opt-services.xserver.displayManager.defaultSession"/> instead.
+
+          Default desktop manager loaded if none have been chosen.
+        '';
       };
 
     };
 
   };
 
-  config.services.xserver.displayManager.session = cfg.session.list;
+  config.services.xserver.displayManager.session = cfg.session;
 }
diff --git a/nixos/modules/services/x11/desktop-managers/enlightenment.nix b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
index 3745069f6ea..1690a7d51a8 100644
--- a/nixos/modules/services/x11/desktop-managers/enlightenment.nix
+++ b/nixos/modules/services/x11/desktop-managers/enlightenment.nix
@@ -16,9 +16,14 @@ let
 in
 
 {
+  imports = [
+    (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "e19" "enable" ] [ "services" "xserver" "desktopManager" "enlightenment" "enable" ])
+  ];
+
   options = {
 
     services.xserver.desktopManager.enlightenment.enable = mkOption {
+      type = types.bool;
       default = false;
       description = "Enable the Enlightenment desktop environment.";
     };
@@ -34,7 +39,7 @@ in
       pkgs.gtk2 # To get GTK's themes.
       pkgs.tango-icon-theme
 
-      pkgs.gnome2.gnome_icon_theme
+      pkgs.gnome-icon-theme
       pkgs.xorg.xcursorthemes
     ];
 
@@ -64,10 +69,7 @@ in
 
     security.wrappers = (import "${e.enlightenment}/e-wrappers.nix").security.wrappers;
 
-    environment.etc = singleton
-      { source = xcfg.xkbDir;
-        target = "X11/xkb";
-      };
+    environment.etc."X11/xkb".source = xcfg.xkbDir;
 
     fonts.fonts = [ pkgs.dejavu_fonts pkgs.ubuntu_font_family ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index 6725595e1cf..bbc7feb2d04 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -57,6 +57,10 @@ in
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   options = {
 
     services.gnome3 = {
@@ -68,6 +72,7 @@ in
 
     services.xserver.desktopManager.gnome3 = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = "Enable Gnome 3 desktop manager.";
       };
@@ -144,7 +149,7 @@ in
       services.gnome3.core-shell.enable = true;
       services.gnome3.core-utilities.enable = mkDefault true;
 
-      services.xserver.displayManager.extraSessionFilePackages = [ pkgs.gnome3.gnome-session ];
+      services.xserver.displayManager.sessionPackages = [ pkgs.gnome3.gnome-session.sessions ];
 
       environment.extraInit = ''
         ${concatMapStrings (p: ''
@@ -171,7 +176,7 @@ in
     })
 
     (mkIf flashbackEnabled {
-      services.xserver.displayManager.extraSessionFilePackages =  map
+      services.xserver.displayManager.sessionPackages =  map
         (wm: pkgs.gnome3.gnome-flashback.mkSessionForWm {
           inherit (wm) wmName wmLabel wmCommand;
         }) (optional cfg.flashback.enableMetacity {
@@ -180,7 +185,7 @@ in
               wmCommand = "${pkgs.gnome3.metacity}/bin/metacity";
             } ++ cfg.flashback.customSessions);
 
-      security.pam.services.gnome-screensaver = {
+      security.pam.services.gnome-flashback = {
         enableGnomeKeyring = true;
       };
 
@@ -191,9 +196,10 @@ in
           inherit (wm) wmName;
         }) cfg.flashback.customSessions);
 
-      services.dbus.packages = [
-        pkgs.gnome3.gnome-screensaver
-      ];
+        # gnome-panel needs these for menu applet
+        environment.sessionVariables.XDG_DATA_DIRS = [ "${pkgs.gnome3.gnome-flashback}/share" ];
+        # TODO: switch to sessionVariables (resolve conflict)
+        environment.variables.XDG_CONFIG_DIRS = [ "${pkgs.gnome3.gnome-flashback}/etc/xdg" ];
     })
 
     (mkIf serviceCfg.core-os-services.enable {
@@ -249,11 +255,16 @@ in
       services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
       services.telepathy.enable = mkDefault true;
 
-      systemd.packages = with pkgs.gnome3; [ vino gnome-session ];
+      systemd.packages = with pkgs.gnome3; [
+        gnome-session
+        gnome-shell
+      ];
 
       services.avahi.enable = mkDefault true;
 
-      xdg.portal.extraPortals = [ pkgs.gnome3.gnome-shell ];
+      xdg.portal.extraPortals = [
+        pkgs.gnome3.gnome-shell
+      ];
 
       services.geoclue2.enable = mkDefault true;
       services.geoclue2.enableDemoAgent = false; # GNOME has its own geoclue agent
@@ -298,7 +309,7 @@ in
         environment = mkForce {};
       };
 
-      # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-32/elements/core/meta-gnome-core-shell.bst
+      # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-36/elements/core/meta-gnome-core-shell.bst
       environment.systemPackages = with pkgs.gnome3; [
         adwaita-icon-theme
         gnome-backgrounds
@@ -317,18 +328,16 @@ in
         pkgs.hicolor-icon-theme
         pkgs.shared-mime-info # for update-mime-database
         pkgs.xdg-user-dirs # Update user dirs as described in http://freedesktop.org/wiki/Software/xdg-user-dirs/
-        vino
       ];
     })
 
-    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-32/elements/core/meta-gnome-core-utilities.bst
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-36/elements/core/meta-gnome-core-utilities.bst
     (mkIf serviceCfg.core-utilities.enable {
       environment.systemPackages = (with pkgs.gnome3; removePackagesByName [
         baobab
         cheese
         eog
         epiphany
-        geary
         gedit
         gnome-calculator
         gnome-calendar
@@ -355,6 +364,7 @@ in
       # Enable default programs
       programs.evince.enable = mkDefault true;
       programs.file-roller.enable = mkDefault true;
+      programs.geary.enable = mkDefault true;
       programs.gnome-disks.enable = mkDefault true;
       programs.gnome-terminal.enable = mkDefault true;
       programs.seahorse.enable = mkDefault true;
diff --git a/nixos/modules/services/x11/desktop-managers/kodi.nix b/nixos/modules/services/x11/desktop-managers/kodi.nix
index 65a7b9c628e..bdae9c3afdb 100644
--- a/nixos/modules/services/x11/desktop-managers/kodi.nix
+++ b/nixos/modules/services/x11/desktop-managers/kodi.nix
@@ -10,6 +10,7 @@ in
   options = {
     services.xserver.desktopManager.kodi = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = "Enable the kodi multimedia center.";
       };
@@ -20,7 +21,7 @@ in
     services.xserver.desktopManager.session = [{
       name = "kodi";
       start = ''
-        ${pkgs.kodi}/bin/kodi --lircdev /run/lirc/lircd --standalone &
+        LIRC_SOCKET_PATH=/run/lirc/lircd ${pkgs.kodi}/bin/kodi --standalone &
         waitPID=$!
       '';
     }];
diff --git a/nixos/modules/services/x11/desktop-managers/mate.nix b/nixos/modules/services/x11/desktop-managers/mate.nix
index fe63f36cf96..f236c14fcf3 100644
--- a/nixos/modules/services/x11/desktop-managers/mate.nix
+++ b/nixos/modules/services/x11/desktop-managers/mate.nix
@@ -44,35 +44,35 @@ in
 
   config = mkIf cfg.enable {
 
-    services.xserver.desktopManager.session = singleton {
-      name = "mate";
-      bgSupport = true;
-      start = ''
-        export XDG_MENU_PREFIX=mate-
+    services.xserver.displayManager.sessionPackages = [
+      pkgs.mate.mate-session-manager
+    ];
 
-        # Let caja find extensions
-        export CAJA_EXTENSION_DIRS=$CAJA_EXTENSION_DIRS''${CAJA_EXTENSION_DIRS:+:}${config.system.path}/lib/caja/extensions-2.0
+    services.xserver.displayManager.sessionCommands = ''
+      if test "$XDG_CURRENT_DESKTOP" = "MATE"; then
+          export XDG_MENU_PREFIX=mate-
 
-        # Let caja extensions find gsettings schemas
-        ${concatMapStrings (p: ''
+          # Let caja find extensions
+          export CAJA_EXTENSION_DIRS=$CAJA_EXTENSION_DIRS''${CAJA_EXTENSION_DIRS:+:}${config.system.path}/lib/caja/extensions-2.0
+
+          # Let caja extensions find gsettings schemas
+          ${concatMapStrings (p: ''
           if [ -d "${p}/lib/caja/extensions-2.0" ]; then
-            ${addToXDGDirs p}
+              ${addToXDGDirs p}
           fi
-          '')
-          config.environment.systemPackages
-        }
+          '') config.environment.systemPackages}
 
-        # Let mate-panel find applets
-        export MATE_PANEL_APPLETS_DIR=$MATE_PANEL_APPLETS_DIR''${MATE_PANEL_APPLETS_DIR:+:}${config.system.path}/share/mate-panel/applets
-        export MATE_PANEL_EXTRA_MODULES=$MATE_PANEL_EXTRA_MODULES''${MATE_PANEL_EXTRA_MODULES:+:}${config.system.path}/lib/mate-panel/applets
+          # 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}
+      fi
+    '';
 
-        # 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}
+    # Let mate-panel find applets
+    environment.sessionVariables."MATE_PANEL_APPLETS_DIR" = "${config.system.path}/share/mate-panel/applets";
+    environment.sessionVariables."MATE_PANEL_EXTRA_MODULES" = "${config.system.path}/lib/mate-panel/applets";
 
-        ${pkgs.mate.mate-session-manager}/bin/mate-session ${optionalString cfg.debug "--debug"} &
-        waitPID=$!
-      '';
-    };
+    # Debugging
+    environment.sessionVariables.MATE_SESSION_DEBUG = mkIf cfg.debug "1";
 
     environment.systemPackages =
       pkgs.mate.basePackages ++
@@ -86,6 +86,7 @@ in
         pkgs.shared-mime-info
         pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
         pkgs.mate.mate-settings-daemon
+        pkgs.yelp # for 'Contents' in 'Help' menus
       ];
 
     programs.dconf.enable = true;
@@ -98,7 +99,6 @@ in
 
     services.gnome3.at-spi2-core.enable = true;
     services.gnome3.gnome-keyring.enable = true;
-    services.gnome3.gnome-settings-daemon.enable = true;
     services.udev.packages = [ pkgs.mate.mate-settings-daemon ];
     services.gvfs.enable = true;
     services.upower.enable = config.powerManagement.enable;
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 80dab135ee2..01fe230b8a4 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -5,6 +5,7 @@ with lib;
 let
 
   cfg = config.services.xserver.desktopManager.pantheon;
+  serviceCfg = config.services.pantheon;
 
   nixos-gsettings-desktop-schemas = pkgs.pantheon.elementary-gsettings-schemas.override {
     extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
@@ -15,10 +16,23 @@ in
 
 {
 
-  meta.maintainers = pkgs.pantheon.maintainers;
+  meta = {
+    doc = ./pantheon.xml;
+    maintainers = pkgs.pantheon.maintainers;
+  };
 
   options = {
 
+    services.pantheon = {
+
+      contractor = {
+         enable = mkEnableOption "contractor, a desktop-wide extension service used by Pantheon";
+      };
+
+      apps.enable = mkEnableOption "Pantheon default applications";
+
+    };
+
     services.xserver.desktopManager.pantheon = {
       enable = mkOption {
         type = types.bool;
@@ -41,6 +55,18 @@ in
         ];
       };
 
+      extraWingpanelIndicators = mkOption {
+        default = null;
+        type = with types; nullOr (listOf package);
+        description = "Indicators to add to Wingpanel.";
+      };
+
+      extraSwitchboardPlugs = mkOption {
+        default = null;
+        type = with types; nullOr (listOf package);
+        description = "Plugs to add to Switchboard.";
+      };
+
       extraGSettingsOverrides = mkOption {
         default = "";
         type = types.lines;
@@ -67,148 +93,204 @@ in
   };
 
 
-  config = mkIf cfg.enable {
+  config = mkMerge [
+    (mkIf cfg.enable {
 
-    services.xserver.displayManager.extraSessionFilePackages = [ pkgs.pantheon.elementary-session-settings ];
+      services.xserver.displayManager.sessionPackages = [ 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
+      # Ensure lightdm is used when Pantheon is enabled
+      # Without it screen locking will be nonfunctional because of the use of lightlocker
+      warnings = optional (config.services.xserver.displayManager.lightdm.enable != true)
+        ''
+          Using Pantheon without LightDM as a displayManager will break screenlocking from the UI.
+        '';
 
-    warnings = optional (config.services.xserver.displayManager.lightdm.enable != true)
-      ''
-        Using Pantheon without LightDM as a displayManager will break screenlocking from the UI.
+      services.xserver.displayManager.lightdm.greeters.pantheon.enable = mkDefault true;
+
+      # Without this, elementary LightDM greeter will pre-select non-existent `default` session
+      # https://github.com/elementary/greeter/issues/368
+      services.xserver.displayManager.defaultSession = "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}
+        fi
       '';
 
-    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}
-      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.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
-    services.dbus.packages = with pkgs.pantheon; [
-      switchboard-plug-power
-      elementary-default-settings
-    ];
-    services.pantheon.contractor.enable = mkDefault true;
-    services.gnome3.at-spi2-core.enable = true;
-    services.gnome3.evolution-data-server.enable = true;
-    services.gnome3.glib-networking.enable = 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.udev.packages = [ pkgs.pantheon.elementary-settings-daemon ];
-    services.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;
-    services.geoclue2.enable = mkDefault true;
-    # pantheon has pantheon-agent-geoclue2
-    services.geoclue2.enableDemoAgent = false;
-    services.geoclue2.appConfig."io.elementary.desktop.agent-geoclue2" = {
-      isAllowed = true;
-      isSystem = true;
-    };
-
-    programs.dconf.enable = true;
-    programs.evince.enable = mkDefault true;
-    programs.file-roller.enable = mkDefault true;
-    # Otherwise you can't store NetworkManager Secrets with
-    # "Store the password only for this user"
-    programs.nm-applet.enable = true;
-
-    # Shell integration for VTE terminals
-    programs.bash.vteIntegration = mkDefault true;
-    programs.zsh.vteIntegration = mkDefault true;
-
-    # Harmonize Qt5 applications under Pantheon
-    qt5.enable = true;
-    qt5.platformTheme = "gnome";
-    qt5.style = "adwaita";
-
-    networking.networkmanager.enable = mkDefault true;
-
-    # Override GSettings schemas
-    environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
-
-    environment.sessionVariables.GNOME_SESSION_DEBUG = optionalString cfg.debug "1";
-
-    # Settings from elementary-default-settings
-    environment.sessionVariables.GTK_CSD = "1";
-    environment.sessionVariables.GTK_MODULES = "pantheon-filechooser-module";
-    environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
-
-    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
-      ] ++ pantheon.apps) config.environment.pantheon.excludePackages)
-      ++ (with pkgs;
-      [
-        adwaita-qt
+      # Default services
+      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.tumbler.enable = mkDefault true;
+      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+      services.dbus.packages = with pkgs.pantheon; [
+        switchboard-plug-power
+        elementary-default-settings # accountsservice extensions
+      ];
+      services.pantheon.apps.enable = mkDefault true;
+      services.pantheon.contractor.enable = mkDefault true;
+      services.gnome3.at-spi2-core.enable = true;
+      services.gnome3.evolution-data-server.enable = true;
+      services.gnome3.glib-networking.enable = true;
+      services.gnome3.gnome-keyring.enable = true;
+      services.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;
+      services.geoclue2.enable = mkDefault true;
+      # pantheon has pantheon-agent-geoclue2
+      services.geoclue2.enableDemoAgent = false;
+      services.geoclue2.appConfig."io.elementary.desktop.agent-geoclue2" = {
+        isAllowed = true;
+        isSystem = true;
+      };
+      # Use gnome-settings-daemon fork
+      services.udev.packages = [
+        pkgs.pantheon.elementary-settings-daemon
+      ];
+      systemd.packages = [
+        pkgs.pantheon.elementary-settings-daemon
+      ];
+      programs.dconf.enable = true;
+      networking.networkmanager.enable = mkDefault true;
+
+      # Global environment
+      environment.systemPackages = with pkgs; [
         desktop-file-utils
         glib
-        glib-networking
         gnome-menus
         gnome3.adwaita-icon-theme
         gtk3.out
         hicolor-icon-theme
         lightlocker
         onboard
-        plank
         qgnomeplatform
         shared-mime-info
         sound-theme-freedesktop
         xdg-user-dirs
-      ]);
+      ] ++ (with pkgs.pantheon; [
+        # Artwork
+        elementary-gtk-theme
+        elementary-icon-theme
+        elementary-sound-theme
+        elementary-wallpapers
+
+        # Desktop
+        elementary-default-settings
+        elementary-dock
+        elementary-session-settings
+        elementary-shortcut-overlay
+        gala
+        (switchboard-with-plugs.override {
+          plugs = cfg.extraSwitchboardPlugs;
+        })
+        (wingpanel-with-indicators.override {
+          indicators = cfg.extraWingpanelIndicators;
+        })
+
+        # Services
+        elementary-capnet-assist
+        elementary-dpms-helper
+        elementary-notifications
+        elementary-settings-daemon
+        pantheon-agent-geoclue2
+        pantheon-agent-polkit
+      ]) ++ (gnome3.removePackagesByName [
+        gnome3.geary
+        gnome3.epiphany
+        gnome3.gnome-font-viewer
+      ] config.environment.pantheon.excludePackages);
 
-    fonts.fonts = with pkgs; [
-      open-sans
-      roboto-mono
-      pantheon.elementary-redacted-script # needed by screenshot-tool
-    ];
+      programs.evince.enable = mkDefault true;
+      programs.file-roller.enable = mkDefault true;
 
-    fonts.fontconfig.defaultFonts = {
-      monospace = [ "Roboto Mono" ];
-      sansSerif = [ "Open Sans" ];
-    };
+      # Settings from elementary-default-settings
+      environment.sessionVariables.GTK_CSD = "1";
+      environment.sessionVariables.GTK3_MODULES = [ "pantheon-filechooser-module" ];
+      environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
 
-  };
+      # Override GSettings schemas
+      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+      environment.sessionVariables.GNOME_SESSION_DEBUG = mkIf cfg.debug "1";
+
+      environment.pathsToLink = [
+        # FIXME: modules should link subdirs of `/share` rather than relying on this
+        "/share"
+      ];
+
+      # Otherwise you can't store NetworkManager Secrets with
+      # "Store the password only for this user"
+      programs.nm-applet.enable = true;
 
+      # Shell integration for VTE terminals
+      programs.bash.vteIntegration = mkDefault true;
+      programs.zsh.vteIntegration = mkDefault true;
+
+      # Harmonize Qt5 applications under Pantheon
+      qt5.enable = true;
+      qt5.platformTheme = "gnome";
+      qt5.style = "adwaita";
+
+      # Default Fonts
+      fonts.fonts = with pkgs; [
+        open-sans
+        roboto-mono
+      ];
+
+      fonts.fontconfig.defaultFonts = {
+        monospace = [ "Roboto Mono" ];
+        sansSerif = [ "Open Sans" ];
+      };
+    })
+
+    (mkIf serviceCfg.apps.enable {
+      environment.systemPackages = (with pkgs.pantheon; pkgs.gnome3.removePackagesByName [
+        elementary-calculator
+        elementary-calendar
+        elementary-camera
+        elementary-code
+        elementary-files
+        elementary-music
+        elementary-photos
+        elementary-screenshot-tool
+        elementary-terminal
+        elementary-videos
+      ] config.environment.pantheon.excludePackages);
+
+      # needed by screenshot-tool
+      fonts.fonts = [
+        pkgs.pantheon.elementary-redacted-script
+      ];
+    })
+
+    (mkIf serviceCfg.contractor.enable {
+      environment.systemPackages = with  pkgs.pantheon; [
+        contractor
+        extra-elementary-contracts
+      ];
+
+      environment.pathsToLink = [
+        "/share/contractor"
+      ];
+    })
+
+  ];
 }
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
new file mode 100644
index 00000000000..9541f2cfd4e
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.xml
@@ -0,0 +1,114 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xml:id="chap-pantheon">
+ <title>Pantheon Destkop</title>
+ <para>
+  Pantheon is the desktop environment created for the elementary OS distribution. It is written from scratch in Vala, utilizing GNOME technologies with GTK 3 and Granite.
+ </para>
+ <section xml:id="sec-pantheon-enable">
+  <title>Enabling Pantheon</title>
+
+  <para>
+   All of Pantheon is working in NixOS and the applications should be available, aside from a few <link xlink:href="https://github.com/NixOS/nixpkgs/issues/58161">exceptions</link>. To enable Pantheon, set
+<programlisting>
+<xref linkend="opt-services.xserver.desktopManager.pantheon.enable"/> = true;
+</programlisting>
+   This automatically enables LightDM and Pantheon's LightDM greeter. If you'd like to disable this, set
+<programlisting>
+<xref linkend="opt-services.xserver.displayManager.lightdm.greeters.pantheon.enable"/> = false;
+<xref linkend="opt-services.xserver.displayManager.lightdm.enable"/> = false;
+</programlisting>
+   but please be aware using Pantheon without LightDM as a display manager will break screenlocking from the UI. The NixOS module for Pantheon installs all of Pantheon's default applications. If you'd like to not install Pantheon's apps, set
+<programlisting>
+<xref linkend="opt-services.pantheon.apps.enable"/> = false;
+</programlisting>
+   You can also use <xref linkend="opt-environment.pantheon.excludePackages"/> to remove any other app (like <package>geary</package>).
+  </para>
+ </section>
+ <section xml:id="sec-pantheon-wingpanel-switchboard">
+  <title>Wingpanel and Switchboard plugins</title>
+
+  <para>
+   Wingpanel and Switchboard work differently than they do in other distributions, as far as using plugins. You cannot install a plugin globally (like with <option>environment.systemPackages</option>) to start using it. You should instead be using the following options:
+   <itemizedlist>
+    <listitem>
+     <para>
+      <xref linkend="opt-services.xserver.desktopManager.pantheon.extraWingpanelIndicators"/>
+     </para>
+    </listitem>
+    <listitem>
+     <para>
+      <xref linkend="opt-services.xserver.desktopManager.pantheon.extraSwitchboardPlugs"/>
+     </para>
+    </listitem>
+   </itemizedlist>
+   to configure the programs with plugs or indicators.
+  </para>
+
+  <para>
+   The difference in NixOS is both these programs are patched to load plugins from a directory that is the value of an environment variable. All of which is controlled in Nix. If you need to configure the particular packages manually you can override the packages like:
+<programlisting>
+wingpanel-with-indicators.override {
+  indicators = [
+    pkgs.some-special-indicator
+  ];
+};
+
+switchboard-with-plugs.override {
+  plugs = [
+    pkgs.some-special-plug
+  ];
+};
+</programlisting>
+   please note that, like how the NixOS options describe these as extra plugins, this would only add to the default plugins included with the programs. If for some reason you'd like to configure which plugins to use exactly, both packages have an argument for this:
+<programlisting>
+wingpanel-with-indicators.override {
+  useDefaultIndicators = false;
+  indicators = specialListOfIndicators;
+};
+
+switchboard-with-plugs.override {
+  useDefaultPlugs = false;
+  plugs = specialListOfPlugs;
+};
+</programlisting>
+   this could be most useful for testing a particular plug-in in isolation.
+  </para>
+ </section>
+ <section xml:id="sec-pantheon-faq">
+  <title>FAQ</title>
+
+  <variablelist>
+   <varlistentry xml:id="sec-pantheon-faq-messed-up-theme">
+    <term>
+     I have switched from a different desktop and Pantheon’s theming looks messed up.
+    </term>
+    <listitem>
+     <para>
+      Open Switchboard and go to: <guilabel>Administration</guilabel> → <guilabel>About</guilabel> → <guilabel>Restore Default Settings</guilabel> → <guibutton>Restore Settings</guibutton>. This will reset any dconf settings to their Pantheon defaults. Note this could reset certain GNOME specific preferences if that desktop was used prior.
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry xml:id="sec-pantheon-faq-gnome3-and-pantheon">
+    <term>
+     I cannot enable both GNOME 3 and Pantheon.
+    </term>
+    <listitem>
+     <para>
+      This is a known <link xlink:href="https://github.com/NixOS/nixpkgs/issues/64611">issue</link> and there is no known workaround.
+     </para>
+    </listitem>
+   </varlistentry>
+   <varlistentry xml:id="sec-pantheon-faq-appcenter">
+    <term>
+     Does AppCenter work, or is it available?
+    </term>
+    <listitem>
+     <para>
+      AppCenter has been available since 20.03, but it is of little use. This is because there is no functioning PackageKit backend for Nix 2.0. In the near future you will be able to install Flatpak applications from AppCenter on NixOS. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/70214">issue</link>.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index da8bdcb78c4..60ef0159ff1 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -8,6 +8,137 @@ let
   cfg = xcfg.desktopManager.plasma5;
 
   inherit (pkgs) kdeApplications plasma5 libsForQt5 qt5;
+  inherit (pkgs) writeText;
+
+  pulseaudio = config.hardware.pulseaudio;
+  pactl = "${getBin pulseaudio.package}/bin/pactl";
+  startplasma-x11 = "${getBin plasma5.plasma-workspace}/bin/startplasma-x11";
+  sed = "${getBin pkgs.gnused}/bin/sed";
+
+  gtkrc2 = writeText "gtkrc-2.0" ''
+    # Default GTK+ 2 config for NixOS Plasma 5
+    include "/run/current-system/sw/share/themes/Breeze/gtk-2.0/gtkrc"
+    style "user-font"
+    {
+      font_name="Sans Serif Regular"
+    }
+    widget_class "*" style "user-font"
+    gtk-font-name="Sans Serif Regular 10"
+    gtk-theme-name="Breeze"
+    gtk-icon-theme-name="breeze"
+    gtk-fallback-icon-theme="hicolor"
+    gtk-cursor-theme-name="breeze_cursors"
+    gtk-toolbar-style=GTK_TOOLBAR_ICONS
+    gtk-menu-images=1
+    gtk-button-images=1
+  '';
+
+  gtk3_settings = writeText "settings.ini" ''
+    [Settings]
+    gtk-font-name=Sans Serif Regular 10
+    gtk-theme-name=Breeze
+    gtk-icon-theme-name=breeze
+    gtk-fallback-icon-theme=hicolor
+    gtk-cursor-theme-name=breeze_cursors
+    gtk-toolbar-style=GTK_TOOLBAR_ICONS
+    gtk-menu-images=1
+    gtk-button-images=1
+  '';
+
+  kcminputrc = writeText "kcminputrc" ''
+    [Mouse]
+    cursorTheme=breeze_cursors
+    cursorSize=0
+  '';
+
+  activationScript = ''
+    ${set_XDG_CONFIG_HOME}
+
+    # The KDE icon cache is supposed to update itself automatically, but it uses
+    # the timestamp on the icon theme directory as a trigger. This doesn't work
+    # on NixOS because the timestamp never changes. As a workaround, delete the
+    # icon cache at login and session activation.
+    # See also: 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 ''${XDG_CONFIG_HOME}/menus/applications-merged/xdg-desktop-menu-dummy.menu
+
+    # Qt writes a weird ‘libraryPath’ line to
+    # ~/.config/Trolltech.conf that causes the KDE plugin
+    # paths of previous KDE invocations to be searched.
+    # Obviously using mismatching KDE libraries is potentially
+    # disastrous, so here we nuke references to the Nix store
+    # in Trolltech.conf.  A better solution would be to stop
+    # Qt from doing this wackiness in the first place.
+    trolltech_conf="''${XDG_CONFIG_HOME}/Trolltech.conf"
+    if [ -e "$trolltech_conf" ]; then
+        ${sed} -i "$trolltech_conf" -e '/nix\\store\|nix\/store/ d'
+    fi
+
+    # 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
+  '';
+
+  set_XDG_CONFIG_HOME = ''
+      # Set the default XDG_CONFIG_HOME if it is unset.
+      # Per the XDG Base Directory Specification:
+      # https://specifications.freedesktop.org/basedir-spec/latest
+      # 1. Never export this variable! If it is unset, then child processes are
+      # expected to set the default themselves.
+      # 2. Contaminate / if $HOME is unset; do not check if $HOME is set.
+      XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
+  '';
+
+  startplasma =
+    ''
+      ${set_XDG_CONFIG_HOME}
+      mkdir -p "''${XDG_CONFIG_HOME}"
+
+    ''
+    + optionalString pulseaudio.enable ''
+      # Load PulseAudio module for routing support.
+      # See also: http://colin.guthr.ie/2009/10/so-how-does-the-kde-pulseaudio-support-work-anyway/
+        ${pactl} load-module module-device-manager "do_routing=1"
+
+    ''
+    + ''
+      ${activationScript}
+
+      # Create default configurations if Plasma has never been started.
+      kdeglobals="''${XDG_CONFIG_HOME}/kdeglobals"
+      if ! [ -f "$kdeglobals" ]
+      then
+          kcminputrc="''${XDG_CONFIG_HOME}/kcminputrc"
+          if ! [ -f "$kcminputrc" ]
+          then
+              cat ${kcminputrc} >"$kcminputrc"
+          fi
+
+          gtkrc2="$HOME/.gtkrc-2.0"
+          if ! [ -f "$gtkrc2" ]
+          then
+              cat ${gtkrc2} >"$gtkrc2"
+          fi
+
+          gtk3_settings="''${XDG_CONFIG_HOME}/gtk-3.0/settings.ini"
+          if ! [ -f "$gtk3_settings" ]
+          then
+              mkdir -p "$(dirname "$gtk3_settings")"
+              cat ${gtk3_settings} >"$gtk3_settings"
+          fi
+      fi
+
+    ''
+    + ''
+      exec "${startplasma-x11}"
+    '';
 
 in
 
@@ -33,6 +164,7 @@ in
 
   imports = [
     (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "enableQt4Support" ] "Phonon no longer supports Qt 4.")
+    (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "kde5" ] [ "services" "xserver" "desktopManager" "plasma5" ])
   ];
 
   config = mkMerge [
@@ -40,27 +172,7 @@ in
       services.xserver.desktopManager.session = singleton {
         name = "plasma5";
         bgSupport = true;
-        start = ''
-          # Load PulseAudio module for routing support.
-          # See http://colin.guthr.ie/2009/10/so-how-does-the-kde-pulseaudio-support-work-anyway/
-          ${optionalString config.hardware.pulseaudio.enable ''
-            ${getBin config.hardware.pulseaudio.package}/bin/pactl load-module module-device-manager "do_routing=1"
-          ''}
-
-          if [ -f "$HOME/.config/kdeglobals" ]
-          then
-              # Remove extraneous font style names.
-              # See also: https://phabricator.kde.org/D9070
-              ${getBin pkgs.gnused}/bin/sed -i "$HOME/.config/kdeglobals" \
-                  -e '/^fixed=/ s/,Regular$//' \
-                  -e '/^font=/ s/,Regular$//' \
-                  -e '/^menuFont=/ s/,Regular$//' \
-                  -e '/^smallestReadableFont=/ s/,Regular$//' \
-                  -e '/^toolBarFont=/ s/,Regular$//'
-          fi
-
-          exec "${getBin plasma5.plasma-workspace}/bin/startkde"
-        '';
+        start = startplasma;
       };
 
       security.wrappers = {
@@ -136,6 +248,7 @@ in
           libkscreen
           libksysguard
           milou
+          plasma-browser-integration
           plasma-integration
           polkit-kde-agent
           systemsettings
@@ -169,7 +282,7 @@ in
         ++ lib.optional (cfg.phononBackend == "vlc") libsForQt5.phonon-backend-vlc
 
         # Optional hardware support features
-        ++ lib.optionals config.hardware.bluetooth.enable [ bluedevil bluez-qt ]
+        ++ lib.optionals config.hardware.bluetooth.enable [ bluedevil bluez-qt openobex obexftp ]
         ++ lib.optional config.networking.networkmanager.enable plasma-nm
         ++ lib.optional config.hardware.pulseaudio.enable plasma-pa
         ++ lib.optional config.powerManagement.enable powerdevil
@@ -182,10 +295,7 @@ in
         "/share"
       ];
 
-      environment.etc = singleton {
-        source = xcfg.xkbDir;
-        target = "X11/xkb";
-      };
+      environment.etc."X11/xkb".source = xcfg.xkbDir;
 
       # Enable GTK applications to load SVG icons
       services.xserver.gdk-pixbuf.modulePackages = [ pkgs.librsvg ];
@@ -228,29 +338,7 @@ in
       xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-kde ];
 
       # Update the start menu for each user that is currently logged in
-      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
-      '';
+      system.userActivationScripts.plasmaSetup = activationScript;
     })
   ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/surf-display.nix b/nixos/modules/services/x11/desktop-managers/surf-display.nix
index 140dde828da..9aeb0bbd2a8 100644
--- a/nixos/modules/services/x11/desktop-managers/surf-display.nix
+++ b/nixos/modules/services/x11/desktop-managers/surf-display.nix
@@ -118,7 +118,7 @@ in {
   };
 
   config = mkIf cfg.enable {
-    services.xserver.displayManager.extraSessionFilePackages = [
+    services.xserver.displayManager.sessionPackages = [
       pkgs.surf-display
     ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index 0b70ad5f29c..d39b4d64904 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -8,6 +8,10 @@ in
 
 {
 
+  meta = {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+
   imports = [
     # added 2019-08-18
     # needed to preserve some semblance of UI familarity
@@ -31,6 +35,7 @@ in
     (mkRenamedOptionModule
       [ "services" "xserver" "desktopManager" "xfce" "extraSessionCommands" ]
       [ "services" "xserver" "displayManager" "sessionCommands" ])
+    (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "xfce" "screenLock" ] "")
   ];
 
   options = {
@@ -128,6 +133,7 @@ in
 
     services.xserver.desktopManager.session = [{
       name = "xfce";
+      desktopNames = [ "XFCE" ];
       bgSupport = true;
       start = ''
         ${pkgs.runtimeShell} ${pkgs.xfce.xfce4-session.xinitrc} &
diff --git a/nixos/modules/services/x11/display-managers/account-service-util.nix b/nixos/modules/services/x11/display-managers/account-service-util.nix
new file mode 100644
index 00000000000..2b08c62d0ad
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/account-service-util.nix
@@ -0,0 +1,44 @@
+{ accountsservice
+, glib
+, gobject-introspection
+, python3
+, wrapGAppsHook
+, lib
+}:
+
+python3.pkgs.buildPythonApplication {
+  name = "set-session";
+
+  format = "other";
+
+  src = ./set-session.py;
+
+  dontUnpack = true;
+
+  strictDeps = false;
+
+  nativeBuildInputs = [
+    wrapGAppsHook
+    gobject-introspection
+  ];
+
+  buildInputs = [
+    accountsservice
+    glib
+  ];
+
+  propagatedBuildInputs = with python3.pkgs; [
+    pygobject3
+    ordered-set
+  ];
+
+  installPhase = ''
+    mkdir -p $out/bin
+    cp $src $out/bin/set-session
+    chmod +x $out/bin/set-session
+  '';
+
+  meta = with lib; {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/auto.nix b/nixos/modules/services/x11/display-managers/auto.nix
deleted file mode 100644
index 1068a344e0c..00000000000
--- a/nixos/modules/services/x11/display-managers/auto.nix
+++ /dev/null
@@ -1,68 +0,0 @@
-{ config, lib, ... }:
-
-with lib;
-
-let
-
-  dmcfg = config.services.xserver.displayManager;
-  cfg = dmcfg.auto;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.xserver.displayManager.auto = {
-
-      enable = mkOption {
-        default = false;
-        description = ''
-          Whether to enable the fake "auto" display manager, which
-          automatically logs in the user specified in the
-          <option>user</option> option.  This is mostly useful for
-          automated tests.
-        '';
-      };
-
-      user = mkOption {
-        default = "root";
-        description = "The user account to login automatically.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    services.xserver.displayManager.lightdm = {
-      enable = true;
-      autoLogin = {
-        enable = true;
-        user = cfg.user;
-      };
-    };
-
-    # lightdm by default doesn't allow auto login for root, which is
-    # required by some nixos tests. Override it here.
-    security.pam.services.lightdm-autologin.text = lib.mkForce ''
-        auth     requisite pam_nologin.so
-        auth     required  pam_succeed_if.so quiet
-        auth     required  pam_permit.so
-
-        account  include   lightdm
-
-        password include   lightdm
-
-        session  include   lightdm
-    '';
-
-  };
-
-}
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index b66856fd4d4..2a7a19e7695 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -27,16 +27,7 @@ let
     Xft.hintstyle: hintslight
   '';
 
-  mkCases = session:
-    concatStrings (
-      mapAttrsToList (name: starts: ''
-                       (${name})
-                         ${concatMapStringsSep "\n  " (n: n.start) starts}
-                         ;;
-                     '') (lib.groupBy (n: n.name) session)
-    );
-
-  # file provided by services.xserver.displayManager.session.wrapper
+  # file provided by services.xserver.displayManager.sessionData.wrapper
   xsessionWrapper = pkgs.writeScript "xsession-wrapper"
     ''
       #! ${pkgs.bash}/bin/bash
@@ -63,14 +54,6 @@ let
         exec &> >(tee ~/.xsession-errors)
       ''}
 
-      # Start PulseAudio if enabled.
-      ${optionalString (config.hardware.pulseaudio.enable) ''
-        # Publish access credentials in the root window.
-        if ${config.hardware.pulseaudio.package.out}/bin/pulseaudio --dump-modules | grep module-x11-publish &> /dev/null; then
-          ${config.hardware.pulseaudio.package.out}/bin/pactl load-module module-x11-publish "display=$DISPLAY"
-        fi
-      ''}
-
       # Tell systemd about our $DISPLAY and $XAUTHORITY.
       # This is needed by the ssh-agent unit.
       #
@@ -116,98 +99,49 @@ let
           # Run the supplied session command. Remove any double quotes with eval.
           eval exec "$@"
       else
-          # Fall back to the default window/desktopManager
-          exec ${cfg.displayManager.session.script}
+          # TODO: Do we need this? Should not the session always exist?
+          echo "error: unknown session $1" 1>&2
+          exit 1
       fi
     '';
 
-  # file provided by services.xserver.displayManager.session.script
-  xsession = wm: dm: pkgs.writeScript "xsession"
-    ''
-      #! ${pkgs.bash}/bin/bash
-
-      # Legacy session script used to construct .desktop files from
-      # `services.xserver.displayManager.session` entries. Called from
-      # `sessionWrapper`.
-
-      # Expected parameters:
-      #   $1 = <desktop-manager>+<window-manager>
-
-      # The first argument of this script is the session type.
-      sessionType="$1"
-      if [ "$sessionType" = default ]; then sessionType=""; fi
-
-      # The session type is "<desktop-manager>+<window-manager>", so
-      # extract those (see:
-      # http://wiki.bash-hackers.org/syntax/pe#substring_removal).
-      windowManager="''${sessionType##*+}"
-      : ''${windowManager:=${cfg.windowManager.default}}
-      desktopManager="''${sessionType%%+*}"
-      : ''${desktopManager:=${cfg.desktopManager.default}}
-
-      # Start the window manager.
-      case "$windowManager" in
-        ${mkCases wm}
-        (*) echo "$0: Window manager '$windowManager' not found.";;
-      esac
-
-      # Start the desktop manager.
-      case "$desktopManager" in
-        ${mkCases dm}
-        (*) echo "$0: Desktop manager '$desktopManager' not found.";;
-      esac
-
-      ${optionalString cfg.updateDbusEnvironment ''
-        ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
-      ''}
-
-      test -n "$waitPID" && wait "$waitPID"
-
-      ${config.systemd.package}/bin/systemctl --user stop graphical-session.target
-
-      exit 0
-    '';
-
-  # Desktop Entry Specification:
-  # - https://standards.freedesktop.org/desktop-entry-spec/latest/
-  # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
-  mkDesktops = names: pkgs.runCommand "desktops"
+  installedSessions = pkgs.runCommand "desktops"
     { # trivial derivation
       preferLocalBuild = true;
       allowSubstitutes = false;
     }
     ''
-      mkdir -p "$out/share/xsessions"
-      ${concatMapStrings (n: ''
-        cat - > "$out/share/xsessions/${n}.desktop" << EODESKTOP
-        [Desktop Entry]
-        Version=1.0
-        Type=XSession
-        TryExec=${cfg.displayManager.session.script}
-        Exec=${cfg.displayManager.session.script} "${n}"
-        Name=${n}
-        Comment=
-        EODESKTOP
-      '') names}
+      mkdir -p "$out/share/"{xsessions,wayland-sessions}
 
       ${concatMapStrings (pkg: ''
+        for n in ${concatStringsSep " " pkg.providedSessions}; do
+          if ! test -f ${pkg}/share/wayland-sessions/$n.desktop -o \
+                    -f ${pkg}/share/xsessions/$n.desktop; then
+            echo "Couldn't find provided session name, $n.desktop, in session package ${pkg.name}:"
+            echo "  ${pkg}"
+            return 1
+          fi
+        done
+
         if test -d ${pkg}/share/xsessions; then
           ${xorg.lndir}/bin/lndir ${pkg}/share/xsessions $out/share/xsessions
         fi
-      '') cfg.displayManager.extraSessionFilePackages}
-
-      ${concatMapStrings (pkg: ''
         if test -d ${pkg}/share/wayland-sessions; then
-          mkdir -p "$out/share/wayland-sessions"
           ${xorg.lndir}/bin/lndir ${pkg}/share/wayland-sessions $out/share/wayland-sessions
         fi
-      '') cfg.displayManager.extraSessionFilePackages}
+      '') cfg.displayManager.sessionPackages}
     '';
 
+  dmDefault = cfg.desktopManager.default;
+  # fallback default for cases when only default wm is set
+  dmFallbackDefault = if dmDefault != null then dmDefault else "none";
+  wmDefault = cfg.windowManager.default;
+
+  defaultSessionFromLegacyOptions = dmFallbackDefault + optionalString (wmDefault != null && wmDefault != "none") "+${wmDefault}";
+
 in
 
 {
-
   options = {
 
     services.xserver.displayManager = {
@@ -262,11 +196,24 @@ in
         '';
       };
 
-      extraSessionFilePackages = mkOption {
-        type = types.listOf types.package;
+      sessionPackages = mkOption {
+        type = with types; listOf (package // {
+          description = "package with provided sessions";
+          check = p: assertMsg
+            (package.check p && p ? providedSessions
+            && p.providedSessions != [] && all isString p.providedSessions)
+            ''
+              Package, '${p.name}', did not specify any session names, as strings, in
+              'passthru.providedSessions'. This is required when used as a session package.
+
+              The session names can be looked up in:
+                ${p}/share/xsessions
+                ${p}/share/wayland-sessions
+           '';
+        });
         default = [];
         description = ''
-          A list of packages containing xsession files to be passed to the display manager.
+          A list of packages containing x11 or wayland session files to be passed to the display manager.
         '';
       };
 
@@ -297,16 +244,48 @@ in
           inside the display manager with the desktop manager name
           followed by the window manager name.
         '';
-        apply = list: rec {
-          wm = filter (s: s.manage == "window") list;
-          dm = filter (s: s.manage == "desktop") list;
-          names = flip concatMap dm
-            (d: map (w: d.name + optionalString (w.name != "none") ("+" + w.name))
-              (filter (w: d.name != "none" || w.name != "none") wm));
-          desktops = mkDesktops names;
-          script = xsession wm dm;
+      };
+
+      sessionData = mkOption {
+        description = "Data exported for display managers’ convenience";
+        internal = true;
+        default = {};
+        apply = val: {
           wrapper = xsessionWrapper;
+          desktops = installedSessions;
+          sessionNames = concatMap (p: p.providedSessions) cfg.displayManager.sessionPackages;
+          # We do not want to force users to set defaultSession when they have only single DE.
+          autologinSession =
+            if cfg.displayManager.defaultSession != null then
+              cfg.displayManager.defaultSession
+            else if cfg.displayManager.sessionData.sessionNames != [] then
+              head cfg.displayManager.sessionData.sessionNames
+            else
+              null;
+        };
+      };
+
+      defaultSession = mkOption {
+        type = with types; nullOr str // {
+          description = "session name";
+          check = d:
+            assertMsg (d != null -> (str.check d && elem d cfg.displayManager.sessionData.sessionNames)) ''
+                Default graphical session, '${d}', not found.
+                Valid names for 'services.xserver.displayManager.defaultSession' are:
+                  ${concatStringsSep "\n  " cfg.displayManager.sessionData.sessionNames}
+              '';
         };
+        default =
+          if dmDefault != null || wmDefault != null then
+            defaultSessionFromLegacyOptions
+          else
+            null;
+        example = "gnome";
+        description = ''
+          Graphical session to pre-select in the session chooser (only effective for GDM and LightDM).
+
+          On GDM, LightDM and SDDM, it will also be used as a session for auto-login.
+        '';
       };
 
       job = {
@@ -357,6 +336,27 @@ in
   };
 
   config = {
+    assertions = [
+      {
+        assertion = cfg.desktopManager.default != null || cfg.windowManager.default != null -> cfg.displayManager.defaultSession == defaultSessionFromLegacyOptions;
+        message = "You cannot use both services.xserver.displayManager.defaultSession option and legacy options (services.xserver.desktopManager.default and services.xserver.windowManager.default).";
+      }
+    ];
+
+    warnings =
+      mkIf (dmDefault != null || wmDefault != null) [
+        ''
+          The following options are deprecated:
+            ${concatStringsSep "\n  " (map ({c, t}: t) (filter ({c, t}: c != null) [
+            { c = dmDefault; t = "- services.xserver.desktopManager.default"; }
+            { c = wmDefault; t = "- services.xserver.windowManager.default"; }
+            ]))}
+          Please use
+            services.xserver.displayManager.defaultSession = "${defaultSessionFromLegacyOptions}";
+          instead.
+        ''
+      ];
+
     services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
 
     systemd.user.targets.graphical-session = {
@@ -365,11 +365,79 @@ in
         StopWhenUnneeded = false;
       };
     };
+
+    # Create desktop files and scripts for starting sessions for WMs/DMs
+    # that do not have upstream session files (those defined using services.{display,desktop,window}Manager.session options).
+    services.xserver.displayManager.sessionPackages =
+      let
+        dms = filter (s: s.manage == "desktop") cfg.displayManager.session;
+        wms = filter (s: s.manage == "window") cfg.displayManager.session;
+
+        # Script responsible for starting the window manager and the desktop manager.
+        xsession = dm: wm: pkgs.writeScript "xsession" ''
+          #! ${pkgs.bash}/bin/bash
+
+          # Legacy session script used to construct .desktop files from
+          # `services.xserver.displayManager.session` entries. Called from
+          # `sessionWrapper`.
+
+          # Start the window manager.
+          ${wm.start}
+
+          # Start the desktop manager.
+          ${dm.start}
+
+          ${optionalString cfg.updateDbusEnvironment ''
+            ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
+          ''}
+
+          test -n "$waitPID" && wait "$waitPID"
+
+          ${config.systemd.package}/bin/systemctl --user stop graphical-session.target
+
+          exit 0
+        '';
+      in
+        # We will generate every possible pair of WM and DM.
+        concatLists (
+          crossLists
+            (dm: wm: let
+              sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
+              script = xsession dm wm;
+              desktopNames = if dm ? desktopNames
+                             then concatStringsSep ";" dm.desktopNames
+                             else sessionName;
+            in
+              optional (dm.name != "none" || wm.name != "none")
+                (pkgs.writeTextFile {
+                  name = "${sessionName}-xsession";
+                  destination = "/share/xsessions/${sessionName}.desktop";
+                  # Desktop Entry Specification:
+                  # - https://standards.freedesktop.org/desktop-entry-spec/latest/
+                  # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
+                  text = ''
+                    [Desktop Entry]
+                    Version=1.0
+                    Type=XSession
+                    TryExec=${script}
+                    Exec=${script}
+                    Name=${sessionName}
+                    DesktopNames=${desktopNames}
+                  '';
+                } // {
+                  providedSessions = [ sessionName ];
+                })
+            )
+            [dms wms]
+          );
   };
 
   imports = [
-   (mkRemovedOptionModule [ "services" "xserver" "displayManager" "desktopManagerHandlesLidAndPower" ]
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "desktopManagerHandlesLidAndPower" ]
      "The option is no longer necessary because all display managers have already delegated lid management to systemd.")
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logsXsession" ] [ "services" "xserver" "displayManager" "job" "logToFile" ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "logToJournal" ] [ "services" "xserver" "displayManager" "job" "logToJournal" ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "extraSessionFilesPackages" ] [ "services" "xserver" "displayManager" "sessionPackages" ])
   ];
 
 }
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 912ec5bd38e..d7bef68e5bc 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -31,48 +31,17 @@ let
     load-module module-position-event-sounds
   '';
 
-  dmDefault = config.services.xserver.desktopManager.default;
-  wmDefault = config.services.xserver.windowManager.default;
-  hasDefaultUserSession = dmDefault != "none" || wmDefault != "none";
-  defaultSessionName = dmDefault + optionalString (wmDefault != "none") ("+" + wmDefault);
-
-  setSessionScript = pkgs.python3.pkgs.buildPythonApplication {
-    name = "set-session";
-
-    format = "other";
-
-    src = ./set-session.py;
-
-    dontUnpack = true;
-
-    strictDeps = false;
-
-    nativeBuildInputs = with pkgs; [
-      wrapGAppsHook
-      gobject-introspection
-    ];
-
-    buildInputs = with pkgs; [
-      accountsservice
-      glib
-    ];
-
-    propagatedBuildInputs = with pkgs.python3.pkgs; [
-      pygobject3
-      ordered-set
-    ];
-
-    installPhase = ''
-      mkdir -p $out/bin
-      cp $src $out/bin/set-session
-      chmod +x $out/bin/set-session
-    '';
-  };
+  defaultSessionName = config.services.xserver.displayManager.defaultSession;
 
+  setSessionScript = pkgs.callPackage ./account-service-util.nix { };
 in
 
 {
 
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
   ###### interface
 
   options = {
@@ -186,7 +155,7 @@ in
         environment = {
           GDM_X_SERVER_EXTRA_ARGS = toString
             (filter (arg: arg != "-terminate") cfg.xserverArgs);
-          XDG_DATA_DIRS = "${cfg.session.desktops}/share/";
+          XDG_DATA_DIRS = "${cfg.sessionData.desktops}/share/";
         } // optionalAttrs (xSessionWrapper != null) {
           # Make GDM use this wrapper before running the session, which runs the
           # configured setupCommands. This relies on a patched GDM which supports
@@ -194,23 +163,36 @@ in
           GDM_X_SESSION_WRAPPER = "${xSessionWrapper}";
         };
         execCmd = "exec ${gdm}/bin/gdm";
-        preStart = optionalString config.hardware.pulseaudio.enable ''
-          mkdir -p /run/gdm/.config/pulse
-          ln -sf ${pulseConfig} /run/gdm/.config/pulse/default.pa
-          chown -R gdm:gdm /run/gdm/.config
-        '' + optionalString config.services.gnome3.gnome-initial-setup.enable ''
-          # Create stamp file for gnome-initial-setup to prevent run.
-          mkdir -p /run/gdm/.config
-          cat - > /run/gdm/.config/gnome-initial-setup-done <<- EOF
-          yes
-          EOF
-        '' + optionalString hasDefaultUserSession ''
-          ${setSessionScript}/bin/set-session ${defaultSessionName}
+        preStart = optionalString (defaultSessionName != null) ''
+          # Set default session in session chooser to a specified values – basically ignore session history.
+          ${setSessionScript}/bin/set-session ${cfg.sessionData.autologinSession}
         '';
       };
 
-    # Because sd_login_monitor_new requires /run/systemd/machines
-    systemd.services.display-manager.wants = [ "systemd-machined.service" ];
+    systemd.tmpfiles.rules = [
+      "d /run/gdm/.config 0711 gdm gdm"
+    ] ++ optionals config.hardware.pulseaudio.enable [
+      "d /run/gdm/.config/pulse 0711 gdm gdm"
+      "L+ /run/gdm/.config/pulse/${pulseConfig.name} - - - - ${pulseConfig}"
+    ] ++ optionals config.services.gnome3.gnome-initial-setup.enable [
+      # Create stamp file for gnome-initial-setup to prevent it starting in GDM.
+      "f /run/gdm/.config/gnome-initial-setup-done 0711 gdm gdm - yes"
+    ];
+
+    # Otherwise GDM will not be able to start correctly and display Wayland sessions
+    systemd.packages = with pkgs.gnome3; [ gnome-session gnome-shell ];
+    environment.systemPackages = [ pkgs.gnome3.adwaita-icon-theme ];
+
+    systemd.services.display-manager.wants = [
+      # Because sd_login_monitor_new requires /run/systemd/machines
+      "systemd-machined.service"
+      # setSessionScript wants AccountsService
+      "accounts-daemon.service"
+      # Failed to open gpu '/dev/dri/card0': GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Operation not permitted
+      # https://github.com/NixOS/nixpkgs/pull/25311#issuecomment-609417621
+      "systemd-udev-settle.service"
+    ];
+
     systemd.services.display-manager.after = [
       "rc-local.service"
       "systemd-machined.service"
@@ -218,6 +200,7 @@ in
       "getty@tty${gdm.initialVT}.service"
       "plymouth-quit.service"
       "plymouth-start.service"
+      "systemd-udev-settle.service"
     ];
     systemd.services.display-manager.conflicts = [
        "getty@tty${gdm.initialVT}.service"
@@ -281,7 +264,7 @@ in
       customDconfDb = pkgs.stdenv.mkDerivation {
         name = "gdm-dconf-db";
         buildCommand = ''
-          ${pkgs.gnome3.dconf}/bin/dconf compile $out ${customDconf}/dconf
+          ${pkgs.dconf}/bin/dconf compile $out ${customDconf}/dconf
         '';
       };
     in pkgs.stdenv.mkDerivation {
@@ -326,7 +309,7 @@ in
       ${optionalString cfg.gdm.debug "Enable=true"}
     '';
 
-    environment.etc."gdm/Xsession".source = config.services.xserver.displayManager.session.wrapper;
+    environment.etc."gdm/Xsession".source = config.services.xserver.displayManager.sessionData.wrapper;
 
     # GDM LFS PAM modules, adapted somehow to NixOS
     security.pam.services = {
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
index fa9445af32e..16d7fdf15cf 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
@@ -14,7 +14,9 @@ let
     user = ${cfg.user}
     show-password-label = true
     password-label-text = Password:
+    invalid-password-text = Invalid Password
     show-input-cursor = true
+    password-alignment = right
 
     [greeter-hotkeys]
     mod-key = meta
@@ -26,6 +28,8 @@ let
     [greeter-theme]
     font = Sans
     font-size = 1em
+    font-weight = bold
+    font-style = normal
     text-color = "#080800"
     error-color = "#F8F8F0"
     background-image = "${ldmcfg.background}"
@@ -36,6 +40,8 @@ let
     layout-space = 15
     password-color = "#F8F8F0"
     password-background-color = "#1B1D1E"
+    password-border-color = "#080800"
+    password-border-width = 2px
 
     ${cfg.extraConfig}
     '';
@@ -53,9 +59,8 @@ in
           Whether to enable lightdm-mini-greeter as the lightdm greeter.
 
           Note that this greeter starts only the default X session.
-          You can configure the default X session by
-          <option>services.xserver.desktopManager.default</option> and
-          <option>services.xserver.windowManager.default</option>.
+          You can configure the default X session using
+          <xref linkend="opt-services.xserver.displayManager.defaultSession"/>.
         '';
       };
 
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
index 29cb6ccbc06..087c6b9c38a 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
@@ -10,6 +10,10 @@ let
 
 in
 {
+  meta = {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+
   options = {
 
     services.xserver.displayManager.lightdm.greeters.pantheon = {
@@ -35,6 +39,9 @@ in
       name = "io.elementary.greeter";
     };
 
+    # Show manual login card.
+    services.xserver.displayManager.lightdm.extraSeatDefaults = "greeter-show-manual-login=true";
+
     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-greeters/tiny.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix
new file mode 100644
index 00000000000..a9ba8e6280d
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/tiny.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  dmcfg = config.services.xserver.displayManager;
+  ldmcfg = dmcfg.lightdm;
+  cfg = ldmcfg.greeters.tiny;
+
+in
+{
+  options = {
+
+    services.xserver.displayManager.lightdm.greeters.tiny = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable lightdm-tiny-greeter as the lightdm greeter.
+
+          Note that this greeter starts only the default X session.
+          You can configure the default X session using
+          <xref linkend="opt-services.xserver.displayManager.defaultSession"/>.
+        '';
+      };
+
+      label = {
+        user = mkOption {
+          type = types.str;
+          default = "Username";
+          description = ''
+            The string to represent the user_text label.
+          '';
+        };
+
+        pass = mkOption {
+          type = types.str;
+          default = "Password";
+          description = ''
+            The string to represent the pass_text label.
+          '';
+        };
+      };
+
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Section to describe style and ui.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf (ldmcfg.enable && cfg.enable) {
+
+    services.xserver.displayManager.lightdm.greeters.gtk.enable = false;
+
+    nixpkgs.config.lightdm-tiny-greeter.conf =
+    let
+      configHeader = ''
+        #include <gtk/gtk.h>
+        static const char *user_text = "${cfg.label.user}";
+        static const char *pass_text = "${cfg.label.pass}";
+        static const char *session = "${dmcfg.defaultSession}";
+      '';
+    in
+      optionalString (cfg.extraConfig != "")
+        (configHeader + cfg.extraConfig);
+
+    services.xserver.displayManager.lightdm.greeter =
+      mkDefault {
+        package = pkgs.lightdm-tiny-greeter.xgreeters;
+        name = "lightdm-tiny-greeter";
+      };
+
+    assertions = [
+      {
+        assertion = dmcfg.defaultSession != null;
+        message = ''
+          Please set: services.xserver.displayManager.defaultSession
+        '';
+      }
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index cf4c05acbcc..479548863b4 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -8,10 +8,9 @@ let
   dmcfg = xcfg.displayManager;
   xEnv = config.systemd.services.display-manager.environment;
   cfg = dmcfg.lightdm;
+  sessionData = dmcfg.sessionData;
 
-  dmDefault = xcfg.desktopManager.default;
-  wmDefault = xcfg.windowManager.default;
-  hasDefaultUserSession = dmDefault != "none" || wmDefault != "none";
+  setSessionScript = pkgs.callPackage ./account-service-util.nix { };
 
   inherit (pkgs) lightdm writeScript writeText;
 
@@ -45,22 +44,19 @@ let
         greeter-user = ${config.users.users.lightdm.name}
         greeters-directory = ${cfg.greeter.package}
       ''}
-      sessions-directory = ${dmcfg.session.desktops}/share/xsessions
+      sessions-directory = ${dmcfg.sessionData.desktops}/share/xsessions:${dmcfg.sessionData.desktops}/share/wayland-sessions
       ${cfg.extraConfig}
 
       [Seat:*]
       xserver-command = ${xserverWrapper}
-      session-wrapper = ${dmcfg.session.wrapper}
+      session-wrapper = ${dmcfg.sessionData.wrapper}
       ${optionalString cfg.greeter.enable ''
         greeter-session = ${cfg.greeter.name}
       ''}
       ${optionalString cfg.autoLogin.enable ''
         autologin-user = ${cfg.autoLogin.user}
         autologin-user-timeout = ${toString cfg.autoLogin.timeout}
-        autologin-session = ${defaultSessionName}
-      ''}
-      ${optionalString hasDefaultUserSession ''
-        user-session=${defaultSessionName}
+        autologin-session = ${sessionData.autologinSession}
       ''}
       ${optionalString (dmcfg.setupCommands != "") ''
         display-setup-script=${pkgs.writeScript "lightdm-display-setup" ''
@@ -71,9 +67,12 @@ let
       ${cfg.extraSeatDefaults}
     '';
 
-  defaultSessionName = dmDefault + optionalString (wmDefault != "none") ("+" + wmDefault);
 in
 {
+  meta = {
+    maintainers = with maintainers; [ worldofpeace ];
+  };
+
   # Note: the order in which lightdm greeter modules are imported
   # here determines the default: later modules (if enable) are
   # preferred.
@@ -82,6 +81,7 @@ in
     ./lightdm-greeters/mini.nix
     ./lightdm-greeters/enso-os.nix
     ./lightdm-greeters/pantheon.nix
+    ./lightdm-greeters/tiny.nix
   ];
 
   options = {
@@ -199,11 +199,9 @@ in
           LightDM auto-login requires services.xserver.displayManager.lightdm.autoLogin.user to be set
         '';
       }
-      { assertion = cfg.autoLogin.enable -> dmDefault != "none" || wmDefault != "none";
+      { assertion = cfg.autoLogin.enable -> sessionData.autologinSession != null;
         message = ''
-          LightDM auto-login requires that services.xserver.desktopManager.default and
-          services.xserver.windowManager.default are set to valid values. The current
-          default session: ${defaultSessionName} is not valid.
+          LightDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
       }
       { assertion = !cfg.greeter.enable -> (cfg.autoLogin.enable && cfg.autoLogin.timeout == 0);
@@ -214,6 +212,20 @@ in
       }
     ];
 
+    # Set default session in session chooser to a specified values – basically ignore session history.
+    # Auto-login is already covered by a config value.
+    services.xserver.displayManager.job.preStart = optionalString (!cfg.autoLogin.enable && dmcfg.defaultSession != null) ''
+      ${setSessionScript}/bin/set-session ${dmcfg.defaultSession}
+    '';
+
+    # setSessionScript needs session-files in XDG_DATA_DIRS
+    services.xserver.displayManager.job.environment.XDG_DATA_DIRS = "${dmcfg.sessionData.desktops}/share/";
+
+    # setSessionScript wants AccountsService
+    systemd.services.display-manager.wants = [
+      "accounts-daemon.service"
+    ];
+
     # lightdm relaunches itself via just `lightdm`, so needs to be on the PATH
     services.xserver.displayManager.job.execCmd = ''
       export PATH=${lightdm}/sbin:$PATH
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 899dd8665a2..4224c557ed6 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -50,8 +50,8 @@ let
     MinimumVT=${toString (if xcfg.tty != null then xcfg.tty else 7)}
     ServerPath=${xserverWrapper}
     XephyrPath=${pkgs.xorg.xorgserver.out}/bin/Xephyr
-    SessionCommand=${dmcfg.session.wrapper}
-    SessionDir=${dmcfg.session.desktops}/share/xsessions
+    SessionCommand=${dmcfg.sessionData.wrapper}
+    SessionDir=${dmcfg.sessionData.desktops}/share/xsessions
     XauthPath=${pkgs.xorg.xauth}/bin/xauth
     DisplayCommand=${Xsetup}
     DisplayStopCommand=${Xstop}
@@ -59,26 +59,27 @@ let
 
     [Wayland]
     EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
-    SessionDir=${dmcfg.session.desktops}/share/wayland-sessions
+    SessionDir=${dmcfg.sessionData.desktops}/share/wayland-sessions
 
     ${optionalString cfg.autoLogin.enable ''
     [Autologin]
     User=${cfg.autoLogin.user}
-    Session=${defaultSessionName}.desktop
+    Session=${autoLoginSessionName}.desktop
     Relogin=${boolToString cfg.autoLogin.relogin}
     ''}
 
     ${cfg.extraConfig}
   '';
 
-  defaultSessionName =
-    let
-      dm = xcfg.desktopManager.default;
-      wm = xcfg.windowManager.default;
-    in dm + optionalString (wm != "none") ("+" + wm);
+  autoLoginSessionName = dmcfg.sessionData.autologinSession;
 
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "sddm" "themes" ]
+      "Set the option `services.xserver.displayManager.sddm.package' instead.")
+  ];
+
   options = {
 
     services.xserver.displayManager.sddm = {
@@ -205,11 +206,9 @@ in
           SDDM auto-login requires services.xserver.displayManager.sddm.autoLogin.user to be set
         '';
       }
-      { assertion = cfg.autoLogin.enable -> elem defaultSessionName dmcfg.session.names;
+      { assertion = cfg.autoLogin.enable -> autoLoginSessionName != null;
         message = ''
-          SDDM auto-login requires that services.xserver.desktopManager.default and
-          services.xserver.windowManager.default are set to valid values. The current
-          default session: ${defaultSessionName} is not valid.
+          SDDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
       }
     ];
diff --git a/nixos/modules/services/x11/display-managers/startx.nix b/nixos/modules/services/x11/display-managers/startx.nix
index 57046984358..3980203b945 100644
--- a/nixos/modules/services/x11/display-managers/startx.nix
+++ b/nixos/modules/services/x11/display-managers/startx.nix
@@ -15,6 +15,7 @@ in
   options = {
     services.xserver.displayManager.startx = {
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to enable the dummy "startx" pseudo-display manager,
diff --git a/nixos/modules/services/x11/extra-layouts.nix b/nixos/modules/services/x11/extra-layouts.nix
index 1af98a1318b..f48216ff446 100644
--- a/nixos/modules/services/x11/extra-layouts.nix
+++ b/nixos/modules/services/x11/extra-layouts.nix
@@ -141,7 +141,7 @@ in
         });
 
         xkbcomp = super.xorg.xkbcomp.overrideAttrs (old: {
-          configureFlags = "--with-xkb-config-root=${self.xkb_patched}/share/X11/xkb";
+          configureFlags = [ "--with-xkb-config-root=${self.xkb_patched}/share/X11/xkb" ];
         });
 
       };
@@ -158,6 +158,12 @@ in
 
     });
 
+    environment.sessionVariables = {
+      # runtime override supported by multiple libraries e. g. libxkbcommon
+      # https://xkbcommon.org/doc/current/group__include-path.html
+      XKB_CONFIG_ROOT = "${pkgs.xkb_patched}/etc/X11/xkb";
+    };
+
     services.xserver = {
       xkbDir = "${pkgs.xkb_patched}/etc/X11/xkb";
       exportConfiguration = config.services.xserver.displayManager.startx.enable;
diff --git a/nixos/modules/services/x11/gdk-pixbuf.nix b/nixos/modules/services/x11/gdk-pixbuf.nix
index 9ad926369ec..e6a24a2f1a3 100644
--- a/nixos/modules/services/x11/gdk-pixbuf.nix
+++ b/nixos/modules/services/x11/gdk-pixbuf.nix
@@ -19,7 +19,7 @@ let
           continue
         fi
         GDK_PIXBUF_MODULEDIR="$module_dir" \
-          ${pkgs.gdk-pixbuf.dev}/bin/gdk-pixbuf-query-loaders
+          ${pkgs.stdenv.hostPlatform.emulator pkgs.buildPackages} ${pkgs.gdk-pixbuf.dev}/bin/gdk-pixbuf-query-loaders
       done
     ) > "$out"
   '';
@@ -37,7 +37,7 @@ in
   # If there is any package configured in modulePackages, we generate the
   # loaders.cache based on that and set the environment variable
   # GDK_PIXBUF_MODULE_FILE to point to it.
-  config = mkIf (cfg.modulePackages != []) {
+  config = mkIf (cfg.modulePackages != [] || pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform) {
     environment.variables = {
       GDK_PIXBUF_MODULE_FILE = "${loadersCache}";
     };
diff --git a/nixos/modules/services/x11/hardware/libinput.nix b/nixos/modules/services/x11/hardware/libinput.nix
index 71065dfc26b..9548ecb8ef6 100644
--- a/nixos/modules/services/x11/hardware/libinput.nix
+++ b/nixos/modules/services/x11/hardware/libinput.nix
@@ -184,7 +184,11 @@ in {
         ''
           Option "DragLockButtons" "L1 B1 L2 B2"
         '';
-        description = "Additional options for libinput touchpad driver.";
+        description = ''
+          Additional options for libinput touchpad driver. See
+          <citerefentry><refentrytitle>libinput</refentrytitle><manvolnum>4</manvolnum></citerefentry>
+          for available options.";
+        '';
       };
 
     };
@@ -198,12 +202,13 @@ in {
 
     environment.systemPackages = [ pkgs.xorg.xf86inputlibinput ];
 
-    environment.etc = [
-      (let cfgPath = "X11/xorg.conf.d/40-libinput.conf"; in {
-        source = pkgs.xorg.xf86inputlibinput.out + "/share/" + cfgPath;
-        target = cfgPath;
-      })
-    ];
+    environment.etc =
+      let cfgPath = "X11/xorg.conf.d/40-libinput.conf";
+      in {
+        ${cfgPath} = {
+          source = pkgs.xorg.xf86inputlibinput.out + "/share/" + cfgPath;
+        };
+      };
 
     services.udev.packages = [ pkgs.libinput.out ];
 
diff --git a/nixos/modules/services/x11/hardware/multitouch.nix b/nixos/modules/services/x11/hardware/multitouch.nix
deleted file mode 100644
index c03bb3b494f..00000000000
--- a/nixos/modules/services/x11/hardware/multitouch.nix
+++ /dev/null
@@ -1,94 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let cfg = config.services.xserver.multitouch;
-    disabledTapConfig = ''
-      Option "MaxTapTime" "0"
-      Option "MaxTapMove" "0"
-      Option "TapButton1" "0"
-      Option "TapButton2" "0"
-      Option "TapButton3" "0"
-    '';
-in {
-
-  options = {
-
-    services.xserver.multitouch = {
-
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable multitouch touchpad support.";
-      };
-
-      invertScroll = mkOption {
-        default = false;
-        type = types.bool;
-        description = "Whether to invert scrolling direction à la OSX Lion";
-      };
-
-      ignorePalm = mkOption {
-        default = false;
-        type = types.bool;
-        description = "Whether to ignore touches detected as being the palm (i.e when typing)";
-      };
-
-      tapButtons = mkOption {
-        type = types.bool;
-        default = true;
-        description = "Whether to enable tap buttons.";
-      };
-
-      buttonsMap = mkOption {
-        type = types.listOf types.int;
-        default = [3 2 0];
-        example = [1 3 2];
-        description = "Remap touchpad buttons.";
-        apply = map toString;
-      };
-
-      additionalOptions = mkOption {
-        type = types.str;
-        default = "";
-        example = ''
-          Option "ScaleDistance" "50"
-          Option "RotateDistance" "60"
-        '';
-        description = ''
-          Additional options for mtrack touchpad driver.
-        '';
-      };
-
-    };
-
-  };
-
-  config = mkIf cfg.enable {
-
-    services.xserver.modules = [ pkgs.xf86_input_mtrack ];
-
-    services.xserver.config =
-      ''
-        # Automatically enable the multitouch driver
-        Section "InputClass"
-          MatchIsTouchpad "on"
-          Identifier "Touchpads"
-          Driver "mtrack"
-          Option "IgnorePalm" "${boolToString cfg.ignorePalm}"
-          Option "ClickFinger1" "${builtins.elemAt cfg.buttonsMap 0}"
-          Option "ClickFinger2" "${builtins.elemAt cfg.buttonsMap 1}"
-          Option "ClickFinger3" "${builtins.elemAt cfg.buttonsMap 2}"
-          ${optionalString (!cfg.tapButtons) disabledTapConfig}
-          ${optionalString cfg.invertScroll ''
-            Option "ScrollUpButton" "5"
-            Option "ScrollDownButton" "4"
-            Option "ScrollLeftButton" "7"
-            Option "ScrollRightButton" "6"
-          ''}
-          ${cfg.additionalOptions}
-        EndSection
-      '';
-
-  };
-
-}
diff --git a/nixos/modules/services/x11/hardware/wacom.nix b/nixos/modules/services/x11/hardware/wacom.nix
index a27889c36a7..dad2b308d1b 100644
--- a/nixos/modules/services/x11/hardware/wacom.nix
+++ b/nixos/modules/services/x11/hardware/wacom.nix
@@ -15,6 +15,7 @@ in
     services.xserver.wacom = {
 
       enable = mkOption {
+        type = types.bool;
         default = false;
         description = ''
           Whether to enable the Wacom touchscreen/digitizer/tablet.
diff --git a/nixos/modules/services/x11/imwheel.nix b/nixos/modules/services/x11/imwheel.nix
new file mode 100644
index 00000000000..3923df498e7
--- /dev/null
+++ b/nixos/modules/services/x11/imwheel.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.xserver.imwheel;
+in
+  {
+    options = {
+      services.xserver.imwheel = {
+        enable = mkEnableOption "IMWheel service";
+
+        extraOptions = mkOption {
+          type = types.listOf types.str;
+          default = [ "--buttons=45" ];
+          example = [ "--debug" ];
+          description = ''
+            Additional command-line arguments to pass to
+            <command>imwheel</command>.
+          '';
+        };
+
+        rules = mkOption {
+          type = types.attrsOf types.str;
+          default = {};
+          example = literalExample ''
+            ".*" = '''
+              None,      Up,   Button4, 8
+              None,      Down, Button5, 8
+              Shift_L,   Up,   Shift_L|Button4, 4
+              Shift_L,   Down, Shift_L|Button5, 4
+              Control_L, Up,   Control_L|Button4
+              Control_L, Down, Control_L|Button5
+            ''';
+          '';
+          description = ''
+            Window class translation rules.
+            /etc/X11/imwheelrc is generated based on this config
+            which means this config is global for all users.
+            See <link xlink:href="http://imwheel.sourceforge.net/imwheel.1.html">offical man pages</link>
+            for more informations.
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ pkgs.imwheel ];
+
+      environment.etc."X11/imwheel/imwheelrc".source =
+        pkgs.writeText "imwheelrc" (concatStringsSep "\n\n"
+          (mapAttrsToList
+            (rule: conf: "\"${rule}\"\n${conf}") cfg.rules
+          ));
+
+      systemd.user.services.imwheel = {
+        description = "imwheel service";
+        wantedBy = [ "graphical-session.target" ];
+        partOf = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${pkgs.imwheel}/bin/imwheel " + escapeShellArgs ([
+            "--detach"
+            "--kill"
+          ] ++ cfg.extraOptions);
+          ExecStop = "${pkgs.procps}/bin/pkill imwheel";
+          Restart = "on-failure";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/x11/compton.nix b/nixos/modules/services/x11/picom.nix
index a94a76ff0c0..e3bd21be73e 100644
--- a/nixos/modules/services/x11/compton.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -5,7 +5,7 @@ with builtins;
 
 let
 
-  cfg = config.services.compton;
+  cfg = config.services.picom;
 
   pairOf = x: with types; addCheck (listOf x) (y: length y == 2);
 
@@ -31,20 +31,24 @@ let
                 (key: value: "${toString key}=${mkValueString value};")
                 v)
             + " }"
-          else abort "compton.mkValueString: unexpected type (v = ${v})";
+          else abort "picom.mkValueString: unexpected type (v = ${v})";
       in "${escape [ sep ] k}${sep}${mkValueString v};")
       attrs);
 
-  configFile = pkgs.writeText "compton.conf" (toConf cfg.settings);
+  configFile = pkgs.writeText "picom.conf" (toConf cfg.settings);
 
 in {
 
-  options.services.compton = {
+  imports = [
+    (mkAliasOptionModule [ "services" "compton" ] [ "services" "picom" ])
+  ];
+
+  options.services.picom = {
     enable = mkOption {
       type = types.bool;
       default = false;
       description = ''
-        Whether of not to enable Compton as the X.org composite manager.
+        Whether of not to enable Picom as the X.org composite manager.
       '';
     };
 
@@ -85,7 +89,7 @@ in {
       ];
       description = ''
         List of conditions of windows that should not be faded.
-        See <literal>compton(1)</literal> man page for more examples.
+        See <literal>picom(1)</literal> man page for more examples.
       '';
     };
 
@@ -125,7 +129,7 @@ in {
       ];
       description = ''
         List of conditions of windows that should have no shadow.
-        See <literal>compton(1)</literal> man page for more examples.
+        See <literal>picom(1)</literal> man page for more examples.
       '';
     };
 
@@ -192,7 +196,7 @@ in {
       apply = x:
         let
           res = x != "none";
-          msg = "The type of services.compton.vSync has changed to bool:"
+          msg = "The type of services.picom.vSync has changed to bool:"
                 + " interpreting ${x} as ${boolToString res}";
         in
           if isBool x then x
@@ -222,13 +226,13 @@ in {
       type = loaOf (types.either configTypes (loaOf (types.either configTypes (loaOf configTypes))));
       default = {};
       description = ''
-        Additional Compton configuration.
+        Additional Picom configuration.
       '';
     };
   };
 
   config = mkIf cfg.enable {
-    services.compton.settings = let
+    services.picom.settings = let
       # Hard conversion to float, literally lib.toInt but toFloat
       toFloat = str: let
         may_be_float = builtins.fromJSON str;
@@ -264,8 +268,8 @@ in {
       refresh-rate     = mkDefault cfg.refreshRate;
     };
 
-    systemd.user.services.compton = {
-      description = "Compton composite manager";
+    systemd.user.services.picom = {
+      description = "Picom composite manager";
       wantedBy = [ "graphical-session.target" ];
       partOf = [ "graphical-session.target" ];
 
@@ -275,13 +279,15 @@ in {
       };
 
       serviceConfig = {
-        ExecStart = "${pkgs.compton}/bin/compton --config ${configFile}";
+        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}";
         RestartSec = 3;
         Restart = "always";
       };
     };
 
-    environment.systemPackages = [ pkgs.compton ];
+    environment.systemPackages = [ pkgs.picom ];
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/x11/unclutter.nix b/nixos/modules/services/x11/unclutter.nix
index 5f16a680050..56e30c79d1f 100644
--- a/nixos/modules/services/x11/unclutter.nix
+++ b/nixos/modules/services/x11/unclutter.nix
@@ -32,7 +32,7 @@ in {
       default = 1;
     };
 
-    threeshold = mkOption {
+    threshold = mkOption {
       description = "Minimum number of pixels considered cursor movement";
       type = types.int;
       default = 1;
@@ -61,7 +61,7 @@ in {
       serviceConfig.ExecStart = ''
         ${cfg.package}/bin/unclutter \
           -idle ${toString cfg.timeout} \
-          -jitter ${toString (cfg.threeshold - 1)} \
+          -jitter ${toString (cfg.threshold - 1)} \
           ${optionalString cfg.keystroke "-keystroke"} \
           ${concatMapStrings (x: " -"+x) cfg.extraOptions} \
           -not ${concatStringsSep " " cfg.excluded} \
@@ -71,4 +71,12 @@ in {
       serviceConfig.Restart = "always";
     };
   };
+
+  imports = [
+    (mkRenamedOptionModule [ "services" "unclutter" "threeshold" ]
+                           [ "services"  "unclutter" "threshold" ])
+  ];
+
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/x11/urxvtd.nix b/nixos/modules/services/x11/urxvtd.nix
index d916fa5bb39..867ac38a944 100644
--- a/nixos/modules/services/x11/urxvtd.nix
+++ b/nixos/modules/services/x11/urxvtd.nix
@@ -18,10 +18,10 @@ in {
     };
 
     package = mkOption {
-      default = pkgs.rxvt_unicode-with-plugins;
-      defaultText = "pkgs.rxvt_unicode-with-plugins";
+      default = pkgs.rxvt-unicode;
+      defaultText = "pkgs.rxvt-unicode";
       description = ''
-        Package to install. Usually pkgs.rxvt_unicode-with-plugins or pkgs.rxvt_unicode
+        Package to install. Usually pkgs.rxvt-unicode.
       '';
       type = types.package;
     };
@@ -45,4 +45,6 @@ in {
     environment.variables.RXVT_SOCKET = "/run/user/$(id -u)/urxvtd-socket";
   };
 
+  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+
 }
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index c17f3830d0e..04a9fc46628 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -59,15 +59,14 @@ in
       };
 
       default = mkOption {
-        type = types.str;
-        default = "none";
+        type = types.nullOr types.str;
+        default = null;
         example = "wmii";
-        description = "Default window manager loaded if none have been chosen.";
-        apply = defaultWM:
-          if any (w: w.name == defaultWM) cfg.session then
-            defaultWM
-          else
-            throw "Default window manager (${defaultWM}) not found.";
+        description = ''
+          <emphasis role="strong">Deprecated</emphasis>, please use <xref linkend="opt-services.xserver.displayManager.defaultSession"/> instead.
+
+          Default window manager loaded if none have been chosen.
+        '';
       };
 
     };
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 70f01dbdbf5..6aec1c0753a 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -149,6 +149,8 @@ in
     [ ./display-managers/default.nix
       ./window-managers/default.nix
       ./desktop-managers/default.nix
+      (mkRemovedOptionModule [ "services" "xserver" "startGnuPGAgent" ]
+        "See the 16.09 release notes for more information.")
     ];
 
 
@@ -329,9 +331,9 @@ in
       };
 
       xkbOptions = mkOption {
-        type = types.str;
+        type = types.commas;
         default = "terminate:ctrl_alt_bksp";
-        example = "grp:caps_toggle, grp_led:scroll";
+        example = "grp:caps_toggle,grp_led:scroll";
         description = ''
           X keyboard options; layout switching goes here.
         '';
@@ -554,8 +556,7 @@ in
 
     services.xserver.displayManager.lightdm.enable =
       let dmconf = cfg.displayManager;
-          default = !( dmconf.auto.enable
-                    || dmconf.gdm.enable
+          default = !(dmconf.gdm.enable
                     || dmconf.sddm.enable
                     || dmconf.xpra.enable );
       in mkIf (default) true;
@@ -572,7 +573,7 @@ in
            then { modules = [xorg.${"xf86video" + name}]; }
            else null)
           knownVideoDrivers;
-      in optional (driver != null) ({ inherit name; modules = []; driverName = name; } // driver));
+      in optional (driver != null) ({ inherit name; modules = []; driverName = name; display = true; } // driver));
 
     assertions = [
       { assertion = config.security.polkit.enable;
@@ -588,19 +589,15 @@ in
     ];
 
     environment.etc =
-      (optionals cfg.exportConfiguration
-        [ { source = "${configFile}";
-            target = "X11/xorg.conf";
-          }
+      (optionalAttrs cfg.exportConfiguration
+        {
+          "X11/xorg.conf".source = "${configFile}";
           # -xkbdir command line option does not seems to be passed to xkbcomp.
-          { source = "${cfg.xkbDir}";
-            target = "X11/xkb";
-          }
-        ])
+          "X11/xkb".source = "${cfg.xkbDir}";
+        })
       # localectl looks into 00-keyboard.conf
-      ++ [
-        {
-          text = ''
+      //{
+          "X11/xorg.conf.d/00-keyboard.conf".text = ''
             Section "InputClass"
               Identifier "Keyboard catchall"
               MatchIsKeyboard "on"
@@ -610,16 +607,12 @@ in
               Option "XkbVariant" "${cfg.xkbVariant}"
             EndSection
           '';
-          target = "X11/xorg.conf.d/00-keyboard.conf";
         }
-      ]
       # Needed since 1.18; see https://bugs.freedesktop.org/show_bug.cgi?id=89023#c5
-      ++ (let cfgPath = "/X11/xorg.conf.d/10-evdev.conf"; in
-        [{
-          source = xorg.xf86inputevdev.out + "/share" + cfgPath;
-          target = cfgPath;
-        }]
-      );
+      // (let cfgPath = "/X11/xorg.conf.d/10-evdev.conf"; in
+        {
+          ${cfgPath}.source = xorg.xf86inputevdev.out + "/share" + cfgPath;
+        });
 
     environment.systemPackages =
       [ xorg.xorgserver.out
@@ -658,8 +651,7 @@ in
     systemd.services.display-manager =
       { description = "X11 Server";
 
-        after = [ "systemd-udev-settle.service" "acpid.service" "systemd-logind.service" ];
-        wants = [ "systemd-udev-settle.service" ];
+        after = [ "acpid.service" "systemd-logind.service" ];
 
         restartIfChanged = false;
 
@@ -747,7 +739,7 @@ in
           ${cfg.serverLayoutSection}
           # Reference the Screen sections for each driver.  This will
           # cause the X server to try each in turn.
-          ${flip concatMapStrings cfg.drivers (d: ''
+          ${flip concatMapStrings (filter (d: d.display) cfg.drivers) (d: ''
             Screen "Screen-${d.name}[0]"
           '')}
         EndSection
@@ -771,42 +763,44 @@ in
             ${driver.deviceSection or ""}
             ${xrandrDeviceSection}
           EndSection
+          ${optionalString driver.display ''
+
+            Section "Screen"
+              Identifier "Screen-${driver.name}[0]"
+              Device "Device-${driver.name}[0]"
+              ${optionalString (cfg.monitorSection != "") ''
+                Monitor "Monitor[0]"
+              ''}
+
+              ${cfg.screenSection}
+              ${driver.screenSection or ""}
+
+              ${optionalString (cfg.defaultDepth != 0) ''
+                DefaultDepth ${toString cfg.defaultDepth}
+              ''}
+
+              ${optionalString
+                  (driver.name != "virtualbox" &&
+                  (cfg.resolutions != [] ||
+                    cfg.extraDisplaySettings != "" ||
+                    cfg.virtualScreen != null))
+                (let
+                  f = depth:
+                    ''
+                      SubSection "Display"
+                        Depth ${toString depth}
+                        ${optionalString (cfg.resolutions != [])
+                          "Modes ${concatMapStrings (res: ''"${toString res.x}x${toString res.y}"'') cfg.resolutions}"}
+                        ${cfg.extraDisplaySettings}
+                        ${optionalString (cfg.virtualScreen != null)
+                          "Virtual ${toString cfg.virtualScreen.x} ${toString cfg.virtualScreen.y}"}
+                      EndSubSection
+                    '';
+                in concatMapStrings f [8 16 24]
+              )}
 
-          Section "Screen"
-            Identifier "Screen-${driver.name}[0]"
-            Device "Device-${driver.name}[0]"
-            ${optionalString (cfg.monitorSection != "") ''
-              Monitor "Monitor[0]"
-            ''}
-
-            ${cfg.screenSection}
-            ${driver.screenSection or ""}
-
-            ${optionalString (cfg.defaultDepth != 0) ''
-              DefaultDepth ${toString cfg.defaultDepth}
-            ''}
-
-            ${optionalString
-                (driver.name != "virtualbox" &&
-                 (cfg.resolutions != [] ||
-                  cfg.extraDisplaySettings != "" ||
-                  cfg.virtualScreen != null))
-              (let
-                f = depth:
-                  ''
-                    SubSection "Display"
-                      Depth ${toString depth}
-                      ${optionalString (cfg.resolutions != [])
-                        "Modes ${concatMapStrings (res: ''"${toString res.x}x${toString res.y}"'') cfg.resolutions}"}
-                      ${cfg.extraDisplaySettings}
-                      ${optionalString (cfg.virtualScreen != null)
-                        "Virtual ${toString cfg.virtualScreen.x} ${toString cfg.virtualScreen.y}"}
-                    EndSubSection
-                  '';
-              in concatMapStrings f [8 16 24]
-            )}
-
-          EndSection
+            EndSection
+          ''}
         '')}
 
         ${xrandrMonitorSections}