summary refs log tree commit diff
path: root/nixos/modules/services
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2021-08-04 10:43:07 +0000
committerAlyssa Ross <hi@alyssa.is>2021-08-04 10:43:07 +0000
commit62614cbef7da005c1eda8c9400160f6bcd6546b8 (patch)
treec2630f69080637987b68acb1ee8676d2681fe304 /nixos/modules/services
parentd9c82ed3044c72cecf01c6ea042489d30914577c (diff)
parente24069138dfec3ef94f211f1da005bb5395adc11 (diff)
downloadnixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.tar
nixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.tar.gz
nixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.tar.bz2
nixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.tar.lz
nixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.tar.xz
nixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.tar.zst
nixpkgs-62614cbef7da005c1eda8c9400160f6bcd6546b8.zip
Merge branch 'nixpkgs-update' into master
Diffstat (limited to 'nixos/modules/services')
-rw-r--r--nixos/modules/services/admin/salt/master.nix4
-rw-r--r--nixos/modules/services/admin/salt/minion.nix2
-rw-r--r--nixos/modules/services/amqp/activemq/default.nix1
-rw-r--r--nixos/modules/services/amqp/rabbitmq.nix2
-rw-r--r--nixos/modules/services/audio/alsa.nix18
-rw-r--r--nixos/modules/services/audio/botamusique.nix114
-rw-r--r--nixos/modules/services/audio/icecast.nix2
-rw-r--r--nixos/modules/services/audio/jack.nix13
-rw-r--r--nixos/modules/services/audio/jmusicbot.nix41
-rw-r--r--nixos/modules/services/audio/mpd.nix127
-rw-r--r--nixos/modules/services/audio/mpdscribble.nix202
-rw-r--r--nixos/modules/services/audio/roon-bridge.nix74
-rw-r--r--nixos/modules/services/audio/slimserver.nix1
-rw-r--r--nixos/modules/services/audio/snapserver.nix39
-rw-r--r--nixos/modules/services/audio/spotifyd.nix1
-rw-r--r--nixos/modules/services/backup/bacula.nix23
-rw-r--r--nixos/modules/services/backup/borgbackup.nix1
-rw-r--r--nixos/modules/services/backup/borgbackup.xml30
-rw-r--r--nixos/modules/services/backup/borgmatic.nix57
-rw-r--r--nixos/modules/services/backup/btrbk.nix220
-rw-r--r--nixos/modules/services/backup/duplicati.nix12
-rw-r--r--nixos/modules/services/backup/duplicity.nix91
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix8
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix17
-rw-r--r--nixos/modules/services/backup/restic.nix15
-rw-r--r--nixos/modules/services/backup/sanoid.nix283
-rw-r--r--nixos/modules/services/backup/syncoid.nix435
-rw-r--r--nixos/modules/services/backup/tarsnap.nix17
-rw-r--r--nixos/modules/services/backup/znapzend.nix6
-rw-r--r--nixos/modules/services/backup/zrepl.nix54
-rw-r--r--nixos/modules/services/blockchain/ethereum/geth.nix178
-rw-r--r--nixos/modules/services/cluster/hadoop/default.nix7
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix46
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/apiserver.nix44
-rw-r--r--nixos/modules/services/cluster/kubernetes/controller-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/default.nix36
-rw-r--r--nixos/modules/services/cluster/kubernetes/flannel.nix40
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix51
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix7
-rw-r--r--nixos/modules/services/cluster/kubernetes/proxy.nix4
-rw-r--r--nixos/modules/services/cluster/kubernetes/scheduler.nix2
-rw-r--r--nixos/modules/services/computing/foldingathome/client.nix10
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix83
-rw-r--r--nixos/modules/services/computing/torque/mom.nix2
-rw-r--r--nixos/modules/services/computing/torque/server.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix3
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildkite-agents.nix13
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix299
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix6
-rw-r--r--nixos/modules/services/continuous-integration/gocd-agent/default.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gocd-server/default.nix3
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix210
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix101
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix64
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/default.nix37
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix64
-rw-r--r--nixos/modules/services/databases/cassandra.nix414
-rw-r--r--nixos/modules/services/databases/clickhouse.nix1
-rw-r--r--nixos/modules/services/databases/couchdb.nix35
-rw-r--r--nixos/modules/services/databases/firebird.nix31
-rw-r--r--nixos/modules/services/databases/foundationdb.nix2
-rw-r--r--nixos/modules/services/databases/memcached.nix15
-rw-r--r--nixos/modules/services/databases/mongodb.nix9
-rw-r--r--nixos/modules/services/databases/mysql.nix19
-rw-r--r--nixos/modules/services/databases/neo4j.nix8
-rw-r--r--nixos/modules/services/databases/openldap.nix454
-rw-r--r--nixos/modules/services/databases/pgmanage.nix1
-rw-r--r--nixos/modules/services/databases/postgresql.nix96
-rw-r--r--nixos/modules/services/databases/redis.nix168
-rw-r--r--nixos/modules/services/databases/riak-cs.nix202
-rw-r--r--nixos/modules/services/databases/riak.nix2
-rw-r--r--nixos/modules/services/databases/stanchion.nix194
-rw-r--r--nixos/modules/services/databases/victoriametrics.nix6
-rw-r--r--nixos/modules/services/databases/virtuoso.nix5
-rw-r--r--nixos/modules/services/desktops/bamf.nix2
-rw-r--r--nixos/modules/services/desktops/deepin/deepin.nix123
-rw-r--r--nixos/modules/services/desktops/espanso.nix1
-rw-r--r--nixos/modules/services/desktops/geoclue2.nix8
-rw-r--r--nixos/modules/services/desktops/gnome/at-spi2-core.nix (renamed from nixos/modules/services/desktops/gnome3/at-spi2-core.nix)14
-rw-r--r--nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix (renamed from nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix)12
-rw-r--r--nixos/modules/services/desktops/gnome/evolution-data-server.nix71
-rw-r--r--nixos/modules/services/desktops/gnome/glib-networking.nix (renamed from nixos/modules/services/desktops/gnome3/glib-networking.nix)12
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-initial-setup.nix (renamed from nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix)16
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-keyring.nix (renamed from nixos/modules/services/desktops/gnome3/gnome-keyring.nix)20
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-online-accounts.nix (renamed from nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix)12
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-online-miners.nix (renamed from nixos/modules/services/desktops/gnome3/gnome-online-miners.nix)16
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix32
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix (renamed from nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix)16
-rw-r--r--nixos/modules/services/desktops/gnome/gnome-user-share.nix (renamed from nixos/modules/services/desktops/gnome3/gnome-user-share.nix)16
-rw-r--r--nixos/modules/services/desktops/gnome/rygel.nix (renamed from nixos/modules/services/desktops/gnome3/rygel.nix)20
-rw-r--r--nixos/modules/services/desktops/gnome/sushi.nix (renamed from nixos/modules/services/desktops/gnome3/sushi.nix)16
-rw-r--r--nixos/modules/services/desktops/gnome/tracker-miners.nix (renamed from nixos/modules/services/desktops/gnome3/tracker-miners.nix)12
-rw-r--r--nixos/modules/services/desktops/gnome/tracker.nix (renamed from nixos/modules/services/desktops/gnome3/tracker.nix)12
-rw-r--r--nixos/modules/services/desktops/gnome3/evolution-data-server.nix45
-rw-r--r--nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix24
-rw-r--r--nixos/modules/services/desktops/gvfs.nix2
-rw-r--r--nixos/modules/services/desktops/pipewire.nix41
-rw-r--r--nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json34
-rw-r--r--nixos/modules/services/desktops/pipewire/bluez-hardware.conf.json197
-rw-r--r--nixos/modules/services/desktops/pipewire/bluez-monitor.conf.json36
-rw-r--r--nixos/modules/services/desktops/pipewire/client-rt.conf.json39
-rw-r--r--nixos/modules/services/desktops/pipewire/client.conf.json31
-rw-r--r--nixos/modules/services/desktops/pipewire/jack.conf.json28
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session.conf.json67
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-media-session.nix135
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json41
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.conf.json93
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix202
-rw-r--r--nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json30
-rw-r--r--nixos/modules/services/desktops/profile-sync-daemon.nix4
-rw-r--r--nixos/modules/services/desktops/telepathy.nix5
-rw-r--r--nixos/modules/services/desktops/tumbler.nix2
-rw-r--r--nixos/modules/services/desktops/zeitgeist.nix2
-rw-r--r--nixos/modules/services/development/blackfire.nix65
-rw-r--r--nixos/modules/services/development/blackfire.xml45
-rw-r--r--nixos/modules/services/development/bloop.nix2
-rw-r--r--nixos/modules/services/development/hoogle.nix12
-rw-r--r--nixos/modules/services/development/jupyter/default.nix2
-rw-r--r--nixos/modules/services/development/jupyterhub/default.nix2
-rw-r--r--nixos/modules/services/development/lorri.nix13
-rw-r--r--nixos/modules/services/display-managers/greetd.nix106
-rw-r--r--nixos/modules/services/editors/emacs.xml12
-rw-r--r--nixos/modules/services/editors/infinoted.nix10
-rw-r--r--nixos/modules/services/games/factorio.nix32
-rw-r--r--nixos/modules/services/games/freeciv.nix187
-rw-r--r--nixos/modules/services/games/minetest-server.nix2
-rw-r--r--nixos/modules/services/games/openarena.nix2
-rw-r--r--nixos/modules/services/games/quake3-server.nix111
-rw-r--r--nixos/modules/services/games/terraria.nix35
-rw-r--r--nixos/modules/services/hardware/acpid.nix31
-rw-r--r--nixos/modules/services/hardware/actkbd.nix2
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix24
-rw-r--r--nixos/modules/services/hardware/bluetooth.nix132
-rw-r--r--nixos/modules/services/hardware/brltty.nix57
-rw-r--r--nixos/modules/services/hardware/ddccontrol.nix36
-rw-r--r--nixos/modules/services/hardware/fancontrol.nix14
-rw-r--r--nixos/modules/services/hardware/fwupd.nix18
-rw-r--r--nixos/modules/services/hardware/lcd.nix5
-rw-r--r--nixos/modules/services/hardware/pcscd.nix86
-rw-r--r--nixos/modules/services/hardware/power-profiles-daemon.nix53
-rw-r--r--nixos/modules/services/hardware/sane.nix19
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4.nix2
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan4_etc_files.nix17
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan5.nix110
-rw-r--r--nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix77
-rw-r--r--nixos/modules/services/hardware/spacenavd.nix25
-rw-r--r--nixos/modules/services/hardware/tcsd.nix35
-rw-r--r--nixos/modules/services/hardware/thermald.nix14
-rw-r--r--nixos/modules/services/hardware/thinkfan.nix260
-rw-r--r--nixos/modules/services/hardware/throttled.nix6
-rw-r--r--nixos/modules/services/hardware/tlp.nix2
-rw-r--r--nixos/modules/services/hardware/trezord.nix4
-rw-r--r--nixos/modules/services/hardware/udev.nix29
-rw-r--r--nixos/modules/services/hardware/undervolt.nix46
-rw-r--r--nixos/modules/services/hardware/xow.nix3
-rw-r--r--nixos/modules/services/logging/graylog.nix6
-rw-r--r--nixos/modules/services/logging/logstash.nix7
-rw-r--r--nixos/modules/services/logging/promtail.nix87
-rw-r--r--nixos/modules/services/logging/vector.nix64
-rw-r--r--nixos/modules/services/mail/dovecot.nix14
-rw-r--r--nixos/modules/services/mail/exim.nix9
-rw-r--r--nixos/modules/services/mail/freepops.nix89
-rw-r--r--nixos/modules/services/mail/mailhog.nix68
-rw-r--r--nixos/modules/services/mail/mailman.nix49
-rw-r--r--nixos/modules/services/mail/mailman.xml45
-rw-r--r--nixos/modules/services/mail/mlmmj.nix43
-rw-r--r--nixos/modules/services/mail/nullmailer.nix1
-rw-r--r--nixos/modules/services/mail/opendkim.nix30
-rw-r--r--nixos/modules/services/mail/postfix.nix54
-rw-r--r--nixos/modules/services/mail/postgrey.nix2
-rw-r--r--nixos/modules/services/mail/roundcube.nix7
-rw-r--r--nixos/modules/services/mail/rspamd.nix48
-rw-r--r--nixos/modules/services/mail/spamassassin.nix65
-rw-r--r--nixos/modules/services/mail/sympa.nix4
-rw-r--r--nixos/modules/services/misc/airsonic.nix2
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix23
-rw-r--r--nixos/modules/services/misc/autofs.nix1
-rw-r--r--nixos/modules/services/misc/autorandr.nix6
-rw-r--r--nixos/modules/services/misc/bazarr.nix1
-rw-r--r--nixos/modules/services/misc/beanstalkd.nix10
-rw-r--r--nixos/modules/services/misc/bees.nix72
-rw-r--r--nixos/modules/services/misc/calibre-server.nix49
-rw-r--r--nixos/modules/services/misc/cfdyndns.nix22
-rw-r--r--nixos/modules/services/misc/cgminer.nix8
-rw-r--r--nixos/modules/services/misc/clipcat.nix31
-rw-r--r--nixos/modules/services/misc/defaultUnicornConfig.rb69
-rw-r--r--nixos/modules/services/misc/dendrite.nix181
-rw-r--r--nixos/modules/services/misc/dictd.nix2
-rw-r--r--nixos/modules/services/misc/disnix.nix13
-rw-r--r--nixos/modules/services/misc/docker-registry.nix2
-rw-r--r--nixos/modules/services/misc/domoticz.nix51
-rw-r--r--nixos/modules/services/misc/duckling.nix39
-rw-r--r--nixos/modules/services/misc/dysnomia.nix88
-rw-r--r--nixos/modules/services/misc/etcd.nix2
-rw-r--r--nixos/modules/services/misc/etebase-server.nix226
-rw-r--r--nixos/modules/services/misc/etesync-dav.nix92
-rw-r--r--nixos/modules/services/misc/exhibitor.nix2
-rw-r--r--nixos/modules/services/misc/felix.nix2
-rw-r--r--nixos/modules/services/misc/fstrim.nix4
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix2
-rw-r--r--nixos/modules/services/misc/geoip-updater.nix306
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix187
-rw-r--r--nixos/modules/services/misc/gitea.nix97
-rw-r--r--nixos/modules/services/misc/gitit.nix1
-rw-r--r--nixos/modules/services/misc/gitlab.nix776
-rw-r--r--nixos/modules/services/misc/gitlab.xml61
-rw-r--r--nixos/modules/services/misc/gitolite.nix2
-rw-r--r--nixos/modules/services/misc/gitweb.nix2
-rw-r--r--nixos/modules/services/misc/gogs.nix8
-rw-r--r--nixos/modules/services/misc/gollum.nix2
-rw-r--r--nixos/modules/services/misc/gpsd.nix2
-rw-r--r--nixos/modules/services/misc/home-assistant.nix147
-rw-r--r--nixos/modules/services/misc/ihaskell.nix1
-rw-r--r--nixos/modules/services/misc/jellyfin.nix60
-rw-r--r--nixos/modules/services/misc/klipper.nix117
-rw-r--r--nixos/modules/services/misc/leaps.nix2
-rw-r--r--nixos/modules/services/misc/lifecycled.nix164
-rw-r--r--nixos/modules/services/misc/mame.nix4
-rw-r--r--nixos/modules/services/misc/matrix-appservice-discord.nix8
-rw-r--r--nixos/modules/services/misc/matrix-appservice-irc.nix229
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix77
-rw-r--r--nixos/modules/services/misc/matrix-synapse.xml11
-rw-r--r--nixos/modules/services/misc/mautrix-telegram.nix23
-rw-r--r--nixos/modules/services/misc/mediatomb.nix237
-rw-r--r--nixos/modules/services/misc/mwlib.nix6
-rw-r--r--nixos/modules/services/misc/n8n.nix78
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix26
-rw-r--r--nixos/modules/services/misc/nix-gc.nix53
-rw-r--r--nixos/modules/services/misc/nzbhydra2.nix78
-rw-r--r--nixos/modules/services/misc/octoprint.nix5
-rw-r--r--nixos/modules/services/misc/ombi.nix81
-rw-r--r--nixos/modules/services/misc/packagekit.nix91
-rw-r--r--nixos/modules/services/misc/paperless.nix2
-rw-r--r--nixos/modules/services/misc/pinnwand.nix69
-rw-r--r--nixos/modules/services/misc/plikd.nix82
-rw-r--r--nixos/modules/services/misc/podgrab.nix50
-rw-r--r--nixos/modules/services/misc/pykms.nix13
-rw-r--r--nixos/modules/services/misc/redmine.nix108
-rw-r--r--nixos/modules/services/misc/rippled.nix1
-rw-r--r--nixos/modules/services/misc/safeeyes.nix4
-rw-r--r--nixos/modules/services/misc/sdrplay.nix35
-rw-r--r--nixos/modules/services/misc/siproxd.nix18
-rw-r--r--nixos/modules/services/misc/snapper.nix12
-rw-r--r--nixos/modules/services/misc/sourcehut/builds.nix234
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix198
-rw-r--r--nixos/modules/services/misc/sourcehut/dispatch.nix125
-rw-r--r--nixos/modules/services/misc/sourcehut/git.nix214
-rw-r--r--nixos/modules/services/misc/sourcehut/hg.nix173
-rw-r--r--nixos/modules/services/misc/sourcehut/hub.nix118
-rw-r--r--nixos/modules/services/misc/sourcehut/lists.nix185
-rw-r--r--nixos/modules/services/misc/sourcehut/man.nix122
-rw-r--r--nixos/modules/services/misc/sourcehut/meta.nix211
-rw-r--r--nixos/modules/services/misc/sourcehut/paste.nix133
-rw-r--r--nixos/modules/services/misc/sourcehut/service.nix66
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml115
-rw-r--r--nixos/modules/services/misc/sourcehut/todo.nix161
-rw-r--r--nixos/modules/services/misc/ssm-agent.nix38
-rw-r--r--nixos/modules/services/misc/sssd.nix4
-rw-r--r--nixos/modules/services/misc/subsonic.nix4
-rw-r--r--nixos/modules/services/misc/svnserve.nix3
-rw-r--r--nixos/modules/services/misc/synergy.nix27
-rw-r--r--nixos/modules/services/misc/weechat.nix1
-rw-r--r--nixos/modules/services/misc/zigbee2mqtt.nix97
-rw-r--r--nixos/modules/services/misc/zookeeper.nix5
-rw-r--r--nixos/modules/services/monitoring/alerta.nix4
-rw-r--r--nixos/modules/services/monitoring/apcupsd.nix2
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix28
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix150
-rw-r--r--nixos/modules/services/monitoring/grafana.nix239
-rw-r--r--nixos/modules/services/monitoring/graphite.nix6
-rw-r--r--nixos/modules/services/monitoring/incron.nix2
-rw-r--r--nixos/modules/services/monitoring/loki.nix4
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix111
-rw-r--r--nixos/modules/services/monitoring/metricbeat.nix152
-rw-r--r--nixos/modules/services/monitoring/monit.nix20
-rw-r--r--nixos/modules/services/monitoring/nagios.nix2
-rw-r--r--nixos/modules/services/monitoring/netdata.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix313
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix68
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix59
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bind.nix12
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bird.nix46
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix82
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix64
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix20
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/domain.nix19
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix17
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/flow.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix40
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/json.nix28
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/kea.nix39
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/knot.nix50
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/mail.nix18
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix10
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginx.nix7
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix51
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/openldap.nix67
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix39
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/pihole.nix74
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postfix.nix25
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/postgres.nix37
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/process.nix48
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix53
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix127
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix78
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/script.nix64
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix60
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/sql.nix104
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/systemd.nix18
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unbound.nix59
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix34
-rw-r--r--nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix26
-rw-r--r--nixos/modules/services/monitoring/scollector.nix2
-rw-r--r--nixos/modules/services/monitoring/smartd.nix2
-rw-r--r--nixos/modules/services/monitoring/teamviewer.nix4
-rw-r--r--nixos/modules/services/monitoring/telegraf.nix61
-rw-r--r--nixos/modules/services/monitoring/thanos.nix4
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix5
-rw-r--r--nixos/modules/services/monitoring/unifi-poller.nix242
-rw-r--r--nixos/modules/services/monitoring/ups.nix2
-rw-r--r--nixos/modules/services/monitoring/vnstat.nix28
-rw-r--r--nixos/modules/services/monitoring/zabbix-agent.nix16
-rw-r--r--nixos/modules/services/network-filesystems/cachefilesd.nix18
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix14
-rw-r--r--nixos/modules/services/network-filesystems/davfs2.nix18
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix53
-rw-r--r--nixos/modules/services/network-filesystems/netatalk.nix140
-rw-r--r--nixos/modules/services/network-filesystems/openafs/client.nix2
-rw-r--r--nixos/modules/services/network-filesystems/openafs/server.nix2
-rw-r--r--nixos/modules/services/network-filesystems/orangefs/server.nix4
-rw-r--r--nixos/modules/services/network-filesystems/rsyncd.nix182
-rw-r--r--nixos/modules/services/network-filesystems/samba-wsdd.nix124
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix17
-rw-r--r--nixos/modules/services/network-filesystems/xtreemfs.nix21
-rw-r--r--nixos/modules/services/network-filesystems/yandex-disk.nix2
-rw-r--r--nixos/modules/services/networking/adguardhome.nix78
-rw-r--r--nixos/modules/services/networking/amuled.nix4
-rw-r--r--nixos/modules/services/networking/autossh.nix2
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix7
-rw-r--r--nixos/modules/services/networking/babeld.nix57
-rw-r--r--nixos/modules/services/networking/bee-clef.nix107
-rw-r--r--nixos/modules/services/networking/bee.nix149
-rw-r--r--nixos/modules/services/networking/biboumi.nix269
-rw-r--r--nixos/modules/services/networking/bind.nix102
-rw-r--r--nixos/modules/services/networking/bird.nix15
-rw-r--r--nixos/modules/services/networking/bitlbee.nix4
-rw-r--r--nixos/modules/services/networking/blockbook-frontend.nix24
-rw-r--r--nixos/modules/services/networking/cjdns.nix18
-rw-r--r--nixos/modules/services/networking/cntlm.nix9
-rw-r--r--nixos/modules/services/networking/connman.nix3
-rw-r--r--nixos/modules/services/networking/consul.nix3
-rw-r--r--nixos/modules/services/networking/corerad.nix14
-rw-r--r--nixos/modules/services/networking/coturn.nix99
-rw-r--r--nixos/modules/services/networking/croc.nix86
-rw-r--r--nixos/modules/services/networking/ddclient.nix11
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix8
-rw-r--r--nixos/modules/services/networking/dnscrypt-proxy2.nix67
-rw-r--r--nixos/modules/services/networking/dnscrypt-wrapper.nix9
-rw-r--r--nixos/modules/services/networking/dnsdist.nix29
-rw-r--r--nixos/modules/services/networking/doh-proxy-rust.nix60
-rw-r--r--nixos/modules/services/networking/epmd.nix2
-rw-r--r--nixos/modules/services/networking/firefox/sync-server.nix2
-rw-r--r--nixos/modules/services/networking/flannel.nix6
-rw-r--r--nixos/modules/services/networking/flashpolicyd.nix85
-rw-r--r--nixos/modules/services/networking/gale.nix181
-rw-r--r--nixos/modules/services/networking/gateone.nix4
-rw-r--r--nixos/modules/services/networking/ghostunnel.nix242
-rw-r--r--nixos/modules/services/networking/git-daemon.nix2
-rw-r--r--nixos/modules/services/networking/globalprotect-vpn.nix43
-rw-r--r--nixos/modules/services/networking/go-neb.nix34
-rw-r--r--nixos/modules/services/networking/gobgpd.nix64
-rw-r--r--nixos/modules/services/networking/gogoclient.nix2
-rw-r--r--nixos/modules/services/networking/gvpe.nix10
-rw-r--r--nixos/modules/services/networking/hans.nix2
-rw-r--r--nixos/modules/services/networking/heyefi.nix82
-rw-r--r--nixos/modules/services/networking/hostapd.nix5
-rwxr-xr-xnixos/modules/services/networking/hylafax/faxq-wait.sh2
-rw-r--r--nixos/modules/services/networking/hylafax/modem-default.nix6
-rw-r--r--nixos/modules/services/networking/hylafax/options.nix37
-rwxr-xr-xnixos/modules/services/networking/hylafax/spool.sh6
-rw-r--r--nixos/modules/services/networking/hylafax/systemd.nix42
-rw-r--r--nixos/modules/services/networking/icecream/daemon.nix155
-rw-r--r--nixos/modules/services/networking/icecream/scheduler.nix101
-rw-r--r--nixos/modules/services/networking/inspircd.nix62
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/default.nix10
-rw-r--r--nixos/modules/services/networking/iscsi/initiator.nix84
-rw-r--r--nixos/modules/services/networking/iscsi/root-initiator.nix181
-rw-r--r--nixos/modules/services/networking/iscsi/target.nix53
-rw-r--r--nixos/modules/services/networking/iwd.nix37
-rw-r--r--nixos/modules/services/networking/jitsi-videobridge.nix12
-rw-r--r--nixos/modules/services/networking/kea.nix361
-rw-r--r--nixos/modules/services/networking/kippo.nix14
-rw-r--r--nixos/modules/services/networking/kresd.nix44
-rw-r--r--nixos/modules/services/networking/libreswan.nix147
-rw-r--r--nixos/modules/services/networking/mailpile.nix4
-rw-r--r--nixos/modules/services/networking/matterbridge.nix6
-rw-r--r--nixos/modules/services/networking/monero.nix24
-rw-r--r--nixos/modules/services/networking/morty.nix16
-rw-r--r--nixos/modules/services/networking/mosquitto.nix84
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix9
-rw-r--r--nixos/modules/services/networking/murmur.nix60
-rw-r--r--nixos/modules/services/networking/mxisd.nix4
-rw-r--r--nixos/modules/services/networking/namecoind.nix8
-rw-r--r--nixos/modules/services/networking/nar-serve.nix55
-rw-r--r--nixos/modules/services/networking/nat.nix120
-rw-r--r--nixos/modules/services/networking/ncdns.nix6
-rw-r--r--nixos/modules/services/networking/nebula.nix219
-rw-r--r--nixos/modules/services/networking/networkmanager.nix115
-rw-r--r--nixos/modules/services/networking/nextdns.nix4
-rw-r--r--nixos/modules/services/networking/nftables.nix2
-rw-r--r--nixos/modules/services/networking/nix-serve.nix10
-rw-r--r--nixos/modules/services/networking/nix-store-gcs-proxy.nix2
-rw-r--r--nixos/modules/services/networking/nomad.nix165
-rw-r--r--nixos/modules/services/networking/nsd.nix26
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix57
-rw-r--r--nixos/modules/services/networking/ntp/ntpd.nix1
-rw-r--r--nixos/modules/services/networking/nylon.nix4
-rw-r--r--nixos/modules/services/networking/onedrive.nix2
-rw-r--r--nixos/modules/services/networking/openvpn.nix4
-rw-r--r--nixos/modules/services/networking/owamp.nix2
-rw-r--r--nixos/modules/services/networking/pdns-recursor.nix44
-rw-r--r--nixos/modules/services/networking/pixiecore.nix1
-rw-r--r--nixos/modules/services/networking/pleroma.nix141
-rw-r--r--nixos/modules/services/networking/pleroma.xml132
-rw-r--r--nixos/modules/services/networking/powerdns.nix40
-rw-r--r--nixos/modules/services/networking/pppd.nix26
-rw-r--r--nixos/modules/services/networking/prayer.nix3
-rw-r--r--nixos/modules/services/networking/privoxy.nix295
-rw-r--r--nixos/modules/services/networking/prosody.nix4
-rw-r--r--nixos/modules/services/networking/prosody.xml13
-rw-r--r--nixos/modules/services/networking/quagga.nix185
-rw-r--r--nixos/modules/services/networking/quassel.nix6
-rw-r--r--nixos/modules/services/networking/quicktun.nix2
-rw-r--r--nixos/modules/services/networking/radicale.nix196
-rw-r--r--nixos/modules/services/networking/radvd.nix1
-rw-r--r--nixos/modules/services/networking/resilio.nix1
-rw-r--r--nixos/modules/services/networking/robustirc-bridge.nix47
-rw-r--r--nixos/modules/services/networking/rxe.nix4
-rw-r--r--nixos/modules/services/networking/sabnzbd.nix3
-rw-r--r--nixos/modules/services/networking/searx.nix214
-rw-r--r--nixos/modules/services/networking/seeks.nix75
-rw-r--r--nixos/modules/services/networking/shadowsocks.nix54
-rw-r--r--nixos/modules/services/networking/shairport-sync.nix2
-rw-r--r--nixos/modules/services/networking/shellhub-agent.nix91
-rw-r--r--nixos/modules/services/networking/smartdns.nix1
-rw-r--r--nixos/modules/services/networking/smokeping.nix30
-rw-r--r--nixos/modules/services/networking/solanum.nix109
-rw-r--r--nixos/modules/services/networking/spacecookie.nix161
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix18
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix57
-rw-r--r--nixos/modules/services/networking/sslh.nix4
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/module.nix2
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix20
-rw-r--r--nixos/modules/services/networking/strongswan.nix2
-rw-r--r--nixos/modules/services/networking/stunnel.nix9
-rw-r--r--nixos/modules/services/networking/supplicant.nix13
-rw-r--r--nixos/modules/services/networking/supybot.nix7
-rw-r--r--nixos/modules/services/networking/syncthing.nix13
-rw-r--r--nixos/modules/services/networking/tailscale.nix47
-rw-r--r--nixos/modules/services/networking/tinc.nix240
-rw-r--r--nixos/modules/services/networking/ucarp.nix183
-rw-r--r--nixos/modules/services/networking/unbound.nix311
-rw-r--r--nixos/modules/services/networking/wakeonlan.nix4
-rw-r--r--nixos/modules/services/networking/wasabibackend.nix2
-rw-r--r--nixos/modules/services/networking/wg-quick.nix8
-rw-r--r--nixos/modules/services/networking/wireguard.nix146
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix48
-rw-r--r--nixos/modules/services/networking/x2goserver.nix161
-rw-r--r--nixos/modules/services/networking/xrdp.nix8
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix5
-rw-r--r--nixos/modules/services/networking/zerobin.nix2
-rw-r--r--nixos/modules/services/networking/znc/default.nix39
-rw-r--r--nixos/modules/services/networking/znc/options.nix8
-rw-r--r--nixos/modules/services/printing/cupsd.nix4
-rw-r--r--nixos/modules/services/scheduling/atd.nix11
-rw-r--r--nixos/modules/services/search/elasticsearch-curator.nix1
-rw-r--r--nixos/modules/services/security/clamav.nix67
-rw-r--r--nixos/modules/services/security/fail2ban.nix44
-rw-r--r--nixos/modules/services/security/fprintd.nix34
-rw-r--r--nixos/modules/services/security/fprot.nix3
-rw-r--r--nixos/modules/services/security/hockeypuck.nix104
-rw-r--r--nixos/modules/services/security/hologram-agent.nix2
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix11
-rw-r--r--nixos/modules/services/security/oauth2_proxy_nginx.nix7
-rw-r--r--nixos/modules/services/security/physlock.nix10
-rw-r--r--nixos/modules/services/security/privacyidea.nix33
-rw-r--r--nixos/modules/services/security/sshguard.nix42
-rw-r--r--nixos/modules/services/security/step-ca.nix134
-rw-r--r--nixos/modules/services/security/tor.nix1425
-rw-r--r--nixos/modules/services/security/usbguard.nix6
-rw-r--r--nixos/modules/services/security/vault.nix50
-rw-r--r--nixos/modules/services/security/vaultwarden/backup.sh (renamed from nixos/modules/services/security/bitwarden_rs/backup.sh)2
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix (renamed from nixos/modules/services/security/bitwarden_rs/default.nix)87
-rw-r--r--nixos/modules/services/system/cloud-init.nix6
-rw-r--r--nixos/modules/services/system/dbus.nix35
-rw-r--r--nixos/modules/services/system/localtime.nix9
-rw-r--r--nixos/modules/services/system/self-deploy.nix172
-rw-r--r--nixos/modules/services/torrent/deluge.nix1
-rw-r--r--nixos/modules/services/torrent/transmission.nix128
-rw-r--r--nixos/modules/services/ttys/getty.nix (renamed from nixos/modules/services/ttys/agetty.nix)67
-rw-r--r--nixos/modules/services/ttys/kmscon.nix7
-rw-r--r--nixos/modules/services/video/epgstation/default.nix295
-rw-r--r--nixos/modules/services/video/epgstation/streaming.json119
-rw-r--r--nixos/modules/services/video/mirakurun.nix47
-rw-r--r--nixos/modules/services/video/unifi-video.nix265
-rw-r--r--nixos/modules/services/wayland/cage.nix3
-rw-r--r--nixos/modules/services/web-apps/bookstack.nix368
-rw-r--r--nixos/modules/services/web-apps/calibre-web.nix165
-rw-r--r--nixos/modules/services/web-apps/discourse.nix1064
-rw-r--r--nixos/modules/services/web-apps/discourse.xml344
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix10
-rw-r--r--nixos/modules/services/web-apps/engelsystem.nix4
-rw-r--r--nixos/modules/services/web-apps/frab.nix222
-rw-r--r--nixos/modules/services/web-apps/galene.nix180
-rw-r--r--nixos/modules/services/web-apps/gerrit.nix2
-rw-r--r--nixos/modules/services/web-apps/grocy.nix6
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix (renamed from nixos/modules/services/web-apps/codimd.nix)148
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix142
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix20
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix9
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix7
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.xml55
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix736
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml206
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix599
-rw-r--r--nixos/modules/services/web-apps/matomo.nix12
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix1
-rw-r--r--nixos/modules/services/web-apps/miniflux.nix26
-rw-r--r--nixos/modules/services/web-apps/moinmoin.nix7
-rw-r--r--nixos/modules/services/web-apps/moodle.nix4
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix210
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml17
-rw-r--r--nixos/modules/services/web-apps/plantuml-server.nix123
-rw-r--r--nixos/modules/services/web-apps/plausible.nix285
-rw-r--r--nixos/modules/services/web-apps/plausible.xml51
-rw-r--r--nixos/modules/services/web-apps/shiori.nix48
-rw-r--r--nixos/modules/services/web-apps/sogo.nix1
-rw-r--r--nixos/modules/services/web-apps/trilium.nix13
-rw-r--r--nixos/modules/services/web-apps/tt-rss.nix2
-rw-r--r--nixos/modules/services/web-apps/vikunja.nix145
-rw-r--r--nixos/modules/services/web-apps/whitebophir.nix52
-rw-r--r--nixos/modules/services/web-apps/wiki-js.nix139
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix134
-rw-r--r--nixos/modules/services/web-apps/zabbix.nix16
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix121
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix2
-rw-r--r--nixos/modules/services/web-servers/caddy.nix124
-rw-r--r--nixos/modules/services/web-servers/darkhttpd.nix2
-rw-r--r--nixos/modules/services/web-servers/jboss/default.nix6
-rw-r--r--nixos/modules/services/web-servers/lighttpd/default.nix4
-rw-r--r--nixos/modules/services/web-servers/minio.nix36
-rw-r--r--nixos/modules/services/web-servers/molly-brown.nix22
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix304
-rw-r--r--nixos/modules/services/web-servers/nginx/gitweb.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix40
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix32
-rw-r--r--nixos/modules/services/web-servers/phpfpm/default.nix8
-rw-r--r--nixos/modules/services/web-servers/pomerium.nix131
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix1
-rw-r--r--nixos/modules/services/web-servers/traefik.nix4
-rw-r--r--nixos/modules/services/web-servers/trafficserver.nix318
-rw-r--r--nixos/modules/services/web-servers/ttyd.nix2
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix2
-rw-r--r--nixos/modules/services/web-servers/uwsgi.nix72
-rw-r--r--nixos/modules/services/x11/clight.nix30
-rw-r--r--nixos/modules/services/x11/desktop-managers/cde.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix211
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix3
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.nix590
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.xml277
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix397
-rw-r--r--nixos/modules/services/x11/desktop-managers/kodi.nix14
-rw-r--r--nixos/modules/services/x11/desktop-managers/lxqt.nix4
-rw-r--r--nixos/modules/services/x11/desktop-managers/mate.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix28
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix35
-rw-r--r--nixos/modules/services/x11/desktop-managers/xfce.nix16
-rw-r--r--nixos/modules/services/x11/display-managers/account-service-util.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix70
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix43
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix4
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix13
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix3
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix185
-rw-r--r--nixos/modules/services/x11/display-managers/startx.nix12
-rw-r--r--nixos/modules/services/x11/hardware/libinput.nix97
-rw-r--r--nixos/modules/services/x11/picom.nix13
-rw-r--r--nixos/modules/services/x11/redshift.nix11
-rw-r--r--nixos/modules/services/x11/terminal-server.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/clfswm.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix3
-rw-r--r--nixos/modules/services/x11/window-managers/e16.nix26
-rw-r--r--nixos/modules/services/x11/window-managers/evilwm.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/exwm.nix18
-rw-r--r--nixos/modules/services/x11/window-managers/fvwm.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/herbstluftwm.nix13
-rw-r--r--nixos/modules/services/x11/window-managers/metacity.nix6
-rw-r--r--nixos/modules/services/x11/window-managers/wmderland.nix61
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix125
-rw-r--r--nixos/modules/services/x11/xserver.nix132
604 files changed, 31642 insertions, 7494 deletions
diff --git a/nixos/modules/services/admin/salt/master.nix b/nixos/modules/services/admin/salt/master.nix
index c6b1b0cc0bd..a3069c81c19 100644
--- a/nixos/modules/services/admin/salt/master.nix
+++ b/nixos/modules/services/admin/salt/master.nix
@@ -45,7 +45,7 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       path = with pkgs; [
-        utillinux  # for dmesg
+        util-linux  # for dmesg
       ];
       serviceConfig = {
         ExecStart = "${pkgs.salt}/bin/salt-master";
@@ -59,5 +59,5 @@ in
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ aneeshusa ];
+  meta.maintainers = with lib.maintainers; [ Flakebi ];
 }
diff --git a/nixos/modules/services/admin/salt/minion.nix b/nixos/modules/services/admin/salt/minion.nix
index c8fa9461a20..ac124c570d8 100644
--- a/nixos/modules/services/admin/salt/minion.nix
+++ b/nixos/modules/services/admin/salt/minion.nix
@@ -50,7 +50,7 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       path = with pkgs; [
-        utillinux
+        util-linux
       ];
       serviceConfig = {
         ExecStart = "${pkgs.salt}/bin/salt-minion";
diff --git a/nixos/modules/services/amqp/activemq/default.nix b/nixos/modules/services/amqp/activemq/default.nix
index 160dbddcd48..178b2f6e144 100644
--- a/nixos/modules/services/amqp/activemq/default.nix
+++ b/nixos/modules/services/amqp/activemq/default.nix
@@ -33,6 +33,7 @@ in {
       };
       configurationDir = mkOption {
         default = "${activemq}/conf";
+        type = types.str;
         description = ''
           The base directory for ActiveMQ's configuration.
           By default, this directory is searched for a file named activemq.xml,
diff --git a/nixos/modules/services/amqp/rabbitmq.nix b/nixos/modules/services/amqp/rabbitmq.nix
index 646708e01c4..fc8a1bc3c23 100644
--- a/nixos/modules/services/amqp/rabbitmq.nix
+++ b/nixos/modules/services/amqp/rabbitmq.nix
@@ -57,7 +57,7 @@ in {
         description = ''
           Port on which RabbitMQ will listen for AMQP connections.
         '';
-        type = types.int;
+        type = types.port;
       };
 
       dataDir = mkOption {
diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix
index 3fe76a16540..0d743ed31da 100644
--- a/nixos/modules/services/audio/alsa.nix
+++ b/nixos/modules/services/audio/alsa.nix
@@ -5,7 +5,7 @@ with lib;
 
 let
 
-  inherit (pkgs) alsaUtils;
+  inherit (pkgs) alsa-utils;
 
   pulseaudioEnabled = config.hardware.pulseaudio.enable;
 
@@ -32,7 +32,7 @@ in
 
       enableOSSEmulation = mkOption {
         type = types.bool;
-        default = true;
+        default = false;
         description = ''
           Whether to enable ALSA OSS emulation (with certain cards sound mixing may not work!).
         '';
@@ -88,13 +88,13 @@ in
 
   config = mkIf config.sound.enable {
 
-    environment.systemPackages = [ alsaUtils ];
+    environment.systemPackages = [ alsa-utils ];
 
     environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "")
       { "asound.conf".text = config.sound.extraConfig; };
 
     # ALSA provides a udev rule for restoring volume settings.
-    services.udev.packages = [ alsaUtils ];
+    services.udev.packages = [ alsa-utils ];
 
     boot.kernelModules = optional config.sound.enableOSSEmulation "snd_pcm_oss";
 
@@ -107,7 +107,7 @@ in
           Type = "oneshot";
           RemainAfterExit = true;
           ExecStart = "${pkgs.coreutils}/bin/mkdir -p /var/lib/alsa";
-          ExecStop = "${alsaUtils}/sbin/alsactl store --ignore";
+          ExecStop = "${alsa-utils}/sbin/alsactl store --ignore";
         };
       };
 
@@ -115,16 +115,16 @@ in
       enable = true;
       bindings = [
         # "Mute" media key
-        { keys = [ 113 ]; events = [ "key" ];       command = "${alsaUtils}/bin/amixer -q set Master toggle"; }
+        { keys = [ 113 ]; events = [ "key" ];       command = "${alsa-utils}/bin/amixer -q set Master toggle"; }
 
         # "Lower Volume" media key
-        { keys = [ 114 ]; events = [ "key" "rep" ]; command = "${alsaUtils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}- unmute"; }
+        { keys = [ 114 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}- unmute"; }
 
         # "Raise Volume" media key
-        { keys = [ 115 ]; events = [ "key" "rep" ]; command = "${alsaUtils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}+ unmute"; }
+        { keys = [ 115 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}+ unmute"; }
 
         # "Mic Mute" media key
-        { keys = [ 190 ]; events = [ "key" ];       command = "${alsaUtils}/bin/amixer -q set Capture toggle"; }
+        { keys = [ 190 ]; events = [ "key" ];       command = "${alsa-utils}/bin/amixer -q set Capture toggle"; }
       ];
     };
 
diff --git a/nixos/modules/services/audio/botamusique.nix b/nixos/modules/services/audio/botamusique.nix
new file mode 100644
index 00000000000..14614d2dd16
--- /dev/null
+++ b/nixos/modules/services/audio/botamusique.nix
@@ -0,0 +1,114 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.botamusique;
+
+  format = pkgs.formats.ini {};
+  configFile = format.generate "botamusique.ini" cfg.settings;
+in
+{
+  meta.maintainers = with lib.maintainers; [ hexa ];
+
+  options.services.botamusique = {
+    enable = mkEnableOption "botamusique, a bot to play audio streams on mumble";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.botamusique;
+      description = "The botamusique package to use.";
+    };
+
+    settings = mkOption {
+      type = with types; submodule {
+        freeformType = format.type;
+        options = {
+          server.host = mkOption {
+            type = types.str;
+            default = "localhost";
+            example = "mumble.example.com";
+            description = "Hostname of the mumble server to connect to.";
+          };
+
+          server.port = mkOption {
+            type = types.port;
+            default = 64738;
+            description = "Port of the mumble server to connect to.";
+          };
+
+          bot.username = mkOption {
+            type = types.str;
+            default = "botamusique";
+            description = "Name the bot should appear with.";
+          };
+
+          bot.comment = mkOption {
+            type = types.str;
+            default = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!";
+            description = "Comment displayed for the bot.";
+          };
+        };
+      };
+      default = {};
+      description = ''
+        Your <filename>configuration.ini</filename> as a Nix attribute set. Look up
+        possible options in the <link xlink:href="https://github.com/azlux/botamusique/blob/master/configuration.example.ini">configuration.example.ini</link>.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.botamusique = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      unitConfig.Documentation = "https://github.com/azlux/botamusique/wiki";
+
+      environment.HOME = "/var/lib/botamusique";
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/botamusique --config ${configFile}";
+        Restart = "always"; # the bot exits when the server connection is lost
+
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DynamicUser = true;
+        IPAddressDeny = [
+          "link-local"
+          "multicast"
+        ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        ProcSubset = "pid";
+        PrivateDevices = true;
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        StateDirectory = "botamusique";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
+        WorkingDirectory = "/var/lib/botamusique";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/icecast.nix b/nixos/modules/services/audio/icecast.nix
index f40ea6be29d..6ca20a7a108 100644
--- a/nixos/modules/services/audio/icecast.nix
+++ b/nixos/modules/services/audio/icecast.nix
@@ -47,7 +47,7 @@ in {
       enable = mkEnableOption "Icecast server";
 
       hostname = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         description = "DNS name or IP address that will be used for the stream directory lookups or possibily the playlist generation if a Host header is not provided.";
         default = config.networking.domain;
       };
diff --git a/nixos/modules/services/audio/jack.nix b/nixos/modules/services/audio/jack.nix
index ceff366d0bb..d0a95b87ee1 100644
--- a/nixos/modules/services/audio/jack.nix
+++ b/nixos/modules/services/audio/jack.nix
@@ -8,7 +8,7 @@ let
   pcmPlugin = cfg.jackd.enable && cfg.alsa.enable;
   loopback = cfg.jackd.enable && cfg.loopback.enable;
 
-  enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsaLib != null;
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null;
 
   umaskNeeded = versionOlder cfg.jackd.package.version "1.9.12";
   bridgeNeeded = versionAtLeast cfg.jackd.package.version "1.9.12";
@@ -129,9 +129,9 @@ in {
     (mkIf pcmPlugin {
       sound.extraConfig = ''
         pcm_type.jack {
-          libs.native = ${pkgs.alsaPlugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
+          libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
           ${lib.optionalString enable32BitAlsaPlugins
-          "libs.32Bit = ${pkgs.pkgsi686Linux.alsaPlugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"}
+          "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"}
         }
         pcm.!default {
           @func getenv
@@ -234,7 +234,7 @@ in {
 
       environment = {
         systemPackages = [ cfg.jackd.package ];
-        etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsaPlugins}/etc/alsa/conf.d/50-jack.conf";
+        etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf";
         variables.JACK_PROMISCUOUS_SERVER = "jackaudio";
       };
 
@@ -246,6 +246,9 @@ in {
         description = "JACK Audio Connection Kit";
         serviceConfig = {
           User = "jackaudio";
+          SupplementaryGroups = lib.optional
+            (config.hardware.pulseaudio.enable
+            && !config.hardware.pulseaudio.systemWide) "users";
           ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
           LimitRTPRIO = 99;
           LimitMEMLOCK = "infinity";
@@ -287,5 +290,5 @@ in {
 
   ];
 
-  meta.maintainers = [ maintainers.gnidorah ];
+  meta.maintainers = [ ];
 }
diff --git a/nixos/modules/services/audio/jmusicbot.nix b/nixos/modules/services/audio/jmusicbot.nix
new file mode 100644
index 00000000000..f573bd2ab8d
--- /dev/null
+++ b/nixos/modules/services/audio/jmusicbot.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.jmusicbot;
+in
+{
+  options = {
+    services.jmusicbot = {
+      enable = mkEnableOption "jmusicbot, a Discord music bot that's easy to set up and run yourself";
+
+      stateDir = mkOption {
+        type = types.path;
+        description = ''
+          The directory where config.txt and serversettings.json is saved.
+          If left as the default value this directory will automatically be created before JMusicBot starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions.
+          Untouched by the value of this option config.txt needs to be placed manually into this directory.
+        '';
+        default = "/var/lib/jmusicbot/";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.jmusicbot = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      description = "Discord music bot that's easy to set up and run yourself!";
+      serviceConfig = mkMerge [{
+        ExecStart = "${pkgs.jmusicbot}/bin/JMusicBot";
+        WorkingDirectory = cfg.stateDir;
+        Restart = "always";
+        RestartSec = 20;
+        DynamicUser = true;
+      }
+        (mkIf (cfg.stateDir == "/var/lib/jmusicbot") { StateDirectory = "jmusicbot"; })];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ SuperSandro2000 ];
+}
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index 1d2a982ac53..e33e860d883 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -10,7 +10,19 @@ let
   gid = config.ids.gids.mpd;
   cfg = config.services.mpd;
 
+  credentialsPlaceholder = (creds:
+    let
+      placeholders = (imap0
+        (i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'')
+        creds);
+    in
+      concatStringsSep "\n" placeholders);
+
   mpdConf = pkgs.writeText "mpd.conf" ''
+    # This file was automatically generated by NixOS. Edit mpd's configuration
+    # via NixOS' configuration.nix, as this file will be rewritten upon mpd's
+    # restart.
+
     music_directory     "${cfg.musicDirectory}"
     playlist_directory  "${cfg.playlistDirectory}"
     ${lib.optionalString (cfg.dbFile != null) ''
@@ -28,6 +40,8 @@ let
       }
     ''}
 
+    ${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)}
+
     ${cfg.extraConfig}
   '';
 
@@ -60,18 +74,24 @@ in {
       musicDirectory = mkOption {
         type = with types; either path (strMatching "(http|https|nfs|smb)://.+");
         default = "${cfg.dataDir}/music";
-        defaultText = ''''${dataDir}/music'';
+        defaultText = "\${dataDir}/music";
         description = ''
-          The directory or NFS/SMB network share where mpd reads music from.
+          The directory or NFS/SMB network share where MPD reads music from. If left
+          as the default value this directory will automatically be created before
+          the MPD server starts, otherwise the sysadmin is responsible for ensuring
+          the directory exists with appropriate ownership and permissions.
         '';
       };
 
       playlistDirectory = mkOption {
         type = types.path;
         default = "${cfg.dataDir}/playlists";
-        defaultText = ''''${dataDir}/playlists'';
+        defaultText = "\${dataDir}/playlists";
         description = ''
-          The directory where mpd stores playlists.
+          The directory where MPD stores playlists. If left as the default value
+          this directory will automatically be created before the MPD server starts,
+          otherwise the sysadmin is responsible for ensuring the directory exists
+          with appropriate ownership and permissions.
         '';
       };
 
@@ -90,8 +110,10 @@ in {
         type = types.path;
         default = "/var/lib/${name}";
         description = ''
-          The directory where MPD stores its state, tag cache,
-          playlists etc.
+          The directory where MPD stores its state, tag cache, playlists etc. If
+          left as the default value this directory will automatically be created
+          before the MPD server starts, otherwise the sysadmin is responsible for
+          ensuring the directory exists with appropriate ownership and permissions.
         '';
       };
 
@@ -133,13 +155,44 @@ in {
       dbFile = mkOption {
         type = types.nullOr types.str;
         default = "${cfg.dataDir}/tag_cache";
-        defaultText = ''''${dataDir}/tag_cache'';
+        defaultText = "\${dataDir}/tag_cache";
         description = ''
           The path to MPD's database. If set to <literal>null</literal> the
           parameter is omitted from the configuration.
         '';
       };
 
+      credentials = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            passwordFile = mkOption {
+              type = types.path;
+              description = ''
+                Path to file containing the password.
+              '';
+            };
+            permissions = let
+              perms = ["read" "add" "control" "admin"];
+            in mkOption {
+              type = types.listOf (types.enum perms);
+              default = [ "read" ];
+              description = ''
+                List of permissions that are granted with this password.
+                Permissions can be "${concatStringsSep "\", \"" perms}".
+              '';
+            };
+          };
+        });
+        description = ''
+          Credentials and permissions for accessing the mpd server.
+        '';
+        default = [];
+        example = [
+          {passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];}
+          {passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];}
+        ];
+      };
+
       fluidsynth = mkOption {
         type = types.bool;
         default = false;
@@ -160,7 +213,9 @@ in {
       description = "Music Player Daemon Socket";
       wantedBy = [ "sockets.target" ];
       listenStreams = [
-        "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}"
+        (if pkgs.lib.hasPrefix "/" cfg.network.listenAddress
+          then cfg.network.listenAddress
+          else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}")
       ];
       socketConfig = {
         Backlog = 5;
@@ -169,31 +224,47 @@ in {
       };
     };
 
-    systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.group} - -"
-      "d '${cfg.playlistDirectory}' - ${cfg.user} ${cfg.group} - -"
-    ];
-
     systemd.services.mpd = {
       after = [ "network.target" "sound.target" ];
       description = "Music Player Daemon";
       wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
 
-      serviceConfig = {
-        User = "${cfg.user}";
-        ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon ${mpdConf}";
-        Type = "notify";
-        LimitRTPRIO = 50;
-        LimitRTTIME = "infinity";
-        ProtectSystem = true;
-        NoNewPrivileges = true;
-        ProtectKernelTunables = true;
-        ProtectControlGroups = true;
-        ProtectKernelModules = true;
-        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
-        RestrictNamespaces = true;
-        Restart = "always";
-      };
+      serviceConfig = mkMerge [
+        {
+          User = "${cfg.user}";
+          ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon /run/mpd/mpd.conf";
+          ExecStartPre = pkgs.writeShellScript "mpd-start-pre" (''
+            set -euo pipefail
+            install -m 600 ${mpdConf} /run/mpd/mpd.conf
+          '' + optionalString (cfg.credentials != [])
+            (concatStringsSep "\n"
+              (imap0
+                (i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'')
+                cfg.credentials))
+          );
+          RuntimeDirectory = "mpd";
+          Type = "notify";
+          LimitRTPRIO = 50;
+          LimitRTTIME = "infinity";
+          ProtectSystem = true;
+          NoNewPrivileges = true;
+          ProtectKernelTunables = true;
+          ProtectControlGroups = true;
+          ProtectKernelModules = true;
+          RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
+          RestrictNamespaces = true;
+          Restart = "always";
+        }
+        (mkIf (cfg.dataDir == "/var/lib/${name}") {
+          StateDirectory = [ name ];
+        })
+        (mkIf (cfg.playlistDirectory == "/var/lib/${name}/playlists") {
+          StateDirectory = [ name "${name}/playlists" ];
+        })
+        (mkIf (cfg.musicDirectory == "/var/lib/${name}/music") {
+          StateDirectory = [ name "${name}/music" ];
+        })
+      ];
     };
 
     users.users = optionalAttrs (cfg.user == name) {
diff --git a/nixos/modules/services/audio/mpdscribble.nix b/nixos/modules/services/audio/mpdscribble.nix
new file mode 100644
index 00000000000..1368543ae1a
--- /dev/null
+++ b/nixos/modules/services/audio/mpdscribble.nix
@@ -0,0 +1,202 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mpdscribble;
+  mpdCfg = config.services.mpd;
+
+  endpointUrls = {
+    "last.fm" = "http://post.audioscrobbler.com";
+    "libre.fm" = "http://turtle.libre.fm";
+    "jamendo" = "http://postaudioscrobbler.jamendo.com";
+    "listenbrainz" = "http://proxy.listenbrainz.org";
+  };
+
+  mkSection = secname: secCfg: ''
+    [${secname}]
+    url      = ${secCfg.url}
+    username = ${secCfg.username}
+    password = {{${secname}_PASSWORD}}
+    journal  = /var/lib/mpdscribble/${secname}.journal
+  '';
+
+  endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints);
+  cfgTemplate = pkgs.writeText "mpdscribble.conf" ''
+    ## This file was automatically genenrated by NixOS and will be overwritten.
+    ## Do not edit. Edit your NixOS configuration instead.
+
+    ## mpdscribble - an audioscrobbler for the Music Player Daemon.
+    ## http://mpd.wikia.com/wiki/Client:mpdscribble
+
+    # HTTP proxy URL.
+    ${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"}
+
+    # The location of the mpdscribble log file.  The special value
+    # "syslog" makes mpdscribble use the local syslog daemon.  On most
+    # systems, log messages will appear in /var/log/daemon.log then.
+    # "-" means log to stderr (the current terminal).
+    log = -
+
+    # How verbose mpdscribble's logging should be.  Default is 1.
+    verbose = ${toString cfg.verbose}
+
+    # How often should mpdscribble save the journal file? [seconds]
+    journal_interval = ${toString cfg.journalInterval}
+
+    # The host running MPD, possibly protected by a password
+    # ([PASSWORD@]HOSTNAME).
+    host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host}
+
+    # The port that the MPD listens on and mpdscribble should try to
+    # connect to.
+    port = ${toString cfg.port}
+
+    ${endpoints}
+  '';
+
+  cfgFile = "/run/mpdscribble/mpdscribble.conf";
+
+  replaceSecret = secretFile: placeholder: targetFile:
+    optionalString (secretFile != null) ''
+      ${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '';
+
+  preStart = pkgs.writeShellScript "mpdscribble-pre-start" ''
+    cp -f "${cfgTemplate}" "${cfgFile}"
+    ${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile}
+    ${concatStringsSep "\n" (mapAttrsToList (secname: cfg:
+      replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile)
+      cfg.endpoints)}
+  '';
+
+  localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1");
+
+in {
+  ###### interface
+
+  options.services.mpdscribble = {
+
+    enable = mkEnableOption "mpdscribble";
+
+    proxy = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = ''
+        HTTP proxy URL.
+      '';
+    };
+
+    verbose = mkOption {
+      default = 1;
+      type = types.int;
+      description = ''
+        Log level for the mpdscribble daemon.
+      '';
+    };
+
+    journalInterval = mkOption {
+      default = 600;
+      example = 60;
+      type = types.int;
+      description = ''
+        How often should mpdscribble save the journal file? [seconds]
+      '';
+    };
+
+    host = mkOption {
+      default = (if mpdCfg.network.listenAddress != "any" then
+        mpdCfg.network.listenAddress
+      else
+        "localhost");
+      type = types.str;
+      description = ''
+        Host for the mpdscribble daemon to search for a mpd daemon on.
+      '';
+    };
+
+    passwordFile = mkOption {
+      default = if localMpd then
+        (findFirst
+          (c: any (x: x == "read") c.permissions)
+          { passwordFile = null; }
+          mpdCfg.credentials).passwordFile
+      else
+        null;
+      type = types.nullOr types.str;
+      description = ''
+        File containing the password for the mpd daemon.
+        If there is a local mpd configured using <option>services.mpd.credentials</option>
+        the default is automatically set to a matching passwordFile of the local mpd.
+      '';
+    };
+
+    port = mkOption {
+      default = mpdCfg.network.port;
+      type = types.port;
+      description = ''
+        Port for the mpdscribble daemon to search for a mpd daemon on.
+      '';
+    };
+
+    endpoints = mkOption {
+      type = (let
+        endpoint = { name, ... }: {
+          options = {
+            url = mkOption {
+              type = types.str;
+              default = endpointUrls.${name} or "";
+              description =
+                "The url endpoint where the scrobble API is listening.";
+            };
+            username = mkOption {
+              type = types.str;
+              description = ''
+                Username for the scrobble service.
+              '';
+            };
+            passwordFile = mkOption {
+              type = types.nullOr types.str;
+              description =
+                "File containing the password, either as MD5SUM or cleartext.";
+            };
+          };
+        };
+      in types.attrsOf (types.submodule endpoint));
+      default = { };
+      example = {
+        "last.fm" = {
+          username = "foo";
+          passwordFile = "/run/secrets/lastfm_password";
+        };
+      };
+      description = ''
+        Endpoints to scrobble to.
+        If the endpoint is one of "${
+          concatStringsSep "\", \"" (attrNames endpointUrls)
+        }" the url is set automatically.
+      '';
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.mpdscribble = {
+      after = [ "network.target" ] ++ (optional localMpd "mpd.service");
+      description = "mpdscribble mpd scrobble client";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "mpdscribble";
+        RuntimeDirectory = "mpdscribble";
+        RuntimeDirectoryMode = "700";
+        # TODO use LoadCredential= instead of running preStart with full privileges?
+        ExecStartPre = "+${preStart}";
+        ExecStart =
+          "${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}";
+      };
+    };
+  };
+
+}
diff --git a/nixos/modules/services/audio/roon-bridge.nix b/nixos/modules/services/audio/roon-bridge.nix
new file mode 100644
index 00000000000..85273a2039c
--- /dev/null
+++ b/nixos/modules/services/audio/roon-bridge.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  name = "roon-bridge";
+  cfg = config.services.roon-bridge;
+in {
+  options = {
+    services.roon-bridge = {
+      enable = mkEnableOption "Roon Bridge";
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the bridge.
+
+          UDP: 9003
+          TCP: 9100 - 9200
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "roon-bridge";
+        description = ''
+          User to run the Roon bridge as.
+        '';
+      };
+      group = mkOption {
+        type = types.str;
+        default = "roon-bridge";
+        description = ''
+          Group to run the Roon Bridge as.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.roon-bridge = {
+      after = [ "network.target" ];
+      description = "Roon Bridge";
+      wantedBy = [ "multi-user.target" ];
+
+      environment.ROON_DATAROOT = "/var/lib/${name}";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.roon-bridge}/start.sh";
+        LimitNOFILE = 8192;
+        User = cfg.user;
+        Group = cfg.group;
+        StateDirectory = name;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPortRanges = [
+        { from = 9100; to = 9200; }
+      ];
+      allowedUDPPorts = [ 9003 ];
+    };
+
+
+    users.groups.${cfg.group} = {};
+    users.users.${cfg.user} =
+      if cfg.user == "roon-bridge" then {
+        isSystemUser = true;
+        description = "Roon Bridge user";
+        group = cfg.group;
+        extraGroups = [ "audio" ];
+      }
+      else {};
+  };
+}
diff --git a/nixos/modules/services/audio/slimserver.nix b/nixos/modules/services/audio/slimserver.nix
index 8f94a2b4940..21632919699 100644
--- a/nixos/modules/services/audio/slimserver.nix
+++ b/nixos/modules/services/audio/slimserver.nix
@@ -63,6 +63,7 @@ in {
         description = "Slimserver daemon user";
         home = cfg.dataDir;
         group = "slimserver";
+        isSystemUser = true;
       };
       groups.slimserver = {};
     };
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index f614f0ba3e1..f96b5f3e194 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -48,8 +48,8 @@ let
     ++ [ "--stream.port ${toString cfg.port}" ]
     ++ optionalNull cfg.sampleFormat "--stream.sampleformat ${cfg.sampleFormat}"
     ++ optionalNull cfg.codec "--stream.codec ${cfg.codec}"
-    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer ${cfg.streamBuffer}"
-    ++ optionalNull cfg.buffer "--stream.buffer ${cfg.buffer}"
+    ++ optionalNull cfg.streamBuffer "--stream.stream_buffer ${toString cfg.streamBuffer}"
+    ++ optionalNull cfg.buffer "--stream.buffer ${toString cfg.buffer}"
     ++ optional cfg.sendToMuted "--stream.send_to_muted"
     # tcp json rpc
     ++ [ "--tcp.enabled ${toString cfg.tcp.enable}" ]
@@ -65,7 +65,7 @@ let
 
 in {
   imports = [
-    (mkRenamedOptionModule [ "services" "snapserver" "controlPort"] [ "services" "snapserver" "tcp" "port" ])
+    (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ])
   ];
 
   ###### interface
@@ -198,13 +198,23 @@ in {
         type = with types; attrsOf (submodule {
           options = {
             location = mkOption {
-              type = types.path;
+              type = types.oneOf [ types.path types.str ];
               description = ''
-                The location of the pipe.
+                For type <literal>pipe</literal> or <literal>file</literal>, the path to the pipe or file.
+                For type <literal>librespot</literal>, <literal>airplay</literal> or <literal>process</literal>, the path to the corresponding binary.
+                For type <literal>tcp</literal>, the <literal>host:port</literal> address to connect to or listen on.
+                For type <literal>meta</literal>, a list of stream names in the form <literal>/one/two/...</literal>. Don't forget the leading slash.
+                For type <literal>alsa</literal>, use an empty string.
+              '';
+              example = literalExample ''
+                "/path/to/pipe"
+                "/path/to/librespot"
+                "192.168.1.2:4444"
+                "/MyTCP/Spotify/MyPipe"
               '';
             };
             type = mkOption {
-              type = types.enum [ "pipe" "file" "process" "spotify" "airplay" ];
+              type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ];
               default = "pipe";
               description = ''
                 The type of input stream.
@@ -219,13 +229,21 @@ in {
               example = literalExample ''
                 # for type == "pipe":
                 {
-                  mode = "listen";
+                  mode = "create";
                 };
                 # for type == "process":
                 {
                   params = "--param1 --param2";
                   logStderr = "true";
                 };
+                # for type == "tcp":
+                {
+                  mode = "client";
+                }
+                # for type == "alsa":
+                {
+                  device = "hw:0,0";
+                }
               '';
             };
             inherit sampleFormat;
@@ -255,6 +273,11 @@ in {
 
   config = mkIf cfg.enable {
 
+    # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85
+    warnings = filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then ''
+      services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
+    '' else "") cfg.streams);
+
     systemd.services.snapserver = {
       after = [ "network.target" ];
       description = "Snapserver";
@@ -272,7 +295,7 @@ in {
         ProtectKernelTunables = true;
         ProtectControlGroups = true;
         ProtectKernelModules = true;
-        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
         RestrictNamespaces = true;
         RuntimeDirectory = name;
         StateDirectory = name;
diff --git a/nixos/modules/services/audio/spotifyd.nix b/nixos/modules/services/audio/spotifyd.nix
index a589153248f..9279a03aed4 100644
--- a/nixos/modules/services/audio/spotifyd.nix
+++ b/nixos/modules/services/audio/spotifyd.nix
@@ -27,6 +27,7 @@ in
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" "sound.target" ];
       description = "spotifyd, a Spotify playing daemon";
+      environment.SHELL = "/bin/sh";
       serviceConfig = {
         ExecStart = "${pkgs.spotifyd}/bin/spotifyd --no-daemon --cache-path /var/cache/spotifyd --config-path ${spotifydConf}";
         Restart = "always";
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
index 3d69a69038a..cc8b77cbfbe 100644
--- a/nixos/modules/services/backup/bacula.nix
+++ b/nixos/modules/services/backup/bacula.nix
@@ -1,5 +1,6 @@
 { config, lib, pkgs, ... }:
 
+
 # TODO: test configuration when building nixexpr (use -t parameter)
 # TODO: support sqlite3 (it's deprecate?) and mysql
 
@@ -111,6 +112,7 @@ let
   {
     options = {
       password = mkOption {
+        type = types.str;
         # TODO: required?
         description = ''
           Specifies the password that must be supplied for the default Bacula
@@ -130,6 +132,7 @@ let
       };
 
       monitor = mkOption {
+        type = types.enum [ "no" "yes" ];
         default = "no";
         example = "yes";
         description = ''
@@ -150,6 +153,7 @@ let
   {
     options = {
       changerDevice = mkOption {
+        type = types.str;
         description = ''
           The specified name-string must be the generic SCSI device name of the
           autochanger that corresponds to the normal read/write Archive Device
@@ -168,6 +172,7 @@ let
       };
 
       changerCommand = mkOption {
+        type = types.str;
         description = ''
           The name-string specifies an external program to be called that will
           automatically change volumes as required by Bacula. Normally, this
@@ -190,12 +195,13 @@ let
       };
 
       devices = mkOption {
-        description = ''
-        '';
+        description = "";
+        type = types.listOf types.str;
       };
 
       extraAutochangerConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Autochanger directive.
         '';
@@ -212,6 +218,7 @@ let
     options = {
       archiveDevice = mkOption {
         # TODO: required?
+        type = types.str;
         description = ''
           The specified name-string gives the system file name of the storage
           device managed by this storage daemon. This will usually be the
@@ -228,6 +235,7 @@ let
 
       mediaType = mkOption {
         # TODO: required?
+        type = types.str;
         description = ''
           The specified name-string names the type of media supported by this
           device, for example, <literal>DLT7000</literal>. Media type names are
@@ -265,6 +273,7 @@ let
 
       extraDeviceConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Device directive.
         '';
@@ -293,6 +302,7 @@ in {
 
       name = mkOption {
         default = "${config.networking.hostName}-fd";
+        type = types.str;
         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
@@ -321,6 +331,7 @@ in {
 
       extraClientConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Client directive.
         '';
@@ -332,6 +343,7 @@ in {
 
       extraMessagesConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Messages directive.
         '';
@@ -352,6 +364,7 @@ in {
 
       name = mkOption {
         default = "${config.networking.hostName}-sd";
+        type = types.str;
         description = ''
           Specifies the Name of the Storage daemon.
         '';
@@ -392,6 +405,7 @@ in {
 
       extraStorageConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Storage directive.
         '';
@@ -403,6 +417,7 @@ in {
 
       extraMessagesConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Messages directive.
         '';
@@ -424,6 +439,7 @@ in {
 
       name = mkOption {
         default = "${config.networking.hostName}-dir";
+        type = types.str;
         description = ''
           The director name used by the system administrator. This directive is
           required.
@@ -445,6 +461,7 @@ in {
 
       password = mkOption {
         # TODO: required?
+        type = types.str;
         description = ''
            Specifies the password that must be supplied for a Director.
         '';
@@ -452,6 +469,7 @@ in {
 
       extraMessagesConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Messages directive.
         '';
@@ -462,6 +480,7 @@ in {
 
       extraDirectorConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra configuration to be passed in Director directive.
         '';
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index be661b201f0..18fb29fd72a 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -169,6 +169,7 @@ let
         (map (mkAuthorizedKey cfg false) cfg.authorizedKeys
         ++ map (mkAuthorizedKey cfg true) cfg.authorizedKeysAppendOnly);
       useDefaultShell = true;
+      isSystemUser = true;
     };
     groups.${cfg.group} = { };
   };
diff --git a/nixos/modules/services/backup/borgbackup.xml b/nixos/modules/services/backup/borgbackup.xml
index bef7db608f8..8f623c93656 100644
--- a/nixos/modules/services/backup/borgbackup.xml
+++ b/nixos/modules/services/backup/borgbackup.xml
@@ -69,10 +69,10 @@
     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>
+<screen>
+<prompt># </prompt>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
+<prompt># </prompt>cat /run/keys/id_ed25519_my_borg_repo
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos</screen>
     </para>
     <para>
       Add the following snippet to your NixOS configuration:
@@ -197,26 +197,8 @@ sudo borg init --encryption=repokey-blake2  \
     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.
+   It can be installed in NixOS e.g. by adding <package>pkgs.vorta</package>
+   to <xref linkend="opt-environment.systemPackages" />.
   </para>
   <para>
     Details about using Vorta can be found under <link
diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix
new file mode 100644
index 00000000000..5e5c0bbeccc
--- /dev/null
+++ b/nixos/modules/services/backup/borgmatic.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.borgmatic;
+  cfgfile = pkgs.writeText "config.yaml" (builtins.toJSON cfg.settings);
+in {
+  options.services.borgmatic = {
+    enable = mkEnableOption "borgmatic";
+
+    settings = mkOption {
+      description = ''
+        See https://torsion.org/borgmatic/docs/reference/configuration/
+      '';
+      type = types.submodule {
+        freeformType = with lib.types; attrsOf anything;
+        options.location = {
+          source_directories = mkOption {
+            type = types.listOf types.str;
+            description = ''
+              List of source directories to backup (required). Globs and
+              tildes are expanded.
+            '';
+            example = [ "/home" "/etc" "/var/log/syslog*" ];
+          };
+          repositories = mkOption {
+            type = types.listOf types.str;
+            description = ''
+              Paths to local or remote repositories (required). Tildes are
+              expanded. Multiple repositories are backed up to in
+              sequence. Borg placeholders can be used. See the output of
+              "borg help placeholders" for details. See ssh_command for
+              SSH options like identity file or port. If systemd service
+              is used, then add local repository paths in the systemd
+              service file to the ReadWritePaths list.
+            '';
+            example = [
+              "user@backupserver:sourcehostname.borg"
+              "user@backupserver:{fqdn}"
+            ];
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.borgmatic ];
+
+    environment.etc."borgmatic/config.yaml".source = cfgfile;
+
+    systemd.packages = [ pkgs.borgmatic ];
+
+  };
+}
diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix
new file mode 100644
index 00000000000..a8ff71f609a
--- /dev/null
+++ b/nixos/modules/services/backup/btrbk.nix
@@ -0,0 +1,220 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.btrbk;
+  sshEnabled = cfg.sshAccess != [ ];
+  serviceEnabled = cfg.instances != { };
+  attr2Lines = attr:
+    let
+      pairs = lib.attrsets.mapAttrsToList (name: value: { inherit name value; }) attr;
+      isSubsection = value:
+        if builtins.isAttrs value then true
+        else if builtins.isString value then false
+        else throw "invalid type in btrbk config ${builtins.typeOf value}";
+      sortedPairs = lib.lists.partition (x: isSubsection x.value) pairs;
+    in
+    lib.flatten (
+      # non subsections go first
+      (
+        map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
+      )
+      ++ # subsections go last
+      (
+        map
+          (
+            pair:
+            lib.mapAttrsToList
+              (
+                childname: value:
+                  [ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
+              )
+              pair.value
+          )
+          sortedPairs.right
+      )
+    )
+  ;
+  addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
+  mkConfigFile = settings: lib.concatStringsSep "\n" (attr2Lines (addDefaults settings));
+  mkTestedConfigFile = name: settings:
+    let
+      configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
+    in
+    pkgs.runCommand "btrbk-${name}-tested.conf" { } ''
+      mkdir foo
+      cp ${configFile} $out
+      if (set +o pipefail; ${pkgs.btrbk}/bin/btrbk -c $out ls foo 2>&1 | grep $out);
+      then
+      echo btrbk configuration is invalid
+      cat $out
+      exit 1
+      fi;
+    '';
+in
+{
+  options = {
+    services.btrbk = {
+      extraPackages = lib.mkOption {
+        description = "Extra packages for btrbk, like compression utilities for <literal>stream_compress</literal>";
+        type = lib.types.listOf lib.types.package;
+        default = [ ];
+        example = lib.literalExample "[ pkgs.xz ]";
+      };
+      niceness = lib.mkOption {
+        description = "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive.";
+        type = lib.types.ints.between (-20) 19;
+        default = 10;
+      };
+      ioSchedulingClass = lib.mkOption {
+        description = "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle.";
+        type = lib.types.enum [ "idle" "best-effort" "realtime" ];
+        default = "best-effort";
+      };
+      instances = lib.mkOption {
+        description = "Set of btrbk instances. The instance named <literal>btrbk</literal> is the default one.";
+        type = with lib.types;
+          attrsOf (
+            submodule {
+              options = {
+                onCalendar = lib.mkOption {
+                  type = lib.types.str;
+                  default = "daily";
+                  description = "How often this btrbk instance is started. See systemd.time(7) for more information about the format.";
+                };
+                settings = lib.mkOption {
+                  type = let t = lib.types.attrsOf (lib.types.either lib.types.str (t // { description = "instances of this type recursively"; })); in t;
+                  default = { };
+                  example = {
+                    snapshot_preserve_min = "2d";
+                    snapshot_preserve = "14d";
+                    volume = {
+                      "/mnt/btr_pool" = {
+                        target = "/mnt/btr_backup/mylaptop";
+                        subvolume = {
+                          "rootfs" = { };
+                          "home" = { snapshot_create = "always"; };
+                        };
+                      };
+                    };
+                  };
+                  description = "configuration options for btrbk. Nested attrsets translate to subsections.";
+                };
+              };
+            }
+          );
+        default = { };
+      };
+      sshAccess = lib.mkOption {
+        description = "SSH keys that should be able to make or push snapshots on this system remotely with btrbk";
+        type = with lib.types; listOf (
+          submodule {
+            options = {
+              key = lib.mkOption {
+                type = str;
+                description = "SSH public key allowed to login as user <literal>btrbk</literal> to run remote backups.";
+              };
+              roles = lib.mkOption {
+                type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]);
+                example = [ "source" "info" "send" ];
+                description = "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details";
+              };
+            };
+          }
+        );
+        default = [ ];
+      };
+    };
+
+  };
+  config = lib.mkIf (sshEnabled || serviceEnabled) {
+    environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
+    security.sudo.extraRules = [
+      {
+        users = [ "btrbk" ];
+        commands = [
+          { command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
+          { command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
+          { command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
+          # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
+          { command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
+          { command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
+          { command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
+        ];
+      }
+    ];
+    users.users.btrbk = {
+      isSystemUser = true;
+      # ssh needs a home directory
+      home = "/var/lib/btrbk";
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+      group = "btrbk";
+      openssh.authorizedKeys.keys = map
+        (
+          v:
+          let
+            options = lib.concatMapStringsSep " " (x: "--" + x) v.roles;
+            ioniceClass = {
+              "idle" = 3;
+              "best-effort" = 2;
+              "realtime" = 1;
+            }.${cfg.ioSchedulingClass};
+          in
+          ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${lib.optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
+        )
+        cfg.sshAccess;
+    };
+    users.groups.btrbk = { };
+    systemd.tmpfiles.rules = [
+      "d /var/lib/btrbk 0750 btrbk btrbk"
+      "d /var/lib/btrbk/.ssh 0700 btrbk btrbk"
+      "f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new"
+    ];
+    environment.etc = lib.mapAttrs'
+      (
+        name: instance: {
+          name = "btrbk/${name}.conf";
+          value.source = mkTestedConfigFile name instance.settings;
+        }
+      )
+      cfg.instances;
+    systemd.services = lib.mapAttrs'
+      (
+        name: _: {
+          name = "btrbk-${name}";
+          value = {
+            description = "Takes BTRFS snapshots and maintains retention policies.";
+            unitConfig.Documentation = "man:btrbk(1)";
+            path = [ "/run/wrappers" ] ++ cfg.extraPackages;
+            serviceConfig = {
+              User = "btrbk";
+              Group = "btrbk";
+              Type = "oneshot";
+              ExecStart = "${pkgs.btrbk}/bin/btrbk -c /etc/btrbk/${name}.conf run";
+              Nice = cfg.niceness;
+              IOSchedulingClass = cfg.ioSchedulingClass;
+              StateDirectory = "btrbk";
+            };
+          };
+        }
+      )
+      cfg.instances;
+
+    systemd.timers = lib.mapAttrs'
+      (
+        name: instance: {
+          name = "btrbk-${name}";
+          value = {
+            description = "Timer to take BTRFS snapshots and maintain retention policies.";
+            wantedBy = [ "timers.target" ];
+            timerConfig = {
+              OnCalendar = instance.onCalendar;
+              AccuracySec = "10min";
+              Persistent = true;
+            };
+          };
+        }
+      )
+      cfg.instances;
+  };
+
+}
diff --git a/nixos/modules/services/backup/duplicati.nix b/nixos/modules/services/backup/duplicati.nix
index 0ff720c5897..cf5aebdecd2 100644
--- a/nixos/modules/services/backup/duplicati.nix
+++ b/nixos/modules/services/backup/duplicati.nix
@@ -54,11 +54,13 @@ in
       };
     };
 
-    users.users.duplicati = lib.optionalAttrs (cfg.user == "duplicati") {
-      uid = config.ids.uids.duplicati;
-      home = "/var/lib/duplicati";
-      createHome = true;
-      group = "duplicati";
+    users.users = lib.optionalAttrs (cfg.user == "duplicati") {
+      duplicati = {
+        uid = config.ids.uids.duplicati;
+        home = "/var/lib/duplicati";
+        createHome = true;
+        group = "duplicati";
+      };
     };
     users.groups.duplicati.gid = config.ids.gids.duplicati;
 
diff --git a/nixos/modules/services/backup/duplicity.nix b/nixos/modules/services/backup/duplicity.nix
index a8d56424862..6949fa8b995 100644
--- a/nixos/modules/services/backup/duplicity.nix
+++ b/nixos/modules/services/backup/duplicity.nix
@@ -1,16 +1,17 @@
-{ config, lib, pkgs, ...}:
+{ config, lib, pkgs, ... }:
 
 with lib;
-
 let
   cfg = config.services.duplicity;
 
   stateDirectory = "/var/lib/duplicity";
 
-  localTarget = if hasPrefix "file://" cfg.targetUrl
+  localTarget =
+    if hasPrefix "file://" cfg.targetUrl
     then removePrefix "file://" cfg.targetUrl else null;
 
-in {
+in
+{
   options.services.duplicity = {
     enable = mkEnableOption "backups with duplicity";
 
@@ -24,7 +25,7 @@ in {
 
     include = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "/home" ];
       description = ''
         List of paths to include into the backups. See the FILE SELECTION
@@ -35,7 +36,7 @@ in {
 
     exclude = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       description = ''
         List of paths to exclude from backups. See the FILE SELECTION section in
         <citerefentry><refentrytitle>duplicity</refentrytitle>
@@ -82,14 +83,60 @@ in {
 
     extraFlags = mkOption {
       type = types.listOf types.str;
-      default = [];
-      example = [ "--full-if-older-than" "1M" ];
+      default = [ ];
+      example = [ "--backend-retry-delay" "100" ];
       description = ''
         Extra command-line flags passed to duplicity. See
         <citerefentry><refentrytitle>duplicity</refentrytitle>
         <manvolnum>1</manvolnum></citerefentry>.
       '';
     };
+
+    fullIfOlderThan = mkOption {
+      type = types.str;
+      default = "never";
+      example = "1M";
+      description = ''
+        If <literal>"never"</literal> (the default) always do incremental
+        backups (the first backup will be a full backup, of course).  If
+        <literal>"always"</literal> always do full backups.  Otherwise, this
+        must be a string representing a duration. Full backups will be made
+        when the latest full backup is older than this duration. If this is not
+        the case, an incremental backup is performed.
+      '';
+    };
+
+    cleanup = {
+      maxAge = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "6M";
+        description = ''
+          If non-null, delete all backup sets older than the given time.  Old backup sets
+          will not be deleted if backup sets newer than time depend on them.
+        '';
+      };
+      maxFull = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 2;
+        description = ''
+          If non-null, delete all backups sets that are older than the count:th last full
+          backup (in other words, keep the last count full backups and
+          associated incremental sets).
+        '';
+      };
+      maxIncr = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 1;
+        description = ''
+          If non-null, delete incremental sets of all backups sets that are
+          older than the count:th last full backup (in other words, keep only
+          old full backups and not their increments).
+        '';
+      };
+    };
   };
 
   config = mkIf cfg.enable {
@@ -99,18 +146,26 @@ in {
 
         environment.HOME = stateDirectory;
 
-        serviceConfig = {
-          ExecStart = ''
-            ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs (
-              [
-                cfg.root
-                cfg.targetUrl
-                "--archive-dir" stateDirectory
-              ]
+        script =
+          let
+            target = escapeShellArg cfg.targetUrl;
+            extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
+            dup = "${pkgs.duplicity}/bin/duplicity";
+          in
+          ''
+            set -x
+            ${dup} cleanup ${target} --force ${extra}
+            ${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
+            ${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
+            ${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-inc-of-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
+            exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs (
+              [ cfg.root cfg.targetUrl ]
               ++ concatMap (p: [ "--include" p ]) cfg.include
               ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
-              ++ cfg.extraFlags)}
+              ++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ])
+              )} ${extra}
           '';
+        serviceConfig = {
           PrivateTmp = true;
           ProtectSystem = "strict";
           ProtectHome = "read-only";
@@ -130,7 +185,7 @@ in {
     assertions = singleton {
       # Duplicity will fail if the last file selection option is an include. It
       # is not always possible to detect but this simple case can be caught.
-      assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != [];
+      assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
       message = ''
         Duplicity will fail if you only specify included paths ("Because the
         default is to include all files, the expression is redundant. Exiting
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index 31d606b141a..9fca2100273 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
 
-  inherit (pkgs) mysql gzip;
+  inherit (pkgs) mariadb gzip;
 
   cfg = config.services.mysqlBackup;
   defaultUser = "mysqlbackup";
@@ -20,7 +20,7 @@ let
   '';
   backupDatabaseScript = db: ''
     dest="${cfg.location}/${db}.gz"
-    if ${mysql}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
+    if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
       mv $dest.tmp $dest
       echo "Backed up to $dest"
     else
@@ -48,6 +48,7 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = defaultUser;
         description = ''
           User to be used to perform backup.
@@ -56,12 +57,14 @@ in
 
       databases = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = ''
           List of database names to dump.
         '';
       };
 
       location = mkOption {
+        type = types.path;
         default = "/var/backup/mysql";
         description = ''
           Location to put the gzipped MySQL database dumps.
@@ -70,6 +73,7 @@ in
 
       singleTransaction = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Whether to create database dump in a single transaction
         '';
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
index 428861a7598..f658eb756f7 100644
--- a/nixos/modules/services/backup/postgresql-backup.nix
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -14,15 +14,21 @@ let
 
       requires = [ "postgresql.service" ];
 
+      path = [ pkgs.coreutils pkgs.gzip config.services.postgresql.package ];
+
       script = ''
+        set -e -o pipefail
+
         umask 0077 # ensure backup is only readable by postgres user
 
         if [ -e ${cfg.location}/${db}.sql.gz ]; then
-          ${pkgs.coreutils}/bin/mv ${cfg.location}/${db}.sql.gz ${cfg.location}/${db}.prev.sql.gz
+          mv ${cfg.location}/${db}.sql.gz ${cfg.location}/${db}.prev.sql.gz
         fi
 
         ${dumpCmd} | \
-          ${pkgs.gzip}/bin/gzip -c > ${cfg.location}/${db}.sql.gz
+          gzip -c > ${cfg.location}/${db}.in-progress.sql.gz
+
+        mv ${cfg.location}/${db}.in-progress.sql.gz ${cfg.location}/${db}.sql.gz
       '';
 
       serviceConfig = {
@@ -48,6 +54,7 @@ in {
 
       startAt = mkOption {
         default = "*-*-* 01:15:00";
+        type = with types; either (listOf str) str;
         description = ''
           This option defines (see <literal>systemd.time</literal> for format) when the
           databases should be dumped.
@@ -70,6 +77,7 @@ in {
 
       databases = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = ''
           List of database names to dump.
         '';
@@ -77,6 +85,7 @@ in {
 
       location = mkOption {
         default = "/var/backup/postgresql";
+        type = types.path;
         description = ''
           Location to put the gzipped PostgreSQL database dumps.
         '';
@@ -110,12 +119,12 @@ in {
     })
     (mkIf (cfg.enable && cfg.backupAll) {
       systemd.services.postgresqlBackup =
-        postgresqlBackupService "all" "${config.services.postgresql.package}/bin/pg_dumpall";
+        postgresqlBackupService "all" "pg_dumpall";
     })
     (mkIf (cfg.enable && !cfg.backupAll) {
       systemd.services = listToAttrs (map (db:
         let
-          cmd = "${config.services.postgresql.package}/bin/pg_dump ${cfg.pgdumpOptions} ${db}";
+          cmd = "pg_dump ${cfg.pgdumpOptions} ${db}";
         in {
           name = "postgresqlBackup-${db}";
           value = postgresqlBackupService db cmd;
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index d869835bf07..ac57f271526 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -93,10 +93,12 @@ in
         };
 
         paths = mkOption {
-          type = types.listOf types.str;
-          default = [];
+          type = types.nullOr (types.listOf types.str);
+          default = null;
           description = ''
-            Which paths to backup.
+            Which paths to backup.  If null or an empty array, no
+            backup command will be run.  This can be used to create a
+            prune-only job.
           '';
           example = [
             "/var/lib/postgresql"
@@ -217,7 +219,7 @@ in
           resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
           filesFromTmpFile = "/run/restic-backups-${name}/includes";
           backupPaths = if (backup.dynamicFilesFrom == null)
-                        then concatStringsSep " " backup.paths
+                        then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
                         else "--files-from ${filesFromTmpFile}";
           pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
             ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) )
@@ -243,9 +245,12 @@ in
           restartIfChanged = false;
           serviceConfig = {
             Type = "oneshot";
-            ExecStart = [ "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ] ++ pruneCmd;
+            ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
+                        ++ pruneCmd;
             User = backup.user;
             RuntimeDirectory = "restic-backups-${name}";
+            CacheDirectory = "restic-backups-${name}";
+            CacheDirectoryMode = "0700";
           } // optionalAttrs (backup.s3CredentialsFile != null) {
             EnvironmentFile = backup.s3CredentialsFile;
           };
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index 0472fb4ba1e..41d0e2e1df6 100644
--- a/nixos/modules/services/backup/sanoid.nix
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -10,74 +10,51 @@ let
       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;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     daily = mkOption {
       description = "Number of daily snapshots.";
-      type = types.ints.unsigned;
-      default = 90;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     monthly = mkOption {
       description = "Number of monthly snapshots.";
-      type = types.ints.unsigned;
-      default = 6;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     yearly = mkOption {
       description = "Number of yearly snapshots.";
-      type = types.ints.unsigned;
-      default = 0;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     autoprune = mkOption {
       description = "Whether to automatically prune old snapshots.";
-      type = types.bool;
-      default = true;
+      type = with types; nullOr bool;
+      default = null;
     };
 
     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;
+      type = with types; nullOr bool;
+      default = null;
     };
   };
 
-  datasetOptions = {
-    useTemplate = mkOption {
+  datasetOptions = rec {
+    use_template = 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 = [];
+      type = types.listOf (types.enum (attrNames cfg.templates));
+      default = [ ];
     };
+    useTemplate = use_template;
 
     recursive = mkOption {
       description = "Whether to recursively snapshot dataset children.";
@@ -85,129 +62,135 @@ let
       default = false;
     };
 
-    processChildrenOnly = mkOption {
+    process_children_only = mkOption {
       description = "Whether to only snapshot child datasets if recursing.";
       type = types.bool;
       default = false;
     };
+    processChildrenOnly = process_children_only;
   };
 
-  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.
+  # Extract unique dataset names
+  datasets = unique (attrNames cfg.datasets);
+
+  # Function to build "zfs allow" and "zfs unallow" commands for the
+  # filesystems we've delegated permissions to.
+  buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
+    # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
+    "-+/run/booted-system/sw/bin/zfs"
+    zfsAction
+    "sanoid"
+    (concatStringsSep "," permissions)
+    dataset
+  ];
+
+  configFile =
+    let
+      mkValueString = v:
+        if builtins.isList v then concatStringsSep "," v
+        else generators.mkValueStringDefault { } v;
+
+      mkKeyValue = k: v:
+        if v == null then ""
+        else if k == "processChildrenOnly" then ""
+        else if k == "useTemplate" then ""
+        else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
+    in
+    generators.toINI { inherit mkKeyValue; } cfg.settings;
+
+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>.
-        '';
-      };
+        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.";
-      };
+    datasets = mkOption {
+      type = types.attrsOf (types.submodule ({ config, options, ... }: {
+        freeformType = datasetSettingsType;
+        options = commonOptions // datasetOptions;
+        config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { });
+        config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { });
+      }));
+      default = { };
+      description = "Datasets to snapshot.";
+    };
 
-      templates = mkOption {
-        type = types.attrsOf (types.submodule ({ config, ... }: {
-          options = commonOptions;
-          config = commonConfig config;
-        }));
-        default = {};
-        description = "Templates for datasets.";
-      };
+    templates = mkOption {
+      type = types.attrsOf (types.submodule {
+        freeformType = datasetSettingsType;
+        options = commonOptions;
+      });
+      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.
-        '';
-      };
+    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.
-        '';
-      };
+    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;
+  # Implementation
+
+  config = mkIf cfg.enable {
+    services.sanoid.settings = mkMerge [
+      (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
+      (mapAttrs (d: v: v) cfg.datasets)
+    ];
+
+    systemd.services.sanoid = {
+      description = "Sanoid snapshot service";
+      serviceConfig = {
+        ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
+        ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
+        ExecStart = lib.escapeShellArgs ([
+          "${pkgs.sanoid}/bin/sanoid"
+          "--cron"
+          "--configdir"
+          (pkgs.writeTextDir "sanoid.conf" configFile)
+        ] ++ cfg.extraArgs);
+        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 ];
-  }
+  meta.maintainers = with maintainers; [ lopsided98 ];
+}
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
index fff119c2cf0..73b01d4b53f 100644
--- a/nixos/modules/services/backup/syncoid.nix
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -4,169 +4,316 @@ with lib;
 
 let
   cfg = config.services.syncoid;
-in {
 
-    # Interface
+  # Extract local dasaset names (so no datasets containing "@")
+  localDatasetName = d: optionals (d != null) (
+    let m = builtins.match "([^/@]+[^@]*)" d; in
+    optionals (m != null) m
+  );
 
-    options.services.syncoid = {
-      enable = mkEnableOption "Syncoid ZFS synchronization service";
+  # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
+  escapeUnitName = name:
+    lib.concatMapStrings (s: if lib.isList s then "-" else s)
+      (builtins.split "[^a-zA-Z0-9_.\\-]+" name);
 
-      interval = mkOption {
-        type = types.str;
-        default = "hourly";
-        example = "*-*-* *:15:00";
-        description = ''
-          Run syncoid at this interval. The default is to run hourly.
+  # Function to build "zfs allow" and "zfs unallow" commands for the
+  # filesystems we've delegated permissions to.
+  buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
+    # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
+    "-+/run/booted-system/sw/bin/zfs"
+    zfsAction
+    cfg.user
+    (concatStringsSep "," permissions)
+    dataset
+  ];
+in
+{
 
-          The format is described in
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
-        '';
-      };
+  # Interface
 
-      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.
-        '';
-      };
+  options.services.syncoid = {
+    enable = mkEnableOption "Syncoid ZFS synchronization service";
 
-      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.
-        '';
-      };
+    interval = mkOption {
+      type = types.str;
+      default = "hourly";
+      example = "*-*-* *:15:00";
+      description = ''
+        Run syncoid at this interval. The default is to run hourly.
 
-      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.
-        '';
-      };
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
 
-      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.
-              '';
-            };
+    user = mkOption {
+      type = types.str;
+      default = "syncoid";
+      example = "backup";
+      description = ''
+        The user for the service. ZFS privilege delegation will be
+        automatically configured for any local pools used by syncoid if this
+        option is set to a user other than root. The user will be given the
+        "hold" and "send" privileges on any pool that has datasets being sent
+        and the "create", "mount", "receive", and "rollback" privileges on
+        any pool that has datasets being received.
+      '';
+    };
 
-            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>).
-              '';
-            };
+    group = mkOption {
+      type = types.str;
+      default = "syncoid";
+      example = "backup";
+      description = "The group for the service.";
+    };
 
-            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;
+      default = null;
+      description = ''
+        SSH private key file to use to login to the remote system. Can be
+        overridden in individual commands.
+      '';
+    };
 
-            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.
-              '';
-            };
+    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.
+      '';
+    };
 
-            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.
-              '';
-            };
+    service = mkOption {
+      type = types.attrs;
+      default = { };
+      description = ''
+        Systemd configuration common to all syncoid services.
+      '';
+    };
 
-            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.
-              '';
-            };
+    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.
+            '';
+          };
 
-            useCommonArgs = mkOption {
-              type = types.bool;
-              default = true;
-              description = ''
-                Whether to add the configured common arguments to this command.
-              '';
-            };
+          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>).
+            '';
+          };
 
-            extraArgs = mkOption {
-              type = types.listOf types.str;
-              default = [];
-              example = [ "--sshport 2222" ];
-              description = "Extra syncoid arguments for this command.";
-            };
+          recursive = mkEnableOption ''the transfer of 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.
+            '';
           };
-          config = {
-            source = mkDefault name;
-            sshKey = mkDefault cfg.sshKey;
+
+          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.
+            '';
           };
-        }));
-        default = {};
-        example = literalExample ''
-          {
-            "pool/test".target = "root@target:pool/test";
-          }
-        '';
-        description = "Syncoid commands to run.";
-      };
+
+          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.
+            '';
+          };
+
+          service = mkOption {
+            type = types.attrs;
+            default = { };
+            description = ''
+              Systemd configuration specific to this syncoid service.
+            '';
+          };
+
+          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;
+  # Implementation
+
+  config = mkIf cfg.enable {
+    users = {
+      users = mkIf (cfg.user == "syncoid") {
+        syncoid = {
+          group = cfg.group;
+          isSystemUser = true;
+          # For syncoid to be able to create /var/lib/syncoid/.ssh/
+          # and to use custom ssh_config or known_hosts.
+          home = "/var/lib/syncoid";
+          createHome = false;
+        };
+      };
+      groups = mkIf (cfg.group == "syncoid") {
+        syncoid = { };
       };
     };
 
-    meta.maintainers = with maintainers; [ lopsided98 ];
-  }
+    systemd.services = mapAttrs'
+      (name: c:
+        nameValuePair "syncoid-${escapeUnitName name}" (mkMerge [
+          {
+            description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
+            after = [ "zfs.target" ];
+            startAt = cfg.interval;
+            # syncoid may need zpool to get feature@extensible_dataset
+            path = [ "/run/booted-system/sw/bin/" ];
+            serviceConfig = {
+              ExecStartPre =
+                # Permissions snapshot and destroy are in case --no-sync-snap is not used
+                (map (buildAllowCommand "allow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++
+                (map (buildAllowCommand "allow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target));
+              ExecStopPost =
+                # Permissions snapshot and destroy are in case --no-sync-snap is not used
+                (map (buildAllowCommand "unallow" [ "bookmark" "hold" "send" "snapshot" "destroy" ]) (localDatasetName c.source)) ++
+                (map (buildAllowCommand "unallow" [ "create" "mount" "receive" "rollback" ]) (localDatasetName c.target));
+              ExecStart = 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
+                "--no-privilege-elevation"
+                c.source
+                c.target
+              ]);
+              User = cfg.user;
+              Group = cfg.group;
+              StateDirectory = [ "syncoid" ];
+              StateDirectoryMode = "700";
+              # Prevent SSH control sockets of different syncoid services from interfering
+              PrivateTmp = true;
+              # Permissive access to /proc because syncoid
+              # calls ps(1) to detect ongoing `zfs receive`.
+              ProcSubset = "all";
+              ProtectProc = "default";
+
+              # The following options are only for optimizing:
+              # systemd-analyze security | grep syncoid-'*'
+              AmbientCapabilities = "";
+              CapabilityBoundingSet = "";
+              DeviceAllow = [ "/dev/zfs" ];
+              LockPersonality = true;
+              MemoryDenyWriteExecute = true;
+              NoNewPrivileges = true;
+              PrivateDevices = true;
+              PrivateMounts = true;
+              PrivateNetwork = mkDefault false;
+              PrivateUsers = true;
+              ProtectClock = true;
+              ProtectControlGroups = true;
+              ProtectHome = true;
+              ProtectHostname = true;
+              ProtectKernelLogs = true;
+              ProtectKernelModules = true;
+              ProtectKernelTunables = true;
+              ProtectSystem = "strict";
+              RemoveIPC = true;
+              RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+              RestrictNamespaces = true;
+              RestrictRealtime = true;
+              RestrictSUIDSGID = true;
+              RootDirectory = "/run/syncoid/${escapeUnitName name}";
+              RootDirectoryStartOnly = true;
+              BindPaths = [ "/dev/zfs" ];
+              BindReadOnlyPaths = [ builtins.storeDir "/etc" "/run" "/bin/sh" ];
+              # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
+              InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
+              MountAPIVFS = true;
+              # Create RootDirectory= in the host's mount namespace.
+              RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
+              RuntimeDirectoryMode = "700";
+              SystemCallFilter = [
+                "@system-service"
+                # Groups in @system-service which do not contain a syscall listed by:
+                # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
+                # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
+                # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
+                "~@aio"
+                "~@chown"
+                "~@keyring"
+                "~@memlock"
+                "~@privileged"
+                "~@resources"
+                "~@setuid"
+                "~@timer"
+              ];
+              SystemCallArchitectures = "native";
+              # This is for BindPaths= and BindReadOnlyPaths=
+              # to allow traversal of directories they create in RootDirectory=.
+              UMask = "0066";
+            };
+          }
+          cfg.service
+          c.service
+        ]))
+      cfg.commands;
+  };
+
+  meta.maintainers = with maintainers; [ julm lopsided98 ];
+}
diff --git a/nixos/modules/services/backup/tarsnap.nix b/nixos/modules/services/backup/tarsnap.nix
index 6d99a1efb61..8187042b4b8 100644
--- a/nixos/modules/services/backup/tarsnap.nix
+++ b/nixos/modules/services/backup/tarsnap.nix
@@ -29,13 +29,7 @@ in
 
   options = {
     services.tarsnap = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable periodic tarsnap backups.
-        '';
-      };
+      enable = mkEnableOption "periodic tarsnap backups";
 
       keyfile = mkOption {
         type = types.str;
@@ -279,7 +273,8 @@ in
           Tarsnap archive configurations. Each attribute names an archive
           to be created at a given time interval, according to the options
           associated with it. When uploading to the tarsnap server,
-          archive names are suffixed by a 1 second resolution timestamp.
+          archive names are suffixed by a 1 second resolution timestamp,
+          with the format <literal>%Y%m%d%H%M%S</literal>.
 
           For each member of the set is created a timer which triggers the
           instanced <literal>tarsnap-archive-name</literal> service unit. You may use
@@ -308,7 +303,7 @@ in
         requires    = [ "network-online.target" ];
         after       = [ "network-online.target" ];
 
-        path = with pkgs; [ iputils tarsnap utillinux ];
+        path = with pkgs; [ iputils tarsnap util-linux ];
 
         # In order for the persistent tarsnap timer to work reliably, we have to
         # make sure that the tarsnap server is reachable after systemd starts up
@@ -355,11 +350,11 @@ in
         description = "Tarsnap restore '${name}'";
         requires    = [ "network-online.target" ];
 
-        path = with pkgs; [ iputils tarsnap utillinux ];
+        path = with pkgs; [ iputils tarsnap util-linux ];
 
         script = let
           tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"'';
-          lastArchive = ''$(${tarsnap} --list-archives | sort | tail -1)'';
+          lastArchive = "$(${tarsnap} --list-archives | sort | tail -1)";
           run = ''${tarsnap} -x -f "${lastArchive}" ${optionalString cfg.verbose "-v"}'';
 
         in if (cfg.cachedir != null) then ''
diff --git a/nixos/modules/services/backup/znapzend.nix b/nixos/modules/services/backup/znapzend.nix
index 8098617d11f..debb2a39705 100644
--- a/nixos/modules/services/backup/znapzend.nix
+++ b/nixos/modules/services/backup/znapzend.nix
@@ -220,7 +220,7 @@ let
       };
 
       destinations = mkOption {
-        type = loaOf (destType config);
+        type = attrsOf (destType config);
         description = "Additional destinations.";
         default = {};
         example = literalExample ''
@@ -279,7 +279,7 @@ let
     src_plan = plan;
     tsformat = timestampFormat;
     zend_delay = toString sendDelay;
-  } // fold (a: b: a // b) {} (
+  } // foldr (a: b: a // b) {} (
     map mkDestAttrs (builtins.attrValues destinations)
   );
 
@@ -328,7 +328,7 @@ in
       };
 
       zetup = mkOption {
-        type = loaOf srcType;
+        type = attrsOf srcType;
         description = "Znapzend configuration.";
         default = {};
         example = literalExample ''
diff --git a/nixos/modules/services/backup/zrepl.nix b/nixos/modules/services/backup/zrepl.nix
new file mode 100644
index 00000000000..4356479b663
--- /dev/null
+++ b/nixos/modules/services/backup/zrepl.nix
@@ -0,0 +1,54 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.zrepl;
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "zrepl.yml" cfg.settings;
+in
+{
+  meta.maintainers = with maintainers; [ cole-h ];
+
+  options = {
+    services.zrepl = {
+      enable = mkEnableOption "zrepl";
+
+      settings = mkOption {
+        default = { };
+        description = ''
+          Configuration for zrepl. See <link
+          xlink:href="https://zrepl.github.io/configuration.html"/>
+          for more information.
+        '';
+        type = types.submodule {
+          freeformType = format.type;
+        };
+      };
+    };
+  };
+
+  ### Implementation ###
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.zrepl ];
+
+    # zrepl looks for its config in this location by default. This
+    # allows the use of e.g. `zrepl signal wakeup <job>` without having
+    # to specify the storepath of the config.
+    environment.etc."zrepl/zrepl.yml".source = configFile;
+
+    systemd.packages = [ pkgs.zrepl ];
+    systemd.services.zrepl = {
+      requires = [ "local-fs.target" ];
+      wantedBy = [ "zfs.target" ];
+      after = [ "zfs.target" ];
+
+      path = [ config.boot.zfs.package ];
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix
new file mode 100644
index 00000000000..be3f40f6bd8
--- /dev/null
+++ b/nixos/modules/services/blockchain/ethereum/geth.nix
@@ -0,0 +1,178 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  eachGeth = config.services.geth;
+
+  gethOpts = { config, lib, name, ...}: {
+
+    options = {
+
+      enable = lib.mkEnableOption "Go Ethereum Node";
+
+      port = mkOption {
+        type = types.port;
+        default = 30303;
+        description = "Port number Go Ethereum will be listening on, both TCP and UDP.";
+      };
+
+      http = {
+        enable = lib.mkEnableOption "Go Ethereum HTTP API";
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address of Go Ethereum HTTP API.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8545;
+          description = "Port number of Go Ethereum HTTP API.";
+        };
+
+        apis = mkOption {
+          type = types.nullOr (types.listOf types.str);
+          default = null;
+          description = "APIs to enable over WebSocket";
+          example = ["net" "eth"];
+        };
+      };
+
+      websocket = {
+        enable = lib.mkEnableOption "Go Ethereum WebSocket API";
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address of Go Ethereum WebSocket API.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8546;
+          description = "Port number of Go Ethereum WebSocket API.";
+        };
+
+        apis = mkOption {
+          type = types.nullOr (types.listOf types.str);
+          default = null;
+          description = "APIs to enable over WebSocket";
+          example = ["net" "eth"];
+        };
+      };
+
+      metrics = {
+        enable = lib.mkEnableOption "Go Ethereum prometheus metrics";
+        address = mkOption {
+          type = types.str;
+          default = "127.0.0.1";
+          description = "Listen address of Go Ethereum metrics service.";
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 6060;
+          description = "Port number of Go Ethereum metrics service.";
+        };
+      };
+
+      network = mkOption {
+        type = types.nullOr (types.enum [ "goerli" "rinkeby" "yolov2" "ropsten" ]);
+        default = null;
+        description = "The network to connect to. Mainnet (null) is the default ethereum network.";
+      };
+
+      syncmode = mkOption {
+        type = types.enum [ "fast" "full" "light" ];
+        default = "fast";
+        description = "Blockchain sync mode.";
+      };
+
+      gcmode = mkOption {
+        type = types.enum [ "full" "archive" ];
+        default = "full";
+        description = "Blockchain garbage collection mode.";
+      };
+
+      maxpeers = mkOption {
+        type = types.int;
+        default = 50;
+        description = "Maximum peers to connect to.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        description = "Additional arguments passed to Go Ethereum.";
+        default = [];
+      };
+
+      package = mkOption {
+        default = pkgs.go-ethereum.geth;
+        type = types.package;
+        description = "Package to use as Go Ethereum node.";
+      };
+    };
+  };
+in
+
+{
+
+  ###### interface
+
+  options = {
+    services.geth = mkOption {
+      type = types.attrsOf (types.submodule gethOpts);
+      default = {};
+      description = "Specification of one or more geth instances.";
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf (eachGeth != {}) {
+
+    environment.systemPackages = flatten (mapAttrsToList (gethName: cfg: [
+      cfg.package
+    ]) eachGeth);
+
+    systemd.services = mapAttrs' (gethName: cfg: (
+      nameValuePair "geth-${gethName}" (mkIf cfg.enable {
+      description = "Go Ethereum node (${gethName})";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        StateDirectory = "goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}";
+
+        # Hardening measures
+        PrivateTmp = "true";
+        ProtectSystem = "full";
+        NoNewPrivileges = "true";
+        PrivateDevices = "true";
+        MemoryDenyWriteExecute = "true";
+      };
+
+      script = ''
+        ${cfg.package}/bin/geth \
+          --nousb \
+          --ipcdisable \
+          ${optionalString (cfg.network != null) ''--${cfg.network}''} \
+          --syncmode ${cfg.syncmode} \
+          --gcmode ${cfg.gcmode} \
+          --port ${toString cfg.port} \
+          --maxpeers ${toString cfg.maxpeers} \
+          ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \
+          ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \
+          ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \
+          ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \
+          ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \
+          ${lib.escapeShellArgs cfg.extraArgs} \
+          --datadir /var/lib/goethereum/${gethName}/${if (cfg.network == null) then "mainnet" else cfg.network}
+      '';
+    }))) eachGeth;
+
+  };
+
+}
diff --git a/nixos/modules/services/cluster/hadoop/default.nix b/nixos/modules/services/cluster/hadoop/default.nix
index bfb73f68371..41ac46e538e 100644
--- a/nixos/modules/services/cluster/hadoop/default.nix
+++ b/nixos/modules/services/cluster/hadoop/default.nix
@@ -7,6 +7,7 @@ with lib;
   options.services.hadoop = {
     coreSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "fs.defaultFS" = "hdfs://localhost";
@@ -17,6 +18,7 @@ with lib;
 
     hdfsSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "dfs.nameservices" = "namenode1";
@@ -27,6 +29,7 @@ with lib;
 
     mapredSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "mapreduce.map.cpu.vcores" = "1";
@@ -37,6 +40,7 @@ with lib;
 
     yarnSite = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       example = literalExample ''
         {
           "yarn.resourcemanager.ha.id" = "resourcemanager1";
@@ -50,8 +54,7 @@ with lib;
       default = pkgs.hadoop;
       defaultText = "pkgs.hadoop";
       example = literalExample "pkgs.hadoop";
-      description = ''
-      '';
+      description = "";
     };
   };
 
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 2e8bf20a68f..e5c51441690 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -35,10 +35,20 @@ in
 
     token = mkOption {
       type = types.str;
-      description = "The k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      description = ''
+        The k3s token to use when connecting to the server. This option only makes sense for an agent.
+        WARNING: This option will expose store your token unencrypted world-readable in the nix store.
+        If this is undesired use the tokenFile option instead.
+      '';
       default = "";
     };
 
+    tokenFile = mkOption {
+      type = types.nullOr types.path;
+      description = "File path containing k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      default = null;
+    };
+
     docker = mkOption {
       type = types.bool;
       default = false;
@@ -47,6 +57,7 @@ in
 
     extraFlags = mkOption {
       description = "Extra flags to pass to the k3s command.";
+      type = types.str;
       default = "";
       example = "--no-deploy traefik --cluster-cidr 10.24.0.0/16";
     };
@@ -56,6 +67,12 @@ in
       default = false;
       description = "Only run the server. This option only makes sense for a server.";
     };
+
+    configPath = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = "File path containing the k3s YAML config. This is useful when the config is generated (for example on boot).";
+    };
   };
 
   # implementation
@@ -63,12 +80,12 @@ in
   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.configPath != null || cfg.serverAddr != "");
+        message = "serverAddr or configPath (with 'server' key) should be set if role is 'agent'";
       }
       {
-        assertion = cfg.role == "agent" -> cfg.token != "";
-        message = "token should be set if role is 'agent'";
+        assertion = cfg.role == "agent" -> cfg.configPath != null || cfg.tokenFile != null || cfg.token != "";
+        message = "token or tokenFile or configPath (with 'token' or 'token-file' keys) should be set if role is 'agent'";
       }
     ];
 
@@ -76,10 +93,18 @@ in
       enable = mkDefault true;
     };
 
+    # TODO: disable this once k3s supports cgroupsv2, either by docker
+    # supporting it, or their bundled containerd
+    systemd.enableUnifiedCgroupHierarchy = false;
+
+    environment.systemPackages = [ config.services.k3s.package ];
+
     systemd.services.k3s = {
       description = "k3s service";
-      after = mkIf cfg.docker [ "docker.service" ];
+      after = [ "network.service" "firewall.service" ] ++ (optional cfg.docker "docker.service");
+      wants = [ "network.service" "firewall.service" ];
       wantedBy = [ "multi-user.target" ];
+      path = optional config.boot.zfs.enabled config.boot.zfs.package;
       serviceConfig = {
         # See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
         Type = if cfg.role == "agent" then "exec" else "notify";
@@ -87,12 +112,19 @@ in
         Delegate = "yes";
         Restart = "always";
         RestartSec = "5s";
+        LimitNOFILE = 1048576;
+        LimitNPROC = "infinity";
+        LimitCORE = "infinity";
+        TasksMax = "infinity";
         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}")
+          ++ (optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
+          ++ (optional (cfg.token != "") "--token ${cfg.token}")
+          ++ (optional (cfg.tokenFile != null) "--token-file ${cfg.tokenFile}")
+          ++ (optional (cfg.configPath != null) "--config ${cfg.configPath}")
           ++ [ cfg.extraFlags ]
         );
       };
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
index f55079300b1..1378b5ccfb7 100644
--- a/nixos/modules/services/cluster/kubernetes/addon-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -62,7 +62,7 @@ in
       '';
     };
 
-    enable = mkEnableOption "Whether to enable Kubernetes addon manager.";
+    enable = mkEnableOption "Kubernetes addon manager.";
   };
 
   ###### implementation
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index f12e866930d..24d86628b21 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  version = "1.6.4";
+  version = "1.7.1";
   cfg = config.services.kubernetes.addons.dns;
   ports = {
     dns = 10053;
@@ -55,9 +55,9 @@ in {
       type = types.attrs;
       default = {
         imageName = "coredns/coredns";
-        imageDigest = "sha256:493ee88e1a92abebac67cbd4b5658b4730e0f33512461442d8d9214ea6734a9b";
+        imageDigest = "sha256:4a6e0769130686518325b21b0c1d0688b54e7c79244d48e1b15634e98e40c6ef";
         finalImageTag = version;
-        sha256 = "0fm9zdjavpf5hni8g7fkdd3csjbhd7n7py7llxjc66sbii087028";
+        sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
       };
     };
   };
@@ -156,7 +156,6 @@ in {
             health :${toString ports.health}
             kubernetes ${cfg.clusterDomain} in-addr.arpa ip6.arpa {
               pods insecure
-              upstream
               fallthrough in-addr.arpa ip6.arpa
             }
             prometheus :${toString ports.metrics}
diff --git a/nixos/modules/services/cluster/kubernetes/apiserver.nix b/nixos/modules/services/cluster/kubernetes/apiserver.nix
index 95bdb4c0d14..f1531caa754 100644
--- a/nixos/modules/services/cluster/kubernetes/apiserver.nix
+++ b/nixos/modules/services/cluster/kubernetes/apiserver.nix
@@ -145,7 +145,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes apiserver extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     extraSANs = mkOption {
@@ -238,14 +238,40 @@ in
       type = int;
     };
 
+    apiAudiences = mkOption {
+      description = ''
+        Kubernetes apiserver ServiceAccount issuer.
+      '';
+      default = "api,https://kubernetes.default.svc";
+      type = str;
+    };
+
+    serviceAccountIssuer = mkOption {
+      description = ''
+        Kubernetes apiserver ServiceAccount issuer.
+      '';
+      default = "https://kubernetes.default.svc";
+      type = str;
+    };
+
+    serviceAccountSigningKeyFile = mkOption {
+      description = ''
+        Path to the file that contains the current private key of the service
+        account token issuer. The issuer will sign issued ID tokens with this
+        private key.
+      '';
+      type = path;
+    };
+
     serviceAccountKeyFile = mkOption {
       description = ''
-        Kubernetes apiserver PEM-encoded x509 RSA private or public key file,
-        used to verify ServiceAccount tokens. By default tls private key file
-        is used.
+        File containing PEM-encoded x509 RSA or ECDSA private or public keys,
+        used to verify ServiceAccount tokens. The specified file can contain
+        multiple keys, and the flag can be specified multiple times with
+        different files. If unspecified, --tls-private-key-file is used.
+        Must be specified when --service-account-signing-key is provided
       '';
-      default = null;
-      type = nullOr path;
+      type = path;
     };
 
     serviceClusterIpRange = mkOption {
@@ -357,8 +383,10 @@ in
               ${optionalString (cfg.runtimeConfig != "")
                 "--runtime-config=${cfg.runtimeConfig}"} \
               --secure-port=${toString cfg.securePort} \
-              ${optionalString (cfg.serviceAccountKeyFile!=null)
-                "--service-account-key-file=${cfg.serviceAccountKeyFile}"} \
+              --api-audiences=${toString cfg.apiAudiences} \
+              --service-account-issuer=${toString cfg.serviceAccountIssuer} \
+              --service-account-signing-key-file=${cfg.serviceAccountSigningKeyFile} \
+              --service-account-key-file=${cfg.serviceAccountKeyFile} \
               --service-cluster-ip-range=${cfg.serviceClusterIpRange} \
               --storage-backend=${cfg.storageBackend} \
               ${optionalString (cfg.tlsCertFile != null)
diff --git a/nixos/modules/services/cluster/kubernetes/controller-manager.nix b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
index a99ef6640e9..0c81fa9ae49 100644
--- a/nixos/modules/services/cluster/kubernetes/controller-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/controller-manager.nix
@@ -38,7 +38,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes controller manager extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix
index 3a11a6513a4..33d217ba60e 100644
--- a/nixos/modules/services/cluster/kubernetes/default.nix
+++ b/nixos/modules/services/cluster/kubernetes/default.nix
@@ -5,6 +5,29 @@ with lib;
 let
   cfg = config.services.kubernetes;
 
+  defaultContainerdConfigFile = pkgs.writeText "containerd.toml" ''
+    version = 2
+    root = "/var/lib/containerd"
+    state = "/run/containerd"
+    oom_score = 0
+
+    [grpc]
+      address = "/run/containerd/containerd.sock"
+
+    [plugins."io.containerd.grpc.v1.cri"]
+      sandbox_image = "pause:latest"
+
+    [plugins."io.containerd.grpc.v1.cri".cni]
+      bin_dir = "/opt/cni/bin"
+      max_conf_num = 0
+
+    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+      runtime_type = "io.containerd.runc.v2"
+
+    [plugins."io.containerd.grpc.v1.cri".containerd.runtimes."io.containerd.runc.v2".options]
+      SystemdCgroup = true
+  '';
+
   mkKubeConfig = name: conf: pkgs.writeText "${name}-kubeconfig" (builtins.toJSON {
     apiVersion = "v1";
     kind = "Config";
@@ -25,8 +48,9 @@ let
         cluster = "local";
         user = name;
       };
-      current-context = "local";
+      name = "local";
     }];
+    current-context = "local";
   });
 
   caCert = secret "ca";
@@ -222,14 +246,9 @@ in {
     })
 
     (mkIf cfg.kubelet.enable {
-      virtualisation.docker = {
+      virtualisation.containerd = {
         enable = mkDefault true;
-
-        # kubernetes needs access to logs
-        logDriver = mkDefault "json-file";
-
-        # iptables must be disabled for kubernetes
-        extraOptions = "--iptables=false --ip-masq=false";
+        configFile = mkDefault defaultContainerdConfigFile;
       };
     })
 
@@ -269,7 +288,6 @@ in {
       users.users.kubernetes = {
         uid = config.ids.uids.kubernetes;
         description = "Kubernetes user";
-        extraGroups = [ "docker" ];
         group = "kubernetes";
         home = cfg.dataDir;
         createHome = true;
diff --git a/nixos/modules/services/cluster/kubernetes/flannel.nix b/nixos/modules/services/cluster/kubernetes/flannel.nix
index 548ffed1ddb..3f55719027f 100644
--- a/nixos/modules/services/cluster/kubernetes/flannel.nix
+++ b/nixos/modules/services/cluster/kubernetes/flannel.nix
@@ -8,16 +8,6 @@ let
 
   # we want flannel to use kubernetes itself as configuration backend, not direct etcd
   storageBackend = "kubernetes";
-
-  # needed for flannel to pass options to docker
-  mkDockerOpts = pkgs.runCommand "mk-docker-opts" {
-    buildInputs = [ pkgs.makeWrapper ];
-  } ''
-    mkdir -p $out
-
-    # bashInteractive needed for `compgen`
-    makeWrapper ${pkgs.bashInteractive}/bin/bash $out/mk-docker-opts --add-flags "${pkgs.kubernetes}/bin/mk-docker-opts.sh"
-  '';
 in
 {
   ###### interface
@@ -43,43 +33,17 @@ in
         cniVersion = "0.3.1";
         delegate = {
           isDefaultGateway = true;
-          bridge = "docker0";
+          bridge = "mynet";
         };
       }];
     };
 
-    systemd.services.mk-docker-opts = {
-      description = "Pre-Docker Actions";
-      path = with pkgs; [ gawk gnugrep ];
-      script = ''
-        ${mkDockerOpts}/mk-docker-opts -d /run/flannel/docker
-        systemctl restart docker
-      '';
-      serviceConfig.Type = "oneshot";
-    };
-
-    systemd.paths.flannel-subnet-env = {
-      wantedBy = [ "flannel.service" ];
-      pathConfig = {
-        PathModified = "/run/flannel/subnet.env";
-        Unit = "mk-docker-opts.service";
-      };
-    };
-
-    systemd.services.docker = {
-      environment.DOCKER_OPTS = "-b none";
-      serviceConfig.EnvironmentFile = "-/run/flannel/docker";
-    };
-
-    # read environment variables generated by mk-docker-opts
-    virtualisation.docker.extraOptions = "$DOCKER_OPTS";
-
     networking = {
       firewall.allowedUDPPorts = [
         8285  # flannel udp
         8472  # flannel vxlan
       ];
-      dhcpcd.denyInterfaces = [ "docker*" "flannel*" ];
+      dhcpcd.denyInterfaces = [ "mynet*" "flannel*" ];
     };
 
     services.kubernetes.pki.certs = {
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index c3d67552cc8..fcfcc843547 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -23,7 +23,7 @@ let
     name = "pause";
     tag = "latest";
     contents = top.package.pause;
-    config.Cmd = "/bin/pause";
+    config.Cmd = ["/bin/pause"];
   };
 
   kubeconfig = top.lib.mkKubeConfig "kubelet" cfg.kubeconfig;
@@ -125,12 +125,24 @@ in
       };
     };
 
+    containerRuntime = mkOption {
+      description = "Which container runtime type to use";
+      type = enum ["docker" "remote"];
+      default = "remote";
+    };
+
+    containerRuntimeEndpoint = mkOption {
+      description = "Endpoint at which to find the container runtime api interface/socket";
+      type = str;
+      default = "unix:///run/containerd/containerd.sock";
+    };
+
     enable = mkEnableOption "Kubernetes kubelet.";
 
     extraOpts = mkOption {
       description = "Kubernetes kubelet extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
@@ -235,17 +247,39 @@ in
   ###### implementation
   config = mkMerge [
     (mkIf cfg.enable {
+
+      environment.etc."cni/net.d".source = cniConfig;
+
       services.kubernetes.kubelet.seedDockerImages = [infraContainer];
 
+      boot.kernel.sysctl = {
+        "net.bridge.bridge-nf-call-iptables"  = 1;
+        "net.ipv4.ip_forward"                 = 1;
+        "net.bridge.bridge-nf-call-ip6tables" = 1;
+      };
+
       systemd.services.kubelet = {
         description = "Kubernetes Kubelet Service";
         wantedBy = [ "kubernetes.target" ];
-        after = [ "network.target" "docker.service" "kube-apiserver.service" ];
-        path = with pkgs; [ gitMinimal openssh docker utillinux iproute ethtool thin-provisioning-tools iptables socat ] ++ top.path;
+        after = [ "containerd.service" "network.target" "kube-apiserver.service" ];
+        path = with pkgs; [
+          gitMinimal
+          openssh
+          util-linux
+          iproute2
+          ethtool
+          thin-provisioning-tools
+          iptables
+          socat
+        ] ++ lib.optional config.boot.zfs.enabled config.boot.zfs.package ++ top.path;
         preStart = ''
           ${concatMapStrings (img: ''
-            echo "Seeding docker image: ${img}"
-            docker load <${img}
+            echo "Seeding container image: ${img}"
+            ${if (lib.hasSuffix "gz" img) then
+              ''${pkgs.gzip}/bin/zcat "${img}" | ${pkgs.containerd}/bin/ctr -n k8s.io image import --all-platforms -''
+            else
+              ''${pkgs.coreutils}/bin/cat "${img}" | ${pkgs.containerd}/bin/ctr -n k8s.io image import --all-platforms -''
+            }
           '') cfg.seedDockerImages}
 
           rm /opt/cni/bin/* || true
@@ -296,6 +330,9 @@ in
             ${optionalString (cfg.tlsKeyFile != null)
               "--tls-private-key-file=${cfg.tlsKeyFile}"} \
             ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
+            --container-runtime=${cfg.containerRuntime} \
+            --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \
+            --cgroup-driver=systemd \
             ${cfg.extraOpts}
           '';
           WorkingDirectory = top.dataDir;
@@ -305,7 +342,7 @@ in
       # Allways include cni plugins
       services.kubernetes.kubelet.cni.packages = [pkgs.cni-plugins];
 
-      boot.kernelModules = ["br_netfilter"];
+      boot.kernelModules = ["br_netfilter" "overlay"];
 
       services.kubernetes.kubelet.hostname = with config.networking;
         mkDefault (hostName + optionalString (domain != null) ".${domain}");
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 4275563f1a3..d9311d3e3a0 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -20,7 +20,7 @@ let
         size = 2048;
     };
     CN = top.masterAddress;
-    hosts = cfg.cfsslAPIExtraSANs;
+    hosts = [top.masterAddress] ++ cfg.cfsslAPIExtraSANs;
   });
 
   cfsslAPITokenBaseName = "apitoken.secret";
@@ -189,6 +189,7 @@ in
         # manually paste it in place. Just symlink.
         # otherwise, create the target file, ready for users to insert the token
 
+        mkdir -p $(dirname ${certmgrAPITokenPath})
         if [ -f "${cfsslAPITokenPath}" ]; then
           ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}"
         else
@@ -228,7 +229,8 @@ in
             };
             private_key = cert.privateKeyOptions;
             request = {
-              inherit (cert) CN hosts;
+              hosts = [cert.CN] ++ cert.hosts;
+              inherit (cert) CN;
               key = {
                 algo = "rsa";
                 size = 2048;
@@ -360,6 +362,7 @@ in
           tlsCertFile = mkDefault cert;
           tlsKeyFile = mkDefault key;
           serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.cert;
+          serviceAccountSigningKeyFile = mkDefault cfg.certs.serviceAccount.key;
           kubeletClientCaFile = mkDefault caCert;
           kubeletClientCertFile = mkDefault cfg.certs.apiserverKubeletClient.cert;
           kubeletClientKeyFile = mkDefault cfg.certs.apiserverKubeletClient.key;
diff --git a/nixos/modules/services/cluster/kubernetes/proxy.nix b/nixos/modules/services/cluster/kubernetes/proxy.nix
index 86d1dc2439b..42729f54643 100644
--- a/nixos/modules/services/cluster/kubernetes/proxy.nix
+++ b/nixos/modules/services/cluster/kubernetes/proxy.nix
@@ -25,7 +25,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes proxy extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
@@ -59,7 +59,7 @@ in
       description = "Kubernetes Proxy Service";
       wantedBy = [ "kubernetes.target" ];
       after = [ "kube-apiserver.service" ];
-      path = with pkgs; [ iptables conntrack_tools ];
+      path = with pkgs; [ iptables conntrack-tools ];
       serviceConfig = {
         Slice = "kubernetes.slice";
         ExecStart = ''${top.package}/bin/kube-proxy \
diff --git a/nixos/modules/services/cluster/kubernetes/scheduler.nix b/nixos/modules/services/cluster/kubernetes/scheduler.nix
index 5f6113227d9..454c689759d 100644
--- a/nixos/modules/services/cluster/kubernetes/scheduler.nix
+++ b/nixos/modules/services/cluster/kubernetes/scheduler.nix
@@ -21,7 +21,7 @@ in
     extraOpts = mkOption {
       description = "Kubernetes scheduler extra command line options.";
       default = "";
-      type = str;
+      type = separatedString " ";
     };
 
     featureGates = mkOption {
diff --git a/nixos/modules/services/computing/foldingathome/client.nix b/nixos/modules/services/computing/foldingathome/client.nix
index 9f99af48c48..fbef6a04b16 100644
--- a/nixos/modules/services/computing/foldingathome/client.nix
+++ b/nixos/modules/services/computing/foldingathome/client.nix
@@ -49,6 +49,15 @@ in
       '';
     };
 
+    daemonNiceLevel = mkOption {
+      type = types.ints.between (-20) 19;
+      default = 0;
+      description = ''
+        Daemon process priority for FAHClient.
+        0 is the default Unix process priority, 19 is the lowest.
+      '';
+    };
+
     extraArgs = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -70,6 +79,7 @@ in
       serviceConfig = {
         DynamicUser = true;
         StateDirectory = "foldingathome";
+        Nice = cfg.daemonNiceLevel;
         WorkingDirectory = "%S/foldingathome";
       };
     };
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 705390a21d4..a3dee94e2dc 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -14,8 +14,8 @@ let
       ClusterName=${cfg.clusterName}
       StateSaveLocation=${cfg.stateSaveLocation}
       SlurmUser=${cfg.user}
-      ${optionalString (cfg.controlMachine != null) ''controlMachine=${cfg.controlMachine}''}
-      ${optionalString (cfg.controlAddr != null) ''controlAddr=${cfg.controlAddr}''}
+      ${optionalString (cfg.controlMachine != null) "controlMachine=${cfg.controlMachine}"}
+      ${optionalString (cfg.controlAddr != null) "controlAddr=${cfg.controlAddr}"}
       ${toString (map (x: "NodeName=${x}\n") cfg.nodeName)}
       ${toString (map (x: "PartitionName=${x}\n") cfg.partitionName)}
       PlugStackConfig=${plugStackConfig}/plugstack.conf
@@ -25,7 +25,7 @@ let
 
   plugStackConfig = pkgs.writeTextDir "plugstack.conf"
     ''
-      ${optionalString cfg.enableSrunX11 ''optional ${pkgs.slurm-spank-x11}/lib/x11.so''}
+      ${optionalString cfg.enableSrunX11 "optional ${pkgs.slurm-spank-x11}/lib/x11.so"}
       ${cfg.extraPlugstackConfig}
     '';
 
@@ -34,13 +34,12 @@ let
      ${cfg.extraCgroupConfig}
    '';
 
-  slurmdbdConf = pkgs.writeTextDir "slurmdbd.conf"
+  slurmdbdConf = pkgs.writeText "slurmdbd.conf"
    ''
      DbdHost=${cfg.dbdserver.dbdHost}
      SlurmUser=${cfg.user}
      StorageType=accounting_storage/mysql
      StorageUser=${cfg.dbdserver.storageUser}
-     ${optionalString (cfg.dbdserver.storagePass != null) "StoragePass=${cfg.dbdserver.storagePass}"}
      ${cfg.dbdserver.extraConfig}
    '';
 
@@ -95,26 +94,12 @@ in
           '';
         };
 
-        storagePass = mkOption {
-          type = types.nullOr types.str;
+        storagePassFile = mkOption {
+          type = with types; nullOr str;
           default = null;
           description = ''
-            Database password. Note that this password will be publicable
-            readable in the nix store. Use <option>configFile</option>
-            to store the and config file and password outside the nix store.
-          '';
-        };
-
-        configFile = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          description = ''
-            Path to <literal>slurmdbd.conf</literal>. The password for the database connection
-            is stored in the config file. Use this option to specfify a path
-            outside the nix store. If this option is unset a configuration file
-            will be generated. See also:
-            <citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
-            <manvolnum>8</manvolnum></citerefentry>.
+            Path to file with database password. The content of this will be used to
+            create the password for the <literal>StoragePass</literal> option.
           '';
         };
 
@@ -122,7 +107,9 @@ in
           type = types.lines;
           default = "";
           description = ''
-            Extra configuration for <literal>slurmdbd.conf</literal>
+            Extra configuration for <literal>slurmdbd.conf</literal> See also:
+            <citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
+            <manvolnum>8</manvolnum></citerefentry>.
           '';
         };
       };
@@ -287,11 +274,30 @@ in
         '';
       };
 
+      etcSlurm = mkOption {
+        type = types.path;
+        internal = true;
+        default = etcSlurm;
+        description = ''
+          Path to directory with slurm config files. This option is set by default from the
+          Slurm module and is meant to make the Slurm config file available to other modules.
+        '';
+      };
 
     };
 
   };
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "slurm" "dbdserver" "storagePass" ] ''
+      This option has been removed so that the database password is not exposed via the nix store.
+      Use services.slurm.dbdserver.storagePassFile to provide the database password.
+    '')
+    (mkRemovedOptionModule [ "services" "slurm" "dbdserver" "configFile" ] ''
+      This option has been removed. Use services.slurm.dbdserver.storagePassFile
+      and services.slurm.dbdserver.extraConfig instead.
+    '')
+  ];
 
   ###### implementation
 
@@ -311,7 +317,7 @@ in
           #!/bin/sh
           if [ -z "$SLURM_CONF" ]
           then
-            SLURM_CONF="${etcSlurm}/slurm.conf" "$EXE" "\$@"
+            SLURM_CONF="${cfg.etcSlurm}/slurm.conf" "$EXE" "\$@"
           else
             "$EXE" "\$0"
           fi
@@ -386,23 +392,32 @@ in
       '';
     };
 
-    systemd.services.slurmdbd = mkIf (cfg.dbdserver.enable) {
+    systemd.services.slurmdbd = let
+      # slurm strips the last component off the path
+      configPath = "$RUNTIME_DIRECTORY/slurmdbd.conf";
+    in mkIf (cfg.dbdserver.enable) {
       path = with pkgs; [ wrappedSlurm munge coreutils ];
 
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" "munged.service" "mysql.service" ];
       requires = [ "munged.service" "mysql.service" ];
 
-      # slurm strips the last component off the path
-      environment.SLURM_CONF =
-        if (cfg.dbdserver.configFile == null) then
-          "${slurmdbdConf}/slurm.conf"
-        else
-          cfg.dbdserver.configFile;
+      preStart = ''
+        install -m 600 -o ${cfg.user} -T ${slurmdbdConf} ${configPath}
+        ${optionalString (cfg.dbdserver.storagePassFile != null) ''
+          echo "StoragePass=$(cat ${cfg.dbdserver.storagePassFile})" \
+            >> ${configPath}
+        ''}
+      '';
+
+      script = ''
+        export SLURM_CONF=${configPath}
+        exec ${cfg.package}/bin/slurmdbd -D
+      '';
 
       serviceConfig = {
-        Type = "forking";
-        ExecStart = "${cfg.package}/bin/slurmdbd";
+        RuntimeDirectory = "slurmdbd";
+        Type = "simple";
         PIDFile = "/run/slurmdbd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
       };
diff --git a/nixos/modules/services/computing/torque/mom.nix b/nixos/modules/services/computing/torque/mom.nix
index 0c5f43cf3e6..6747bd4b0d5 100644
--- a/nixos/modules/services/computing/torque/mom.nix
+++ b/nixos/modules/services/computing/torque/mom.nix
@@ -32,7 +32,7 @@ in
     environment.systemPackages = [ pkgs.torque ];
 
     systemd.services.torque-mom-init = {
-      path = with pkgs; [ torque utillinux procps inetutils ];
+      path = with pkgs; [ torque util-linux procps inetutils ];
 
       script = ''
         pbs_mkdirs -v aux
diff --git a/nixos/modules/services/computing/torque/server.nix b/nixos/modules/services/computing/torque/server.nix
index 21c5a4f4672..8d923fc04d4 100644
--- a/nixos/modules/services/computing/torque/server.nix
+++ b/nixos/modules/services/computing/torque/server.nix
@@ -21,7 +21,7 @@ in
     environment.systemPackages = [ pkgs.torque ];
 
     systemd.services.torque-server-init = {
-      path = with pkgs; [ torque utillinux procps inetutils ];
+      path = with pkgs; [ torque util-linux procps inetutils ];
 
       script = ''
         tmpsetup=$(mktemp -t torque-XXXX)
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index e1950b91382..f668e69e5df 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -223,6 +223,7 @@ in {
       };
 
       pythonPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = pythonPackages: with pythonPackages; [ ];
         defaultText = "pythonPackages: with pythonPackages; [ ]";
         description = "Packages to add the to the PYTHONPATH of the buildbot process.";
@@ -282,5 +283,5 @@ in {
     '')
   ];
 
-  meta.maintainers = with lib.maintainers; [ nand0p mic92 ];
+  meta.maintainers = with lib.maintainers; [ mic92 lopsided98 ];
 }
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 7b8a35f54bf..708b3e1cc18 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -191,6 +191,6 @@ in {
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ nand0p ];
+  meta.maintainers = with lib.maintainers; [ ];
 
 }
diff --git a/nixos/modules/services/continuous-integration/buildkite-agents.nix b/nixos/modules/services/continuous-integration/buildkite-agents.nix
index b0045409ae6..b8982d757db 100644
--- a/nixos/modules/services/continuous-integration/buildkite-agents.nix
+++ b/nixos/modules/services/continuous-integration/buildkite-agents.nix
@@ -76,7 +76,7 @@ let
       };
 
       tags = mkOption {
-        type = types.attrsOf types.str;
+        type = types.attrsOf (types.either types.str (types.listOf types.str));
         default = {};
         example = { queue = "default"; docker = "true"; ruby2 ="true"; };
         description = ''
@@ -230,18 +230,21 @@ 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);
+          tagStr = name: value:
+            if lib.isList value
+            then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value)
+            else "${name}=${value}";
+          tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags);
         in
           optionalString (cfg.privateSshKeyPath != null) ''
             mkdir -m 0700 -p "${sshDir}"
-            cp -f "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
-            chmod 600 "${sshDir}"/id_rsa
+            install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa"
           '' + ''
             cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF
             token="$(cat ${toString cfg.tokenPath})"
             name="${cfg.name}"
             shell="${cfg.shell}"
-            tags="${tagStr}"
+            tags="${tagsStr}"
             build-path="${cfg.dataDir}/builds"
             hooks-path="${cfg.hooksPath}"
             ${cfg.extraConfig}
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
new file mode 100644
index 00000000000..9627b723f8f
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -0,0 +1,299 @@
+{ config, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.github-runner;
+  svcName = "github-runner";
+  systemdDir = "${svcName}/${cfg.name}";
+  # %t: Runtime directory root (usually /run); see systemd.unit(5)
+  runtimeDir = "%t/${systemdDir}";
+  # %S: State directory root (usually /var/lib); see systemd.unit(5)
+  stateDir = "%S/${systemdDir}";
+  # %L: Log directory root (usually /var/log); see systemd.unit(5)
+  logsDir = "%L/${systemdDir}";
+in
+{
+  options.services.github-runner = {
+    enable = mkOption {
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable GitHub Actions runner.
+
+        Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here:
+        <link xlink:href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners"
+        >About self-hosted runners</link>.
+      '';
+      type = lib.types.bool;
+    };
+
+    url = mkOption {
+      type = types.str;
+      description = ''
+        Repository to add the runner to.
+
+        Changing this option triggers a new runner registration.
+      '';
+      example = "https://github.com/nixos/nixpkgs";
+    };
+
+    tokenFile = mkOption {
+      type = types.path;
+      description = ''
+        The full path to a file which contains the runner registration token.
+        The file should contain exactly one line with the token without any newline.
+        The token can be used to re-register a runner of the same name but is time-limited.
+
+        Changing this option or the file's content triggers a new runner registration.
+      '';
+      example = "/run/secrets/github-runner/nixos.token";
+    };
+
+    name = mkOption {
+      # Same pattern as for `networking.hostName`
+      type = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
+      description = ''
+        Name of the runner to configure. Defaults to the hostname.
+
+        Changing this option triggers a new runner registration.
+      '';
+      example = "nixos";
+      default = config.networking.hostName;
+    };
+
+    runnerGroup = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Name of the runner group to add this runner to (defaults to the default runner group).
+
+        Changing this option triggers a new runner registration.
+      '';
+      default = null;
+    };
+
+    extraLabels = mkOption {
+      type = types.listOf types.str;
+      description = ''
+        Extra labels in addition to the default (<literal>["self-hosted", "Linux", "X64"]</literal>).
+
+        Changing this option triggers a new runner registration.
+      '';
+      example = literalExample ''[ "nixos" ]'';
+      default = [ ];
+    };
+
+    replace = mkOption {
+      type = types.bool;
+      description = ''
+        Replace any existing runner with the same name.
+
+        Without this flag, registering a new runner with the same name fails.
+      '';
+      default = false;
+    };
+
+    extraPackages = mkOption {
+      type = types.listOf types.package;
+      description = ''
+        Extra packages to add to <literal>PATH</literal> of the service to make them available to workflows.
+      '';
+      default = [ ];
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = optionals (isStorePath cfg.tokenFile) [
+      ''
+        `services.github-runner.tokenFile` points to the Nix store and, therefore, is world-readable.
+        Consider using a path outside of the Nix store to keep the token private.
+      ''
+    ];
+
+    systemd.services.${svcName} = {
+      description = "GitHub Actions runner";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
+
+      environment = {
+        HOME = runtimeDir;
+        RUNNER_ROOT = runtimeDir;
+      };
+
+      path = (with pkgs; [
+        bash
+        coreutils
+        git
+        gnutar
+        gzip
+      ]) ++ [
+        config.nix.package
+      ] ++ cfg.extraPackages;
+
+      serviceConfig = rec {
+        ExecStart = "${pkgs.github-runner}/bin/runsvc.sh";
+
+        # Does the following, sequentially:
+        # - Copy the current and the previous `tokenFile` to the $RUNTIME_DIRECTORY
+        #   and make it accessible to the service user to allow for a content
+        #   comparison.
+        # - If the module configuration or the token has changed, clear the state directory.
+        # - Configure the runner.
+        # - Copy the configured `tokenFile` to the $STATE_DIRECTORY and make it
+        #   inaccessible to the service user.
+        # - Set up the directory structure by creating the necessary symlinks.
+        ExecStartPre =
+          let
+            # Wrapper script which expects the full path of the state, runtime and logs
+            # directory as arguments. Overrides the respective systemd variables to provide
+            # unambiguous directory names. This becomes relevant, for example, if the
+            # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
+            # to contain more than one directory. This causes systemd to set the respective
+            # environment variables with the path of all of the given directories, separated
+            # by a colon.
+            writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
+              set -euo pipefail
+
+              STATE_DIRECTORY="$1"
+              RUNTIME_DIRECTORY="$2"
+              LOGS_DIRECTORY="$3"
+
+              ${lines}
+            '';
+            currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
+            runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" ] cfg;
+            newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
+            currentConfigTokenFilename = ".current-token";
+            newConfigTokenFilename = ".new-token";
+            runnerCredFiles = [
+              ".credentials"
+              ".credentials_rsaparams"
+              ".runner"
+            ];
+            ownConfigTokens = writeScript "own-config-tokens" ''
+              # Copy current and new token file to runtime dir and make it accessible to the service user
+              cp ${escapeShellArg cfg.tokenFile} "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
+              chmod 600 "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
+              chown "$USER" "$RUNTIME_DIRECTORY/${newConfigTokenFilename}"
+
+              if [[ -e "$STATE_DIRECTORY/${currentConfigTokenFilename}" ]]; then
+                cp "$STATE_DIRECTORY/${currentConfigTokenFilename}" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
+                chmod 600 "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
+                chown "$USER" "$RUNTIME_DIRECTORY/${currentConfigTokenFilename}"
+              fi
+            '';
+            disownConfigTokens = writeScript "disown-config-tokens" ''
+              # Make the token inaccessible to the runner service user
+              chmod 600 "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+              chown root:root "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+            '';
+            unconfigureRunner = writeScript "unconfigure" ''
+              differs=
+              # Set `differs = 1` if current and new runner config differ or if `currentConfigPath` does not exist
+              ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 || differs=1
+              # Also trigger a registration if the token content changed
+              ${pkgs.diffutils}/bin/diff -q \
+                "$RUNTIME_DIRECTORY"/{${currentConfigTokenFilename},${newConfigTokenFilename}} \
+                >/dev/null 2>&1 || differs=1
+
+              if [[ -n "$differs" ]]; then
+                echo "Config has changed, removing old runner state."
+                echo "The old runner will still appear in the GitHub Actions UI." \
+                  "You have to remove it manually."
+                find "$STATE_DIRECTORY/" -mindepth 1 -delete
+              fi
+            '';
+            configureRunner = writeScript "configure" ''
+              empty=$(ls -A "$STATE_DIRECTORY")
+              if [[ -z "$empty" ]]; then
+                echo "Configuring GitHub Actions Runner"
+                token=$(< "$RUNTIME_DIRECTORY"/${newConfigTokenFilename})
+                RUNNER_ROOT="$STATE_DIRECTORY" ${pkgs.github-runner}/bin/config.sh \
+                  --unattended \
+                  --work "$RUNTIME_DIRECTORY" \
+                  --url ${escapeShellArg cfg.url} \
+                  --token "$token" \
+                  --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} \
+                  --name ${escapeShellArg cfg.name} \
+                  ${optionalString cfg.replace "--replace"} \
+                  ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
+
+                # Move the automatically created _diag dir to the logs dir
+                mkdir -p  "$STATE_DIRECTORY/_diag"
+                cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
+                rm    -rf "$STATE_DIRECTORY/_diag/"
+
+                # Cleanup token from config
+                rm -f "$RUNTIME_DIRECTORY"/${currentConfigTokenFilename}
+                mv    "$RUNTIME_DIRECTORY"/${newConfigTokenFilename} "$STATE_DIRECTORY/${currentConfigTokenFilename}"
+
+                # Symlink to new config
+                ln -s '${newConfigPath}' "${currentConfigPath}"
+              fi
+            '';
+            setupRuntimeDir = writeScript "setup-runtime-dirs" ''
+              # Link _diag dir
+              ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag"
+
+              # Link the runner credentials to the runtime dir
+              ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/"
+            '';
+          in
+          map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
+            "+${ownConfigTokens}" # runs as root
+            unconfigureRunner
+            configureRunner
+            "+${disownConfigTokens}" # runs as root
+            setupRuntimeDir
+          ];
+
+        # Contains _diag
+        LogsDirectory = [ systemdDir ];
+        # Default RUNNER_ROOT which contains ephemeral Runner data
+        RuntimeDirectory = [ systemdDir ];
+        # Home of persistent runner data, e.g., credentials
+        StateDirectory = [ systemdDir ];
+        StateDirectoryMode = "0700";
+        WorkingDirectory = runtimeDir;
+
+        # By default, use a dynamically allocated user
+        DynamicUser = true;
+
+        KillMode = "process";
+        KillSignal = "SIGTERM";
+
+        # Hardening (may overlap with DynamicUser=)
+        # The following options are only for optimizing:
+        # systemd-analyze security github-runner
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        UMask = "0066";
+
+        # Needs network access
+        PrivateNetwork = false;
+        # Cannot be true due to Node
+        MemoryDenyWriteExecute = false;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index 431555309cc..2c6d9530a6b 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -66,10 +66,10 @@ let
             ++ optional service.debugTraceDisabled
             "--debug-trace-disabled"
             ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
-            ++ optionals (service.executor == "docker") (
+            ++ optionals (hasPrefix "docker" service.executor) (
               assert (
                 assertMsg (service.dockerImage != null)
-                  "dockerImage option is required for docker executor (${name})");
+                  "dockerImage option is required for ${service.executor} executor (${name})");
               [ "--docker-image ${service.dockerImage}" ]
               ++ optional service.dockerDisableCache
               "--docker-disable-cache"
@@ -541,7 +541,7 @@ in
         jq
         moreutils
         remarshal
-        utillinux
+        util-linux
         cfg.package
       ] ++ cfg.extraPackages;
       reloadIfChanged = true;
diff --git a/nixos/modules/services/continuous-integration/gocd-agent/default.nix b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
index 2e9e1c94857..8cae08bf1fa 100644
--- a/nixos/modules/services/continuous-integration/gocd-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-agent/default.nix
@@ -90,6 +90,7 @@ in {
       };
 
       startupOptions = mkOption {
+        type = types.listOf types.str;
         default = [
           "-Xms${cfg.initialJavaHeapSize}"
           "-Xmx${cfg.maxJavaHeapMemory}"
@@ -105,6 +106,7 @@ in {
 
       extraOptions = mkOption {
         default = [ ];
+        type = types.listOf types.str;
         example = [
           "-X debug"
           "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"
diff --git a/nixos/modules/services/continuous-integration/gocd-server/default.nix b/nixos/modules/services/continuous-integration/gocd-server/default.nix
index 4fa41ac49ed..4c829664a0a 100644
--- a/nixos/modules/services/continuous-integration/gocd-server/default.nix
+++ b/nixos/modules/services/continuous-integration/gocd-server/default.nix
@@ -27,6 +27,7 @@ in {
 
       extraGroups = mkOption {
         default = [ ];
+        type = types.listOf types.str;
         example = [ "wheel" "docker" ];
         description = ''
           List of extra groups that the "gocd-server" user should be a part of.
@@ -92,6 +93,7 @@ in {
       };
 
       startupOptions = mkOption {
+        type = types.listOf types.str;
         default = [
           "-Xms${cfg.initialJavaHeapSize}"
           "-Xmx${cfg.maxJavaHeapMemory}"
@@ -113,6 +115,7 @@ in {
 
       extraOptions = mkOption {
         default = [ ];
+        type = types.listOf types.str;
         example = [
           "-X debug"
           "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
new file mode 100644
index 00000000000..70d85a97f3b
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/common.nix
@@ -0,0 +1,210 @@
+/*
+
+This file is for options that NixOS and nix-darwin have in common.
+
+Platform-specific code is in the respective default.nix files.
+
+ */
+
+{ config, lib, options, pkgs, ... }:
+let
+  inherit (lib)
+    filterAttrs
+    literalExample
+    mkIf
+    mkOption
+    mkRemovedOptionModule
+    mkRenamedOptionModule
+    types
+    ;
+
+  cfg =
+    config.services.hercules-ci-agent;
+
+  format = pkgs.formats.toml { };
+
+  settingsModule = { config, ... }: {
+    freeformType = format.type;
+    options = {
+      baseDirectory = mkOption {
+        type = types.path;
+        default = "/var/lib/hercules-ci-agent";
+        description = ''
+          State directory (secrets, work directory, etc) for agent
+        '';
+      };
+      concurrentTasks = mkOption {
+        description = ''
+          Number of tasks to perform simultaneously.
+
+          A task is a single derivation build, an evaluation or an effect run.
+          At minimum, you need 2 concurrent tasks for <literal>x86_64-linux</literal>
+          in your cluster, to allow for import from derivation.
+
+          <literal>concurrentTasks</literal> can be around the CPU core count or lower if memory is
+          the bottleneck.
+
+          The optimal value depends on the resource consumption characteristics of your workload,
+          including memory usage and in-task parallelism. This is typically determined empirically.
+
+          When scaling, it is generally better to have a double-size machine than two machines,
+          because each split of resources causes inefficiencies; particularly with regards
+          to build latency because of extra downloads.
+        '';
+        type = types.either types.ints.positive (types.enum [ "auto" ]);
+        default = "auto";
+      };
+      workDirectory = mkOption {
+        description = ''
+          The directory in which temporary subdirectories are created for task state. This includes sources for Nix evaluation.
+        '';
+        type = types.path;
+        default = config.baseDirectory + "/work";
+        defaultText = literalExample ''baseDirectory + "/work"'';
+      };
+      staticSecretsDirectory = mkOption {
+        description = ''
+          This is the default directory to look for statically configured secrets like <literal>cluster-join-token.key</literal>.
+        '';
+        type = types.path;
+        default = config.baseDirectory + "/secrets";
+        defaultText = literalExample ''baseDirectory + "/secrets"'';
+      };
+      clusterJoinTokenPath = mkOption {
+        description = ''
+          Location of the cluster-join-token.key file.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/cluster-join-token.key";
+        defaultText = literalExample ''staticSecretsDirectory + "/cluster-join-token.key"'';
+        # internal: It's a bit too detailed to show by default in the docs,
+        # but useful to define explicitly to allow reuse by other modules.
+        internal = true;
+      };
+      binaryCachesPath = mkOption {
+        description = ''
+          Location of the binary-caches.json file.
+        '';
+        type = types.path;
+        default = config.staticSecretsDirectory + "/binary-caches.json";
+        defaultText = literalExample ''staticSecretsDirectory + "/binary-caches.json"'';
+        # internal: It's a bit too detailed to show by default in the docs,
+        # but useful to define explicitly to allow reuse by other modules.
+        internal = true;
+      };
+    };
+  };
+
+  # TODO (roberth, >=2022) remove
+  checkNix =
+    if !cfg.checkNix
+    then ""
+    else if lib.versionAtLeast config.nix.package.version "2.3.10"
+    then ""
+    else
+      pkgs.stdenv.mkDerivation {
+        name = "hercules-ci-check-system-nix-src";
+        inherit (config.nix.package) src patches;
+        dontConfigure = true;
+        buildPhase = ''
+          echo "Checking in-memory pathInfoCache expiry"
+          if ! grep 'PathInfoCacheValue' src/libstore/store-api.hh >/dev/null; then
+            cat 1>&2 <<EOF
+
+            You are deploying Hercules CI Agent on a system with an incompatible
+            nix-daemon. Please make sure nix.package is set to a Nix version of at
+            least 2.3.10 or a master version more recent than Mar 12, 2020.
+          EOF
+            exit 1
+          fi
+        '';
+        installPhase = "touch $out";
+      };
+
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "extraOptions" ] [ "services" "hercules-ci-agent" "settings" ])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "baseDirectory" ] [ "services" "hercules-ci-agent" "settings" "baseDirectory" ])
+    (mkRenamedOptionModule [ "services" "hercules-ci-agent" "concurrentTasks" ] [ "services" "hercules-ci-agent" "settings" "concurrentTasks" ])
+    (mkRemovedOptionModule [ "services" "hercules-ci-agent" "patchNix" ] "Nix versions packaged in this version of Nixpkgs don't need a patched nix-daemon to work correctly in Hercules CI Agent clusters.")
+  ];
+
+  options.services.hercules-ci-agent = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable to run Hercules CI Agent as a system service.
+
+        <link xlink:href="https://hercules-ci.com">Hercules CI</link> is a
+        continuous integation service that is centered around Nix.
+
+        Support is available at <link xlink:href="mailto:help@hercules-ci.com">help@hercules-ci.com</link>.
+      '';
+    };
+    checkNix = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to make sure that the system's Nix (nix-daemon) is compatible.
+
+        If you set this to false, please keep up with the change log.
+      '';
+    };
+    package = mkOption {
+      description = ''
+        Package containing the bin/hercules-ci-agent executable.
+      '';
+      type = types.package;
+      default = pkgs.hercules-ci-agent;
+      defaultText = literalExample "pkgs.hercules-ci-agent";
+    };
+    settings = mkOption {
+      description = ''
+        These settings are written to the <literal>agent.toml</literal> file.
+
+        Not all settings are listed as options, can be set nonetheless.
+
+        For the exhaustive list of settings, see <link xlink:href="https://docs.hercules-ci.com/hercules-ci/reference/agent-config/"/>.
+      '';
+      type = types.submoduleWith { modules = [ settingsModule ]; };
+    };
+
+    /*
+      Internal and/or computed values.
+
+      These are written as options instead of let binding to allow sharing with
+      default.nix on both NixOS and nix-darwin.
+     */
+    tomlFile = mkOption {
+      type = types.path;
+      internal = true;
+      defaultText = "generated hercules-ci-agent.toml";
+      description = ''
+        The fully assembled config file.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    nix.extraOptions = lib.addContextFrom checkNix ''
+      # A store path that was missing at first may well have finished building,
+      # even shortly after the previous lookup. This *also* applies to the daemon.
+      narinfo-cache-negative-ttl = 0
+    '';
+    services.hercules-ci-agent = {
+      tomlFile =
+        format.generate "hercules-ci-agent.toml" cfg.settings;
+
+      settings.labels = {
+        agent.source =
+          if options.services.hercules-ci-agent.package.highestPrio == (lib.modules.mkOptionDefault { }).priority
+          then "nixpkgs"
+          else lib.mkOptionDefault "override";
+        pkgs.version = pkgs.lib.version;
+        lib.version = lib.version;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
new file mode 100644
index 00000000000..06c174e7d37
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -0,0 +1,101 @@
+/*
+
+This file is for NixOS-specific options and configs.
+
+Code that is shared with nix-darwin goes in common.nix.
+
+ */
+
+{ pkgs, config, lib, ... }:
+let
+  inherit (lib) mkIf mkDefault;
+
+  cfg = config.services.hercules-ci-agent;
+
+  command = "${cfg.package}/bin/hercules-ci-agent --config ${cfg.tomlFile}";
+  testCommand = "${command} --test-configuration";
+
+in
+{
+  imports = [
+    ./common.nix
+    (lib.mkRenamedOptionModule [ "services" "hercules-ci-agent" "user" ] [ "systemd" "services" "hercules-ci-agent" "serviceConfig" "User" ])
+  ];
+
+  config = mkIf cfg.enable {
+    systemd.services.hercules-ci-agent = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      path = [ config.nix.package ];
+      startLimitBurst = 30 * 1000000; # practically infinite
+      serviceConfig = {
+        User = "hercules-ci-agent";
+        ExecStart = command;
+        ExecStartPre = testCommand;
+        Restart = "on-failure";
+        RestartSec = 120;
+      };
+    };
+
+    # Changes in the secrets do not affect the unit in any way that would cause
+    # a restart, which is currently necessary to reload the secrets.
+    systemd.paths.hercules-ci-agent-restart-files = {
+      wantedBy = [ "hercules-ci-agent.service" ];
+      pathConfig = {
+        Unit = "hercules-ci-agent-restarter.service";
+        PathChanged = [ cfg.settings.clusterJoinTokenPath cfg.settings.binaryCachesPath ];
+      };
+    };
+    systemd.services.hercules-ci-agent-restarter = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        # Wait a bit, with the effect of bundling up file changes into a single
+        # run of this script and hopefully a single restart.
+        sleep 10
+        if systemctl is-active --quiet hercules-ci-agent.service; then
+          if ${testCommand}; then
+            systemctl restart hercules-ci-agent.service
+          else
+            echo 1>&2 "WARNING: Not restarting agent because config is not valid at this time."
+          fi
+        else
+          echo 1>&2 "Not restarting hercules-ci-agent despite config file update, because it is not already active."
+        fi
+      '';
+    };
+
+    # Trusted user allows simplified configuration and better performance
+    # when operating in a cluster.
+    nix.trustedUsers = [ config.systemd.services.hercules-ci-agent.serviceConfig.User ];
+    services.hercules-ci-agent = {
+      settings = {
+        nixUserIsTrusted = true;
+        labels =
+          let
+            mkIfNotNull = x: mkIf (x != null) x;
+          in
+          {
+            nixos.configurationRevision = mkIfNotNull config.system.configurationRevision;
+            nixos.release = config.system.nixos.release;
+            nixos.label = mkIfNotNull config.system.nixos.label;
+            nixos.codeName = config.system.nixos.codeName;
+            nixos.tags = config.system.nixos.tags;
+            nixos.systemName = mkIfNotNull config.system.name;
+          };
+      };
+    };
+
+    users.users.hercules-ci-agent = {
+      home = cfg.settings.baseDirectory;
+      createHome = true;
+      group = "hercules-ci-agent";
+      description = "Hercules CI Agent system user";
+      isSystemUser = true;
+    };
+
+    users.groups.hercules-ci-agent = { };
+  };
+
+  meta.maintainers = [ lib.maintainers.roberth ];
+}
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 502a5898a5d..0103cd723d2 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -37,8 +37,6 @@ let
 
   haveLocalDB = cfg.dbi == localDB;
 
-  inherit (config.system) stateVersion;
-
   hydra-package =
   let
     makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv);
@@ -91,12 +89,18 @@ in
         example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
         description = ''
           The DBI string for Hydra database connection.
+
+          NOTE: Attempts to set `application_name` will be overridden by
+          `hydra-TYPE` (where TYPE is e.g. `evaluator`, `queue-runner`,
+          etc.) in all hydra services to more easily distinguish where
+          queries are coming from.
         '';
       };
 
       package = mkOption {
         type = types.package;
-        defaultText = "pkgs.hydra";
+        default = pkgs.hydra-unstable;
+        defaultText = "pkgs.hydra-unstable";
         description = "The Hydra package.";
       };
 
@@ -225,34 +229,6 @@ 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;
     };
@@ -260,7 +236,7 @@ in
     users.users.hydra =
       { description = "Hydra";
         group = "hydra";
-        createHome = true;
+        # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
         home = baseDir;
         useDefaultShell = true;
         uid = config.ids.uids.hydra;
@@ -304,6 +280,8 @@ in
       keep-outputs = true
       keep-derivations = true
 
+
+    '' + optionalString (versionOlder (getVersion config.nix.package.out) "2.4pre") ''
       # The default (`true') slows Nix down a lot since the build farm
       # has so many GC roots.
       gc-check-reachability = false
@@ -313,7 +291,9 @@ in
       { wantedBy = [ "multi-user.target" ];
         requires = optional haveLocalDB "postgresql.service";
         after = optional haveLocalDB "postgresql.service";
-        environment = env;
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
+        };
         preStart = ''
           mkdir -p ${baseDir}
           chown hydra.hydra ${baseDir}
@@ -368,7 +348,9 @@ in
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" ];
-        environment = serverEnv;
+        environment = serverEnv // {
+          HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
+        };
         restartTriggers = [ hydraConf ];
         serviceConfig =
           { ExecStart =
@@ -390,6 +372,7 @@ in
         environment = env // {
           PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
           IN_SYSTEMD = "1"; # to get log severity levels
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner";
         };
         serviceConfig =
           { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
@@ -409,7 +392,9 @@ in
         after = [ "hydra-init.service" "network.target" ];
         path = with pkgs; [ hydra-package nettools jq ];
         restartTriggers = [ hydraConf ];
-        environment = env;
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
+        };
         serviceConfig =
           { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
             User = "hydra";
@@ -421,7 +406,9 @@ in
     systemd.services.hydra-update-gc-roots =
       { requires = [ "hydra-init.service" ];
         after = [ "hydra-init.service" ];
-        environment = env;
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots";
+        };
         serviceConfig =
           { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
             User = "hydra";
@@ -432,7 +419,9 @@ in
     systemd.services.hydra-send-stats =
       { wantedBy = [ "multi-user.target" ];
         after = [ "hydra-init.service" ];
-        environment = env;
+        environment = env // {
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats";
+        };
         serviceConfig =
           { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
             User = "hydra";
@@ -446,6 +435,7 @@ in
         restartTriggers = [ hydraConf ];
         environment = env // {
           PGPASSFILE = "${baseDir}/pgpass-queue-runner";
+          HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
         };
         serviceConfig =
           { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix
index 1477c471f8a..889688a2685 100644
--- a/nixos/modules/services/continuous-integration/jenkins/default.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/default.nix
@@ -2,6 +2,7 @@
 with lib;
 let
   cfg = config.services.jenkins;
+  jenkinsUrl = "http://${cfg.listenAddress}:${toString cfg.port}${cfg.prefix}";
 in {
   options = {
     services.jenkins = {
@@ -86,8 +87,8 @@ in {
       };
 
       packages = mkOption {
-        default = [ pkgs.stdenv pkgs.git pkgs.jdk config.programs.ssh.package pkgs.nix ];
-        defaultText = "[ pkgs.stdenv pkgs.git pkgs.jdk config.programs.ssh.package pkgs.nix ]";
+        default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ];
+        defaultText = "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]";
         type = types.listOf types.package;
         description = ''
           Packages to add to PATH for the jenkins process.
@@ -141,14 +142,34 @@ in {
           Additional command line arguments to pass to the Java run time (as opposed to Jenkins).
         '';
       };
+
+      withCLI = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to make the CLI available.
+
+          More info about the CLI available at
+          <link xlink:href="https://www.jenkins.io/doc/book/managing/cli">
+          https://www.jenkins.io/doc/book/managing/cli</link> .
+        '';
+      };
     };
   };
 
   config = mkIf cfg.enable {
-    # server references the dejavu fonts
-    environment.systemPackages = [
-      pkgs.dejavu_fonts
-    ];
+    environment = {
+      # server references the dejavu fonts
+      systemPackages = [
+        pkgs.dejavu_fonts
+      ] ++ optional cfg.withCLI cfg.package;
+
+      variables = {}
+        // optionalAttrs cfg.withCLI {
+          # Make it more convenient to use the `jenkins-cli`.
+          JENKINS_URL = jenkinsUrl;
+        };
+    };
 
     users.groups = optionalAttrs (cfg.group == "jenkins") {
       jenkins.gid = config.ids.gids.jenkins;
@@ -207,7 +228,7 @@ in {
 
       # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript
       script = ''
-        ${pkgs.jdk}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
+        ${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \
                                                   --httpPort=${toString cfg.port} \
                                                   --prefix=${cfg.prefix} \
                                                   -Djava.awt.headless=true \
@@ -215,7 +236,7 @@ in {
       '';
 
       postStart = ''
-        until [[ $(${pkgs.curl.bin}/bin/curl -L -s --head -w '\n%{http_code}' http://${cfg.listenAddress}:${toString cfg.port}${cfg.prefix} | tail -n1) =~ ^(200|403)$ ]]; do
+        until [[ $(${pkgs.curl.bin}/bin/curl -L -s --head -w '\n%{http_code}' ${jenkinsUrl} | tail -n1) =~ ^(200|403)$ ]]; do
           sleep 1
         done
       '';
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
index 5d1bfe4ec40..536d394b3fd 100644
--- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -165,6 +165,42 @@ in {
           '';
         in
           ''
+            joinByString()
+            {
+                local separator="$1"
+                shift
+                local first="$1"
+                shift
+                printf "%s" "$first" "''${@/#/$separator}"
+            }
+
+            # Map a relative directory path in the output from
+            # jenkins-job-builder (jobname) to the layout expected by jenkins:
+            # each directory level gets prepended "jobs/".
+            getJenkinsJobDir()
+            {
+                IFS='/' read -ra input_dirs <<< "$1"
+                printf "jobs/"
+                joinByString "/jobs/" "''${input_dirs[@]}"
+            }
+
+            # The inverse of getJenkinsJobDir (remove the "jobs/" prefixes)
+            getJobname()
+            {
+                IFS='/' read -ra input_dirs <<< "$1"
+                local i=0
+                local nelem=''${#input_dirs[@]}
+                for e in "''${input_dirs[@]}"; do
+                    if [ $((i % 2)) -eq 1 ]; then
+                        printf "$e"
+                        if [ $i -lt $(( nelem - 1 )) ]; then
+                            printf "/"
+                        fi
+                    fi
+                    i=$((i + 1))
+                done
+            }
+
             rm -rf ${jobBuilderOutputDir}
             cur_decl_jobs=/run/jenkins-job-builder/declarative-jobs
             rm -f "$cur_decl_jobs"
@@ -172,27 +208,27 @@ in {
             # Create / update jobs
             mkdir -p ${jobBuilderOutputDir}
             for inputFile in ${yamlJobsFile} ${concatStringsSep " " jsonJobsFiles}; do
-                HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test -o "${jobBuilderOutputDir}" "$inputFile"
+                HOME="${jenkinsCfg.home}" "${pkgs.jenkins-job-builder}/bin/jenkins-jobs" --ignore-cache test --config-xml -o "${jobBuilderOutputDir}" "$inputFile"
             done
 
-            for file in "${jobBuilderOutputDir}/"*; do
-                test -f "$file" || continue
-                jobname="$(basename $file)"
-                jobdir="${jenkinsCfg.home}/jobs/$jobname"
+            find "${jobBuilderOutputDir}" -type f -name config.xml | while read -r f; do echo "$(dirname "$f")"; done | sort | while read -r dir; do
+                jobname="$(realpath --relative-to="${jobBuilderOutputDir}" "$dir")"
+                jenkinsjobname=$(getJenkinsJobDir "$jobname")
+                jenkinsjobdir="${jenkinsCfg.home}/$jenkinsjobname"
                 echo "Creating / updating job \"$jobname\""
-                mkdir -p "$jobdir"
-                touch "$jobdir/${ownerStamp}"
-                cp "$file" "$jobdir/config.xml"
-                echo "$jobname" >> "$cur_decl_jobs"
+                mkdir -p "$jenkinsjobdir"
+                touch "$jenkinsjobdir/${ownerStamp}"
+                cp "$dir"/config.xml "$jenkinsjobdir/config.xml"
+                echo "$jenkinsjobname" >> "$cur_decl_jobs"
             done
 
             # Remove stale jobs
-            for file in "${jenkinsCfg.home}"/jobs/*/${ownerStamp}; do
-                test -f "$file" || continue
-                jobdir="$(dirname $file)"
-                jobname="$(basename "$jobdir")"
-                grep --quiet --line-regexp "$jobname" "$cur_decl_jobs" 2>/dev/null && continue
+            find "${jenkinsCfg.home}" -type f -name "${ownerStamp}" | while read -r f; do echo "$(dirname "$f")"; done | sort --reverse | while read -r dir; do
+                jenkinsjobname="$(realpath --relative-to="${jenkinsCfg.home}" "$dir")"
+                grep --quiet --line-regexp "$jenkinsjobname" "$cur_decl_jobs" 2>/dev/null && continue
+                jobname=$(getJobname "$jenkinsjobname")
                 echo "Deleting stale job \"$jobname\""
+                jobdir="${jenkinsCfg.home}/$jenkinsjobname"
                 rm -rf "$jobdir"
             done
           '' + (if cfg.accessUser != "" then reloadScript else "");
diff --git a/nixos/modules/services/databases/cassandra.nix b/nixos/modules/services/databases/cassandra.nix
index 90c094f68b6..820be5085de 100644
--- a/nixos/modules/services/databases/cassandra.nix
+++ b/nixos/modules/services/databases/cassandra.nix
@@ -1,74 +1,108 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
+  inherit (lib)
+    concatStringsSep
+    flip
+    literalExample
+    optionalAttrs
+    optionals
+    recursiveUpdate
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+    versionAtLeast
+    ;
+
   cfg = config.services.cassandra;
+
   defaultUser = "cassandra";
-  cassandraConfig = flip recursiveUpdate cfg.extraConfig
-    ({ commitlog_sync = "batch";
-       commitlog_sync_batch_window_in_ms = 2;
-       start_native_transport = cfg.allowClients;
-       cluster_name = cfg.clusterName;
-       partitioner = "org.apache.cassandra.dht.Murmur3Partitioner";
-       endpoint_snitch = "SimpleSnitch";
-       data_file_directories = [ "${cfg.homeDir}/data" ];
-       commitlog_directory = "${cfg.homeDir}/commitlog";
-       saved_caches_directory = "${cfg.homeDir}/saved_caches";
-     } // (lib.optionalAttrs (cfg.seedAddresses != []) {
-       seed_provider = [{
-         class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
-         parameters = [ { seeds = concatStringsSep "," cfg.seedAddresses; } ];
-       }];
-     }) // (lib.optionalAttrs (lib.versionAtLeast cfg.package.version "3") {
-       hints_directory = "${cfg.homeDir}/hints";
-     })
-    );
-  cassandraConfigWithAddresses = cassandraConfig //
-    ( if cfg.listenAddress == null
-        then { listen_interface = cfg.listenInterface; }
-        else { listen_address = cfg.listenAddress; }
-    ) // (
-      if cfg.rpcAddress == null
-        then { rpc_interface = cfg.rpcInterface; }
-        else { rpc_address = cfg.rpcAddress; }
-    );
-  cassandraEtc = pkgs.stdenv.mkDerivation
-    { name = "cassandra-etc";
-      cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
-      cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
-      cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
-      buildCommand = ''
-        mkdir -p "$out"
-
-        echo "$cassandraYaml" > "$out/cassandra.yaml"
-        ln -s "$cassandraLogbackConfig" "$out/logback.xml"
-
-        cp "$cassandraEnvPkg" "$out/cassandra-env.sh"
-
-        # Delete default JMX Port, otherwise we can't set it using env variable
-        sed -i '/JMX_PORT="7199"/d' "$out/cassandra-env.sh"
-
-        # Delete default password file
-        sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh"
-      '';
-    };
-  defaultJmxRolesFile = builtins.foldl'
-     (left: right: left + right) ""
-     (map (role: "${role.username} ${role.password}") cfg.jmxRoles);
-  fullJvmOptions = cfg.jvmOpts
-    ++ lib.optionals (cfg.jmxRoles != []) [
+
+  cassandraConfig = flip recursiveUpdate cfg.extraConfig (
+    {
+      commitlog_sync = "batch";
+      commitlog_sync_batch_window_in_ms = 2;
+      start_native_transport = cfg.allowClients;
+      cluster_name = cfg.clusterName;
+      partitioner = "org.apache.cassandra.dht.Murmur3Partitioner";
+      endpoint_snitch = "SimpleSnitch";
+      data_file_directories = [ "${cfg.homeDir}/data" ];
+      commitlog_directory = "${cfg.homeDir}/commitlog";
+      saved_caches_directory = "${cfg.homeDir}/saved_caches";
+    } // optionalAttrs (cfg.seedAddresses != [ ]) {
+      seed_provider = [
+        {
+          class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
+          parameters = [{ seeds = concatStringsSep "," cfg.seedAddresses; }];
+        }
+      ];
+    } // optionalAttrs (versionAtLeast cfg.package.version "3") {
+      hints_directory = "${cfg.homeDir}/hints";
+    }
+  );
+
+  cassandraConfigWithAddresses = cassandraConfig // (
+    if cfg.listenAddress == null
+    then { listen_interface = cfg.listenInterface; }
+    else { listen_address = cfg.listenAddress; }
+  ) // (
+    if cfg.rpcAddress == null
+    then { rpc_interface = cfg.rpcInterface; }
+    else { rpc_address = cfg.rpcAddress; }
+  );
+
+  cassandraEtc = pkgs.stdenv.mkDerivation {
+    name = "cassandra-etc";
+
+    cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
+    cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
+    cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
+
+    passAsFile = [ "extraEnvSh" ];
+    inherit (cfg) extraEnvSh;
+
+    buildCommand = ''
+      mkdir -p "$out"
+
+      echo "$cassandraYaml" > "$out/cassandra.yaml"
+      ln -s "$cassandraLogbackConfig" "$out/logback.xml"
+
+      ( cat "$cassandraEnvPkg"
+        echo "# lines from services.cassandra.extraEnvSh: "
+        cat "$extraEnvShPath"
+      ) > "$out/cassandra-env.sh"
+
+      # Delete default JMX Port, otherwise we can't set it using env variable
+      sed -i '/JMX_PORT="7199"/d' "$out/cassandra-env.sh"
+
+      # Delete default password file
+      sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh"
+    '';
+  };
+
+  defaultJmxRolesFile =
+    builtins.foldl'
+      (left: right: left + right) ""
+      (map (role: "${role.username} ${role.password}") cfg.jmxRoles);
+
+  fullJvmOptions =
+    cfg.jvmOpts
+    ++ optionals (cfg.jmxRoles != [ ]) [
       "-Dcom.sun.management.jmxremote.authenticate=true"
       "-Dcom.sun.management.jmxremote.password.file=${cfg.jmxRolesFile}"
-    ]
-    ++ lib.optionals cfg.remoteJmx [
+    ] ++ optionals cfg.remoteJmx [
       "-Djava.rmi.server.hostname=${cfg.rpcAddress}"
     ];
-in {
+
+in
+{
   options.services.cassandra = {
+
     enable = mkEnableOption ''
       Apache Cassandra – Scalable and highly available database.
     '';
+
     clusterName = mkOption {
       type = types.str;
       default = "Test Cluster";
@@ -78,16 +112,19 @@ in {
         another. All nodes in a cluster must have the same value.
       '';
     };
+
     user = mkOption {
       type = types.str;
       default = defaultUser;
       description = "Run Apache Cassandra under this user.";
     };
+
     group = mkOption {
       type = types.str;
       default = defaultUser;
       description = "Run Apache Cassandra under this group.";
     };
+
     homeDir = mkOption {
       type = types.path;
       default = "/var/lib/cassandra";
@@ -95,6 +132,7 @@ in {
         Home directory for Apache Cassandra.
       '';
     };
+
     package = mkOption {
       type = types.package;
       default = pkgs.cassandra;
@@ -104,17 +142,19 @@ in {
         The Apache Cassandra package to use.
       '';
     };
+
     jvmOpts = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       description = ''
         Populate the JVM_OPT environment variable.
       '';
     };
+
     listenAddress = mkOption {
       type = types.nullOr types.str;
       default = "127.0.0.1";
-      example = literalExample "null";
+      example = null;
       description = ''
         Address or interface to bind to and tell other Cassandra nodes
         to connect to. You _must_ change this if you want multiple
@@ -131,6 +171,7 @@ in {
         Setting listen_address to 0.0.0.0 is always wrong.
       '';
     };
+
     listenInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -141,10 +182,11 @@ in {
         supported.
       '';
     };
+
     rpcAddress = mkOption {
       type = types.nullOr types.str;
       default = "127.0.0.1";
-      example = literalExample "null";
+      example = null;
       description = ''
         The address or interface to bind the native transport server to.
 
@@ -162,6 +204,7 @@ in {
         internet. Firewall it if needed.
       '';
     };
+
     rpcInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -171,6 +214,7 @@ in {
         correspond to a single address, IP aliasing is not supported.
       '';
     };
+
     logbackConfig = mkOption {
       type = types.lines;
       default = ''
@@ -192,6 +236,7 @@ in {
         XML logback configuration for cassandra
       '';
     };
+
     seedAddresses = mkOption {
       type = types.listOf types.str;
       default = [ "127.0.0.1" ];
@@ -202,6 +247,7 @@ in {
         Set to 127.0.0.1 for a single node cluster.
       '';
     };
+
     allowClients = mkOption {
       type = types.bool;
       default = true;
@@ -214,58 +260,74 @@ in {
         <literal>extraConfig</literal>.
       '';
     };
+
     extraConfig = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       example =
-        { commitlog_sync_batch_window_in_ms = 3;
+        {
+          commitlog_sync_batch_window_in_ms = 3;
         };
       description = ''
         Extra options to be merged into cassandra.yaml as nix attribute set.
       '';
     };
+
+    extraEnvSh = mkOption {
+      type = types.lines;
+      default = "";
+      example = "CLASSPATH=$CLASSPATH:\${extraJar}";
+      description = ''
+        Extra shell lines to be appended onto cassandra-env.sh.
+      '';
+    };
+
     fullRepairInterval = mkOption {
       type = types.nullOr types.str;
       default = "3w";
-      example = literalExample "null";
+      example = null;
       description = ''
-          Set the interval how often full repairs are run, i.e.
-          <literal>nodetool repair --full</literal> is executed. See
-          https://cassandra.apache.org/doc/latest/operating/repair.html
-          for more information.
+        Set the interval how often full repairs are run, i.e.
+        <literal>nodetool repair --full</literal> is executed. See
+        https://cassandra.apache.org/doc/latest/operating/repair.html
+        for more information.
 
-          Set to <literal>null</literal> to disable full repairs.
-        '';
+        Set to <literal>null</literal> to disable full repairs.
+      '';
     };
+
     fullRepairOptions = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "--partitioner-range" ];
       description = ''
-          Options passed through to the full repair command.
-        '';
+        Options passed through to the full repair command.
+      '';
     };
+
     incrementalRepairInterval = mkOption {
       type = types.nullOr types.str;
       default = "3d";
-      example = literalExample "null";
+      example = null;
       description = ''
-          Set the interval how often incremental repairs are run, i.e.
-          <literal>nodetool repair</literal> is executed. See
-          https://cassandra.apache.org/doc/latest/operating/repair.html
-          for more information.
+        Set the interval how often incremental repairs are run, i.e.
+        <literal>nodetool repair</literal> is executed. See
+        https://cassandra.apache.org/doc/latest/operating/repair.html
+        for more information.
 
-          Set to <literal>null</literal> to disable incremental repairs.
-        '';
+        Set to <literal>null</literal> to disable incremental repairs.
+      '';
     };
+
     incrementalRepairOptions = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "--partitioner-range" ];
       description = ''
-          Options passed through to the incremental repair command.
-        '';
+        Options passed through to the incremental repair command.
+      '';
     };
+
     maxHeapSize = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -286,6 +348,7 @@ in {
         expensive GC will be (usually).
       '';
     };
+
     heapNewSize = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -309,6 +372,7 @@ in {
         100 MB per physical CPU core.
       '';
     };
+
     mallocArenaMax = mkOption {
       type = types.nullOr types.int;
       default = null;
@@ -317,6 +381,7 @@ in {
         Set this to control the amount of arenas per-thread in glibc.
       '';
     };
+
     remoteJmx = mkOption {
       type = types.bool;
       default = false;
@@ -328,6 +393,7 @@ in {
         See: https://wiki.apache.org/cassandra/JmxSecurity
       '';
     };
+
     jmxPort = mkOption {
       type = types.int;
       default = 7199;
@@ -338,8 +404,9 @@ in {
         Firewall it if needed.
       '';
     };
+
     jmxRoles = mkOption {
-      default = [];
+      default = [ ];
       description = ''
         Roles that are allowed to access the JMX (e.g. nodetool)
         BEWARE: The passwords will be stored world readable in the nix-store.
@@ -362,11 +429,13 @@ in {
         };
       });
     };
+
     jmxRolesFile = mkOption {
       type = types.nullOr types.path;
-      default = if (lib.versionAtLeast cfg.package.version "3.11")
-                then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
-                else null;
+      default =
+        if versionAtLeast cfg.package.version "3.11"
+        then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
+        else null;
       example = "/var/lib/cassandra/jmx.password";
       description = ''
         Specify your own jmx roles file.
@@ -378,102 +447,115 @@ in {
   };
 
   config = mkIf cfg.enable {
-    assertions =
-      [ { assertion = (cfg.listenAddress == null) != (cfg.listenInterface == null);
-          message = "You have to set either listenAddress or listenInterface";
-        }
-        { assertion = (cfg.rpcAddress == null) != (cfg.rpcInterface == null);
-          message = "You have to set either rpcAddress or rpcInterface";
-        }
-        { assertion = (cfg.maxHeapSize == null) == (cfg.heapNewSize == null);
-          message = "If you set either of maxHeapSize or heapNewSize you have to set both";
-        }
-        { assertion = cfg.remoteJmx -> cfg.jmxRolesFile != null;
-          message = ''
-            If you want JMX available remotely you need to set a password using
-            <literal>jmxRoles</literal> or <literal>jmxRolesFile</literal> if
-            using Cassandra older than v3.11.
-          '';
-        }
-      ];
+    assertions = [
+      {
+        assertion = (cfg.listenAddress == null) != (cfg.listenInterface == null);
+        message = "You have to set either listenAddress or listenInterface";
+      }
+      {
+        assertion = (cfg.rpcAddress == null) != (cfg.rpcInterface == null);
+        message = "You have to set either rpcAddress or rpcInterface";
+      }
+      {
+        assertion = (cfg.maxHeapSize == null) == (cfg.heapNewSize == null);
+        message = "If you set either of maxHeapSize or heapNewSize you have to set both";
+      }
+      {
+        assertion = cfg.remoteJmx -> cfg.jmxRolesFile != null;
+        message = ''
+          If you want JMX available remotely you need to set a password using
+          <literal>jmxRoles</literal> or <literal>jmxRolesFile</literal> if
+          using Cassandra older than v3.11.
+        '';
+      }
+    ];
     users = mkIf (cfg.user == defaultUser) {
-      extraUsers.${defaultUser} =
-        {  group = cfg.group;
-           home = cfg.homeDir;
-           createHome = true;
-           uid = config.ids.uids.cassandra;
-           description = "Cassandra service user";
-        };
-      extraGroups.${defaultUser}.gid = config.ids.gids.cassandra;
+      users.${defaultUser} = {
+        group = cfg.group;
+        home = cfg.homeDir;
+        createHome = true;
+        uid = config.ids.uids.cassandra;
+        description = "Cassandra service user";
+      };
+      groups.${defaultUser}.gid = config.ids.gids.cassandra;
     };
 
-    systemd.services.cassandra =
-      { description = "Apache Cassandra service";
-        after = [ "network.target" ];
-        environment =
-          { CASSANDRA_CONF = "${cassandraEtc}";
-            JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions;
-            MAX_HEAP_SIZE = toString cfg.maxHeapSize;
-            HEAP_NEWSIZE = toString cfg.heapNewSize;
-            MALLOC_ARENA_MAX = toString cfg.mallocArenaMax;
-            LOCAL_JMX = if cfg.remoteJmx then "no" else "yes";
-            JMX_PORT = toString cfg.jmxPort;
-          };
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig =
-          { User = cfg.user;
-            Group = cfg.group;
-            ExecStart = "${cfg.package}/bin/cassandra -f";
-            SuccessExitStatus = 143;
-          };
+    systemd.services.cassandra = {
+      description = "Apache Cassandra service";
+      after = [ "network.target" ];
+      environment = {
+        CASSANDRA_CONF = "${cassandraEtc}";
+        JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions;
+        MAX_HEAP_SIZE = toString cfg.maxHeapSize;
+        HEAP_NEWSIZE = toString cfg.heapNewSize;
+        MALLOC_ARENA_MAX = toString cfg.mallocArenaMax;
+        LOCAL_JMX = if cfg.remoteJmx then "no" else "yes";
+        JMX_PORT = toString cfg.jmxPort;
+      };
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/cassandra -f";
+        SuccessExitStatus = 143;
       };
+    };
 
-    systemd.services.cassandra-full-repair =
-      { description = "Perform a full repair on this Cassandra node";
-        after = [ "cassandra.service" ];
-        requires = [ "cassandra.service" ];
-        serviceConfig =
-          { User = cfg.user;
-            Group = cfg.group;
-            ExecStart =
-              lib.concatStringsSep " "
-                ([ "${cfg.package}/bin/nodetool" "repair" "--full"
-                 ] ++ cfg.fullRepairOptions);
-          };
+    systemd.services.cassandra-full-repair = {
+      description = "Perform a full repair on this Cassandra node";
+      after = [ "cassandra.service" ];
+      requires = [ "cassandra.service" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart =
+          concatStringsSep " "
+            ([
+              "${cfg.package}/bin/nodetool"
+              "repair"
+              "--full"
+            ] ++ cfg.fullRepairOptions);
       };
+    };
+
     systemd.timers.cassandra-full-repair =
       mkIf (cfg.fullRepairInterval != null) {
         description = "Schedule full repairs on Cassandra";
         wantedBy = [ "timers.target" ];
-        timerConfig =
-          { OnBootSec = cfg.fullRepairInterval;
-            OnUnitActiveSec = cfg.fullRepairInterval;
-            Persistent = true;
-          };
+        timerConfig = {
+          OnBootSec = cfg.fullRepairInterval;
+          OnUnitActiveSec = cfg.fullRepairInterval;
+          Persistent = true;
+        };
       };
 
-    systemd.services.cassandra-incremental-repair =
-      { description = "Perform an incremental repair on this cassandra node.";
-        after = [ "cassandra.service" ];
-        requires = [ "cassandra.service" ];
-        serviceConfig =
-          { User = cfg.user;
-            Group = cfg.group;
-            ExecStart =
-              lib.concatStringsSep " "
-                ([ "${cfg.package}/bin/nodetool" "repair"
-                 ] ++ cfg.incrementalRepairOptions);
-          };
+    systemd.services.cassandra-incremental-repair = {
+      description = "Perform an incremental repair on this cassandra node.";
+      after = [ "cassandra.service" ];
+      requires = [ "cassandra.service" ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart =
+          concatStringsSep " "
+            ([
+              "${cfg.package}/bin/nodetool"
+              "repair"
+            ] ++ cfg.incrementalRepairOptions);
       };
+    };
+
     systemd.timers.cassandra-incremental-repair =
       mkIf (cfg.incrementalRepairInterval != null) {
         description = "Schedule incremental repairs on Cassandra";
         wantedBy = [ "timers.target" ];
-        timerConfig =
-          { OnBootSec = cfg.incrementalRepairInterval;
-            OnUnitActiveSec = cfg.incrementalRepairInterval;
-            Persistent = true;
-          };
+        timerConfig = {
+          OnBootSec = cfg.incrementalRepairInterval;
+          OnUnitActiveSec = cfg.incrementalRepairInterval;
+          Persistent = true;
+        };
       };
   };
+
+  meta.maintainers = with lib.maintainers; [ roberth ];
 }
diff --git a/nixos/modules/services/databases/clickhouse.nix b/nixos/modules/services/databases/clickhouse.nix
index 27440fec4e1..f2f4e9d2554 100644
--- a/nixos/modules/services/databases/clickhouse.nix
+++ b/nixos/modules/services/databases/clickhouse.nix
@@ -42,6 +42,7 @@ with lib;
         User = "clickhouse";
         Group = "clickhouse";
         ConfigurationDirectory = "clickhouse-server";
+        AmbientCapabilities = "CAP_SYS_NICE";
         StateDirectory = "clickhouse";
         LogsDirectory = "clickhouse";
         ExecStart = "${pkgs.clickhouse}/bin/clickhouse-server --config-file=${pkgs.clickhouse}/etc/clickhouse-server/config.xml";
diff --git a/nixos/modules/services/databases/couchdb.nix b/nixos/modules/services/databases/couchdb.nix
index 53224db1d89..6cc29cd717e 100644
--- a/nixos/modules/services/databases/couchdb.nix
+++ b/nixos/modules/services/databases/couchdb.nix
@@ -4,19 +4,17 @@ with lib;
 
 let
   cfg = config.services.couchdb;
-  useVersion2 = strings.versionAtLeast (strings.getVersion cfg.package) "2.0";
   configFile = pkgs.writeText "couchdb.ini" (
     ''
       [couchdb]
       database_dir = ${cfg.databaseDir}
       uri_file = ${cfg.uriFile}
       view_index_dir = ${cfg.viewIndexDir}
-    '' + (if useVersion2 then
-    ''
+    '' + (optionalString (cfg.adminPass != null) ''
+      [admins]
+      ${cfg.adminUser} = ${cfg.adminPass}
+    '' + ''
       [chttpd]
-    '' else
-    ''
-      [httpd]
     '') +
     ''
       port = ${toString cfg.port}
@@ -25,8 +23,7 @@ let
       [log]
       file = ${cfg.logFile}
     '');
-  executable = if useVersion2 then "${cfg.package}/bin/couchdb"
-    else ''${cfg.package}/bin/couchdb -a ${configFile} -a ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} -a ${cfg.configFile}'';
+  executable = "${cfg.package}/bin/couchdb";
 
 in {
 
@@ -54,6 +51,23 @@ in {
         '';
       };
 
+      adminUser = mkOption {
+        type = types.str;
+        default = "admin";
+        description = ''
+          Couchdb (i.e. fauxton) account with permission for all dbs and
+          tasks.
+        '';
+      };
+
+      adminPass = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Couchdb (i.e. fauxton) account with permission for all dbs and
+          tasks.
+        '';
+      };
 
       user = mkOption {
         type = types.str;
@@ -155,8 +169,7 @@ in {
 
     environment.systemPackages = [ cfg.package ];
 
-    services.couchdb.configFile = mkDefault
-      (if useVersion2 then "/var/lib/couchdb/local.ini" else "/var/lib/couchdb/couchdb.ini");
+    services.couchdb.configFile = mkDefault "/var/lib/couchdb/local.ini";
 
     systemd.tmpfiles.rules = [
       "d '${dirOf cfg.uriFile}' - ${cfg.user} ${cfg.group} - -"
@@ -173,7 +186,7 @@ in {
         touch ${cfg.configFile}
       '';
 
-      environment = mkIf useVersion2 {
+      environment = {
         # we are actually specifying 4 configuration files:
         # 1. the preinstalled default.ini
         # 2. the module configuration
diff --git a/nixos/modules/services/databases/firebird.nix b/nixos/modules/services/databases/firebird.nix
index 95837aa1cea..0815487d4a1 100644
--- a/nixos/modules/services/databases/firebird.nix
+++ b/nixos/modules/services/databases/firebird.nix
@@ -43,22 +43,21 @@ in
       enable = mkEnableOption "the Firebird super server";
 
       package = mkOption {
-        default = pkgs.firebirdSuper;
-        defaultText = "pkgs.firebirdSuper";
+        default = pkgs.firebird;
+        defaultText = "pkgs.firebird";
         type = types.package;
-        /*
-          Example: <code>package = pkgs.firebirdSuper.override { icu =
-            pkgs.icu; };</code> which is not recommended for compatibility
-            reasons. See comments at the firebirdSuper derivation
-        */
-
+        example = ''
+          <code>package = pkgs.firebird_3;</code>
+        '';
         description = ''
-          Which firebird derivation to use.
+          Which Firebird package to be installed: <code>pkgs.firebird_3</code>
+          For SuperServer use override: <code>pkgs.firebird_3.override { superServer = true; };</code>
         '';
       };
 
       port = mkOption {
         default = "3050";
+        type = types.port;
         description = ''
           Port Firebird uses.
         '';
@@ -66,13 +65,15 @@ in
 
       user = mkOption {
         default = "firebird";
+        type = types.str;
         description = ''
           User account under which firebird runs.
         '';
       };
 
       baseDir = mkOption {
-        default = "/var/db/firebird"; # ubuntu is using /var/lib/firebird/2.1/data/.. ?
+        default = "/var/lib/firebird";
+        type = types.str;
         description = ''
           Location containing data/ and system/ directories.
           data/ stores the databases, system/ stores the password database security2.fdb.
@@ -108,13 +109,21 @@ in
                 cp ${firebird}/security2.fdb "${systemDir}"
             fi
 
+            if ! test -e "${systemDir}/security3.fdb"; then
+                cp ${firebird}/security3.fdb "${systemDir}"
+            fi
+
+            if ! test -e "${systemDir}/security4.fdb"; then
+                cp ${firebird}/security4.fdb "${systemDir}"
+            fi
+
             chmod -R 700         "${dataDir}" "${systemDir}" /var/log/firebird
           '';
 
         serviceConfig.User = cfg.user;
         serviceConfig.LogsDirectory = "firebird";
         serviceConfig.LogsDirectoryMode = "0700";
-        serviceConfig.ExecStart = ''${firebird}/bin/fbserver -d'';
+        serviceConfig.ExecStart = "${firebird}/bin/fbserver -d";
 
         # TODO think about shutdown
       };
diff --git a/nixos/modules/services/databases/foundationdb.nix b/nixos/modules/services/databases/foundationdb.nix
index 18727acc7c7..e22127403e9 100644
--- a/nixos/modules/services/databases/foundationdb.nix
+++ b/nixos/modules/services/databases/foundationdb.nix
@@ -233,7 +233,7 @@ in
             type = types.str;
             default = "Check.Valid=1,Check.Unexpired=1";
             description = ''
-	      "Peer verification string". This may be used to adjust which TLS
+              "Peer verification string". This may be used to adjust which TLS
               client certificates a server will accept, as a form of user
               authorization; for example, it may only accept TLS clients who
               offer a certificate abiding by some locality or organization name.
diff --git a/nixos/modules/services/databases/memcached.nix b/nixos/modules/services/databases/memcached.nix
index f54bb6cc9b1..ca7b20eb049 100644
--- a/nixos/modules/services/databases/memcached.nix
+++ b/nixos/modules/services/databases/memcached.nix
@@ -17,39 +17,44 @@ in
   options = {
 
     services.memcached = {
-
       enable = mkEnableOption "Memcached";
 
       user = mkOption {
+        type = types.str;
         default = "memcached";
         description = "The user to run Memcached as";
       };
 
       listen = mkOption {
+        type = types.str;
         default = "127.0.0.1";
-        description = "The IP address to bind to";
+        description = "The IP address to bind to.";
       };
 
       port = mkOption {
+        type = types.port;
         default = 11211;
-        description = "The port to bind to";
+        description = "The port to bind to.";
       };
 
       enableUnixSocket = mkEnableOption "unix socket at /run/memcached/memcached.sock";
 
       maxMemory = mkOption {
+        type = types.ints.unsigned;
         default = 64;
         description = "The maximum amount of memory to use for storage, in megabytes.";
       };
 
       maxConnections = mkOption {
+        type = types.ints.unsigned;
         default = 1024;
-        description = "The maximum number of simultaneous connections";
+        description = "The maximum number of simultaneous connections.";
       };
 
       extraOptions = mkOption {
+        type = types.listOf types.str;
         default = [];
-        description = "A list of extra options that will be added as a suffix when running memcached";
+        description = "A list of extra options that will be added as a suffix when running memcached.";
       };
     };
 
diff --git a/nixos/modules/services/databases/mongodb.nix b/nixos/modules/services/databases/mongodb.nix
index 4453a182990..db1e5fedf50 100644
--- a/nixos/modules/services/databases/mongodb.nix
+++ b/nixos/modules/services/databases/mongodb.nix
@@ -41,16 +41,19 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "mongodb";
         description = "User account under which MongoDB runs";
       };
 
       bind_ip = mkOption {
+        type = types.str;
         default = "127.0.0.1";
         description = "IP to bind to";
       };
 
       quiet = mkOption {
+        type = types.bool;
         default = false;
         description = "quieter output";
       };
@@ -68,16 +71,19 @@ in
       };
 
       dbpath = mkOption {
+        type = types.str;
         default = "/var/db/mongodb";
         description = "Location where MongoDB stores its files";
       };
 
       pidFile = mkOption {
+        type = types.str;
         default = "/run/mongodb.pid";
         description = "Location of MongoDB pid file";
       };
 
       replSetName = mkOption {
+        type = types.str;
         default = "";
         description = ''
           If this instance is part of a replica set, set its name here.
@@ -86,6 +92,7 @@ in
       };
 
       extraConfig = mkOption {
+        type = types.lines;
         default = "";
         example = ''
           storage.journal.enabled: false
@@ -176,7 +183,7 @@ in
         postStart = ''
             if test -e "${cfg.dbpath}/.first_startup"; then
               ${optionalString (cfg.initialScript != null) ''
-                ${mongodb}/bin/mongo -u root -p ${cfg.initialRootPassword} admin "${cfg.initialScript}"
+                ${mongodb}/bin/mongo ${optionalString (cfg.enableAuth) "-u root -p ${cfg.initialRootPassword}"} admin "${cfg.initialScript}"
               ''}
               rm -f "${cfg.dbpath}/.first_startup"
             fi
diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix
index 7d0a3f9afc4..b801b5cce63 100644
--- a/nixos/modules/services/databases/mysql.nix
+++ b/nixos/modules/services/databases/mysql.nix
@@ -34,7 +34,7 @@ in
 
       package = mkOption {
         type = types.package;
-        example = literalExample "pkgs.mysql";
+        example = literalExample "pkgs.mariadb";
         description = "
           Which MySQL derivation to use. MariaDB packages are supported too.
         ";
@@ -48,7 +48,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 3306;
         description = "Port of MySQL.";
       };
@@ -375,6 +375,18 @@ in
           fi
         '';
 
+        script = ''
+          # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
+          if test -n "''${_WSREP_START_POSITION}"; then
+            if test -e "${cfg.package}/bin/galera_recovery"; then
+              VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
+            fi
+          fi
+
+          # The last two environment variables are used for starting Galera clusters
+          exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
+        '';
+
         postStart = let
           # The super user account to use on *first* run of MySQL server
           superUser = if isMariaDB then cfg.user else "root";
@@ -481,8 +493,7 @@ in
           Type = if hasNotify then "notify" else "simple";
           Restart = "on-abort";
           RestartSec = "5s";
-          # The last two environment variables are used for starting Galera clusters
-          ExecStart = "${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
+
           # User and group
           User = cfg.user;
           Group = cfg.group;
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
index 09b453e7584..53760bb24c4 100644
--- a/nixos/modules/services/databases/neo4j.nix
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -16,14 +16,14 @@ let
       ''}
       dbms.ssl.policy.${name}.client_auth=${conf.clientAuth}
       ${if length (splitString "/" conf.privateKey) > 1 then
-        ''dbms.ssl.policy.${name}.private_key=${conf.privateKey}''
+        "dbms.ssl.policy.${name}.private_key=${conf.privateKey}"
       else
-        ''dbms.ssl.policy.${name}.private_key=${conf.baseDirectory}/${conf.privateKey}''
+        "dbms.ssl.policy.${name}.private_key=${conf.baseDirectory}/${conf.privateKey}"
       }
       ${if length (splitString "/" conf.privateKey) > 1 then
-        ''dbms.ssl.policy.${name}.public_certificate=${conf.publicCertificate}''
+        "dbms.ssl.policy.${name}.public_certificate=${conf.publicCertificate}"
       else
-        ''dbms.ssl.policy.${name}.public_certificate=${conf.baseDirectory}/${conf.publicCertificate}''
+        "dbms.ssl.policy.${name}.public_certificate=${conf.baseDirectory}/${conf.publicCertificate}"
       }
       dbms.ssl.policy.${name}.revoked_dir=${conf.revokedDir}
       dbms.ssl.policy.${name}.tls_versions=${concatStringsSep "," conf.tlsVersions}
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 7472538b887..f0efc659cff 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -1,43 +1,121 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.openldap;
+  legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ];
   openldap = cfg.package;
-
-  dataFile = pkgs.writeText "ldap-contents.ldif" cfg.declarativeContents;
-  configFile = pkgs.writeText "slapd.conf" ((optionalString cfg.defaultSchemas ''
-    include ${openldap.out}/etc/schema/core.schema
-    include ${openldap.out}/etc/schema/cosine.schema
-    include ${openldap.out}/etc/schema/inetorgperson.schema
-    include ${openldap.out}/etc/schema/nis.schema
-  '') + ''
-    ${cfg.extraConfig}
-    database ${cfg.database}
-    suffix ${cfg.suffix}
-    rootdn ${cfg.rootdn}
-    ${if (cfg.rootpw != null) then ''
-      rootpw ${cfg.rootpw}
-    '' else ''
-      include ${cfg.rootpwFile}
-    ''}
-    directory ${cfg.dataDir}
-    ${cfg.extraDatabaseConfig}
-  '');
-  configOpts = if cfg.configDir == null then "-f ${configFile}"
-               else "-F ${cfg.configDir}";
-in
-
-{
-
-  ###### interface
-
+  configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
+
+  ldapValueType = let
+    # Can't do types.either with multiple non-overlapping submodules, so define our own
+    singleLdapValueType = lib.mkOptionType rec {
+      name = "LDAP";
+      description = "LDAP value";
+      check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
+      merge = lib.mergeEqualOption;
+    };
+    # We don't coerce to lists of single values, as some values must be unique
+  in types.either singleLdapValueType (types.listOf singleLdapValueType);
+
+  ldapAttrsType =
+    let
+      options = {
+        attrs = mkOption {
+          type = types.attrsOf ldapValueType;
+          default = {};
+          description = "Attributes of the parent entry.";
+        };
+        children = mkOption {
+          # Hide the child attributes, to avoid infinite recursion in e.g. documentation
+          # Actual Nix evaluation is lazy, so this is not an issue there
+          type = let
+            hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
+          in types.attrsOf (types.submodule { options = hiddenOptions; });
+          default = {};
+          description = "Child entries of the current entry, with recursively the same structure.";
+          example = lib.literalExample ''
+            {
+                "cn=schema" = {
+                # The attribute used in the DN must be defined
+                attrs = { cn = "schema"; };
+                children = {
+                    # This entry's DN is expanded to "cn=foo,cn=schema"
+                    "cn=foo" = { ... };
+                };
+                # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
+                includes = [ ... ];
+                };
+            }
+          '';
+        };
+        includes = mkOption {
+          type = types.listOf types.path;
+          default = [];
+          description = ''
+            LDIF files to include after the parent's attributes but before its children.
+          '';
+        };
+      };
+    in types.submodule { inherit options; };
+
+  valueToLdif = attr: values: let
+    listValues = if lib.isList values then values else lib.singleton values;
+  in map (value:
+    if lib.isAttrs value then
+      if lib.hasAttr "path" value
+      then "${attr}:< file://${value.path}"
+      else "${attr}:: ${value.base64}"
+    else "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
+  ) listValues;
+
+  attrsToLdif = dn: { attrs, children, includes, ... }: [''
+    dn: ${dn}
+    ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
+  ''] ++ (map (path: "include: file://${path}\n") includes) ++ (
+    lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
+  );
+in {
+  imports = let
+    deprecationNote = "This option is removed due to the deprecation of `slapd.conf` upstream. Please migrate to `services.openldap.settings`, see the release notes for advice with this process.";
+    mkDatabaseOption = old: new:
+      lib.mkChangedOptionModule [ "services" "openldap" old ] [ "services" "openldap" "settings" "children" ]
+        (config: let
+          database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
+          value = lib.getAttrFromPath [ "services" "openldap" old ] config;
+        in lib.setAttrByPath ([ "olcDatabase={1}${database}" "attrs" ] ++ new) value);
+  in [
+    (lib.mkRemovedOptionModule [ "services" "openldap" "extraConfig" ] deprecationNote)
+    (lib.mkRemovedOptionModule [ "services" "openldap" "extraDatabaseConfig" ] deprecationNote)
+
+    (lib.mkChangedOptionModule [ "services" "openldap" "logLevel" ] [ "services" "openldap" "settings" "attrs" "olcLogLevel" ]
+      (config: lib.splitString " " (lib.getAttrFromPath [ "services" "openldap" "logLevel" ] config)))
+    (lib.mkChangedOptionModule [ "services" "openldap" "defaultSchemas" ] [ "services" "openldap" "settings" "children" "cn=schema" "includes"]
+      (config: lib.optionals (lib.getAttrFromPath [ "services" "openldap" "defaultSchemas" ] config) (
+        map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ])))
+
+    (lib.mkChangedOptionModule [ "services" "openldap" "database" ] [ "services" "openldap" "settings" "children" ]
+      (config: let
+        database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
+      in {
+        "olcDatabase={1}${database}".attrs = {
+          # objectClass is case-insensitive, so don't need to capitalize ${database}
+          objectClass = [ "olcdatabaseconfig" "olc${database}config" ];
+          olcDatabase = "{1}${database}";
+          olcDbDirectory = lib.mkDefault "/var/db/openldap";
+        };
+        "cn=schema".includes = lib.mkDefault (
+          map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ]
+        );
+      }))
+    (mkDatabaseOption "rootpwFile" [ "olcRootPW" "path" ])
+    (mkDatabaseOption "suffix" [ "olcSuffix" ])
+    (mkDatabaseOption "dataDir" [ "olcDbDirectory" ])
+    (mkDatabaseOption "rootdn" [ "olcRootDN" ])
+    (mkDatabaseOption "rootpw" [ "olcRootPW" ])
+  ];
   options = {
-
     services.openldap = {
-
       enable = mkOption {
         type = types.bool;
         default = false;
@@ -77,224 +155,170 @@ in
         example = [ "ldaps:///" ];
       };
 
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/db/openldap";
-        description = "The database directory.";
-      };
-
-      defaultSchemas = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Include the default schemas core, cosine, inetorgperson and nis.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      database = mkOption {
-        type = types.str;
-        default = "mdb";
-        description = ''
-          Database type to use for the LDAP.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      suffix = mkOption {
-        type = types.str;
-        example = "dc=example,dc=org";
-        description = ''
-          Specify the DN suffix of queries that will be passed to this backend
-          database.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      rootdn = mkOption {
-        type = types.str;
-        example = "cn=admin,dc=example,dc=org";
-        description = ''
-          Specify the distinguished name that is not subject to access control
-          or administrative limit restrictions for operations on this database.
-          This setting will be ignored if configDir is set.
-        '';
-      };
-
-      rootpw = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Password for the root user.
-          This setting will be ignored if configDir is set.
-          Using this option will store the root password in plain text in the
-          world-readable nix store. To avoid this the <literal>rootpwFile</literal> can be used.
+      settings = mkOption {
+        type = ldapAttrsType;
+        description = "Configuration for OpenLDAP, in OLC format";
+        example = lib.literalExample ''
+          {
+            attrs.olcLogLevel = [ "stats" ];
+            children = {
+              "cn=schema".includes = [
+                 "\${pkgs.openldap}/etc/schema/core.ldif"
+                 "\${pkgs.openldap}/etc/schema/cosine.ldif"
+                 "\${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+              ];
+              "olcDatabase={-1}frontend" = {
+                attrs = {
+                  objectClass = "olcDatabaseConfig";
+                  olcDatabase = "{-1}frontend";
+                  olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
+                };
+              };
+              "olcDatabase={0}config" = {
+                attrs = {
+                  objectClass = "olcDatabaseConfig";
+                  olcDatabase = "{0}config";
+                  olcAccess = [ "{0}to * by * none break" ];
+                };
+              };
+              "olcDatabase={1}mdb" = {
+                attrs = {
+                  objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+                  olcDatabase = "{1}mdb";
+                  olcDbDirectory = "/var/db/ldap";
+                  olcDbIndex = [
+                    "objectClass eq"
+                    "cn pres,eq"
+                    "uid pres,eq"
+                    "sn pres,eq,subany"
+                  ];
+                  olcSuffix = "dc=example,dc=com";
+                  olcAccess = [ "{0}to * by * read break" ];
+                };
+              };
+            };
+          };
         '';
       };
 
-      rootpwFile = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = ''
-          Password file for the root user.
-          The file should contain the string <literal>rootpw</literal> followed by the password.
-          e.g.: <literal>rootpw mysecurepassword</literal>
-        '';
-      };
-
-      logLevel = mkOption {
-        type = types.str;
-        default = "0";
-        example = "acl trace";
-        description = "The log level selector of slapd.";
-      };
-
+      # This option overrides settings
       configDir = mkOption {
         type = types.nullOr types.path;
         default = null;
-        description = "Use this optional config directory instead of using slapd.conf";
+        description = ''
+          Use this config directory instead of generating one from the
+          <literal>settings</literal> option. Overrides all NixOS settings. If
+          you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
+        '';
         example = "/var/db/slapd.d";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "
-          slapd.conf configuration
-        ";
-        example = literalExample ''
-            '''
-            include ${openldap.out}/etc/schema/core.schema
-            include ${openldap.out}/etc/schema/cosine.schema
-            include ${openldap.out}/etc/schema/inetorgperson.schema
-            include ${openldap.out}/etc/schema/nis.schema
-
-            database bdb
-            suffix dc=example,dc=org
-            rootdn cn=admin,dc=example,dc=org
-            # NOTE: change after first start
-            rootpw secret
-            directory /var/db/openldap
-            '''
-          '';
-      };
-
       declarativeContents = mkOption {
-        type = with types; nullOr lines;
-        default = null;
+        type = with types; attrsOf lines;
+        default = {};
         description = ''
-          Declarative contents for the LDAP database, in LDIF format.
+          Declarative contents for the LDAP database, in LDIF format by suffix.
 
-          Note a few facts when using it. First, the database
-          <emphasis>must</emphasis> be stored in the directory defined by
-          <code>dataDir</code>. Second, all <code>dataDir</code> will be erased
-          when starting the LDAP server. Third, modifications to the database
-          are not prevented, they are just dropped on the next reboot of the
-          server. Finally, performance-wise the database and indexes are rebuilt
-          on each server startup, so this will slow down server startup,
+          All data will be erased when starting the LDAP server. Modifications
+          to the database are not prevented, they are just dropped on the next
+          reboot of the server. Performance-wise the database and indexes are
+          rebuilt on each server startup, so this will slow down server startup,
           especially with large databases.
         '';
-        example = ''
-          dn: dc=example,dc=org
-          objectClass: domain
-          dc: example
-
-          dn: ou=users,dc=example,dc=org
-          objectClass = organizationalUnit
-          ou: users
-
-          # ...
+        example = lib.literalExample ''
+          {
+            "dc=example,dc=org" = '''
+              dn= dn: dc=example,dc=org
+              objectClass: domain
+              dc: example
+
+              dn: ou=users,dc=example,dc=org
+              objectClass = organizationalUnit
+              ou: users
+
+              # ...
+            ''';
+          }
         '';
       };
-
-      extraDatabaseConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          slapd.conf configuration after the database option.
-          This setting will be ignored if configDir is set.
-        '';
-        example = ''
-          # Indices to maintain for this directory
-          # unique id so equality match only
-          index uid eq
-          # allows general searching on commonname, givenname and email
-          index cn,gn,mail eq,sub
-          # allows multiple variants on surname searching
-          index sn eq,sub
-          # sub above includes subintial,subany,subfinal
-          # optimise department searches
-          index ou eq
-          # if searches will include objectClass uncomment following
-          # index objectClass eq
-          # shows use of default index parameter
-          index default eq,sub
-          # indices missing - uses default eq,sub
-          index telephonenumber
-
-          # other database parameters
-          # read more in slapd.conf reference section
-          cachesize 10000
-          checkpoint 128 15
-        '';
-      };
-
     };
-
-  };
-
-  meta = {
-    maintainers = [ lib.maintainers.mic92 ];
   };
 
-
-  ###### implementation
+  meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
 
   config = mkIf cfg.enable {
-    assertions = [
-      {
-        assertion = cfg.configDir != null || cfg.rootpwFile != null || cfg.rootpw != null;
-        message = "services.openldap: Unless configDir is set, either rootpw or rootpwFile must be set";
-      }
-    ];
-
+    assertions = map (opt: {
+      assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule");
+      message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)";
+    }) legacyOptions;
     environment.systemPackages = [ openldap ];
 
+    # Literal attributes must always be set
+    services.openldap.settings = {
+      attrs = {
+        objectClass = "olcGlobal";
+        cn = "config";
+        olcPidFile = "/run/slapd/slapd.pid";
+      };
+      children."cn=schema".attrs = {
+        cn = "schema";
+        objectClass = "olcSchemaConfig";
+      };
+    };
+
     systemd.services.openldap = {
       description = "LDAP server";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      preStart = ''
+      preStart = let
+        settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
+
+        dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
+        dataDirs = lib.mapAttrs' (name: value: lib.nameValuePair value.attrs.olcSuffix value.attrs.olcDbDirectory)
+          (lib.filterAttrs (_: value: value.attrs ? olcDbDirectory) dbSettings);
+        dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
+        mkLoadScript = dn: let
+          dataDir = lib.escapeShellArg (getAttr dn dataDirs);
+        in  ''
+          rm -rf ${dataDir}/*
+          ${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles}
+          chown -R "${cfg.user}:${cfg.group}" ${dataDir}
+        '';
+      in ''
         mkdir -p /run/slapd
         chown -R "${cfg.user}:${cfg.group}" /run/slapd
-        ${optionalString (cfg.declarativeContents != null) ''
-          rm -Rf "${cfg.dataDir}"
-        ''}
-        mkdir -p "${cfg.dataDir}"
-        ${optionalString (cfg.declarativeContents != null) ''
-          ${openldap.out}/bin/slapadd ${configOpts} -l ${dataFile}
-        ''}
-        chown -R "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
 
-        ${openldap}/bin/slaptest ${configOpts}
+        mkdir -p ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
+        chown "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
+
+        ${lib.optionalString (cfg.configDir == null) (''
+          rm -Rf ${configDir}/*
+          ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
+        '')}
+        chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir}
+
+        ${lib.concatStrings (map mkLoadScript (lib.attrNames cfg.declarativeContents))}
+        ${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
       '';
-      serviceConfig.ExecStart =
-        "${openldap.out}/libexec/slapd -d '${cfg.logLevel}' " +
-          "-u '${cfg.user}' -g '${cfg.group}' " +
-          "-h '${concatStringsSep " " cfg.urlList}' " +
-          "${configOpts}";
+      serviceConfig = {
+        ExecStart = lib.escapeShellArgs ([
+          "${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
+          "-h" (lib.concatStringsSep " " cfg.urlList)
+        ]);
+        Type = "forking";
+        PIDFile = cfg.settings.attrs.olcPidFile;
+      };
     };
 
-    users.users.openldap =
-      { name = cfg.user;
+    users.users = lib.optionalAttrs (cfg.user == "openldap") {
+      openldap = {
         group = cfg.group;
-        uid = config.ids.uids.openldap;
-      };
-
-    users.groups.openldap =
-      { name = cfg.group;
-        gid = config.ids.gids.openldap;
+        isSystemUser = true;
       };
+    };
 
+    users.groups = lib.optionalAttrs (cfg.group == "openldap") {
+      openldap = {};
+    };
   };
 }
diff --git a/nixos/modules/services/databases/pgmanage.nix b/nixos/modules/services/databases/pgmanage.nix
index 0f8634dab31..8508e76b5cd 100644
--- a/nixos/modules/services/databases/pgmanage.nix
+++ b/nixos/modules/services/databases/pgmanage.nix
@@ -197,6 +197,7 @@ in {
         group = pgmanage;
         home  = cfg.sqlRoot;
         createHome = true;
+        isSystemUser = true;
       };
       groups.${pgmanage} = {
         name = pgmanage;
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index c726a08e34f..fd4a195787f 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -11,23 +11,28 @@ let
       then cfg.package
       else cfg.package.withPackages (_: cfg.extraPlugins);
 
+  toStr = value:
+    if true == value then "yes"
+    else if false == value then "no"
+    else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'"
+    else toString value;
+
   # The main PostgreSQL configuration file.
-  configFile = pkgs.writeText "postgresql.conf"
-    ''
-      hba_file = '${pkgs.writeText "pg_hba.conf" cfg.authentication}'
-      ident_file = '${pkgs.writeText "pg_ident.conf" cfg.identMap}'
-      log_destination = 'stderr'
-      log_line_prefix = '${cfg.logLinePrefix}'
-      listen_addresses = '${if cfg.enableTCPIP then "*" else "localhost"}'
-      port = ${toString cfg.port}
-      ${cfg.extraConfig}
-    '';
+  configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") cfg.settings));
+
+  configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} ''
+    ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null
+    touch $out
+  '';
 
   groupAccessAvailable = versionAtLeast postgresql.version "11.0";
 
 in
 
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.")
+  ];
 
   ###### interface
 
@@ -53,6 +58,12 @@ in
         '';
       };
 
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Check the syntax of the configuration file at compile time";
+      };
+
       dataDir = mkOption {
         type = types.path;
         defaultText = "/var/lib/postgresql/\${config.services.postgresql.package.psqlSchema}";
@@ -69,11 +80,16 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Defines how users authenticate themselves to the server. By
-          default, "trust" access to local users will always be granted
-          along with any other custom options. If you do not want this,
-          set this option using "lib.mkForce" to override this
-          behaviour.
+          Defines how users authenticate themselves to the server. See the
+          <link xlink:href="https://www.postgresql.org/docs/current/auth-pg-hba-conf.html">
+          PostgreSQL documentation for pg_hba.conf</link>
+          for details on the expected format of this option. By default,
+          peer based authentication will be used for users connecting
+          via the Unix socket, and md5 password authentication will be
+          used for users connecting via TCP. Any added rules will be
+          inserted above the default rules. If you'd like to replace the
+          default rules entirely, you can use <function>lib.mkForce</function> in your
+          module.
         '';
       };
 
@@ -143,11 +159,11 @@ in
                 For more information on how to specify the target
                 and on which privileges exist, see the
                 <link xlink:href="https://www.postgresql.org/docs/current/sql-grant.html">GRANT syntax</link>.
-                The attributes are used as <code>GRANT ''${attrName} ON ''${attrValue}</code>.
+                The attributes are used as <code>GRANT ''${attrValue} ON ''${attrName}</code>.
               '';
               example = literalExample ''
                 {
-                  "DATABASE nextcloud" = "ALL PRIVILEGES";
+                  "DATABASE \"nextcloud\"" = "ALL PRIVILEGES";
                   "ALL TABLES IN SCHEMA public" = "ALL PRIVILEGES";
                 }
               '';
@@ -212,10 +228,28 @@ in
         '';
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "Additional text to be appended to <filename>postgresql.conf</filename>.";
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool float int str ]);
+        default = {};
+        description = ''
+          PostgreSQL configuration. Refer to
+          <link xlink:href="https://www.postgresql.org/docs/11/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE"/>
+          for an overview of <literal>postgresql.conf</literal>.
+
+          <note><para>
+            String values will automatically be enclosed in single quotes. Single quotes will be
+            escaped with two single quotes as described by the upstream documentation linked above.
+          </para></note>
+        '';
+        example = literalExample ''
+          {
+            log_connections = true;
+            log_statement = "all";
+            logging_collector = true
+            log_disconnections = true
+            log_destination = lib.mkForce "syslog";
+          }
+        '';
       };
 
       recoveryConfig = mkOption {
@@ -245,14 +279,24 @@ in
 
   config = mkIf cfg.enable {
 
+    services.postgresql.settings =
+      {
+        hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
+        ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
+        log_destination = "stderr";
+        log_line_prefix = cfg.logLinePrefix;
+        listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
+        port = cfg.port;
+      };
+
     services.postgresql.package =
       # Note: when changing the default, make it conditional on
       # ‘system.stateVersion’ to maintain compatibility with existing
       # systems!
-      mkDefault (if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
+      mkDefault (if versionAtLeast config.system.stateVersion "21.11" then pkgs.postgresql_13
+            else if versionAtLeast config.system.stateVersion "20.03" then pkgs.postgresql_11
             else if versionAtLeast config.system.stateVersion "17.09" then pkgs.postgresql_9_6
-            else if versionAtLeast config.system.stateVersion "16.03" then pkgs.postgresql_9_5
-            else throw "postgresql_9_4 was removed, please upgrade your postgresql version.");
+            else throw "postgresql_9_5 was removed, please upgrade your postgresql version.");
 
     services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
 
@@ -281,6 +325,8 @@ in
      "/share/postgresql"
     ];
 
+    system.extraDependencies = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
+
     systemd.services.postgresql =
       { description = "PostgreSQL Server";
 
@@ -304,7 +350,7 @@ in
               touch "${cfg.dataDir}/.first_startup"
             fi
 
-            ln -sfn "${configFile}" "${cfg.dataDir}/postgresql.conf"
+            ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
             ${optionalString (cfg.recoveryConfig != null) ''
               ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
                 "${cfg.dataDir}/recovery.conf"
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index f1777854e14..9c0740f28c9 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -4,39 +4,27 @@ with lib;
 
 let
   cfg = config.services.redis;
-  redisBool = b: if b then "yes" else "no";
-  condOption = name: value: if value != null then "${name} ${toString value}" else "";
-
-  redisConfig = pkgs.writeText "redis.conf" ''
-    port ${toString cfg.port}
-    ${condOption "bind" cfg.bind}
-    ${condOption "unixsocket" cfg.unixSocket}
-    daemonize no
-    supervised systemd
-    loglevel ${cfg.logLevel}
-    logfile ${cfg.logfile}
-    syslog-enabled ${redisBool cfg.syslog}
-    databases ${toString cfg.databases}
-    ${concatMapStrings (d: "save ${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}\n") cfg.save}
-    dbfilename dump.rdb
-    dir /var/lib/redis
-    ${if cfg.slaveOf != null then "slaveof ${cfg.slaveOf.ip} ${toString cfg.slaveOf.port}" else ""}
-    ${condOption "masterauth" cfg.masterAuth}
-    ${condOption "requirepass" cfg.requirePass}
-    appendOnly ${redisBool cfg.appendOnly}
-    appendfsync ${cfg.appendFsync}
-    slowlog-log-slower-than ${toString cfg.slowLogLogSlowerThan}
-    slowlog-max-len ${toString cfg.slowLogMaxLen}
-    ${cfg.extraConfig}
-  '';
-in
-{
+
+  ulimitNofile = cfg.maxclients + 32;
+
+  mkValueString = value:
+    if value == true then "yes"
+    else if value == false then "no"
+    else generators.mkValueStringDefault { } value;
+
+  redisConfig = pkgs.writeText "redis.conf" (generators.toKeyValue {
+    listsAsDuplicateKeys = true;
+    mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
+  } cfg.settings);
+
+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.")
+    (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.settings instead.")
   ];
 
   ###### interface
@@ -64,7 +52,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 6379;
         description = "The port for Redis to listen to.";
       };
@@ -87,9 +75,12 @@ in
 
       bind = mkOption {
         type = with types; nullOr str;
-        default = null; # All interfaces
-        description = "The IP interface to bind to.";
-        example = "127.0.0.1";
+        default = "127.0.0.1";
+        description = ''
+          The IP interface to bind to.
+          <literal>null</literal> means "all interfaces".
+        '';
+        example = "192.0.2.1";
       };
 
       unixSocket = mkOption {
@@ -99,6 +90,13 @@ in
         example = "/run/redis/redis.sock";
       };
 
+      unixSocketPerm = mkOption {
+        type = types.int;
+        default = 750;
+        description = "Change permissions for the socket";
+        example = 700;
+      };
+
       logLevel = mkOption {
         type = types.str;
         default = "notice"; # debug, verbose, notice, warning
@@ -125,6 +123,12 @@ in
         description = "Set the number of databases.";
       };
 
+      maxclients = mkOption {
+        type = types.int;
+        default = 10000;
+        description = "Set the max number of connected clients at the same time.";
+      };
+
       save = mkOption {
         type = with types; listOf (listOf int);
         default = [ [900 1] [300 10] [60 10000] ];
@@ -133,12 +137,29 @@ in
       };
 
       slaveOf = mkOption {
-        default = null; # { ip, port }
-        description = "An attribute set with two attributes: ip and port to which this redis instance acts as a slave.";
+        type = with types; nullOr (submodule ({ ... }: {
+          options = {
+            ip = mkOption {
+              type = str;
+              description = "IP of the Redis master";
+              example = "192.168.1.100";
+            };
+
+            port = mkOption {
+              type = port;
+              description = "port of the Redis master";
+              default = 6379;
+            };
+          };
+        }));
+
+        default = null;
+        description = "IP and port to which this redis instance acts as a slave.";
         example = { ip = "192.168.1.100"; port = 6379; };
       };
 
       masterAuth = mkOption {
+        type = with types; nullOr str;
         default = null;
         description = ''If the master is password protected (using the requirePass configuration)
         it is possible to tell the slave to authenticate before starting the replication synchronization
@@ -188,10 +209,19 @@ in
         description = "Maximum number of items to keep in slow log.";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "Extra configuration options for redis.conf.";
+      settings = mkOption {
+        type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+        default = {};
+        description = ''
+          Redis configuration. Refer to
+          <link xlink:href="https://redis.io/topics/config"/>
+          for details on supported values.
+        '';
+        example = literalExample ''
+          {
+            loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ];
+          }
+        '';
       };
     };
 
@@ -222,6 +252,31 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
+    services.redis.settings = mkMerge [
+      {
+        port = cfg.port;
+        daemonize = false;
+        supervised = "systemd";
+        loglevel = cfg.logLevel;
+        logfile = cfg.logfile;
+        syslog-enabled = cfg.syslog;
+        databases = cfg.databases;
+        maxclients = cfg.maxclients;
+        save = map (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") cfg.save;
+        dbfilename = "dump.rdb";
+        dir = "/var/lib/redis";
+        appendOnly = cfg.appendOnly;
+        appendfsync = cfg.appendFsync;
+        slowlog-log-slower-than = cfg.slowLogLogSlowerThan;
+        slowlog-max-len = cfg.slowLogMaxLen;
+      }
+      (mkIf (cfg.bind != null) { bind = cfg.bind; })
+      (mkIf (cfg.unixSocket != null) { unixsocket = cfg.unixSocket; unixsocketperm = "${toString cfg.unixSocketPerm}"; })
+      (mkIf (cfg.slaveOf != null) { slaveof = "${cfg.slaveOf.ip} ${cfg.slaveOf.port}"; })
+      (mkIf (cfg.masterAuth != null) { masterauth = cfg.masterAuth; })
+      (mkIf (cfg.requirePass != null) { requirepass = cfg.requirePass; })
+    ];
+
     systemd.services.redis = {
       description = "Redis Server";
 
@@ -237,11 +292,46 @@ in
 
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/redis-server /run/redis/redis.conf";
-        RuntimeDirectory = "redis";
-        StateDirectory = "redis";
         Type = "notify";
+        # User and group
         User = "redis";
         Group = "redis";
+        # Runtime directory and mode
+        RuntimeDirectory = "redis";
+        RuntimeDirectoryMode = "0750";
+        # State directory and mode
+        StateDirectory = "redis";
+        StateDirectoryMode = "0700";
+        # Access write directories
+        UMask = "0077";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Process Properties
+        LimitNOFILE = "${toString ulimitNofile}";
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid";
       };
     };
   };
diff --git a/nixos/modules/services/databases/riak-cs.nix b/nixos/modules/services/databases/riak-cs.nix
deleted file mode 100644
index fa6ac886331..00000000000
--- a/nixos/modules/services/databases/riak-cs.nix
+++ /dev/null
@@ -1,202 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.riak-cs;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.riak-cs = {
-
-      enable = mkEnableOption "riak-cs";
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.riak-cs;
-        defaultText = "pkgs.riak-cs";
-        example = literalExample "pkgs.riak-cs";
-        description = ''
-          Riak package to use.
-        '';
-      };
-
-      nodeName = mkOption {
-        type = types.str;
-        default = "riak-cs@127.0.0.1";
-        description = ''
-          Name of the Erlang node.
-        '';
-      };
-
-      anonymousUserCreation = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Anonymous user creation.
-        '';
-      };
-
-      riakHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8087";
-        description = ''
-          Name of riak hosting service.
-        '';
-      };
-
-      listener = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8080";
-        description = ''
-          Name of Riak CS listening service.
-        '';
-      };
-
-      stanchionHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8085";
-        description = ''
-          Name of stanchion hosting service.
-        '';
-      };
-
-      stanchionSsl = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Tell stanchion to use SSL.
-        '';
-      };
-
-      distributedCookie = mkOption {
-        type = types.str;
-        default = "riak";
-        description = ''
-          Cookie for distributed node communication.  All nodes in the
-          same cluster should use the same cookie or they will not be able to
-          communicate.
-        '';
-      };
-
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/db/riak-cs";
-        description = ''
-          Data directory for Riak CS.
-        '';
-      };
-
-      logDir = mkOption {
-        type = types.path;
-        default = "/var/log/riak-cs";
-        description = ''
-          Log directory for Riak CS.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>riak-cs.conf</filename>.
-        '';
-      };
-
-      extraAdvancedConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>advanced.config</filename>.
-        '';
-      };
-    };
-
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ];
-    environment.etc."riak-cs/riak-cs.conf".text = ''
-      nodename = ${cfg.nodeName}
-      distributed_cookie = ${cfg.distributedCookie}
-
-      platform_log_dir = ${cfg.logDir}
-
-      riak_host = ${cfg.riakHost}
-      listener = ${cfg.listener}
-      stanchion_host = ${cfg.stanchionHost}
-
-      anonymous_user_creation = ${if cfg.anonymousUserCreation then "on" else "off"}
-
-      ${cfg.extraConfig}
-    '';
-
-    environment.etc."riak-cs/advanced.config".text = ''
-      ${cfg.extraAdvancedConfig}
-    '';
-
-    users.users.riak-cs = {
-      name = "riak-cs";
-      uid = config.ids.uids.riak-cs;
-      group = "riak";
-      description = "Riak CS server user";
-    };
-
-  systemd.services.riak-cs = {
-      description = "Riak CS Server";
-
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      path = [
-        pkgs.utillinux # for `logger`
-        pkgs.bash
-      ];
-
-      environment.HOME = "${cfg.dataDir}";
-      environment.RIAK_CS_DATA_DIR = "${cfg.dataDir}";
-      environment.RIAK_CS_LOG_DIR = "${cfg.logDir}";
-      environment.RIAK_CS_ETC_DIR = "/etc/riak";
-
-      preStart = ''
-        if ! test -e ${cfg.logDir}; then
-          mkdir -m 0755 -p ${cfg.logDir}
-          chown -R riak-cs ${cfg.logDir}
-        fi
-
-        if ! test -e ${cfg.dataDir}; then
-          mkdir -m 0700 -p ${cfg.dataDir}
-          chown -R riak-cs ${cfg.dataDir}
-        fi
-      '';
-
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/riak-cs console";
-        ExecStop = "${cfg.package}/bin/riak-cs stop";
-        StandardInput = "tty";
-        User = "riak-cs";
-        Group = "riak-cs";
-        PermissionsStartOnly = true;
-        # Give Riak a decent amount of time to clean up.
-        TimeoutStopSec = 120;
-        LimitNOFILE = 65536;
-      };
-
-      unitConfig.RequiresMountsFor = [
-        "${cfg.dataDir}"
-        "${cfg.logDir}"
-        "/etc/riak"
-      ];
-    };
-  };
-}
diff --git a/nixos/modules/services/databases/riak.nix b/nixos/modules/services/databases/riak.nix
index 885215209bd..657eeea87bf 100644
--- a/nixos/modules/services/databases/riak.nix
+++ b/nixos/modules/services/databases/riak.nix
@@ -118,7 +118,7 @@ in
       after = [ "network.target" ];
 
       path = [
-        pkgs.utillinux # for `logger`
+        pkgs.util-linux # for `logger`
         pkgs.bash
       ];
 
diff --git a/nixos/modules/services/databases/stanchion.nix b/nixos/modules/services/databases/stanchion.nix
deleted file mode 100644
index 97e55bc70c4..00000000000
--- a/nixos/modules/services/databases/stanchion.nix
+++ /dev/null
@@ -1,194 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.stanchion;
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.stanchion = {
-
-      enable = mkEnableOption "stanchion";
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.stanchion;
-        defaultText = "pkgs.stanchion";
-        example = literalExample "pkgs.stanchion";
-        description = ''
-          Stanchion package to use.
-        '';
-      };
-
-      nodeName = mkOption {
-        type = types.str;
-        default = "stanchion@127.0.0.1";
-        description = ''
-          Name of the Erlang node.
-        '';
-      };
-
-      adminKey = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Name of admin user.
-        '';
-      };
-
-      adminSecret = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Name of admin secret
-        '';
-      };
-
-      riakHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8087";
-        description = ''
-          Name of riak hosting service.
-        '';
-      };
-
-      listener = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8085";
-        description = ''
-          Name of Riak CS listening service.
-        '';
-      };
-
-      stanchionHost = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8085";
-        description = ''
-          Name of stanchion hosting service.
-        '';
-      };
-
-      distributedCookie = mkOption {
-        type = types.str;
-        default = "riak";
-        description = ''
-          Cookie for distributed node communication.  All nodes in the
-          same cluster should use the same cookie or they will not be able to
-          communicate.
-        '';
-      };
-
-      dataDir = mkOption {
-        type = types.path;
-        default = "/var/db/stanchion";
-        description = ''
-          Data directory for Stanchion.
-        '';
-      };
-
-      logDir = mkOption {
-        type = types.path;
-        default = "/var/log/stanchion";
-        description = ''
-          Log directory for Stanchion.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be appended to <filename>stanchion.conf</filename>.
-        '';
-      };
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ];
-
-    environment.etc."stanchion/advanced.config".text = ''
-      [{stanchion, []}].
-    '';
-
-    environment.etc."stanchion/stanchion.conf".text = ''
-      listener = ${cfg.listener}
-
-      riak_host = ${cfg.riakHost}
-
-      ${optionalString (cfg.adminKey == "") "#"} admin.key=${optionalString (cfg.adminKey != "") cfg.adminKey}
-      ${optionalString (cfg.adminSecret == "") "#"} admin.secret=${optionalString (cfg.adminSecret != "") cfg.adminSecret}
-
-      platform_bin_dir = ${pkgs.stanchion}/bin
-      platform_data_dir = ${cfg.dataDir}
-      platform_etc_dir = /etc/stanchion
-      platform_lib_dir = ${pkgs.stanchion}/lib
-      platform_log_dir = ${cfg.logDir}
-
-      nodename = ${cfg.nodeName}
-
-      distributed_cookie = ${cfg.distributedCookie}
-
-      ${cfg.extraConfig}
-    '';
-
-    users.users.stanchion = {
-      name = "stanchion";
-      uid = config.ids.uids.stanchion;
-      group = "stanchion";
-      description = "Stanchion server user";
-    };
-
-    users.groups.stanchion.gid = config.ids.gids.stanchion;
-
-    systemd.tmpfiles.rules = [
-      "d '${cfg.logDir}' - stanchion stanchion --"
-      "d '${cfg.dataDir}' 0700 stanchion stanchion --"
-    ];
-
-    systemd.services.stanchion = {
-      description = "Stanchion Server";
-
-      wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
-
-      path = [
-        pkgs.utillinux # for `logger`
-        pkgs.bash
-      ];
-
-      environment.HOME = "${cfg.dataDir}";
-      environment.STANCHION_DATA_DIR = "${cfg.dataDir}";
-      environment.STANCHION_LOG_DIR = "${cfg.logDir}";
-      environment.STANCHION_ETC_DIR = "/etc/stanchion";
-
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/stanchion console";
-        ExecStop = "${cfg.package}/bin/stanchion stop";
-        StandardInput = "tty";
-        User = "stanchion";
-        Group = "stanchion";
-        # Give Stanchion a decent amount of time to clean up.
-        TimeoutStopSec = 120;
-        LimitNOFILE = 65536;
-      };
-
-      unitConfig.RequiresMountsFor = [
-        "${cfg.dataDir}"
-        "${cfg.logDir}"
-        "/etc/stanchion"
-      ];
-    };
-  };
-}
diff --git a/nixos/modules/services/databases/victoriametrics.nix b/nixos/modules/services/databases/victoriametrics.nix
index cb6bf8508fb..5b09115bb2f 100644
--- a/nixos/modules/services/databases/victoriametrics.nix
+++ b/nixos/modules/services/databases/victoriametrics.nix
@@ -40,17 +40,17 @@ let cfg = config.services.victoriametrics; in
     systemd.services.victoriametrics = {
       description = "VictoriaMetrics time series database";
       after = [ "network.target" ];
+      startLimitBurst = 5;
       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}
+              -httpListenAddr ${cfg.listenAddress} \
+              -retentionPeriod ${toString cfg.retentionPeriod} \
               ${lib.escapeShellArgs cfg.extraOptions}
         '';
       };
diff --git a/nixos/modules/services/databases/virtuoso.nix b/nixos/modules/services/databases/virtuoso.nix
index 6eb09e0a58f..8b01622ecb0 100644
--- a/nixos/modules/services/databases/virtuoso.nix
+++ b/nixos/modules/services/databases/virtuoso.nix
@@ -16,28 +16,33 @@ with lib;
       enable = mkEnableOption "Virtuoso Opensource database server";
 
       config = mkOption {
+        type = types.lines;
         default = "";
         description = "Extra options to put into Virtuoso configuration file.";
       };
 
       parameters = mkOption {
+        type = types.lines;
         default = "";
         description = "Extra options to put into [Parameters] section of Virtuoso configuration file.";
       };
 
       listenAddress = mkOption {
+        type = types.str;
         default = "1111";
         example = "myserver:1323";
         description = "ip:port or port to listen on.";
       };
 
       httpListenAddress = mkOption {
+        type = types.nullOr types.str;
         default = null;
         example = "myserver:8080";
         description = "ip:port or port for Virtuoso HTTP server to listen on.";
       };
 
       dirsAllowed = mkOption {
+        type = types.nullOr types.str; # XXX Maybe use a list in the future?
         default = null;
         example = "/www, /home/";
         description = "A list of directories Virtuoso is allowed to access";
diff --git a/nixos/modules/services/desktops/bamf.nix b/nixos/modules/services/desktops/bamf.nix
index 4b35146d084..37121c219a3 100644
--- a/nixos/modules/services/desktops/bamf.nix
+++ b/nixos/modules/services/desktops/bamf.nix
@@ -6,7 +6,7 @@ with lib;
 
 {
   meta = {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 
   ###### interface
diff --git a/nixos/modules/services/desktops/deepin/deepin.nix b/nixos/modules/services/desktops/deepin/deepin.nix
deleted file mode 100644
index f8fb73701af..00000000000
--- a/nixos/modules/services/desktops/deepin/deepin.nix
+++ /dev/null
@@ -1,123 +0,0 @@
-# deepin
-
-{ config, pkgs, lib, ... }:
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.deepin.core.enable = lib.mkEnableOption "
-      Basic dbus and systemd services, groups and users needed by the
-      Deepin Desktop Environment.
-    ";
-
-    services.deepin.deepin-menu.enable = lib.mkEnableOption "
-      DBus service for unified menus in Deepin Desktop Environment.
-    ";
-
-    services.deepin.deepin-turbo.enable = lib.mkEnableOption "
-      Turbo service for the Deepin Desktop Environment. It is a daemon
-      that helps to launch applications faster.
-    ";
-
-  };
-
-
-  ###### implementation
-
-  config = lib.mkMerge [
-
-    (lib.mkIf config.services.deepin.core.enable {
-      environment.systemPackages = [
-        pkgs.deepin.dde-api
-        pkgs.deepin.dde-calendar
-        pkgs.deepin.dde-control-center
-        pkgs.deepin.dde-daemon
-        pkgs.deepin.dde-dock
-        pkgs.deepin.dde-launcher
-        pkgs.deepin.dde-file-manager
-        pkgs.deepin.dde-session-ui
-        pkgs.deepin.deepin-anything
-        pkgs.deepin.deepin-image-viewer
-      ];
-
-      services.dbus.packages = [
-        pkgs.deepin.dde-api
-        pkgs.deepin.dde-calendar
-        pkgs.deepin.dde-control-center
-        pkgs.deepin.dde-daemon
-        pkgs.deepin.dde-dock
-        pkgs.deepin.dde-launcher
-        pkgs.deepin.dde-file-manager
-        pkgs.deepin.dde-session-ui
-        pkgs.deepin.deepin-anything
-        pkgs.deepin.deepin-image-viewer
-      ];
-
-      systemd.packages = [
-        pkgs.deepin.dde-api
-        pkgs.deepin.dde-daemon
-        pkgs.deepin.dde-file-manager
-        pkgs.deepin.deepin-anything
-      ];
-
-      boot.extraModulePackages = [ config.boot.kernelPackages.deepin-anything ];
-
-      boot.kernelModules = [ "vfs_monitor" ];
-
-      users.groups.deepin-sound-player = { };
-
-      users.users.deepin-sound-player = {
-        description = "Deepin sound player";
-        group = "deepin-sound-player";
-        isSystemUser = true;
-      };
-
-      users.groups.deepin-daemon = { };
-
-      users.users.deepin-daemon = {
-        description = "Deepin daemon user";
-        group = "deepin-daemon";
-        isSystemUser = true;
-      };
-
-      users.groups.deepin_anything_server = { };
-
-      users.users.deepin_anything_server = {
-        description = "Deepin Anything Server";
-        group = "deepin_anything_server";
-        isSystemUser = true;
-      };
-
-      security.pam.services.deepin-auth-keyboard.text = ''
-        # original at ${pkgs.deepin.dde-daemon}/etc/pam.d/deepin-auth-keyboard
-        auth	[success=2 default=ignore]	pam_lsass.so
-        auth	[success=1 default=ignore]	pam_unix.so nullok_secure try_first_pass
-        auth	requisite	pam_deny.so
-        auth	required	pam_permit.so
-      '';
-
-      environment.etc = {
-        "polkit-1/localauthority/10-vendor.d/com.deepin.api.device.pkla".source = "${pkgs.deepin.dde-api}/etc/polkit-1/localauthority/10-vendor.d/com.deepin.api.device.pkla";
-        "polkit-1/localauthority/10-vendor.d/com.deepin.daemon.Accounts.pkla".source = "${pkgs.deepin.dde-daemon}/etc/polkit-1/localauthority/10-vendor.d/com.deepin.daemon.Accounts.pkla";
-        "polkit-1/localauthority/10-vendor.d/com.deepin.daemon.Grub2.pkla".source = "${pkgs.deepin.dde-daemon}/etc/polkit-1/localauthority/10-vendor.d/com.deepin.daemon.Grub2.pkla";
-      };
-
-      services.deepin.deepin-menu.enable = true;
-      services.deepin.deepin-turbo.enable = true;
-    })
-
-    (lib.mkIf config.services.deepin.deepin-menu.enable {
-      services.dbus.packages = [ pkgs.deepin.deepin-menu ];
-    })
-
-    (lib.mkIf config.services.deepin.deepin-turbo.enable {
-      environment.systemPackages = [ pkgs.deepin.deepin-turbo ];
-      systemd.packages = [ pkgs.deepin.deepin-turbo ];
-    })
-
-  ];
-
-}
diff --git a/nixos/modules/services/desktops/espanso.nix b/nixos/modules/services/desktops/espanso.nix
index cd2eadf8816..4ef6724dda0 100644
--- a/nixos/modules/services/desktops/espanso.nix
+++ b/nixos/modules/services/desktops/espanso.nix
@@ -12,7 +12,6 @@ in {
   config = mkIf cfg.enable {
     systemd.user.services.espanso = {
       description = "Espanso daemon";
-      path = with pkgs; [ espanso libnotify xclip ];
       serviceConfig = {
         ExecStart = "${pkgs.espanso}/bin/espanso daemon";
         Restart = "on-failure";
diff --git a/nixos/modules/services/desktops/geoclue2.nix b/nixos/modules/services/desktops/geoclue2.nix
index 542b2ead410..e9ec787e5ad 100644
--- a/nixos/modules/services/desktops/geoclue2.nix
+++ b/nixos/modules/services/desktops/geoclue2.nix
@@ -160,7 +160,7 @@ in
       };
 
       appConfig = mkOption {
-        type = types.loaOf appConfigModule;
+        type = types.attrsOf appConfigModule;
         default = {};
         example = literalExample ''
           "com.github.app" = {
@@ -188,7 +188,8 @@ in
 
     systemd.packages = [ package ];
 
-    # we cannot use DynamicUser as we need the the geoclue user to exist for the dbus policy to work
+    # we cannot use DynamicUser as we need the the geoclue user to exist for the
+    # dbus policy to work
     users = {
       users.geoclue = {
         isSystemUser = true;
@@ -217,6 +218,7 @@ in
         # we can't be part of a system service, and the agent should
         # be okay with the main service coming and going
         wantedBy = [ "default.target" ];
+        unitConfig.ConditionUser = "!@system";
         serviceConfig = {
           Type = "exec";
           ExecStart = "${package}/libexec/geoclue-2.0/demos/agent";
@@ -264,5 +266,5 @@ in
       } // mapAttrs' appConfigToINICompatible cfg.appConfig);
   };
 
-  meta.maintainers = with lib.maintainers; [ worldofpeace ];
+  meta.maintainers = with lib.maintainers; [ ];
 }
diff --git a/nixos/modules/services/desktops/gnome3/at-spi2-core.nix b/nixos/modules/services/desktops/gnome/at-spi2-core.nix
index 492242e3296..1268a9d49b8 100644
--- a/nixos/modules/services/desktops/gnome3/at-spi2-core.nix
+++ b/nixos/modules/services/desktops/gnome/at-spi2-core.nix
@@ -12,9 +12,17 @@ with lib;
 
   ###### interface
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "at-spi2-core" "enable" ]
+      [ "services" "gnome" "at-spi2-core" "enable" ]
+    )
+  ];
+
   options = {
 
-    services.gnome3.at-spi2-core = {
+    services.gnome.at-spi2-core = {
 
       enable = mkOption {
         type = types.bool;
@@ -36,13 +44,13 @@ with lib;
   ###### implementation
 
   config = mkMerge [
-    (mkIf config.services.gnome3.at-spi2-core.enable {
+    (mkIf config.services.gnome.at-spi2-core.enable {
       environment.systemPackages = [ pkgs.at-spi2-core ];
       services.dbus.packages = [ pkgs.at-spi2-core ];
       systemd.packages = [ pkgs.at-spi2-core ];
     })
 
-    (mkIf (!config.services.gnome3.at-spi2-core.enable) {
+    (mkIf (!config.services.gnome.at-spi2-core.enable) {
       environment.variables.NO_AT_BRIDGE = "1";
     })
   ];
diff --git a/nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix b/nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix
index 3c7f217b18d..15c5bfbd821 100644
--- a/nixos/modules/services/desktops/gnome3/chrome-gnome-shell.nix
+++ b/nixos/modules/services/desktops/gnome/chrome-gnome-shell.nix
@@ -8,9 +8,17 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "chrome-gnome-shell" "enable" ]
+      [ "services" "gnome" "chrome-gnome-shell" "enable" ]
+    )
+  ];
+
   ###### interface
   options = {
-    services.gnome3.chrome-gnome-shell.enable = mkEnableOption ''
+    services.gnome.chrome-gnome-shell.enable = mkEnableOption ''
       Chrome GNOME Shell native host connector, a DBus service
       allowing to install GNOME Shell extensions from a web browser.
     '';
@@ -18,7 +26,7 @@ with lib;
 
 
   ###### implementation
-  config = mkIf config.services.gnome3.chrome-gnome-shell.enable {
+  config = mkIf config.services.gnome.chrome-gnome-shell.enable {
     environment.etc = {
       "chromium/native-messaging-hosts/org.gnome.chrome_gnome_shell.json".source = "${pkgs.chrome-gnome-shell}/etc/chromium/native-messaging-hosts/org.gnome.chrome_gnome_shell.json";
       "opt/chrome/native-messaging-hosts/org.gnome.chrome_gnome_shell.json".source = "${pkgs.chrome-gnome-shell}/etc/opt/chrome/native-messaging-hosts/org.gnome.chrome_gnome_shell.json";
diff --git a/nixos/modules/services/desktops/gnome/evolution-data-server.nix b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
new file mode 100644
index 00000000000..ef5ad797c27
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
@@ -0,0 +1,71 @@
+# Evolution Data Server daemon.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "evolution-data-server" "enable" ]
+      [ "services" "gnome" "evolution-data-server" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "evolution-data-server" "plugins" ]
+      [ "services" "gnome" "evolution-data-server" "plugins" ]
+    )
+  ];
+
+  ###### interface
+
+  options = {
+
+    services.gnome.evolution-data-server = {
+      enable = mkEnableOption "Evolution Data Server, a collection of services for storing addressbooks and calendars.";
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = "Plugins for Evolution Data Server.";
+      };
+    };
+    programs.evolution = {
+      enable = mkEnableOption "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality.";
+      plugins = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        example = literalExample "[ pkgs.evolution-ews ]";
+        description = "Plugins for Evolution.";
+      };
+
+    };
+  };
+
+  ###### implementation
+
+  config =
+    let
+      bundle = pkgs.evolutionWithPlugins.override { inherit (config.services.gnome.evolution-data-server) plugins; };
+    in
+    mkMerge [
+      (mkIf config.services.gnome.evolution-data-server.enable {
+        environment.systemPackages = [ bundle ];
+
+        services.dbus.packages = [ bundle ];
+
+        systemd.packages = [ bundle ];
+      })
+      (mkIf config.programs.evolution.enable {
+        services.gnome.evolution-data-server = {
+          enable = true;
+          plugins = [ pkgs.evolution ] ++ config.programs.evolution.plugins;
+        };
+        services.gnome.gnome-keyring.enable = true;
+      })
+    ];
+}
diff --git a/nixos/modules/services/desktops/gnome3/glib-networking.nix b/nixos/modules/services/desktops/gnome/glib-networking.nix
index 7e667b6b1f0..4288b6b5de6 100644
--- a/nixos/modules/services/desktops/gnome3/glib-networking.nix
+++ b/nixos/modules/services/desktops/gnome/glib-networking.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "glib-networking" "enable" ]
+      [ "services" "gnome" "glib-networking" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.glib-networking = {
+    services.gnome.glib-networking = {
 
       enable = mkEnableOption "network extensions for GLib";
 
@@ -24,7 +32,7 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.glib-networking.enable {
+  config = mkIf config.services.gnome.glib-networking.enable {
 
     services.dbus.packages = [ pkgs.glib-networking ];
 
diff --git a/nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix b/nixos/modules/services/desktops/gnome/gnome-initial-setup.nix
index c391ad9694c..9e9771cf541 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-initial-setup.nix
+++ b/nixos/modules/services/desktops/gnome/gnome-initial-setup.nix
@@ -48,11 +48,19 @@ in
     maintainers = teams.gnome.members;
   };
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-initial-setup" "enable" ]
+      [ "services" "gnome" "gnome-initial-setup" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.gnome-initial-setup = {
+    services.gnome.gnome-initial-setup = {
 
       enable = mkEnableOption "GNOME Initial Setup, a Simple, easy, and safe way to prepare a new system";
 
@@ -63,16 +71,16 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.gnome-initial-setup.enable {
+  config = mkIf config.services.gnome.gnome-initial-setup.enable {
 
     environment.systemPackages = [
-      pkgs.gnome3.gnome-initial-setup
+      pkgs.gnome.gnome-initial-setup
     ]
     ++ optional (versionOlder config.system.stateVersion "20.03") createGisStampFilesAutostart
     ;
 
     systemd.packages = [
-      pkgs.gnome3.gnome-initial-setup
+      pkgs.gnome.gnome-initial-setup
     ];
 
     systemd.user.targets."gnome-session".wants = [
diff --git a/nixos/modules/services/desktops/gnome3/gnome-keyring.nix b/nixos/modules/services/desktops/gnome/gnome-keyring.nix
index 2916a3c82b3..cda44bab8bf 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-keyring.nix
+++ b/nixos/modules/services/desktops/gnome/gnome-keyring.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-keyring" "enable" ]
+      [ "services" "gnome" "gnome-keyring" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.gnome-keyring = {
+    services.gnome.gnome-keyring = {
 
       enable = mkOption {
         type = types.bool;
@@ -33,18 +41,18 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.gnome-keyring.enable {
+  config = mkIf config.services.gnome.gnome-keyring.enable {
 
-    environment.systemPackages = [ pkgs.gnome3.gnome-keyring ];
+    environment.systemPackages = [ pkgs.gnome.gnome-keyring ];
 
-    services.dbus.packages = [ pkgs.gnome3.gnome-keyring pkgs.gcr ];
+    services.dbus.packages = [ pkgs.gnome.gnome-keyring pkgs.gcr ];
 
-    xdg.portal.extraPortals = [ pkgs.gnome3.gnome-keyring ];
+    xdg.portal.extraPortals = [ pkgs.gnome.gnome-keyring ];
 
     security.pam.services.login.enableGnomeKeyring = true;
 
     security.wrappers.gnome-keyring-daemon = {
-      source = "${pkgs.gnome3.gnome-keyring}/bin/gnome-keyring-daemon";
+      source = "${pkgs.gnome.gnome-keyring}/bin/gnome-keyring-daemon";
       capabilities = "cap_ipc_lock=ep";
     };
 
diff --git a/nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix b/nixos/modules/services/desktops/gnome/gnome-online-accounts.nix
index 3f9ced5e86b..01f7e3695cf 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-online-accounts.nix
+++ b/nixos/modules/services/desktops/gnome/gnome-online-accounts.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-online-accounts" "enable" ]
+      [ "services" "gnome" "gnome-online-accounts" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.gnome-online-accounts = {
+    services.gnome.gnome-online-accounts = {
 
       enable = mkOption {
         type = types.bool;
@@ -32,7 +40,7 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.gnome-online-accounts.enable {
+  config = mkIf config.services.gnome.gnome-online-accounts.enable {
 
     environment.systemPackages = [ pkgs.gnome-online-accounts ];
 
diff --git a/nixos/modules/services/desktops/gnome3/gnome-online-miners.nix b/nixos/modules/services/desktops/gnome/gnome-online-miners.nix
index 39d669e8b30..5f9039f68c4 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-online-miners.nix
+++ b/nixos/modules/services/desktops/gnome/gnome-online-miners.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-online-miners" "enable" ]
+      [ "services" "gnome" "gnome-online-miners" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.gnome-online-miners = {
+    services.gnome.gnome-online-miners = {
 
       enable = mkOption {
         type = types.bool;
@@ -32,11 +40,11 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.gnome-online-miners.enable {
+  config = mkIf config.services.gnome.gnome-online-miners.enable {
 
-    environment.systemPackages = [ pkgs.gnome3.gnome-online-miners ];
+    environment.systemPackages = [ pkgs.gnome.gnome-online-miners ];
 
-    services.dbus.packages = [ pkgs.gnome3.gnome-online-miners ];
+    services.dbus.packages = [ pkgs.gnome.gnome-online-miners ];
 
   };
 
diff --git a/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix b/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix
new file mode 100644
index 00000000000..b5573d2fc21
--- /dev/null
+++ b/nixos/modules/services/desktops/gnome/gnome-remote-desktop.nix
@@ -0,0 +1,32 @@
+# Remote desktop daemon using Pipewire.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  meta = {
+    maintainers = teams.gnome.members;
+  };
+
+  # Added 2021-05-07
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-remote-desktop" "enable" ]
+      [ "services" "gnome" "gnome-remote-desktop" "enable" ]
+    )
+  ];
+
+  ###### interface
+  options = {
+    services.gnome.gnome-remote-desktop = {
+      enable = mkEnableOption "Remote Desktop support using Pipewire";
+    };
+  };
+
+  ###### implementation
+  config = mkIf config.services.gnome.gnome-remote-desktop.enable {
+    services.pipewire.enable = true;
+
+    systemd.packages = [ pkgs.gnome.gnome-remote-desktop ];
+  };
+}
diff --git a/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix b/nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix
index 1c33ed064a1..05b5c86ddcb 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-settings-daemon.nix
+++ b/nixos/modules/services/desktops/gnome/gnome-settings-daemon.nix
@@ -6,7 +6,7 @@ with lib;
 
 let
 
-  cfg = config.services.gnome3.gnome-settings-daemon;
+  cfg = config.services.gnome.gnome-settings-daemon;
 
 in
 
@@ -20,13 +20,19 @@ in
     (mkRemovedOptionModule
       ["services" "gnome3" "gnome-settings-daemon" "package"]
       "")
+
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-settings-daemon" "enable" ]
+      [ "services" "gnome" "gnome-settings-daemon" "enable" ]
+    )
   ];
 
   ###### interface
 
   options = {
 
-    services.gnome3.gnome-settings-daemon = {
+    services.gnome.gnome-settings-daemon = {
 
       enable = mkEnableOption "GNOME Settings Daemon";
 
@@ -40,15 +46,15 @@ in
   config = mkIf cfg.enable {
 
     environment.systemPackages = [
-      pkgs.gnome3.gnome-settings-daemon
+      pkgs.gnome.gnome-settings-daemon
     ];
 
     services.udev.packages = [
-      pkgs.gnome3.gnome-settings-daemon
+      pkgs.gnome.gnome-settings-daemon
     ];
 
     systemd.packages = [
-      pkgs.gnome3.gnome-settings-daemon
+      pkgs.gnome.gnome-settings-daemon
     ];
 
     systemd.user.targets."gnome-session-initialized".wants = [
diff --git a/nixos/modules/services/desktops/gnome3/gnome-user-share.nix b/nixos/modules/services/desktops/gnome/gnome-user-share.nix
index f2fe8b41a9e..38256af309c 100644
--- a/nixos/modules/services/desktops/gnome3/gnome-user-share.nix
+++ b/nixos/modules/services/desktops/gnome/gnome-user-share.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "gnome-user-share" "enable" ]
+      [ "services" "gnome" "gnome-user-share" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.gnome-user-share = {
+    services.gnome.gnome-user-share = {
 
       enable = mkEnableOption "GNOME User Share, a user-level file sharing service for GNOME";
 
@@ -25,14 +33,14 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.gnome-user-share.enable {
+  config = mkIf config.services.gnome.gnome-user-share.enable {
 
     environment.systemPackages = [
-      pkgs.gnome3.gnome-user-share
+      pkgs.gnome.gnome-user-share
     ];
 
     systemd.packages = [
-      pkgs.gnome3.gnome-user-share
+      pkgs.gnome.gnome-user-share
     ];
 
   };
diff --git a/nixos/modules/services/desktops/gnome3/rygel.nix b/nixos/modules/services/desktops/gnome/rygel.nix
index 917a1d6541e..7ea9778fc40 100644
--- a/nixos/modules/services/desktops/gnome3/rygel.nix
+++ b/nixos/modules/services/desktops/gnome/rygel.nix
@@ -8,9 +8,17 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "rygel" "enable" ]
+      [ "services" "gnome" "rygel" "enable" ]
+    )
+  ];
+
   ###### interface
   options = {
-    services.gnome3.rygel = {
+    services.gnome.rygel = {
       enable = mkOption {
         default = false;
         description = ''
@@ -24,13 +32,13 @@ with lib;
   };
 
   ###### implementation
-  config = mkIf config.services.gnome3.rygel.enable {
-    environment.systemPackages = [ pkgs.gnome3.rygel ];
+  config = mkIf config.services.gnome.rygel.enable {
+    environment.systemPackages = [ pkgs.gnome.rygel ];
 
-    services.dbus.packages = [ pkgs.gnome3.rygel ];
+    services.dbus.packages = [ pkgs.gnome.rygel ];
 
-    systemd.packages = [ pkgs.gnome3.rygel ];
+    systemd.packages = [ pkgs.gnome.rygel ];
 
-    environment.etc."rygel.conf".source = "${pkgs.gnome3.rygel}/etc/rygel.conf";
+    environment.etc."rygel.conf".source = "${pkgs.gnome.rygel}/etc/rygel.conf";
   };
 }
diff --git a/nixos/modules/services/desktops/gnome3/sushi.nix b/nixos/modules/services/desktops/gnome/sushi.nix
index 83b17365d5d..3133a3a0d98 100644
--- a/nixos/modules/services/desktops/gnome3/sushi.nix
+++ b/nixos/modules/services/desktops/gnome/sushi.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "sushi" "enable" ]
+      [ "services" "gnome" "sushi" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.sushi = {
+    services.gnome.sushi = {
 
       enable = mkOption {
         type = types.bool;
@@ -31,11 +39,11 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.sushi.enable {
+  config = mkIf config.services.gnome.sushi.enable {
 
-    environment.systemPackages = [ pkgs.gnome3.sushi ];
+    environment.systemPackages = [ pkgs.gnome.sushi ];
 
-    services.dbus.packages = [ pkgs.gnome3.sushi ];
+    services.dbus.packages = [ pkgs.gnome.sushi ];
 
   };
 
diff --git a/nixos/modules/services/desktops/gnome3/tracker-miners.nix b/nixos/modules/services/desktops/gnome/tracker-miners.nix
index f2af4024927..c9101f0caa6 100644
--- a/nixos/modules/services/desktops/gnome3/tracker-miners.nix
+++ b/nixos/modules/services/desktops/gnome/tracker-miners.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "tracker-miners" "enable" ]
+      [ "services" "gnome" "tracker-miners" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.tracker-miners = {
+    services.gnome.tracker-miners = {
 
       enable = mkOption {
         type = types.bool;
@@ -31,7 +39,7 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.tracker-miners.enable {
+  config = mkIf config.services.gnome.tracker-miners.enable {
 
     environment.systemPackages = [ pkgs.tracker-miners ];
 
diff --git a/nixos/modules/services/desktops/gnome3/tracker.nix b/nixos/modules/services/desktops/gnome/tracker.nix
index cd196e38553..29d9662b0b8 100644
--- a/nixos/modules/services/desktops/gnome3/tracker.nix
+++ b/nixos/modules/services/desktops/gnome/tracker.nix
@@ -10,11 +10,19 @@ with lib;
     maintainers = teams.gnome.members;
   };
 
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "tracker" "enable" ]
+      [ "services" "gnome" "tracker" "enable" ]
+    )
+  ];
+
   ###### interface
 
   options = {
 
-    services.gnome3.tracker = {
+    services.gnome.tracker = {
 
       enable = mkOption {
         type = types.bool;
@@ -32,7 +40,7 @@ with lib;
 
   ###### implementation
 
-  config = mkIf config.services.gnome3.tracker.enable {
+  config = mkIf config.services.gnome.tracker.enable {
 
     environment.systemPackages = [ pkgs.tracker ];
 
diff --git a/nixos/modules/services/desktops/gnome3/evolution-data-server.nix b/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
deleted file mode 100644
index bd62d16f61c..00000000000
--- a/nixos/modules/services/desktops/gnome3/evolution-data-server.nix
+++ /dev/null
@@ -1,45 +0,0 @@
-# Evolution Data Server daemon.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-{
-
-  meta = {
-    maintainers = teams.gnome.members;
-  };
-
-  ###### interface
-
-  options = {
-
-    services.gnome3.evolution-data-server = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable Evolution Data Server, a collection of services for
-          storing addressbooks and calendars.
-        '';
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.services.gnome3.evolution-data-server.enable {
-
-    environment.systemPackages = [ pkgs.gnome3.evolution-data-server ];
-
-    services.dbus.packages = [ pkgs.gnome3.evolution-data-server ];
-
-    systemd.packages = [ pkgs.gnome3.evolution-data-server ];
-
-  };
-
-}
diff --git a/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix b/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix
deleted file mode 100644
index 164a0a44f8c..00000000000
--- a/nixos/modules/services/desktops/gnome3/gnome-remote-desktop.nix
+++ /dev/null
@@ -1,24 +0,0 @@
-# Remote desktop daemon using Pipewire.
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-{
-  meta = {
-    maintainers = teams.gnome.members;
-  };
-
-  ###### interface
-  options = {
-    services.gnome3.gnome-remote-desktop = {
-      enable = mkEnableOption "Remote Desktop support using Pipewire";
-    };
-  };
-
-  ###### 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/gvfs.nix b/nixos/modules/services/desktops/gvfs.nix
index 250ea6d4575..966a4d38662 100644
--- a/nixos/modules/services/desktops/gvfs.nix
+++ b/nixos/modules/services/desktops/gvfs.nix
@@ -34,7 +34,7 @@ in
       # gvfs can be built with multiple configurations
       package = mkOption {
         type = types.package;
-        default = pkgs.gnome3.gvfs;
+        default = pkgs.gnome.gvfs;
         description = "Which GVfs package to use.";
       };
 
diff --git a/nixos/modules/services/desktops/pipewire.nix b/nixos/modules/services/desktops/pipewire.nix
deleted file mode 100644
index 5aee59cfdcc..00000000000
--- a/nixos/modules/services/desktops/pipewire.nix
+++ /dev/null
@@ -1,41 +0,0 @@
-# pipewire service.
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.pipewire;
-  packages = with pkgs; [ pipewire ];
-
-in {
-
-  meta = {
-    maintainers = teams.freedesktop.members;
-  };
-
-  ###### interface
-  options = {
-    services.pipewire = {
-      enable = mkEnableOption "pipewire service";
-
-      socketActivation = mkOption {
-        default = true;
-        type = types.bool;
-        description = ''
-          Automatically run pipewire when connections are made to the pipewire socket.
-        '';
-      };
-    };
-  };
-
-
-  ###### implementation
-  config = mkIf cfg.enable {
-    environment.systemPackages = packages;
-
-    systemd.packages = packages;
-
-    systemd.user.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
-  };
-
-}
diff --git a/nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json b/nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json
new file mode 100644
index 00000000000..53fc9cc9634
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/alsa-monitor.conf.json
@@ -0,0 +1,34 @@
+{
+  "properties": {},
+  "rules": [
+    {
+      "matches": [
+        {
+          "device.name": "~alsa_card.*"
+        }
+      ],
+      "actions": {
+        "update-props": {
+          "api.alsa.use-acp": true,
+          "api.acp.auto-profile": false,
+          "api.acp.auto-port": false
+        }
+      }
+    },
+    {
+      "matches": [
+        {
+          "node.name": "~alsa_input.*"
+        },
+        {
+          "node.name": "~alsa_output.*"
+        }
+      ],
+      "actions": {
+        "update-props": {
+          "node.pause-on-idle": false
+        }
+      }
+    }
+  ]
+}
diff --git a/nixos/modules/services/desktops/pipewire/bluez-hardware.conf.json b/nixos/modules/services/desktops/pipewire/bluez-hardware.conf.json
new file mode 100644
index 00000000000..7c527b29215
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/bluez-hardware.conf.json
@@ -0,0 +1,197 @@
+{
+  "bluez5.features.device": [
+    {
+      "name": "Air 1 Plus",
+      "no-features": [
+        "hw-volume-mic"
+      ]
+    },
+    {
+      "name": "AirPods",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "name": "AirPods Pro",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "name": "AXLOIE Goin",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "name": "JBL Endurance RUN BT",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl",
+        "sbc-xq"
+      ]
+    },
+    {
+      "name": "JBL LIVE650BTNC"
+    },
+    {
+      "name": "Soundcore Life P2-L",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "name": "Urbanista Stockholm Plus",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "address": "~^94:16:25:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^9c:64:8b:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^a0:e9:db:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^0c:a6:94:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^00:14:02:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^44:5e:f3:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^d4:9c:28:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^00:18:6b:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^b8:ad:3e:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^a0:e9:db:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^00:24:1c:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^00:11:b1:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^a4:15:66:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^00:14:f1:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^00:26:7e:",
+      "no-features": [
+        "hw-volume"
+      ]
+    },
+    {
+      "address": "~^90:03:b7:",
+      "no-features": [
+        "hw-volume"
+      ]
+    }
+  ],
+  "bluez5.features.adapter": [
+    {
+      "bus-type": "usb",
+      "vendor-id": "usb:0bda"
+    },
+    {
+      "bus-type": "usb",
+      "no-features": [
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "no-features": [
+        "msbc-alt1-rtl"
+      ]
+    }
+  ],
+  "bluez5.features.kernel": [
+    {
+      "sysname": "Linux",
+      "release": "~^[0-4]\\.",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "sysname": "Linux",
+      "release": "~^5\\.[1-7]\\.",
+      "no-features": [
+        "msbc-alt1",
+        "msbc-alt1-rtl"
+      ]
+    },
+    {
+      "sysname": "Linux",
+      "release": "~^5\\.(8|9|10)\\.",
+      "no-features": [
+        "msbc-alt1"
+      ]
+    },
+    {
+      "no-features": []
+    }
+  ]
+}
diff --git a/nixos/modules/services/desktops/pipewire/bluez-monitor.conf.json b/nixos/modules/services/desktops/pipewire/bluez-monitor.conf.json
new file mode 100644
index 00000000000..6d1c23e8256
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/bluez-monitor.conf.json
@@ -0,0 +1,36 @@
+{
+  "properties": {},
+  "rules": [
+    {
+      "matches": [
+        {
+          "device.name": "~bluez_card.*"
+        }
+      ],
+      "actions": {
+        "update-props": {
+          "bluez5.auto-connect": [
+            "hfp_hf",
+            "hsp_hs",
+            "a2dp_sink"
+          ]
+        }
+      }
+    },
+    {
+      "matches": [
+        {
+          "node.name": "~bluez_input.*"
+        },
+        {
+          "node.name": "~bluez_output.*"
+        }
+      ],
+      "actions": {
+        "update-props": {
+          "node.pause-on-idle": false
+        }
+      }
+    }
+  ]
+}
diff --git a/nixos/modules/services/desktops/pipewire/client-rt.conf.json b/nixos/modules/services/desktops/pipewire/client-rt.conf.json
new file mode 100644
index 00000000000..284d8c394a6
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/client-rt.conf.json
@@ -0,0 +1,39 @@
+{
+  "context.properties": {
+    "log.level": 0
+  },
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rtkit",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-client-device"
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    },
+    {
+      "name": "libpipewire-module-session-manager"
+    }
+  ],
+  "filter.properties": {},
+  "stream.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/client.conf.json b/nixos/modules/services/desktops/pipewire/client.conf.json
new file mode 100644
index 00000000000..71294a0e78a
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/client.conf.json
@@ -0,0 +1,31 @@
+{
+  "context.properties": {
+    "log.level": 0
+  },
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-client-device"
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    },
+    {
+      "name": "libpipewire-module-session-manager"
+    }
+  ],
+  "filter.properties": {},
+  "stream.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/jack.conf.json b/nixos/modules/services/desktops/pipewire/jack.conf.json
new file mode 100644
index 00000000000..e36e04fffcf
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/jack.conf.json
@@ -0,0 +1,28 @@
+{
+  "context.properties": {
+    "log.level": 0
+  },
+  "context.spa-libs": {
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rt",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    }
+  ],
+  "jack.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/media-session.conf.json b/nixos/modules/services/desktops/pipewire/media-session.conf.json
new file mode 100644
index 00000000000..24906e767d6
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/media-session.conf.json
@@ -0,0 +1,67 @@
+{
+  "context.properties": {},
+  "context.spa-libs": {
+    "api.bluez5.*": "bluez5/libspa-bluez5",
+    "api.alsa.*": "alsa/libspa-alsa",
+    "api.v4l2.*": "v4l2/libspa-v4l2",
+    "api.libcamera.*": "libcamera/libspa-libcamera"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rtkit",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-client-device"
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    },
+    {
+      "name": "libpipewire-module-session-manager"
+    }
+  ],
+  "session.modules": {
+    "default": [
+      "flatpak",
+      "portal",
+      "v4l2",
+      "suspend-node",
+      "policy-node"
+    ],
+    "with-audio": [
+      "metadata",
+      "default-nodes",
+      "default-profile",
+      "default-routes",
+      "alsa-seq",
+      "alsa-monitor"
+    ],
+    "with-alsa": [
+      "with-audio"
+    ],
+    "with-jack": [
+      "with-audio"
+    ],
+    "with-pulseaudio": [
+      "with-audio",
+      "bluez5",
+      "logind",
+      "restore-stream",
+      "streams-follow-default"
+    ]
+  }
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
new file mode 100644
index 00000000000..41ab995e329
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
@@ -0,0 +1,135 @@
+# pipewire example session manager.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  json = pkgs.formats.json {};
+  cfg = config.services.pipewire.media-session;
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit
+                           && pkgs.stdenv.isx86_64
+                           && pkgs.pkgsi686Linux.pipewire != null;
+
+  # Use upstream config files passed through spa-json-dump as the base
+  # Patched here as necessary for them to work with this module
+  defaults = {
+    alsa-monitor = (builtins.fromJSON (builtins.readFile ./alsa-monitor.conf.json));
+    bluez-monitor = (builtins.fromJSON (builtins.readFile ./bluez-monitor.conf.json));
+    bluez-hardware = (builtins.fromJSON (builtins.readFile ./bluez-hardware.conf.json));
+    media-session = (builtins.fromJSON (builtins.readFile ./media-session.conf.json));
+    v4l2-monitor = (builtins.fromJSON (builtins.readFile ./v4l2-monitor.conf.json));
+  };
+
+  configs = {
+    alsa-monitor = recursiveUpdate defaults.alsa-monitor cfg.config.alsa-monitor;
+    bluez-monitor = recursiveUpdate defaults.bluez-monitor cfg.config.bluez-monitor;
+    bluez-hardware = defaults.bluez-hardware;
+    media-session = recursiveUpdate defaults.media-session cfg.config.media-session;
+    v4l2-monitor = recursiveUpdate defaults.v4l2-monitor cfg.config.v4l2-monitor;
+  };
+in {
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  ###### interface
+  options = {
+    services.pipewire.media-session = {
+      enable = mkOption {
+        type = types.bool;
+        default = config.services.pipewire.enable;
+        defaultText = "config.services.pipewire.enable";
+        description = "Example pipewire session manager";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pipewire.mediaSession;
+        example = literalExample "pkgs.pipewire.mediaSession";
+        description = ''
+          The pipewire-media-session derivation to use.
+        '';
+      };
+
+      config = {
+        media-session = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the media session core. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/media-session.conf
+          '';
+          default = {};
+        };
+
+        alsa-monitor = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the alsa monitor. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/alsa-monitor.conf
+          '';
+          default = {};
+        };
+
+        bluez-monitor = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the bluez5 monitor. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/bluez-monitor.conf
+          '';
+          default = {};
+        };
+
+        v4l2-monitor = mkOption {
+          type = json.type;
+          description = ''
+            Configuration for the V4L2 monitor. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/media-session.d/v4l2-monitor.conf
+          '';
+          default = {};
+        };
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+    systemd.user.services.pipewire-media-session.wantedBy = [ "pipewire.service" ];
+
+    environment.etc."pipewire/media-session.d/media-session.conf" = {
+      source = json.generate "media-session.conf" configs.media-session;
+    };
+    environment.etc."pipewire/media-session.d/v4l2-monitor.conf" = {
+      source = json.generate "v4l2-monitor.conf" configs.v4l2-monitor;
+    };
+
+    environment.etc."pipewire/media-session.d/with-alsa" =
+      mkIf config.services.pipewire.alsa.enable {
+        text = "";
+      };
+    environment.etc."pipewire/media-session.d/alsa-monitor.conf" =
+      mkIf config.services.pipewire.alsa.enable {
+        source = json.generate "alsa-monitor.conf" configs.alsa-monitor;
+      };
+
+    environment.etc."pipewire/media-session.d/with-pulseaudio" =
+      mkIf config.services.pipewire.pulse.enable {
+        text = "";
+      };
+    environment.etc."pipewire/media-session.d/bluez-monitor.conf" =
+      mkIf config.services.pipewire.pulse.enable {
+        source = json.generate "bluez-monitor.conf" configs.bluez-monitor;
+      };
+    environment.etc."pipewire/media-session.d/bluez-hardware.conf" =
+      mkIf config.services.pipewire.pulse.enable {
+        source = json.generate "bluez-hardware.conf" configs.bluez-hardware;
+      };
+
+    environment.etc."pipewire/media-session.d/with-jack" =
+      mkIf config.services.pipewire.jack.enable {
+        text = "";
+      };
+  };
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json b/nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json
new file mode 100644
index 00000000000..17bbbdef117
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire-pulse.conf.json
@@ -0,0 +1,41 @@
+{
+  "context.properties": {},
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rtkit",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    },
+    {
+      "name": "libpipewire-module-protocol-pulse",
+      "args": {
+        "server.address": [
+          "unix:native"
+        ],
+        "vm.overrides": {
+          "pulse.min.quantum": "1024/48000"
+        }
+      }
+    }
+  ],
+  "stream.properties": {}
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.conf.json b/nixos/modules/services/desktops/pipewire/pipewire.conf.json
new file mode 100644
index 00000000000..a923ab4db23
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire.conf.json
@@ -0,0 +1,93 @@
+{
+  "context.properties": {
+    "link.max-buffers": 16,
+    "core.daemon": true,
+    "core.name": "pipewire-0",
+    "vm.overrides": {
+      "default.clock.min-quantum": 1024
+    }
+  },
+  "context.spa-libs": {
+    "audio.convert.*": "audioconvert/libspa-audioconvert",
+    "api.alsa.*": "alsa/libspa-alsa",
+    "api.v4l2.*": "v4l2/libspa-v4l2",
+    "api.libcamera.*": "libcamera/libspa-libcamera",
+    "api.bluez5.*": "bluez5/libspa-bluez5",
+    "api.vulkan.*": "vulkan/libspa-vulkan",
+    "api.jack.*": "jack/libspa-jack",
+    "support.*": "support/libspa-support"
+  },
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rtkit",
+      "args": {},
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-protocol-native"
+    },
+    {
+      "name": "libpipewire-module-profiler"
+    },
+    {
+      "name": "libpipewire-module-metadata"
+    },
+    {
+      "name": "libpipewire-module-spa-device-factory"
+    },
+    {
+      "name": "libpipewire-module-spa-node-factory"
+    },
+    {
+      "name": "libpipewire-module-client-node"
+    },
+    {
+      "name": "libpipewire-module-client-device"
+    },
+    {
+      "name": "libpipewire-module-portal",
+      "flags": [
+        "ifexists",
+        "nofail"
+      ]
+    },
+    {
+      "name": "libpipewire-module-access",
+      "args": {}
+    },
+    {
+      "name": "libpipewire-module-adapter"
+    },
+    {
+      "name": "libpipewire-module-link-factory"
+    },
+    {
+      "name": "libpipewire-module-session-manager"
+    }
+  ],
+  "context.objects": [
+    {
+      "factory": "spa-node-factory",
+      "args": {
+        "factory.name": "support.node.driver",
+        "node.name": "Dummy-Driver",
+        "node.group": "pipewire.dummy",
+        "priority.driver": 20000
+      }
+    },
+    {
+      "factory": "spa-node-factory",
+      "args": {
+        "factory.name": "support.node.driver",
+        "node.name": "Freewheel-Driver",
+        "priority.driver": 19000,
+        "node.group": "pipewire.freewheel",
+        "node.freewheel": true
+      }
+    }
+  ],
+  "context.exec": []
+}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
new file mode 100644
index 00000000000..dbd6c5d87e1
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -0,0 +1,202 @@
+# pipewire service.
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  json = pkgs.formats.json {};
+  cfg = config.services.pipewire;
+  enable32BitAlsaPlugins = cfg.alsa.support32Bit
+                           && pkgs.stdenv.isx86_64
+                           && pkgs.pkgsi686Linux.pipewire != null;
+
+  # The package doesn't output to $out/lib/pipewire directly so that the
+  # overlays can use the outputs to replace the originals in FHS environments.
+  #
+  # This doesn't work in general because of missing development information.
+  jack-libs = pkgs.runCommand "jack-libs" {} ''
+    mkdir -p "$out/lib"
+    ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
+  '';
+
+  # Use upstream config files passed through spa-json-dump as the base
+  # Patched here as necessary for them to work with this module
+  defaults = {
+    client = builtins.fromJSON (builtins.readFile ./client.conf.json);
+    client-rt = builtins.fromJSON (builtins.readFile ./client-rt.conf.json);
+    jack = builtins.fromJSON (builtins.readFile ./jack.conf.json);
+    # Remove session manager invocation from the upstream generated file, it points to the wrong path
+    pipewire = builtins.fromJSON (builtins.readFile ./pipewire.conf.json);
+    pipewire-pulse = builtins.fromJSON (builtins.readFile ./pipewire-pulse.conf.json);
+  };
+
+  configs = {
+    client = recursiveUpdate defaults.client cfg.config.client;
+    client-rt = recursiveUpdate defaults.client-rt cfg.config.client-rt;
+    jack = recursiveUpdate defaults.jack cfg.config.jack;
+    pipewire = recursiveUpdate defaults.pipewire cfg.config.pipewire;
+    pipewire-pulse = recursiveUpdate defaults.pipewire-pulse cfg.config.pipewire-pulse;
+  };
+in {
+
+  meta = {
+    maintainers = teams.freedesktop.members;
+  };
+
+  ###### interface
+  options = {
+    services.pipewire = {
+      enable = mkEnableOption "pipewire service";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pipewire;
+        defaultText = "pkgs.pipewire";
+        example = literalExample "pkgs.pipewire";
+        description = ''
+          The pipewire derivation to use.
+        '';
+      };
+
+      socketActivation = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Automatically run pipewire when connections are made to the pipewire socket.
+        '';
+      };
+
+      config = {
+        client = mkOption {
+          type = json.type;
+          default = {};
+          description = ''
+            Configuration for pipewire clients. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/client.conf.in
+          '';
+        };
+
+        client-rt = mkOption {
+          type = json.type;
+          default = {};
+          description = ''
+            Configuration for realtime pipewire clients. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/client-rt.conf.in
+          '';
+        };
+
+        jack = mkOption {
+          type = json.type;
+          default = {};
+          description = ''
+            Configuration for the pipewire daemon's jack module. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/jack.conf.in
+          '';
+        };
+
+        pipewire = mkOption {
+          type = json.type;
+          default = {};
+          description = ''
+            Configuration for the pipewire daemon. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/pipewire.conf.in
+          '';
+        };
+
+        pipewire-pulse = mkOption {
+          type = json.type;
+          default = {};
+          description = ''
+            Configuration for the pipewire-pulse daemon. For details see
+            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/pipewire-pulse.conf.in
+          '';
+        };
+      };
+
+      alsa = {
+        enable = mkEnableOption "ALSA support";
+        support32Bit = mkEnableOption "32-bit ALSA support on 64-bit systems";
+      };
+
+      jack = {
+        enable = mkEnableOption "JACK audio emulation";
+      };
+
+      pulse = {
+        enable = mkEnableOption "PulseAudio server emulation";
+      };
+    };
+  };
+
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.pulse.enable -> !config.hardware.pulseaudio.enable;
+        message = "PipeWire based PulseAudio server emulation replaces PulseAudio. This option requires `hardware.pulseaudio.enable` to be set to false";
+      }
+      {
+        assertion = cfg.jack.enable -> !config.services.jack.jackd.enable;
+        message = "PipeWire based JACK emulation doesn't use the JACK service. This option requires `services.jack.jackd.enable` to be set to false";
+      }
+    ];
+
+    environment.systemPackages = [ cfg.package ]
+                                 ++ lib.optional cfg.jack.enable jack-libs;
+
+    systemd.packages = [ cfg.package ]
+                       ++ lib.optional cfg.pulse.enable cfg.package.pulse;
+
+    # PipeWire depends on DBUS but doesn't list it. Without this booting
+    # into a terminal results in the service crashing with an error.
+    systemd.user.sockets.pipewire.wantedBy = lib.mkIf cfg.socketActivation [ "sockets.target" ];
+    systemd.user.sockets.pipewire-pulse.wantedBy = lib.mkIf (cfg.socketActivation && cfg.pulse.enable) ["sockets.target"];
+    systemd.user.services.pipewire.bindsTo = [ "dbus.service" ];
+    services.udev.packages = [ cfg.package ];
+
+    # If any paths are updated here they must also be updated in the package test.
+    environment.etc."alsa/conf.d/49-pipewire-modules.conf" = mkIf cfg.alsa.enable {
+      text = ''
+        pcm_type.pipewire {
+          libs.native = ${cfg.package.lib}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;
+          ${optionalString enable32BitAlsaPlugins
+            "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire.lib}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;"}
+        }
+        ctl_type.pipewire {
+          libs.native = ${cfg.package.lib}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;
+          ${optionalString enable32BitAlsaPlugins
+            "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire.lib}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;"}
+        }
+      '';
+    };
+    environment.etc."alsa/conf.d/50-pipewire.conf" = mkIf cfg.alsa.enable {
+      source = "${cfg.package}/share/alsa/alsa.conf.d/50-pipewire.conf";
+    };
+    environment.etc."alsa/conf.d/99-pipewire-default.conf" = mkIf cfg.alsa.enable {
+      source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
+    };
+
+    environment.etc."pipewire/client.conf" = {
+      source = json.generate "client.conf" configs.client;
+    };
+    environment.etc."pipewire/client-rt.conf" = {
+      source = json.generate "client-rt.conf" configs.client-rt;
+    };
+    environment.etc."pipewire/jack.conf" = {
+      source = json.generate "jack.conf" configs.jack;
+    };
+    environment.etc."pipewire/pipewire.conf" = {
+      source = json.generate "pipewire.conf" configs.pipewire;
+    };
+    environment.etc."pipewire/pipewire-pulse.conf" = {
+      source = json.generate "pipewire-pulse.conf" configs.pipewire-pulse;
+    };
+
+    environment.sessionVariables.LD_LIBRARY_PATH =
+      lib.optional cfg.jack.enable "/run/current-system/sw/lib/pipewire";
+
+    # https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/464#note_723554
+    systemd.user.services.pipewire.environment."PIPEWIRE_LINK_PASSIVE" = "1";
+  };
+}
diff --git a/nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json b/nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json
new file mode 100644
index 00000000000..b08cba1b604
--- /dev/null
+++ b/nixos/modules/services/desktops/pipewire/v4l2-monitor.conf.json
@@ -0,0 +1,30 @@
+{
+  "properties": {},
+  "rules": [
+    {
+      "matches": [
+        {
+          "device.name": "~v4l2_device.*"
+        }
+      ],
+      "actions": {
+        "update-props": {}
+      }
+    },
+    {
+      "matches": [
+        {
+          "node.name": "~v4l2_input.*"
+        },
+        {
+          "node.name": "~v4l2_output.*"
+        }
+      ],
+      "actions": {
+        "update-props": {
+          "node.pause-on-idle": false
+        }
+      }
+    }
+  ]
+}
diff --git a/nixos/modules/services/desktops/profile-sync-daemon.nix b/nixos/modules/services/desktops/profile-sync-daemon.nix
index a8ac22ac127..6206295272f 100644
--- a/nixos/modules/services/desktops/profile-sync-daemon.nix
+++ b/nixos/modules/services/desktops/profile-sync-daemon.nix
@@ -36,7 +36,7 @@ in {
             description = "Profile Sync daemon";
             wants = [ "psd-resync.service" ];
             wantedBy = [ "default.target" ];
-            path = with pkgs; [ rsync kmod gawk nettools utillinux profile-sync-daemon ];
+            path = with pkgs; [ rsync kmod gawk nettools util-linux profile-sync-daemon ];
             unitConfig = {
               RequiresMountsFor = [ "/home/" ];
             };
@@ -55,7 +55,7 @@ in {
             wants = [ "psd-resync.timer" ];
             partOf = [ "psd.service" ];
             wantedBy = [ "default.target" ];
-            path = with pkgs; [ rsync kmod gawk nettools utillinux profile-sync-daemon ];
+            path = with pkgs; [ rsync kmod gawk nettools util-linux profile-sync-daemon ];
             serviceConfig = {
               Type = "oneshot";
               ExecStart = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon resync";
diff --git a/nixos/modules/services/desktops/telepathy.nix b/nixos/modules/services/desktops/telepathy.nix
index 34596bf7818..b5f6a5fcbcf 100644
--- a/nixos/modules/services/desktops/telepathy.nix
+++ b/nixos/modules/services/desktops/telepathy.nix
@@ -38,6 +38,11 @@ with lib;
 
     services.dbus.packages = [ pkgs.telepathy-mission-control ];
 
+    # Enable runtime optional telepathy in gnome-shell
+    services.xserver.desktopManager.gnome.sessionPath = with pkgs; [
+      telepathy-glib
+      telepathy-logger
+    ];
   };
 
 }
diff --git a/nixos/modules/services/desktops/tumbler.nix b/nixos/modules/services/desktops/tumbler.nix
index a09079517f0..8d9248cb983 100644
--- a/nixos/modules/services/desktops/tumbler.nix
+++ b/nixos/modules/services/desktops/tumbler.nix
@@ -19,7 +19,7 @@ in
   ];
 
   meta = {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 
   ###### interface
diff --git a/nixos/modules/services/desktops/zeitgeist.nix b/nixos/modules/services/desktops/zeitgeist.nix
index cf7dd5fe3a1..fb0218da304 100644
--- a/nixos/modules/services/desktops/zeitgeist.nix
+++ b/nixos/modules/services/desktops/zeitgeist.nix
@@ -7,7 +7,7 @@ with lib;
 {
 
   meta = {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 
   ###### interface
diff --git a/nixos/modules/services/development/blackfire.nix b/nixos/modules/services/development/blackfire.nix
new file mode 100644
index 00000000000..6fd948cce38
--- /dev/null
+++ b/nixos/modules/services/development/blackfire.nix
@@ -0,0 +1,65 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.blackfire-agent;
+
+  agentConfigFile = lib.generators.toINI {} {
+    blackfire =  cfg.settings;
+  };
+
+  agentSock = "blackfire/agent.sock";
+in {
+  meta = {
+    maintainers = pkgs.blackfire.meta.maintainers;
+    doc = ./blackfire.xml;
+  };
+
+  options = {
+    services.blackfire-agent = {
+      enable = lib.mkEnableOption "Blackfire profiler agent";
+      settings = lib.mkOption {
+        description = ''
+          See https://blackfire.io/docs/configuration/agent
+        '';
+        type = lib.types.submodule {
+          freeformType = with lib.types; attrsOf str;
+
+          options = {
+            server-id = lib.mkOption {
+              type = lib.types.str;
+              description = ''
+                Sets the server id used to authenticate with Blackfire
+
+                You can find your personal server-id at https://blackfire.io/my/settings/credentials
+              '';
+            };
+
+            server-token = lib.mkOption {
+              type = lib.types.str;
+              description = ''
+                Sets the server token used to authenticate with Blackfire
+
+                You can find your personal server-token at https://blackfire.io/my/settings/credentials
+              '';
+            };
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.etc."blackfire/agent".text = agentConfigFile;
+
+    services.blackfire-agent.settings.socket = "unix:///run/${agentSock}";
+
+    systemd.services.blackfire-agent = {
+      description = "Blackfire agent";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.blackfire}/bin/blackfire-agent";
+        RuntimeDirectory = "blackfire";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/development/blackfire.xml b/nixos/modules/services/development/blackfire.xml
new file mode 100644
index 00000000000..ad4af35788d
--- /dev/null
+++ b/nixos/modules/services/development/blackfire.xml
@@ -0,0 +1,45 @@
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="module-services-blackfire">
+ <title>Blackfire profiler</title>
+ <para>
+  <emphasis>Source:</emphasis>
+  <filename>modules/services/development/blackfire.nix</filename>
+ </para>
+ <para>
+  <emphasis>Upstream documentation:</emphasis>
+  <link xlink:href="https://blackfire.io/docs/introduction"/>
+ </para>
+ <para>
+  <link xlink:href="https://blackfire.io">Blackfire</link> is a proprietary tool for profiling applications. There are several languages supported by the product but currently only PHP support is packaged in Nixpkgs. The back-end consists of a module that is loaded into the language runtime (called <firstterm>probe</firstterm>) and a service (<firstterm>agent</firstterm>) that the probe connects to and that sends the profiles to the server.
+ </para>
+ <para>
+  To use it, you will need to enable the agent and the probe on your server. The exact method will depend on the way you use PHP but here is an example of NixOS configuration for PHP-FPM:
+<programlisting>let
+  php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [
+    blackfire
+  ]));
+in {
+  # Enable the probe extension for PHP-FPM.
+  services.phpfpm = {
+    phpPackage = php;
+  };
+
+  # Enable and configure the agent.
+  services.blackfire-agent = {
+    enable = true;
+    settings = {
+      # You will need to get credentials at https://blackfire.io/my/settings/credentials
+      # You can also use other options described in https://blackfire.io/docs/configuration/agent
+      server-id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
+      server-token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
+    };
+  };
+
+  # Make the agent run on start-up.
+  # Alternately, you can start it manually with `systemctl start blackfire-agent`.
+  systemd.services.blackfire-agent.wantedBy = [ "phpfpm-foo.service" ];
+}</programlisting>
+ </para>
+ <para>
+  On your developer machine, you will also want to install <link xlink:href="https://blackfire.io/docs/up-and-running/installation#install-a-profiling-client">the client</link> (see <package>blackfire</package> package) or the browser extension to actually trigger the profiling.
+ </para>
+</chapter>
diff --git a/nixos/modules/services/development/bloop.nix b/nixos/modules/services/development/bloop.nix
index 226718a9e80..c1180a8bbdd 100644
--- a/nixos/modules/services/development/bloop.nix
+++ b/nixos/modules/services/development/bloop.nix
@@ -44,7 +44,7 @@ in {
       };
       serviceConfig = {
         Type        = "simple";
-        ExecStart   = ''${pkgs.bloop}/bin/bloop server'';
+        ExecStart   = "${pkgs.bloop}/bin/bloop server";
         Restart     = "always";
       };
     };
diff --git a/nixos/modules/services/development/hoogle.nix b/nixos/modules/services/development/hoogle.nix
index 1a98f005602..6d6c88b9b2a 100644
--- a/nixos/modules/services/development/hoogle.nix
+++ b/nixos/modules/services/development/hoogle.nix
@@ -25,6 +25,7 @@ in {
     };
 
     packages = mkOption {
+      type = types.functionTo (types.listOf types.package);
       default = hp: [];
       defaultText = "hp: []";
       example = "hp: with hp; [ text lens ]";
@@ -49,6 +50,11 @@ in {
       default = "https://hoogle.haskell.org";
     };
 
+    host = mkOption {
+      type = types.str;
+      description = "Set the host to bind on.";
+      default = "127.0.0.1";
+    };
   };
 
   config = mkIf cfg.enable {
@@ -59,12 +65,10 @@ in {
 
       serviceConfig = {
         Restart = "always";
-        ExecStart = ''${hoogleEnv}/bin/hoogle server --local --port ${toString cfg.port} --home ${cfg.home}'';
+        ExecStart = ''${hoogleEnv}/bin/hoogle server --local --port ${toString cfg.port} --home ${cfg.home} --host ${cfg.host}'';
 
-        User = "nobody";
-        Group = "nogroup";
+        DynamicUser = true;
 
-        PrivateTmp = true;
         ProtectHome = true;
 
         RuntimeDirectory = "hoogle";
diff --git a/nixos/modules/services/development/jupyter/default.nix b/nixos/modules/services/development/jupyter/default.nix
index 6a5fd6b2940..21b84b3bcda 100644
--- a/nixos/modules/services/development/jupyter/default.nix
+++ b/nixos/modules/services/development/jupyter/default.nix
@@ -131,7 +131,7 @@ in {
             env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
                     ipykernel
                     pandas
-                    scikitlearn
+                    scikit-learn
                   ]));
           in {
             displayName = "Python 3 for machine learning";
diff --git a/nixos/modules/services/development/jupyterhub/default.nix b/nixos/modules/services/development/jupyterhub/default.nix
index f1dcab68b00..a1df4468cff 100644
--- a/nixos/modules/services/development/jupyterhub/default.nix
+++ b/nixos/modules/services/development/jupyterhub/default.nix
@@ -117,7 +117,7 @@ in {
             env = (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [
                     ipykernel
                     pandas
-                    scikitlearn
+                    scikit-learn
                   ]));
           in {
             displayName = "Python 3 for machine learning";
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index c843aa56d13..fc576e4c18b 100644
--- a/nixos/modules/services/development/lorri.nix
+++ b/nixos/modules/services/development/lorri.nix
@@ -15,6 +15,15 @@ in {
           issued by the `lorri` command.
         '';
       };
+      package = lib.mkOption {
+        default = pkgs.lorri;
+        type = lib.types.package;
+        description = ''
+          The lorri package to use.
+        '';
+        defaultText = lib.literalExample "pkgs.lorri";
+        example = lib.literalExample "pkgs.lorri";
+      };
     };
   };
 
@@ -34,7 +43,7 @@ in {
       after = [ "lorri.socket" ];
       path = with pkgs; [ config.nix.package git gnutar gzip ];
       serviceConfig = {
-        ExecStart = "${pkgs.lorri}/bin/lorri daemon";
+        ExecStart = "${cfg.package}/bin/lorri daemon";
         PrivateTmp = true;
         ProtectSystem = "strict";
         ProtectHome = "read-only";
@@ -42,6 +51,6 @@ in {
       };
     };
 
-    environment.systemPackages = [ pkgs.lorri ];
+    environment.systemPackages = [ cfg.package ];
   };
 }
diff --git a/nixos/modules/services/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix
new file mode 100644
index 00000000000..c3072bf0996
--- /dev/null
+++ b/nixos/modules/services/display-managers/greetd.nix
@@ -0,0 +1,106 @@
+{ config, lib, pkgs, ... }:
+with lib;
+
+let
+  cfg = config.services.greetd;
+  tty = "tty${toString cfg.vt}";
+  settingsFormat = pkgs.formats.toml {};
+in
+{
+  options.services.greetd = {
+    enable = mkEnableOption "greetd";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.greetd.greetd;
+      defaultText = "pkgs.greetd.greetd";
+      description = "The greetd package that should be used.";
+    };
+
+    settings = mkOption {
+      type = settingsFormat.type;
+      example = literalExample ''
+        {
+          default_session = {
+            command = "''${pkgs.greetd.greetd}/bin/agreety --cmd sway";
+          };
+        }
+      '';
+      description = ''
+        greetd configuration (<link xlink:href="https://man.sr.ht/~kennylevinsen/greetd/">documentation</link>)
+        as a Nix attribute set.
+      '';
+    };
+
+    vt = mkOption  {
+      type = types.int;
+      default = 1;
+      description = ''
+        The virtual console (tty) that greetd should use. This option also disables getty on that tty.
+      '';
+    };
+
+    restart = mkOption {
+      type = types.bool;
+      default = !(cfg.settings ? initial_session);
+      defaultText = "!(config.services.greetd.settings ? initial_session)";
+      description = ''
+        Wether to restart greetd when it terminates (e.g. on failure).
+        This is usually desirable so a user can always log in, but should be disabled when using 'settings.initial_session' (autologin),
+        because every greetd restart will trigger the autologin again.
+      '';
+    };
+  };
+  config = mkIf cfg.enable {
+
+    services.greetd.settings.terminal.vt = mkDefault cfg.vt;
+    services.greetd.settings.default_session = mkDefault "greeter";
+
+    security.pam.services.greetd = {
+      allowNullPassword = true;
+      startSession = true;
+    };
+
+    # This prevents nixos-rebuild from killing greetd by activating getty again
+    systemd.services."autovt@${tty}".enable = false;
+
+    systemd.services.greetd = {
+      unitConfig = {
+        Wants = [
+          "systemd-user-sessions.service"
+        ];
+        After = [
+          "systemd-user-sessions.service"
+          "plymouth-quit-wait.service"
+          "getty@${tty}.service"
+        ];
+        Conflicts = [
+          "getty@${tty}.service"
+        ];
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.greetd.greetd}/bin/greetd --config ${settingsFormat.generate "greetd.toml" cfg.settings}";
+
+        Restart = mkIf cfg.restart "always";
+
+        # Defaults from greetd upstream configuration
+        IgnoreSIGPIPE = false;
+        SendSIGHUP = true;
+        TimeoutStopSec = "30s";
+        KeyringMode = "shared";
+      };
+
+      # Don't kill a user session when using nixos-rebuild
+      restartIfChanged = false;
+
+      wantedBy = [ "graphical.target" ];
+    };
+
+    systemd.defaultUnit = "graphical.target";
+
+    users.users.greeter.isSystemUser = true;
+  };
+
+  meta.maintainers = with maintainers; [ queezle ];
+}
diff --git a/nixos/modules/services/editors/emacs.xml b/nixos/modules/services/editors/emacs.xml
index 05f87df43bc..fd99ee9442c 100644
--- a/nixos/modules/services/editors/emacs.xml
+++ b/nixos/modules/services/editors/emacs.xml
@@ -156,7 +156,7 @@ $ ./result/bin/emacs
 
 let
   myEmacs = pkgs.emacs; <co xml:id="ex-emacsNix-2" />
-  emacsWithPackages = (pkgs.emacsPackagesGen myEmacs).emacsWithPackages; <co xml:id="ex-emacsNix-3" />
+  emacsWithPackages = (pkgs.emacsPackagesFor myEmacs).emacsWithPackages; <co xml:id="ex-emacsNix-3" />
 in
   emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [ <co xml:id="ex-emacsNix-4" />
     magit          # ; Integrate git &lt;C-x g&gt;
@@ -254,10 +254,10 @@ in
     <example xml:id="module-services-emacs-querying-packages">
      <title>Querying Emacs packages</title>
 <programlisting><![CDATA[
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.elpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.melpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.melpaStablePackages
-nix-env -f "<nixpkgs>" -qaP -A emacsPackages.orgPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.elpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaStablePackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.orgPackages
 ]]></programlisting>
     </example>
    </para>
@@ -322,7 +322,7 @@ https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides
     If you want, you can tweak the Emacs package itself from your
     <filename>emacs.nix</filename>. For example, if you want to have a
     GTK 3-based Emacs instead of the default GTK 2-based binary and remove the
-    automatically generated <filename>emacs.desktop</filename> (useful is you
+    automatically generated <filename>emacs.desktop</filename> (useful if you
     only use <command>emacsclient</command>), you can change your file
     <filename>emacs.nix</filename> in this way:
    </para>
diff --git a/nixos/modules/services/editors/infinoted.nix b/nixos/modules/services/editors/infinoted.nix
index 8b997ccbf66..3eb0753194d 100644
--- a/nixos/modules/services/editors/infinoted.nix
+++ b/nixos/modules/services/editors/infinoted.nix
@@ -51,7 +51,7 @@ in {
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 6523;
       description = ''
         Port to listen on
@@ -141,14 +141,14 @@ in {
           install -o ${cfg.user} -g ${cfg.group} -m 0600 /dev/null /var/lib/infinoted/infinoted.conf
           cat >>/var/lib/infinoted/infinoted.conf <<EOF
           [infinoted]
-          ${optionalString (cfg.keyFile != null) ''key-file=${cfg.keyFile}''}
-          ${optionalString (cfg.certificateFile != null) ''certificate-file=${cfg.certificateFile}''}
-          ${optionalString (cfg.certificateChain != null) ''certificate-chain=${cfg.certificateChain}''}
+          ${optionalString (cfg.keyFile != null) "key-file=${cfg.keyFile}"}
+          ${optionalString (cfg.certificateFile != null) "certificate-file=${cfg.certificateFile}"}
+          ${optionalString (cfg.certificateChain != null) "certificate-chain=${cfg.certificateChain}"}
           port=${toString cfg.port}
           security-policy=${cfg.securityPolicy}
           root-directory=${cfg.rootDirectory}
           plugins=${concatStringsSep ";" cfg.plugins}
-          ${optionalString (cfg.passwordFile != null) ''password=$(head -n 1 ${cfg.passwordFile})''}
+          ${optionalString (cfg.passwordFile != null) "password=$(head -n 1 ${cfg.passwordFile})"}
 
           ${cfg.extraConfig}
           EOF
diff --git a/nixos/modules/services/games/factorio.nix b/nixos/modules/services/games/factorio.nix
index 4b2e1a3c07f..3cb14275792 100644
--- a/nixos/modules/services/games/factorio.nix
+++ b/nixos/modules/services/games/factorio.nix
@@ -35,9 +35,10 @@ let
     auto_pause = true;
     only_admins_can_pause_the_game = true;
     autosave_only_on_server = true;
-    admins = [];
+    non_blocking_saving = cfg.nonBlockingSaving;
   } // cfg.extraSettings;
   serverSettingsFile = pkgs.writeText "server-settings.json" (builtins.toJSON (filterAttrsRecursive (n: v: v != null) serverSettings));
+  serverAdminsFile = pkgs.writeText "server-adminlist.json" (builtins.toJSON cfg.admins);
   modDir = pkgs.factorio-utils.mkModDirDrv cfg.mods;
 in
 {
@@ -49,8 +50,23 @@ in
         default = 34197;
         description = ''
           The port to which the service should bind.
+        '';
+      };
 
-          This option will also open up the UDP port in the firewall configuration.
+      admins = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "username" ];
+        description = ''
+          List of player names which will be admin.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to automatically open the specified UDP port in the firewall.
         '';
       };
       saveName = mkOption {
@@ -188,6 +204,15 @@ in
           Autosave interval in minutes.
         '';
       };
+      nonBlockingSaving = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Highly experimental feature, enable only at your own risk of losing your saves.
+          On UNIX systems, server will fork itself to create an autosave.
+          Autosaving on connected Windows clients will be disabled regardless of autosave_only_on_server option.
+        '';
+      };
     };
   };
 
@@ -219,6 +244,7 @@ in
           "--start-server=${mkSavePath cfg.saveName}"
           "--server-settings=${serverSettingsFile}"
           (optionalString (cfg.mods != []) "--mod-directory=${modDir}")
+          (optionalString (cfg.admins != []) "--server-adminlist=${serverAdminsFile}")
         ];
 
         # Sandboxing
@@ -237,6 +263,6 @@ in
       };
     };
 
-    networking.firewall.allowedUDPPorts = [ cfg.port ];
+    networking.firewall.allowedUDPPorts = if cfg.openFirewall then [ cfg.port ] else [];
   };
 }
diff --git a/nixos/modules/services/games/freeciv.nix b/nixos/modules/services/games/freeciv.nix
new file mode 100644
index 00000000000..4923891a617
--- /dev/null
+++ b/nixos/modules/services/games/freeciv.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.freeciv;
+  inherit (config.users) groups;
+  rootDir = "/run/freeciv";
+  argsFormat = {
+    type = with lib.types; let
+      valueType = nullOr (oneOf [
+        bool int float str
+        (listOf valueType)
+      ]) // {
+        description = "freeciv-server params";
+      };
+    in valueType;
+    generate = name: value:
+      let mkParam = k: v:
+            if v == null then []
+            else if isBool v then if v then [("--"+k)] else []
+            else [("--"+k) v];
+          mkParams = k: v: map (mkParam k) (if isList v then v else [v]);
+      in escapeShellArgs (concatLists (concatLists (mapAttrsToList mkParams value)));
+  };
+in
+{
+  options = {
+    services.freeciv = {
+      enable = mkEnableOption ''freeciv'';
+      settings = mkOption {
+        description = ''
+          Parameters of freeciv-server.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = argsFormat.type;
+          options.Announce = mkOption {
+            type = types.enum ["IPv4" "IPv6" "none"];
+            default = "none";
+            description = "Announce game in LAN using given protocol.";
+          };
+          options.auth = mkEnableOption "server authentication";
+          options.Database = mkOption {
+            type = types.nullOr types.str;
+            apply = pkgs.writeText "auth.conf";
+            default = ''
+              [fcdb]
+                backend="sqlite"
+                database="/var/lib/freeciv/auth.sqlite"
+            '';
+            description = "Enable database connection with given configuration.";
+          };
+          options.debug = mkOption {
+            type = types.ints.between 0 3;
+            default = 0;
+            description = "Set debug log level.";
+          };
+          options.exit-on-end = mkEnableOption "exit instead of restarting when a game ends.";
+          options.Guests = mkEnableOption "guests to login if auth is enabled";
+          options.Newusers = mkEnableOption "new users to login if auth is enabled";
+          options.port = mkOption {
+            type = types.port;
+            default = 5556;
+            description = "Listen for clients on given port";
+          };
+          options.quitidle = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            description = "Quit if no players for given time in seconds.";
+          };
+          options.read = mkOption {
+            type = types.lines;
+            apply = v: pkgs.writeTextDir "read.serv" v + "/read";
+            default = ''
+              /fcdb lua sqlite_createdb()
+            '';
+            description = "Startup script.";
+          };
+          options.saves = mkOption {
+            type = types.nullOr types.str;
+            default = "/var/lib/freeciv/saves/";
+            description = ''
+              Save games to given directory,
+              a sub-directory named after the starting date of the service
+              will me inserted to preserve older saves.
+            '';
+          };
+        };
+      };
+      openFirewall = mkEnableOption "opening the firewall for the port listening for clients";
+    };
+  };
+  config = mkIf cfg.enable {
+    users.groups.freeciv = {};
+    # Use with:
+    #   journalctl -u freeciv.service -f -o cat &
+    #   cat >/run/freeciv.stdin
+    #   load saves/2020-11-14_05-22-27/freeciv-T0005-Y-3750-interrupted.sav.bz2
+    systemd.sockets.freeciv = {
+      wantedBy = [ "sockets.target" ];
+      socketConfig = {
+        ListenFIFO = "/run/freeciv.stdin";
+        SocketGroup = groups.freeciv.name;
+        SocketMode = "660";
+        RemoveOnStop = true;
+      };
+    };
+    systemd.services.freeciv = {
+      description = "Freeciv Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.HOME = "/var/lib/freeciv";
+      serviceConfig = {
+        Restart = "on-failure";
+        RestartSec = "5s";
+        StandardInput = "fd:freeciv.socket";
+        StandardOutput = "journal";
+        StandardError = "journal";
+        ExecStart = pkgs.writeShellScript "freeciv-server" (''
+          set -eux
+          savedir=$(date +%Y-%m-%d_%H-%M-%S)
+          '' + "${pkgs.freeciv}/bin/freeciv-server"
+          + " " + optionalString (cfg.settings.saves != null)
+            (concatStringsSep " " [ "--saves" "${escapeShellArg cfg.settings.saves}/$savedir" ])
+          + " " + argsFormat.generate "freeciv-server" (cfg.settings // { saves = null; }));
+        DynamicUser = true;
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "755";
+        StateDirectory = [ "freeciv" ];
+        WorkingDirectory = "/var/lib/freeciv";
+        # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
+        InaccessiblePaths = ["-+${rootDir}"];
+        # This is for BindPaths= and BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        MountAPIVFS = true;
+        BindReadOnlyPaths = [
+          builtins.storeDir
+          "/etc"
+          "/run"
+        ];
+        # The following options are only for optimizing:
+        # systemd-analyze security freeciv
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall listed by:
+          # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' freeciv-server
+          # in tests, and seem likely not necessary for freeciv-server.
+          "~@aio" "~@chown" "~@ipc" "~@keyring" "~@memlock"
+          "~@resources" "~@setuid" "~@sync" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+    networking.firewall = mkIf cfg.openFirewall
+      { allowedTCPPorts = [ cfg.settings.port ]; };
+  };
+  meta.maintainers = with lib.maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
index f52079fc1ef..2111c970d4f 100644
--- a/nixos/modules/services/games/minetest-server.nix
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg   = config.services.minetest-server;
-  flag  = val: name: if val != null then "--${name} ${val} " else "";
+  flag  = val: name: if val != null then "--${name} ${toString val} " else "";
   flags = [
     (flag cfg.gameId "gameid")
     (flag cfg.world "world")
diff --git a/nixos/modules/services/games/openarena.nix b/nixos/modules/services/games/openarena.nix
index 8c014d78809..9c441e98b20 100644
--- a/nixos/modules/services/games/openarena.nix
+++ b/nixos/modules/services/games/openarena.nix
@@ -19,7 +19,7 @@ in
       extraFlags = mkOption {
         type = types.listOf types.str;
         default = [];
-        description = ''Extra flags to pass to <command>oa_ded</command>'';
+        description = "Extra flags to pass to <command>oa_ded</command>";
         example = [
           "+set dedicated 2"
           "+set sv_hostname 'My NixOS OpenArena Server'"
diff --git a/nixos/modules/services/games/quake3-server.nix b/nixos/modules/services/games/quake3-server.nix
new file mode 100644
index 00000000000..1dc01260e8f
--- /dev/null
+++ b/nixos/modules/services/games/quake3-server.nix
@@ -0,0 +1,111 @@
+{ config, pkgs, lib, ... }:
+with lib;
+
+let
+  cfg = config.services.quake3-server;
+  configFile = pkgs.writeText "q3ds-extra.cfg" ''
+    set net_port ${builtins.toString cfg.port}
+
+    ${cfg.extraConfig}
+  '';
+  defaultBaseq3 = pkgs.requireFile rec {
+    name = "baseq3";
+    hashMode = "recursive";
+    sha256 = "5dd8ee09eabd45e80450f31d7a8b69b846f59738726929298d8a813ce5725ed3";
+    message = ''
+      Unfortunately, we cannot download ${name} automatically.
+      Please purchase a legitimate copy of Quake 3 and change into the installation directory.
+
+      You can either add all relevant files to the nix-store like this:
+      mkdir /tmp/baseq3
+      cp baseq3/pak*.pk3 /tmp/baseq3
+      nix-store --add-fixed sha256 --recursive /tmp/baseq3
+
+      Alternatively you can set services.quake3-server.baseq3 to a path and copy the baseq3 directory into
+      $services.quake3-server.baseq3/.q3a/
+    '';
+  };
+  home = pkgs.runCommand "quake3-home" {} ''
+      mkdir -p $out/.q3a/baseq3
+
+      for file in ${cfg.baseq3}/*; do
+        ln -s $file $out/.q3a/baseq3/$(basename $file)
+      done
+
+      ln -s ${configFile} $out/.q3a/baseq3/nix.cfg
+  '';
+in {
+  options = {
+    services.quake3-server = {
+      enable = mkEnableOption "Quake 3 dedicated server";
+
+      port = mkOption {
+        type = types.port;
+        default = 27960;
+        description = ''
+          UDP Port the server should listen on.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the firewall.
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          seta rconPassword "superSecret"      // sets RCON password for remote console
+          seta sv_hostname "My Quake 3 server"      // name that appears in server list
+        '';
+        description = ''
+          Extra configuration options. Note that options changed via RCON will not be persisted. To list all possible
+          options, use "cvarlist 1" via RCON.
+        '';
+      };
+
+      baseq3 = mkOption {
+        type = types.either types.package types.path;
+        default = defaultBaseq3;
+        example = "/var/lib/q3ds";
+        description = ''
+          Path to the baseq3 files (pak*.pk3). If this is on the nix store (type = package) all .pk3 files should be saved
+          in the top-level directory. If this is on another filesystem (e.g /var/lib/baseq3) the .pk3 files are searched in
+          $baseq3/.q3a/baseq3/
+        '';
+      };
+    };
+  };
+
+  config = let
+    baseq3InStore = builtins.typeOf cfg.baseq3 == "set";
+  in mkIf cfg.enable {
+    networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    systemd.services.q3ds = {
+      description = "Quake 3 dedicated server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+
+      environment.HOME = if baseq3InStore then home else cfg.baseq3;
+
+      serviceConfig = with lib; {
+        Restart = "always";
+        DynamicUser = true;
+        WorkingDirectory = home;
+
+        # It is possible to alter configuration files via RCON. To ensure reproducibility we have to prevent this
+        ReadOnlyPaths = if baseq3InStore then home else cfg.baseq3;
+        ExecStartPre = optionalString (!baseq3InStore) "+${pkgs.coreutils}/bin/cp ${configFile} ${cfg.baseq3}/.q3a/baseq3/nix.cfg";
+
+        ExecStart = "${pkgs.ioquake3}/ioq3ded.x86_64 +exec nix.cfg";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ f4814n ];
+}
diff --git a/nixos/modules/services/games/terraria.nix b/nixos/modules/services/games/terraria.nix
index 413660321ec..7312c7e6b63 100644
--- a/nixos/modules/services/games/terraria.nix
+++ b/nixos/modules/services/games/terraria.nix
@@ -25,7 +25,7 @@ let
       exit 0
     fi
 
-    ${getBin pkgs.tmux}/bin/tmux -S /var/lib/terraria/terraria.sock send-keys Enter exit Enter
+    ${getBin pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/terraria.sock send-keys Enter exit Enter
     ${getBin pkgs.coreutils}/bin/tail --pid="$1" -f /dev/null
   '';
 in
@@ -36,13 +36,13 @@ in
         type        = types.bool;
         default     = false;
         description = ''
-          If enabled, starts a Terraria server. The server can be connected to via <literal>tmux -S /var/lib/terraria/terraria.sock attach</literal>
+          If enabled, starts a Terraria server. The server can be connected to via <literal>tmux -S ${cfg.dataDir}/terraria.sock attach</literal>
           for administration by users who are a part of the <literal>terraria</literal> group (use <literal>C-b d</literal> shortcut to detach again).
         '';
       };
 
       port = mkOption {
-        type        = types.int;
+        type        = types.port;
         default     = 7777;
         description = ''
           Specifies the port to listen on.
@@ -50,7 +50,7 @@ in
       };
 
       maxPlayers = mkOption {
-        type        = types.int;
+        type        = types.ints.u8;
         default     = 255;
         description = ''
           Sets the max number of players (between 1 and 255).
@@ -111,13 +111,26 @@ in
         default     = false;
         description = "Disables automatic Universal Plug and Play.";
       };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Wheter to open ports in the firewall";
+      };
+
+      dataDir = mkOption {
+        type        = types.str;
+        default     = "/var/lib/terraria";
+        example     = "/srv/terraria";
+        description = "Path to variable state data directory for terraria.";
+      };
     };
   };
 
   config = mkIf cfg.enable {
     users.users.terraria = {
       description = "Terraria server service user";
-      home        = "/var/lib/terraria";
+      home        = cfg.dataDir;
       createHome  = true;
       uid         = config.ids.uids.terraria;
     };
@@ -136,14 +149,20 @@ in
         User    = "terraria";
         Type = "forking";
         GuessMainPID = true;
-        ExecStart = "${getBin pkgs.tmux}/bin/tmux -S /var/lib/terraria/terraria.sock new -d ${pkgs.terraria-server}/bin/TerrariaServer ${concatStringsSep " " flags}";
+        ExecStart = "${getBin pkgs.tmux}/bin/tmux -S ${cfg.dataDir}/terraria.sock new -d ${pkgs.terraria-server}/bin/TerrariaServer ${concatStringsSep " " flags}";
         ExecStop = "${stopScript} $MAINPID";
       };
 
       postStart = ''
-        ${pkgs.coreutils}/bin/chmod 660 /var/lib/terraria/terraria.sock
-        ${pkgs.coreutils}/bin/chgrp terraria /var/lib/terraria/terraria.sock
+        ${pkgs.coreutils}/bin/chmod 660 ${cfg.dataDir}/terraria.sock
+        ${pkgs.coreutils}/bin/chgrp terraria ${cfg.dataDir}/terraria.sock
       '';
     };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+      allowedUDPPorts = [ cfg.port ];
+    };
+
   };
 }
diff --git a/nixos/modules/services/hardware/acpid.nix b/nixos/modules/services/hardware/acpid.nix
index 4c97485d972..3e619fe32ef 100644
--- a/nixos/modules/services/hardware/acpid.nix
+++ b/nixos/modules/services/hardware/acpid.nix
@@ -3,21 +3,22 @@
 with lib;
 
 let
+  cfg = config.services.acpid;
 
   canonicalHandlers = {
     powerEvent = {
       event = "button/power.*";
-      action = config.services.acpid.powerEventCommands;
+      action = cfg.powerEventCommands;
     };
 
     lidEvent = {
       event = "button/lid.*";
-      action = config.services.acpid.lidEventCommands;
+      action = cfg.lidEventCommands;
     };
 
     acEvent = {
       event = "ac_adapter.*";
-      action = config.services.acpid.acEventCommands;
+      action = cfg.acEventCommands;
     };
   };
 
@@ -33,7 +34,7 @@ let
             echo "event=${handler.event}" > $fn
             echo "action=${pkgs.writeShellScriptBin "${name}.sh" handler.action }/bin/${name}.sh '%e'" >> $fn
           '';
-        in concatStringsSep "\n" (mapAttrsToList f (canonicalHandlers // config.services.acpid.handlers))
+        in concatStringsSep "\n" (mapAttrsToList f (canonicalHandlers // cfg.handlers))
       }
     '';
 
@@ -47,11 +48,7 @@ in
 
     services.acpid = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable the ACPI daemon.";
-      };
+      enable = mkEnableOption "the ACPI daemon";
 
       logEvents = mkOption {
         type = types.bool;
@@ -129,26 +126,28 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.acpid.enable {
+  config = mkIf cfg.enable {
 
     systemd.services.acpid = {
       description = "ACPI Daemon";
+      documentation = [ "man:acpid(8)" ];
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "systemd-udev-settle.service" ];
-
-      path = [ pkgs.acpid ];
 
       serviceConfig = {
-        Type = "forking";
+        ExecStart = escapeShellArgs
+          ([ "${pkgs.acpid}/bin/acpid"
+             "--foreground"
+             "--netlink"
+             "--confdir" "${acpiConfDir}"
+           ] ++ optional cfg.logEvents "--logevents"
+          );
       };
-
       unitConfig = {
         ConditionVirtualization = "!systemd-nspawn";
         ConditionPathExists = [ "/proc/acpi" ];
       };
 
-      script = "acpid ${optionalString config.services.acpid.logEvents "--logevents"} --confdir ${acpiConfDir}";
     };
 
   };
diff --git a/nixos/modules/services/hardware/actkbd.nix b/nixos/modules/services/hardware/actkbd.nix
index daa407ca1f0..f7770f85da3 100644
--- a/nixos/modules/services/hardware/actkbd.nix
+++ b/nixos/modules/services/hardware/actkbd.nix
@@ -75,7 +75,7 @@ in
         type = types.listOf (types.submodule bindingCfg);
         default = [];
         example = lib.literalExample ''
-          [ { keys = [ 113 ]; events = [ "key" ]; command = "''${pkgs.alsaUtils}/bin/amixer -q set Master toggle"; }
+          [ { keys = [ 113 ]; events = [ "key" ]; command = "''${pkgs.alsa-utils}/bin/amixer -q set Master toggle"; }
           ]
         '';
         description = ''
diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
new file mode 100644
index 00000000000..f846476b30b
--- /dev/null
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -0,0 +1,24 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.auto-cpufreq;
+in {
+  options = {
+    services.auto-cpufreq = {
+      enable = mkEnableOption "auto-cpufreq daemon";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.auto-cpufreq ];
+
+    systemd = {
+      packages = [ pkgs.auto-cpufreq ];
+      services.auto-cpufreq = {
+        # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
+        wantedBy = [ "multi-user.target" ];
+        path = with pkgs; [ bash coreutils ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/bluetooth.nix b/nixos/modules/services/hardware/bluetooth.nix
index dfa39e7f602..08ad90126b1 100644
--- a/nixos/modules/services/hardware/bluetooth.nix
+++ b/nixos/modules/services/hardware/bluetooth.nix
@@ -1,12 +1,39 @@
 { config, lib, pkgs, ... }:
-
-with lib;
-
 let
   cfg = config.hardware.bluetooth;
-  bluez-bluetooth = cfg.package;
+  package = cfg.package;
+
+  inherit (lib)
+    mkDefault mkEnableOption mkIf mkOption
+    mkRenamedOptionModule mkRemovedOptionModule
+    concatStringsSep escapeShellArgs
+    optional optionals optionalAttrs recursiveUpdate types;
+
+  cfgFmt = pkgs.formats.ini { };
+
+  # bluez will complain if some of the sections are not found, so just make them
+  # empty (but present in the file) for now
+  defaults = {
+    General.ControllerMode = "dual";
+    Controller = { };
+    GATT = { };
+    Policy.AutoEnable = cfg.powerOnBoot;
+  };
+
+  hasDisabledPlugins = builtins.length cfg.disabledPlugins > 0;
 
-in {
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "hardware" "bluetooth" "config" ] [ "hardware" "bluetooth" "settings" ])
+    (mkRemovedOptionModule [ "hardware" "bluetooth" "extraConfig" ] ''
+      Use hardware.bluetooth.settings instead.
+
+      This is part of the general move to use structured settings instead of raw
+      text for config as introduced by RFC0042:
+      https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
+    '')
+  ];
 
   ###### interface
 
@@ -15,8 +42,10 @@ in {
     hardware.bluetooth = {
       enable = mkEnableOption "support for Bluetooth";
 
+      hsphfpd.enable = mkEnableOption "support for hsphfpd[-prototype] implementation";
+
       powerOnBoot = mkOption {
-        type    = types.bool;
+        type = types.bool;
         default = true;
         description = "Whether to power up the default Bluetooth controller on boot.";
       };
@@ -36,8 +65,15 @@ in {
         '';
       };
 
-      config = mkOption {
-        type = with types; attrsOf (attrsOf (oneOf [ bool int str ]));
+      disabledPlugins = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = "Built-in plugins to disable";
+      };
+
+      settings = mkOption {
+        type = cfgFmt.type;
+        default = { };
         example = {
           General = {
             ControllerMode = "bredr";
@@ -45,55 +81,65 @@ in {
         };
         description = "Set configuration for system-wide bluetooth (/etc/bluetooth/main.conf).";
       };
-
-      extraConfig = mkOption {
-        type = with types; nullOr lines;
-        default = null;
-        example = ''
-          [General]
-          ControllerMode = bredr
-        '';
-        description = ''
-          Set additional configuration for system-wide bluetooth (/etc/bluetooth/main.conf).
-        '';
-      };
     };
-
   };
 
   ###### implementation
 
   config = mkIf cfg.enable {
-    warnings = optional (cfg.extraConfig != null) "hardware.bluetooth.`extraConfig` is deprecated, please use hardware.bluetooth.`config`.";
+    environment.systemPackages = [ package ]
+      ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
 
-    hardware.bluetooth.config = {
-      Policy = {
-        AutoEnable = mkDefault cfg.powerOnBoot;
-      };
-    };
-
-    environment.systemPackages = [ bluez-bluetooth ];
-
-    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 ];
-    services.dbus.packages = [ bluez-bluetooth ];
-    systemd.packages       = [ bluez-bluetooth ];
+    environment.etc."bluetooth/main.conf".source =
+      cfgFmt.generate "main.conf" (recursiveUpdate defaults cfg.settings);
+    services.udev.packages = [ package ];
+    services.dbus.packages = [ package ]
+      ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
+    systemd.packages = [ package ];
 
     systemd.services = {
-      bluetooth = {
+      bluetooth =
+        let
+          # `man bluetoothd` will refer to main.conf in the nix store but bluez
+          # will in fact load the configuration file at /etc/bluetooth/main.conf
+          # so force it here to avoid any ambiguity and things suddenly breaking
+          # if/when the bluez derivation is changed.
+          args = [ "-f" "/etc/bluetooth/main.conf" ]
+            ++ optional hasDisabledPlugins
+            "--noplugin=${concatStringsSep "," cfg.disabledPlugins}";
+        in
+        {
+          wantedBy = [ "bluetooth.target" ];
+          aliases = [ "dbus-org.bluez.service" ];
+          serviceConfig.ExecStart = [
+            ""
+            "${package}/libexec/bluetooth/bluetoothd ${escapeShellArgs args}"
+          ];
+          # restarting can leave people without a mouse/keyboard
+          unitConfig.X-RestartIfChanged = false;
+        };
+    }
+    // (optionalAttrs cfg.hsphfpd.enable {
+      hsphfpd = {
+        after = [ "bluetooth.service" ];
+        requires = [ "bluetooth.service" ];
         wantedBy = [ "bluetooth.target" ];
-        aliases  = [ "dbus-org.bluez.service" ];
+
+        description = "A prototype implementation used for connecting HSP/HFP Bluetooth devices";
+        serviceConfig.ExecStart = "${pkgs.hsphfpd}/bin/hsphfpd.pl";
       };
-    };
+    });
 
     systemd.user.services = {
       obex.aliases = [ "dbus-org.bluez.obex.service" ];
-    };
+    }
+    // optionalAttrs cfg.hsphfpd.enable {
+      telephony_client = {
+        wantedBy = [ "default.target" ];
 
+        description = "telephony_client for hsphfpd";
+        serviceConfig.ExecStart = "${pkgs.hsphfpd}/bin/telephony_client.pl";
+      };
+    };
   };
-
 }
diff --git a/nixos/modules/services/hardware/brltty.nix b/nixos/modules/services/hardware/brltty.nix
index 1266e8f81e5..73056017532 100644
--- a/nixos/modules/services/hardware/brltty.nix
+++ b/nixos/modules/services/hardware/brltty.nix
@@ -5,6 +5,19 @@ with lib;
 let
   cfg = config.services.brltty;
 
+  targets = [
+    "default.target" "multi-user.target"
+    "rescue.target" "emergency.target"
+  ];
+
+  genApiKey = pkgs.writers.writeDash "generate-brlapi-key" ''
+    if ! test -f /etc/brlapi.key; then
+      echo -n generating brlapi key...
+      ${pkgs.brltty}/bin/brltty-genkey -f /etc/brlapi.key
+      echo done
+    fi
+  '';
+
 in {
 
   options = {
@@ -18,33 +31,27 @@ in {
   };
 
   config = mkIf cfg.enable {
-
-    systemd.services.brltty = {
-      description = "Braille Device Support";
-      unitConfig = {
-        Documentation = "http://mielke.cc/brltty/";
-        DefaultDependencies = "no";
-        RequiresMountsFor = "${pkgs.brltty}/var/lib/brltty";
-      };
-      serviceConfig = {
-        ExecStart = "${pkgs.brltty}/bin/brltty --no-daemon";
-        Type = "notify";
-        TimeoutStartSec = 5;
-        TimeoutStopSec = 10;
-        Restart = "always";
-        RestartSec = 30;
-        Nice = -10;
-        OOMScoreAdjust = -900;
-        ProtectHome = "read-only";
-        ProtectSystem = "full";
-        SystemCallArchitectures = "native";
-      };
-      wants = [ "systemd-udev-settle.service" ];
-      after = [ "local-fs.target" "systemd-udev-settle.service" ];
-      before = [ "sysinit.target" ];
-      wantedBy = [ "sysinit.target" ];
+    users.users.brltty = {
+      description = "BRLTTY daemon user";
+      group = "brltty";
+    };
+    users.groups = {
+      brltty = { };
+      brlapi = { };
     };
 
+    systemd.services."brltty@".serviceConfig =
+      { ExecStartPre = "!${genApiKey}"; };
+
+    # Install all upstream-provided files
+    systemd.packages = [ pkgs.brltty ];
+    systemd.tmpfiles.packages = [ pkgs.brltty ];
+    services.udev.packages = [ pkgs.brltty ];
+    environment.systemPackages = [ pkgs.brltty ];
+
+    # Add missing WantedBys (see issue #81138)
+    systemd.paths.brltty.wantedBy = targets;
+    systemd.paths."brltty@".wantedBy = targets;
   };
 
 }
diff --git a/nixos/modules/services/hardware/ddccontrol.nix b/nixos/modules/services/hardware/ddccontrol.nix
new file mode 100644
index 00000000000..766bf12ee9f
--- /dev/null
+++ b/nixos/modules/services/hardware/ddccontrol.nix
@@ -0,0 +1,36 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.ddccontrol;
+in
+
+{
+  ###### interface
+
+  options = {
+    services.ddccontrol = {
+      enable = lib.mkEnableOption "ddccontrol for controlling displays";
+    };
+  };
+
+  ###### implementation
+
+  config = lib.mkIf cfg.enable {
+    # Give users access to the "gddccontrol" tool
+    environment.systemPackages = [
+      pkgs.ddccontrol
+    ];
+
+    services.dbus.packages = [
+      pkgs.ddccontrol
+    ];
+
+    systemd.packages = [
+      pkgs.ddccontrol
+    ];
+  };
+}
diff --git a/nixos/modules/services/hardware/fancontrol.nix b/nixos/modules/services/hardware/fancontrol.nix
index bb4541a784d..5574c5a132e 100644
--- a/nixos/modules/services/hardware/fancontrol.nix
+++ b/nixos/modules/services/hardware/fancontrol.nix
@@ -6,21 +6,21 @@ let
   cfg = config.hardware.fancontrol;
   configFile = pkgs.writeText "fancontrol.conf" cfg.config;
 
-in{
+in
+{
   options.hardware.fancontrol = {
     enable = mkEnableOption "software fan control (requires fancontrol.config)";
 
     config = mkOption {
-      default = null;
       type = types.lines;
-      description = "Fancontrol configuration file content. See <citerefentry><refentrytitle>pwmconfig</refentrytitle><manvolnum>8</manvolnum></citerefentry> from the lm_sensors package.";
+      description = "Required fancontrol configuration file content. See <citerefentry><refentrytitle>pwmconfig</refentrytitle><manvolnum>8</manvolnum></citerefentry> from the lm_sensors package.";
       example = ''
         # Configuration file generated by pwmconfig
         INTERVAL=10
         DEVPATH=hwmon3=devices/virtual/thermal/thermal_zone2 hwmon4=devices/platform/f71882fg.656
         DEVNAME=hwmon3=soc_dts1 hwmon4=f71869a
         FCTEMPS=hwmon4/device/pwm1=hwmon3/temp1_input
-        FCFANS= hwmon4/device/pwm1=hwmon4/device/fan1_input
+        FCFANS=hwmon4/device/pwm1=hwmon4/device/fan1_input
         MINTEMP=hwmon4/device/pwm1=35
         MAXTEMP=hwmon4/device/pwm1=65
         MINSTART=hwmon4/device/pwm1=150
@@ -30,16 +30,18 @@ in{
   };
 
   config = mkIf cfg.enable {
+
     systemd.services.fancontrol = {
-      unitConfig.Documentation = "man:fancontrol(8)";
+      documentation = [ "man:fancontrol(8)" ];
       description = "software fan control";
       wantedBy = [ "multi-user.target" ];
       after = [ "lm_sensors.service" ];
 
       serviceConfig = {
-        Type = "simple";
         ExecStart = "${pkgs.lm_sensors}/sbin/fancontrol ${configFile}";
       };
     };
   };
+
+  meta.maintainers = [ maintainers.evils ];
 }
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 222ac8e487e..51eca19dca3 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -11,8 +11,8 @@ let
     "fwupd/daemon.conf" = {
       source = pkgs.writeText "daemon.conf" ''
         [fwupd]
-        BlacklistDevices=${lib.concatStringsSep ";" cfg.blacklistDevices}
-        BlacklistPlugins=${lib.concatStringsSep ";" cfg.blacklistPlugins}
+        DisabledDevices=${lib.concatStringsSep ";" cfg.disabledDevices}
+        DisabledPlugins=${lib.concatStringsSep ";" cfg.disabledPlugins}
       '';
     };
     "fwupd/uefi.conf" = {
@@ -59,21 +59,21 @@ in {
         '';
       };
 
-      blacklistDevices = mkOption {
+      disabledDevices = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad" ];
         description = ''
-          Allow blacklisting specific devices by their GUID
+          Allow disabling specific devices by their GUID
         '';
       };
 
-      blacklistPlugins = mkOption {
+      disabledPlugins = mkOption {
         type = types.listOf types.str;
         default = [];
         example = [ "udev" ];
         description = ''
-          Allow blacklisting specific plugins
+          Allow disabling specific plugins
         '';
       };
 
@@ -105,11 +105,15 @@ in {
     };
   };
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "fwupd" "blacklistDevices"] [ "services" "fwupd" "disabledDevices" ])
+    (mkRenamedOptionModule [ "services" "fwupd" "blacklistPlugins"] [ "services" "fwupd" "disabledPlugins" ])
+  ];
 
   ###### 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;
+    services.fwupd.disabledPlugins = cfg.package.defaultDisabledPlugins;
 
     environment.systemPackages = [ cfg.package ];
 
diff --git a/nixos/modules/services/hardware/lcd.nix b/nixos/modules/services/hardware/lcd.nix
index d78d742cd31..dc8595ea60c 100644
--- a/nixos/modules/services/hardware/lcd.nix
+++ b/nixos/modules/services/hardware/lcd.nix
@@ -151,14 +151,13 @@ in with lib; {
         description = "LCDproc - client";
         after = [ "lcdd.service" ];
         wantedBy = [ "lcd.target" ];
+        # Allow restarting for eternity
+        startLimitIntervalSec = lib.mkIf cfg.client.restartForever 0;
         serviceConfig = serviceCfg // {
           ExecStart = "${pkg}/bin/lcdproc -f -c ${clientCfg}";
           # If the server is being restarted at the same time, the client will
           # fail as it cannot connect, so space it out a bit.
           RestartSec = "5";
-          # Allow restarting for eternity
-          StartLimitIntervalSec = lib.mkIf cfg.client.restartForever "0";
-          StartLimitBurst = lib.mkIf cfg.client.restartForever "0";
         };
       };
     };
diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix
index f3fc4c3cc79..4fc1e351f50 100644
--- a/nixos/modules/services/hardware/pcscd.nix
+++ b/nixos/modules/services/hardware/pcscd.nix
@@ -10,39 +10,37 @@ let
     paths = map (p: "${p}/pcsc/drivers") config.services.pcscd.plugins;
   };
 
-in {
+in
+{
 
   ###### interface
 
-  options = {
-
-    services.pcscd = {
-      enable = mkEnableOption "PCSC-Lite daemon";
-
-      plugins = mkOption {
-        type = types.listOf types.package;
-        default = [ pkgs.ccid ];
-        defaultText = "[ pkgs.ccid ]";
-        example = literalExample "[ pkgs.pcsc-cyberjack ]";
-        description = "Plugin packages to be used for PCSC-Lite.";
-      };
-
-      readerConfig = mkOption {
-        type = types.lines;
-        default = "";
-        example = ''
-          FRIENDLYNAME      "Some serial reader"
-          DEVICENAME        /dev/ttyS0
-          LIBPATH           /path/to/serial_reader.so
-          CHANNELID         1
-        '';
-        description = ''
-          Configuration for devices that aren't hotpluggable.
-
-          See <citerefentry><refentrytitle>reader.conf</refentrytitle>
-          <manvolnum>5</manvolnum></citerefentry> for valid options.
-        '';
-      };
+  options.services.pcscd = {
+    enable = mkEnableOption "PCSC-Lite daemon";
+
+    plugins = mkOption {
+      type = types.listOf types.package;
+      default = [ pkgs.ccid ];
+      defaultText = "[ pkgs.ccid ]";
+      example = literalExample "[ pkgs.pcsc-cyberjack ]";
+      description = "Plugin packages to be used for PCSC-Lite.";
+    };
+
+    readerConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        FRIENDLYNAME      "Some serial reader"
+        DEVICENAME        /dev/ttyS0
+        LIBPATH           /path/to/serial_reader.so
+        CHANNELID         1
+      '';
+      description = ''
+        Configuration for devices that aren't hotpluggable.
+
+        See <citerefentry><refentrytitle>reader.conf</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for valid options.
+      '';
     };
   };
 
@@ -50,20 +48,26 @@ in {
 
   config = mkIf config.services.pcscd.enable {
 
-    systemd.sockets.pcscd = {
-      description = "PCSC-Lite Socket";
-      wantedBy = [ "sockets.target" ];
-      before = [ "multi-user.target" ];
-      socketConfig.ListenStream = "/run/pcscd/pcscd.comm";
-    };
+    environment.etc."reader.conf".source = cfgFile;
+
+    environment.systemPackages = [ pkgs.pcsclite ];
+    systemd.packages = [ (getBin pkgs.pcsclite) ];
+
+    systemd.sockets.pcscd.wantedBy = [ "sockets.target" ];
 
     systemd.services.pcscd = {
-      description = "PCSC-Lite daemon";
       environment.PCSCLITE_HP_DROPDIR = pluginEnv;
-      serviceConfig = {
-        ExecStart = "${getBin pkgs.pcsclite}/sbin/pcscd -f -x -c ${cfgFile}";
-        ExecReload = "${getBin pkgs.pcsclite}/sbin/pcscd -H";
-      };
+      restartTriggers = [ "/etc/reader.conf" ];
+
+      # If the cfgFile is empty and not specified (in which case the default
+      # /etc/reader.conf is assumed), pcscd will happily start going through the
+      # entire confdir (/etc in our case) looking for a config file and try to
+      # parse everything it finds. Doesn't take a lot of imagination to see how
+      # well that works. It really shouldn't do that to begin with, but to work
+      # around it, we force the path to the cfgFile.
+      #
+      # https://github.com/NixOS/nixpkgs/issues/121088
+      serviceConfig.ExecStart = [ "" "${getBin pkgs.pcsclite}/bin/pcscd -f -x -c ${cfgFile}" ];
     };
   };
 }
diff --git a/nixos/modules/services/hardware/power-profiles-daemon.nix b/nixos/modules/services/hardware/power-profiles-daemon.nix
new file mode 100644
index 00000000000..70b7a72b8ba
--- /dev/null
+++ b/nixos/modules/services/hardware/power-profiles-daemon.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.power-profiles-daemon;
+  package = pkgs.power-profiles-daemon;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.power-profiles-daemon = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable power-profiles-daemon, a DBus daemon that allows
+          changing system behavior based upon user-selected power profiles.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = !config.services.tlp.enable;
+        message = ''
+          You have set services.power-profiles-daemon.enable = true;
+          which conflicts with services.tlp.enable = true;
+        '';
+      }
+    ];
+
+    services.dbus.packages = [ package ];
+
+    services.udev.packages = [ package ];
+
+    systemd.packages = [ package ];
+
+  };
+
+}
diff --git a/nixos/modules/services/hardware/sane.nix b/nixos/modules/services/hardware/sane.nix
index b344dfc2061..8c1bde7b415 100644
--- a/nixos/modules/services/hardware/sane.nix
+++ b/nixos/modules/services/hardware/sane.nix
@@ -4,9 +4,7 @@ with lib;
 
 let
 
-  pkg = if config.hardware.sane.snapshot
-    then pkgs.sane-backends-git
-    else pkgs.sane-backends;
+  pkg = pkgs.sane-backends;
 
   sanedConf = pkgs.writeTextFile {
     name = "saned.conf";
@@ -32,7 +30,7 @@ let
   };
 
   backends = [ pkg netConf ] ++ optional config.services.saned.enable sanedConf ++ config.hardware.sane.extraBackends;
-  saneConfig = pkgs.mkSaneConfig { paths = backends; };
+  saneConfig = pkgs.mkSaneConfig { paths = backends; inherit (config.hardware.sane) disabledDefaultBackends; };
 
   enabled = config.hardware.sane.enable || config.services.saned.enable;
 
@@ -75,6 +73,16 @@ in
       example = literalExample "[ pkgs.hplipWithPlugin ]";
     };
 
+    hardware.sane.disabledDefaultBackends = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "v4l" ];
+      description = ''
+        Names of backends which are enabled by default but should be disabled.
+        See <literal>$SANE_CONFIG_DIR/dll.conf</literal> for the list of possible names.
+      '';
+    };
+
     hardware.sane.configDir = mkOption {
       type = types.str;
       internal = true;
@@ -148,13 +156,14 @@ in
           # saned needs to distinguish between IPv4 and IPv6 to open matching data sockets.
           BindIPv6Only = "ipv6-only";
           Accept = true;
-          MaxConnections = 1;
+          MaxConnections = 64;
         };
       };
 
       users.users.scanner = {
         uid = config.ids.uids.scanner;
         group = "scanner";
+        extraGroups = [ "lp" ] ++ optionals config.services.avahi.enable [ "avahi" ];
       };
     })
   ];
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
index 6f49a1ab6d4..a6afa01dd81 100644
--- a/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix
@@ -81,7 +81,7 @@ in
         { office1 = { model = "MFC-7860DW"; ip = "192.168.1.2"; };
           office2 = { model = "MFC-7860DW"; nodename = "BRW0080927AFBCE"; };
         };
-      type = with types; loaOf (submodule netDeviceOpts);
+      type = with types; attrsOf (submodule netDeviceOpts);
       description = ''
         The list of network devices that will be registered against the brscan4
         sane backend.
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 ec0457bbd58..9d083a615a2 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
@@ -19,18 +19,16 @@ nix-shell -E 'with import <nixpkgs> { }; brscan4-etc-files.override{netDevices=[
 
 */
 
-with lib;
-
 let
 
   addNetDev = nd: ''
     brsaneconfig4 -a \
     name="${nd.name}" \
     model="${nd.model}" \
-    ${if (hasAttr "nodename" nd && nd.nodename != null) then
+    ${if (lib.hasAttr "nodename" nd && nd.nodename != null) then
       ''nodename="${nd.nodename}"'' else
       ''ip="${nd.ip}"''}'';
-  addAllNetDev = xs: concatStringsSep "\n" (map addNetDev xs);
+  addAllNetDev = xs: lib.concatStringsSep "\n" (map addNetDev xs);
 in
 
 stdenv.mkDerivation {
@@ -56,16 +54,15 @@ stdenv.mkDerivation {
     ${addAllNetDev netDevices}
   '';
 
-  installPhase = ":";
-
+  dontInstall = true;
   dontStrip = true;
   dontPatchELF = true;
 
-  meta = {
+  meta = with lib; {
     description = "Brother brscan4 sane backend driver etc files";
     homepage = "http://www.brother.com";
-    platforms = stdenv.lib.platforms.linux;
-    license = stdenv.lib.licenses.unfree;
-    maintainers = with stdenv.lib.maintainers; [ jraygauthier ];
+    platforms = platforms.linux;
+    license = licenses.unfree;
+    maintainers = with maintainers; [ jraygauthier ];
   };
 }
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan5.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan5.nix
new file mode 100644
index 00000000000..89b5ff0e028
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan5.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.hardware.sane.brscan5;
+
+  netDeviceList = attrValues cfg.netDevices;
+
+  etcFiles = pkgs.callPackage ./brscan5_etc_files.nix { netDevices = netDeviceList; };
+
+  netDeviceOpts = { name, ... }: {
+
+    options = {
+
+      name = mkOption {
+        type = types.str;
+        description = ''
+          The friendly name you give to the network device. If undefined,
+          the name of attribute will be used.
+        '';
+
+        example = literalExample "office1";
+      };
+
+      model = mkOption {
+        type = types.str;
+        description = ''
+          The model of the network device.
+        '';
+
+        example = literalExample "ADS-1200";
+      };
+
+      ip = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The ip address of the device. If undefined, you will have to
+          provide a nodename.
+        '';
+
+        example = literalExample "192.168.1.2";
+      };
+
+      nodename = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = ''
+          The node name of the device. If undefined, you will have to
+          provide an ip.
+        '';
+
+        example = literalExample "BRW0080927AFBCE";
+      };
+
+    };
+
+
+    config =
+      { name = mkDefault name;
+      };
+  };
+
+in
+
+{
+  options = {
+
+    hardware.sane.brscan5.enable =
+      mkEnableOption "the Brother brscan5 sane backend";
+
+    hardware.sane.brscan5.netDevices = mkOption {
+      default = {};
+      example =
+        { office1 = { model = "MFC-7860DW"; ip = "192.168.1.2"; };
+          office2 = { model = "MFC-7860DW"; nodename = "BRW0080927AFBCE"; };
+        };
+      type = with types; attrsOf (submodule netDeviceOpts);
+      description = ''
+        The list of network devices that will be registered against the brscan5
+        sane backend.
+      '';
+    };
+  };
+
+  config = mkIf (config.hardware.sane.enable && cfg.enable) {
+
+    hardware.sane.extraBackends = [
+      pkgs.brscan5
+    ];
+
+    environment.etc."opt/brother/scanner/brscan5" =
+      { source = "${etcFiles}/etc/opt/brother/scanner/brscan5"; };
+    environment.etc."opt/brother/scanner/models" =
+      { source = "${etcFiles}/etc/opt/brother/scanner/brscan5/models"; };
+    environment.etc."sane.d/dll.d/brother5.conf".source = "${pkgs.brscan5}/etc/sane.d/dll.d/brother.conf";
+
+    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.brscan5.netDevices`, only one of its `ip` or `nodename`
+          attribute should be specified, not both!
+        '';
+      }
+    ];
+
+  };
+}
diff --git a/nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix b/nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix
new file mode 100644
index 00000000000..432f0316a4f
--- /dev/null
+++ b/nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix
@@ -0,0 +1,77 @@
+{ stdenv, lib, brscan5, netDevices ? [] }:
+
+/*
+
+Testing
+-------
+From nixpkgs repo
+
+No net devices:
+
+~~~
+nix-build -E 'let pkgs = import ./. {};
+                  brscan5-etc-files = pkgs.callPackage (import ./nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix) {};
+              in brscan5-etc-files'
+~~~
+
+Two net devices:
+
+~~~
+nix-build -E 'let pkgs = import ./. {};
+                  brscan5-etc-files = pkgs.callPackage (import ./nixos/modules/services/hardware/sane_extra_backends/brscan5_etc_files.nix) {};
+              in brscan5-etc-files.override {
+                   netDevices = [
+                     {name="a"; model="ADS-1200"; nodename="BRW0080927AFBCE";}
+                     {name="b"; model="ADS-1200"; ip="192.168.1.2";}
+                   ];
+              }'
+~~~
+
+*/
+
+let
+
+  addNetDev = nd: ''
+    brsaneconfig5 -a \
+    name="${nd.name}" \
+    model="${nd.model}" \
+    ${if (lib.hasAttr "nodename" nd && nd.nodename != null) then
+      ''nodename="${nd.nodename}"'' else
+      ''ip="${nd.ip}"''}'';
+  addAllNetDev = xs: lib.concatStringsSep "\n" (map addNetDev xs);
+in
+
+stdenv.mkDerivation {
+
+  name = "brscan5-etc-files";
+  version = "1.2.6-0";
+  src = "${brscan5}/opt/brother/scanner/brscan5";
+
+  nativeBuildInputs = [ brscan5 ];
+
+  dontConfigure = true;
+
+  buildPhase = ''
+    TARGET_DIR="$out/etc/opt/brother/scanner/brscan5"
+    mkdir -p "$TARGET_DIR"
+    cp -rp "./models" "$TARGET_DIR"
+    cp -rp "./brscan5.ini" "$TARGET_DIR"
+    cp -rp "./brsanenetdevice.cfg" "$TARGET_DIR"
+
+    export NIX_REDIRECTS="/etc/opt/brother/scanner/brscan5/=$TARGET_DIR/"
+
+    printf '${addAllNetDev netDevices}\n'
+
+    ${addAllNetDev netDevices}
+  '';
+
+  dontInstall = true;
+
+  meta = with lib; {
+    description = "Brother brscan5 sane backend driver etc files";
+    homepage = "https://www.brother.com";
+    platforms = platforms.linux;
+    license = licenses.unfree;
+    maintainers = with maintainers; [ mattchrist ];
+  };
+}
diff --git a/nixos/modules/services/hardware/spacenavd.nix b/nixos/modules/services/hardware/spacenavd.nix
new file mode 100644
index 00000000000..74725dd23d2
--- /dev/null
+++ b/nixos/modules/services/hardware/spacenavd.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.hardware.spacenavd;
+
+in {
+
+  options = {
+    hardware.spacenavd = {
+      enable = mkEnableOption "spacenavd to support 3DConnexion devices";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.spacenavd = {
+      description = "Daemon for the Spacenavigator 6DOF mice by 3Dconnexion";
+      after = [ "syslog.target" ];
+      wantedBy = [ "graphical.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.spacenavd}/bin/spacenavd -d -l syslog";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/tcsd.nix b/nixos/modules/services/hardware/tcsd.nix
index 68cb5d791aa..0d36bce357b 100644
--- a/nixos/modules/services/hardware/tcsd.nix
+++ b/nixos/modules/services/hardware/tcsd.nix
@@ -119,22 +119,31 @@ in
 
     environment.systemPackages = [ pkgs.trousers ];
 
-#    system.activationScripts.tcsd =
-#      ''
-#        chown ${cfg.user}:${cfg.group} ${tcsdConf}
-#      '';
+    services.udev.extraRules = ''
+      # Give tcsd ownership of all TPM devices
+      KERNEL=="tpm[0-9]*", MODE="0660", OWNER="${cfg.user}", GROUP="${cfg.group}"
+      # Tag TPM devices to create a .device unit for tcsd to depend on
+      ACTION=="add", KERNEL=="tpm[0-9]*", TAG+="systemd"
+    '';
+
+    systemd.tmpfiles.rules = [
+      # Initialise the state directory
+      "d ${cfg.stateDir} 0770 ${cfg.user} ${cfg.group} - -"
+    ];
 
     systemd.services.tcsd = {
-      description = "TCSD";
-      after = [ "systemd-udev-settle.service" ];
+      description = "Manager for Trusted Computing resources";
+      documentation = [ "man:tcsd(8)" ];
+
+      requires = [ "dev-tpm0.device" ];
+      after = [ "dev-tpm0.device" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.trousers ];
-      preStart =
-        ''
-        mkdir -m 0700 -p ${cfg.stateDir}
-        chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
-        '';
-      serviceConfig.ExecStart = "${pkgs.trousers}/sbin/tcsd -f -c ${tcsdConf}";
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.trousers}/sbin/tcsd -f -c ${tcsdConf}";
+      };
     };
 
     users.users = optionalAttrs (cfg.user == "tss") {
diff --git a/nixos/modules/services/hardware/thermald.nix b/nixos/modules/services/hardware/thermald.nix
index ecb529e9bf0..aa936ac09d1 100644
--- a/nixos/modules/services/hardware/thermald.nix
+++ b/nixos/modules/services/hardware/thermald.nix
@@ -23,23 +23,31 @@ in {
         default = null;
         description = "the thermald manual configuration file.";
       };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.thermald;
+        defaultText = "pkgs.thermald";
+        description = "Which thermald package to use.";
+      };
     };
   };
 
   ###### implementation
   config = mkIf cfg.enable {
-    services.dbus.packages = [ pkgs.thermald ];
+    services.dbus.packages = [ cfg.package ];
 
     systemd.services.thermald = {
       description = "Thermal Daemon Service";
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.thermald}/sbin/thermald \
+          ${cfg.package}/sbin/thermald \
             --no-daemon \
             ${optionalString cfg.debug "--loglevel=debug"} \
             ${optionalString (cfg.configFile != null) "--config-file ${cfg.configFile}"} \
-            --dbus-enable
+            --dbus-enable \
+            --adaptive
         '';
       };
     };
diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix
index 3bda61ed1a9..7a5a7e1c41c 100644
--- a/nixos/modules/services/hardware/thinkfan.nix
+++ b/nixos/modules/services/hardware/thinkfan.nix
@@ -5,49 +5,95 @@ with lib;
 let
 
   cfg = config.services.thinkfan;
-  configFile = pkgs.writeText "thinkfan.conf" ''
-    # ATTENTION: There is only very basic sanity checking on the configuration.
-    # That means you can set your temperature limits as insane as you like. You
-    # can do anything stupid, e.g. turn off your fan when your CPU reaches 70°C.
-    #
-    # That's why this program is called THINKfan: You gotta think for yourself.
-    #
-    ######################################################################
-    #
-    # IBM/Lenovo Thinkpads (thinkpad_acpi, /proc/acpi/ibm)
-    # ====================================================
-    #
-    # IMPORTANT:
-    #
-    # To keep your HD from overheating, you have to specify a correction value for
-    # the sensor that has the HD's temperature. You need to do this because
-    # thinkfan uses only the highest temperature it can find in the system, and
-    # that'll most likely never be your HD, as most HDs are already out of spec
-    # when they reach 55 °C.
-    # Correction values are applied from left to right in the same order as the
-    # temperatures are read from the file.
-    #
-    # For example:
-    # tp_thermal /proc/acpi/ibm/thermal (0, 0, 10)
-    # will add a fixed value of 10 °C the 3rd value read from that file. Check out
-    # http://www.thinkwiki.org/wiki/Thermal_Sensors to find out how much you may
-    # want to add to certain temperatures.
-
-    ${cfg.fan}
-    ${cfg.sensors}
-
-    #  Syntax:
-    #  (LEVEL, LOW, HIGH)
-    #  LEVEL is the fan level to use (0-7 with thinkpad_acpi)
-    #  LOW is the temperature at which to step down to the previous level
-    #  HIGH is the temperature at which to step up to the next level
-    #  All numbers are integers.
-    #
-
-    ${cfg.levels}
-  '';
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "thinkfan.yaml" cfg.settings;
+  thinkfan = pkgs.thinkfan.override { inherit (cfg) smartSupport; };
+
+  # fan-speed and temperature levels
+  levelType = with types;
+    let
+      tuple = ts: mkOptionType {
+        name = "tuple";
+        merge = mergeOneOption;
+        check = xs: all id (zipListsWith (t: x: t.check x) ts xs);
+        description = "tuple of" + concatMapStrings (t: " (${t.description})") ts;
+      };
+      level = ints.unsigned;
+      special = enum [ "level auto" "level full-speed" "level disengage" ];
+    in
+      tuple [ (either level special) level level ];
+
+  # sensor or fan config
+  sensorType = name: types.submodule {
+    freeformType = types.attrsOf settingsFormat.type;
+    options = {
+      type = mkOption {
+        type = types.enum [ "hwmon" "atasmart" "tpacpi" "nvml" ];
+        description = ''
+          The ${name} type, can be
+          <literal>hwmon</literal> for standard ${name}s,
 
-  thinkfan = pkgs.thinkfan.override { smartSupport = cfg.smartSupport; };
+          <literal>atasmart</literal> to read the temperature via
+          S.M.A.R.T (requires smartSupport to be enabled),
+
+          <literal>tpacpi</literal> for the legacy thinkpac_acpi driver, or
+
+          <literal>nvml</literal> for the (proprietary) nVidia driver.
+        '';
+      };
+      query = mkOption {
+        type = types.str;
+        description = ''
+          The query string used to match one or more ${name}s: can be
+          a fullpath to the temperature file (single ${name}) or a fullpath
+          to a driver directory (multiple ${name}s).
+
+          <note><para>
+            When multiple ${name}s match, the query can be restricted using the
+            <option>name</option> or <option>indices</option> options.
+          </para></note>
+        '';
+      };
+      indices = mkOption {
+        type = with types; nullOr (listOf ints.unsigned);
+        default = null;
+        description = ''
+          A list of ${name}s to pick in case multiple ${name}s match the query.
+
+          <note><para>Indices start from 0.</para></note>
+        '';
+      };
+    } // optionalAttrs (name == "sensor") {
+      correction = mkOption {
+        type = with types; nullOr (listOf int);
+        default = null;
+        description = ''
+          A list of values to be added to the temperature of each sensor,
+          can be used to equalize small discrepancies in temperature ratings.
+        '';
+      };
+    };
+  };
+
+  # removes NixOS special and unused attributes
+  sensorToConf = { type, query, ... }@args:
+    (filterAttrs (k: v: v != null && !(elem k ["type" "query"])) args)
+    // { "${type}" = query; };
+
+  syntaxNote = name: ''
+    <note><para>
+      This section slightly departs from the thinkfan.conf syntax.
+      The type and path must be specified like this:
+      <literal>
+        type = "tpacpi";
+        query = "/proc/acpi/ibm/${name}";
+      </literal>
+      instead of a single declaration like:
+      <literal>
+        - tpacpi: /proc/acpi/ibm/${name}
+      </literal>
+    </para></note>
+  '';
 
 in {
 
@@ -59,76 +105,93 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable thinkfan, fan controller for IBM/Lenovo ThinkPads.
+          Whether to enable thinkfan, a fan control program.
+
+          <note><para>
+            This module targets IBM/Lenovo thinkpads by default, for
+            other hardware you will have configure it more carefully.
+          </para></note>
         '';
+        relatedPackages = [ "thinkfan" ];
       };
 
       smartSupport = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Whether to build thinkfan with SMART support to read temperatures
+          Whether to build thinkfan with S.M.A.R.T. support to read temperatures
           directly from hard disks.
         '';
       };
 
       sensors = mkOption {
-        type = types.lines;
-        default = ''
-          tp_thermal /proc/acpi/ibm/thermal (0,0,10)
-        '';
-        description =''
-          thinkfan can read temperatures from three possible sources:
-
-            /proc/acpi/ibm/thermal
-              Which is provided by the thinkpad_acpi kernel
-              module (keyword tp_thermal)
-
-            /sys/class/hwmon/*/temp*_input
-              Which may be provided by any hwmon drivers (keyword
-              hwmon)
-
-            S.M.A.R.T. (requires smartSupport to be enabled)
-              Which reads the temperature directly from the hard
-              disk using libatasmart (keyword atasmart)
-
-          Multiple sensors may be added, in which case they will be
-          numbered in their order of appearance.
-        '';
+        type = types.listOf (sensorType "sensor");
+        default = [
+          { type = "tpacpi";
+            query = "/proc/acpi/ibm/thermal";
+          }
+        ];
+        description = ''
+          List of temperature sensors thinkfan will monitor.
+        '' + syntaxNote "thermal";
       };
 
-      fan = mkOption {
-        type = types.str;
-        default = "tp_fan /proc/acpi/ibm/fan";
-        description =''
-          Specifies the fan we want to use.
-          On anything other than a Thinkpad you'll probably
-          use some PWM control file in /sys/class/hwmon.
-          A sysfs fan would be specified like this:
-            pwm_fan /sys/class/hwmon/hwmon2/device/pwm1
-        '';
+      fans = mkOption {
+        type = types.listOf (sensorType "fan");
+        default = [
+          { type = "tpacpi";
+            query = "/proc/acpi/ibm/fan";
+          }
+        ];
+        description = ''
+          List of fans thinkfan will control.
+        '' + syntaxNote "fan";
       };
 
       levels = mkOption {
-        type = types.lines;
-        default = ''
-          (0,     0,      55)
-          (1,     48,     60)
-          (2,     50,     61)
-          (3,     52,     63)
-          (6,     56,     65)
-          (7,     60,     85)
-          (127,   80,     32767)
-        '';
+        type = types.listOf levelType;
+        default = [
+          [0  0   55]
+          [1  48  60]
+          [2  50  61]
+          [3  52  63]
+          [6  56  65]
+          [7  60  85]
+          ["level auto" 80 32767]
+        ];
         description = ''
-          (LEVEL, LOW, HIGH)
-          LEVEL is the fan level to use (0-7 with thinkpad_acpi).
+          [LEVEL LOW HIGH]
+
+          LEVEL is the fan level to use: it can be an integer (0-7 with thinkpad_acpi),
+          "level auto" (to keep the default firmware behavior), "level full-speed" or
+          "level disengage" (to run the fan as fast as possible).
           LOW is the temperature at which to step down to the previous level.
           HIGH is the temperature at which to step up to the next level.
           All numbers are integers.
         '';
       };
 
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "-b" "0" ];
+        description = ''
+          A list of extra command line arguments to pass to thinkfan.
+          Check the thinkfan(1) manpage for available arguments.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrsOf settingsFormat.type;
+        default = { };
+        description = ''
+          Thinkfan settings. Use this option to configure thinkfan
+          settings not exposed in a NixOS option or to bypass one.
+          Before changing this, read the <literal>thinkfan.conf(5)</literal>
+          manpage and take a look at the example config file at
+          <link xlink:href="https://github.com/vmatare/thinkfan/blob/master/examples/thinkfan.yaml"/>
+        '';
+      };
 
     };
 
@@ -138,12 +201,21 @@ in {
 
     environment.systemPackages = [ thinkfan ];
 
-    systemd.services.thinkfan = {
-      description = "Thinkfan";
-      after = [ "basic.target" ];
-      wantedBy = [ "multi-user.target" ];
-      path = [ thinkfan ];
-      serviceConfig.ExecStart = "${thinkfan}/bin/thinkfan -n -c ${configFile}";
+    services.thinkfan.settings = mapAttrs (k: v: mkDefault v) {
+      sensors = map sensorToConf cfg.sensors;
+      fans    = map sensorToConf cfg.fans;
+      levels  = cfg.levels;
+    };
+
+    systemd.packages = [ thinkfan ];
+
+    systemd.services = {
+      thinkfan.environment.THINKFAN_ARGS = escapeShellArgs ([ "-c" configFile ] ++ cfg.extraArgs);
+
+      # must be added manually, see issue #81138
+      thinkfan.wantedBy = [ "multi-user.target" ];
+      thinkfan-wakeup.wantedBy = [ "sleep.target" ];
+      thinkfan-sleep.wantedBy = [ "sleep.target" ];
     };
 
     boot.extraModprobeConfig = "options thinkpad_acpi experimental=1 fan_control=1";
diff --git a/nixos/modules/services/hardware/throttled.nix b/nixos/modules/services/hardware/throttled.nix
index 7617c4492d7..1905eb565c6 100644
--- a/nixos/modules/services/hardware/throttled.nix
+++ b/nixos/modules/services/hardware/throttled.nix
@@ -26,5 +26,11 @@ in {
       if cfg.extraConfig != ""
       then pkgs.writeText "lenovo_fix.conf" cfg.extraConfig
       else "${pkgs.throttled}/etc/lenovo_fix.conf";
+
+    # Kernel 5.9 spams warnings whenever userspace writes to CPU MSRs.
+    # See https://github.com/erpalma/throttled/issues/215
+    boot.kernelParams =
+      optional (versionAtLeast config.boot.kernelPackages.kernel.version "5.9")
+      "msr.allow_writes=on";
   };
 }
diff --git a/nixos/modules/services/hardware/tlp.nix b/nixos/modules/services/hardware/tlp.nix
index 4230f2edd27..eb53f565a67 100644
--- a/nixos/modules/services/hardware/tlp.nix
+++ b/nixos/modules/services/hardware/tlp.nix
@@ -39,7 +39,7 @@ in
         default = "";
         description = ''
           Verbatim additional configuration variables for TLP.
-          DEPRECATED: use services.tlp.config instead.
+          DEPRECATED: use services.tlp.settings instead.
         '';
       };
     };
diff --git a/nixos/modules/services/hardware/trezord.nix b/nixos/modules/services/hardware/trezord.nix
index 2594ac74371..a65d4250c2e 100644
--- a/nixos/modules/services/hardware/trezord.nix
+++ b/nixos/modules/services/hardware/trezord.nix
@@ -47,8 +47,8 @@ in {
     services.udev.packages = [ pkgs.trezor-udev-rules ];
 
     systemd.services.trezord = {
-      description = "TREZOR Bridge";
-      after = [ "systemd-udev-settle.service" "network.target" ];
+      description = "Trezor Bridge";
+      after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       path = [];
       serviceConfig = {
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index 587b9b0234a..d48b5444677 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -57,8 +57,8 @@ let
         substituteInPlace $i \
           --replace \"/sbin/modprobe \"${pkgs.kmod}/bin/modprobe \
           --replace \"/sbin/mdadm \"${pkgs.mdadm}/sbin/mdadm \
-          --replace \"/sbin/blkid \"${pkgs.utillinux}/sbin/blkid \
-          --replace \"/bin/mount \"${pkgs.utillinux}/bin/mount \
+          --replace \"/sbin/blkid \"${pkgs.util-linux}/sbin/blkid \
+          --replace \"/bin/mount \"${pkgs.util-linux}/bin/mount \
           --replace /usr/bin/readlink ${pkgs.coreutils}/bin/readlink \
           --replace /usr/bin/basename ${pkgs.coreutils}/bin/basename
       done
@@ -202,10 +202,24 @@ in
         '';
       };
 
+      initrdRules = mkOption {
+        default = "";
+        example = ''
+          SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:1D:60:B9:6D:4F", KERNEL=="eth*", NAME="my_fast_network_card"
+        '';
+        type = types.lines;
+        description = ''
+          <command>udev</command> rules to include in the initrd
+          <emphasis>only</emphasis>. They'll be written into file
+          <filename>99-local.rules</filename>. Thus they are read and applied
+          after the essential initrd rules.
+        '';
+      };
+
       extraRules = mkOption {
         default = "";
         example = ''
-          KERNEL=="eth*", ATTR{address}=="00:1D:60:B9:6D:4F", NAME="my_fast_network_card"
+          ENV{ID_VENDOR_ID}=="046d", ENV{ID_MODEL_ID}=="0825", ENV{PULSE_IGNORE}="1"
         '';
         type = types.lines;
         description = ''
@@ -280,10 +294,17 @@ in
 
     services.udev.packages = [ extraUdevRules extraHwdbFile ];
 
-    services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.utillinux udev ];
+    services.udev.path = [ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.util-linux udev ];
 
     boot.kernelParams = mkIf (!config.networking.usePredictableInterfaceNames) [ "net.ifnames=0" ];
 
+    boot.initrd.extraUdevRulesCommands = optionalString (cfg.initrdRules != "")
+      ''
+        cat <<'EOF' > $out/99-local.rules
+        ${cfg.initrdRules}
+        EOF
+      '';
+
     environment.etc =
       {
         "udev/rules.d".source = udevRules;
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index 054ffa35050..9c2f78a755d 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -3,7 +3,12 @@
 with lib;
 let
   cfg = config.services.undervolt;
-  cliArgs = lib.cli.toGNUCommandLineShell {} {
+
+  mkPLimit = limit: window:
+    if (isNull limit && isNull window) then null
+    else assert asserts.assertMsg (!isNull limit && !isNull window) "Both power limit and window must be set";
+      "${toString limit} ${toString window}";
+  cliArgs = lib.cli.toGNUCommandLine {} {
     inherit (cfg)
       verbose
       temp
@@ -21,6 +26,9 @@ let
 
     temp-bat = cfg.tempBat;
     temp-ac = cfg.tempAc;
+
+    power-limit-long = mkPLimit cfg.p1.limit cfg.p1.window;
+    power-limit-short = mkPLimit cfg.p2.limit cfg.p2.window;
   };
 in
 {
@@ -104,6 +112,40 @@ in
       '';
     };
 
+    p1.limit = mkOption {
+      type = with types; nullOr int;
+      default = null;
+      description = ''
+        The P1 Power Limit in Watts.
+        Both limit and window must be set.
+      '';
+    };
+    p1.window = mkOption {
+      type = with types; nullOr (oneOf [ float int ]);
+      default = null;
+      description = ''
+        The P1 Time Window in seconds.
+        Both limit and window must be set.
+      '';
+    };
+
+    p2.limit = mkOption {
+      type = with types; nullOr int;
+      default = null;
+      description = ''
+        The P2 Power Limit in Watts.
+        Both limit and window must be set.
+      '';
+    };
+    p2.window = mkOption {
+      type = with types; nullOr (oneOf [ float int ]);
+      default = null;
+      description = ''
+        The P2 Time Window in seconds.
+        Both limit and window must be set.
+      '';
+    };
+
     useTimer = mkOption {
       type = types.bool;
       default = false;
@@ -133,7 +175,7 @@ in
       serviceConfig = {
         Type = "oneshot";
         Restart = "no";
-        ExecStart = "${pkgs.undervolt}/bin/undervolt ${cliArgs}";
+        ExecStart = "${pkgs.undervolt}/bin/undervolt ${toString cliArgs}";
       };
     };
 
diff --git a/nixos/modules/services/hardware/xow.nix b/nixos/modules/services/hardware/xow.nix
index a18d60ad83b..311181176bd 100644
--- a/nixos/modules/services/hardware/xow.nix
+++ b/nixos/modules/services/hardware/xow.nix
@@ -10,7 +10,10 @@ in {
   config = lib.mkIf cfg.enable {
     hardware.uinput.enable = true;
 
+    boot.extraModprobeConfig = lib.readFile "${pkgs.xow}/lib/modprobe.d/xow-blacklist.conf";
+
     systemd.packages = [ pkgs.xow ];
+    systemd.services.xow.wantedBy = [ "multi-user.target" ];
 
     services.udev.packages = [ pkgs.xow ];
   };
diff --git a/nixos/modules/services/logging/graylog.nix b/nixos/modules/services/logging/graylog.nix
index a889a44d4b2..af70d27fcf9 100644
--- a/nixos/modules/services/logging/graylog.nix
+++ b/nixos/modules/services/logging/graylog.nix
@@ -39,7 +39,6 @@ in
         type = types.package;
         default = pkgs.graylog;
         defaultText = "pkgs.graylog";
-        example = literalExample "pkgs.graylog";
         description = "Graylog package to use.";
       };
 
@@ -138,14 +137,13 @@ in
       "d '${cfg.messageJournalDir}' - ${cfg.user} - - -"
     ];
 
-    systemd.services.graylog = with pkgs; {
+    systemd.services.graylog = {
       description = "Graylog Server";
       wantedBy = [ "multi-user.target" ];
       environment = {
-        JAVA_HOME = jre;
         GRAYLOG_CONF = "${confFile}";
       };
-      path = [ pkgs.jre_headless pkgs.which pkgs.procps ];
+      path = [ pkgs.which pkgs.procps ];
       preStart = ''
         rm -rf /var/lib/graylog/plugins || true
         mkdir -p /var/lib/graylog/plugins -m 755
diff --git a/nixos/modules/services/logging/logstash.nix b/nixos/modules/services/logging/logstash.nix
index bf92425f998..7a2f5681612 100644
--- a/nixos/modules/services/logging/logstash.nix
+++ b/nixos/modules/services/logging/logstash.nix
@@ -100,7 +100,7 @@ in
 
       inputConfig = mkOption {
         type = types.lines;
-        default = ''generator { }'';
+        default = "generator { }";
         description = "Logstash input configuration.";
         example = ''
           # Read from journal
@@ -131,7 +131,7 @@ in
 
       outputConfig = mkOption {
         type = types.lines;
-        default = ''stdout { codec => rubydebug }'';
+        default = "stdout { codec => rubydebug }";
         description = "Logstash output configuration.";
         example = ''
           redis { host => ["localhost"] data_type => "list" key => "logstash" codec => json }
@@ -159,10 +159,9 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-    systemd.services.logstash = with pkgs; {
+    systemd.services.logstash = {
       description = "Logstash Daemon";
       wantedBy = [ "multi-user.target" ];
-      environment = { JAVA_HOME = jre; };
       path = [ pkgs.bash ];
       serviceConfig = {
         ExecStartPre = ''${pkgs.coreutils}/bin/mkdir -p "${cfg.dataDir}" ; ${pkgs.coreutils}/bin/chmod 700 "${cfg.dataDir}"'';
diff --git a/nixos/modules/services/logging/promtail.nix b/nixos/modules/services/logging/promtail.nix
new file mode 100644
index 00000000000..34211687dc1
--- /dev/null
+++ b/nixos/modules/services/logging/promtail.nix
@@ -0,0 +1,87 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.services.promtail;
+
+  prettyJSON = conf: pkgs.runCommandLocal "promtail-config.json" {} ''
+    echo '${builtins.toJSON conf}' | ${pkgs.buildPackages.jq}/bin/jq 'del(._module)' > $out
+  '';
+
+  allowSystemdJournal = cfg.configuration ? scrape_configs && lib.any (v: v ? journal) cfg.configuration.scrape_configs;
+in {
+  options.services.promtail = with types; {
+    enable = mkEnableOption "the Promtail ingresser";
+
+
+    configuration = mkOption {
+      type = (pkgs.formats.json {}).type;
+      description = ''
+        Specify the configuration for Promtail in Nix.
+      '';
+    };
+
+    extraFlags = mkOption {
+      type = listOf str;
+      default = [];
+      example = [ "--server.http-listen-port=3101" ];
+      description = ''
+        Specify a list of additional command line flags,
+        which get escaped and are then passed to Loki.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.promtail.configuration.positions.filename = mkDefault "/var/cache/promtail/positions.yaml";
+
+    systemd.services.promtail = {
+      description = "Promtail log ingress";
+      wantedBy = [ "multi-user.target" ];
+      stopIfChanged = false;
+
+      serviceConfig = {
+        Restart = "on-failure";
+        TimeoutStopSec = 10;
+
+        ExecStart = "${pkgs.grafana-loki}/bin/promtail -config.file=${prettyJSON cfg.configuration} ${escapeShellArgs cfg.extraFlags}";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        CacheDirectory = "promtail";
+
+        User = "promtail";
+        Group = "promtail";
+
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+
+        ProtectKernelModules = true;
+        SystemCallArchitectures = "native";
+        ProtectKernelLogs = true;
+        ProtectClock = true;
+
+        LockPersonality = true;
+        ProtectHostname = true;
+        RestrictRealtime = true;
+        MemoryDenyWriteExecute = true;
+        PrivateUsers = true;
+
+        SupplementaryGroups = lib.optional (allowSystemdJournal) "systemd-journal";
+      } // (optionalAttrs (!pkgs.stdenv.isAarch64) { # FIXME: figure out why this breaks on aarch64
+        SystemCallFilter = "@system-service";
+      });
+    };
+
+    users.groups.promtail = {};
+    users.users.promtail = {
+      description = "Promtail service user";
+      isSystemUser = true;
+      group = "promtail";
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/vector.nix b/nixos/modules/services/logging/vector.nix
new file mode 100644
index 00000000000..be36b2a41bb
--- /dev/null
+++ b/nixos/modules/services/logging/vector.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let cfg = config.services.vector;
+
+in
+{
+  options.services.vector = {
+    enable = mkEnableOption "Vector";
+
+    journaldAccess = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable Vector to access journald.
+      '';
+    };
+
+    settings = mkOption {
+      type = (pkgs.formats.json { }).type;
+      default = { };
+      description = ''
+        Specify the configuration for Vector in Nix.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups.vector = { };
+    users.users.vector = {
+      description = "Vector service user";
+      group = "vector";
+      isSystemUser = true;
+    };
+    systemd.services.vector = {
+      description = "Vector event and log aggregator";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      requires = [ "network-online.target" ];
+      serviceConfig =
+        let
+          format = pkgs.formats.toml { };
+          conf = format.generate "vector.toml" cfg.settings;
+          validateConfig = file:
+            pkgs.runCommand "validate-vector-conf" { } ''
+              ${pkgs.vector}/bin/vector validate --no-environment "${file}"
+              ln -s "${file}" "$out"
+            '';
+        in
+        {
+          ExecStart = "${pkgs.vector}/bin/vector --config ${validateConfig conf}";
+          User = "vector";
+          Group = "vector";
+          Restart = "no";
+          StateDirectory = "vector";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+          # This group is required for accessing journald.
+          SupplementaryGroups = mkIf cfg.journaldAccess "systemd-journal";
+        };
+    };
+  };
+}
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index c166ef68f29..1ccfb357750 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -84,11 +84,9 @@ let
 
     (
       optionalString (cfg.mailboxes != {}) ''
-        protocol imap {
-          namespace inbox {
-            inbox=yes
-            ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
-          }
+        namespace inbox {
+          inbox=yes
+          ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
         }
       ''
     )
@@ -407,7 +405,7 @@ in
         };
     } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
       ${cfg.mailUser} =
-        { description = "Virtual Mail User"; } // optionalAttrs (cfg.mailGroup != null)
+        { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
           { group = cfg.mailGroup; };
     };
 
@@ -429,12 +427,12 @@ in
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ cfg.configFile modulesDir ];
 
+      startLimitIntervalSec = 60;  # 1 min
       serviceConfig = {
         ExecStart = "${dovecotPkg}/sbin/dovecot -F";
         ExecReload = "${dovecotPkg}/sbin/doveadm reload";
         Restart = "on-failure";
         RestartSec = "1s";
-        StartLimitInterval = "1min";
         RuntimeDirectory = [ "dovecot2" ];
       };
 
@@ -465,7 +463,7 @@ in
     environment.systemPackages = [ dovecotPkg ];
 
     warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
-      "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.03! See the release notes for more info for migration."
+      "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration."
     ];
 
     assertions = [
diff --git a/nixos/modules/services/mail/exim.nix b/nixos/modules/services/mail/exim.nix
index 892fbd33214..8927d84b478 100644
--- a/nixos/modules/services/mail/exim.nix
+++ b/nixos/modules/services/mail/exim.nix
@@ -67,6 +67,13 @@ in
         '';
       };
 
+      queueRunnerInterval = mkOption {
+        type = types.str;
+        default = "5m";
+        description = ''
+          How often to spawn a new queue runner.
+        '';
+      };
     };
 
   };
@@ -104,7 +111,7 @@ in
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."exim.conf".source ];
       serviceConfig = {
-        ExecStart   = "${cfg.package}/bin/exim -bdf -q30m";
+        ExecStart   = "${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
         ExecReload  = "${coreutils}/bin/kill -HUP $MAINPID";
       };
       preStart = ''
diff --git a/nixos/modules/services/mail/freepops.nix b/nixos/modules/services/mail/freepops.nix
deleted file mode 100644
index 5b729ca50a5..00000000000
--- a/nixos/modules/services/mail/freepops.nix
+++ /dev/null
@@ -1,89 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.mail.freepopsd;
-in
-
-{
-  options = {
-    services.mail.freepopsd = {
-      enable = mkOption {
-        default = false;
-        type = with types; bool;
-        description = ''
-          Enables Freepops, a POP3 webmail wrapper.
-        '';
-      };
-
-      port = mkOption {
-        default = 2000;
-        type = with types; uniq int;
-        description = ''
-          Port on which the pop server will listen.
-        '';
-      };
-
-      threads = mkOption {
-        default = 5;
-        type = with types; uniq int;
-        description = ''
-          Max simultaneous connections.
-        '';
-      };
-
-      bind = mkOption {
-        default = "0.0.0.0";
-        type = types.str;
-        description = ''
-          Bind over an IPv4 address instead of any.
-        '';
-      };
-
-      logFile = mkOption {
-        default = "/var/log/freepopsd";
-        example = "syslog";
-        type = types.str;
-        description = ''
-          Filename of the log file or syslog to rely on the logging daemon.
-        '';
-      };
-
-      suid = {
-        user = mkOption {
-          default = "nobody";
-          type = types.str;
-          description = ''
-            User name under which freepopsd will be after binding the port.
-          '';
-        };
-
-        group = mkOption {
-          default = "nogroup";
-          type = types.str;
-          description = ''
-            Group under which freepopsd will be after binding the port.
-          '';
-        };
-      };
-
-    };
-  };
-
-  config = mkIf cfg.enable {
-    systemd.services.freepopsd = {
-      description = "Freepopsd (webmail over POP3)";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      script = ''
-        ${pkgs.freepops}/bin/freepopsd \
-          -p ${toString cfg.port} \
-          -t ${toString cfg.threads} \
-          -b ${cfg.bind} \
-          -vv -l ${cfg.logFile} \
-          -s ${cfg.suid.user}.${cfg.suid.group}
-      '';
-    };
-  };
-}
diff --git a/nixos/modules/services/mail/mailhog.nix b/nixos/modules/services/mail/mailhog.nix
index 0f998c6d0ea..b113f4ff3de 100644
--- a/nixos/modules/services/mail/mailhog.nix
+++ b/nixos/modules/services/mail/mailhog.nix
@@ -4,17 +4,59 @@ with lib;
 
 let
   cfg = config.services.mailhog;
-in {
+
+  args = lib.concatStringsSep " " (
+    [
+      "-api-bind-addr :${toString cfg.apiPort}"
+      "-smtp-bind-addr :${toString cfg.smtpPort}"
+      "-ui-bind-addr :${toString cfg.uiPort}"
+      "-storage ${cfg.storage}"
+    ] ++ lib.optional (cfg.storage == "maildir")
+      "-maildir-path $STATE_DIRECTORY"
+    ++ cfg.extraArgs
+  );
+
+in
+{
   ###### interface
 
+  imports = [
+    (mkRemovedOptionModule [ "services" "mailhog" "user" ] "")
+  ];
+
   options = {
 
     services.mailhog = {
       enable = mkEnableOption "MailHog";
-      user = mkOption {
-        type = types.str;
-        default = "mailhog";
-        description = "User account under which mailhog runs.";
+
+      storage = mkOption {
+        type = types.enum [ "maildir" "memory" ];
+        default = "memory";
+        description = "Store mails on disk or in memory.";
+      };
+
+      apiPort = mkOption {
+        type = types.port;
+        default = 8025;
+        description = "Port on which the API endpoint will listen.";
+      };
+
+      smtpPort = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "Port on which the SMTP endpoint will listen.";
+      };
+
+      uiPort = mkOption {
+        type = types.port;
+        default = 8025;
+        description = "Port on which the HTTP UI will listen.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "List of additional arguments to pass to the MailHog process.";
       };
     };
   };
@@ -24,20 +66,16 @@ in {
 
   config = mkIf cfg.enable {
 
-    users.users.mailhog = {
-      name = cfg.user;
-      description = "MailHog service user";
-      isSystemUser = true;
-    };
-
     systemd.services.mailhog = {
-      description = "MailHog service";
+      description = "MailHog - Web and API based SMTP testing";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        Type = "simple";
-        ExecStart = "${pkgs.mailhog}/bin/MailHog";
-        User = cfg.user;
+        Type = "exec";
+        ExecStart = "${pkgs.mailhog}/bin/MailHog ${args}";
+        DynamicUser = true;
+        Restart = "on-failure";
+        StateDirectory = "mailhog";
       };
     };
   };
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 5c61cfbebf6..831175d5625 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -38,7 +38,7 @@ let
   webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings);
 
   # TODO: Should this be RFC42-ised so that users can set additional options without modifying the module?
-  mtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
+  postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
     [postfix]
     postmap_command: ${pkgs.postfix}/bin/postmap
     transport_file_type: hash
@@ -81,7 +81,7 @@ in {
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = "Enable Mailman on this host. Requires an active Postfix installation.";
+        description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
       };
 
       package = mkOption {
@@ -92,6 +92,20 @@ in {
         description = "Mailman package to use";
       };
 
+      enablePostfix = mkOption {
+        type = types.bool;
+        default = true;
+        example = false;
+        description = ''
+          Enable Postfix integration. Requires an active Postfix installation.
+
+          If you want to use another MTA, set this option to false and configure
+          settings in services.mailman.settings.mta.
+
+          Refer to the Mailman manual for more info.
+        '';
+      };
+
       siteOwner = mkOption {
         type = types.str;
         example = "postmaster@example.org";
@@ -151,7 +165,7 @@ in {
 
         baseUrl = mkOption {
           type = types.str;
-          default = "http://localhost/hyperkitty/";
+          default = "http://localhost:18507/archives/";
           description = ''
             Where can Mailman connect to Hyperkitty's internal API, preferably on
             localhost?
@@ -182,7 +196,7 @@ in {
         pid_file = "/run/mailman/master.pid";
       };
 
-      mta.configuration = lib.mkDefault "${mtaConfig}";
+      mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA.");
 
       "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
         class = "mailman_hyperkitty.Archiver";
@@ -211,14 +225,22 @@ in {
               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
             '';
           };
-    in [
+    in (lib.optionals cfg.enablePostfix [
       { assertion = postfix.enable;
-        message = "Mailman requires Postfix";
+        message = ''
+          Mailman's default NixOS configuration requires Postfix to be enabled.
+
+          If you want to use another MTA, set services.mailman.enablePostfix
+          to false and configure settings in services.mailman.settings.mta.
+
+          Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
+          for more info.
+        '';
       }
       (requirePostfixHash [ "relayDomains" ] "postfix_domains")
       (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
       (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
-    ];
+    ]);
 
     users.users.mailman = {
       description = "GNU Mailman";
@@ -241,7 +263,8 @@ in {
       # settings_local.json is loaded.
       os.environ["SECRET_KEY"] = ""
 
-      from mailman_web.settings import *
+      from mailman_web.settings.base import *
+      from mailman_web.settings.mailman import *
 
       import json
 
@@ -275,7 +298,7 @@ in {
       '';
     }) ];
 
-    services.postfix = {
+    services.postfix = lib.mkIf cfg.enablePostfix {
       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
       config = {
         owner_request_special = "no";   # Mailman handles -owner addresses on its own
@@ -310,6 +333,7 @@ in {
         before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
         requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
         path = with pkgs; [ jq ];
+        serviceConfig.Type = "oneshot";
         script = ''
           mailmanDir=/var/lib/mailman
           mailmanWebDir=/var/lib/mailman-web
@@ -345,7 +369,7 @@ in {
 
       mailman-web-setup = {
         description = "Prepare mailman-web files and database";
-        before = [ "uwsgi.service" "mailman-uwsgi.service" ];
+        before = [ "mailman-uwsgi.service" ];
         requiredBy = [ "mailman-uwsgi.service" ];
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         script = ''
@@ -368,6 +392,7 @@ in {
           plugins = ["python3"];
           home = pythonEnv;
           module = "mailman_web.wsgi";
+          http = "127.0.0.1:18507";
         };
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
       in {
@@ -421,7 +446,7 @@ in {
         inherit startAt;
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         serviceConfig = {
-          ExecStart = "${pythonEnv}/bin/mailman-web runjobs minutely";
+          ExecStart = "${pythonEnv}/bin/mailman-web runjobs ${name}";
           User = cfg.webUser;
           Group = "mailman";
           WorkingDirectory = "/var/lib/mailman-web";
@@ -430,7 +455,7 @@ in {
   };
 
   meta = {
-    maintainers = with lib.maintainers; [ lheckemann ];
+    maintainers = with lib.maintainers; [ lheckemann qyliss ];
     doc = ./mailman.xml;
   };
 
diff --git a/nixos/modules/services/mail/mailman.xml b/nixos/modules/services/mail/mailman.xml
index cbe50ed0b91..27247fb064f 100644
--- a/nixos/modules/services/mail/mailman.xml
+++ b/nixos/modules/services/mail/mailman.xml
@@ -13,9 +13,9 @@
   </para>
 
   <section xml:id="module-services-mailman-basic-usage">
-    <title>Basic usage</title>
+    <title>Basic usage with Postfix</title>
     <para>
-      For a basic configuration, the following settings are suggested:
+      For a basic configuration with Postfix as the MTA, the following settings are suggested:
       <programlisting>{ config, ... }: {
   services.postfix = {
     enable = true;
@@ -31,11 +31,11 @@
     <link linkend="opt-services.mailman.enable">enable</link> = true;
     <link linkend="opt-services.mailman.serve.enable">serve.enable</link> = true;
     <link linkend="opt-services.mailman.hyperkitty.enable">hyperkitty.enable</link> = true;
-    <link linkend="opt-services.mailman.hyperkitty.enable">webHosts</link> = ["lists.example.org"];
-    <link linkend="opt-services.mailman.hyperkitty.enable">siteOwner</link> = "mailman@example.org";
+    <link linkend="opt-services.mailman.webHosts">webHosts</link> = ["lists.example.org"];
+    <link linkend="opt-services.mailman.siteOwner">siteOwner</link> = "mailman@example.org";
   };
   <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">services.nginx.virtualHosts."lists.example.org".enableACME</link> = true;
-  <link linkend="opt-services.mailman.hyperkitty.enable">networking.firewall.allowedTCPPorts</link> = [ 25 80 443 ];
+  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 25 80 443 ];
 }</programlisting>
     </para>
     <para>
@@ -56,4 +56,39 @@
       necessary, but outside the scope of the Mailman module.
     </para>
   </section>
+  <section xml:id="module-services-mailman-other-mtas">
+    <title>Using with other MTAs</title>
+    <para>
+      Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
+      <programlisting>{ config, ... }: {
+  services = {
+    mailman = {
+      enable = true;
+      siteOwner = "mailman@example.org";
+      <link linkend="opt-services.mailman.enablePostfix">enablePostfix</link> = false;
+      settings.mta = {
+        incoming = "mailman.mta.exim4.LMTP";
+        outgoing = "mailman.mta.deliver.deliver";
+        lmtp_host = "localhost";
+        lmtp_port = "8024";
+        smtp_host = "localhost";
+        smtp_port = "25";
+        configuration = "python:mailman.config.exim4";
+      };
+    };
+    exim = {
+      enable = true;
+      # You can configure Exim in a separate file to reduce configuration.nix clutter
+      config = builtins.readFile ./exim.conf;
+    };
+  };
+}</programlisting>
+    </para>
+    <para>
+      The exim config needs some special additions to work with Mailman. Currently
+      NixOS can't manage Exim config with such granularity. Please refer to
+      <link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman documentation</link>
+      for more info on configuring Mailman for working with Exim.
+    </para>
+  </section>
 </chapter>
diff --git a/nixos/modules/services/mail/mlmmj.nix b/nixos/modules/services/mail/mlmmj.nix
index d58d93c4214..fd74f2dc5f0 100644
--- a/nixos/modules/services/mail/mlmmj.nix
+++ b/nixos/modules/services/mail/mlmmj.nix
@@ -16,7 +16,14 @@ let
   alias = domain: list: "${list}: \"|${pkgs.mlmmj}/bin/mlmmj-receive -L ${listDir domain list}/\"";
   subjectPrefix = list: "[${list}]";
   listAddress = domain: list: "${list}@${domain}";
-  customHeaders = domain: list: [ "List-Id: ${list}" "Reply-To: ${list}@${domain}" ];
+  customHeaders = domain: list: [
+    "List-Id: ${list}"
+    "Reply-To: ${list}@${domain}"
+    "List-Post: <mailto:${list}@${domain}>"
+    "List-Help: <mailto:${list}+help@${domain}>"
+    "List-Subscribe: <mailto:${list}+subscribe@${domain}>"
+    "List-Unsubscribe: <mailto:${list}+unsubscribe@${domain}>"
+  ];
   footer = domain: list: "To unsubscribe send a mail to ${list}+unsubscribe@${domain}";
   createList = d: l:
     let ctlDir = listCtl d l; in
@@ -110,17 +117,29 @@ in
     services.postfix = {
       enable = true;
       recipientDelimiter= "+";
-      extraMasterConf = ''
-        mlmmj unix - n n - - pipe flags=ORhu user=mlmmj argv=${pkgs.mlmmj}/bin/mlmmj-receive -F -L ${spoolDir}/$nexthop
-      '';
+      masterConfig.mlmmj = {
+        type = "unix";
+        private = true;
+        privileged = true;
+        chroot = false;
+        wakeup = 0;
+        command = "pipe";
+        args = [
+          "flags=ORhu"
+          "user=mlmmj"
+          "argv=${pkgs.mlmmj}/bin/mlmmj-receive"
+          "-F"
+          "-L"
+          "${spoolDir}/$nexthop"
+        ];
+      };
 
       extraAliases = concatMapLines (alias cfg.listDomain) cfg.mailLists;
 
-      extraConfig = ''
-        transport_maps = hash:${stateDir}/transports
-        virtual_alias_maps = hash:${stateDir}/virtuals
-        propagate_unmatched_extensions = virtual
-      '';
+      extraConfig = "propagate_unmatched_extensions = virtual";
+
+      virtual = concatMapLines (virtual cfg.listDomain) cfg.mailLists;
+      transport = concatMapLines (transport cfg.listDomain) cfg.mailLists;
     };
 
     environment.systemPackages = [ pkgs.mlmmj ];
@@ -129,10 +148,8 @@ in
           ${pkgs.coreutils}/bin/mkdir -p ${stateDir} ${spoolDir}/${cfg.listDomain}
           ${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${spoolDir}
           ${concatMapLines (createList cfg.listDomain) cfg.mailLists}
-          echo "${concatMapLines (virtual cfg.listDomain) cfg.mailLists}" > ${stateDir}/virtuals
-          echo "${concatMapLines (transport cfg.listDomain) cfg.mailLists}" > ${stateDir}/transports
-          ${pkgs.postfix}/bin/postmap ${stateDir}/virtuals
-          ${pkgs.postfix}/bin/postmap ${stateDir}/transports
+          ${pkgs.postfix}/bin/postmap /etc/postfix/virtual
+          ${pkgs.postfix}/bin/postmap /etc/postfix/transport
       '';
 
     systemd.services.mlmmj-maintd = {
diff --git a/nixos/modules/services/mail/nullmailer.nix b/nixos/modules/services/mail/nullmailer.nix
index fe3f8ef9b39..09874ca0ed7 100644
--- a/nixos/modules/services/mail/nullmailer.nix
+++ b/nixos/modules/services/mail/nullmailer.nix
@@ -204,6 +204,7 @@ with lib;
       users.${cfg.user} = {
         description = "Nullmailer relay-only mta user";
         group = cfg.group;
+        isSystemUser = true;
       };
 
       groups.${cfg.group} = { };
diff --git a/nixos/modules/services/mail/opendkim.nix b/nixos/modules/services/mail/opendkim.nix
index eb6a426684d..beff57613af 100644
--- a/nixos/modules/services/mail/opendkim.nix
+++ b/nixos/modules/services/mail/opendkim.nix
@@ -129,6 +129,36 @@ in {
         User = cfg.user;
         Group = cfg.group;
         RuntimeDirectory = optional (cfg.socket == defaultSock) "opendkim";
+        StateDirectory = "opendkim";
+        StateDirectoryMode = "0700";
+        ReadWritePaths = [ cfg.keyPath ];
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6 AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
+        UMask = "0077";
       };
     };
 
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index fd4d16cdc37..9b0a5bba2fe 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -11,6 +11,7 @@ let
 
   haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != ""
                       || cfg.extraAliases != "";
+  haveCanonical = cfg.canonical != "";
   haveTransport = cfg.transport != "";
   haveVirtual = cfg.virtual != "";
   haveLocalRecipients = cfg.localRecipients != null;
@@ -25,8 +26,6 @@ let
 
   clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
 
-  smtpTlsSecurityLevel = if cfg.useDane then "dane" else "may";
-
   mainCf = let
     escape = replaceStrings ["$"] ["$$"];
     mkList = items: "\n  " + concatStringsSep ",\n  " items;
@@ -52,7 +51,7 @@ let
       };
 
       type = mkOption {
-        type = types.enum [ "inet" "unix" "fifo" "pass" ];
+        type = types.enum [ "inet" "unix" "unix-dgram" "fifo" "pass" ];
         default = "unix";
         example = "inet";
         description = "The type of the service";
@@ -195,7 +194,7 @@ let
       # We need to handle the last column specially here, because it's
       # open-ended (command + args).
       lines = [ labels labelDefaults ] ++ (map (l: init l ++ [""]) masterCf);
-    in fold foldLine (genList (const 0) (length labels)) lines;
+    in foldr foldLine (genList (const 0) (length labels)) lines;
 
     # Pad a string with spaces from the right (opposite of fixedWidthString).
     pad = width: str: let
@@ -204,7 +203,7 @@ let
     in str + optionalString (padWidth > 0) padding;
 
     # It's + 2 here, because that's the amount of spacing between columns.
-    fullWidth = fold (width: acc: acc + width + 2) 0 maxWidths;
+    fullWidth = foldr (width: acc: acc + width + 2) 0 maxWidths;
 
     formatLine = line: concatStringsSep "  " (zipListsWith pad maxWidths line);
 
@@ -246,6 +245,7 @@ let
   ;
 
   aliasesFile = pkgs.writeText "postfix-aliases" aliases;
+  canonicalFile = pkgs.writeText "postfix-canonical" cfg.canonical;
   virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
   localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" (concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients);
   checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
@@ -510,14 +510,6 @@ in
         '';
       };
 
-      useDane = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Sets smtp_tls_security_level to "dane" rather than "may". See postconf(5) for details.
-        '';
-      };
-
       sslCert = mkOption {
         type = types.str;
         default = "";
@@ -539,6 +531,15 @@ in
         ";
       };
 
+      canonical = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Entries for the <citerefentry><refentrytitle>canonical</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> table.
+        '';
+      };
+
       virtual = mkOption {
         type = types.lines;
         default = "";
@@ -570,6 +571,7 @@ in
 
       transport = mkOption {
         default = "";
+        type = types.lines;
         description = "
           Entries for the transport map, cf. man-page transport(8).
         ";
@@ -583,6 +585,7 @@ in
 
       dnsBlacklistOverrides = mkOption {
         default = "";
+        type = types.lines;
         description = "contents of check_client_access for overriding dnsBlacklists";
       };
 
@@ -770,7 +773,7 @@ in
         };
 
       services.postfix.config = (mapAttrs (_: v: mkDefault v) {
-        compatibility_level  = "9999";
+        compatibility_level  = pkgs.postfix.version;
         mail_owner           = cfg.user;
         default_privs        = "nobody";
 
@@ -819,13 +822,13 @@ in
       // optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; }
       // optionalAttrs (cfg.tlsTrustedAuthorities != "") {
         smtp_tls_CAfile = cfg.tlsTrustedAuthorities;
-        smtp_tls_security_level = smtpTlsSecurityLevel;
+        smtp_tls_security_level = mkDefault "may";
       }
       // optionalAttrs (cfg.sslCert != "") {
         smtp_tls_cert_file = cfg.sslCert;
         smtp_tls_key_file = cfg.sslKey;
 
-        smtp_tls_security_level = smtpTlsSecurityLevel;
+        smtp_tls_security_level = mkDefault "may";
 
         smtpd_tls_cert_file = cfg.sslCert;
         smtpd_tls_key_file = cfg.sslKey;
@@ -834,12 +837,6 @@ in
       };
 
       services.postfix.masterConfig = {
-        smtp_inet = {
-          name = "smtp";
-          type = "inet";
-          private = false;
-          command = "smtpd";
-        };
         pickup = {
           private = false;
           wakeup = 60;
@@ -921,6 +918,12 @@ in
           in concatLists (mapAttrsToList mkKeyVal cfg.submissionOptions);
         };
       } // optionalAttrs cfg.enableSmtp {
+        smtp_inet = {
+          name = "smtp";
+          type = "inet";
+          private = false;
+          command = "smtpd";
+        };
         smtp = {};
         relay = {
           command = "smtp";
@@ -949,6 +952,9 @@ in
     (mkIf haveAliases {
       services.postfix.aliasFiles.aliases = aliasesFile;
     })
+    (mkIf haveCanonical {
+      services.postfix.mapFiles.canonical = canonicalFile;
+    })
     (mkIf haveTransport {
       services.postfix.mapFiles.transport = transportFile;
     })
@@ -969,5 +975,9 @@ in
   imports = [
    (mkRemovedOptionModule [ "services" "postfix" "sslCACert" ]
      "services.postfix.sslCACert was replaced by services.postfix.tlsTrustedAuthorities. In case you intend that your server should validate requested client certificates use services.postfix.extraConfig.")
+
+   (mkChangedOptionModule [ "services" "postfix" "useDane" ]
+     [ "services" "postfix" "config" "smtp_tls_security_level" ]
+     (config: mkIf config.services.postfix.useDane "dane"))
   ];
 }
diff --git a/nixos/modules/services/mail/postgrey.nix b/nixos/modules/services/mail/postgrey.nix
index 709f6b21aa0..7c206e3725e 100644
--- a/nixos/modules/services/mail/postgrey.nix
+++ b/nixos/modules/services/mail/postgrey.nix
@@ -163,7 +163,7 @@ in {
 
     systemd.services.postgrey = let
       bind-flag = if cfg.socket ? path then
-        ''--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}''
+        "--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}"
       else
         ''--inet=${optionalString (cfg.socket.addr != null) (cfg.socket.addr + ":")}${toString cfg.socket.port}'';
     in {
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index a0bbab64985..f9b63000473 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -7,7 +7,7 @@ let
   fpm = config.services.phpfpm.pools.roundcube;
   localDB = cfg.database.host == "localhost";
   user = cfg.database.username;
-  phpWithPspell = pkgs.php.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
+  phpWithPspell = pkgs.php74.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
 in
 {
   options.services.roundcube = {
@@ -204,6 +204,11 @@ in
     };
     systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
 
+    # Restart on config changes.
+    systemd.services.phpfpm-roundcube.restartTriggers = [
+      config.environment.etc."roundcube/config.inc.php".source
+    ];
+
     systemd.services.roundcube-setup = mkMerge [
       (mkIf (cfg.database.host == "localhost") {
         requires = [ "postgresql.service" ];
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix
index aacdbe2aeed..473ddd52357 100644
--- a/nixos/modules/services/mail/rspamd.nix
+++ b/nixos/modules/services/mail/rspamd.nix
@@ -153,7 +153,7 @@ let
 
       ${concatStringsSep "\n" (mapAttrsToList (name: value: let
           includeName = if name == "rspamd_proxy" then "proxy" else name;
-          tryOverride = if value.extraConfig == "" then "true" else "false";
+          tryOverride = boolToString (value.extraConfig == "");
         in ''
         worker "${value.type}" {
           type = "${value.type}";
@@ -371,6 +371,9 @@ in
     };
     services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
 
+    systemd.services.postfix.serviceConfig.SupplementaryGroups =
+      mkIf cfg.postfix.enable [ postfixCfg.group ];
+
     # Allow users to run 'rspamc' and 'rspamadm'.
     environment.systemPackages = [ pkgs.rspamd ];
 
@@ -394,21 +397,50 @@ in
       restartTriggers = [ rspamdDir ];
 
       serviceConfig = {
-        ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c /etc/rspamd/rspamd.conf -f";
+        ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
         Restart = "always";
+
+        User = "${cfg.user}";
+        Group = "${cfg.group}";
+        SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
+
         RuntimeDirectory = "rspamd";
+        RuntimeDirectoryMode = "0755";
+        StateDirectory = "rspamd";
+        StateDirectoryMode = "0700";
+
+        AmbientCapabilities = [];
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
         PrivateTmp = true;
+        # we need to chown socket to rspamd-milter
+        PrivateUsers = !cfg.postfix.enable;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
       };
-
-      preStart = ''
-        ${pkgs.coreutils}/bin/mkdir -p /var/lib/rspamd
-        ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd
-      '';
     };
   };
   imports = [
     (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ]
-	     "Socket activation never worked correctly and could at this time not be fixed and so was removed")
+       "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/spamassassin.nix b/nixos/modules/services/mail/spamassassin.nix
index 4e642542ec6..ac878222b26 100644
--- a/nixos/modules/services/mail/spamassassin.nix
+++ b/nixos/modules/services/mail/spamassassin.nix
@@ -126,19 +126,36 @@ in
     };
 
     systemd.services.sa-update = {
+      # Needs to be able to contact the update server.
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = "spamd";
+        Group = "spamd";
+        StateDirectory = "spamassassin";
+        ExecStartPost = "+${pkgs.systemd}/bin/systemctl -q --no-block try-reload-or-restart spamd.service";
+      };
+
       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/" spamd
-
-        v=$?
+        ${pkgs.spamassassin}/bin/sa-update --verbose --gpghomedir=/var/lib/spamassassin/sa-update-keys/
+        rc=$?
         set -e
-        if [ $v -gt 1 ]; then
-          echo "sa-update execution error"
-          exit $v
+
+        if [[ $rc -gt 1 ]]; then
+          # sa-update failed.
+          exit $rc
         fi
-        if [ $v -eq 0 ]; then
-          systemctl reload spamd.service
+
+        if [[ $rc -eq 1 ]]; then
+          # No update was available, exit successfully.
+          exit 0
         fi
+
+        # An update was available and installed. Compile the rules.
+        ${pkgs.spamassassin}/bin/sa-compile
       '';
     };
 
@@ -153,32 +170,22 @@ in
     };
 
     systemd.services.spamd = {
-      description = "Spam Assassin Server";
+      description = "SpamAssassin Server";
 
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
+      wants = [ "sa-update.service" ];
+      after = [
+        "network.target"
+        "sa-update.service"
+      ];
 
       serviceConfig = {
-        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";
+        User = "spamd";
+        Group = "spamd";
+        ExecStart = "+${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --virtual-config-dir=%S/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
+        ExecReload = "+${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        StateDirectory = "spamassassin";
       };
-
-      # 0 and 1 no error, exitcode > 1 means error:
-      # https://spamassassin.apache.org/full/3.1.x/doc/sa-update.html#exit_codes
-      preStart = ''
-        echo "Recreating '/var/lib/spamasassin' with creating '3.004001' (or similar) and 'sa-update-keys'"
-        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/" spamd
-        v=$?
-        set -e
-        if [ $v -gt 1 ]; then
-          echo "sa-update execution error"
-          exit $v
-        fi
-        chown spamd:spamd /var/lib/spamassassin -R
-      '';
     };
   };
 }
diff --git a/nixos/modules/services/mail/sympa.nix b/nixos/modules/services/mail/sympa.nix
index 0cad09927b2..491b6dba9aa 100644
--- a/nixos/modules/services/mail/sympa.nix
+++ b/nixos/modules/services/mail/sympa.nix
@@ -513,10 +513,6 @@ in
           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/";
diff --git a/nixos/modules/services/misc/airsonic.nix b/nixos/modules/services/misc/airsonic.nix
index 5cc2ff7f4bd..a572f1f6d6f 100644
--- a/nixos/modules/services/misc/airsonic.nix
+++ b/nixos/modules/services/misc/airsonic.nix
@@ -118,7 +118,7 @@ in {
       '';
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
+          ${pkgs.jre8}/bin/java -Xmx${toString cfg.maxMemory}m \
           -Dairsonic.home=${cfg.home} \
           -Dserver.address=${cfg.listenAddress} \
           -Dserver.port=${toString cfg.port} \
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
index f3a650a260f..69dfadfe54e 100644
--- a/nixos/modules/services/misc/apache-kafka.nix
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -90,19 +90,7 @@ in {
 
     jvmOptions = mkOption {
       description = "Extra command line options for the JVM running Kafka.";
-      default = [
-        "-server"
-        "-Xmx1G"
-        "-Xms1G"
-        "-XX:+UseCompressedOops"
-        "-XX:+UseParNewGC"
-        "-XX:+UseConcMarkSweepGC"
-        "-XX:+CMSClassUnloadingEnabled"
-        "-XX:+CMSScavengeBeforeRemark"
-        "-XX:+DisableExplicitGC"
-        "-Djava.awt.headless=true"
-        "-Djava.net.preferIPv4Stack=true"
-      ];
+      default = [];
       type = types.listOf types.str;
       example = [
         "-Djava.net.preferIPv4Stack=true"
@@ -118,6 +106,13 @@ in {
       type = types.package;
     };
 
+    jre = mkOption {
+      description = "The JRE with which to run Kafka";
+      default = cfg.package.passthru.jre;
+      defaultText = "pkgs.apacheKafka.passthru.jre";
+      type = types.package;
+    };
+
   };
 
   config = mkIf cfg.enable {
@@ -138,7 +133,7 @@ in {
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.jre}/bin/java \
+          ${cfg.jre}/bin/java \
             -cp "${cfg.package}/libs/*" \
             -Dlog4j.configuration=file:${logConfig} \
             ${toString cfg.jvmOptions} \
diff --git a/nixos/modules/services/misc/autofs.nix b/nixos/modules/services/misc/autofs.nix
index 5e7c1e66828..541f0d2db19 100644
--- a/nixos/modules/services/misc/autofs.nix
+++ b/nixos/modules/services/misc/autofs.nix
@@ -52,6 +52,7 @@ in
       };
 
       timeout = mkOption {
+        type = types.int;
         default = 600;
         description = "Set the global minimum timeout, in seconds, until directories are unmounted";
       };
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index cf7fb5f78d3..95cee5046e8 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -37,9 +37,9 @@ in {
       description = "Autorandr execution hook";
       after = [ "sleep.target" ];
 
+      startLimitIntervalSec = 5;
+      startLimitBurst = 1;
       serviceConfig = {
-        StartLimitInterval = 5;
-        StartLimitBurst = 1;
         ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
         Type = "oneshot";
         RemainAfterExit = false;
@@ -48,5 +48,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 }
diff --git a/nixos/modules/services/misc/bazarr.nix b/nixos/modules/services/misc/bazarr.nix
index d3fd5b08cc8..99343a146a7 100644
--- a/nixos/modules/services/misc/bazarr.nix
+++ b/nixos/modules/services/misc/bazarr.nix
@@ -64,6 +64,7 @@ in
 
     users.users = mkIf (cfg.user == "bazarr") {
       bazarr = {
+        isSystemUser = true;
         group = cfg.group;
         home = "/var/lib/${config.systemd.services.bazarr.serviceConfig.StateDirectory}";
       };
diff --git a/nixos/modules/services/misc/beanstalkd.nix b/nixos/modules/services/misc/beanstalkd.nix
index bcd133c9741..1c674a5b23b 100644
--- a/nixos/modules/services/misc/beanstalkd.nix
+++ b/nixos/modules/services/misc/beanstalkd.nix
@@ -28,6 +28,12 @@ in
           example = "0.0.0.0";
         };
       };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to open ports in the firewall for the server.";
+      };
     };
   };
 
@@ -35,6 +41,10 @@ in
 
   config = mkIf cfg.enable {
 
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
     environment.systemPackages = [ pkg ];
 
     systemd.services.beanstalkd = {
diff --git a/nixos/modules/services/misc/bees.nix b/nixos/modules/services/misc/bees.nix
index b0ed2d5c286..6b8cae84642 100644
--- a/nixos/modules/services/misc/bees.nix
+++ b/nixos/modules/services/misc/bees.nix
@@ -57,7 +57,7 @@ let
     };
     options.extraOptions = mkOption {
       type = listOf str;
-      default = [];
+      default = [ ];
       description = ''
         Extra command-line options passed to the daemon. See upstream bees documentation.
       '';
@@ -67,7 +67,8 @@ let
     };
   };
 
-in {
+in
+{
 
   options.services.beesd = {
     filesystems = mkOption {
@@ -87,37 +88,42 @@ in {
     };
   };
   config = {
-    systemd.services = mapAttrs' (name: fs: nameValuePair "beesd@${name}" {
-      description = "Block-level BTRFS deduplication for %i";
-      after = [ "sysinit.target" ];
+    systemd.services = mapAttrs'
+      (name: fs: nameValuePair "beesd@${name}" {
+        description = "Block-level BTRFS deduplication for %i";
+        after = [ "sysinit.target" ];
 
-      serviceConfig = let
-        configOpts = [
-          fs.spec
-          "verbosity=${toString fs.verbosity}"
-          "idxSizeMB=${toString fs.hashTableSizeMB}"
-          "workDir=${fs.workDir}"
-        ];
-        configOptsStr = escapeShellArgs configOpts;
-      in {
-        # Values from https://github.com/Zygo/bees/blob/v0.6.1/scripts/beesd%40.service.in
-        ExecStart = "${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${escapeShellArgs fs.extraOptions}";
-        ExecStopPost = "${pkgs.bees}/bin/bees-service-wrapper cleanup ${configOptsStr}";
-        CPUAccounting = true;
-        CPUWeight = 12;
-        IOSchedulingClass = "idle";
-        IOSchedulingPriority = 7;
-        IOWeight = 10;
-        KillMode = "control-group";
-        KillSignal = "SIGTERM";
-        MemoryAccounting = true;
-        Nice = 19;
-        Restart = "on-abnormal";
-        StartupCPUWeight = 25;
-        StartupIOWeight = 25;
-        SyslogIdentifier = "bees"; # would otherwise be "bees-service-wrapper"
-      };
-      wantedBy = ["multi-user.target"];
-    }) cfg.filesystems;
+        serviceConfig =
+          let
+            configOpts = [
+              fs.spec
+              "verbosity=${toString fs.verbosity}"
+              "idxSizeMB=${toString fs.hashTableSizeMB}"
+              "workDir=${fs.workDir}"
+            ];
+            configOptsStr = escapeShellArgs configOpts;
+          in
+          {
+            # Values from https://github.com/Zygo/bees/blob/v0.6.5/scripts/beesd@.service.in
+            ExecStart = "${pkgs.bees}/bin/bees-service-wrapper run ${configOptsStr} -- --no-timestamps ${escapeShellArgs fs.extraOptions}";
+            ExecStopPost = "${pkgs.bees}/bin/bees-service-wrapper cleanup ${configOptsStr}";
+            CPUAccounting = true;
+            CPUSchedulingPolicy = "batch";
+            CPUWeight = 12;
+            IOSchedulingClass = "idle";
+            IOSchedulingPriority = 7;
+            IOWeight = 10;
+            KillMode = "control-group";
+            KillSignal = "SIGTERM";
+            MemoryAccounting = true;
+            Nice = 19;
+            Restart = "on-abnormal";
+            StartupCPUWeight = 25;
+            StartupIOWeight = 25;
+            SyslogIdentifier = "beesd"; # would otherwise be "bees-service-wrapper"
+          };
+        wantedBy = [ "multi-user.target" ];
+      })
+      cfg.filesystems;
   };
 }
diff --git a/nixos/modules/services/misc/calibre-server.nix b/nixos/modules/services/misc/calibre-server.nix
index 84c04f403d3..2467d34b524 100644
--- a/nixos/modules/services/misc/calibre-server.nix
+++ b/nixos/modules/services/misc/calibre-server.nix
@@ -9,24 +9,42 @@ let
 in
 
 {
+  imports = [
+    (mkChangedOptionModule [ "services" "calibre-server" "libraryDir" ] [ "services" "calibre-server" "libraries" ]
+      (config:
+        let libraryDir = getAttrFromPath [ "services" "calibre-server" "libraryDir" ] config;
+        in [ libraryDir ]
+      )
+    )
+  ];
 
   ###### interface
 
   options = {
-
     services.calibre-server = {
 
       enable = mkEnableOption "calibre-server";
 
-      libraryDir = mkOption {
+      libraries = mkOption {
         description = ''
-          The directory where the Calibre library to serve is.
-          '';
-          type = types.path;
+          The directories of the libraries to serve. They must be readable for the user under which the server runs.
+        '';
+        type = types.listOf types.path;
       };
 
-    };
+      user = mkOption {
+        description = "The user under which calibre-server runs.";
+        type = types.str;
+        default = "calibre-server";
+      };
+
+      group = mkOption {
+        description = "The group under which calibre-server runs.";
+        type = types.str;
+        default = "calibre-server";
+      };
 
+    };
   };
 
 
@@ -34,29 +52,34 @@ in
 
   config = mkIf cfg.enable {
 
-    systemd.services.calibre-server =
-      {
+    systemd.services.calibre-server = {
         description = "Calibre Server";
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
-          User = "calibre-server";
+          User = cfg.user;
           Restart = "always";
-          ExecStart = "${pkgs.calibre}/bin/calibre-server ${cfg.libraryDir}";
+          ExecStart = "${pkgs.calibre}/bin/calibre-server ${lib.concatStringsSep " " cfg.libraries}";
         };
 
       };
 
     environment.systemPackages = [ pkgs.calibre ];
 
-    users.users.calibre-server = {
+    users.users = optionalAttrs (cfg.user == "calibre-server") {
+      calibre-server = {
+        home = "/var/lib/calibre-server";
+        createHome = true;
         uid = config.ids.uids.calibre-server;
-        group = "calibre-server";
+        group = cfg.group;
       };
+    };
 
-    users.groups.calibre-server = {
+    users.groups = optionalAttrs (cfg.group == "calibre-server") {
+      calibre-server = {
         gid = config.ids.gids.calibre-server;
       };
+    };
 
   };
 
diff --git a/nixos/modules/services/misc/cfdyndns.nix b/nixos/modules/services/misc/cfdyndns.nix
index dcf41602273..15af1f50da1 100644
--- a/nixos/modules/services/misc/cfdyndns.nix
+++ b/nixos/modules/services/misc/cfdyndns.nix
@@ -6,6 +6,12 @@ let
   cfg = config.services.cfdyndns;
 in
 {
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "cfdyndns" "apikey" ]
+      "Use services.cfdyndns.apikeyFile instead.")
+  ];
+
   options = {
     services.cfdyndns = {
       enable = mkEnableOption "Cloudflare Dynamic DNS Client";
@@ -17,10 +23,12 @@ in
         '';
       };
 
-      apikey = mkOption {
-        type = types.str;
+      apikeyFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
         description = ''
-          The API Key to use to authenticate to CloudFlare.
+          The path to a file containing the API Key
+          used to authenticate with CloudFlare.
         '';
       };
 
@@ -45,13 +53,17 @@ in
         Type = "simple";
         User = config.ids.uids.cfdyndns;
         Group = config.ids.gids.cfdyndns;
-        ExecStart = "/bin/sh -c '${pkgs.cfdyndns}/bin/cfdyndns'";
       };
       environment = {
         CLOUDFLARE_EMAIL="${cfg.email}";
-        CLOUDFLARE_APIKEY="${cfg.apikey}";
         CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}";
       };
+      script = ''
+        ${optionalString (cfg.apikeyFile != null) ''
+          export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})"
+        ''}
+        ${pkgs.cfdyndns}/bin/cfdyndns
+      '';
     };
 
     users.users = {
diff --git a/nixos/modules/services/misc/cgminer.nix b/nixos/modules/services/misc/cgminer.nix
index 7635c2a0f4e..662570f9451 100644
--- a/nixos/modules/services/misc/cgminer.nix
+++ b/nixos/modules/services/misc/cgminer.nix
@@ -41,12 +41,14 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "cgminer";
         description = "User account under which cgminer runs";
       };
 
       pools = mkOption {
         default = [];  # Run benchmark
+        type = types.listOf (types.attrsOf types.str);
         description = "List of pools where to mine";
         example = [{
           url = "http://p2pool.org:9332";
@@ -57,6 +59,7 @@ in
 
       hardware = mkOption {
         default = []; # Run without options
+        type = types.listOf (types.attrsOf (types.either types.str types.int));
         description= "List of config options for every GPU";
         example = [
         {
@@ -83,6 +86,7 @@ in
 
       config = mkOption {
         default = {};
+        type = (types.either types.bool types.int);
         description = "Additional config";
         example = {
           auto-fan = true;
@@ -120,18 +124,18 @@ in
       wantedBy = [ "multi-user.target" ];
 
       environment = {
-        LD_LIBRARY_PATH = ''/run/opengl-driver/lib:/run/opengl-driver-32/lib'';
+        LD_LIBRARY_PATH = "/run/opengl-driver/lib:/run/opengl-driver-32/lib";
         DISPLAY = ":${toString config.services.xserver.display}";
         GPU_MAX_ALLOC_PERCENT = "100";
         GPU_USE_SYNC_OBJECTS = "1";
       };
 
+      startLimitIntervalSec = 60;  # 1 min
       serviceConfig = {
         ExecStart = "${pkgs.cgminer}/bin/cgminer --syslog --text-only --config ${cgminerConfig}";
         User = cfg.user;
         RestartSec = "30s";
         Restart = "always";
-        StartLimitInterval = "1m";
       };
     };
 
diff --git a/nixos/modules/services/misc/clipcat.nix b/nixos/modules/services/misc/clipcat.nix
new file mode 100644
index 00000000000..128bb9a89d6
--- /dev/null
+++ b/nixos/modules/services/misc/clipcat.nix
@@ -0,0 +1,31 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.clipcat;
+in {
+
+  options.services.clipcat= {
+    enable = mkEnableOption "Clipcat clipboard daemon";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.clipcat;
+      defaultText = "pkgs.clipcat";
+      description = "clipcat derivation to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.user.services.clipcat = {
+      enable      = true;
+      description = "clipcat daemon";
+      wantedBy = [ "graphical-session.target" ];
+      after    = [ "graphical-session.target" ];
+      serviceConfig.ExecStart = "${cfg.package}/bin/clipcatd --no-daemon";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/misc/defaultUnicornConfig.rb b/nixos/modules/services/misc/defaultUnicornConfig.rb
deleted file mode 100644
index 0b58c59c7a5..00000000000
--- a/nixos/modules/services/misc/defaultUnicornConfig.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-worker_processes 3
-
-listen ENV["UNICORN_PATH"] + "/tmp/sockets/gitlab.socket", :backlog => 1024
-listen "/run/gitlab/gitlab.socket", :backlog => 1024
-
-working_directory ENV["GITLAB_PATH"]
-
-pid ENV["UNICORN_PATH"] + "/tmp/pids/unicorn.pid"
-
-timeout 60
-
-# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
-# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
-preload_app true
-GC.respond_to?(:copy_on_write_friendly=) and
-  GC.copy_on_write_friendly = true
-
-check_client_connection false
-
-before_fork do |server, worker|
-  # the following is highly recommended for Rails + "preload_app true"
-  # as there's no need for the master process to hold a connection
-  defined?(ActiveRecord::Base) and
-    ActiveRecord::Base.connection.disconnect!
-
-  # The following is only recommended for memory/DB-constrained
-  # installations.  It is not needed if your system can house
-  # twice as many worker_processes as you have configured.
-  #
-  # This allows a new master process to incrementally
-  # phase out the old master process with SIGTTOU to avoid a
-  # thundering herd (especially in the "preload_app false" case)
-  # when doing a transparent upgrade.  The last worker spawned
-  # will then kill off the old master process with a SIGQUIT.
-  old_pid = "#{server.config[:pid]}.oldbin"
-  if old_pid != server.pid
-    begin
-      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
-      Process.kill(sig, File.read(old_pid).to_i)
-    rescue Errno::ENOENT, Errno::ESRCH
-    end
-  end
-
-  # Throttle the master from forking too quickly by sleeping.  Due
-  # to the implementation of standard Unix signal handlers, this
-  # helps (but does not completely) prevent identical, repeated signals
-  # from being lost when the receiving process is busy.
-  # sleep 1
-end
-
-after_fork do |server, worker|
-  # per-process listener ports for debugging/admin/migrations
-  # addr = "127.0.0.1:#{9293 + worker.nr}"
-  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
-
-  # the following is *required* for Rails + "preload_app true",
-  defined?(ActiveRecord::Base) and
-    ActiveRecord::Base.establish_connection
-
-  # reset prometheus client, this will cause any opened metrics files to be closed
-  defined?(::Prometheus::Client.reinitialize_on_pid_change) &&
-    Prometheus::Client.reinitialize_on_pid_change
-
-  # if preload_app is true, then you may also want to check and
-  # restart any other shared sockets/descriptors such as Memcached,
-  # and Redis.  TokyoCabinet file handles are safe to reuse
-  # between any number of forked children (assuming your kernel
-  # correctly implements pread()/pwrite() system calls)
-end
diff --git a/nixos/modules/services/misc/dendrite.nix b/nixos/modules/services/misc/dendrite.nix
new file mode 100644
index 00000000000..c967fc3a362
--- /dev/null
+++ b/nixos/modules/services/misc/dendrite.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.dendrite;
+  settingsFormat = pkgs.formats.yaml { };
+  configurationYaml = settingsFormat.generate "dendrite.yaml" cfg.settings;
+  workingDir = "/var/lib/dendrite";
+in
+{
+  options.services.dendrite = {
+    enable = lib.mkEnableOption "matrix.org dendrite";
+    httpPort = lib.mkOption {
+      type = lib.types.nullOr lib.types.port;
+      default = 8008;
+      description = ''
+        The port to listen for HTTP requests on.
+      '';
+    };
+    httpsPort = lib.mkOption {
+      type = lib.types.nullOr lib.types.port;
+      default = null;
+      description = ''
+        The port to listen for HTTPS requests on.
+      '';
+    };
+    tlsCert = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/server.cert";
+      default = null;
+      description = ''
+        The path to the TLS certificate.
+
+        <programlisting>
+          nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+        </programlisting>
+      '';
+    };
+    tlsKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/server.key";
+      default = null;
+      description = ''
+        The path to the TLS key.
+
+        <programlisting>
+          nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+        </programlisting>
+      '';
+    };
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      example = "/var/lib/dendrite/registration_secret";
+      default = null;
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+        Secrets may be passed to the service without adding them to the world-readable
+        Nix store, by specifying placeholder variables as the option value in Nix and
+        setting these variables accordingly in the environment file. Currently only used
+        for the registration secret to allow secure registration when
+        client_api.registration_disabled is true.
+
+        <programlisting>
+          # snippet of dendrite-related config
+          services.dendrite.settings.client_api.registration_shared_secret = "$REGISTRATION_SHARED_SECRET";
+        </programlisting>
+
+        <programlisting>
+          # content of the environment file
+          REGISTRATION_SHARED_SECRET=verysecretpassword
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>dendrite</literal> is running.
+      '';
+    };
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+        options.global = {
+          server_name = lib.mkOption {
+            type = lib.types.str;
+            example = "example.com";
+            description = ''
+              The domain name of the server, with optional explicit port.
+              This is used by remote servers to connect to this server.
+              This is also the last part of your UserID.
+            '';
+          };
+          private_key = lib.mkOption {
+            type = lib.types.path;
+            example = "${workingDir}/matrix_key.pem";
+            description = ''
+              The path to the signing private key file, used to sign
+              requests and events.
+
+              <programlisting>
+                nix-shell -p dendrite --command "generate-keys --private-key matrix_key.pem"
+              </programlisting>
+            '';
+          };
+          trusted_third_party_id_servers = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            example = [ "matrix.org" ];
+            default = [ "matrix.org" "vector.im" ];
+            description = ''
+              Lists of domains that the server will trust as identity
+              servers to verify third party identifiers such as phone
+              numbers and email addresses
+            '';
+          };
+        };
+        options.client_api = {
+          registration_disabled = lib.mkOption {
+            type = lib.types.bool;
+            default = true;
+            description = ''
+              Whether to disable user registration to the server
+              without the shared secret.
+            '';
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Configuration for dendrite, see:
+        <link xlink:href="https://github.com/matrix-org/dendrite/blob/master/dendrite-config.yaml"/>
+        for available options with which to populate settings.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [{
+      assertion = cfg.httpsPort != null -> (cfg.tlsCert != null && cfg.tlsKey != null);
+      message = ''
+        If Dendrite is configured to use https, tlsCert and tlsKey must be provided.
+
+        nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
+      '';
+    }];
+
+    systemd.services.dendrite = {
+      description = "Dendrite Matrix homeserver";
+      after = [
+        "network.target"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "dendrite";
+        WorkingDirectory = workingDir;
+        RuntimeDirectory = "dendrite";
+        RuntimeDirectoryMode = "0700";
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStartPre =
+          if (cfg.environmentFile != null) then ''
+            ${pkgs.envsubst}/bin/envsubst \
+              -i ${configurationYaml} \
+              -o /run/dendrite/dendrite.yaml
+          '' else ''
+            ${pkgs.coreutils}/bin/cp ${configurationYaml} /run/dendrite/dendrite.yaml
+          '';
+        ExecStart = lib.strings.concatStringsSep " " ([
+          "${pkgs.dendrite}/bin/dendrite-monolith-server"
+          "--config /run/dendrite/dendrite.yaml"
+        ] ++ lib.optionals (cfg.httpPort != null) [
+          "--http-bind-address :${builtins.toString cfg.httpPort}"
+        ] ++ lib.optionals (cfg.httpsPort != null) [
+          "--https-bind-address :${builtins.toString cfg.httpsPort}"
+          "--tls-cert ${cfg.tlsCert}"
+          "--tls-key ${cfg.tlsKey}"
+        ]);
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        Restart = "on-failure";
+      };
+    };
+  };
+  meta.maintainers = lib.teams.matrix.members;
+}
diff --git a/nixos/modules/services/misc/dictd.nix b/nixos/modules/services/misc/dictd.nix
index d175854d2d1..6e796a3a1fc 100644
--- a/nixos/modules/services/misc/dictd.nix
+++ b/nixos/modules/services/misc/dictd.nix
@@ -27,7 +27,7 @@ in
         default = with pkgs.dictdDBs; [ wiktionary wordnet ];
         defaultText = "with pkgs.dictdDBs; [ wiktionary wordnet ]";
         example = literalExample "[ pkgs.dictdDBs.nld2eng ]";
-        description = ''List of databases to make available.'';
+        description = "List of databases to make available.";
       };
 
     };
diff --git a/nixos/modules/services/misc/disnix.nix b/nixos/modules/services/misc/disnix.nix
index 69386cdbb38..24a259bb4d2 100644
--- a/nixos/modules/services/misc/disnix.nix
+++ b/nixos/modules/services/misc/disnix.nix
@@ -34,6 +34,14 @@ in
         defaultText = "pkgs.disnix";
       };
 
+      enableProfilePath = mkEnableOption "exposing the Disnix profiles in the system's PATH";
+
+      profiles = mkOption {
+        type = types.listOf types.str;
+        default = [ "default" ];
+        example = [ "default" ];
+        description = "Names of the Disnix profiles to expose in the system's PATH";
+      };
     };
 
   };
@@ -44,6 +52,8 @@ in
     dysnomia.enable = true;
 
     environment.systemPackages = [ pkgs.disnix ] ++ optional cfg.useWebServiceInterface pkgs.DisnixWebService;
+    environment.variables.PATH = lib.optionals cfg.enableProfilePath (map (profileName: "/nix/var/nix/profiles/disnix/${profileName}/bin" ) cfg.profiles);
+    environment.variables.DISNIX_REMOTE_CLIENT = lib.optionalString (cfg.enableMultiUser) "disnix-client";
 
     services.dbus.enable = true;
     services.dbus.packages = [ pkgs.disnix ];
@@ -68,7 +78,8 @@ in
           ++ optional config.services.postgresql.enable "postgresql.service"
           ++ optional config.services.tomcat.enable "tomcat.service"
           ++ optional config.services.svnserve.enable "svnserve.service"
-          ++ optional config.services.mongodb.enable "mongodb.service";
+          ++ optional config.services.mongodb.enable "mongodb.service"
+          ++ optional config.services.influxdb.enable "influxdb.service";
 
         restartIfChanged = false;
 
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index 1c2e2cc5359..e212f581c28 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -58,7 +58,7 @@ in {
     port = mkOption {
       description = "Docker registry port to bind to.";
       default = 5000;
-      type = types.int;
+      type = types.port;
     };
 
     storagePath = mkOption {
diff --git a/nixos/modules/services/misc/domoticz.nix b/nixos/modules/services/misc/domoticz.nix
new file mode 100644
index 00000000000..b1353d48404
--- /dev/null
+++ b/nixos/modules/services/misc/domoticz.nix
@@ -0,0 +1,51 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.domoticz;
+  pkgDesc = "Domoticz home automation";
+
+in {
+
+  options = {
+
+    services.domoticz = {
+      enable = mkEnableOption pkgDesc;
+
+      bind = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "IP address to bind to.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Port to bind to for HTTP, set to 0 to disable HTTP.";
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.services."domoticz" = {
+      description = pkgDesc;
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "domoticz";
+        Restart = "always";
+        ExecStart = ''
+          ${pkgs.domoticz}/bin/domoticz -noupdates -www ${toString cfg.port} -wwwbind ${cfg.bind} -sslwww 0 -userdata /var/lib/domoticz -approot ${pkgs.domoticz}/share/domoticz/ -pidfile /var/run/domoticz.pid
+        '';
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/misc/duckling.nix b/nixos/modules/services/misc/duckling.nix
new file mode 100644
index 00000000000..77d2a92380b
--- /dev/null
+++ b/nixos/modules/services/misc/duckling.nix
@@ -0,0 +1,39 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.duckling;
+in {
+  options = {
+    services.duckling = {
+      enable = mkEnableOption "duckling";
+
+      port = mkOption {
+        type = types.port;
+        default = 8080;
+        description = ''
+          Port on which duckling will run.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.duckling = {
+      description = "Duckling server service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+
+      environment = {
+        PORT = builtins.toString cfg.port;
+      };
+
+      serviceConfig = {
+        ExecStart = "${pkgs.haskellPackages.duckling}/bin/duckling-example-exe --no-access-log --no-error-log";
+        Restart = "always";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/dysnomia.nix b/nixos/modules/services/misc/dysnomia.nix
index 4b52963500d..333ba651cde 100644
--- a/nixos/modules/services/misc/dysnomia.nix
+++ b/nixos/modules/services/misc/dysnomia.nix
@@ -66,6 +66,19 @@ let
       ) (builtins.attrNames cfg.components)}
     '';
   };
+
+  dysnomiaFlags = {
+    enableApacheWebApplication = config.services.httpd.enable;
+    enableAxis2WebService = config.services.tomcat.axis2.enable;
+    enableDockerContainer = config.virtualisation.docker.enable;
+    enableEjabberdDump = config.services.ejabberd.enable;
+    enableMySQLDatabase = config.services.mysql.enable;
+    enablePostgreSQLDatabase = config.services.postgresql.enable;
+    enableTomcatWebApplication = config.services.tomcat.enable;
+    enableMongoDatabase = config.services.mongodb.enable;
+    enableSubversionRepository = config.services.svnserve.enable;
+    enableInfluxDatabase = config.services.influxdb.enable;
+  };
 in
 {
   options = {
@@ -117,6 +130,12 @@ in
         description = "A list of paths containing additional modules that are added to the search folders";
         default = [];
       };
+
+      enableLegacyModules = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to enable Dysnomia legacy process and wrapper modules";
+      };
     };
   };
 
@@ -142,34 +161,48 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
-    dysnomia.package = pkgs.dysnomia.override (origArgs: {
-      enableApacheWebApplication = config.services.httpd.enable;
-      enableAxis2WebService = config.services.tomcat.axis2.enable;
-      enableEjabberdDump = config.services.ejabberd.enable;
-      enableMySQLDatabase = config.services.mysql.enable;
-      enablePostgreSQLDatabase = config.services.postgresql.enable;
-      enableSubversionRepository = config.services.svnserve.enable;
-      enableTomcatWebApplication = config.services.tomcat.enable;
-      enableMongoDatabase = config.services.mongodb.enable;
-      enableInfluxDatabase = config.services.influxdb.enable;
+    dysnomia.package = pkgs.dysnomia.override (origArgs: dysnomiaFlags // lib.optionalAttrs (cfg.enableLegacyModules) {
+      enableLegacy = builtins.trace ''
+        WARNING: Dysnomia has been configured to use the legacy 'process' and 'wrapper'
+        modules for compatibility reasons! If you rely on these modules, consider
+        migrating to better alternatives.
+
+        More information: https://raw.githubusercontent.com/svanderburg/dysnomia/f65a9a84827bcc4024d6b16527098b33b02e4054/README-legacy.md
+
+        If you have migrated already or don't rely on these Dysnomia modules, you can
+        disable legacy mode with the following NixOS configuration option:
+
+        dysnomia.enableLegacyModules = false;
+
+        In a future version of Dysnomia (and NixOS) the legacy option will go away!
+      '' true;
     });
 
     dysnomia.properties = {
       hostname = config.networking.hostName;
       inherit (config.nixpkgs.localSystem) system;
 
-      supportedTypes = (import "${pkgs.stdenv.mkDerivation {
-        name = "supportedtypes";
-        buildCommand = ''
-          ( echo -n "[ "
-            cd ${cfg.package}/libexec/dysnomia
-            for i in *
-            do
-                echo -n "\"$i\" "
-            done
-            echo -n " ]") > $out
-        '';
-      }}");
+      supportedTypes = [
+        "echo"
+        "fileset"
+        "process"
+        "wrapper"
+
+        # These are not base modules, but they are still enabled because they work with technology that are always enabled in NixOS
+        "systemd-unit"
+        "sysvinit-script"
+        "nixos-configuration"
+      ]
+      ++ optional (dysnomiaFlags.enableApacheWebApplication) "apache-webapplication"
+      ++ optional (dysnomiaFlags.enableAxis2WebService) "axis2-webservice"
+      ++ optional (dysnomiaFlags.enableDockerContainer) "docker-container"
+      ++ optional (dysnomiaFlags.enableEjabberdDump) "ejabberd-dump"
+      ++ optional (dysnomiaFlags.enableInfluxDatabase) "influx-database"
+      ++ optional (dysnomiaFlags.enableMySQLDatabase) "mysql-database"
+      ++ optional (dysnomiaFlags.enablePostgreSQLDatabase) "postgresql-database"
+      ++ optional (dysnomiaFlags.enableTomcatWebApplication) "tomcat-webapplication"
+      ++ optional (dysnomiaFlags.enableMongoDatabase) "mongo-database"
+      ++ optional (dysnomiaFlags.enableSubversionRepository) "subversion-repository";
     };
 
     dysnomia.containers = lib.recursiveUpdate ({
@@ -185,9 +218,9 @@ in
     }; }
     // lib.optionalAttrs (config.services.mysql.enable) { mysql-database = {
         mysqlPort = config.services.mysql.port;
+        mysqlSocket = "/run/mysqld/mysqld.sock";
       } // lib.optionalAttrs cfg.enableAuthentication {
         mysqlUsername = "root";
-        mysqlPassword = builtins.readFile (config.services.mysql.rootPassword);
       };
     }
     // lib.optionalAttrs (config.services.postgresql.enable) { postgresql-database = {
@@ -199,10 +232,19 @@ in
       tomcatPort = 8080;
     }; }
     // lib.optionalAttrs (config.services.mongodb.enable) { mongo-database = {}; }
+    // lib.optionalAttrs (config.services.influxdb.enable) {
+      influx-database = {
+        influxdbUsername = config.services.influxdb.user;
+        influxdbDataDir = "${config.services.influxdb.dataDir}/data";
+        influxdbMetaDir = "${config.services.influxdb.dataDir}/meta";
+      };
+    }
     // lib.optionalAttrs (config.services.svnserve.enable) { subversion-repository = {
       svnBaseDir = config.services.svnserve.svnBaseDir;
     }; }) cfg.extraContainerProperties;
 
+    boot.extraSystemdUnitPaths = [ "/etc/systemd-mutable/system" ];
+
     system.activationScripts.dysnomia = ''
       mkdir -p /etc/systemd-mutable/system
       if [ ! -f /etc/systemd-mutable/system/dysnomia.target ]
diff --git a/nixos/modules/services/misc/etcd.nix b/nixos/modules/services/misc/etcd.nix
index 32360d43768..eb266f043eb 100644
--- a/nixos/modules/services/misc/etcd.nix
+++ b/nixos/modules/services/misc/etcd.nix
@@ -184,7 +184,7 @@ in {
       };
     };
 
-    environment.systemPackages = [ pkgs.etcdctl ];
+    environment.systemPackages = [ pkgs.etcd ];
 
     users.users.etcd = {
       uid = config.ids.uids.etcd;
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
new file mode 100644
index 00000000000..b6bd6e9fd37
--- /dev/null
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -0,0 +1,226 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etebase-server;
+
+  pythonEnv = pkgs.python3.withPackages (ps: with ps;
+    [ etebase-server daphne ]);
+
+  iniFmt = pkgs.formats.ini {};
+
+  configIni = iniFmt.generate "etebase-server.ini" cfg.settings;
+
+  defaultUser = "etebase-server";
+in
+{
+  imports = [
+    (mkRemovedOptionModule
+      [ "services" "etebase-server" "customIni" ]
+      "Set the option `services.etebase-server.settings' instead.")
+    (mkRemovedOptionModule
+      [ "services" "etebase-server" "database" ]
+      "Set the option `services.etebase-server.settings.database' instead.")
+    (mkRenamedOptionModule
+      [ "services" "etebase-server" "secretFile" ]
+      [ "services" "etebase-server" "settings" "secret_file" ])
+    (mkRenamedOptionModule
+      [ "services" "etebase-server" "host" ]
+      [ "services" "etebase-server" "settings" "allowed_hosts" "allowed_host1" ])
+  ];
+
+  options = {
+    services.etebase-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable the Etebase server.
+
+          Once enabled you need to create an admin user by invoking the
+          shell command <literal>etebase-server createsuperuser</literal> with
+          the user specified by the <literal>user</literal> option or a superuser.
+          Then you can login and create accounts on your-etebase-server.com/admin
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/etebase-server";
+        description = "Directory to store the Etebase server data.";
+      };
+
+      port = mkOption {
+        type = with types; nullOr port;
+        default = 8001;
+        description = "Port to listen on.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      unixSocket = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        description = "The path to the socket to bind to.";
+        example = "/run/etebase-server/etebase-server.sock";
+      };
+
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = iniFmt.type;
+
+          options = {
+            global = {
+              debug = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to set django's DEBUG flag.
+                '';
+              };
+              secret_file = mkOption {
+                type = with types; nullOr str;
+                default = null;
+                description = ''
+                  The path to a file containing the secret
+                  used as django's SECRET_KEY.
+                '';
+              };
+              static_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/static";
+                defaultText = "\${config.services.etebase-server.dataDir}/static";
+                description = "The directory for static files.";
+              };
+              media_root = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/media";
+                defaultText = "\${config.services.etebase-server.dataDir}/media";
+                description = "The media directory.";
+              };
+            };
+            allowed_hosts = {
+              allowed_host1 = mkOption {
+                type = types.str;
+                default = "0.0.0.0";
+                example = "localhost";
+                description = ''
+                  The main host that is allowed access.
+                '';
+              };
+            };
+            database = {
+              engine = mkOption {
+                type = types.enum [ "django.db.backends.sqlite3" "django.db.backends.postgresql" ];
+                default = "django.db.backends.sqlite3";
+                description = "The database engine to use.";
+              };
+              name = mkOption {
+                type = types.str;
+                default = "${cfg.dataDir}/db.sqlite3";
+                defaultText = "\${config.services.etebase-server.dataDir}/db.sqlite3";
+                description = "The database name.";
+              };
+            };
+          };
+        };
+        default = {};
+        description = ''
+          Configuration for <package>etebase-server</package>. Refer to
+          <link xlink:href="https://github.com/etesync/server/blob/master/etebase-server.ini.example" />
+          and <link xlink:href="https://github.com/etesync/server/wiki" />
+          for details on supported values.
+        '';
+        example = {
+          global = {
+            debug = true;
+            media_root = "/path/to/media";
+          };
+          allowed_hosts = {
+            allowed_host2 = "localhost";
+          };
+        };
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = "User under which Etebase server runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = with pkgs; [
+      (runCommand "etebase-server" {
+        buildInputs = [ makeWrapper ];
+      } ''
+        makeWrapper ${pythonEnv}/bin/etebase-server \
+          $out/bin/etebase-server \
+          --run "cd ${cfg.dataDir}" \
+          --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
+      '')
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+    ];
+
+    systemd.services.etebase-server = {
+      description = "An Etebase (EteSync 2.0) server";
+      after = [ "network.target" "systemd-tmpfiles-setup.service" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        User = cfg.user;
+        Restart = "always";
+        WorkingDirectory = cfg.dataDir;
+      };
+      environment = {
+        PYTHONPATH = "${pythonEnv}/${pkgs.python3.sitePackages}";
+        ETEBASE_EASY_CONFIG_PATH = configIni;
+      };
+      preStart = ''
+        # Auto-migrate on first run or if the package has changed
+        versionFile="${cfg.dataDir}/src-version"
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${pkgs.etebase-server} ]]; then
+          ${pythonEnv}/bin/etebase-server migrate --no-input
+          ${pythonEnv}/bin/etebase-server collectstatic --no-input --clear
+          echo ${pkgs.etebase-server} > "$versionFile"
+        fi
+      '';
+      script =
+        let
+          networking = if cfg.unixSocket != null
+          then "-u ${cfg.unixSocket}"
+          else "-b 0.0.0.0 -p ${toString cfg.port}";
+        in ''
+          cd "${pythonEnv}/lib/etebase-server";
+          ${pythonEnv}/bin/daphne ${networking} \
+            etebase_server.asgi:application
+        '';
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users.${defaultUser} = {
+        isSystemUser = true;
+        group = defaultUser;
+        home = cfg.dataDir;
+      };
+
+      groups.${defaultUser} = {};
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/etesync-dav.nix b/nixos/modules/services/misc/etesync-dav.nix
new file mode 100644
index 00000000000..9d7cfda371b
--- /dev/null
+++ b/nixos/modules/services/misc/etesync-dav.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.etesync-dav;
+in
+  {
+    options.services.etesync-dav = {
+      enable = mkEnableOption "etesync-dav";
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "The server host address.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 37358;
+        description = "The server host port.";
+      };
+
+      apiUrl = mkOption {
+        type = types.str;
+        default = "https://api.etesync.com/";
+        description = "The url to the etesync API.";
+      };
+
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified port.";
+      };
+
+      sslCertificate = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.crt";
+        description = ''
+          Path to server SSL certificate. It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+
+      sslCertificateKey = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/etesync.key";
+        description = ''
+          Path to server SSL certificate key.  It will be copied into
+          etesync-dav's data directory.
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+      systemd.services.etesync-dav = {
+        description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        path = [ pkgs.etesync-dav ];
+        environment = {
+          ETESYNC_LISTEN_ADDRESS = cfg.host;
+          ETESYNC_LISTEN_PORT = toString cfg.port;
+          ETESYNC_URL = cfg.apiUrl;
+          ETESYNC_DATA_DIR = "/var/lib/etesync-dav";
+        };
+
+        serviceConfig = {
+          Type = "simple";
+          DynamicUser = true;
+          StateDirectory = "etesync-dav";
+          ExecStart = "${pkgs.etesync-dav}/bin/etesync-dav";
+          ExecStartPre = mkIf (cfg.sslCertificate != null || cfg.sslCertificateKey != null) (
+            pkgs.writers.writeBash "etesync-dav-copy-keys" ''
+              ${optionalString (cfg.sslCertificate != null) ''
+                cp ${toString cfg.sslCertificate} $STATE_DIRECTORY/etesync.crt
+              ''}
+              ${optionalString (cfg.sslCertificateKey != null) ''
+                cp ${toString cfg.sslCertificateKey} $STATE_DIRECTORY/etesync.key
+              ''}
+            ''
+          );
+          Restart = "on-failure";
+          RestartSec = "30min 1s";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/misc/exhibitor.nix b/nixos/modules/services/misc/exhibitor.nix
index f8c79f892da..28c98edf47a 100644
--- a/nixos/modules/services/misc/exhibitor.nix
+++ b/nixos/modules/services/misc/exhibitor.nix
@@ -185,7 +185,7 @@ in
       };
       zkExtraCfg = mkOption {
         type = types.str;
-        default = ''initLimit=5&syncLimit=2&tickTime=2000'';
+        default = "initLimit=5&syncLimit=2&tickTime=2000";
         description = ''
           Extra options to pass into Zookeeper
         '';
diff --git a/nixos/modules/services/misc/felix.nix b/nixos/modules/services/misc/felix.nix
index 21740c8c0b7..8d438bb9eb1 100644
--- a/nixos/modules/services/misc/felix.nix
+++ b/nixos/modules/services/misc/felix.nix
@@ -27,11 +27,13 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "osgi";
         description = "User account under which Apache Felix runs.";
       };
 
       group = mkOption {
+        type = types.str;
         default = "osgi";
         description = "Group account under which Apache Felix runs.";
       };
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
index b8841a7fe74..a9fc04b46f0 100644
--- a/nixos/modules/services/misc/fstrim.nix
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -31,7 +31,7 @@ in {
 
   config = mkIf cfg.enable {
 
-    systemd.packages = [ pkgs.utillinux ];
+    systemd.packages = [ pkgs.util-linux ];
 
     systemd.timers.fstrim = {
       timerConfig = {
@@ -42,5 +42,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 }
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
index 3057d7fd1a0..552725f1384 100644
--- a/nixos/modules/services/misc/gammu-smsd.nix
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -172,7 +172,7 @@ in {
           };
 
           database = mkOption {
-            type = types.str;
+            type = types.nullOr types.str;
             default = null;
             description = "Database name to store sms data";
           };
diff --git a/nixos/modules/services/misc/geoip-updater.nix b/nixos/modules/services/misc/geoip-updater.nix
deleted file mode 100644
index baf0a8d73d1..00000000000
--- a/nixos/modules/services/misc/geoip-updater.nix
+++ /dev/null
@@ -1,306 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.geoip-updater;
-
-  dbBaseUrl = "https://geolite.maxmind.com/download/geoip/database";
-
-  randomizedTimerDelaySec = "3600";
-
-  # Use writeScriptBin instead of writeScript, so that argv[0] (logged to the
-  # journal) doesn't include the long nix store path hash. (Prefixing the
-  # ExecStart= command with '@' doesn't work because we start a shell (new
-  # process) that creates a new argv[0].)
-  geoip-updater = pkgs.writeScriptBin "geoip-updater" ''
-    #!${pkgs.runtimeShell}
-    skipExisting=0
-    debug()
-    {
-        echo "<7>$@"
-    }
-    info()
-    {
-        echo "<6>$@"
-    }
-    error()
-    {
-        echo "<3>$@"
-    }
-    die()
-    {
-        error "$@"
-        exit 1
-    }
-    waitNetworkOnline()
-    {
-        ret=1
-        for i in $(seq 6); do
-            curl_out=$("${pkgs.curl.bin}/bin/curl" \
-                --silent --fail --show-error --max-time 60 "${dbBaseUrl}" 2>&1)
-            if [ $? -eq 0 ]; then
-                debug "Server is reachable (try $i)"
-                ret=0
-                break
-            else
-                debug "Server is unreachable (try $i): $curl_out"
-                sleep 10
-            fi
-        done
-        return $ret
-    }
-    dbFnameTmp()
-    {
-        dburl=$1
-        echo "${cfg.databaseDir}/.$(basename "$dburl")"
-    }
-    dbFnameTmpDecompressed()
-    {
-        dburl=$1
-        echo "${cfg.databaseDir}/.$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//'
-    }
-    dbFname()
-    {
-        dburl=$1
-        echo "${cfg.databaseDir}/$(basename "$dburl")" | sed 's/\.\(gz\|xz\)$//'
-    }
-    downloadDb()
-    {
-        dburl=$1
-        curl_out=$("${pkgs.curl.bin}/bin/curl" \
-            --silent --fail --show-error --max-time 900 -L -o "$(dbFnameTmp "$dburl")" "$dburl" 2>&1)
-        if [ $? -ne 0 ]; then
-            error "Failed to download $dburl: $curl_out"
-            return 1
-        fi
-    }
-    decompressDb()
-    {
-        fn=$(dbFnameTmp "$1")
-        ret=0
-        case "$fn" in
-            *.gz)
-                cmd_out=$("${pkgs.gzip}/bin/gzip" --decompress --force "$fn" 2>&1)
-                ;;
-            *.xz)
-                cmd_out=$("${pkgs.xz.bin}/bin/xz" --decompress --force "$fn" 2>&1)
-                ;;
-            *)
-                cmd_out=$(echo "File \"$fn\" is neither a .gz nor .xz file")
-                false
-                ;;
-        esac
-        if [ $? -ne 0 ]; then
-            error "$cmd_out"
-            ret=1
-        fi
-    }
-    atomicRename()
-    {
-        dburl=$1
-        mv "$(dbFnameTmpDecompressed "$dburl")" "$(dbFname "$dburl")"
-    }
-    removeIfNotInConfig()
-    {
-        # Arg 1 is the full path of an installed DB.
-        # If the corresponding database is not specified in the NixOS config we
-        # remove it.
-        db=$1
-        for cdb in ${lib.concatStringsSep " " cfg.databases}; do
-            confDb=$(echo "$cdb" | sed 's/\.\(gz\|xz\)$//')
-            if [ "$(basename "$db")" = "$(basename "$confDb")" ]; then
-                return 0
-            fi
-        done
-        rm "$db"
-        if [ $? -eq 0 ]; then
-            debug "Removed $(basename "$db") (not listed in services.geoip-updater.databases)"
-        else
-            error "Failed to remove $db"
-        fi
-    }
-    removeUnspecifiedDbs()
-    {
-        for f in "${cfg.databaseDir}/"*; do
-            test -f "$f" || continue
-            case "$f" in
-                *.dat|*.mmdb|*.csv)
-                    removeIfNotInConfig "$f"
-                    ;;
-                *)
-                    debug "Not removing \"$f\" (unknown file extension)"
-                    ;;
-            esac
-        done
-    }
-    downloadAndInstall()
-    {
-        dburl=$1
-        if [ "$skipExisting" -eq 1 -a -f "$(dbFname "$dburl")" ]; then
-            debug "Skipping existing file: $(dbFname "$dburl")"
-            return 0
-        fi
-        downloadDb "$dburl" || return 1
-        decompressDb "$dburl" || return 1
-        atomicRename "$dburl" || return 1
-        info "Updated $(basename "$(dbFname "$dburl")")"
-    }
-    for arg in "$@"; do
-        case "$arg" in
-            --skip-existing)
-                skipExisting=1
-                info "Option --skip-existing is set: not updating existing databases"
-                ;;
-            *)
-                error "Unknown argument: $arg";;
-        esac
-    done
-    waitNetworkOnline || die "Network is down (${dbBaseUrl} is unreachable)"
-    test -d "${cfg.databaseDir}" || die "Database directory (${cfg.databaseDir}) doesn't exist"
-    debug "Starting update of GeoIP databases in ${cfg.databaseDir}"
-    all_ret=0
-    for db in ${lib.concatStringsSep " \\\n        " cfg.databases}; do
-        downloadAndInstall "${dbBaseUrl}/$db" || all_ret=1
-    done
-    removeUnspecifiedDbs || all_ret=1
-    if [ $all_ret -eq 0 ]; then
-        info "Completed GeoIP database update in ${cfg.databaseDir}"
-    else
-        error "Completed GeoIP database update in ${cfg.databaseDir}, with error(s)"
-    fi
-    # Hack to work around systemd journal race:
-    # https://github.com/systemd/systemd/issues/2913
-    sleep 2
-    exit $all_ret
-  '';
-
-in
-
-{
-  options = {
-    services.geoip-updater = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Whether to enable periodic downloading of GeoIP databases from
-          maxmind.com. You might want to enable this if you, for instance, use
-          ntopng or Wireshark.
-        '';
-      };
-
-      interval = mkOption {
-        type = types.str;
-        default = "weekly";
-        description = ''
-          Update the GeoIP databases at this time / interval.
-          The format is described in
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
-          To prevent load spikes on maxmind.com, the timer interval is
-          randomized by an additional delay of ${randomizedTimerDelaySec}
-          seconds. Setting a shorter interval than this is not recommended.
-        '';
-      };
-
-      databaseDir = mkOption {
-        type = types.path;
-        default = "/var/lib/geoip-databases";
-        description = ''
-          Directory that will contain GeoIP databases.
-        '';
-      };
-
-      databases = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "GeoLiteCountry/GeoIP.dat.gz"
-          "GeoIPv6.dat.gz"
-          "GeoLiteCity.dat.xz"
-          "GeoLiteCityv6-beta/GeoLiteCityv6.dat.gz"
-          "asnum/GeoIPASNum.dat.gz"
-          "asnum/GeoIPASNumv6.dat.gz"
-          "GeoLite2-Country.mmdb.gz"
-          "GeoLite2-City.mmdb.gz"
-        ];
-        description = ''
-          Which GeoIP databases to update. The full URL is ${dbBaseUrl}/ +
-          <literal>the_database</literal>.
-        '';
-      };
-
-    };
-
-  };
-
-  config = mkIf cfg.enable {
-
-    assertions = [
-      { assertion = (builtins.filter
-          (x: builtins.match ".*\\.(gz|xz)$" x == null) cfg.databases) == [];
-        message = ''
-          services.geoip-updater.databases supports only .gz and .xz databases.
-
-          Current value:
-          ${toString cfg.databases}
-
-          Offending element(s):
-          ${toString (builtins.filter (x: builtins.match ".*\\.(gz|xz)$" x == null) cfg.databases)};
-        '';
-      }
-    ];
-
-    users.users.geoip = {
-      group = "root";
-      description = "GeoIP database updater";
-      uid = config.ids.uids.geoip;
-    };
-
-    systemd.timers.geoip-updater =
-      { description = "GeoIP Updater Timer";
-        partOf = [ "geoip-updater.service" ];
-        wantedBy = [ "timers.target" ];
-        timerConfig.OnCalendar = cfg.interval;
-        timerConfig.Persistent = "true";
-        timerConfig.RandomizedDelaySec = randomizedTimerDelaySec;
-      };
-
-    systemd.services.geoip-updater = {
-      description = "GeoIP Updater";
-      after = [ "network-online.target" "nss-lookup.target" ];
-      wants = [ "network-online.target" ];
-      preStart = ''
-        mkdir -p "${cfg.databaseDir}"
-        chmod 755 "${cfg.databaseDir}"
-        chown geoip:root "${cfg.databaseDir}"
-      '';
-      serviceConfig = {
-        ExecStart = "${geoip-updater}/bin/geoip-updater";
-        User = "geoip";
-        PermissionsStartOnly = true;
-      };
-    };
-
-    systemd.services.geoip-updater-setup = {
-      description = "GeoIP Updater Setup";
-      after = [ "network-online.target" "nss-lookup.target" ];
-      wants = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
-      conflicts = [ "geoip-updater.service" ];
-      preStart = ''
-        mkdir -p "${cfg.databaseDir}"
-        chmod 755 "${cfg.databaseDir}"
-        chown geoip:root "${cfg.databaseDir}"
-      '';
-      serviceConfig = {
-        ExecStart = "${geoip-updater}/bin/geoip-updater --skip-existing";
-        User = "geoip";
-        PermissionsStartOnly = true;
-        # So it won't be (needlessly) restarted:
-        RemainAfterExit = true;
-      };
-    };
-
-  };
-}
diff --git a/nixos/modules/services/misc/geoipupdate.nix b/nixos/modules/services/misc/geoipupdate.nix
new file mode 100644
index 00000000000..3211d4d88e4
--- /dev/null
+++ b/nixos/modules/services/misc/geoipupdate.nix
@@ -0,0 +1,187 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.geoipupdate;
+in
+{
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "geoip-updater" ] "services.geoip-updater has been removed, use services.geoipupdate instead.")
+  ];
+
+  options = {
+    services.geoipupdate = {
+      enable = lib.mkEnableOption ''
+        periodic downloading of GeoIP databases using
+        <productname>geoipupdate</productname>.
+      '';
+
+      interval = lib.mkOption {
+        type = lib.types.str;
+        default = "weekly";
+        description = ''
+          Update the GeoIP databases at this time / interval.
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      settings = lib.mkOption {
+        description = ''
+          <productname>geoipupdate</productname> configuration
+          options. See
+          <link xlink:href="https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md" />
+          for a full list of available options.
+        '';
+        type = lib.types.submodule {
+          freeformType =
+            with lib.types;
+            let
+              type = oneOf [str int bool];
+            in
+              attrsOf (either type (listOf type));
+
+          options = {
+
+            AccountID = lib.mkOption {
+              type = lib.types.int;
+              description = ''
+                Your MaxMind account ID.
+              '';
+            };
+
+            EditionIDs = lib.mkOption {
+              type = with lib.types; listOf (either str int);
+              example = [
+                "GeoLite2-ASN"
+                "GeoLite2-City"
+                "GeoLite2-Country"
+              ];
+              description = ''
+                List of database edition IDs. This includes new string
+                IDs like <literal>GeoIP2-City</literal> and old
+                numeric IDs like <literal>106</literal>.
+              '';
+            };
+
+            LicenseKey = lib.mkOption {
+              type = lib.types.path;
+              description = ''
+                A file containing the <productname>MaxMind</productname>
+                license key.
+              '';
+            };
+
+            DatabaseDirectory = lib.mkOption {
+              type = lib.types.path;
+              default = "/var/lib/GeoIP";
+              example = "/run/GeoIP";
+              description = ''
+                The directory to store the database files in. The
+                directory will be automatically created, the owner
+                changed to <literal>geoip</literal> and permissions
+                set to world readable. This applies if the directory
+                already exists as well, so don't use a directory with
+                sensitive contents.
+              '';
+            };
+
+          };
+        };
+      };
+    };
+
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    services.geoipupdate.settings = {
+      LockFile = "/run/geoipupdate/.lock";
+    };
+
+    systemd.services.geoipupdate-create-db-dir = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p ${cfg.settings.DatabaseDirectory}
+        chmod 0755 ${cfg.settings.DatabaseDirectory}
+      '';
+    };
+
+    systemd.services.geoipupdate = {
+      description = "GeoIP Updater";
+      requires = [ "geoipupdate-create-db-dir.service" ];
+      after = [
+        "geoipupdate-create-db-dir.service"
+        "network-online.target"
+        "nss-lookup.target"
+      ];
+      wants = [ "network-online.target" ];
+      startAt = cfg.interval;
+      serviceConfig = {
+        ExecStartPre =
+          let
+            geoipupdateKeyValue = lib.generators.toKeyValue {
+              mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
+                mkValueString = v: with builtins;
+                  if isInt           v then toString v
+                  else if isString   v then v
+                  else if true  ==   v then "1"
+                  else if false ==   v then "0"
+                  else if isList     v then lib.concatMapStringsSep " " mkValueString v
+                  else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+              };
+            };
+
+            geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
+
+            script = ''
+              chown geoip "${cfg.settings.DatabaseDirectory}"
+
+              cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
+              ${pkgs.replace-secret}/bin/replace-secret '${cfg.settings.LicenseKey}' \
+                                                        '${cfg.settings.LicenseKey}' \
+                                                        /run/geoipupdate/GeoIP.conf
+            '';
+          in
+            "+${pkgs.writeShellScript "start-pre-full-privileges" script}";
+        ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf";
+        User = "geoip";
+        DynamicUser = true;
+        ReadWritePaths = cfg.settings.DatabaseDirectory;
+        RuntimeDirectory = "geoipupdate";
+        RuntimeDirectoryMode = 0700;
+        CapabilityBoundingSet = "";
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        MemoryDenyWriteExecute = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    systemd.timers.geoipupdate-initial-run = {
+      wantedBy = [ "timers.target" ];
+      unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
+      timerConfig = {
+        Unit = "geoipupdate.service";
+        OnActiveSec = 0;
+      };
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index af80e99746b..b6c1ca3e61a 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -82,7 +82,7 @@ in
         };
 
         port = mkOption {
-          type = types.int;
+          type = types.port;
           default = (if !usePostgresql then 3306 else pg.port);
           description = "Database host port.";
         };
@@ -349,7 +349,7 @@ in
         {
           DOMAIN = cfg.domain;
           STATIC_ROOT_PATH = cfg.staticRootPath;
-          LFS_JWT_SECRET = "#jwtsecret#";
+          LFS_JWT_SECRET = "#lfsjwtsecret#";
           ROOT_URL = cfg.rootUrl;
         }
         (mkIf cfg.enableUnixSocket {
@@ -381,6 +381,7 @@ in
 
       security = {
         SECRET_KEY = "#secretkey#";
+        INTERNAL_TOKEN = "#internaltoken#";
         INSTALL_LOCK = true;
       };
 
@@ -396,6 +397,10 @@ in
       mailer = mkIf (cfg.mailerPasswordFile != null) {
         PASSWD = "#mailerpass#";
       };
+
+      oauth2 = {
+        JWT_SECRET = "#oauth2jwtsecret#";
+      };
     };
 
     services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
@@ -453,39 +458,68 @@ in
       description = "gitea";
       after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
       wantedBy = [ "multi-user.target" ];
-      path = [ gitea pkgs.gitAndTools.git ];
-
+      path = [ gitea pkgs.git ];
+
+      # In older versions the secret naming for JWT was kind of confusing.
+      # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
+      # wasn't persistant at all.
+      # To fix that, there is now the file oauth2_jwt_secret containing the
+      # values for JWT_SECRET and the file jwt_secret gets renamed to
+      # lfs_jwt_secret.
+      # We have to consider this to stay compatible with older installations.
       preStart = let
         runConfig = "${cfg.stateDir}/custom/conf/app.ini";
         secretKey = "${cfg.stateDir}/custom/conf/secret_key";
-        jwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret";
+        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
       in ''
         # copy custom configuration and generate a random secret key if needed
         ${optionalString (cfg.useWizard == false) ''
-          cp -f ${configFile} ${runConfig}
-
-          if [ ! -e ${secretKey} ]; then
-              ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
-          fi
-
-          if [ ! -e ${jwtSecret} ]; then
-              ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${jwtSecret}
-          fi
-
-          KEY="$(head -n1 ${secretKey})"
-          DBPASS="$(head -n1 ${cfg.database.passwordFile})"
-          JWTSECRET="$(head -n1 ${jwtSecret})"
-          ${if (cfg.mailerPasswordFile == null) then ''
-            MAILERPASSWORD="#mailerpass#"
-          '' else ''
-            MAILERPASSWORD="$(head -n1 ${cfg.mailerPasswordFile} || :)"
-          ''}
-          sed -e "s,#secretkey#,$KEY,g" \
-              -e "s,#dbpass#,$DBPASS,g" \
-              -e "s,#jwtsecret#,$JWTSECRET,g" \
-              -e "s,#mailerpass#,$MAILERPASSWORD,g" \
-              -i ${runConfig}
-          chmod 640 ${runConfig} ${secretKey} ${jwtSecret}
+          function gitea_setup {
+            cp -f ${configFile} ${runConfig}
+
+            if [ ! -e ${secretKey} ]; then
+                ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
+            fi
+
+            # Migrate LFS_JWT_SECRET filename
+            if [[ -e ${oldLfsJwtSecret} && ! -e ${lfsJwtSecret} ]]; then
+                mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+            fi
+
+            if [ ! -e ${oauth2JwtSecret} ]; then
+                ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
+            fi
+
+            if [ ! -e ${lfsJwtSecret} ]; then
+                ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+            fi
+
+            if [ ! -e ${internalToken} ]; then
+                ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
+            fi
+
+            SECRETKEY="$(head -n1 ${secretKey})"
+            DBPASS="$(head -n1 ${cfg.database.passwordFile})"
+            OAUTH2JWTSECRET="$(head -n1 ${oauth2JwtSecret})"
+            LFSJWTSECRET="$(head -n1 ${lfsJwtSecret})"
+            INTERNALTOKEN="$(head -n1 ${internalToken})"
+            ${if (cfg.mailerPasswordFile == null) then ''
+              MAILERPASSWORD="#mailerpass#"
+            '' else ''
+              MAILERPASSWORD="$(head -n1 ${cfg.mailerPasswordFile} || :)"
+            ''}
+            sed -e "s,#secretkey#,$SECRETKEY,g" \
+                -e "s,#dbpass#,$DBPASS,g" \
+                -e "s,#oauth2jwtsecret#,$OAUTH2JWTSECRET,g" \
+                -e "s,#lfsjwtsecret#,$LFSJWTSECRET,g" \
+                -e "s,#internaltoken#,$INTERNALTOKEN,g" \
+                -e "s,#mailerpass#,$MAILERPASSWORD,g" \
+                -i ${runConfig}
+          }
+          (umask 027; gitea_setup)
         ''}
 
         # update all hooks' binary paths
@@ -565,8 +599,7 @@ in
     users.groups.gitea = {};
 
     warnings =
-      optional (cfg.database.password != "") ''
-        config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead.'' ++
+      optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
       optional (cfg.extraConfig != null) ''
         services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`.
       '';
@@ -605,5 +638,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
-  meta.maintainers = with lib.maintainers; [ srhb ];
+  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
 }
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
index 1ec030549f9..f09565283f3 100644
--- a/nixos/modules/services/misc/gitit.nix
+++ b/nixos/modules/services/misc/gitit.nix
@@ -42,6 +42,7 @@ let
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           haskellPackages: [
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 425f35f37cb..1514cc0665d 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -10,7 +10,7 @@ let
   postgresqlPackage = if config.services.postgresql.enable then
                         config.services.postgresql.package
                       else
-                        pkgs.postgresql;
+                        pkgs.postgresql_12;
 
   gitlabSocket = "${cfg.statePath}/tmp/sockets/gitlab.socket";
   gitalySocket = "${cfg.statePath}/tmp/sockets/gitaly.socket";
@@ -43,9 +43,16 @@ let
 
     [gitlab-shell]
     dir = "${cfg.packages.gitlab-shell}"
+
+    [hooks]
+    custom_hooks_dir = "${cfg.statePath}/custom_hooks"
+
+    [gitlab]
     secret_file = "${cfg.statePath}/gitlab_shell_secret"
-    gitlab_url = "http+unix://${pathUrlQuote gitlabSocket}"
-    http_settings = { self_signed_cert = false }
+    url = "http+unix://${pathUrlQuote gitlabSocket}"
+
+    [gitlab.http-settings]
+    self_signed_cert = false
 
     ${concatStringsSep "\n" (attrValues (mapAttrs (k: v: ''
     [[storage]]
@@ -61,7 +68,6 @@ let
     repos_path = "${cfg.statePath}/repositories";
     secret_file = "${cfg.statePath}/gitlab_shell_secret";
     log_file = "${cfg.statePath}/log/gitlab-shell.log";
-    custom_hooks_dir = "${cfg.statePath}/custom_hooks";
     redis = {
       bin = "${pkgs.redis}/bin/redis-cli";
       host = "127.0.0.1";
@@ -73,6 +79,11 @@ let
 
   redisConfig.production.url = cfg.redisUrl;
 
+  pagesArgs = [
+    "-pages-domain" gitlabConfig.production.pages.host
+    "-pages-root" "${gitlabConfig.production.shared.path}/pages"
+  ] ++ cfg.pagesExtraArgs;
+
   gitlabConfig = {
     # These are the default settings from config/gitlab.example.yml
     production = flip recursiveUpdate cfg.extraConfig {
@@ -105,7 +116,12 @@ let
       omniauth.enabled = false;
       shared.path = "${cfg.statePath}/shared";
       gitaly.client_path = "${cfg.packages.gitaly}/bin";
-      backup.path = "${cfg.backupPath}";
+      backup = {
+        path = cfg.backup.path;
+        keep_time = cfg.backup.keepTime;
+      } // (optionalAttrs (cfg.backup.uploadOptions != {}) {
+        upload = cfg.backup.uploadOptions;
+      });
       gitlab_shell = {
         path = "${cfg.packages.gitlab-shell}";
         hooks_path = "${cfg.statePath}/shell/hooks";
@@ -114,6 +130,7 @@ let
         receive_pack = true;
       };
       workhorse.secret_file = "${cfg.statePath}/.gitlab_workhorse_secret";
+      gitlab_kas.secret_file = "${cfg.statePath}/.gitlab_kas_secret";
       git.bin_path = "git";
       monitoring = {
         ip_whitelist = [ "127.0.0.0/8" "::1/128" ];
@@ -123,14 +140,22 @@ let
           port = 3807;
         };
       };
+      registry = lib.optionalAttrs cfg.registry.enable {
+        enabled = true;
+        host = cfg.registry.externalAddress;
+        port = cfg.registry.externalPort;
+        key = cfg.registry.keyFile;
+        api_url = "http://${config.services.dockerRegistry.listenAddress}:${toString config.services.dockerRegistry.port}/";
+        issuer = "gitlab-issuer";
+      };
       extra = {};
       uploads.storage_path = cfg.statePath;
     };
   };
 
-  gitlabEnv = {
+  gitlabEnv = cfg.packages.gitlab.gitlabEnv // {
     HOME = "${cfg.statePath}/home";
-    UNICORN_PATH = "${cfg.statePath}/";
+    PUMA_PATH = "${cfg.statePath}/";
     GITLAB_PATH = "${cfg.packages.gitlab}/share/gitlab/";
     SCHEMA = "${cfg.statePath}/db/structure.sql";
     GITLAB_UPLOADS_PATH = "${cfg.statePath}/uploads";
@@ -138,7 +163,8 @@ let
     GITLAB_REDIS_CONFIG_FILE = pkgs.writeText "redis.yml" (builtins.toJSON redisConfig);
     prometheus_multiproc_dir = "/run/gitlab";
     RAILS_ENV = "production";
-  };
+    MALLOC_ARENA_MAX = "2";
+  } // cfg.extraEnv;
 
   gitlab-rake = pkgs.stdenv.mkDerivation {
     name = "gitlab-rake";
@@ -184,6 +210,7 @@ let
         domain: "${cfg.smtp.domain}",
         ${optionalString (cfg.smtp.authentication != null) "authentication: :${cfg.smtp.authentication},"}
         enable_starttls_auto: ${boolToString cfg.smtp.enableStartTLSAuto},
+        tls: ${boolToString cfg.smtp.tls},
         ca_file: "/etc/ssl/certs/ca-certificates.crt",
         openssl_verify_mode: '${cfg.smtp.opensslVerifyMode}'
       }
@@ -194,6 +221,7 @@ in {
 
   imports = [
     (mkRenamedOptionModule [ "services" "gitlab" "stateDir" ] [ "services" "gitlab" "statePath" ])
+    (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
     (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
   ];
 
@@ -236,11 +264,18 @@ in {
         description = "Reference to the gitaly package";
       };
 
+      packages.pages = mkOption {
+        type = types.package;
+        default = pkgs.gitlab-pages;
+        defaultText = "pkgs.gitlab-pages";
+        description = "Reference to the gitlab-pages package";
+      };
+
       statePath = mkOption {
         type = types.str;
         default = "/var/gitlab/state";
         description = ''
-          Gitlab state directory. Configuration, repositories and
+          GitLab state directory. Configuration, repositories and
           logs, among other things, are stored here.
 
           The directory will be created automatically if it doesn't
@@ -250,17 +285,116 @@ in {
         '';
       };
 
-      backupPath = mkOption {
+      extraEnv = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = ''
+          Additional environment variables for the GitLab environment.
+        '';
+      };
+
+      backup.startAt = mkOption {
+        type = with types; either str (listOf str);
+        default = [];
+        example = "03:00";
+        description = ''
+          The time(s) to run automatic backup of GitLab
+          state. Specified in systemd's time format; see
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      backup.path = mkOption {
         type = types.str;
         default = cfg.statePath + "/backup";
-        description = "Gitlab path for backups.";
+        description = "GitLab path for backups.";
+      };
+
+      backup.keepTime = mkOption {
+        type = types.int;
+        default = 0;
+        example = 48;
+        apply = x: x * 60 * 60;
+        description = ''
+          How long to keep the backups around, in
+          hours. <literal>0</literal> means <quote>keep
+          forever</quote>.
+        '';
+      };
+
+      backup.skip = mkOption {
+        type = with types;
+          let value = enum [
+                "db"
+                "uploads"
+                "builds"
+                "artifacts"
+                "lfs"
+                "registry"
+                "pages"
+                "repositories"
+                "tar"
+              ];
+          in
+            either value (listOf value);
+        default = [];
+        example = [ "artifacts" "lfs" ];
+        apply = x: if isString x then x else concatStringsSep "," x;
+        description = ''
+          Directories to exclude from the backup. The example excludes
+          CI artifacts and LFS objects from the backups. The
+          <literal>tar</literal> option skips the creation of a tar
+          file.
+
+          Refer to <link xlink:href="https://docs.gitlab.com/ee/raketasks/backup_restore.html#excluding-specific-directories-from-the-backup"/>
+          for more information.
+        '';
+      };
+
+      backup.uploadOptions = mkOption {
+        type = types.attrs;
+        default = {};
+        example = literalExample ''
+          {
+            # Fog storage connection settings, see http://fog.io/storage/
+            connection = {
+              provider = "AWS";
+              region = "eu-north-1";
+              aws_access_key_id = "AKIAXXXXXXXXXXXXXXXX";
+              aws_secret_access_key = { _secret = config.deployment.keys.aws_access_key.path; };
+            };
+
+            # The remote 'directory' to store your backups in.
+            # For S3, this would be the bucket name.
+            remote_directory = "my-gitlab-backups";
+
+            # Use multipart uploads when file size reaches 100MB, see
+            # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
+            multipart_chunk_size = 104857600;
+
+            # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+            encryption = "AES256";
+
+            # Specifies Amazon S3 storage class to use for backups, this is optional
+            storage_class = "STANDARD";
+          };
+        '';
+        description = ''
+          GitLab automatic upload specification. Tells GitLab to
+          upload the backup to a remote location when done.
+
+          Attributes specified here are added under
+          <literal>production -> backup -> upload</literal> in
+          <filename>config/gitlab.yml</filename>.
+        '';
       };
 
       databaseHost = mkOption {
         type = types.str;
         default = "";
         description = ''
-          Gitlab database hostname. An empty string means <quote>use
+          GitLab database hostname. An empty string means <quote>use
           local unix socket connection</quote>.
         '';
       };
@@ -269,7 +403,7 @@ in {
         type = with types; nullOr path;
         default = null;
         description = ''
-          File containing the Gitlab database user password.
+          File containing the GitLab database user password.
 
           This should be a string, not a nix path, since nix paths are
           copied into the world-readable nix store.
@@ -290,13 +424,13 @@ in {
       databaseName = mkOption {
         type = types.str;
         default = "gitlab";
-        description = "Gitlab database name.";
+        description = "GitLab database name.";
       };
 
       databaseUsername = mkOption {
         type = types.str;
         default = "gitlab";
-        description = "Gitlab database user.";
+        description = "GitLab database user.";
       };
 
       databasePool = mkOption {
@@ -340,14 +474,14 @@ in {
       host = mkOption {
         type = types.str;
         default = config.networking.hostName;
-        description = "Gitlab host name. Used e.g. for copy-paste URLs.";
+        description = "GitLab host name. Used e.g. for copy-paste URLs.";
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8080;
         description = ''
-          Gitlab server port for copy-paste URLs, e.g. 80 or 443 if you're
+          GitLab server port for copy-paste URLs, e.g. 80 or 443 if you're
           service over https.
         '';
       };
@@ -390,6 +524,58 @@ in {
         '';
       };
 
+      registry = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Enable GitLab container registry.";
+        };
+        host = mkOption {
+          type = types.str;
+          default = config.services.gitlab.host;
+          description = "GitLab container registry host name.";
+        };
+        port = mkOption {
+          type = types.int;
+          default = 4567;
+          description = "GitLab container registry port.";
+        };
+        certFile = mkOption {
+          type = types.path;
+          default = null;
+          description = "Path to GitLab container registry certificate.";
+        };
+        keyFile = mkOption {
+          type = types.path;
+          default = null;
+          description = "Path to GitLab container registry certificate-key.";
+        };
+        defaultForProjects = mkOption {
+          type = types.bool;
+          default = cfg.registry.enable;
+          description = "If GitLab container registry should be enabled by default for projects.";
+        };
+        issuer = mkOption {
+          type = types.str;
+          default = "gitlab-issuer";
+          description = "GitLab container registry issuer.";
+        };
+        serviceName = mkOption {
+          type = types.str;
+          default = "container_registry";
+          description = "GitLab container registry service name.";
+        };
+        externalAddress = mkOption {
+          type = types.str;
+          default = "";
+          description = "External address used to access registry from the internet";
+        };
+        externalPort = mkOption {
+          type = types.int;
+          description = "External port used to access registry from the internet";
+        };
+      };
+
       smtp = {
         enable = mkOption {
           type = types.bool;
@@ -400,26 +586,26 @@ in {
         address = mkOption {
           type = types.str;
           default = "localhost";
-          description = "Address of the SMTP server for Gitlab.";
+          description = "Address of the SMTP server for GitLab.";
         };
 
         port = mkOption {
           type = types.int;
-          default = 465;
-          description = "Port of the SMTP server for Gitlab.";
+          default = 25;
+          description = "Port of the SMTP server for GitLab.";
         };
 
         username = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = "Username of the SMTP server for Gitlab.";
+          description = "Username of the SMTP server for GitLab.";
         };
 
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
           description = ''
-            File containing the password of the SMTP server for Gitlab.
+            File containing the password of the SMTP server for GitLab.
 
             This should be a string, not a nix path, since nix paths
             are copied into the world-readable nix store.
@@ -435,7 +621,7 @@ in {
         authentication = mkOption {
           type = with types; nullOr str;
           default = null;
-          description = "Authentitcation type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
+          description = "Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html";
         };
 
         enableStartTLSAuto = mkOption {
@@ -444,6 +630,12 @@ in {
           description = "Whether to try to use StartTLS.";
         };
 
+        tls = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether to use TLS wrapper-mode.";
+        };
+
         opensslVerifyMode = mkOption {
           type = types.str;
           default = "peer";
@@ -451,6 +643,12 @@ in {
         };
       };
 
+      pagesExtraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ "-listen-proxy" "127.0.0.1:8090" ];
+        description = "Arguments to pass to the gitlab-pages daemon";
+      };
+
       secrets.secretFile = mkOption {
         type = with types; nullOr path;
         default = null;
@@ -459,7 +657,7 @@ in {
           the DB. If you change or lose this key you will be unable to
           access variables stored in database.
 
-          Make sure the secret is at least 30 characters and all random,
+          Make sure the secret is at least 32 characters and all random,
           no regular words or you'll be exposed to dictionary attacks.
 
           This should be a string, not a nix path, since nix paths are
@@ -475,7 +673,7 @@ in {
           the DB. If you change or lose this key you will be unable to
           access variables stored in database.
 
-          Make sure the secret is at least 30 characters and all random,
+          Make sure the secret is at least 32 characters and all random,
           no regular words or you'll be exposed to dictionary attacks.
 
           This should be a string, not a nix path, since nix paths are
@@ -491,7 +689,7 @@ in {
           tokens. If you change or lose this key, users which have 2FA
           enabled for login won't be able to login anymore.
 
-          Make sure the secret is at least 30 characters and all random,
+          Make sure the secret is at least 32 characters and all random,
           no regular words or you'll be exposed to dictionary attacks.
 
           This should be a string, not a nix path, since nix paths are
@@ -523,6 +721,105 @@ in {
         description = "Extra configuration to merge into shell-config.yml";
       };
 
+      puma.workers = mkOption {
+        type = types.int;
+        default = 2;
+        apply = x: builtins.toString x;
+        description = ''
+          The number of worker processes Puma should spawn. This
+          controls the amount of parallel Ruby code can be
+          executed. GitLab recommends <quote>Number of CPU cores -
+          1</quote>, but at least two.
+
+          <note>
+            <para>
+              Each worker consumes quite a bit of memory, so
+              be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      puma.threadsMin = mkOption {
+        type = types.int;
+        default = 0;
+        apply = x: builtins.toString x;
+        description = ''
+          The minimum number of threads Puma should use per
+          worker.
+
+          <note>
+            <para>
+              Each thread consumes memory and contributes to Global VM
+              Lock contention, so be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      puma.threadsMax = mkOption {
+        type = types.int;
+        default = 4;
+        apply = x: builtins.toString x;
+        description = ''
+          The maximum number of threads Puma should use per
+          worker. This limits how many threads Puma will automatically
+          spawn in response to requests. In contrast to workers,
+          threads will never be able to run Ruby code in parallel, but
+          give higher IO parallelism.
+
+          <note>
+            <para>
+              Each thread consumes memory and contributes to Global VM
+              Lock contention, so be careful when increasing this.
+            </para>
+          </note>
+        '';
+      };
+
+      sidekiq.memoryKiller.enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the Sidekiq MemoryKiller should be turned
+          on. MemoryKiller kills Sidekiq when its memory consumption
+          exceeds a certain limit.
+
+          See <link xlink:href="https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html"/>
+          for details.
+        '';
+      };
+
+      sidekiq.memoryKiller.maxMemory = mkOption {
+        type = types.int;
+        default = 2000;
+        apply = x: builtins.toString (x * 1024);
+        description = ''
+          The maximum amount of memory, in MiB, a Sidekiq worker is
+          allowed to consume before being killed.
+        '';
+      };
+
+      sidekiq.memoryKiller.graceTime = mkOption {
+        type = types.int;
+        default = 900;
+        apply = x: builtins.toString x;
+        description = ''
+          The time MemoryKiller waits after noticing excessive memory
+          consumption before killing Sidekiq.
+        '';
+      };
+
+      sidekiq.memoryKiller.shutdownWait = mkOption {
+        type = types.int;
+        default = 30;
+        apply = x: builtins.toString x;
+        description = ''
+          The time allowed for all jobs to finish before Sidekiq is
+          killed forcefully.
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.attrs;
         default = {};
@@ -612,10 +909,19 @@ in {
         assertion = cfg.secrets.jwsFile != null;
         message = "services.gitlab.secrets.jwsFile must be set!";
       }
+      {
+        assertion = versionAtLeast postgresqlPackage.version "12.0.0";
+        message = "PostgreSQL >=12 is required to run GitLab 14. Follow the instructions in the manual section for upgrading PostgreSQL here: https://nixos.org/manual/nixos/stable/index.html#module-services-postgres-upgrading";
+      }
     ];
 
     environment.systemPackages = [ pkgs.git gitlab-rake gitlab-rails cfg.packages.gitlab-shell ];
 
+    systemd.targets.gitlab = {
+      description = "Common target for all GitLab services.";
+      wantedBy = [ "multi-user.target" ];
+    };
+
     # Redis is required for the sidekiq queue runner.
     services.redis.enable = mkDefault true;
 
@@ -630,35 +936,83 @@ in {
     # here.
     systemd.services.gitlab-postgresql = let pgsql = config.services.postgresql; in mkIf databaseActuallyCreateLocally {
       after = [ "postgresql.service" ];
-      wantedBy = [ "multi-user.target" ];
-      path = [ pgsql.package ];
+      bindsTo = [ "postgresql.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = [
+        pgsql.package
+        pkgs.util-linux
+      ];
       script = ''
         set -eu
 
-        PSQL="${pkgs.utillinux}/bin/runuser -u ${pgsql.superUser} -- psql --port=${toString pgsql.port}"
+        PSQL() {
+            psql --port=${toString pgsql.port} "$@"
+        }
 
-        $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
-        current_owner=$($PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
+        PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.databaseName}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.databaseName}" OWNER "${cfg.databaseUsername}"'
+        current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.databaseName}'")
         if [[ "$current_owner" != "${cfg.databaseUsername}" ]]; then
-            $PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
+            PSQL -tAc 'ALTER DATABASE "${cfg.databaseName}" OWNER TO "${cfg.databaseUsername}"'
             if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}" ]]; then
                 echo "Reassigning ownership of database ${cfg.databaseName} to user ${cfg.databaseUsername} failed on last boot. Failing..."
                 exit 1
             fi
             touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
-            $PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
+            PSQL "${cfg.databaseName}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.databaseUsername}\""
             rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.databaseName}"
         fi
-        $PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+        PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+        PSQL '${cfg.databaseName}' -tAc "CREATE EXTENSION IF NOT EXISTS btree_gist;"
       '';
 
       serviceConfig = {
+        User = pgsql.superUser;
         Type = "oneshot";
+        RemainAfterExit = true;
+      };
+    };
+
+    systemd.services.gitlab-registry-cert = optionalAttrs cfg.registry.enable {
+      path = with pkgs; [ openssl ];
+
+      script = ''
+        mkdir -p $(dirname ${cfg.registry.keyFile})
+        mkdir -p $(dirname ${cfg.registry.certFile})
+        openssl req -nodes -newkey rsa:4096 -keyout ${cfg.registry.keyFile} -out /tmp/registry-auth.csr -subj "/CN=${cfg.registry.issuer}"
+        openssl x509 -in /tmp/registry-auth.csr -out ${cfg.registry.certFile} -req -signkey ${cfg.registry.keyFile} -days 3650
+        chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.keyFile})
+        chown ${cfg.user}:${cfg.group} $(dirname ${cfg.registry.certFile})
+        chown ${cfg.user}:${cfg.group} ${cfg.registry.keyFile}
+        chown ${cfg.user}:${cfg.group} ${cfg.registry.certFile}
+      '';
+
+      serviceConfig = {
+        ConditionPathExists = "!${cfg.registry.certFile}";
+      };
+    };
+
+    # Ensure Docker Registry launches after the certificate generation job
+    systemd.services.docker-registry = optionalAttrs cfg.registry.enable {
+      wants = [ "gitlab-registry-cert.service" ];
+    };
+
+    # Enable Docker Registry, if GitLab-Container Registry is enabled
+    services.dockerRegistry = optionalAttrs cfg.registry.enable {
+      enable = true;
+      enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
+      extraConfig = {
+        auth.token = {
+          realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth";
+          service = cfg.registry.serviceName;
+          issuer = cfg.registry.issuer;
+          rootcertbundle = cfg.registry.certFile;
+        };
       };
     };
 
     # Use postfix to send out mails.
-    services.postfix.enable = mkDefault true;
+    services.postfix.enable = mkDefault (cfg.smtp.enable && cfg.smtp.address == "localhost");
 
     users.users.${cfg.user} =
       { group = cfg.group;
@@ -673,11 +1027,10 @@ in {
       "d /run/gitlab 0755 ${cfg.user} ${cfg.group} -"
       "d ${gitlabEnv.HOME} 0750 ${cfg.user} ${cfg.group} -"
       "z ${gitlabEnv.HOME}/.ssh/authorized_keys 0600 ${cfg.user} ${cfg.group} -"
-      "d ${cfg.backupPath} 0750 ${cfg.user} ${cfg.group} -"
+      "d ${cfg.backup.path} 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath} 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/builds 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/config 0750 ${cfg.user} ${cfg.group} -"
-      "d ${cfg.statePath}/config/initializers 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/db 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/log 0750 ${cfg.user} ${cfg.group} -"
       "d ${cfg.statePath}/repositories 2770 ${cfg.user} ${cfg.group} -"
@@ -700,17 +1053,163 @@ in {
       "L+ /run/gitlab/uploads - - - - ${cfg.statePath}/uploads"
 
       "L+ /run/gitlab/shell-config.yml - - - - ${pkgs.writeText "config.yml" (builtins.toJSON gitlabShellConfig)}"
-
-      "L+ ${cfg.statePath}/config/unicorn.rb - - - - ${./defaultUnicornConfig.rb}"
     ];
 
+
+    systemd.services.gitlab-config = {
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      path = with pkgs; [
+        jq
+        openssl
+        replace-secret
+        git
+      ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        RemainAfterExit = true;
+
+        ExecStartPre = let
+          preStartFullPrivileges = ''
+            shopt -s dotglob nullglob
+            set -eu
+
+            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
+            if [[ -n "$(ls -A '${cfg.statePath}'/config/)" ]]; then
+              chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
+            fi
+          '';
+        in "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}";
+
+        ExecStart = pkgs.writeShellScript "gitlab-config" ''
+          set -eu
+
+          umask u=rwx,g=rx,o=
+
+          cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
+          rm -rf ${cfg.statePath}/db/*
+          rm -f ${cfg.statePath}/lib
+          find '${cfg.statePath}/config/' -maxdepth 1 -mindepth 1 -type d -execdir rm -rf {} \;
+          cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
+          cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
+          ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
+
+          ${cfg.packages.gitlab-shell}/bin/install
+
+          ${optionalString cfg.smtp.enable ''
+              install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
+              ${optionalString (cfg.smtp.passwordFile != null) ''
+                  replace-secret '@smtpPassword@' '${cfg.smtp.passwordFile}' '${cfg.statePath}/config/initializers/smtp_settings.rb'
+              ''}
+          ''}
+
+          (
+            umask u=rwx,g=,o=
+
+            openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
+
+            rm -f '${cfg.statePath}/config/database.yml'
+
+            ${if cfg.databasePasswordFile != null then ''
+                export db_password="$(<'${cfg.databasePasswordFile}')"
+
+                if [[ -z "$db_password" ]]; then
+                  >&2 echo "Database password was an empty string!"
+                  exit 1
+                fi
+
+                jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
+                   '.production.password = $ENV.db_password' \
+                   >'${cfg.statePath}/config/database.yml'
+              ''
+              else ''
+                jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
+                   >'${cfg.statePath}/config/database.yml'
+              ''
+            }
+
+            ${utils.genJqSecretsReplacementSnippet
+                gitlabConfig
+                "${cfg.statePath}/config/gitlab.yml"
+            }
+
+            rm -f '${cfg.statePath}/config/secrets.yml'
+
+            export secret="$(<'${cfg.secrets.secretFile}')"
+            export db="$(<'${cfg.secrets.dbFile}')"
+            export otp="$(<'${cfg.secrets.otpFile}')"
+            export jws="$(<'${cfg.secrets.jwsFile}')"
+            jq -n '{production: {secret_key_base: $ENV.secret,
+                    otp_key_base: $ENV.otp,
+                    db_key_base: $ENV.db,
+                    openid_connect_signing_key: $ENV.jws}}' \
+               > '${cfg.statePath}/config/secrets.yml'
+          )
+
+          # We remove potentially broken links to old gitlab-shell versions
+          rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
+
+          git config --global core.autocrlf "input"
+        '';
+      };
+    };
+
+    systemd.services.gitlab-db-config = {
+      after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ];
+      bindsTo = [
+        "gitlab-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service"
+        ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
+        RemainAfterExit = true;
+
+        ExecStart = pkgs.writeShellScript "gitlab-db-config" ''
+          set -eu
+          umask u=rwx,g=rx,o=
+
+          initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
+          ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
+                                                             GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
+        '';
+      };
+    };
+
     systemd.services.gitlab-sidekiq = {
-      after = [ "network.target" "redis.service" "gitlab.service" ];
-      wantedBy = [ "multi-user.target" ];
-      environment = gitlabEnv;
+      after = [
+        "network.target"
+        "redis.service"
+        "postgresql.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable {
+        SIDEKIQ_MEMORY_KILLER_MAX_RSS = cfg.sidekiq.memoryKiller.maxMemory;
+        SIDEKIQ_MEMORY_KILLER_GRACE_TIME = cfg.sidekiq.memoryKiller.graceTime;
+        SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT = cfg.sidekiq.memoryKiller.shutdownWait;
+      });
       path = with pkgs; [
         postgresqlPackage
-        gitAndTools.git
+        git
         ruby
         openssh
         nodejs
@@ -719,25 +1218,29 @@ in {
         # Needed for GitLab project imports
         gnutar
         gzip
+
+        procps # Sidekiq MemoryKiller
       ];
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
         Group = cfg.group;
         TimeoutSec = "infinity";
-        Restart = "on-failure";
+        Restart = "always";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
         ExecStart="${cfg.packages.gitlab.rubyEnv}/bin/sidekiq -C \"${cfg.packages.gitlab}/share/gitlab/config/sidekiq_queues.yml\" -e production";
       };
     };
 
     systemd.services.gitaly = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       path = with pkgs; [
         openssh
         procps  # See https://gitlab.com/gitlab-org/gitaly/issues/1562
-        gitAndTools.git
+        git
         cfg.packages.gitaly.rubyEnv
         cfg.packages.gitaly.rubyEnv.wrappedRuby
         gzip
@@ -754,12 +1257,35 @@ in {
       };
     };
 
+    systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) {
+      description = "GitLab static pages daemon";
+      after = [ "network.target" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+
+      path = [ pkgs.unzip ];
+
+      serviceConfig = {
+        Type = "simple";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+
+        User = cfg.user;
+        Group = cfg.group;
+
+        ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}";
+        WorkingDirectory = gitlabEnv.HOME;
+      };
+    };
+
     systemd.services.gitlab-workhorse = {
       after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       path = with pkgs; [
         exiftool
-        gitAndTools.git
+        git
         gnutar
         gzip
         openssh
@@ -783,14 +1309,44 @@ in {
       };
     };
 
+    systemd.services.gitlab-mailroom = mkIf (gitlabConfig.production.incoming_email.enabled or false) {
+      description = "GitLab incoming mail daemon";
+      after = [ "network.target" "redis.service" "gitlab-config.service" ];
+      bindsTo = [ "gitlab-config.service" ];
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
+      environment = gitlabEnv;
+      serviceConfig = {
+        Type = "simple";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/bundle exec mail_room -c ${cfg.statePath}/config/mail_room.yml";
+        WorkingDirectory = gitlabEnv.HOME;
+      };
+    };
+
     systemd.services.gitlab = {
-      after = [ "gitlab-workhorse.service" "gitaly.service" "network.target" "gitlab-postgresql.service" "redis.service" ];
-      requires = [ "gitlab-sidekiq.service" ];
-      wantedBy = [ "multi-user.target" ];
+      after = [
+        "gitlab-workhorse.service"
+        "network.target"
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ];
+      bindsTo = [
+        "redis.service"
+        "gitlab-config.service"
+        "gitlab-db-config.service"
+      ] ++ optional (cfg.databaseHost == "") "postgresql.service";
+      wantedBy = [ "gitlab.target" ];
+      partOf = [ "gitlab.target" ];
       environment = gitlabEnv;
       path = with pkgs; [
         postgresqlPackage
-        gitAndTools.git
+        git
         openssh
         nodejs
         procps
@@ -804,100 +1360,34 @@ in {
         TimeoutSec = "infinity";
         Restart = "on-failure";
         WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab";
-        ExecStartPre = let
-          preStartFullPrivileges = ''
-            shopt -s dotglob nullglob
-            set -eu
-
-            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/*
-            chown --no-dereference '${cfg.user}':'${cfg.group}' '${cfg.statePath}'/config/*
-          '';
-          preStart = ''
-            set -eu
-
-            cp -f ${cfg.packages.gitlab}/share/gitlab/VERSION ${cfg.statePath}/VERSION
-            rm -rf ${cfg.statePath}/db/*
-            rm -rf ${cfg.statePath}/config/initializers/*
-            rm -f ${cfg.statePath}/lib
-            cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/config.dist/* ${cfg.statePath}/config
-            cp -rf --no-preserve=mode ${cfg.packages.gitlab}/share/gitlab/db/* ${cfg.statePath}/db
-            ln -sf ${extraGitlabRb} ${cfg.statePath}/config/initializers/extra-gitlab.rb
-
-            ${cfg.packages.gitlab-shell}/bin/install
-
-            ${optionalString cfg.smtp.enable ''
-              install -m u=rw ${smtpSettings} ${cfg.statePath}/config/initializers/smtp_settings.rb
-              ${optionalString (cfg.smtp.passwordFile != null) ''
-                smtp_password=$(<'${cfg.smtp.passwordFile}')
-                ${pkgs.replace}/bin/replace-literal -e '@smtpPassword@' "$smtp_password" '${cfg.statePath}/config/initializers/smtp_settings.rb'
-              ''}
-            ''}
-
-            (
-              umask u=rwx,g=,o=
-
-              ${pkgs.openssl}/bin/openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
-
-              if [[ -h '${cfg.statePath}/config/database.yml' ]]; then
-                rm '${cfg.statePath}/config/database.yml'
-              fi
-
-              ${if cfg.databasePasswordFile != null then ''
-                  export db_password="$(<'${cfg.databasePasswordFile}')"
-
-                  if [[ -z "$db_password" ]]; then
-                    >&2 echo "Database password was an empty string!"
-                    exit 1
-                  fi
-
-                  ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                                    '.production.password = $ENV.db_password' \
-                                    >'${cfg.statePath}/config/database.yml'
-                ''
-                else ''
-                  ${pkgs.jq}/bin/jq <${pkgs.writeText "database.yml" (builtins.toJSON databaseConfig)} \
-                                    >'${cfg.statePath}/config/database.yml'
-                ''
-              }
-
-              ${utils.genJqSecretsReplacementSnippet
-                  gitlabConfig
-                  "${cfg.statePath}/config/gitlab.yml"
-              }
-
-              if [[ -h '${cfg.statePath}/config/secrets.yml' ]]; then
-                rm '${cfg.statePath}/config/secrets.yml'
-              fi
-
-              export secret="$(<'${cfg.secrets.secretFile}')"
-              export db="$(<'${cfg.secrets.dbFile}')"
-              export otp="$(<'${cfg.secrets.otpFile}')"
-              export jws="$(<'${cfg.secrets.jwsFile}')"
-              ${pkgs.jq}/bin/jq -n '{production: {secret_key_base: $ENV.secret,
-                                                  otp_key_base: $ENV.otp,
-                                                  db_key_base: $ENV.db,
-                                                  openid_connect_signing_key: $ENV.jws}}' \
-                                > '${cfg.statePath}/config/secrets.yml'
-            )
-
-            initial_root_password="$(<'${cfg.initialRootPasswordFile}')"
-            ${gitlab-rake}/bin/gitlab-rake gitlab:db:configure GITLAB_ROOT_PASSWORD="$initial_root_password" \
-                                                               GITLAB_ROOT_EMAIL='${cfg.initialRootEmail}' > /dev/null
-
-            # We remove potentially broken links to old gitlab-shell versions
-            rm -Rf ${cfg.statePath}/repositories/**/*.git/hooks
-
-            ${pkgs.git}/bin/git config --global core.autocrlf "input"
-          '';
-        in [
-          "+${pkgs.writeShellScript "gitlab-pre-start-full-privileges" preStartFullPrivileges}"
-          "${pkgs.writeShellScript "gitlab-pre-start" preStart}"
+        ExecStart = concatStringsSep " " [
+          "${cfg.packages.gitlab.rubyEnv}/bin/puma"
+          "-e production"
+          "-C ${cfg.statePath}/config/puma.rb"
+          "-w ${cfg.puma.workers}"
+          "-t ${cfg.puma.threadsMin}:${cfg.puma.threadsMax}"
         ];
-        ExecStart = "${cfg.packages.gitlab.rubyEnv}/bin/unicorn -c ${cfg.statePath}/config/unicorn.rb -E production";
       };
 
     };
 
+    systemd.services.gitlab-backup = {
+      after = [ "gitlab.service" ];
+      bindsTo = [ "gitlab.service" ];
+      startAt = cfg.backup.startAt;
+      environment = {
+        RAILS_ENV = "production";
+        CRON = "1";
+      } // optionalAttrs (stringLength cfg.backup.skip > 0) {
+        SKIP = cfg.backup.skip;
+      };
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${gitlab-rake}/bin/gitlab-rake gitlab:backup:create";
+      };
+    };
+
   };
 
   meta.doc = ./gitlab.xml;
diff --git a/nixos/modules/services/misc/gitlab.xml b/nixos/modules/services/misc/gitlab.xml
index b6171a9a194..40424c5039a 100644
--- a/nixos/modules/services/misc/gitlab.xml
+++ b/nixos/modules/services/misc/gitlab.xml
@@ -3,15 +3,15 @@
          xmlns:xi="http://www.w3.org/2001/XInclude"
          version="5.0"
          xml:id="module-services-gitlab">
- <title>Gitlab</title>
+ <title>GitLab</title>
  <para>
-  Gitlab is a feature-rich git hosting service.
+  GitLab is a feature-rich git hosting service.
  </para>
  <section xml:id="module-services-gitlab-prerequisites">
   <title>Prerequisites</title>
 
   <para>
-   The gitlab service exposes only an Unix socket at
+   The <literal>gitlab</literal> service exposes only an Unix socket at
    <literal>/run/gitlab/gitlab-workhorse.socket</literal>. You need to
    configure a webserver to proxy HTTP requests to the socket.
   </para>
@@ -39,7 +39,7 @@
   <title>Configuring</title>
 
   <para>
-   Gitlab depends on both PostgreSQL and Redis and will automatically enable
+   GitLab depends on both PostgreSQL and Redis and will automatically enable
    both services. In the case of PostgreSQL, a database and a role will be
    created.
   </para>
@@ -85,19 +85,25 @@ services.gitlab = {
   </para>
 
   <para>
-   If you're setting up a new Gitlab instance, generate new
+   If you're setting up a new GitLab instance, generate new
    secrets. You for instance use <literal>tr -dc A-Za-z0-9 &lt;
    /dev/urandom | head -c 128 &gt; /var/keys/gitlab/db</literal> to
    generate a new db secret. Make sure the files can be read by, and
    only by, the user specified by <link
-   linkend="opt-services.gitlab.user">services.gitlab.user</link>. Gitlab
+   linkend="opt-services.gitlab.user">services.gitlab.user</link>. GitLab
    encrypts sensitive data stored in the database. If you're restoring
-   an existing Gitlab instance, you must specify the secrets secret
-   from <literal>config/secrets.yml</literal> located in your Gitlab
+   an existing GitLab instance, you must specify the secrets secret
+   from <literal>config/secrets.yml</literal> located in your GitLab
    state folder.
   </para>
 
   <para>
+    When <literal>incoming_mail.enabled</literal> is set to <literal>true</literal>
+    in <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> an additional
+    service called <literal>gitlab-mailroom</literal> is enabled for fetching incoming mail.
+  </para>
+
+  <para>
    Refer to <xref linkend="ch-options" /> for all available configuration
    options for the
    <link linkend="opt-services.gitlab.enable">services.gitlab</link> module.
@@ -106,21 +112,40 @@ services.gitlab = {
  <section xml:id="module-services-gitlab-maintenance">
   <title>Maintenance</title>
 
-  <para>
-   You can run Gitlab's rake tasks with <literal>gitlab-rake</literal> which
-   will be available on the system when gitlab is enabled. You will have to run
-   the command as the user that you configured to run gitlab with.
-  </para>
+  <section xml:id="module-services-gitlab-maintenance-backups">
+   <title>Backups</title>
+   <para>
+     Backups can be configured with the options in <link
+     linkend="opt-services.gitlab.backup.keepTime">services.gitlab.backup</link>. Use
+     the <link
+     linkend="opt-services.gitlab.backup.startAt">services.gitlab.backup.startAt</link>
+     option to configure regular backups.
+   </para>
 
-  <para>
-   For example, to backup a Gitlab instance:
+   <para>
+     To run a manual backup, start the <literal>gitlab-backup</literal> service:
 <screen>
-<prompt>$ </prompt>sudo -u git -H gitlab-rake gitlab:backup:create
+<prompt>$ </prompt>systemctl start gitlab-backup.service
 </screen>
-   A list of all availabe rake tasks can be obtained by running:
+   </para>
+  </section>
+
+  <section xml:id="module-services-gitlab-maintenance-rake">
+   <title>Rake tasks</title>
+
+   <para>
+    You can run GitLab's rake tasks with <literal>gitlab-rake</literal>
+    which will be available on the system when GitLab is enabled. You
+    will have to run the command as the user that you configured to run
+    GitLab with.
+   </para>
+
+   <para>
+    A list of all availabe rake tasks can be obtained by running:
 <screen>
 <prompt>$ </prompt>sudo -u git -H gitlab-rake -T
 </screen>
-  </para>
+   </para>
+  </section>
  </section>
 </chapter>
diff --git a/nixos/modules/services/misc/gitolite.nix b/nixos/modules/services/misc/gitolite.nix
index 59cbdac319c..190ea9212d2 100644
--- a/nixos/modules/services/misc/gitolite.nix
+++ b/nixos/modules/services/misc/gitolite.nix
@@ -227,6 +227,6 @@ in
     };
 
     environment.systemPackages = [ pkgs.gitolite pkgs.git ]
-        ++ optional cfg.enableGitAnnex pkgs.gitAndTools.git-annex;
+        ++ optional cfg.enableGitAnnex pkgs.git-annex;
   });
 }
diff --git a/nixos/modules/services/misc/gitweb.nix b/nixos/modules/services/misc/gitweb.nix
index ca21366b779..13396bf2eb0 100644
--- a/nixos/modules/services/misc/gitweb.nix
+++ b/nixos/modules/services/misc/gitweb.nix
@@ -54,6 +54,6 @@ in
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 
 }
diff --git a/nixos/modules/services/misc/gogs.nix b/nixos/modules/services/misc/gogs.nix
index c5070aaa356..d7233f10c7c 100644
--- a/nixos/modules/services/misc/gogs.nix
+++ b/nixos/modules/services/misc/gogs.nix
@@ -25,7 +25,6 @@ let
     HTTP_ADDR = ${cfg.httpAddress}
     HTTP_PORT = ${toString cfg.httpPort}
     ROOT_URL = ${cfg.rootUrl}
-    STATIC_ROOT_PATH = ${cfg.staticRootPath}
 
     [session]
     COOKIE_NAME = session
@@ -179,13 +178,6 @@ in
         '';
       };
 
-      staticRootPath = mkOption {
-        type = types.str;
-        default = "${pkgs.gogs.data}";
-        example = "/var/lib/gogs/data";
-        description = "Upper level of template and static files path.";
-      };
-
       extraConfig = mkOption {
         type = types.str;
         default = "";
diff --git a/nixos/modules/services/misc/gollum.nix b/nixos/modules/services/misc/gollum.nix
index 0c9c7548305..4053afa69be 100644
--- a/nixos/modules/services/misc/gollum.nix
+++ b/nixos/modules/services/misc/gollum.nix
@@ -115,4 +115,6 @@ in
       };
     };
   };
+
+  meta.maintainers = with lib.maintainers; [ erictapen ];
 }
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index f954249942a..fafea10daba 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -62,7 +62,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 2947;
         description = ''
           The port where to listen for TCP connections.
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 0477254e7c1..dcd825bba43 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -63,10 +63,12 @@ let
   };
 
 in {
-  meta.maintainers = with maintainers; [ dotlambda ];
+  meta.maintainers = teams.home-assistant.members;
 
   options.services.home-assistant = {
-    enable = mkEnableOption "Home Assistant";
+    # Running home-assistant on NixOS is considered an installation method that is unsupported by the upstream project.
+    # https://github.com/home-assistant/architecture/blob/master/adr/0012-define-supported-installation-method.md#decision
+    enable = mkEnableOption "Home Assistant. Please note that this installation method is unsupported upstream";
 
     configDir = mkOption {
       default = "/var/lib/hass";
@@ -183,8 +185,14 @@ in {
     };
 
     package = mkOption {
-      default = pkgs.home-assistant;
-      defaultText = "pkgs.home-assistant";
+      default = pkgs.home-assistant.overrideAttrs (oldAttrs: {
+        doInstallCheck = false;
+      });
+      defaultText = literalExample ''
+        pkgs.home-assistant.overrideAttrs (oldAttrs: {
+          doInstallCheck = false;
+        })
+      '';
       type = types.package;
       example = literalExample ''
         pkgs.home-assistant.override {
@@ -192,10 +200,11 @@ in {
         }
       '';
       description = ''
-        Home Assistant package to use.
+        Home Assistant package to use. By default the tests are disabled, as they take a considerable amout of time to complete.
         Override <literal>extraPackages</literal> or <literal>extraComponents</literal> in order to add additional dependencies.
         If you specify <option>config</option> and do not set <option>autoExtraComponents</option>
         to <literal>false</literal>, overriding <literal>extraComponents</literal> will have no effect.
+        Avoid <literal>home-assistant.overridePythonAttrs</literal> if you use <literal>autoExtraComponents</literal>.
       '';
     };
 
@@ -238,18 +247,135 @@ in {
         rm -f "${cfg.configDir}/ui-lovelace.yaml"
         ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
       '');
-      serviceConfig = {
-        ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
+      serviceConfig = let
+        # List of capabilities to equip home-assistant with, depending on configured components
+        capabilities = [
+          # Empty string first, so we will never accidentally have an empty capability bounding set
+          # https://github.com/NixOS/nixpkgs/issues/120617#issuecomment-830685115
+          ""
+        ] ++ (unique (optionals (useComponent "bluetooth_tracker" || useComponent "bluetooth_le_tracker") [
+          # Required for interaction with hci devices and bluetooth sockets
+          # https://www.home-assistant.io/integrations/bluetooth_le_tracker/#rootless-setup-on-core-installs
+          "CAP_NET_ADMIN"
+          "CAP_NET_RAW"
+        ] ++ lib.optionals (useComponent "emulated_hue") [
+          # Alexa looks for the service on port 80
+          # https://www.home-assistant.io/integrations/emulated_hue
+          "CAP_NET_BIND_SERVICE"
+        ] ++ lib.optionals (useComponent "nmap_tracker") [
+          # https://www.home-assistant.io/integrations/nmap_tracker#linux-capabilities
+          "CAP_NET_ADMIN"
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ]));
+        componentsUsingBluetooth = [
+          # Components that require the AF_BLUETOOTH address family
+          "bluetooth_tracker"
+          "bluetooth_le_tracker"
+        ];
+        componentsUsingSerialDevices = [
+          # Components that require access to serial devices (/dev/tty*)
+          # List generated from home-assistant documentation:
+          #   git clone https://github.com/home-assistant/home-assistant.io/
+          #   cd source/_integrations
+          #   rg "/dev/tty" -l | cut -d'/' -f3 | cut -d'.' -f1 | sort
+          # And then extended by references found in the source code, these
+          # mostly the ones using config flows already.
+          "acer_projector"
+          "alarmdecoder"
+          "arduino"
+          "blackbird"
+          "dsmr"
+          "edl21"
+          "elkm1"
+          "elv"
+          "enocean"
+          "firmata"
+          "flexit"
+          "gpsd"
+          "insteon"
+          "kwb"
+          "lacrosse"
+          "mhz19"
+          "modbus"
+          "modem_callerid"
+          "mysensors"
+          "nad"
+          "numato"
+          "rflink"
+          "rfxtrx"
+          "scsgate"
+          "serial"
+          "serial_pm"
+          "sms"
+          "upb"
+          "velbus"
+          "w800rf32"
+          "xbee"
+          "zha"
+          "zwave"
+        ];
+      in {
+        ExecStart = "${package}/bin/hass --runner --config '${cfg.configDir}'";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = "hass";
         Group = "hass";
         Restart = "on-failure";
-        ProtectSystem = "strict";
-        ReadWritePaths = "${cfg.configDir}";
+        RestartForceExitStatus = "100";
+        SuccessExitStatus = "100";
         KillSignal = "SIGINT";
+
+        # Hardening
+        AmbientCapabilities = capabilities;
+        CapabilityBoundingSet = capabilities;
+        DeviceAllow = (optionals (any useComponent componentsUsingSerialDevices) [
+          "char-ttyACM rw"
+          "char-ttyAMA rw"
+          "char-ttyUSB rw"
+        ]);
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
         PrivateTmp = true;
+        PrivateUsers = false; # prevents gaining capabilities in the host namespace
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "all";
+        ProtectSystem = "strict";
         RemoveIPC = true;
-        AmbientCapabilities = "cap_net_raw,cap_net_admin+eip";
+        ReadWritePaths = let
+          # Allow rw access to explicitly configured paths
+          cfgPath = [ "config" "homeassistant" "allowlist_external_dirs" ];
+          value = attrByPath cfgPath [] cfg;
+          allowPaths = if isList value then value else singleton value;
+        in [ "${cfg.configDir}" ] ++ allowPaths;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ] ++ optionals (any useComponent componentsUsingBluetooth) [
+          "AF_BLUETOOTH"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = optionals (any useComponent componentsUsingSerialDevices) [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
+        UMask = "0077";
       };
       path = [
         "/run/wrappers" # needed for ping
@@ -267,7 +393,6 @@ 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 684a242d738..c7332b87803 100644
--- a/nixos/modules/services/misc/ihaskell.nix
+++ b/nixos/modules/services/misc/ihaskell.nix
@@ -21,6 +21,7 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           haskellPackages: [
diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix
index 0493dadea94..6d64acc0291 100644
--- a/nixos/modules/services/misc/jellyfin.nix
+++ b/nixos/modules/services/misc/jellyfin.nix
@@ -18,6 +18,7 @@ in
 
       package = mkOption {
         type = types.package;
+        default = pkgs.jellyfin;
         example = literalExample "pkgs.jellyfin";
         description = ''
           Jellyfin package to use.
@@ -29,6 +30,16 @@ in
         default = "jellyfin";
         description = "Group under which jellyfin runs.";
       };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open the default ports in the firewall for the media server. The
+          HTTP/HTTPS ports can be changed in the Web UI, so this option should
+          only be used if they are unchanged.
+        '';
+      };
     };
   };
 
@@ -45,14 +56,47 @@ in
         CacheDirectory = "jellyfin";
         ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'";
         Restart = "on-failure";
+
+        # Security options:
+
+        NoNewPrivileges = true;
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RemoveIPC = true;
+
+        RestrictNamespaces = true;
+        # AF_NETLINK needed because Jellyfin monitors the network connection
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"
+        ];
       };
     };
 
-    services.jellyfin.package = mkDefault (
-      if versionAtLeast config.system.stateVersion "20.09" then pkgs.jellyfin
-        else pkgs.jellyfin_10_5
-    );
-
     users.users = mkIf (cfg.user == "jellyfin") {
       jellyfin = {
         group = cfg.group;
@@ -64,6 +108,12 @@ in
       jellyfin = {};
     };
 
+    networking.firewall = mkIf cfg.openFirewall {
+      # from https://jellyfin.org/docs/general/networking/index.html
+      allowedTCPPorts = [ 8096 8920 ];
+      allowedUDPPorts = [ 1900 7359 ];
+    };
+
   };
 
   meta.maintainers = with lib.maintainers; [ minijackson ];
diff --git a/nixos/modules/services/misc/klipper.nix b/nixos/modules/services/misc/klipper.nix
new file mode 100644
index 00000000000..909408225e0
--- /dev/null
+++ b/nixos/modules/services/misc/klipper.nix
@@ -0,0 +1,117 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.klipper;
+  format = pkgs.formats.ini {
+    # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
+    listToValue = l:
+      if builtins.length l == 1 then generators.mkValueStringDefault {} (head l)
+      else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
+    mkKeyValue = generators.mkKeyValueDefault {} ":";
+  };
+in
+{
+  ##### interface
+  options = {
+    services.klipper = {
+      enable = mkEnableOption "Klipper, the 3D printer firmware";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.klipper;
+        description = "The Klipper package.";
+      };
+
+      inputTTY = mkOption {
+        type = types.path;
+        default = "/run/klipper/tty";
+        description = "Path of the virtual printer symlink to create.";
+      };
+
+      apiSocket = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/klipper/api";
+        description = "Path of the API socket to create.";
+      };
+
+      octoprintIntegration = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allows Octoprint to control Klipper.";
+      };
+
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          User account under which Klipper runs.
+
+          If null is specified (default), a temporary user will be created by systemd.
+        '';
+      };
+
+      group = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Group account under which Klipper runs.
+
+          If null is specified (default), a temporary user will be created by systemd.
+        '';
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = { };
+        description = ''
+          Configuration for Klipper. See the <link xlink:href="https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides">documentation</link>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
+        message = "Option klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
+      }
+      {
+        assertion = cfg.user != null -> cfg.group != null;
+        message = "Option klipper.group is not set when a user is specified.";
+      }
+    ];
+
+    environment.etc."klipper.cfg".source = format.generate "klipper.cfg" cfg.settings;
+
+    services.klipper = mkIf cfg.octoprintIntegration {
+      user = config.services.octoprint.user;
+      group = config.services.octoprint.group;
+    };
+
+    systemd.services.klipper = let
+      klippyArgs = "--input-tty=${cfg.inputTTY}"
+        + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}";
+    in {
+      description = "Klipper 3D Printer Firmware";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/lib/klipper/klippy.py ${klippyArgs} /etc/klipper.cfg";
+        RuntimeDirectory = "klipper";
+        SupplementaryGroups = [ "dialout" ];
+        WorkingDirectory = "${cfg.package}/lib";
+      } // (if cfg.user != null then {
+        Group = cfg.group;
+        User = cfg.user;
+      } else {
+        DynamicUser = true;
+        User = "klipper";
+      });
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/leaps.nix b/nixos/modules/services/misc/leaps.nix
index ef89d3e64d0..f797218522c 100644
--- a/nixos/modules/services/misc/leaps.nix
+++ b/nixos/modules/services/misc/leaps.nix
@@ -11,7 +11,7 @@ in
     services.leaps = {
       enable = mkEnableOption "leaps";
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8080;
         description = "A port where leaps listens for incoming http requests";
       };
diff --git a/nixos/modules/services/misc/lifecycled.nix b/nixos/modules/services/misc/lifecycled.nix
new file mode 100644
index 00000000000..1c8942998d6
--- /dev/null
+++ b/nixos/modules/services/misc/lifecycled.nix
@@ -0,0 +1,164 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.lifecycled;
+
+  # TODO: Add the ability to extend this with an rfc 42-like interface.
+  # In the meantime, one can modify the environment (as
+  # long as it's not overriding anything from here) with
+  # systemd.services.lifecycled.serviceConfig.Environment
+  configFile = pkgs.writeText "lifecycled" ''
+    LIFECYCLED_HANDLER=${cfg.handler}
+    ${lib.optionalString (cfg.cloudwatchGroup != null) "LIFECYCLED_CLOUDWATCH_GROUP=${cfg.cloudwatchGroup}"}
+    ${lib.optionalString (cfg.cloudwatchStream != null) "LIFECYCLED_CLOUDWATCH_STREAM=${cfg.cloudwatchStream}"}
+    ${lib.optionalString cfg.debug "LIFECYCLED_DEBUG=${lib.boolToString cfg.debug}"}
+    ${lib.optionalString (cfg.instanceId != null) "LIFECYCLED_INSTANCE_ID=${cfg.instanceId}"}
+    ${lib.optionalString cfg.json "LIFECYCLED_JSON=${lib.boolToString cfg.json}"}
+    ${lib.optionalString cfg.noSpot "LIFECYCLED_NO_SPOT=${lib.boolToString cfg.noSpot}"}
+    ${lib.optionalString (cfg.snsTopic != null) "LIFECYCLED_SNS_TOPIC=${cfg.snsTopic}"}
+    ${lib.optionalString (cfg.awsRegion != null) "AWS_REGION=${cfg.awsRegion}"}
+  '';
+in
+{
+  meta.maintainers = with maintainers; [ cole-h grahamc ];
+
+  options = {
+    services.lifecycled = {
+      enable = mkEnableOption "lifecycled";
+
+      queueCleaner = {
+        enable = mkEnableOption "lifecycled-queue-cleaner";
+
+        frequency = mkOption {
+          type = types.str;
+          default = "hourly";
+          description = ''
+            How often to trigger the queue cleaner.
+
+            NOTE: This string should be a valid value for a systemd
+            timer's <literal>OnCalendar</literal> configuration. See
+            <citerefentry><refentrytitle>systemd.timer</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+            for more information.
+          '';
+        };
+
+        parallel = mkOption {
+          type = types.ints.unsigned;
+          default = 20;
+          description = ''
+            The number of parallel deletes to run.
+          '';
+        };
+      };
+
+      instanceId = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The instance ID to listen for events for.
+        '';
+      };
+
+      snsTopic = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The SNS topic that receives events.
+        '';
+      };
+
+      noSpot = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Disable the spot termination listener.
+        '';
+      };
+
+      handler = mkOption {
+        type = types.path;
+        description = ''
+          The script to invoke to handle events.
+        '';
+      };
+
+      json = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable JSON logging.
+        '';
+      };
+
+      cloudwatchGroup = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Write logs to a specific Cloudwatch Logs group.
+        '';
+      };
+
+      cloudwatchStream = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Write logs to a specific Cloudwatch Logs stream. Defaults to the instance ID.
+        '';
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable debugging information.
+        '';
+      };
+
+      # XXX: Can be removed if / when
+      # https://github.com/buildkite/lifecycled/pull/91 is merged.
+      awsRegion = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          The region used for accessing AWS services.
+        '';
+      };
+    };
+  };
+
+  ### Implementation ###
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      environment.etc."lifecycled".source = configFile;
+
+      systemd.packages = [ pkgs.lifecycled ];
+      systemd.services.lifecycled = {
+        wantedBy = [ "network-online.target" ];
+        restartTriggers = [ configFile ];
+      };
+    })
+
+    (mkIf cfg.queueCleaner.enable {
+      systemd.services.lifecycled-queue-cleaner = {
+        description = "Lifecycle Daemon Queue Cleaner";
+        environment = optionalAttrs (cfg.awsRegion != null) { AWS_REGION = cfg.awsRegion; };
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${pkgs.lifecycled}/bin/lifecycled-queue-cleaner -parallel ${toString cfg.queueCleaner.parallel}";
+        };
+      };
+
+      systemd.timers.lifecycled-queue-cleaner = {
+        description = "Lifecycle Daemon Queue Cleaner Timer";
+        wantedBy = [ "timers.target" ];
+        after = [ "network-online.target" ];
+        timerConfig = {
+          Unit = "lifecycled-queue-cleaner.service";
+          OnCalendar = "${cfg.queueCleaner.frequency}";
+        };
+      };
+    })
+  ];
+}
diff --git a/nixos/modules/services/misc/mame.nix b/nixos/modules/services/misc/mame.nix
index c5d5e9e4837..4b9a04be7c2 100644
--- a/nixos/modules/services/misc/mame.nix
+++ b/nixos/modules/services/misc/mame.nix
@@ -53,7 +53,7 @@ in
       description = "MAME TUN/TAP Ethernet interface";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.iproute ];
+      path = [ pkgs.iproute2 ];
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
@@ -63,5 +63,5 @@ in
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ gnidorah ];
+  meta.maintainers = with lib.maintainers; [ ];
 }
diff --git a/nixos/modules/services/misc/matrix-appservice-discord.nix b/nixos/modules/services/misc/matrix-appservice-discord.nix
index 49c41ff637a..71d1227f4ff 100644
--- a/nixos/modules/services/misc/matrix-appservice-discord.nix
+++ b/nixos/modules/services/misc/matrix-appservice-discord.nix
@@ -5,7 +5,7 @@ with lib;
 let
   dataDir = "/var/lib/matrix-appservice-discord";
   registrationFile = "${dataDir}/discord-registration.yaml";
-  appDir = "${pkgs.matrix-appservice-discord}/lib/node_modules/matrix-appservice-discord";
+  appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
   cfg = config.services.matrix-appservice-discord;
   # TODO: switch to configGen.json once RFC42 is implemented
   settingsFile = pkgs.writeText "matrix-appservice-discord-settings.json" (builtins.toJSON cfg.settings);
@@ -22,12 +22,6 @@ in {
         default = {
           database = {
             filename = "${dataDir}/discord.db";
-
-            # TODO: remove those old config keys once the following issues are solved:
-            # * https://github.com/Half-Shot/matrix-appservice-discord/issues/490
-            # * https://github.com/Half-Shot/matrix-appservice-discord/issues/498
-            userStorePath = "${dataDir}/user-store.db";
-            roomStorePath = "${dataDir}/room-store.db";
           };
 
           # empty values necessary for registration file generation
diff --git a/nixos/modules/services/misc/matrix-appservice-irc.nix b/nixos/modules/services/misc/matrix-appservice-irc.nix
new file mode 100644
index 00000000000..a0a5973d30f
--- /dev/null
+++ b/nixos/modules/services/misc/matrix-appservice-irc.nix
@@ -0,0 +1,229 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.matrix-appservice-irc;
+
+  pkg = pkgs.matrix-appservice-irc;
+  bin = "${pkg}/bin/matrix-appservice-irc";
+
+  jsonType = (pkgs.formats.json {}).type;
+
+  configFile = pkgs.runCommandNoCC "matrix-appservice-irc.yml" {
+    # Because this program will be run at build time, we need `nativeBuildInputs`
+    nativeBuildInputs = [ (pkgs.python3.withPackages (ps: [ ps.pyyaml ps.jsonschema ])) ];
+    preferLocalBuild = true;
+
+    config = builtins.toJSON cfg.settings;
+    passAsFile = [ "config" ];
+  } ''
+    # The schema is given as yaml, we need to convert it to json
+    python -c 'import json; import yaml; import sys; json.dump(yaml.safe_load(sys.stdin), sys.stdout)' \
+      < ${pkg}/lib/node_modules/matrix-appservice-irc/config.schema.yml \
+      > config.schema.json
+    python -m jsonschema config.schema.json -i $configPath
+    cp "$configPath" "$out"
+  '';
+  registrationFile = "/var/lib/matrix-appservice-irc/registration.yml";
+in {
+  options.services.matrix-appservice-irc = with types; {
+    enable = mkEnableOption "the Matrix/IRC bridge";
+
+    port = mkOption {
+      type = port;
+      description = "The port to listen on";
+      default = 8009;
+    };
+
+    needBindingCap = mkOption {
+      type = bool;
+      description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)";
+      default = false;
+    };
+
+    passwordEncryptionKeyLength = mkOption {
+      type = ints.unsigned;
+      description = "Length of the key to encrypt IRC passwords with";
+      default = 4096;
+      example = 8192;
+    };
+
+    registrationUrl = mkOption {
+      type = str;
+      description = ''
+        The URL where the application service is listening for homeserver requests,
+        from the Matrix homeserver perspective.
+      '';
+      example = "http://localhost:8009";
+    };
+
+    localpart = mkOption {
+      type = str;
+      description = "The user_id localpart to assign to the appservice";
+      default = "appservice-irc";
+    };
+
+    settings = mkOption {
+      description = ''
+        Configuration for the appservice, see
+        <link xlink:href="https://github.com/matrix-org/matrix-appservice-irc/blob/${pkgs.matrix-appservice-irc.version}/config.sample.yaml"/>
+        for supported values
+      '';
+      default = {};
+      type = submodule {
+        freeformType = jsonType;
+
+        options = {
+          homeserver = mkOption {
+            description = "Homeserver configuration";
+            default = {};
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                url = mkOption {
+                  type = str;
+                  description = "The URL to the home server for client-server API calls";
+                };
+
+                domain = mkOption {
+                  type = str;
+                  description = ''
+                    The 'domain' part for user IDs on this home server. Usually
+                    (but not always) is the "domain name" part of the homeserver URL.
+                  '';
+                };
+              };
+            };
+          };
+
+          database = mkOption {
+            default = {};
+            description = "Configuration for the database";
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                engine = mkOption {
+                  type = str;
+                  description = "Which database engine to use";
+                  default = "nedb";
+                  example = "postgres";
+                };
+
+                connectionString = mkOption {
+                  type = str;
+                  description = "The database connection string";
+                  default = "nedb://var/lib/matrix-appservice-irc/data";
+                  example = "postgres://username:password@host:port/databasename";
+                };
+              };
+            };
+          };
+
+          ircService = mkOption {
+            default = {};
+            description = "IRC bridge configuration";
+            type = submodule {
+              freeformType = jsonType;
+
+              options = {
+                passwordEncryptionKeyPath = mkOption {
+                  type = str;
+                  description = ''
+                    Location of the key with which IRC passwords are encrypted
+                    for storage. Will be generated on first run if not present.
+                  '';
+                  default = "/var/lib/matrix-appservice-irc/passkey.pem";
+                };
+
+                servers = mkOption {
+                  type = submodule { freeformType = jsonType; };
+                  description = "IRC servers to connect to";
+                };
+              };
+            };
+          };
+        };
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+    systemd.services.matrix-appservice-irc = {
+      description = "Matrix-IRC bridge";
+      before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        umask 077
+        # Generate key for crypting passwords
+        if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then
+          ${pkgs.openssl}/bin/openssl genpkey \
+              -out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \
+              -outform PEM \
+              -algorithm RSA \
+              -pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}"
+        fi
+        # Generate registration file
+        if ! [ -f "${registrationFile}" ]; then
+          # The easy case: the file has not been generated yet
+          ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
+        else
+          # The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to
+          # regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them.
+          # 1. Backup
+          id=$(grep "^id:.*$" ${registrationFile})
+          hs_token=$(grep "^hs_token:.*$" ${registrationFile})
+          as_token=$(grep "^as_token:.*$" ${registrationFile})
+          # 2. Regenerate
+          ${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
+          # 3. Restore
+          sed -i "s/^id:.*$/$id/g" ${registrationFile}
+          sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile}
+          sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
+        fi
+        # Allow synapse access to the registration
+        if ${getBin pkgs.glibc}/bin/getent group matrix-synapse > /dev/null; then
+          chgrp matrix-synapse ${registrationFile}
+          chmod g+r ${registrationFile}
+        fi
+      '';
+
+      serviceConfig = rec {
+        Type = "simple";
+        ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}";
+
+        ProtectHome = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        StateDirectory = "matrix-appservice-irc";
+        StateDirectoryMode = "755";
+
+        User = "matrix-appservice-irc";
+        Group = "matrix-appservice-irc";
+
+        CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE";
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+
+        LockPersonality = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        SystemCallFilter = "~@aio @clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @setuid @swap";
+        SystemCallArchitectures = "native";
+        # AF_UNIX is required to connect to a postgres socket.
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
+      };
+    };
+
+    users.groups.matrix-appservice-irc = {};
+    users.users.matrix-appservice-irc = {
+      description = "Service user for the Matrix-IRC bridge";
+      group = "matrix-appservice-irc";
+      isSystemUser = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 3eb1073387f..3c734a94819 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -86,7 +86,9 @@ account_threepid_delegates:
   ${optionalString (cfg.account_threepid_delegates.email != null) "email: ${cfg.account_threepid_delegates.email}"}
   ${optionalString (cfg.account_threepid_delegates.msisdn != null) "msisdn: ${cfg.account_threepid_delegates.msisdn}"}
 
-room_invite_state_types: ${builtins.toJSON cfg.room_invite_state_types}
+room_prejoin_state:
+  disable_default_event_types: ${boolToString cfg.room_prejoin_state.disable_default_event_types}
+  additional_event_types: ${builtins.toJSON cfg.room_prejoin_state.additional_event_types}
 ${optionalString (cfg.macaroon_secret_key != null) ''
   macaroon_secret_key: "${cfg.macaroon_secret_key}"
 ''}
@@ -131,11 +133,23 @@ in {
       plugins = mkOption {
         type = types.listOf types.package;
         default = [ ];
-        defaultText = "with config.services.matrix-synapse.package.plugins [ matrix-synapse-ldap3 matrix-synapse-pam ]";
+        example = literalExample ''
+          with config.services.matrix-synapse.package.plugins; [
+            matrix-synapse-ldap3
+            matrix-synapse-pam
+          ];
+        '';
         description = ''
           List of additional Matrix plugins to make available.
         '';
       };
+      withJemalloc = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to preload jemalloc to reduce memory fragmentation and overall usage.
+        '';
+      };
       no_tls = mkOption {
         type = types.bool;
         default = false;
@@ -224,7 +238,7 @@ in {
         type = types.listOf (types.submodule {
           options = {
             port = mkOption {
-              type = types.int;
+              type = types.port;
               example = 8448;
               description = ''
                 The port to listen for HTTP(S) requests on.
@@ -499,8 +513,7 @@ in {
       report_stats = mkOption {
         type = types.bool;
         default = false;
-        description = ''
-        '';
+        description = "";
       };
       servers = mkOption {
         type = types.attrsOf (types.attrsOf types.str);
@@ -573,11 +586,28 @@ in {
           Delegate SMS sending to this local process (https://localhost:8090)
         '';
       };
-      room_invite_state_types = mkOption {
+      room_prejoin_state.additional_event_types = mkOption {
+        default = [];
         type = types.listOf types.str;
-        default = ["m.room.join_rules" "m.room.canonical_alias" "m.room.avatar" "m.room.name"];
         description = ''
-          A list of event types that will be included in the room_invite_state
+          Additional events to share with users who received an invite.
+        '';
+      };
+      room_prejoin_state.disable_default_event_types = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Whether to disable the default state-event types for users invited to a room.
+          These are:
+
+          <itemizedlist>
+          <listitem><para>m.room.join_rules</para></listitem>
+          <listitem><para>m.room.canonical_alias</para></listitem>
+          <listitem><para>m.room.avatar</para></listitem>
+          <listitem><para>m.room.encryption</para></listitem>
+          <listitem><para>m.room.name</para></listitem>
+          <listitem><para>m.room.create</para></listitem>
+          </itemizedlist>
         '';
       };
       macaroon_secret_key = mkOption {
@@ -676,12 +706,12 @@ in {
     ];
 
     users.users.matrix-synapse = {
-        group = "matrix-synapse";
-        home = cfg.dataDir;
-        createHome = true;
-        shell = "${pkgs.bash}/bin/bash";
-        uid = config.ids.uids.matrix-synapse;
-      };
+      group = "matrix-synapse";
+      home = cfg.dataDir;
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+      uid = config.ids.uids.matrix-synapse;
+    };
 
     users.groups.matrix-synapse = {
       gid = config.ids.gids.matrix-synapse;
@@ -697,19 +727,28 @@ in {
           --keys-directory ${cfg.dataDir} \
           --generate-keys
       '';
-      environment.PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
+      environment = {
+        PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ];
+      } // optionalAttrs (cfg.withJemalloc) {
+        LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
+      };
       serviceConfig = {
         Type = "notify";
         User = "matrix-synapse";
         Group = "matrix-synapse";
         WorkingDirectory = cfg.dataDir;
+        ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" ''
+          chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key
+          chmod 0600 ${cfg.dataDir}/homeserver.signing.key
+        '')) ];
         ExecStart = ''
           ${cfg.package}/bin/homeserver \
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
             --keys-directory ${cfg.dataDir}
         '';
-        ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID";
+        ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID";
         Restart = "on-failure";
+        UMask = "0077";
       };
     };
   };
@@ -724,6 +763,12 @@ in {
       <nixpkgs/nixos/tests/matrix-synapse.nix>
     '')
     (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
+    (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
+      You may add additional event types via
+      `services.matrix-synapse.room_prejoin_state.additional_event_types` and
+      disable the default events via
+      `services.matrix-synapse.room_prejoin_state.disable_default_event_types`.
+    '')
   ];
 
   meta.doc = ./matrix-synapse.xml;
diff --git a/nixos/modules/services/misc/matrix-synapse.xml b/nixos/modules/services/misc/matrix-synapse.xml
index fbfa838b168..41a56df0f2b 100644
--- a/nixos/modules/services/misc/matrix-synapse.xml
+++ b/nixos/modules/services/misc/matrix-synapse.xml
@@ -33,11 +33,11 @@
    <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation">
    installation instructions of Synapse </link>.
 <programlisting>
-{ pkgs, ... }:
+{ pkgs, lib, ... }:
 let
   fqdn =
     let
-      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+      join = hostName: domain: hostName + lib.optionalString (domain != null) ".${domain}";
     in join config.networking.hostName config.networking.domain;
 in {
   networking = {
@@ -69,6 +69,9 @@ in {
       # 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_.enableACME">enableACME</link> = true;
+        <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
+
         <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
@@ -129,7 +132,7 @@ in {
       }
     ];
   };
-};
+}
 </programlisting>
   </para>
 
@@ -203,7 +206,7 @@ Success!
     <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override {
       conf = {
         default_server_config."m.homeserver" = {
-          "base_url" = "${config.networking.domain}";
+          "base_url" = "https://${fqdn}";
           "server_name" = "${fqdn}";
         };
       };
diff --git a/nixos/modules/services/misc/mautrix-telegram.nix b/nixos/modules/services/misc/mautrix-telegram.nix
index c5e8a5b85ec..0ae5797fea0 100644
--- a/nixos/modules/services/misc/mautrix-telegram.nix
+++ b/nixos/modules/services/misc/mautrix-telegram.nix
@@ -6,8 +6,9 @@ let
   dataDir = "/var/lib/mautrix-telegram";
   registrationFile = "${dataDir}/telegram-registration.yaml";
   cfg = config.services.mautrix-telegram;
-  # TODO: switch to configGen.json once RFC42 is implemented
-  settingsFile = pkgs.writeText "mautrix-telegram-settings.json" (builtins.toJSON cfg.settings);
+  settingsFormat = pkgs.formats.json {};
+  settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config-unsubstituted.json" cfg.settings;
+  settingsFile = "${dataDir}/config.json";
 
 in {
   options = {
@@ -15,12 +16,12 @@ in {
       enable = mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
 
       settings = mkOption rec {
-        # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
-        type = types.attrs;
         apply = recursiveUpdate default;
+        inherit (settingsFormat) type;
         default = {
           appservice = rec {
             database = "sqlite:///${dataDir}/mautrix-telegram.db";
+            database_opts = {};
             hostname = "0.0.0.0";
             port = 8080;
             address = "http://localhost:${toString port}";
@@ -29,6 +30,8 @@ in {
           bridge = {
             permissions."*" = "relaybot";
             relaybot.whitelist = [ ];
+            double_puppet_server_map = {};
+            login_shared_secret_map = {};
           };
 
           logging = {
@@ -121,6 +124,16 @@ in {
       after = [ "network-online.target" ] ++ cfg.serviceDependencies;
 
       preStart = ''
+        # Not all secrets can be passed as environment variable (yet)
+        # https://github.com/tulir/mautrix-telegram/issues/584
+        [ -f ${settingsFile} ] && rm -f ${settingsFile}
+        old_umask=$(umask)
+        umask 0277
+        ${pkgs.envsubst}/bin/envsubst \
+          -o ${settingsFile} \
+          -i ${settingsFileUnsubstituted}
+        umask $old_umask
+
         # generate the appservice's registration file if absent
         if [ ! -f '${registrationFile}' ]; then
           ${pkgs.mautrix-telegram}/bin/mautrix-telegram \
@@ -156,6 +169,8 @@ in {
             --config='${settingsFile}'
         '';
       };
+
+      restartTriggers = [ settingsFileUnsubstituted ];
     };
   };
 
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index 529f584a201..a19b73889ce 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -6,37 +6,97 @@ let
 
   gid = config.ids.gids.mediatomb;
   cfg = config.services.mediatomb;
+  name = cfg.package.pname;
+  pkg = cfg.package;
+  optionYesNo = option: if option then "yes" else "no";
+  # configuration on media directory
+  mediaDirectory = {
+    options = {
+      path = mkOption {
+        type = types.str;
+        description = ''
+          Absolute directory path to the media directory to index.
+        '';
+      };
+      recursive = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether the indexation must take place recursively or not.";
+      };
+      hidden-files = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to index the hidden files or not.";
+      };
+    };
+  };
+  toMediaDirectory = d: "<directory location=\"${d.path}\" mode=\"inotify\" recursive=\"${optionYesNo d.recursive}\" hidden-files=\"${optionYesNo d.hidden-files}\" />\n";
 
-  mtConf = pkgs.writeText "config.xml" ''
-  <?xml version="1.0" encoding="UTF-8"?>
-  <config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
+  transcodingConfig = if cfg.transcoding then with pkgs; ''
+    <transcoding enabled="yes">
+      <mimetype-profile-mappings>
+        <transcode mimetype="video/x-flv" using="vlcmpeg" />
+        <transcode mimetype="application/ogg" using="vlcmpeg" />
+        <transcode mimetype="audio/ogg" using="ogg2mp3" />
+        <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
+      </mimetype-profile-mappings>
+      <profiles>
+        <profile name="ogg2mp3" enabled="no" type="external">
+          <mimetype>audio/mpeg</mimetype>
+          <accept-url>no</accept-url>
+          <first-resource>yes</first-resource>
+          <accept-ogg-theora>no</accept-ogg-theora>
+          <agent command="${ffmpeg}/bin/ffmpeg" arguments="-y -i %in -f mp3 %out" />
+          <buffer size="1048576" chunk-size="131072" fill-size="262144" />
+        </profile>
+        <profile name="vlcmpeg" enabled="no" type="external">
+          <mimetype>video/mpeg</mimetype>
+          <accept-url>yes</accept-url>
+          <first-resource>yes</first-resource>
+          <accept-ogg-theora>yes</accept-ogg-theora>
+          <agent command="${libsForQt5.vlc}/bin/vlc"
+            arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit" />
+          <buffer size="14400000" chunk-size="512000" fill-size="120000" />
+        </profile>
+      </profiles>
+    </transcoding>
+'' else ''
+    <transcoding enabled="no">
+    </transcoding>
+'';
+
+  configText = optionalString (! cfg.customCfg) ''
+<?xml version="1.0" encoding="UTF-8"?>
+<config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
     <server>
       <ui enabled="yes" show-tooltips="yes">
         <accounts enabled="no" session-timeout="30">
-          <account user="mediatomb" password="mediatomb"/>
+          <account user="${name}" password="${name}"/>
         </accounts>
       </ui>
       <name>${cfg.serverName}</name>
       <udn>uuid:${cfg.uuid}</udn>
       <home>${cfg.dataDir}</home>
-      <webroot>${pkgs.mediatomb}/share/mediatomb/web</webroot>
+      <interface>${cfg.interface}</interface>
+      <webroot>${pkg}/share/${name}/web</webroot>
+      <pc-directory upnp-hide="${optionYesNo cfg.pcDirectoryHide}"/>
       <storage>
         <sqlite3 enabled="yes">
-          <database-file>mediatomb.db</database-file>
+          <database-file>${name}.db</database-file>
         </sqlite3>
       </storage>
-      <protocolInfo extend="${if cfg.ps3Support then "yes" else "no"}"/>
-      ${if cfg.dsmSupport then ''
+      <protocolInfo extend="${optionYesNo cfg.ps3Support}"/>
+      ${optionalString cfg.dsmSupport ''
       <custom-http-headers>
         <add header="X-User-Agent: redsonic"/>
       </custom-http-headers>
 
       <manufacturerURL>redsonic.com</manufacturerURL>
       <modelNumber>105</modelNumber>
-      '' else ""}
-      ${if cfg.tg100Support then ''
+      ''}
+        ${optionalString cfg.tg100Support ''
       <upnp-string-limit>101</upnp-string-limit>
-      '' else ""}
+      ''}
       <extended-runtime-options>
         <mark-played-items enabled="yes" suppress-cds-updates="yes">
           <string mode="prepend">*</string>
@@ -47,11 +107,14 @@ let
       </extended-runtime-options>
     </server>
     <import hidden-files="no">
+      <autoscan use-inotify="auto">
+      ${concatMapStrings toMediaDirectory cfg.mediaDirectories}
+      </autoscan>
       <scripting script-charset="UTF-8">
-        <common-script>${pkgs.mediatomb}/share/mediatomb/js/common.js</common-script>
-        <playlist-script>${pkgs.mediatomb}/share/mediatomb/js/playlists.js</playlist-script>
+        <common-script>${pkg}/share/${name}/js/common.js</common-script>
+        <playlist-script>${pkg}/share/${name}/js/playlists.js</playlist-script>
         <virtual-layout type="builtin">
-          <import-script>${pkgs.mediatomb}/share/mediatomb/js/import.js</import-script>
+          <import-script>${pkg}/share/${name}/js/import.js</import-script>
         </virtual-layout>
       </scripting>
       <mappings>
@@ -75,12 +138,12 @@ let
           <map from="flv" to="video/x-flv"/>
           <map from="mkv" to="video/x-matroska"/>
           <map from="mka" to="audio/x-matroska"/>
-          ${if cfg.ps3Support then ''
+          ${optionalString cfg.ps3Support ''
           <map from="avi" to="video/divx"/>
-          '' else ""}
-          ${if cfg.dsmSupport then ''
+          ''}
+          ${optionalString cfg.dsmSupport ''
           <map from="avi" to="video/avi"/>
-          '' else ""}
+          ''}
         </extension-mimetype>
         <mimetype-upnpclass>
           <map from="audio/*" to="object.item.audioItem.musicTrack"/>
@@ -108,46 +171,27 @@ let
       </mappings>
       <online-content>
         <YouTube enabled="no" refresh="28800" update-at-start="no" purge-after="604800" racy-content="exclude" format="mp4" hd="no">
-          <favorites user="mediatomb"/>
+          <favorites user="${name}"/>
           <standardfeed feed="most_viewed" time-range="today"/>
-          <playlists user="mediatomb"/>
-          <uploads user="mediatomb"/>
+          <playlists user="${name}"/>
+          <uploads user="${name}"/>
           <standardfeed feed="recently_featured" time-range="today"/>
         </YouTube>
       </online-content>
     </import>
-    <transcoding enabled="${if cfg.transcoding then "yes" else "no"}">
-      <mimetype-profile-mappings>
-        <transcode mimetype="video/x-flv" using="vlcmpeg"/>
-        <transcode mimetype="application/ogg" using="vlcmpeg"/>
-        <transcode mimetype="application/ogg" using="oggflac2raw"/>
-        <transcode mimetype="audio/x-flac" using="oggflac2raw"/>
-      </mimetype-profile-mappings>
-      <profiles>
-        <profile name="oggflac2raw" enabled="no" type="external">
-          <mimetype>audio/L16</mimetype>
-          <accept-url>no</accept-url>
-          <first-resource>yes</first-resource>
-          <accept-ogg-theora>no</accept-ogg-theora>
-          <agent command="ogg123" arguments="-d raw -o byteorder:big -f %out %in"/>
-          <buffer size="1048576" chunk-size="131072" fill-size="262144"/>
-        </profile>
-        <profile name="vlcmpeg" enabled="no" type="external">
-          <mimetype>video/mpeg</mimetype>
-          <accept-url>yes</accept-url>
-          <first-resource>yes</first-resource>
-          <accept-ogg-theora>yes</accept-ogg-theora>
-          <agent command="vlc" arguments="-I dummy %in --sout #transcode{venc=ffmpeg,vcodec=mp2v,vb=4096,fps=25,aenc=ffmpeg,acodec=mpga,ab=192,samplerate=44100,channels=2}:standard{access=file,mux=ps,dst=%out} vlc:quit"/>
-          <buffer size="14400000" chunk-size="512000" fill-size="120000"/>
-        </profile>
-      </profiles>
-    </transcoding>
+    ${transcodingConfig}
   </config>
-  '';
+'';
+  defaultFirewallRules = {
+    # udp 1900 port needs to be opened for SSDP (not configurable within
+    # mediatomb/gerbera) cf.
+    # http://docs.gerbera.io/en/latest/run.html?highlight=udp%20port#network-setup
+    allowedUDPPorts = [ 1900 cfg.port ];
+    allowedTCPPorts = [ cfg.port ];
+  };
 
 in {
 
-
   ###### interface
 
   options = {
@@ -158,18 +202,27 @@ in {
         type = types.bool;
         default = false;
         description = ''
-          Whether to enable the mediatomb DLNA server.
+          Whether to enable the Gerbera/Mediatomb DLNA server.
         '';
       };
 
       serverName = mkOption {
         type = types.str;
-        default = "mediatomb";
+        default = "Gerbera (Mediatomb)";
         description = ''
           How to identify the server on the network.
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        example = literalExample "pkgs.mediatomb";
+        default = pkgs.gerbera;
+        description = ''
+          Underlying package to be used with the module (default: pkgs.gerbera).
+        '';
+      };
+
       ps3Support = mkOption {
         type = types.bool;
         default = false;
@@ -206,23 +259,34 @@ in {
 
       dataDir = mkOption {
         type = types.path;
-        default = "/var/lib/mediatomb";
+        default = "/var/lib/${name}";
         description = ''
-          The directory where mediatomb stores its state, data, etc.
+          The directory where Gerbera/Mediatomb stores its state, data, etc.
+        '';
+      };
+
+      pcDirectoryHide = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to list the top-level directory or not (from upnp client standpoint).
         '';
       };
 
       user = mkOption {
+        type = types.str;
         default = "mediatomb";
-        description = "User account under which mediatomb runs.";
+        description = "User account under which ${name} runs.";
       };
 
       group = mkOption {
+        type = types.str;
         default = "mediatomb";
-        description = "Group account under which mediatomb runs.";
+        description = "Group account under which ${name} runs.";
       };
 
       port = mkOption {
+        type = types.int;
         default = 49152;
         description = ''
           The network port to listen on.
@@ -230,40 +294,76 @@ in {
       };
 
       interface = mkOption {
+        type = types.str;
         default = "";
         description = ''
           A specific interface to bind to.
         '';
       };
 
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          If false (the default), this is up to the user to declare the firewall rules.
+          If true, this opens port 1900 (tcp and udp) and the port specified by
+          <option>sercvices.mediatomb.port</option>.
+
+          If the option <option>services.mediatomb.interface</option> is set,
+          the firewall rules opened are dedicated to that interface. Otherwise,
+          those rules are opened globally.
+        '';
+      };
+
       uuid = mkOption {
+        type = types.str;
         default = "fdfc8a4e-a3ad-4c1d-b43d-a2eedb03a687";
         description = ''
           A unique (on your network) to identify the server by.
         '';
       };
 
+      mediaDirectories = mkOption {
+        type = with types; listOf (submodule mediaDirectory);
+        default = {};
+        description = ''
+          Declare media directories to index.
+        '';
+        example = [
+          { path = "/data/pictures"; recursive = false; hidden-files = false; }
+          { path = "/data/audio"; recursive = true; hidden-files = false; }
+        ];
+      };
+
       customCfg = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Allow mediatomb to create and use its own config file inside ${cfg.dataDir}.
+          Allow ${name} to create and use its own config file inside the <literal>dataDir</literal> as
+          configured by <option>services.mediatomb.dataDir</option>.
+          Deactivated by default, the service then runs with the configuration generated from this module.
+          Otherwise, when enabled, no service configuration is generated. Gerbera/Mediatomb then starts using
+          config.xml within the configured <literal>dataDir</literal>. It's up to the user to make a correct
+          configuration file.
         '';
       };
+
     };
   };
 
 
   ###### implementation
 
-  config = mkIf cfg.enable {
+  config = let binaryCommand = "${pkg}/bin/${name}";
+               interfaceFlag = optionalString ( cfg.interface != "") "--interface ${cfg.interface}";
+               configFlag = optionalString (! cfg.customCfg) "--config ${pkgs.writeText "config.xml" configText}";
+    in mkIf cfg.enable {
     systemd.services.mediatomb = {
-      description = "MediaTomb media Server";
+      description = "${cfg.serverName} media Server";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.mediatomb ];
-      serviceConfig.ExecStart = "${pkgs.mediatomb}/bin/mediatomb -p ${toString cfg.port} ${if cfg.interface!="" then "-e ${cfg.interface}" else ""} ${if cfg.customCfg then "" else "-c ${mtConf}"} -m ${cfg.dataDir}";
-      serviceConfig.User = "${cfg.user}";
+      serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
+      serviceConfig.User = cfg.user;
     };
 
     users.groups = optionalAttrs (cfg.group == "mediatomb") {
@@ -274,15 +374,18 @@ in {
       mediatomb = {
         isSystemUser = true;
         group = cfg.group;
-        home = "${cfg.dataDir}";
+        home = cfg.dataDir;
         createHome = true;
-        description = "Mediatomb DLNA Server User";
+        description = "${name} DLNA Server User";
       };
     };
 
-    networking.firewall = {
-      allowedUDPPorts = [ 1900 cfg.port ];
-      allowedTCPPorts = [ cfg.port ];
-    };
+    # Open firewall only if users enable it
+    networking.firewall = mkMerge [
+      (mkIf (cfg.openFirewall && cfg.interface != "") {
+        interfaces."${cfg.interface}" = defaultFirewallRules;
+      })
+      (mkIf (cfg.openFirewall && cfg.interface == "") defaultFirewallRules)
+    ];
   };
 }
diff --git a/nixos/modules/services/misc/mwlib.nix b/nixos/modules/services/misc/mwlib.nix
index 6b41b552a86..8dd17c06c0b 100644
--- a/nixos/modules/services/misc/mwlib.nix
+++ b/nixos/modules/services/misc/mwlib.nix
@@ -34,7 +34,7 @@ in
 
       port = mkOption {
         default = 8899;
-        type = types.int;
+        type = types.port;
         description = "Specify port to listen on.";
       }; # nserve.port
 
@@ -68,7 +68,7 @@ in
 
       port = mkOption {
         default = 14311;
-        type = types.int;
+        type = types.port;
         description = "Specify port to listen on.";
       }; # qserve.port
 
@@ -137,7 +137,7 @@ in
 
             port = mkOption {
               default = 8898;
-              type = types.int;
+              type = types.port;
               description = "Port to listen to when serving files from cache.";
             }; # nslave.http.port
 
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
new file mode 100644
index 00000000000..516d0f70ef0
--- /dev/null
+++ b/nixos/modules/services/misc/n8n.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.n8n;
+  format = pkgs.formats.json {};
+  configFile = format.generate "n8n.json" cfg.settings;
+in
+{
+  options.services.n8n = {
+
+    enable = mkEnableOption "n8n server";
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Open ports in the firewall for the n8n web interface.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Configuration for n8n, see <link xlink:href="https://docs.n8n.io/reference/configuration.html"/>
+        for supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    services.n8n.settings = {
+      # We use this to open the firewall, so we need to know about the default at eval time
+      port = lib.mkDefault 5678;
+    };
+
+    systemd.services.n8n = {
+      description = "N8N service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        # This folder must be writeable as the application is storing
+        # its data in it, so the StateDirectory is a good choice
+        N8N_USER_FOLDER = "/var/lib/n8n";
+        N8N_CONFIG_FILES = "${configFile}";
+      };
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.n8n}/bin/n8n";
+        Restart = "on-failure";
+        StateDirectory = "n8n";
+
+        # Basic Hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        DynamicUser = "true";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "yes";
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index 924a007efc6..133e96da0ec 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -21,6 +21,7 @@ let
          calls in `libstore/build.cc', don't add any supplementary group
          here except "nixbld".  */
       uid = builtins.add config.ids.uids.nixbld nr;
+      isSystemUser = true;
       group = "nixbld";
       extraGroups = [ "nixbld" ];
     };
@@ -45,7 +46,7 @@ let
         trusted-substituters = ${toString cfg.trustedBinaryCaches}
         trusted-public-keys = ${toString cfg.binaryCachePublicKeys}
         auto-optimise-store = ${boolToString cfg.autoOptimiseStore}
-        require-sigs = ${if cfg.requireSignedBinaryCaches then "true" else "false"}
+        require-sigs = ${boolToString cfg.requireSignedBinaryCaches}
         trusted-users = ${toString cfg.trustedUsers}
         allowed-users = ${toString cfg.allowedUsers}
         ${optionalString (!cfg.distributedBuilds) ''
@@ -500,13 +501,6 @@ in
 
   config = {
 
-    assertions = [
-      {
-        assertion = config.nix.distributedBuilds || config.nix.buildMachines == [];
-        message = "You must set `nix.distributedBuilds = true` to use nix.buildMachines";
-      }
-    ];
-
     nix.binaryCachePublicKeys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ];
     nix.binaryCaches = [ "https://cache.nixos.org/" ];
 
@@ -546,7 +540,7 @@ in
     systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ];
 
     systemd.services.nix-daemon =
-      { path = [ nix pkgs.utillinux config.programs.ssh.package ]
+      { path = [ nix pkgs.util-linux config.programs.ssh.package ]
           ++ optionals cfg.distributedBuilds [ pkgs.gzip ];
 
         environment = cfg.envVars
@@ -594,16 +588,10 @@ in
 
     nix.systemFeatures = mkDefault (
       [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
-      optionals (pkgs.stdenv.isx86_64 && pkgs.hostPlatform.platform ? gcc.arch) (
-        # a x86_64 builder can run code for `platform.gcc.arch` and minor architectures:
-        [ "gccarch-${pkgs.hostPlatform.platform.gcc.arch}" ] ++ {
-          sandybridge    = [ "gccarch-westmere" ];
-          ivybridge      = [ "gccarch-westmere" "gccarch-sandybridge" ];
-          haswell        = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" ];
-          broadwell      = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" "gccarch-haswell" ];
-          skylake        = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" "gccarch-haswell" "gccarch-broadwell" ];
-          skylake-avx512 = [ "gccarch-westmere" "gccarch-sandybridge" "gccarch-ivybridge" "gccarch-haswell" "gccarch-broadwell" "gccarch-skylake" ];
-        }.${pkgs.hostPlatform.platform.gcc.arch} or []
+      optionals (pkgs.hostPlatform ? gcc.arch) (
+        # a builder can run code for `gcc.arch` and inferior architectures
+        [ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
+        map (x: "gccarch-${x}") lib.systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
       )
     );
 
diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix
index 12bed05757a..a7a6a3b5964 100644
--- a/nixos/modules/services/misc/nix-gc.nix
+++ b/nixos/modules/services/misc/nix-gc.nix
@@ -21,13 +21,45 @@ in
       };
 
       dates = mkOption {
+        type = types.str;
         default = "03:15";
+        example = "weekly";
+        description = ''
+          How often or when garbage collection is performed. For most desktop and server systems
+          a sufficient garbage collection is once a week.
+
+          The format is described in
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry>.
+        '';
+      };
+
+      randomizedDelaySec = mkOption {
+        default = "0";
         type = types.str;
+        example = "45min";
         description = ''
-          Specification (in the format described by
+          Add a randomized delay before each automatic upgrade.
+          The delay will be chosen between zero and this value.
+          This value must be a time span in the format specified by
           <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>) of the time at
-          which the garbage collector will run.
+          <manvolnum>7</manvolnum></citerefentry>
+        '';
+      };
+
+      persistent = mkOption {
+        default = true;
+        type = types.bool;
+        example = false;
+        description = ''
+          Takes a boolean argument. If true, the time when the service
+          unit was last triggered is stored on disk. When the timer is
+          activated, the service unit is triggered immediately if it
+          would have been triggered at least once during the time when
+          the timer was inactive. Such triggering is nonetheless
+          subject to the delay imposed by RandomizedDelaySec=. This is
+          useful to catch up on missed runs of the service when the
+          system was powered down.
         '';
       };
 
@@ -50,11 +82,18 @@ in
 
   config = {
 
-    systemd.services.nix-gc =
-      { description = "Nix Garbage Collector";
-        script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
-        startAt = optional cfg.automatic cfg.dates;
+    systemd.services.nix-gc = {
+      description = "Nix Garbage Collector";
+      script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}";
+      startAt = optional cfg.automatic cfg.dates;
+    };
+
+    systemd.timers.nix-gc = lib.mkIf cfg.automatic {
+      timerConfig = {
+        RandomizedDelaySec = cfg.randomizedDelaySec;
+        Persistent = cfg.persistent;
       };
+    };
 
   };
 
diff --git a/nixos/modules/services/misc/nzbhydra2.nix b/nixos/modules/services/misc/nzbhydra2.nix
new file mode 100644
index 00000000000..c396b4b8f6e
--- /dev/null
+++ b/nixos/modules/services/misc/nzbhydra2.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.nzbhydra2;
+
+in {
+  options = {
+    services.nzbhydra2 = {
+      enable = mkEnableOption "NZBHydra2";
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/nzbhydra2";
+        description = "The directory where NZBHydra2 stores its data files.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Open ports in the firewall for the NZBHydra2 web interface.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nzbhydra2;
+        defaultText = "pkgs.nzbhydra2";
+        description = "NZBHydra2 package to use.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.dataDir}' 0700 nzbhydra2 nzbhydra2 - -" ];
+
+    systemd.services.nzbhydra2 = {
+      description = "NZBHydra2";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "nzbhydra2";
+        Group = "nzbhydra2";
+        ExecStart =
+          "${cfg.package}/bin/nzbhydra2 --nobrowser --datafolder '${cfg.dataDir}'";
+        Restart = "on-failure";
+        # Hardening
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ReadWritePaths = cfg.dataDir;
+        ProtectHome = "read-only";
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies ="AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        LockPersonality = true;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 5076 ]; };
+
+    users.users.nzbhydra2 = {
+      group = "nzbhydra2";
+      isSystemUser = true;
+    };
+
+    users.groups.nzbhydra2 = {};
+  };
+}
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index e2fbd3b401c..c926d889b37 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -40,7 +40,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 5000;
         description = ''
           Port to bind OctoPrint to.
@@ -66,9 +66,10 @@ in
       };
 
       plugins = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = plugins: [];
         defaultText = "plugins: []";
-        example = literalExample "plugins: with plugins; [ m33-fio stlviewer ]";
+        example = literalExample "plugins: with plugins; [ themeify stlviewer ]";
         description = "Additional plugins to be used. Available plugins are passed through the plugins input.";
       };
 
diff --git a/nixos/modules/services/misc/ombi.nix b/nixos/modules/services/misc/ombi.nix
new file mode 100644
index 00000000000..b5882168e51
--- /dev/null
+++ b/nixos/modules/services/misc/ombi.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.ombi;
+
+in {
+  options = {
+    services.ombi = {
+      enable = mkEnableOption ''
+        Ombi.
+        Optionally see <link xlink:href="https://docs.ombi.app/info/reverse-proxy"/>
+        on how to set up a reverse proxy
+      '';
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/ombi";
+        description = "The directory where Ombi stores its data files.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5000;
+        description = "The port for the Ombi web interface.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the Ombi web interface.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "ombi";
+        description = "User account under which Ombi runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "ombi";
+        description = "Group under which Ombi runs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.ombi = {
+      description = "Ombi";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.ombi}/bin/Ombi --storage '${cfg.dataDir}' --host 'http://*:${toString cfg.port}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+
+    users.users = mkIf (cfg.user == "ombi") {
+      ombi = {
+        isSystemUser = true;
+        group = cfg.group;
+        home = cfg.dataDir;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "ombi") { ombi = { }; };
+  };
+}
diff --git a/nixos/modules/services/misc/packagekit.nix b/nixos/modules/services/misc/packagekit.nix
index 325c4e84e0d..93bd206bd98 100644
--- a/nixos/modules/services/misc/packagekit.nix
+++ b/nixos/modules/services/misc/packagekit.nix
@@ -1,55 +1,60 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
-
   cfg = config.services.packagekit;
 
-  packagekitConf = ''
-    [Daemon]
-    DefaultBackend=${cfg.backend}
-    KeepCache=false
-  '';
+  inherit (lib)
+    mkEnableOption mkOption mkIf mkRemovedOptionModule types
+    listToAttrs recursiveUpdate;
 
-  vendorConf = ''
-    [PackagesNotFound]
-    DefaultUrl=https://github.com/NixOS/nixpkgs
-    CodecUrl=https://github.com/NixOS/nixpkgs
-    HardwareUrl=https://github.com/NixOS/nixpkgs
-    FontUrl=https://github.com/NixOS/nixpkgs
-    MimeUrl=https://github.com/NixOS/nixpkgs
-  '';
+  iniFmt = pkgs.formats.ini { };
 
-in
+  confFiles = [
+    (iniFmt.generate "PackageKit.conf" (recursiveUpdate
+      {
+        Daemon = {
+          DefaultBackend = "test_nop";
+          KeepCache = false;
+        };
+      }
+      cfg.settings))
 
+    (iniFmt.generate "Vendor.conf" (recursiveUpdate
+      {
+        PackagesNotFound = rec {
+          DefaultUrl = "https://github.com/NixOS/nixpkgs";
+          CodecUrl = DefaultUrl;
+          HardwareUrl = DefaultUrl;
+          FontUrl = DefaultUrl;
+          MimeUrl = DefaultUrl;
+        };
+      }
+      cfg.vendorSettings))
+  ];
+
+in
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "packagekit" "backend" ] "The only backend that doesn't blow up is `test_nop`.")
+  ];
 
-  options = {
+  options.services.packagekit = {
+    enable = mkEnableOption ''
+      PackageKit provides a cross-platform D-Bus abstraction layer for
+      installing software. Software utilizing PackageKit can install
+      software regardless of the package manager.
+    '';
 
-    services.packagekit = {
-      enable = mkEnableOption
-        ''
-          PackageKit provides a cross-platform D-Bus abstraction layer for
-          installing software. Software utilizing PackageKit can install
-          software regardless of the package manager.
-        '';
+    settings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to PackageKit.conf";
+    };
 
-      # TODO: integrate with PolicyKit if the nix backend matures to the point
-      # where it will require elevated permissions
-      backend = mkOption {
-        type = types.enum [ "test_nop" ];
-        default = "test_nop";
-        description = ''
-          PackageKit supports multiple different backends and <literal>auto</literal> which
-          should do the right thing.
-          </para>
-          <para>
-          On NixOS however, we do not have a backend compatible with nix 2.0
-          (refer to <link xlink:href="https://github.com/NixOS/nix/issues/233">this issue</link> so we have to force
-          it to <literal>test_nop</literal> for now.
-        '';
-      };
+    vendorSettings = mkOption {
+      type = iniFmt.type;
+      default = { };
+      description = "Additional settings passed straight through to Vendor.conf";
     };
   };
 
@@ -59,7 +64,9 @@ in
 
     systemd.packages = with pkgs; [ packagekit ];
 
-    environment.etc."PackageKit/PackageKit.conf".text = packagekitConf;
-    environment.etc."PackageKit/Vendor.conf".text = vendorConf;
+    environment.etc = listToAttrs (map
+      (e:
+        lib.nameValuePair "PackageKit/${e.name}" { source = e; })
+      confFiles);
   };
 }
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index bfaf760fb83..43730b80eb2 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -67,7 +67,7 @@ in
     };
 
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = 28981;
       description = "Server port to listen on.";
     };
diff --git a/nixos/modules/services/misc/pinnwand.nix b/nixos/modules/services/misc/pinnwand.nix
index aa1ee5cfaa7..cbc796c9a7c 100644
--- a/nixos/modules/services/misc/pinnwand.nix
+++ b/nixos/modules/services/misc/pinnwand.nix
@@ -24,55 +24,80 @@ in
         Your <filename>pinnwand.toml</filename> as a Nix attribute set. Look up
         possible options in the <link xlink:href="https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example">pinnwand.toml-example</link>.
       '';
-      default = {
-        # https://github.com/supakeen/pinnwand/blob/master/pinnwand.toml-example
-        database_uri = "sqlite:///var/lib/pinnwand/pinnwand.db";
-        preferred_lexeres = [];
-        paste_size = 262144;
-        paste_help = ''
-          <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
-        '';
-        footer = ''
-          View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
-        '';
-      };
+      default = {};
     };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.pinnwand = {
-      description = "Pinnwannd HTTP Server";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+    services.pinnwand.settings = {
+      database_uri = mkDefault "sqlite:////var/lib/pinnwand/pinnwand.db";
+      paste_size = mkDefault 262144;
+      paste_help = mkDefault ''
+        <p>Welcome to pinnwand, this site is a pastebin. It allows you to share code with others. If you write code in the text area below and press the paste button you will be given a link you can share with others so they can view your code as well.</p><p>People with the link can view your pasted code, only you can remove your paste and it expires automatically. Note that anyone could guess the URI to your paste so don't rely on it being private.</p>
+      '';
+      footer = mkDefault ''
+        View <a href="//github.com/supakeen/pinnwand" target="_BLANK">source code</a>, the <a href="/removal">removal</a> or <a href="/expiry">expiry</a> stories, or read the <a href="/about">about</a> page.
+      '';
+    };
+
+    systemd.services = let
+      hardeningOptions = {
+        User = "pinnwand";
+        DynamicUser = true;
 
-      unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
-      serviceConfig = {
-        ExecStart = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile} http --port ${toString(cfg.port)}";
         StateDirectory = "pinnwand";
         StateDirectoryMode = "0700";
 
         AmbientCapabilities = [];
         CapabilityBoundingSet = "";
         DevicePolicy = "closed";
-        DynamicUser = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
         PrivateDevices = true;
         PrivateUsers = true;
+        ProcSubset = "pid";
         ProtectClock = true;
         ProtectControlGroups = true;
-        ProtectKernelLogs = true;
         ProtectHome = true;
         ProtectHostname = true;
+        ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
-        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        ProtectProc = "invisible";
+        RestrictAddressFamilies = [
+          "AF_UNIX"
+          "AF_INET"
+          "AF_INET6"
+        ];
         RestrictNamespaces = true;
         RestrictRealtime = true;
         SystemCallArchitectures = "native";
         SystemCallFilter = "@system-service";
         UMask = "0077";
       };
+
+      command = "${pkgs.pinnwand}/bin/pinnwand --configuration-path ${configFile}";
+    in {
+      pinnwand = {
+        description = "Pinnwannd HTTP Server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        unitConfig.Documentation = "https://pinnwand.readthedocs.io/en/latest/";
+
+        serviceConfig = {
+          ExecStart = "${command} http --port ${toString(cfg.port)}";
+        } // hardeningOptions;
+      };
+
+      pinnwand-reaper = {
+        description = "Pinnwand Reaper";
+        startAt = "daily";
+
+        serviceConfig = {
+          ExecStart = "${command} -vvvv reap";  # verbosity increased to show number of deleted pastes
+        } // hardeningOptions;
+      };
     };
   };
 }
diff --git a/nixos/modules/services/misc/plikd.nix b/nixos/modules/services/misc/plikd.nix
new file mode 100644
index 00000000000..a62dbef1d2a
--- /dev/null
+++ b/nixos/modules/services/misc/plikd.nix
@@ -0,0 +1,82 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plikd;
+
+  format = pkgs.formats.toml {};
+  plikdCfg = format.generate "plikd.cfg" cfg.settings;
+in
+{
+  options = {
+    services.plikd = {
+      enable = mkEnableOption "the plikd server";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Open ports in the firewall for the plikd.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Configuration for plikd, see <link xlink:href="https://github.com/root-gg/plik/blob/master/server/plikd.cfg"/>
+          for supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.plikd.settings = mapAttrs (name: mkDefault) {
+      ListenPort = 8080;
+      ListenAddress = "localhost";
+      DataBackend = "file";
+      DataBackendConfig = {
+         Directory = "/var/lib/plikd";
+      };
+      MetadataBackendConfig = {
+        Driver = "sqlite3";
+        ConnectionString = "/var/lib/plikd/plik.db";
+      };
+    };
+
+    systemd.services.plikd = {
+      description = "Plikd file sharing server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.plikd}/bin/plikd --config ${plikdCfg}";
+        Restart = "on-failure";
+        StateDirectory = "plikd";
+        LogsDirectory = "plikd";
+        DynamicUser = true;
+
+        # Basic hardening
+        NoNewPrivileges = "yes";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+        ProtectControlGroups = "yes";
+        ProtectKernelModules = "yes";
+        ProtectKernelTunables = "yes";
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = "yes";
+        RestrictRealtime = "yes";
+        RestrictSUIDSGID = "yes";
+        MemoryDenyWriteExecute = "yes";
+        LockPersonality = "yes";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.settings.ListenPort ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/podgrab.nix b/nixos/modules/services/misc/podgrab.nix
new file mode 100644
index 00000000000..7077408b794
--- /dev/null
+++ b/nixos/modules/services/misc/podgrab.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.podgrab;
+in
+{
+  options.services.podgrab = with lib; {
+    enable = mkEnableOption "Podgrab, a self-hosted podcast manager";
+
+    passwordFile = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "/run/secrets/password.env";
+      description = ''
+        The path to a file containing the PASSWORD environment variable
+        definition for Podgrab's authentification.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      example = 4242;
+      description = "The port on which Podgrab will listen for incoming HTTP traffic.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.podgrab = {
+      description = "Podgrab podcast manager";
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        CONFIG = "/var/lib/podgrab/config";
+        DATA = "/var/lib/podgrab/data";
+        GIN_MODE = "release";
+        PORT = toString cfg.port;
+      };
+      serviceConfig = {
+        DynamicUser = true;
+        EnvironmentFile = lib.optional (cfg.passwordFile != null) [
+          cfg.passwordFile
+        ];
+        ExecStart = "${pkgs.podgrab}/bin/podgrab";
+        WorkingDirectory = "${pkgs.podgrab}/share";
+        StateDirectory = [ "podgrab/config" "podgrab/data" ];
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ambroisie ];
+}
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index d6aeae48ccb..2f752bcc7ed 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -1,12 +1,12 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
   cfg = config.services.pykms;
   libDir = "/var/lib/pykms";
 
-in {
+in
+{
   meta.maintainers = with lib.maintainers; [ peterhoeg ];
 
   imports = [
@@ -46,14 +46,14 @@ in {
       };
 
       logLevel = mkOption {
-        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MINI" ];
+        type = types.enum [ "CRITICAL" "ERROR" "WARNING" "INFO" "DEBUG" "MININFO" ];
         default = "INFO";
         description = "How much to log";
       };
 
       extraArgs = mkOption {
         type = types.listOf types.str;
-        default = [];
+        default = [ ];
         description = "Additional arguments";
       };
     };
@@ -74,8 +74,9 @@ in {
         ExecStartPre = "${getBin pykms}/libexec/create_pykms_db.sh ${libDir}/clients.db";
         ExecStart = lib.concatStringsSep " " ([
           "${getBin pykms}/bin/server"
-          "--logfile STDOUT"
-          "--loglevel ${cfg.logLevel}"
+          "--logfile=STDOUT"
+          "--loglevel=${cfg.logLevel}"
+          "--sqlite=${libDir}/clients.db"
         ] ++ cfg.extraArgs ++ [
           cfg.listenAddress
           (toString cfg.port)
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 0e71cf92569..66c8e558fb0 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -1,12 +1,12 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mkDefault mkEnableOption mkIf mkOption types;
+  inherit (lib) mkBefore mkDefault mkEnableOption mkIf mkOption mkRemovedOptionModule types;
   inherit (lib) concatStringsSep literalExample mapAttrsToList;
-  inherit (lib) optional optionalAttrs optionalString singleton versionAtLeast;
+  inherit (lib) optional optionalAttrs optionalString;
 
   cfg = config.services.redmine;
-
+  format = pkgs.formats.yaml {};
   bundle = "${cfg.package}/share/redmine/bin/bundle";
 
   databaseYml = pkgs.writeText "database.yml" ''
@@ -20,31 +20,15 @@ let
       ${optionalString (cfg.database.type == "mysql2" && cfg.database.socket != null) "socket: ${cfg.database.socket}"}
   '';
 
-  configurationYml = pkgs.writeText "configuration.yml" ''
-    default:
-      scm_subversion_command: ${pkgs.subversion}/bin/svn
-      scm_mercurial_command: ${pkgs.mercurial}/bin/hg
-      scm_git_command: ${pkgs.gitAndTools.git}/bin/git
-      scm_cvs_command: ${pkgs.cvs}/bin/cvs
-      scm_bazaar_command: ${pkgs.breezy}/bin/bzr
-      scm_darcs_command: ${pkgs.darcs}/bin/darcs
-
-    ${cfg.extraConfig}
-  '';
-
-  additionalEnvironment = pkgs.writeText "additional_environment.rb" ''
-    config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
-    config.logger.level = Logger::INFO
-
-    ${cfg.extraEnv}
-  '';
+  configurationYml = format.generate "configuration.yml" cfg.settings;
+  additionalEnvironment = pkgs.writeText "additional_environment.rb" cfg.extraEnv;
 
   unpackTheme = unpack "theme";
   unpackPlugin = unpack "plugin";
   unpack = id: (name: source:
     pkgs.stdenv.mkDerivation {
       name = "redmine-${id}-${name}";
-      buildInputs = [ pkgs.unzip ];
+      nativeBuildInputs = [ pkgs.unzip ];
       buildCommand = ''
         mkdir -p $out
         cd $out
@@ -56,8 +40,13 @@ let
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "postgresql";
 
 in
-
 {
+  imports = [
+    (mkRemovedOptionModule [ "services" "redmine" "extraConfig" ] "Use services.redmine.settings instead.")
+    (mkRemovedOptionModule [ "services" "redmine" "database" "password" ] "Use services.redmine.database.passwordFile instead.")
+  ];
+
+  # interface
   options = {
     services.redmine = {
       enable = mkEnableOption "Redmine";
@@ -82,7 +71,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 3000;
         description = "Port on which Redmine is ran.";
       };
@@ -93,21 +82,24 @@ in
         description = "The state directory, logs and plugins are stored here.";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
+      settings = mkOption {
+        type = format.type;
+        default = {};
         description = ''
-          Extra configuration in configuration.yml.
-
-          See <link xlink:href="https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration"/>
+          Redmine configuration (<filename>configuration.yml</filename>). Refer to
+          <link xlink:href="https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration"/>
           for details.
         '';
         example = literalExample ''
-          email_delivery:
-            delivery_method: smtp
-            smtp_settings:
-              address: mail.example.com
-              port: 25
+          {
+            email_delivery = {
+              delivery_method = "smtp";
+              smtp_settings = {
+                address = "mail.example.com";
+                port = 25;
+              };
+            };
+          }
         '';
       };
 
@@ -186,16 +178,6 @@ in
           description = "Database user.";
         };
 
-        password = mkOption {
-          type = types.str;
-          default = "";
-          description = ''
-            The password corresponding to <option>database.user</option>.
-            Warning: this is stored in cleartext in the Nix store!
-            Use <option>database.passwordFile</option> instead.
-          '';
-        };
-
         passwordFile = mkOption {
           type = types.nullOr types.path;
           default = null;
@@ -226,11 +208,12 @@ in
     };
   };
 
+  # implementation
   config = mkIf cfg.enable {
 
     assertions = [
-      { assertion = cfg.database.passwordFile != null || cfg.database.password != "" || cfg.database.socket != null;
-        message = "one of services.redmine.database.socket, services.redmine.database.passwordFile, or services.redmine.database.password must be set";
+      { assertion = cfg.database.passwordFile != null || cfg.database.socket != null;
+        message = "one of services.redmine.database.socket or services.redmine.database.passwordFile must be set";
       }
       { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
         message = "services.redmine.database.user must be set to ${cfg.user} if services.redmine.database.createLocally is set true";
@@ -243,6 +226,22 @@ in
       }
     ];
 
+    services.redmine.settings = {
+      production = {
+        scm_subversion_command = "${pkgs.subversion}/bin/svn";
+        scm_mercurial_command = "${pkgs.mercurial}/bin/hg";
+        scm_git_command = "${pkgs.git}/bin/git";
+        scm_cvs_command = "${pkgs.cvs}/bin/cvs";
+        scm_bazaar_command = "${pkgs.breezy}/bin/bzr";
+        scm_darcs_command = "${pkgs.darcs}/bin/darcs";
+      };
+    };
+
+    services.redmine.extraEnv = mkBefore ''
+      config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
+      config.logger.level = Logger::INFO
+    '';
+
     services.mysql = mkIf mysqlLocal {
       enable = true;
       package = mkDefault pkgs.mariadb;
@@ -300,7 +299,7 @@ in
         breezy
         cvs
         darcs
-        gitAndTools.git
+        git
         mercurial
         subversion
       ];
@@ -338,7 +337,7 @@ in
 
 
         # handle database.passwordFile & permissions
-        DBPASS=$(head -n1 ${cfg.database.passwordFile})
+        DBPASS=${optionalString (cfg.database.passwordFile != null) "$(head -n1 ${cfg.database.passwordFile})"}
         cp -f ${databaseYml} "${cfg.stateDir}/config/database.yml"
         sed -e "s,#dbpass#,$DBPASS,g" -i "${cfg.stateDir}/config/database.yml"
         chmod 440 "${cfg.stateDir}/config/database.yml"
@@ -379,17 +378,6 @@ in
       redmine.gid = config.ids.gids.redmine;
     };
 
-    warnings = optional (cfg.database.password != "")
-      ''config.services.redmine.database.password will be stored as plaintext
-      in the Nix store. Use database.passwordFile instead.'';
-
-    # Create database passwordFile default when password is configured.
-    services.redmine.database.passwordFile =
-      (mkDefault (toString (pkgs.writeTextFile {
-        name = "redmine-database-password";
-        text = cfg.database.password;
-      })));
-
   };
 
 }
diff --git a/nixos/modules/services/misc/rippled.nix b/nixos/modules/services/misc/rippled.nix
index ef34e3a779f..2fce3b9dc94 100644
--- a/nixos/modules/services/misc/rippled.nix
+++ b/nixos/modules/services/misc/rippled.nix
@@ -389,6 +389,7 @@ in
 
       extraConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Extra lines to be added verbatim to the rippled.cfg configuration file.
         '';
diff --git a/nixos/modules/services/misc/safeeyes.nix b/nixos/modules/services/misc/safeeyes.nix
index 6ecb0d13187..1e748195e41 100644
--- a/nixos/modules/services/misc/safeeyes.nix
+++ b/nixos/modules/services/misc/safeeyes.nix
@@ -32,14 +32,14 @@ in
       wantedBy = [ "graphical-session.target" ];
       partOf   = [ "graphical-session.target" ];
 
+      startLimitIntervalSec = 350;
+      startLimitBurst = 10;
       serviceConfig = {
         ExecStart = ''
           ${pkgs.safeeyes}/bin/safeeyes
         '';
         Restart = "on-failure";
         RestartSec = 3;
-        StartLimitInterval = 350;
-        StartLimitBurst = 10;
       };
     };
 
diff --git a/nixos/modules/services/misc/sdrplay.nix b/nixos/modules/services/misc/sdrplay.nix
new file mode 100644
index 00000000000..2801108f082
--- /dev/null
+++ b/nixos/modules/services/misc/sdrplay.nix
@@ -0,0 +1,35 @@
+{ config, lib, pkgs, ... }:
+with lib;
+{
+  options.services.sdrplayApi = {
+    enable = mkOption {
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable the SDRplay API service and udev rules.
+
+        <note><para>
+          To enable integration with SoapySDR and GUI applications like gqrx create an overlay containing
+          <literal>soapysdr-with-plugins = super.soapysdr.override { extraPackages = [ super.soapysdrplay ]; };</literal>
+        </para></note>
+      '';
+      type = lib.types.bool;
+    };
+  };
+
+  config = mkIf config.services.sdrplayApi.enable {
+    systemd.services.sdrplayApi = {
+      description = "SDRplay API Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.sdrplay}/bin/sdrplay_apiService";
+        DynamicUser = true;
+        Restart = "on-failure";
+        RestartSec = "1s";
+      };
+    };
+    services.udev.packages = [ pkgs.sdrplay ];
+
+  };
+}
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index 0e87fc461d3..20fe0793b84 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -39,7 +39,7 @@ in
         default = false;
         description = ''
           Whether to enable the Siproxd SIP
-	  proxy/masquerading daemon.
+          proxy/masquerading daemon.
         '';
       };
 
@@ -57,29 +57,29 @@ in
 
       hostsAllowReg = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "192.168.1.0/24" "192.168.2.0/24" ];
-	description = ''
+        description = ''
           Acess control list for incoming SIP registrations.
         '';
       };
 
       hostsAllowSip = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "123.45.0.0/16" "123.46.0.0/16" ];
-	description = ''
+        description = ''
           Acess control list for incoming SIP traffic.
         '';
       };
 
       hostsDenySip = mkOption {
         type = types.listOf types.str;
-	default = [ ];
+        default = [ ];
         example = [ "10.0.0.0/8" "11.0.0.0/8" ];
-	description = ''
+        description = ''
           Acess control list for denying incoming
-	   SIP registrations and traffic.
+          SIP registrations and traffic.
         '';
       };
 
@@ -87,7 +87,7 @@ in
         type = types.int;
         default = 5060;
         description = ''
-	  Port to listen for incoming SIP messages.
+          Port to listen for incoming SIP messages.
         '';
       };
 
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
index 6f3aaa973a0..a821b9b6bf6 100644
--- a/nixos/modules/services/misc/snapper.nix
+++ b/nixos/modules/services/misc/snapper.nix
@@ -48,6 +48,8 @@ in
           subvolume = "/home";
           extraConfig = ''
             ALLOW_USERS="alice"
+            TIMELINE_CREATE=yes
+            TIMELINE_CLEANUP=yes
           '';
         };
       };
@@ -121,6 +123,16 @@ in
 
     services.dbus.packages = [ pkgs.snapper ];
 
+    systemd.services.snapperd = {
+      description = "DBus interface for snapper";
+      inherit documentation;
+      serviceConfig = {
+        Type = "dbus";
+        BusName = "org.opensuse.Snapper";
+        ExecStart = "${pkgs.snapper}/bin/snapperd";
+      };
+    };
+
     systemd.services.snapper-timeline = {
       description = "Timeline of Snapper Snapshots";
       inherit documentation;
diff --git a/nixos/modules/services/misc/sourcehut/builds.nix b/nixos/modules/services/misc/sourcehut/builds.nix
new file mode 100644
index 00000000000..a17a1010dbf
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/builds.nix
@@ -0,0 +1,234 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  scfg = cfg.builds;
+  rcfg = config.services.redis;
+  iniKey = "builds.sr.ht";
+
+  drv = pkgs.sourcehut.buildsrht;
+in
+{
+  options.services.sourcehut.builds = {
+    user = mkOption {
+      type = types.str;
+      default = "buildsrht";
+      description = ''
+        User for builds.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5002;
+      description = ''
+        Port on which the "builds" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "builds.sr.ht";
+      description = ''
+        PostgreSQL database name for builds.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/buildsrht";
+      description = ''
+        State path for builds.sr.ht.
+      '';
+    };
+
+    enableWorker = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Run workers for builds.sr.ht.
+      '';
+    };
+
+    images = mkOption {
+      type = types.attrsOf (types.attrsOf (types.attrsOf types.package));
+      default = { };
+      example = lib.literalExample ''(let
+          # Pinning unstable to allow usage with flakes and limit rebuilds.
+          pkgs_unstable = builtins.fetchGit {
+              url = "https://github.com/NixOS/nixpkgs";
+              rev = "ff96a0fa5635770390b184ae74debea75c3fd534";
+              ref = "nixos-unstable";
+          };
+          image_from_nixpkgs = pkgs_unstable: (import ("${pkgs.sourcehut.buildsrht}/lib/images/nixos/image.nix") {
+            pkgs = (import pkgs_unstable {});
+          });
+        in
+        {
+          nixos.unstable.x86_64 = image_from_nixpkgs pkgs_unstable;
+        }
+      )'';
+      description = ''
+        Images for builds.sr.ht. Each package should be distro.release.arch and point to a /nix/store/package/root.img.qcow2.
+      '';
+    };
+
+  };
+
+  config = with scfg; let
+    image_dirs = lib.lists.flatten (
+      lib.attrsets.mapAttrsToList
+        (distro: revs:
+          lib.attrsets.mapAttrsToList
+            (rev: archs:
+              lib.attrsets.mapAttrsToList
+                (arch: image:
+                  pkgs.runCommandNoCC "buildsrht-images" { } ''
+                    mkdir -p $out/${distro}/${rev}/${arch}
+                    ln -s ${image}/*.qcow2 $out/${distro}/${rev}/${arch}/root.img.qcow2
+                  '')
+                archs)
+            revs)
+        scfg.images);
+    image_dir_pre = pkgs.symlinkJoin {
+      name = "builds.sr.ht-worker-images-pre";
+      paths = image_dirs ++ [
+        "${pkgs.sourcehut.buildsrht}/lib/images"
+      ];
+    };
+    image_dir = pkgs.runCommandNoCC "builds.sr.ht-worker-images" { } ''
+      mkdir -p $out/images
+      cp -Lr ${image_dir_pre}/* $out/images
+    '';
+  in
+  lib.mkIf (cfg.enable && elem "builds" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = lib.optionals cfg.builds.enableWorker [ "docker" ];
+          description = "builds.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0755 ${user} ${user} -"
+      ] ++ (lib.optionals cfg.builds.enableWorker
+        [ "d ${statePath}/logs 0775 ${user} ${user} - -" ]
+      );
+
+      services = {
+        buildsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey
+          {
+            after = [ "postgresql.service" "network.target" ];
+            requires = [ "postgresql.service" ];
+            wantedBy = [ "multi-user.target" ];
+
+            description = "builds.sr.ht website service";
+
+            serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+
+            # Hack to bypass this hack: https://git.sr.ht/~sircmpwn/core.sr.ht/tree/master/item/srht-update-profiles#L6
+          } // { preStart = " "; };
+
+        buildsrht-worker = {
+          enable = scfg.enableWorker;
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+          partOf = [ "buildsrht.service" ];
+          description = "builds.sr.ht worker service";
+          path = [ pkgs.openssh pkgs.docker ];
+          preStart = let qemuPackage = pkgs.qemu_kvm;
+          in ''
+            if [[ "$(docker images -q qemu:latest 2> /dev/null)" == "" || "$(cat ${statePath}/docker-image-qemu 2> /dev/null || true)" != "${qemuPackage.version}" ]]; then
+              # Create and import qemu:latest image for docker
+              ${
+                pkgs.dockerTools.streamLayeredImage {
+                  name = "qemu";
+                  tag = "latest";
+                  contents = [ qemuPackage ];
+                }
+              } | docker load
+              # Mark down current package version
+              printf "%s" "${qemuPackage.version}" > ${statePath}/docker-image-qemu
+            fi
+          '';
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Group = "nginx";
+            Restart = "always";
+          };
+          serviceConfig.ExecStart = "${pkgs.sourcehut.buildsrht}/bin/builds.sr.ht-worker";
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL builds.sr.ht is being served at (protocol://domain)
+      "builds.sr.ht".origin = mkDefault "http://builds.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "builds.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "builds.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "builds.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "builds.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # builds.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "builds.sr.ht".oauth-client-id = mkDefault null;
+      "builds.sr.ht".oauth-client-secret = mkDefault null;
+      # The redis connection used for the celery worker
+      "builds.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/3";
+      # The shell used for ssh
+      "builds.sr.ht".shell = mkDefault "runner-shell";
+      # Register the builds.sr.ht dispatcher
+      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.buildsrht}/bin/buildsrht-keys"} = mkDefault "${user}:${user}";
+
+      # Location for build logs, images, and control command
+    } // lib.attrsets.optionalAttrs scfg.enableWorker {
+      # Default worker stores logs that are accessible via this address:port
+      "builds.sr.ht::worker".name = mkDefault "127.0.0.1:5020";
+      "builds.sr.ht::worker".buildlogs = mkDefault "${scfg.statePath}/logs";
+      "builds.sr.ht::worker".images = mkDefault "${image_dir}/images";
+      "builds.sr.ht::worker".controlcmd = mkDefault "${image_dir}/images/control";
+      "builds.sr.ht::worker".timeout = mkDefault "3m";
+    };
+
+    services.nginx.virtualHosts."logs.${cfg.originBase}" =
+      if scfg.enableWorker then {
+        listen = with builtins; let address = split ":" cfg.settings."builds.sr.ht::worker".name;
+        in [{ addr = elemAt address 0; port = lib.toInt (elemAt address 2); }];
+        locations."/logs".root = "${scfg.statePath}";
+      } else { };
+
+    services.nginx.virtualHosts."builds.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.buildsrht}/${pkgs.sourcehut.python.sitePackages}/buildsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
new file mode 100644
index 00000000000..9c812d6b043
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -0,0 +1,198 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  settingsFormat = pkgs.formats.ini { };
+
+  # Specialized python containing all the modules
+  python = pkgs.sourcehut.python.withPackages (ps: with ps; [
+    gunicorn
+    # Sourcehut services
+    srht
+    buildsrht
+    dispatchsrht
+    gitsrht
+    hgsrht
+    hubsrht
+    listssrht
+    mansrht
+    metasrht
+    pastesrht
+    todosrht
+  ]);
+in
+{
+  imports =
+    [
+      ./git.nix
+      ./hg.nix
+      ./hub.nix
+      ./todo.nix
+      ./man.nix
+      ./meta.nix
+      ./paste.nix
+      ./builds.nix
+      ./lists.nix
+      ./dispatch.nix
+      (mkRemovedOptionModule [ "services" "sourcehut" "nginx" "enable" ] ''
+        The sourcehut module supports `nginx` as a local reverse-proxy by default and doesn't
+        support other reverse-proxies officially.
+
+        However it's possible to use an alternative reverse-proxy by
+
+          * disabling nginx
+          * adjusting the relevant settings for server addresses and ports directly
+
+        Further details about this can be found in the `Sourcehut`-section of the NixOS-manual.
+      '')
+    ];
+
+  options.services.sourcehut = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Enable sourcehut - git hosting, continuous integration, mailing list, ticket tracking,
+        task dispatching, wiki and account management services
+      '';
+    };
+
+    services = mkOption {
+      type = types.nonEmptyListOf (types.enum [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ]);
+      default = [ "man" "meta" "paste" ];
+      example = [ "builds" "dispatch" "git" "hub" "hg" "lists" "man" "meta" "paste" "todo" ];
+      description = ''
+        Services to enable on the sourcehut network.
+      '';
+    };
+
+    originBase = mkOption {
+      type = types.str;
+      default = with config.networking; hostName + lib.optionalString (domain != null) ".${domain}";
+      description = ''
+        Host name used by reverse-proxy and for default settings. Will host services at git."''${originBase}". For example: git.sr.ht
+      '';
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Address to bind to.
+      '';
+    };
+
+    python = mkOption {
+      internal = true;
+      type = types.package;
+      default = python;
+      description = ''
+        The python package to use. It should contain references to the *srht modules and also
+        gunicorn.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "/var/lib/sourcehut";
+      description = ''
+        Root state path for the sourcehut network. If left as the default value
+        this directory will automatically be created before the sourcehut server
+        starts, otherwise the sysadmin is responsible for ensuring the
+        directory exists with appropriate ownership and permissions.
+      '';
+    };
+
+    settings = mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+      };
+      default = { };
+      description = ''
+        The configuration for the sourcehut network.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions =
+      [
+        {
+          assertion = with cfgIni.webhooks; private-key != null && stringLength private-key == 44;
+          message = "The webhook's private key must be defined and of a 44 byte length.";
+        }
+
+        {
+          assertion = hasAttrByPath [ "meta.sr.ht" "origin" ] cfgIni && cfgIni."meta.sr.ht".origin != null;
+          message = "meta.sr.ht's origin must be defined.";
+        }
+      ];
+
+    virtualisation.docker.enable = true;
+    environment.etc."sr.ht/config.ini".source =
+      settingsFormat.generate "sourcehut-config.ini" (mapAttrsRecursive
+        (
+          path: v: if v == null then "" else v
+        )
+        cfg.settings);
+
+    environment.systemPackages = [ pkgs.sourcehut.coresrht ];
+
+    # PostgreSQL server
+    services.postgresql.enable = mkOverride 999 true;
+    # Mail server
+    services.postfix.enable = mkOverride 999 true;
+    # Cron daemon
+    services.cron.enable = mkOverride 999 true;
+    # Redis server
+    services.redis.enable = mkOverride 999 true;
+    services.redis.bind = mkOverride 999 "127.0.0.1";
+
+    services.sourcehut.settings = {
+      # The name of your network of sr.ht-based sites
+      "sr.ht".site-name = mkDefault "sourcehut";
+      # The top-level info page for your site
+      "sr.ht".site-info = mkDefault "https://sourcehut.org";
+      # {{ site-name }}, {{ site-blurb }}
+      "sr.ht".site-blurb = mkDefault "the hacker's forge";
+      # If this != production, we add a banner to each page
+      "sr.ht".environment = mkDefault "development";
+      # Contact information for the site owners
+      "sr.ht".owner-name = mkDefault "Drew DeVault";
+      "sr.ht".owner-email = mkDefault "sir@cmpwn.com";
+      # The source code for your fork of sr.ht
+      "sr.ht".source-url = mkDefault "https://git.sr.ht/~sircmpwn/srht";
+      # A secret key to encrypt session cookies with
+      "sr.ht".secret-key = mkDefault null;
+      "sr.ht".global-domain = mkDefault null;
+
+      # Outgoing SMTP settings
+      mail.smtp-host = mkDefault null;
+      mail.smtp-port = mkDefault null;
+      mail.smtp-user = mkDefault null;
+      mail.smtp-password = mkDefault null;
+      mail.smtp-from = mkDefault null;
+      # Application exceptions are emailed to this address
+      mail.error-to = mkDefault null;
+      mail.error-from = mkDefault null;
+      # Your PGP key information (DO NOT mix up pub and priv here)
+      # You must remove the password from your secret key, if present.
+      # You can do this with gpg --edit-key [key-id], then use the passwd
+      # command and do not enter a new password.
+      mail.pgp-privkey = mkDefault null;
+      mail.pgp-pubkey = mkDefault null;
+      mail.pgp-key-id = mkDefault null;
+
+      # base64-encoded Ed25519 key for signing webhook payloads. This should be
+      # consistent for all *.sr.ht sites, as we'll use this key to verify signatures
+      # from other sites in your network.
+      #
+      # Use the srht-webhook-keygen command to generate a key.
+      webhooks.private-key = mkDefault null;
+    };
+  };
+  meta.doc = ./sourcehut.xml;
+  meta.maintainers = with maintainers; [ tomberek ];
+}
diff --git a/nixos/modules/services/misc/sourcehut/dispatch.nix b/nixos/modules/services/misc/sourcehut/dispatch.nix
new file mode 100644
index 00000000000..a9db17bebe8
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/dispatch.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.dispatch;
+  iniKey = "dispatch.sr.ht";
+
+  drv = pkgs.sourcehut.dispatchsrht;
+in
+{
+  options.services.sourcehut.dispatch = {
+    user = mkOption {
+      type = types.str;
+      default = "dispatchsrht";
+      description = ''
+        User for dispatch.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5005;
+      description = ''
+        Port on which the "dispatch" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "dispatch.sr.ht";
+      description = ''
+        PostgreSQL database name for dispatch.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/dispatchsrht";
+      description = ''
+        State path for dispatch.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "dispatch" cfg.services) {
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "dispatch.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.dispatchsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "dispatch.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL dispatch.sr.ht is being served at (protocol://domain)
+      "dispatch.sr.ht".origin = mkDefault "http://dispatch.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "dispatch.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "dispatch.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "dispatch.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "dispatch.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # dispatch.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "dispatch.sr.ht".oauth-client-id = mkDefault null;
+      "dispatch.sr.ht".oauth-client-secret = mkDefault null;
+
+      # Github Integration
+      "dispatch.sr.ht::github".oauth-client-id = mkDefault null;
+      "dispatch.sr.ht::github".oauth-client-secret = mkDefault null;
+
+      # Gitlab Integration
+      "dispatch.sr.ht::gitlab".enabled = mkDefault null;
+      "dispatch.sr.ht::gitlab".canonical-upstream = mkDefault "gitlab.com";
+      "dispatch.sr.ht::gitlab".repo-cache = mkDefault "./repo-cache";
+      # "dispatch.sr.ht::gitlab"."gitlab.com" = mkDefault "GitLab:application id:secret";
+    };
+
+    services.nginx.virtualHosts."dispatch.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.dispatchsrht}/${pkgs.sourcehut.python.sitePackages}/dispatchsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/git.nix b/nixos/modules/services/misc/sourcehut/git.nix
new file mode 100644
index 00000000000..99b9aec0612
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/git.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  scfg = cfg.git;
+  iniKey = "git.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.gitsrht;
+in
+{
+  options.services.sourcehut.git = {
+    user = mkOption {
+      type = types.str;
+      visible = false;
+      internal = true;
+      readOnly = true;
+      default = "git";
+      description = ''
+        User for git.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5001;
+      description = ''
+        Port on which the "git" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "git.sr.ht";
+      description = ''
+        PostgreSQL database name for git.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/gitsrht";
+      description = ''
+        State path for git.sr.ht.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.git;
+      example = literalExample "pkgs.gitFull";
+      description = ''
+        Git package for git.sr.ht. This can help silence collisions.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "git" cfg.services) {
+    # sshd refuses to run with `Unsafe AuthorizedKeysCommand ... bad ownership or modes for directory /nix/store`
+    environment.etc."ssh/gitsrht-dispatch" = {
+      mode = "0755";
+      text = ''
+        #! ${pkgs.stdenv.shell}
+        ${cfg.python}/bin/gitsrht-dispatch "$@"
+      '';
+    };
+
+    # Needs this in the $PATH when sshing into the server
+    environment.systemPackages = [ cfg.git.package ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          # https://stackoverflow.com/questions/22314298/git-push-results-in-fatal-protocol-error-bad-line-length-character-this
+          # Probably could use gitsrht-shell if output is restricted to just parameters...
+          shell = pkgs.bash;
+          description = "git.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services = {
+      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/gitsrht-periodic" ];
+      fcgiwrap.enable = true;
+
+      openssh.authorizedKeysCommand = ''/etc/ssh/gitsrht-dispatch "%u" "%h" "%t" "%k"'';
+      openssh.authorizedKeysCommandUser = "root";
+      openssh.extraConfig = ''
+        PermitUserEnvironment SRHT_*
+      '';
+
+      postgresql = {
+        authentication = ''
+          local ${database} ${user} trust
+        '';
+        ensureDatabases = [ database ];
+        ensureUsers = [
+          {
+            name = user;
+            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        # /var/log is owned by root
+        "f /var/log/git-srht-shell 0644 ${user} ${user} -"
+
+        "d ${statePath} 0750 ${user} ${user} -"
+        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
+      ];
+
+      services = {
+        gitsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "redis.service" "postgresql.service" "network.target" ];
+          requires = [ "redis.service" "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          # Needs internally to create repos at the very least
+          path = [ pkgs.git ];
+          description = "git.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        gitsrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "git.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+          };
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL git.sr.ht is being served at (protocol://domain)
+      "git.sr.ht".origin = mkDefault "http://git.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "git.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "git.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "git.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "git.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # The redis connection used for the webhooks worker
+      "git.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+
+      # A post-update script which is installed in every git repo.
+      "git.sr.ht".post-update-script = mkDefault "${pkgs.sourcehut.gitsrht}/bin/gitsrht-update-hook";
+
+      # git.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "git.sr.ht".oauth-client-id = mkDefault null;
+      "git.sr.ht".oauth-client-secret = mkDefault null;
+      # Path to git repositories on disk
+      "git.sr.ht".repos = mkDefault "/var/lib/git";
+
+      "git.sr.ht".outgoing-domain = mkDefault "http://git.${cfg.originBase}";
+
+      # The authorized keys hook uses this to dispatch to various handlers
+      # The format is a program to exec into as the key, and the user to match as the
+      # value. When someone tries to log in as this user, this program is executed
+      # and is expected to omit an AuthorizedKeys file.
+      #
+      # Discard of the string context is in order to allow derivation-derived strings.
+      # This is safe if the relevant package is installed which will be the case if the setting is utilized.
+      "git.sr.ht::dispatch".${builtins.unsafeDiscardStringContext "${pkgs.sourcehut.gitsrht}/bin/gitsrht-keys"} = mkDefault "${user}:${user}";
+    };
+
+    services.nginx.virtualHosts."git.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.gitsrht}/${pkgs.sourcehut.python.sitePackages}/gitsrht";
+      extraConfig = ''
+            location = /authorize {
+            proxy_pass http://${cfg.address}:${toString port};
+            proxy_pass_request_body off;
+            proxy_set_header Content-Length "";
+            proxy_set_header X-Original-URI $request_uri;
+        }
+            location ~ ^/([^/]+)/([^/]+)/(HEAD|info/refs|objects/info/.*|git-upload-pack).*$ {
+                auth_request /authorize;
+                root /var/lib/git;
+                fastcgi_pass unix:/run/fcgiwrap.sock;
+                fastcgi_param SCRIPT_FILENAME ${pkgs.git}/bin/git-http-backend;
+                fastcgi_param PATH_INFO $uri;
+                fastcgi_param GIT_PROJECT_ROOT $document_root;
+                fastcgi_read_timeout 500s;
+                include ${pkgs.nginx}/conf/fastcgi_params;
+                gzip off;
+            }
+      '';
+
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/hg.nix b/nixos/modules/services/misc/sourcehut/hg.nix
new file mode 100644
index 00000000000..5cd36bb0455
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hg.nix
@@ -0,0 +1,173 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  scfg = cfg.hg;
+  iniKey = "hg.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.hgsrht;
+in
+{
+  options.services.sourcehut.hg = {
+    user = mkOption {
+      type = types.str;
+      internal = true;
+      readOnly = true;
+      default = "hg";
+      description = ''
+        User for hg.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5010;
+      description = ''
+        Port on which the "hg" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "hg.sr.ht";
+      description = ''
+        PostgreSQL database name for hg.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/hgsrht";
+      description = ''
+        State path for hg.sr.ht.
+      '';
+    };
+
+    cloneBundles = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Generate clonebundles (which require more disk space but dramatically speed up cloning large repositories).
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "hg" cfg.services) {
+    # In case it ever comes into being
+    environment.etc."ssh/hgsrht-dispatch" = {
+      mode = "0755";
+      text = ''
+        #! ${pkgs.stdenv.shell}
+        ${cfg.python}/bin/gitsrht-dispatch $@
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.mercurial ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          # Assuming hg.sr.ht needs this too
+          shell = pkgs.bash;
+          description = "hg.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services = {
+      cron.systemCronJobs = [ "*/20 * * * * ${cfg.python}/bin/hgsrht-periodic" ]
+        ++ optional cloneBundles "0 * * * * ${cfg.python}/bin/hgsrht-clonebundles";
+
+      openssh.authorizedKeysCommand = ''/etc/ssh/hgsrht-dispatch "%u" "%h" "%t" "%k"'';
+      openssh.authorizedKeysCommandUser = "root";
+      openssh.extraConfig = ''
+        PermitUserEnvironment SRHT_*
+      '';
+
+      postgresql = {
+        authentication = ''
+          local ${database} ${user} trust
+        '';
+        ensureDatabases = [ database ];
+        ensureUsers = [
+          {
+            name = user;
+            ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+          }
+        ];
+      };
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        # /var/log is owned by root
+        "f /var/log/hg-srht-shell 0644 ${user} ${user} -"
+
+        "d ${statePath} 0750 ${user} ${user} -"
+        "d ${cfg.settings."${iniKey}".repos} 2755 ${user} ${user} -"
+      ];
+
+      services.hgsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "redis.service" "postgresql.service" "network.target" ];
+        requires = [ "redis.service" "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        path = [ pkgs.mercurial ];
+        description = "hg.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL hg.sr.ht is being served at (protocol://domain)
+      "hg.sr.ht".origin = mkDefault "http://hg.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "hg.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "hg.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "hg.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # The redis connection used for the webhooks worker
+      "hg.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+      # A post-update script which is installed in every mercurial repo.
+      "hg.sr.ht".changegroup-script = mkDefault "${cfg.python}/bin/hgsrht-hook-changegroup";
+      # hg.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "hg.sr.ht".oauth-client-id = mkDefault null;
+      "hg.sr.ht".oauth-client-secret = mkDefault null;
+      # Path to mercurial repositories on disk
+      "hg.sr.ht".repos = mkDefault "/var/lib/hg";
+      # Path to the srht mercurial extension
+      # (defaults to where the hgsrht code is)
+      # "hg.sr.ht".srhtext = mkDefault null;
+      # .hg/store size (in MB) past which the nightly job generates clone bundles.
+      # "hg.sr.ht".clone_bundle_threshold = mkDefault 50;
+      # Path to hg-ssh (if not in $PATH)
+      # "hg.sr.ht".hg_ssh = mkDefault /path/to/hg-ssh;
+
+      # The authorized keys hook uses this to dispatch to various handlers
+      # The format is a program to exec into as the key, and the user to match as the
+      # value. When someone tries to log in as this user, this program is executed
+      # and is expected to omit an AuthorizedKeys file.
+      #
+      # Uncomment the relevant lines to enable the various sr.ht dispatchers.
+      "hg.sr.ht::dispatch"."/run/current-system/sw/bin/hgsrht-keys" = mkDefault "${user}:${user}";
+    };
+
+    # TODO: requires testing and addition of hg-specific requirements
+    services.nginx.virtualHosts."hg.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.hgsrht}/${pkgs.sourcehut.python.sitePackages}/hgsrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/hub.nix b/nixos/modules/services/misc/sourcehut/hub.nix
new file mode 100644
index 00000000000..be3ea21011c
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/hub.nix
@@ -0,0 +1,118 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.hub;
+  iniKey = "hub.sr.ht";
+
+  drv = pkgs.sourcehut.hubsrht;
+in
+{
+  options.services.sourcehut.hub = {
+    user = mkOption {
+      type = types.str;
+      default = "hubsrht";
+      description = ''
+        User for hub.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5014;
+      description = ''
+        Port on which the "hub" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "hub.sr.ht";
+      description = ''
+        PostgreSQL database name for hub.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/hubsrht";
+      description = ''
+        State path for hub.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "hub" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "hub.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.hubsrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "hub.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL hub.sr.ht is being served at (protocol://domain)
+      "hub.sr.ht".origin = mkDefault "http://hub.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "hub.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "hub.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "hub.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "hub.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # hub.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "hub.sr.ht".oauth-client-id = mkDefault null;
+      "hub.sr.ht".oauth-client-secret = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.hubsrht}/${pkgs.sourcehut.python.sitePackages}/hubsrht";
+    };
+    services.nginx.virtualHosts."hub.${cfg.originBase}" = {
+      globalRedirect = "${cfg.originBase}";
+      forceSSL = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/lists.nix b/nixos/modules/services/misc/sourcehut/lists.nix
new file mode 100644
index 00000000000..7b1fe9fd463
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/lists.nix
@@ -0,0 +1,185 @@
+# Email setup is fairly involved, useful references:
+# https://drewdevault.com/2018/08/05/Local-mail-server.html
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.lists;
+  iniKey = "lists.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.listssrht;
+in
+{
+  options.services.sourcehut.lists = {
+    user = mkOption {
+      type = types.str;
+      default = "listssrht";
+      description = ''
+        User for lists.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5006;
+      description = ''
+        Port on which the "lists" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "lists.sr.ht";
+      description = ''
+        PostgreSQL database name for lists.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/listssrht";
+      description = ''
+        State path for lists.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "lists" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = [ "postfix" ];
+          description = "lists.sr.ht user";
+        };
+      };
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        listssrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        listssrht-process = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht process service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.process worker --loglevel=info";
+          };
+        };
+
+        listssrht-lmtp = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht process service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/listssrht-lmtp";
+          };
+        };
+
+
+        listssrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "lists.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL lists.sr.ht is being served at (protocol://domain)
+      "lists.sr.ht".origin = mkDefault "http://lists.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "lists.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "lists.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "lists.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "lists.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # lists.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "lists.sr.ht".oauth-client-id = mkDefault null;
+      "lists.sr.ht".oauth-client-secret = mkDefault null;
+      # Outgoing email for notifications generated by users
+      "lists.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
+      # The redis connection used for the webhooks worker
+      "lists.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/2";
+      # The redis connection used for the celery worker
+      "lists.sr.ht".redis = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/4";
+      # Network-key
+      "lists.sr.ht".network-key = mkDefault null;
+      # Allow creation
+      "lists.sr.ht".allow-new-lists = mkDefault "no";
+      # Posting Domain
+      "lists.sr.ht".posting-domain = mkDefault "lists.${cfg.originBase}";
+
+      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
+      "lists.sr.ht::worker".sock = mkDefault "/tmp/lists.sr.ht-lmtp.sock";
+      # The lmtp daemon will make the unix socket group-read/write for users in this
+      # group.
+      "lists.sr.ht::worker".sock-group = mkDefault "postfix";
+      "lists.sr.ht::worker".reject-url = mkDefault "https://man.sr.ht/lists.sr.ht/etiquette.md";
+      "lists.sr.ht::worker".reject-mimetypes = mkDefault "text/html";
+
+    };
+
+    services.nginx.virtualHosts."lists.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.listssrht}/${pkgs.sourcehut.python.sitePackages}/listssrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/man.nix b/nixos/modules/services/misc/sourcehut/man.nix
new file mode 100644
index 00000000000..7693396d187
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/man.nix
@@ -0,0 +1,122 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.man;
+  iniKey = "man.sr.ht";
+
+  drv = pkgs.sourcehut.mansrht;
+in
+{
+  options.services.sourcehut.man = {
+    user = mkOption {
+      type = types.str;
+      default = "mansrht";
+      description = ''
+        User for man.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5004;
+      description = ''
+        Port on which the "man" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "man.sr.ht";
+      description = ''
+        PostgreSQL database name for man.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/mansrht";
+      description = ''
+        State path for man.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "man" cfg.services) {
+    assertions =
+      [
+        {
+          assertion = hasAttrByPath [ "git.sr.ht" "oauth-client-id" ] cfgIni;
+          message = "man.sr.ht needs access to git.sr.ht.";
+        }
+      ];
+
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "man.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services.mansrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+        after = [ "postgresql.service" "network.target" ];
+        requires = [ "postgresql.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        description = "man.sr.ht website service";
+
+        serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL man.sr.ht is being served at (protocol://domain)
+      "man.sr.ht".origin = mkDefault "http://man.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "man.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "man.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "man.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "man.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # man.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "man.sr.ht".oauth-client-id = mkDefault null;
+      "man.sr.ht".oauth-client-secret = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."man.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.mansrht}/${pkgs.sourcehut.python.sitePackages}/mansrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/meta.nix b/nixos/modules/services/misc/sourcehut/meta.nix
new file mode 100644
index 00000000000..56127a824eb
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/meta.nix
@@ -0,0 +1,211 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.meta;
+  iniKey = "meta.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.metasrht;
+in
+{
+  options.services.sourcehut.meta = {
+    user = mkOption {
+      type = types.str;
+      default = "metasrht";
+      description = ''
+        User for meta.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      description = ''
+        Port on which the "meta" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "meta.sr.ht";
+      description = ''
+        PostgreSQL database name for meta.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/metasrht";
+      description = ''
+        State path for meta.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "meta" cfg.services) {
+    assertions =
+      [
+        {
+          assertion = with cfgIni."meta.sr.ht::billing"; enabled == "yes" -> (stripe-public-key != null && stripe-secret-key != null);
+          message = "If meta.sr.ht::billing is enabled, the keys should be defined.";
+        }
+      ];
+
+    users = {
+      users = {
+        ${user} = {
+          isSystemUser = true;
+          group = user;
+          description = "meta.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.cron.systemCronJobs = [ "0 0 * * * ${cfg.python}/bin/metasrht-daily" ];
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        metasrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht website service";
+
+          preStart = ''
+            # Configure client(s) as "preauthorized"
+            ${concatMapStringsSep "\n\n"
+              (attr: ''
+                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
+                  # Configure ${attr}'s OAuth client as "preauthorized"
+                  psql ${database} \
+                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
+
+                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
+                fi
+              '')
+              (builtins.attrNames (filterAttrs
+                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
+                cfg.settings))}
+          '';
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        metasrht-api = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht api service";
+
+          preStart = ''
+            # Configure client(s) as "preauthorized"
+            ${concatMapStringsSep "\n\n"
+              (attr: ''
+                if ! test -e "${statePath}/${attr}.oauth" || [ "$(cat ${statePath}/${attr}.oauth)" != "${cfgIni."${attr}".oauth-client-id}" ]; then
+                  # Configure ${attr}'s OAuth client as "preauthorized"
+                  psql ${database} \
+                    -c "UPDATE oauthclient SET preauthorized = true WHERE client_id = '${cfgIni."${attr}".oauth-client-id}'"
+
+                  printf "%s" "${cfgIni."${attr}".oauth-client-id}" > "${statePath}/${attr}.oauth"
+                fi
+              '')
+              (builtins.attrNames (filterAttrs
+                (k: v: !(hasInfix "::" k) && builtins.hasAttr "oauth-client-id" v && v.oauth-client-id != null)
+                cfg.settings))}
+          '';
+
+          serviceConfig.ExecStart = "${pkgs.sourcehut.metasrht}/bin/metasrht-api -b :${toString (port + 100)}";
+        };
+
+        metasrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "meta.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL meta.sr.ht is being served at (protocol://domain)
+      "meta.sr.ht".origin = mkDefault "https://meta.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "meta.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "meta.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "meta.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "meta.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # If "yes", the user will be sent the stock sourcehut welcome emails after
+      # signup (requires cron to be configured properly). These are specific to the
+      # sr.ht instance so you probably want to patch these before enabling this.
+      "meta.sr.ht".welcome-emails = mkDefault "no";
+
+      # The redis connection used for the webhooks worker
+      "meta.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/6";
+
+      # If "no", public registration will not be permitted.
+      "meta.sr.ht::settings".registration = mkDefault "no";
+      # Where to redirect new users upon registration
+      "meta.sr.ht::settings".onboarding-redirect = mkDefault "https://meta.${cfg.originBase}";
+      # How many invites each user is issued upon registration (only applicable if
+      # open registration is disabled)
+      "meta.sr.ht::settings".user-invites = mkDefault 5;
+
+      # Origin URL for API, 100 more than web
+      "meta.sr.ht".api-origin = mkDefault "http://localhost:5100";
+
+      # You can add aliases for the client IDs of commonly used OAuth clients here.
+      #
+      # Example:
+      "meta.sr.ht::aliases" = mkDefault { };
+      # "meta.sr.ht::aliases"."git.sr.ht" = 12345;
+
+      # "yes" to enable the billing system
+      "meta.sr.ht::billing".enabled = mkDefault "no";
+      # Get your keys at https://dashboard.stripe.com/account/apikeys
+      "meta.sr.ht::billing".stripe-public-key = mkDefault null;
+      "meta.sr.ht::billing".stripe-secret-key = mkDefault null;
+    };
+
+    services.nginx.virtualHosts."meta.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.metasrht}/${pkgs.sourcehut.python.sitePackages}/metasrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/paste.nix b/nixos/modules/services/misc/sourcehut/paste.nix
new file mode 100644
index 00000000000..b2d5151969e
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/paste.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.paste;
+  iniKey = "paste.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.pastesrht;
+in
+{
+  options.services.sourcehut.paste = {
+    user = mkOption {
+      type = types.str;
+      default = "pastesrht";
+      description = ''
+        User for paste.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5011;
+      description = ''
+        Port on which the "paste" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "paste.sr.ht";
+      description = ''
+        PostgreSQL database name for paste.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/pastesrht";
+      description = ''
+        State path for pastesrht.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "paste" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          description = "paste.sr.ht user";
+        };
+      };
+
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        pastesrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "paste.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+        pastesrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "paste.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL paste.sr.ht is being served at (protocol://domain)
+      "paste.sr.ht".origin = mkDefault "http://paste.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "paste.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "paste.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "paste.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "paste.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # paste.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "paste.sr.ht".oauth-client-id = mkDefault null;
+      "paste.sr.ht".oauth-client-secret = mkDefault null;
+      "paste.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/5";
+    };
+
+    services.nginx.virtualHosts."paste.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.pastesrht}/${pkgs.sourcehut.python.sitePackages}/pastesrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/sourcehut/service.nix b/nixos/modules/services/misc/sourcehut/service.nix
new file mode 100644
index 00000000000..65b4ad020f9
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/service.nix
@@ -0,0 +1,66 @@
+{ config, pkgs, lib }:
+serviceCfg: serviceDrv: iniKey: attrs:
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings."${iniKey}";
+  pgSuperUser = config.services.postgresql.superUser;
+
+  setupDB = pkgs.writeScript "${serviceDrv.pname}-gen-db" ''
+    #! ${cfg.python}/bin/python
+    from ${serviceDrv.pname}.app import db
+    db.create()
+  '';
+in
+with serviceCfg; with lib; recursiveUpdate
+{
+  environment.HOME = statePath;
+  path = [ config.services.postgresql.package ] ++ (attrs.path or [ ]);
+  restartTriggers = [ config.environment.etc."sr.ht/config.ini".source ];
+  serviceConfig = {
+    Type = "simple";
+    User = user;
+    Group = user;
+    Restart = "always";
+    WorkingDirectory = statePath;
+  } // (if (cfg.statePath == "/var/lib/sourcehut/${serviceDrv.pname}") then {
+          StateDirectory = [ "sourcehut/${serviceDrv.pname}" ];
+        } else {})
+  ;
+
+  preStart = ''
+    if ! test -e ${statePath}/db; then
+      # Setup the initial database
+      ${setupDB}
+
+      # Set the initial state of the database for future database upgrades
+      if test -e ${cfg.python}/bin/${serviceDrv.pname}-migrate; then
+        # Run alembic stamp head once to tell alembic the schema is up-to-date
+        ${cfg.python}/bin/${serviceDrv.pname}-migrate stamp head
+      fi
+
+      printf "%s" "${serviceDrv.version}" > ${statePath}/db
+    fi
+
+    # Update copy of each users' profile to the latest
+    # See https://lists.sr.ht/~sircmpwn/sr.ht-admins/<20190302181207.GA13778%40cirno.my.domain>
+    if ! test -e ${statePath}/webhook; then
+      # Update ${iniKey}'s users' profile copy to the latest
+      ${cfg.python}/bin/srht-update-profiles ${iniKey}
+
+      touch ${statePath}/webhook
+    fi
+
+    ${optionalString (builtins.hasAttr "migrate-on-upgrade" cfgIni && cfgIni.migrate-on-upgrade == "yes") ''
+      if [ "$(cat ${statePath}/db)" != "${serviceDrv.version}" ]; then
+        # Manage schema migrations using alembic
+        ${cfg.python}/bin/${serviceDrv.pname}-migrate -a upgrade head
+
+        # Mark down current package version
+        printf "%s" "${serviceDrv.version}" > ${statePath}/db
+      fi
+    ''}
+
+    ${attrs.preStart or ""}
+  '';
+}
+  (builtins.removeAttrs attrs [ "path" "preStart" ])
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
new file mode 100644
index 00000000000..ab9a8c6cb4b
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/sourcehut.xml
@@ -0,0 +1,115 @@
+<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-sourcehut">
+ <title>Sourcehut</title>
+ <para>
+  <link xlink:href="https://sr.ht.com/">Sourcehut</link> is an open-source,
+  self-hostable software development platform. The server setup can be automated using
+  <link linkend="opt-services.sourcehut.enable">services.sourcehut</link>.
+ </para>
+
+ <section xml:id="module-services-sourcehut-basic-usage">
+  <title>Basic usage</title>
+  <para>
+   Sourcehut is a Python and Go based set of applications.
+   <literal><link linkend="opt-services.sourcehut.enable">services.sourcehut</link></literal>
+   by default will use
+   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
+   <literal><link linkend="opt-services.nginx.enable">services.redis</link></literal>,
+   <literal><link linkend="opt-services.nginx.enable">services.cron</link></literal>,
+   and
+   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>.
+  </para>
+
+  <para>
+   A very basic configuration may look like this:
+<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> = "srht";
+    <link linkend="opt-networking.domain">domain</link> = "tld";
+    <link linkend="opt-networking.firewall.allowedTCPPorts">firewall.allowedTCPPorts</link> = [ 22 80 443 ];
+  };
+
+  services.sourcehut = {
+    <link linkend="opt-services.sourcehut.enable">enable</link> = true;
+    <link linkend="opt-services.sourcehut.originBase">originBase</link> = fqdn;
+    <link linkend="opt-services.sourcehut.services">services</link> = [ "meta" "man" "git" ];
+    <link linkend="opt-services.sourcehut.settings">settings</link> = {
+        "sr.ht" = {
+          environment = "production";
+          global-domain = fqdn;
+          origin = "https://${fqdn}";
+          # Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
+          network-key = "SECRET";
+          service-key = "SECRET";
+        };
+        webhooks.private-key= "SECRET";
+    };
+  };
+
+  <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."${fqdn}".extraDomainNames</link> = [
+    "meta.${fqdn}"
+    "man.${fqdn}"
+    "git.${fqdn}"
+  ];
+
+  services.nginx = {
+    <link linkend="opt-services.nginx.enable">enable</link> = true;
+    # only recommendedProxySettings 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;
+
+    # Settings to setup what certificates are used for which endpoint.
+    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
+      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">"${fqdn}".enableACME</link> = true;
+      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"meta.${fqdn}".useACMEHost</link> = fqdn:
+      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"man.${fqdn}".useACMEHost</link> = fqdn:
+      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"git.${fqdn}".useACMEHost</link> = fqdn:
+    };
+  };
+}
+</programlisting>
+  </para>
+
+  <para>
+   The <literal>hostName</literal> option is used internally to configure the nginx
+   reverse-proxy. The <literal>settings</literal> attribute set is
+   used by the configuration generator and the result is placed in <literal>/etc/sr.ht/config.ini</literal>.
+  </para>
+ </section>
+
+ <section xml:id="module-services-sourcehut-configuration">
+  <title>Configuration</title>
+
+  <para>
+   All configuration parameters are also stored in
+   <literal>/etc/sr.ht/config.ini</literal> which is generated by
+   the module and linked from the store to ensure that all values from <literal>config.ini</literal>
+   can be modified by the module.
+  </para>
+
+ </section>
+
+ <section xml:id="module-services-sourcehut-httpd">
+  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
+  <para>
+   By default, <package>nginx</package> is used as reverse-proxy for <package>sourcehut</package>.
+   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
+   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
+   <literal>settings</literal>.
+  </para>
+</section>
+
+</chapter>
diff --git a/nixos/modules/services/misc/sourcehut/todo.nix b/nixos/modules/services/misc/sourcehut/todo.nix
new file mode 100644
index 00000000000..aec773b0669
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/todo.nix
@@ -0,0 +1,161 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.sourcehut;
+  cfgIni = cfg.settings;
+  scfg = cfg.todo;
+  iniKey = "todo.sr.ht";
+
+  rcfg = config.services.redis;
+  drv = pkgs.sourcehut.todosrht;
+in
+{
+  options.services.sourcehut.todo = {
+    user = mkOption {
+      type = types.str;
+      default = "todosrht";
+      description = ''
+        User for todo.sr.ht.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5003;
+      description = ''
+        Port on which the "todo" module should listen.
+      '';
+    };
+
+    database = mkOption {
+      type = types.str;
+      default = "todo.sr.ht";
+      description = ''
+        PostgreSQL database name for todo.sr.ht.
+      '';
+    };
+
+    statePath = mkOption {
+      type = types.path;
+      default = "${cfg.statePath}/todosrht";
+      description = ''
+        State path for todo.sr.ht.
+      '';
+    };
+  };
+
+  config = with scfg; lib.mkIf (cfg.enable && elem "todo" cfg.services) {
+    users = {
+      users = {
+        "${user}" = {
+          isSystemUser = true;
+          group = user;
+          extraGroups = [ "postfix" ];
+          description = "todo.sr.ht user";
+        };
+      };
+      groups = {
+        "${user}" = { };
+      };
+    };
+
+    services.postgresql = {
+      authentication = ''
+        local ${database} ${user} trust
+      '';
+      ensureDatabases = [ database ];
+      ensureUsers = [
+        {
+          name = user;
+          ensurePermissions = { "DATABASE \"${database}\"" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    systemd = {
+      tmpfiles.rules = [
+        "d ${statePath} 0750 ${user} ${user} -"
+      ];
+
+      services = {
+        todosrht = import ./service.nix { inherit config pkgs lib; } scfg drv iniKey {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "todo.sr.ht website service";
+
+          serviceConfig.ExecStart = "${cfg.python}/bin/gunicorn ${drv.pname}.app:app -b ${cfg.address}:${toString port}";
+        };
+
+       todosrht-lmtp = {
+         after = [ "postgresql.service" "network.target" ];
+         bindsTo = [ "postgresql.service" ];
+         wantedBy = [ "multi-user.target" ];
+
+         description = "todo.sr.ht process service";
+         serviceConfig = {
+           Type = "simple";
+           User = user;
+           Restart = "always";
+           ExecStart = "${cfg.python}/bin/todosrht-lmtp";
+         };
+       };
+
+        todosrht-webhooks = {
+          after = [ "postgresql.service" "network.target" ];
+          requires = [ "postgresql.service" ];
+          wantedBy = [ "multi-user.target" ];
+
+          description = "todo.sr.ht webhooks service";
+          serviceConfig = {
+            Type = "simple";
+            User = user;
+            Restart = "always";
+            ExecStart = "${cfg.python}/bin/celery -A ${drv.pname}.webhooks worker --loglevel=info";
+          };
+
+        };
+      };
+    };
+
+    services.sourcehut.settings = {
+      # URL todo.sr.ht is being served at (protocol://domain)
+      "todo.sr.ht".origin = mkDefault "http://todo.${cfg.originBase}";
+      # Address and port to bind the debug server to
+      "todo.sr.ht".debug-host = mkDefault "0.0.0.0";
+      "todo.sr.ht".debug-port = mkDefault port;
+      # Configures the SQLAlchemy connection string for the database.
+      "todo.sr.ht".connection-string = mkDefault "postgresql:///${database}?user=${user}&host=/var/run/postgresql";
+      # Set to "yes" to automatically run migrations on package upgrade.
+      "todo.sr.ht".migrate-on-upgrade = mkDefault "yes";
+      # todo.sr.ht's OAuth client ID and secret for meta.sr.ht
+      # Register your client at meta.example.org/oauth
+      "todo.sr.ht".oauth-client-id = mkDefault null;
+      "todo.sr.ht".oauth-client-secret = mkDefault null;
+      # Outgoing email for notifications generated by users
+      "todo.sr.ht".notify-from = mkDefault "CHANGEME@example.org";
+      # The redis connection used for the webhooks worker
+      "todo.sr.ht".webhooks = mkDefault "redis://${rcfg.bind}:${toString rcfg.port}/1";
+      # Network-key
+      "todo.sr.ht".network-key = mkDefault null;
+
+      # Path for the lmtp daemon's unix socket. Direct incoming mail to this socket.
+      # Alternatively, specify IP:PORT and an SMTP server will be run instead.
+      "todo.sr.ht::mail".sock = mkDefault "/tmp/todo.sr.ht-lmtp.sock";
+      # The lmtp daemon will make the unix socket group-read/write for users in this
+      # group.
+      "todo.sr.ht::mail".sock-group = mkDefault "postfix";
+
+      "todo.sr.ht::mail".posting-domain = mkDefault "todo.${cfg.originBase}";
+    };
+
+    services.nginx.virtualHosts."todo.${cfg.originBase}" = {
+      forceSSL = true;
+      locations."/".proxyPass = "http://${cfg.address}:${toString port}";
+      locations."/query".proxyPass = "http://${cfg.address}:${toString (port + 100)}";
+      locations."/static".root = "${pkgs.sourcehut.todosrht}/${pkgs.sourcehut.python.sitePackages}/todosrht";
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/ssm-agent.nix b/nixos/modules/services/misc/ssm-agent.nix
index 00e806695fd..c29d03d199b 100644
--- a/nixos/modules/services/misc/ssm-agent.nix
+++ b/nixos/modules/services/misc/ssm-agent.nix
@@ -22,15 +22,13 @@ in {
     package = mkOption {
       type = types.path;
       description = "The SSM agent package to use";
-      default = pkgs.ssm-agent;
-      defaultText = "pkgs.ssm-agent";
+      default = pkgs.ssm-agent.override { overrideEtc = false; };
+      defaultText = "pkgs.ssm-agent.override { overrideEtc = false; }";
     };
   };
 
   config = mkIf cfg.enable {
     systemd.services.ssm-agent = {
-      users.extraUsers.ssm-user = {};
-
       inherit (cfg.package.meta) description;
       after    = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
@@ -39,9 +37,37 @@ in {
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/amazon-ssm-agent";
         KillMode = "process";
-        Restart = "on-failure";
-        RestartSec = "15min";
+        # We want this restating pretty frequently. It could be our only means
+        # of accessing the instance.
+        Restart = "always";
+        RestartSec = "1min";
       };
     };
+
+    # Add user that Session Manager needs, and give it sudo.
+    # This is consistent with Amazon Linux 2 images.
+    security.sudo.extraRules = [
+      {
+        users = [ "ssm-user" ];
+        commands = [
+          {
+            command = "ALL";
+            options = [ "NOPASSWD" ];
+          }
+        ];
+      }
+    ];
+    # On Amazon Linux 2 images, the ssm-user user is pretty much a
+    # normal user with its own group. We do the same.
+    users.groups.ssm-user = {};
+    users.users.ssm-user = {
+      isNormalUser = true;
+      group = "ssm-user";
+    };
+
+    environment.etc."amazon/ssm/seelog.xml".source = "${cfg.package}/seelog.xml.template";
+
+    environment.etc."amazon/ssm/amazon-ssm-agent.json".source =  "${cfg.package}/etc/amazon/ssm/amazon-ssm-agent.json.template";
+
   };
 }
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
index 3da99a3b38c..386281e2b7c 100644
--- a/nixos/modules/services/misc/sssd.nix
+++ b/nixos/modules/services/misc/sssd.nix
@@ -69,7 +69,7 @@ in {
         mode = "0400";
       };
 
-      system.nssModules = pkgs.sssd;
+      system.nssModules = [ pkgs.sssd ];
       system.nssDatabases = {
         group = [ "sss" ];
         passwd = [ "sss" ];
@@ -92,4 +92,6 @@ in {
     services.openssh.authorizedKeysCommand = "/etc/ssh/authorized_keys_command";
     services.openssh.authorizedKeysCommandUser = "nobody";
   })];
+
+  meta.maintainers = with maintainers; [ bbigras ];
 }
diff --git a/nixos/modules/services/misc/subsonic.nix b/nixos/modules/services/misc/subsonic.nix
index 152917d345c..e17a98a5e1d 100644
--- a/nixos/modules/services/misc/subsonic.nix
+++ b/nixos/modules/services/misc/subsonic.nix
@@ -28,7 +28,7 @@ let cfg = config.services.subsonic; in {
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 4040;
         description = ''
           The port on which Subsonic will listen for
@@ -37,7 +37,7 @@ let cfg = config.services.subsonic; in {
       };
 
       httpsPort = mkOption {
-        type = types.int;
+        type = types.port;
         default = 0;
         description = ''
           The port on which Subsonic will listen for
diff --git a/nixos/modules/services/misc/svnserve.nix b/nixos/modules/services/misc/svnserve.nix
index 3335ed09d40..5fa262ca3b9 100644
--- a/nixos/modules/services/misc/svnserve.nix
+++ b/nixos/modules/services/misc/svnserve.nix
@@ -24,8 +24,9 @@ in
       };
 
       svnBaseDir = mkOption {
+        type = types.str;
         default = "/repos";
-	description = "Base directory from which Subversion repositories are accessed.";
+        description = "Base directory from which Subversion repositories are accessed.";
       };
     };
 
diff --git a/nixos/modules/services/misc/synergy.nix b/nixos/modules/services/misc/synergy.nix
index 5b7cf3ac46c..d6cd5d7f0d6 100644
--- a/nixos/modules/services/misc/synergy.nix
+++ b/nixos/modules/services/misc/synergy.nix
@@ -23,12 +23,14 @@ in
 
         screenName = mkOption {
           default = "";
+          type = types.str;
           description = ''
             Use the given name instead of the hostname to identify
             ourselves to the server.
           '';
         };
         serverAddress = mkOption {
+          type = types.str;
           description = ''
             The server address is of the form: [hostname][:port].  The
             hostname must be the address or hostname of the server.  The
@@ -46,10 +48,12 @@ in
         enable = mkEnableOption "the Synergy server (send keyboard and mouse events)";
 
         configFile = mkOption {
+          type = types.path;
           default = "/etc/synergy-server.conf";
           description = "The Synergy server configuration file.";
         };
         screenName = mkOption {
+          type = types.str;
           default = "";
           description = ''
             Use the given name instead of the hostname to identify
@@ -57,6 +61,7 @@ in
           '';
         };
         address = mkOption {
+          type = types.str;
           default = "";
           description = "Address on which to listen for clients.";
         };
@@ -65,6 +70,26 @@ in
           type = types.bool;
           description = "Whether the Synergy server should be started automatically.";
         };
+        tls = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether TLS encryption should be used.
+
+              Using this requires a TLS certificate that can be
+              generated by starting the Synergy GUI once and entering
+              a valid product key.
+            '';
+          };
+
+          cert = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "~/.synergy/SSL/Synergy.pem";
+            description = "The TLS certificate to use for encryption.";
+          };
+        };
       };
     };
 
@@ -90,7 +115,7 @@ in
         description = "Synergy server";
         wantedBy = optional cfgS.autoStart "graphical-session.target";
         path = [ pkgs.synergy ];
-        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f ${optionalString (cfgS.address != "") "-a ${cfgS.address}"} ${optionalString (cfgS.screenName != "") "-n ${cfgS.screenName}" }'';
+        serviceConfig.ExecStart = ''${pkgs.synergy}/bin/synergys -c ${cfgS.configFile} -f${optionalString (cfgS.address != "") " -a ${cfgS.address}"}${optionalString (cfgS.screenName != "") " -n ${cfgS.screenName}"}${optionalString cfgS.tls.enable " --enable-crypto"}${optionalString (cfgS.tls.cert != null) (" --tls-cert=${cfgS.tls.cert}")}'';
         serviceConfig.Restart = "on-failure";
       };
     })
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
index c6ff540ea12..b71250f62e0 100644
--- a/nixos/modules/services/misc/weechat.nix
+++ b/nixos/modules/services/misc/weechat.nix
@@ -20,6 +20,7 @@ in
       type = types.str;
     };
     binary = mkOption {
+      type = types.path;
       description = "Binary to execute (by default \${weechat}/bin/weechat).";
       example = literalExample ''
         ''${pkgs.weechat}/bin/weechat-headless
diff --git a/nixos/modules/services/misc/zigbee2mqtt.nix b/nixos/modules/services/misc/zigbee2mqtt.nix
index 0957920f1a0..4458da1346b 100644
--- a/nixos/modules/services/misc/zigbee2mqtt.nix
+++ b/nixos/modules/services/misc/zigbee2mqtt.nix
@@ -5,29 +5,17 @@ with lib;
 let
   cfg = config.services.zigbee2mqtt;
 
-  configJSON = pkgs.writeText "configuration.json"
-    (builtins.toJSON (recursiveUpdate defaultConfig cfg.config));
-  configFile = pkgs.runCommand "configuration.yaml" { preferLocalBuild = true; } ''
-    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
-  '';
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "zigbee2mqtt.yaml" cfg.settings;
 
-  # the default config contains all required settings,
-  # so the service starts up without crashing.
-  defaultConfig = {
-    homeassistant = false;
-    permit_join = false;
-    mqtt = {
-      base_topic = "zigbee2mqtt";
-      server = "mqtt://localhost:1883";
-    };
-    serial.port = "/dev/ttyACM0";
-    # put device configuration into separate file because configuration.yaml
-    # is copied from the store on startup
-    devices = "devices.yaml";
-  };
 in
 {
-  meta.maintainers = with maintainers; [ sweber ];
+  meta.maintainers = with maintainers; [ sweber hexa ];
+
+  imports = [
+    # Remove warning before the 21.11 release
+    (mkRenamedOptionModule [ "services" "zigbee2mqtt" "config" ] [ "services" "zigbee2mqtt" "settings" ])
+  ];
 
   options.services.zigbee2mqtt = {
     enable = mkEnableOption "enable zigbee2mqtt service";
@@ -37,7 +25,11 @@ in
       default = pkgs.zigbee2mqtt.override {
         dataDir = cfg.dataDir;
       };
-      defaultText = "pkgs.zigbee2mqtt";
+      defaultText = literalExample ''
+        pkgs.zigbee2mqtt {
+          dataDir = services.zigbee2mqtt.dataDir
+        }
+      '';
       type = types.package;
     };
 
@@ -47,9 +39,9 @@ in
       type = types.path;
     };
 
-    config = mkOption {
+    settings = mkOption {
+      type = format.type;
       default = {};
-      type = with types; nullOr attrs;
       example = literalExample ''
         {
           homeassistant = config.services.home-assistant.enable;
@@ -61,24 +53,80 @@ in
       '';
       description = ''
         Your <filename>configuration.yaml</filename> as a Nix attribute set.
+        Check the <link xlink:href="https://www.zigbee2mqtt.io/information/configuration.html">documentation</link>
+        for possible options.
       '';
     };
   };
 
   config = mkIf (cfg.enable) {
+
+    # preset config values
+    services.zigbee2mqtt.settings = {
+      homeassistant = mkDefault config.services.home-assistant.enable;
+      permit_join = mkDefault false;
+      mqtt = {
+        base_topic = mkDefault "zigbee2mqtt";
+        server = mkDefault "mqtt://localhost:1883";
+      };
+      serial.port = mkDefault "/dev/ttyACM0";
+      # reference device configuration, that is kept in a separate file
+      # to prevent it being overwritten in the units ExecStartPre script
+      devices = mkDefault "devices.yaml";
+    };
+
     systemd.services.zigbee2mqtt = {
       description = "Zigbee2mqtt Service";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
+      environment.ZIGBEE2MQTT_DATA = cfg.dataDir;
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/zigbee2mqtt";
         User = "zigbee2mqtt";
         WorkingDirectory = cfg.dataDir;
         Restart = "on-failure";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DeviceAllow = [
+          config.services.zigbee2mqtt.settings.serial.port
+        ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = false;
+        NoNewPrivileges = true;
+        PrivateDevices = false; # prevents access to /dev/serial, because it is set 0700 root:root
+        PrivateUsers = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
         ProtectSystem = "strict";
         ReadWritePaths = cfg.dataDir;
-        PrivateTmp = true;
         RemoveIPC = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SupplementaryGroups = [
+          "dialout"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
       };
       preStart = ''
         cp --no-preserve=mode ${configFile} "${cfg.dataDir}/configuration.yaml"
@@ -89,7 +137,6 @@ in
       home = cfg.dataDir;
       createHome = true;
       group = "zigbee2mqtt";
-      extraGroups = [ "dialout" ];
       uid = config.ids.uids.zigbee2mqtt;
     };
 
diff --git a/nixos/modules/services/misc/zookeeper.nix b/nixos/modules/services/misc/zookeeper.nix
index f6af7c75eba..1d12e81a9ec 100644
--- a/nixos/modules/services/misc/zookeeper.nix
+++ b/nixos/modules/services/misc/zookeeper.nix
@@ -76,6 +76,7 @@ in {
       default = ''
         zookeeper.root.logger=INFO, CONSOLE
         log4j.rootLogger=INFO, CONSOLE
+        log4j.logger.org.apache.zookeeper.audit.Log4jAuditLogger=INFO, CONSOLE
         log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
         log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
         log4j.appender.CONSOLE.layout.ConversionPattern=[myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
@@ -128,11 +129,10 @@ in {
       description = "Zookeeper Daemon";
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      environment = { ZOOCFGDIR = configDir; };
       serviceConfig = {
         ExecStart = ''
           ${pkgs.jre}/bin/java \
-            -cp "${cfg.package}/lib/*:${cfg.package}/${cfg.package.name}.jar:${configDir}" \
+            -cp "${cfg.package}/lib/*:${configDir}" \
             ${escapeShellArgs cfg.extraCmdLineOptions} \
             -Dzookeeper.datadir.autocreate=false \
             ${optionalString cfg.preferIPv4 "-Djava.net.preferIPv4Stack=true"} \
@@ -143,6 +143,7 @@ in {
       };
       preStart = ''
         echo "${toString cfg.id}" > ${cfg.dataDir}/myid
+        mkdir -p ${cfg.dataDir}/version-2
       '';
     };
 
diff --git a/nixos/modules/services/monitoring/alerta.nix b/nixos/modules/services/monitoring/alerta.nix
index 34f2d41706a..7c6eff713cb 100644
--- a/nixos/modules/services/monitoring/alerta.nix
+++ b/nixos/modules/services/monitoring/alerta.nix
@@ -95,13 +95,13 @@ in
         ALERTA_SVR_CONF_FILE = alertaConf;
       };
       serviceConfig = {
-        ExecStart = "${pkgs.python36Packages.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}";
+        ExecStart = "${pkgs.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}";
         User = "alerta";
         Group = "alerta";
       };
     };
 
-    environment.systemPackages = [ pkgs.python36Packages.alerta ];
+    environment.systemPackages = [ pkgs.alerta ];
 
     users.users.alerta = {
       uid = config.ids.uids.alerta;
diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix
index 75218aa1d46..1dccbc93edf 100644
--- a/nixos/modules/services/monitoring/apcupsd.nix
+++ b/nixos/modules/services/monitoring/apcupsd.nix
@@ -104,7 +104,7 @@ in
       hooks = mkOption {
         default = {};
         example = {
-          doshutdown = ''# shell commands to notify that the computer is shutting down'';
+          doshutdown = "# shell commands to notify that the computer is shutting down";
         };
         type = types.attrsOf types.lines;
         description = ''
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index f1cb890794e..b25a53435d0 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -6,7 +6,6 @@ let
   cfg = config.services.datadog-agent;
 
   ddConf = {
-    dd_url              = "https://app.datadoghq.com";
     skip_ssl_validation = false;
     confd_path          = "/etc/datadog-agent/conf.d";
     additional_checksd  = "/etc/datadog-agent/checks.d";
@@ -14,6 +13,8 @@ let
   }
   // optionalAttrs (cfg.logLevel != null) { log_level = cfg.logLevel; }
   // optionalAttrs (cfg.hostname != null) { inherit (cfg) hostname; }
+  // optionalAttrs (cfg.ddUrl != null) { dd_url = cfg.ddUrl; }
+  // optionalAttrs (cfg.site != null) { site = cfg.site; }
   // optionalAttrs (cfg.tags != null ) { tags = concatStringsSep ", " cfg.tags; }
   // optionalAttrs (cfg.enableLiveProcessCollection) { process_config = { enabled = "true"; }; }
   // optionalAttrs (cfg.enableTraceAgent) { apm_config = { enabled = true; }; }
@@ -77,6 +78,27 @@ in {
       type = types.path;
     };
 
+    ddUrl = mkOption {
+      description = ''
+        Custom dd_url to configure the agent with. Useful if traffic to datadog
+        needs to go through a proxy.
+        Don't use this to point to another datadog site (EU) - use site instead.
+      '';
+      default = null;
+      example = "http://haproxy.example.com:3834";
+      type = types.nullOr types.str;
+    };
+
+    site = mkOption {
+      description = ''
+        The datadog site to point the agent towards.
+        Set to datadoghq.eu to point it to their EU site.
+      '';
+      default = null;
+      example = "datadoghq.eu";
+      type = types.nullOr types.str;
+    };
+
     tags = mkOption {
       description = "The tags to mark this Datadog agent";
       example = [ "test" "service" ];
@@ -203,7 +225,7 @@ in {
     };
   };
   config = mkIf cfg.enable {
-    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute ];
+    environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ];
 
     users.users.datadog = {
       description = "Datadog Agent User";
@@ -217,7 +239,7 @@ in {
 
     systemd.services = let
       makeService = attrs: recursiveUpdate {
-        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute ];
+        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute2 ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           User = "datadog";
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
new file mode 100644
index 00000000000..b8b95d846c6
--- /dev/null
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -0,0 +1,150 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.grafana-image-renderer;
+
+  format = pkgs.formats.json { };
+
+  configFile = format.generate "grafana-image-renderer-config.json" cfg.settings;
+in {
+  options.services.grafana-image-renderer = {
+    enable = mkEnableOption "grafana-image-renderer";
+
+    chromium = mkOption {
+      type = types.package;
+      description = ''
+        The chromium to use for image rendering.
+      '';
+    };
+
+    verbose = mkEnableOption "verbosity for the service";
+
+    provisionGrafana = mkEnableOption "Grafana configuration for grafana-image-renderer";
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = format.type;
+
+        options = {
+          service = {
+            port = mkOption {
+              type = types.port;
+              default = 8081;
+              description = ''
+                The TCP port to use for the rendering server.
+              '';
+            };
+            logging.level = mkOption {
+              type = types.enum [ "error" "warning" "info" "debug" ];
+              default = "info";
+              description = ''
+                The log-level of the <filename>grafana-image-renderer.service</filename>-unit.
+              '';
+            };
+          };
+          rendering = {
+            width = mkOption {
+              default = 1000;
+              type = types.ints.positive;
+              description = ''
+                Width of the PNG used to display the alerting graph.
+              '';
+            };
+            height = mkOption {
+              default = 500;
+              type = types.ints.positive;
+              description = ''
+                Height of the PNG used to display the alerting graph.
+              '';
+            };
+            mode = mkOption {
+              default = "default";
+              type = types.enum [ "default" "reusable" "clustered" ];
+              description = ''
+                Rendering mode of <package>grafana-image-renderer</package>:
+                <itemizedlist>
+                <listitem><para><literal>default:</literal> Creates on browser-instance
+                  per rendering request.</para></listitem>
+                <listitem><para><literal>reusable:</literal> One browser instance
+                  will be started and reused for each rendering request.</para></listitem>
+                <listitem><para><literal>clustered:</literal> allows to precisely
+                  configure how many browser-instances are supposed to be used. The values
+                  for that mode can be declared in <literal>rendering.clustering</literal>.
+                  </para></listitem>
+                </itemizedlist>
+              '';
+            };
+            args = mkOption {
+              type = types.listOf types.str;
+              default = [ "--no-sandbox" ];
+              description = ''
+                List of CLI flags passed to <package>chromium</package>.
+              '';
+            };
+          };
+        };
+      };
+
+      default = {};
+
+      description = ''
+        Configuration attributes for <package>grafana-image-renderer</package>.
+
+        See <link xlink:href="https://github.com/grafana/grafana-image-renderer/blob/ce1f81438e5f69c7fd7c73ce08bab624c4c92e25/default.json" />
+        for supported values.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.provisionGrafana -> config.services.grafana.enable;
+        message = ''
+          To provision a Grafana instance to use grafana-image-renderer,
+          `services.grafana.enable` must be set to `true`!
+        '';
+      }
+    ];
+
+    services.grafana.extraOptions = mkIf cfg.provisionGrafana {
+      RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render";
+      RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}";
+    };
+
+    services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
+
+    services.grafana-image-renderer.settings = {
+      rendering = mapAttrs (const mkDefault) {
+        chromeBin = "${cfg.chromium}/bin/chromium";
+        verboseLogging = cfg.verbose;
+        timezone = config.time.timeZone;
+      };
+
+      service = {
+        logging.level = mkIf cfg.verbose (mkDefault "debug");
+        metrics.enabled = mkDefault false;
+      };
+    };
+
+    systemd.services.grafana-image-renderer = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = " A Grafana backend plugin that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)";
+
+      environment = {
+        PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = "true";
+      };
+
+      serviceConfig = {
+        DynamicUser = true;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.grafana-image-renderer}/bin/grafana-image-renderer server --config=${configFile}";
+        Restart = "always";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+}
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index b0c81a46d4d..e0b2624b6ca 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -5,15 +5,17 @@ with lib;
 let
   cfg = config.services.grafana;
   opt = options.services.grafana;
+  declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
 
   envOptions = {
     PATHS_DATA = cfg.dataDir;
-    PATHS_PLUGINS = "${cfg.dataDir}/plugins";
+    PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
     PATHS_LOGS = "${cfg.dataDir}/log";
 
     SERVER_PROTOCOL = cfg.protocol;
     SERVER_HTTP_ADDR = cfg.addr;
     SERVER_HTTP_PORT = cfg.port;
+    SERVER_SOCKET = cfg.socket;
     SERVER_DOMAIN = cfg.domain;
     SERVER_ROOT_URL = cfg.rootUrl;
     SERVER_STATIC_ROOT_PATH = cfg.staticRootPath;
@@ -40,6 +42,9 @@ let
     AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable;
     AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name;
     AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role;
+    AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable;
+    AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp;
+    AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId;
 
     ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable;
 
@@ -64,10 +69,18 @@ let
 
   dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
 
+  notifierConfiguration = {
+    apiVersion = 1;
+    notifiers = cfg.provision.notifiers;
+  };
+
+  notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
+
   provisionConfDir =  pkgs.runCommand "grafana-provisioning" { } ''
-    mkdir -p $out/{datasources,dashboards}
+    mkdir -p $out/{datasources,dashboards,notifiers}
     ln -sf ${datasourceFile} $out/datasources/datasource.yaml
     ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
+    ln -sf ${notifierFile} $out/notifiers/notifier.yaml
   '';
 
   # Get a submodule without any embedded metadata:
@@ -78,80 +91,80 @@ let
     options = {
       name = mkOption {
         type = types.str;
-        description = "Name of the datasource. Required";
+        description = "Name of the datasource. Required.";
       };
       type = mkOption {
-        type = types.enum ["graphite" "prometheus" "cloudwatch" "elasticsearch" "influxdb" "opentsdb" "mysql" "mssql" "postgres" "loki"];
-        description = "Datasource type. Required";
+        type = types.str;
+        description = "Datasource type. Required.";
       };
       access = mkOption {
         type = types.enum ["proxy" "direct"];
         default = "proxy";
-        description = "Access mode. proxy or direct (Server or Browser in the UI). Required";
+        description = "Access mode. proxy or direct (Server or Browser in the UI). Required.";
       };
       orgId = mkOption {
         type = types.int;
         default = 1;
-        description = "Org id. will default to orgId 1 if not specified";
+        description = "Org id. will default to orgId 1 if not specified.";
       };
       url = mkOption {
         type = types.str;
-        description = "Url of the datasource";
+        description = "Url of the datasource.";
       };
       password = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Database password, if used";
+        description = "Database password, if used.";
       };
       user = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Database user, if used";
+        description = "Database user, if used.";
       };
       database = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Database name, if used";
+        description = "Database name, if used.";
       };
       basicAuth = mkOption {
         type = types.nullOr types.bool;
         default = null;
-        description = "Enable/disable basic auth";
+        description = "Enable/disable basic auth.";
       };
       basicAuthUser = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Basic auth username";
+        description = "Basic auth username.";
       };
       basicAuthPassword = mkOption {
         type = types.nullOr types.str;
         default = null;
-        description = "Basic auth password";
+        description = "Basic auth password.";
       };
       withCredentials = mkOption {
         type = types.bool;
         default = false;
-        description = "Enable/disable with credentials headers";
+        description = "Enable/disable with credentials headers.";
       };
       isDefault = mkOption {
         type = types.bool;
         default = false;
-        description = "Mark as default datasource. Max one per org";
+        description = "Mark as default datasource. Max one per org.";
       };
       jsonData = mkOption {
         type = types.nullOr types.attrs;
         default = null;
-        description = "Datasource specific configuration";
+        description = "Datasource specific configuration.";
       };
       secureJsonData = mkOption {
         type = types.nullOr types.attrs;
         default = null;
-        description = "Datasource specific secure configuration";
+        description = "Datasource specific secure configuration.";
       };
       version = mkOption {
         type = types.int;
         default = 1;
-        description = "Version";
+        description = "Version.";
       };
       editable = mkOption {
         type = types.bool;
@@ -167,41 +180,99 @@ let
       name = mkOption {
         type = types.str;
         default = "default";
-        description = "Provider name";
+        description = "Provider name.";
       };
       orgId = mkOption {
         type = types.int;
         default = 1;
-        description = "Organization ID";
+        description = "Organization ID.";
       };
       folder = mkOption {
         type = types.str;
         default = "";
-        description = "Add dashboards to the specified folder";
+        description = "Add dashboards to the specified folder.";
       };
       type = mkOption {
         type = types.str;
         default = "file";
-        description = "Dashboard provider type";
+        description = "Dashboard provider type.";
       };
       disableDeletion = mkOption {
         type = types.bool;
         default = false;
-        description = "Disable deletion when JSON file is removed";
+        description = "Disable deletion when JSON file is removed.";
       };
       updateIntervalSeconds = mkOption {
         type = types.int;
         default = 10;
-        description = "How often Grafana will scan for changed dashboards";
+        description = "How often Grafana will scan for changed dashboards.";
       };
       options = {
         path = mkOption {
           type = types.path;
-          description = "Path grafana will watch for dashboards";
+          description = "Path grafana will watch for dashboards.";
         };
       };
     };
   };
+
+  grafanaTypes.notifierConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = "default";
+        description = "Notifier name.";
+      };
+      type = mkOption {
+        type = types.enum ["dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook"];
+        description = "Notifier type.";
+      };
+      uid = mkOption {
+        type = types.str;
+        description = "Unique notifier identifier.";
+      };
+      org_id = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Organization ID.";
+      };
+      org_name = mkOption {
+        type = types.str;
+        default = "Main Org.";
+        description = "Organization name.";
+      };
+      is_default = mkOption {
+        type = types.bool;
+        description = "Is the default notifier.";
+        default = false;
+      };
+      send_reminder = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Should the notifier be sent reminder notifications while alerts continue to fire.";
+      };
+      frequency = mkOption {
+        type = types.str;
+        default = "5m";
+        description = "How frequently should the notifier be sent reminders.";
+      };
+      disable_resolve_message = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Turn off the message that sends when an alert returns to OK.";
+      };
+      settings = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Settings for the notifier type.";
+      };
+      secure_settings = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Secure settings for the notifier type.";
+      };
+    };
+  };
 in {
   options.services.grafana = {
     enable = mkEnableOption "grafana";
@@ -221,7 +292,13 @@ in {
     port = mkOption {
       description = "Listening port.";
       default = 3000;
-      type = types.int;
+      type = types.port;
+    };
+
+    socket = mkOption {
+      description = "Listening socket.";
+      default = "/run/grafana/grafana.sock";
+      type = types.str;
     };
 
     domain = mkOption {
@@ -261,6 +338,17 @@ in {
       type = types.package;
     };
 
+    declarativePlugins = mkOption {
+      type = with types; nullOr (listOf path);
+      default = null;
+      description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed.";
+      example = literalExample "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]";
+      # Make sure each plugin is added only once; otherwise building
+      # the link farm fails, since the same path is added multiple
+      # times.
+      apply = x: if isList x then lib.unique x else x;
+    };
+
     dataDir = mkOption {
       description = "Data directory.";
       default = "/var/lib/grafana";
@@ -330,17 +418,23 @@ in {
     provision = {
       enable = mkEnableOption "provision";
       datasources = mkOption {
-        description = "Grafana datasources configuration";
+        description = "Grafana datasources configuration.";
         default = [];
         type = types.listOf grafanaTypes.datasourceConfig;
         apply = x: map _filter x;
       };
       dashboards = mkOption {
-        description = "Grafana dashboard configuration";
+        description = "Grafana dashboard configuration.";
         default = [];
         type = types.listOf grafanaTypes.dashboardConfig;
         apply = x: map _filter x;
       };
+      notifiers = mkOption {
+        description = "Grafana notifier configuration.";
+        default = [];
+        type = types.listOf grafanaTypes.notifierConfig;
+        apply = x: map _filter x;
+      };
     };
 
     security = {
@@ -384,12 +478,12 @@ in {
     smtp = {
       enable = mkEnableOption "smtp";
       host = mkOption {
-        description = "Host to connect to";
+        description = "Host to connect to.";
         default = "localhost:25";
         type = types.str;
       };
       user = mkOption {
-        description = "User used for authentication";
+        description = "User used for authentication.";
         default = "";
         type = types.str;
       };
@@ -410,7 +504,7 @@ in {
         type = types.nullOr types.path;
       };
       fromAddress = mkOption {
-        description = "Email address used for sending";
+        description = "Email address used for sending.";
         default = "admin@grafana.localhost";
         type = types.str;
       };
@@ -418,7 +512,7 @@ in {
 
     users = {
       allowSignUp = mkOption {
-        description = "Disable user signup / registration";
+        description = "Disable user signup / registration.";
         default = false;
         type = types.bool;
       };
@@ -442,28 +536,51 @@ in {
       };
     };
 
-    auth.anonymous = {
-      enable = mkOption {
-        description = "Whether to allow anonymous access";
-        default = false;
-        type = types.bool;
-      };
-      org_name = mkOption {
-        description = "Which organization to allow anonymous access to";
-        default = "Main Org.";
-        type = types.str;
+    auth = {
+      anonymous = {
+        enable = mkOption {
+          description = "Whether to allow anonymous access.";
+          default = false;
+          type = types.bool;
+        };
+        org_name = mkOption {
+          description = "Which organization to allow anonymous access to.";
+          default = "Main Org.";
+          type = types.str;
+        };
+        org_role = mkOption {
+          description = "Which role anonymous users have in the organization.";
+          default = "Viewer";
+          type = types.str;
+        };
       };
-      org_role = mkOption {
-        description = "Which role anonymous users have in the organization";
-        default = "Viewer";
-        type = types.str;
+      google = {
+        enable = mkOption {
+          description = "Whether to allow Google OAuth2.";
+          default = false;
+          type = types.bool;
+        };
+        allowSignUp = mkOption {
+          description = "Whether to allow sign up with Google OAuth2.";
+          default = false;
+          type = types.bool;
+        };
+        clientId = mkOption {
+          description = "Google OAuth2 client ID.";
+          default = "";
+          type = types.str;
+        };
+        clientSecretFile = mkOption {
+          description = "Google OAuth2 client secret.";
+          default = null;
+          type = types.nullOr types.path;
+        };
       };
-
     };
 
     analytics.reporting = {
       enable = mkOption {
-        description = "Whether to allow anonymous usage reporting to stats.grafana.net";
+        description = "Whether to allow anonymous usage reporting to stats.grafana.net.";
         default = true;
         type = types.bool;
       };
@@ -489,6 +606,9 @@ in {
       (optional (
         any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources
       ) "Datasource passwords will be stored as plaintext in the Nix store!")
+      (optional (
+        any (x: x.secure_settings != null) cfg.provision.notifiers
+      ) "Notifier secure settings will be stored as plaintext in the Nix store!")
     ];
 
     environment.systemPackages = [ cfg.package ];
@@ -520,17 +640,28 @@ in {
         QT_QPA_PLATFORM = "offscreen";
       } // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions;
       script = ''
+        set -o errexit -o pipefail -o nounset -o errtrace
+        shopt -s inherit_errexit
+
+        ${optionalString (cfg.auth.google.clientSecretFile != null) ''
+          GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})"
+          export GF_AUTH_GOOGLE_CLIENT_SECRET
+        ''}
         ${optionalString (cfg.database.passwordFile != null) ''
-          export GF_DATABASE_PASSWORD="$(cat ${escapeShellArg cfg.database.passwordFile})"
+          GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})"
+          export GF_DATABASE_PASSWORD
         ''}
         ${optionalString (cfg.security.adminPasswordFile != null) ''
-          export GF_SECURITY_ADMIN_PASSWORD="$(cat ${escapeShellArg cfg.security.adminPasswordFile})"
+          GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})"
+          export GF_SECURITY_ADMIN_PASSWORD
         ''}
         ${optionalString (cfg.security.secretKeyFile != null) ''
-          export GF_SECURITY_SECRET_KEY="$(cat ${escapeShellArg cfg.security.secretKeyFile})"
+          GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})"
+          export GF_SECURITY_SECRET_KEY
         ''}
         ${optionalString (cfg.smtp.passwordFile != null) ''
-          export GF_SMTP_PASSWORD="$(cat ${escapeShellArg cfg.smtp.passwordFile})"
+          GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})"
+          export GF_SMTP_PASSWORD
         ''}
         ${optionalString cfg.provision.enable ''
           export GF_PATHS_PROVISIONING=${provisionConfDir};
@@ -540,6 +671,8 @@ in {
       serviceConfig = {
         WorkingDirectory = cfg.dataDir;
         User = "grafana";
+        RuntimeDirectory = "grafana";
+        RuntimeDirectoryMode = "0755";
       };
       preStart = ''
         ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir}
diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix
index 64d9d61950d..9213748d3c9 100644
--- a/nixos/modules/services/monitoring/graphite.nix
+++ b/nixos/modules/services/monitoring/graphite.nix
@@ -25,10 +25,10 @@ let
 
   graphiteApiConfig = pkgs.writeText "graphite-api.yaml" ''
     search_index: ${dataDir}/index
-    ${optionalString (config.time.timeZone != null) ''time_zone: ${config.time.timeZone}''}
-    ${optionalString (cfg.api.finders != []) ''finders:''}
+    ${optionalString (config.time.timeZone != null) "time_zone: ${config.time.timeZone}"}
+    ${optionalString (cfg.api.finders != []) "finders:"}
     ${concatMapStringsSep "\n" (f: "  - " + f.moduleName) cfg.api.finders}
-    ${optionalString (cfg.api.functions != []) ''functions:''}
+    ${optionalString (cfg.api.functions != []) "functions:"}
     ${concatMapStringsSep "\n" (f: "  - " + f) cfg.api.functions}
     ${cfg.api.extraConfig}
   '';
diff --git a/nixos/modules/services/monitoring/incron.nix b/nixos/modules/services/monitoring/incron.nix
index 1789fd9f205..dc97af58562 100644
--- a/nixos/modules/services/monitoring/incron.nix
+++ b/nixos/modules/services/monitoring/incron.nix
@@ -67,7 +67,7 @@ in
   config = mkIf cfg.enable {
 
     warnings = optional (cfg.allow != null && cfg.deny != null)
-      ''If `services.incron.allow` is set then `services.incron.deny` will be ignored.'';
+      "If `services.incron.allow` is set then `services.incron.deny` will be ignored.";
 
     environment.systemPackages = [ pkgs.incron ];
 
diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix
index f4eec7e0d28..51cabaa274a 100644
--- a/nixos/modules/services/monitoring/loki.nix
+++ b/nixos/modules/services/monitoring/loki.nix
@@ -39,7 +39,7 @@ in {
     };
 
     configuration = mkOption {
-      type = types.attrs;
+      type = (pkgs.formats.json {}).type;
       default = {};
       description = ''
         Specify the configuration for Loki in Nix.
@@ -78,6 +78,8 @@ in {
       '';
     }];
 
+    environment.systemPackages = [ pkgs.grafana-loki ]; # logcli
+
     users.groups.${cfg.group} = { };
     users.users.${cfg.user} = {
       description = "Loki Service User";
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
new file mode 100644
index 00000000000..7046de9d403
--- /dev/null
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -0,0 +1,111 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mackerel-agent;
+  settingsFmt = pkgs.formats.toml {};
+in {
+  options.services.mackerel-agent = {
+    enable = mkEnableOption "mackerel.io agent";
+
+    # the upstream package runs as root, but doesn't seem to be strictly
+    # necessary for basic functionality
+    runAsRoot = mkEnableOption "Whether to run as root.";
+
+    autoRetirement = mkEnableOption ''
+      Whether to automatically retire the host upon OS shutdown.
+    '';
+
+    apiKeyFile = mkOption {
+      type = types.path;
+      default = "";
+      example = "/run/keys/mackerel-api-key";
+      description = ''
+        Path to file containing the Mackerel API key. The file should contain a
+        single line of the following form:
+
+        <literallayout>apikey = "EXAMPLE_API_KEY"</literallayout>
+      '';
+    };
+
+    settings = mkOption {
+      description = ''
+        Options for mackerel-agent.conf.
+
+        Documentation:
+        <link xlink:href="https://mackerel.io/docs/entry/spec/agent"/>
+      '';
+
+      default = {};
+      example = {
+        verbose = false;
+        silent = false;
+      };
+
+      type = types.submodule {
+        freeformType = settingsFmt.type;
+
+        options.host_status = {
+          on_start = mkOption {
+            type = types.enum [ "working" "standby" "maintenance" "poweroff" ];
+            description = "Host status after agent startup.";
+            default = "working";
+          };
+          on_stop = mkOption {
+            type = types.enum [ "working" "standby" "maintenance" "poweroff" ];
+            description = "Host status after agent shutdown.";
+            default = "poweroff";
+          };
+        };
+
+        options.diagnostic =
+          mkEnableOption "Collect memory usage for the agent itself";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ mackerel-agent ];
+
+    environment.etc = {
+      "mackerel-agent/mackerel-agent.conf".source =
+        settingsFmt.generate "mackerel-agent.conf" cfg.settings;
+      "mackerel-agent/conf.d/api-key.conf".source = cfg.apiKeyFile;
+    };
+
+    services.mackerel-agent.settings = {
+      root = mkDefault "/var/lib/mackerel-agent";
+      pidfile = mkDefault "/run/mackerel-agent/mackerel-agent.pid";
+
+      # conf.d stores the symlink to cfg.apiKeyFile
+      include = mkDefault "/etc/mackerel-agent/conf.d/*.conf";
+    };
+
+    # upstream service file in https://git.io/JUt4Q
+    systemd.services.mackerel-agent = {
+      description = "mackerel.io agent";
+      after = [ "network-online.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        MACKEREL_PLUGIN_WORKDIR = mkDefault "%C/mackerel-agent";
+      };
+      serviceConfig = {
+        DynamicUser = !cfg.runAsRoot;
+        PrivateTmp = mkDefault true;
+        CacheDirectory = "mackerel-agent";
+        ConfigurationDirectory = "mackerel-agent";
+        RuntimeDirectory = "mackerel-agent";
+        StateDirectory = "mackerel-agent";
+        ExecStart = "${pkgs.mackerel-agent}/bin/mackerel-agent supervise";
+        ExecStopPost = mkIf cfg.autoRetirement "${pkg.mackerel-agent}/bin/mackerel-agent retire -force";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        LimitNOFILE = mkDefault 65536;
+        LimitNPROC = mkDefault 65536;
+      };
+      restartTriggers = [
+        config.environment.etc."mackerel-agent/mackerel-agent.conf".source
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/metricbeat.nix b/nixos/modules/services/monitoring/metricbeat.nix
new file mode 100644
index 00000000000..b285559eaa9
--- /dev/null
+++ b/nixos/modules/services/monitoring/metricbeat.nix
@@ -0,0 +1,152 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    attrValues
+    literalExample
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+    ;
+  cfg = config.services.metricbeat;
+
+  settingsFormat = pkgs.formats.yaml {};
+
+in
+{
+  options = {
+
+    services.metricbeat = {
+
+      enable = mkEnableOption "metricbeat";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.metricbeat;
+        defaultText = literalExample "pkgs.metricbeat";
+        example = literalExample "pkgs.metricbeat7";
+        description = ''
+          The metricbeat package to use
+        '';
+      };
+
+      modules = mkOption {
+        description = ''
+          Metricbeat modules are responsible for reading metrics from the various sources.
+
+          This is like <literal>services.metricbeat.settings.metricbeat.modules</literal>,
+          but structured as an attribute set. This has the benefit that multiple
+          NixOS modules can contribute settings to a single metricbeat module.
+
+          A module can be specified multiple times by choosing a different <literal>&lt;name></literal>
+          for each, but setting <xref linkend="opt-services.metricbeat.modules._name_.module"/> to the same value.
+
+          See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = settingsFormat.type;
+          options = {
+            module = mkOption {
+              type = types.str;
+              default = name;
+              defaultText = literalExample ''<name>'';
+              description = ''
+                The name of the module.
+
+                Look for the value after <literal>module:</literal> on the individual
+                module pages linked from <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>.
+              '';
+            };
+          };
+        }));
+        example = {
+          system = {
+            metricsets = ["cpu" "load" "memory" "network" "process" "process_summary" "uptime" "socket_summary"];
+            enabled = true;
+            period = "10s";
+            processes = [".*"];
+            cpu.metrics = ["percentages" "normalized_percentages"];
+            core.metrics = ["percentages"];
+          };
+        };
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+          options = {
+
+            name = mkOption {
+              type = types.str;
+              default = "";
+              description = ''
+                Name of the beat. Defaults to the hostname.
+                See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_name"/>.
+              '';
+            };
+
+            tags = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                Tags to place on the shipped metrics.
+                See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_tags_2"/>.
+              '';
+            };
+
+            metricbeat.modules = mkOption {
+              type = types.listOf settingsFormat.type;
+              default = [];
+              internal = true;
+              description = ''
+                The metric collecting modules. Use <xref linkend="opt-services.metricbeat.modules"/> instead.
+
+                See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html"/>.
+              '';
+            };
+          };
+        };
+        default = {};
+        description = ''
+          Configuration for metricbeat. See <link xlink:href="https://www.elastic.co/guide/en/beats/metricbeat/current/configuring-howto-metricbeat.html"/> for supported values.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        # empty modules would cause a failure at runtime
+        assertion = cfg.settings.metricbeat.modules != [];
+        message = "services.metricbeat: You must configure one or more modules.";
+      }
+    ];
+
+    services.metricbeat.settings.metricbeat.modules = attrValues cfg.modules;
+
+    systemd.services.metricbeat = {
+      description = "metricbeat metrics shipper";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/metricbeat \
+            -c ${settingsFormat.generate "metricbeat.yml" cfg.settings} \
+            --path.data $STATE_DIRECTORY \
+            --path.logs $LOGS_DIRECTORY \
+            ;
+        '';
+        Restart = "always";
+        DynamicUser = true;
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+        StateDirectory = "metricbeat";
+        LogsDirectory = "metricbeat";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/monit.nix b/nixos/modules/services/monitoring/monit.nix
index aa51b83912c..379ee967620 100644
--- a/nixos/modules/services/monitoring/monit.nix
+++ b/nixos/modules/services/monitoring/monit.nix
@@ -4,29 +4,19 @@ with lib;
 
 let
   cfg = config.services.monit;
-  extraConfig = pkgs.writeText "monitConfig" cfg.extraConfig;
 in
 
 {
-  imports = [
-    (mkRenamedOptionModule [ "services" "monit" "config" ] ["services" "monit" "extraConfig" ])
-  ];
-
   options.services.monit = {
 
     enable = mkEnableOption "Monit";
 
-    configFiles = mkOption {
-      type = types.listOf types.path;
-      default = [];
-      description = "List of paths to be included in the monitrc file";
-    };
-
-    extraConfig = mkOption {
+    config = mkOption {
       type = types.lines;
       default = "";
-      description = "Additional monit config as string";
+      description = "monitrc content";
     };
+
   };
 
   config = mkIf cfg.enable {
@@ -34,7 +24,7 @@ in
     environment.systemPackages = [ pkgs.monit ];
 
     environment.etc.monitrc = {
-      text = concatMapStringsSep "\n" (path: "include ${path}")  (cfg.configFiles ++ [extraConfig]);
+      text = cfg.config;
       mode = "0400";
     };
 
@@ -53,4 +43,6 @@ in
     };
 
   };
+
+  meta.maintainers = with maintainers; [ ryantm ];
 }
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 9ac6869068f..61214508a9c 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -192,6 +192,7 @@ in
       path     = [ pkgs.nagios ] ++ cfg.plugins;
       wantedBy = [ "multi-user.target" ];
       after    = [ "network.target" ];
+      restartTriggers = [ nagiosCfgFile ];
 
       serviceConfig = {
         User = "nagios";
@@ -201,7 +202,6 @@ in
         LogsDirectory = "nagios";
         StateDirectory = "nagios";
         ExecStart = "${pkgs.nagios}/bin/nagios /etc/nagios.cfg";
-        X-ReloadIfChanged = nagiosCfgFile;
       };
     };
 
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index 2e73e15d3a8..561ce3eec62 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -8,6 +8,7 @@ let
   wrappedPlugins = pkgs.runCommand "wrapped-plugins" { preferLocalBuild = true; } ''
     mkdir -p $out/libexec/netdata/plugins.d
     ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin
+    ln -s /run/wrappers/bin/cgroup-network $out/libexec/netdata/plugins.d/cgroup-network
     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
@@ -26,6 +27,10 @@ let
       "web files owner" = "root";
       "web files group" = "root";
     };
+    "plugin:cgroups" = {
+      "script to get cgroup network interfaces" = "${wrappedPlugins}/libexec/netdata/plugins.d/cgroup-network";
+      "use unified cgroups" = "yes";
+    };
   };
   mkConfig = generators.toINI {} (recursiveUpdate localConfig cfg.config);
   configFile = pkgs.writeText "netdata.conf" (if cfg.configText != null then cfg.configText else mkConfig);
@@ -77,6 +82,7 @@ in {
           '';
         };
         extraPackages = mkOption {
+          type = types.functionTo (types.listOf types.package);
           default = ps: [];
           defaultText = "ps: []";
           example = literalExample ''
@@ -122,9 +128,20 @@ in {
             "error log" = "syslog";
           };
         '';
-        };
+      };
+
+      enableAnalyticsReporting = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable reporting of anonymous usage statistics to Netdata Inc. via either
+          Google Analytics (in versions prior to 1.29.4), or Netdata Inc.'s
+          self-hosted PostHog (in versions 1.29.4 and later).
+          See: <link xlink:href="https://learn.netdata.cloud/docs/agent/anonymous-statistics"/>
+        '';
       };
     };
+  };
 
   config = mkIf cfg.enable {
     assertions =
@@ -137,12 +154,17 @@ in {
       description = "Real time performance monitoring";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = (with pkgs; [ curl gawk which ]) ++ lib.optional cfg.python.enable
-        (pkgs.python3.withPackages cfg.python.extraPackages);
+      path = (with pkgs; [ curl gawk iproute2 which ])
+        ++ lib.optional cfg.python.enable (pkgs.python3.withPackages cfg.python.extraPackages)
+        ++ lib.optional config.virtualisation.libvirtd.enable (config.virtualisation.libvirtd.package);
+      environment = {
+        PYTHONPATH = "${cfg.package}/libexec/netdata/python.d/python_modules";
+      } // lib.optionalAttrs (!cfg.enableAnalyticsReporting) {
+        DO_NOT_TRACK = "1";
+      };
       serviceConfig = {
-        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";
+        ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID";
         TimeoutStopSec = 60;
         Restart = "on-failure";
         # User and group
@@ -175,6 +197,8 @@ in {
           "CAP_SYS_PTRACE"        # is required for apps plugin
           "CAP_SYS_RESOURCE"      # is required for ebpf plugin
           "CAP_NET_RAW"           # is required for fping app
+          "CAP_SYS_CHROOT"        # is required for cgroups plugin
+          "CAP_SETUID"            # is required for cgroups and cgroups-network plugins
         ];
         # Sandboxing
         ProtectSystem = "full";
@@ -192,7 +216,15 @@ in {
       capabilities = "cap_dac_read_search,cap_sys_ptrace+ep";
       owner = cfg.user;
       group = cfg.group;
-      permissions = "u+rx,g+rx,o-rwx";
+      permissions = "u+rx,g+x,o-rwx";
+    };
+
+    security.wrappers."cgroup-network" = {
+      source = "${cfg.package}/libexec/netdata/plugins.d/cgroup-network.org";
+      capabilities = "cap_setuid+ep";
+      owner = cfg.user;
+      group = cfg.group;
+      permissions = "u+rx,g+x,o-rwx";
     };
 
     security.wrappers."freeipmi.plugin" = {
@@ -200,7 +232,7 @@ in {
       capabilities = "cap_dac_override,cap_fowner+ep";
       owner = cfg.user;
       group = cfg.group;
-      permissions = "u+rx,g+rx,o-rwx";
+      permissions = "u+rx,g+x,o-rwx";
     };
 
     security.wrappers."perf.plugin" = {
@@ -208,7 +240,7 @@ in {
       capabilities = "cap_sys_admin+ep";
       owner = cfg.user;
       group = cfg.group;
-      permissions = "u+rx,g+rx,o-rx";
+      permissions = "u+rx,g+x,o-rwx";
     };
 
     security.wrappers."slabinfo.plugin" = {
@@ -216,7 +248,7 @@ in {
       capabilities = "cap_dac_override+ep";
       owner = cfg.user;
       group = cfg.group;
-      permissions = "u+rx,g+rx,o-rx";
+      permissions = "u+rx,g+x,o-rwx";
     };
 
     security.pam.loginLimits = [
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index d7e06484b69..3be247ffb24 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -32,6 +32,8 @@ let
       (pkgs.writeText "prometheus.rules" (concatStringsSep "\n" cfg.rules))
     ]);
     scrape_configs = filterValidPrometheus cfg.scrapeConfigs;
+    remote_write = filterValidPrometheus cfg.remoteWrite;
+    remote_read = filterValidPrometheus cfg.remoteRead;
     alerting = {
       inherit (cfg) alertmanagers;
     };
@@ -45,12 +47,12 @@ let
 
   cmdlineArgs = cfg.extraFlags ++ [
     "--storage.tsdb.path=${workingDir}/data/"
-    "--config.file=${prometheusYml}"
+    "--config.file=/run/prometheus/prometheus-substituted.yaml"
     "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}"
     "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity}"
     "--alertmanager.timeout=${toString cfg.alertmanagerTimeout}s"
-  ] ++
-  optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}";
+  ] ++ optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}"
+    ++ optional (cfg.retentionTime != null)  "--storage.tsdb.retention.time=${cfg.retentionTime}";
 
   filterValidPrometheus = filterAttrsListRecursive (n: v: !(n == "_module" || v == null));
   filterAttrsListRecursive = pred: x:
@@ -101,6 +103,157 @@ let
     };
   };
 
+  promTypes.remote_read = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          ServerName extension to indicate the name of the server.
+          http://tools.ietf.org/html/rfc4366#section-3.1
+        '';
+      };
+      name = mkOpt types.str ''
+        Name of the remote read config, which if specified must be unique among remote read configs.
+        The name will be used in metrics and logging in place of a generated value to help users distinguish between
+        remote read configs.
+      '';
+      required_matchers = mkOpt (types.attrsOf types.str) ''
+        An optional list of equality matchers which have to be
+        present in a selector to query the remote read endpoint.
+      '';
+      remote_timeout = mkOpt types.str ''
+        Timeout for requests to the remote read endpoint.
+      '';
+      read_recent = mkOpt types.bool ''
+        Whether reads should be made for queries for time ranges that
+        the local storage should have complete data for.
+      '';
+      basic_auth = mkOpt (types.submodule {
+        options = {
+          username = mkOption {
+            type = types.str;
+            description = ''
+              HTTP username
+            '';
+          };
+          password = mkOpt types.str "HTTP password";
+          password_file = mkOpt types.str "HTTP password file";
+        };
+      }) ''
+        Sets the `Authorization` header on every remote read request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every remote read request with
+        the configured bearer token. It is mutually exclusive with `bearer_token_file`.
+      '';
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every remote read request with the bearer token
+        read from the configured file. It is mutually exclusive with `bearer_token`.
+      '';
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the remote read request's TLS settings.
+      '';
+      proxy_url = mkOpt types.str "Optional Proxy URL.";
+    };
+  };
+
+  promTypes.remote_write = types.submodule {
+    options = {
+      url = mkOption {
+        type = types.str;
+        description = ''
+          ServerName extension to indicate the name of the server.
+          http://tools.ietf.org/html/rfc4366#section-3.1
+        '';
+      };
+      remote_timeout = mkOpt types.str ''
+        Timeout for requests to the remote write endpoint.
+      '';
+      write_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
+        List of remote write relabel configurations.
+      '';
+      name = mkOpt types.str ''
+        Name of the remote write config, which if specified must be unique among remote write configs.
+        The name will be used in metrics and logging in place of a generated value to help users distinguish between
+        remote write configs.
+      '';
+      basic_auth = mkOpt (types.submodule {
+        options = {
+          username = mkOption {
+            type = types.str;
+            description = ''
+              HTTP username
+            '';
+          };
+          password = mkOpt types.str "HTTP password";
+          password_file = mkOpt types.str "HTTP password file";
+        };
+      }) ''
+        Sets the `Authorization` header on every remote write request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
+      '';
+      bearer_token = mkOpt types.str ''
+        Sets the `Authorization` header on every remote write request with
+        the configured bearer token. It is mutually exclusive with `bearer_token_file`.
+      '';
+      bearer_token_file = mkOpt types.str ''
+        Sets the `Authorization` header on every remote write request with the bearer token
+        read from the configured file. It is mutually exclusive with `bearer_token`.
+      '';
+      tls_config = mkOpt promTypes.tls_config ''
+        Configures the remote write request's TLS settings.
+      '';
+      proxy_url = mkOpt types.str "Optional Proxy URL.";
+      queue_config = mkOpt (types.submodule {
+        options = {
+          capacity = mkOpt types.int ''
+            Number of samples to buffer per shard before we block reading of more
+            samples from the WAL. It is recommended to have enough capacity in each
+            shard to buffer several requests to keep throughput up while processing
+            occasional slow remote requests.
+          '';
+          max_shards = mkOpt types.int ''
+            Maximum number of shards, i.e. amount of concurrency.
+          '';
+          min_shards = mkOpt types.int ''
+            Minimum number of shards, i.e. amount of concurrency.
+          '';
+          max_samples_per_send = mkOpt types.int ''
+            Maximum number of samples per send.
+          '';
+          batch_send_deadline = mkOpt types.str ''
+            Maximum time a sample will wait in buffer.
+          '';
+          min_backoff = mkOpt types.str ''
+            Initial retry delay. Gets doubled for every retry.
+          '';
+          max_backoff = mkOpt types.str ''
+            Maximum retry delay.
+          '';
+        };
+      }) ''
+        Configures the queue used to write to remote storage.
+      '';
+      metadata_config = mkOpt (types.submodule {
+        options = {
+          send = mkOpt types.bool ''
+            Whether metric metadata is sent to remote storage or not.
+          '';
+          send_interval = mkOpt types.str ''
+            How frequently metric metadata is sent to remote storage.
+          '';
+        };
+      }) ''
+        Configures the sending of series metadata to remote storage.
+        Metadata configuration is subject to change at any point
+        or be removed in future releases.
+      '';
+    };
+  };
+
   promTypes.scrape_config = types.submodule {
     options = {
       job_name = mkOption {
@@ -170,15 +323,13 @@ let
               HTTP username
             '';
           };
-          password = mkOption {
-            type = types.str;
-            description = ''
-              HTTP password
-            '';
-          };
+          password = mkOpt types.str "HTTP password";
+          password_file = mkOpt types.str "HTTP password file";
         };
       }) ''
-        Optional http login credentials for metrics scraping.
+        Sets the `Authorization` header on every scrape request with the
+        configured username and password.
+        password and password_file are mutually exclusive.
       '';
 
       bearer_token = mkOpt types.str ''
@@ -217,6 +368,14 @@ let
         List of file service discovery configurations.
       '';
 
+      gce_sd_configs = mkOpt (types.listOf promTypes.gce_sd_config) ''
+        List of Google Compute Engine service discovery configurations.
+
+        See <link
+        xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config">the
+        relevant Prometheus configuration docs</link> for more detail.
+      '';
+
       static_configs = mkOpt (types.listOf promTypes.static_config) ''
         List of labeled target groups for this job.
       '';
@@ -225,6 +384,10 @@ let
         List of relabel configurations.
       '';
 
+      metric_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) ''
+        List of metric relabel configurations.
+      '';
+
       sample_limit = mkDefOpt types.int "0" ''
         Per-scrape limit on number of scraped samples that will be accepted.
         If more than this number of samples are present after metric relabelling
@@ -307,7 +470,7 @@ let
         '';
       };
 
-      value = mkOption {
+      values = mkOption {
         type = types.listOf types.str;
         default = [];
         description = ''
@@ -402,6 +565,52 @@ let
     };
   };
 
+  promTypes.gce_sd_config = types.submodule {
+    options = {
+      # Use `mkOption` instead of `mkOpt` for project and zone because they are
+      # required configuration values for `gce_sd_config`.
+      project = mkOption {
+        type = types.str;
+        description = ''
+          The GCP Project.
+        '';
+      };
+
+      zone = mkOption {
+        type = types.str;
+        description = ''
+          The zone of the scrape targets. If you need multiple zones use multiple
+          gce_sd_configs.
+        '';
+      };
+
+      filter = mkOpt types.str ''
+        Filter can be used optionally to filter the instance list by other
+        criteria Syntax of this filter string is described here in the filter
+        query parameter section: <link
+        xlink:href="https://cloud.google.com/compute/docs/reference/latest/instances/list"
+        />.
+      '';
+
+      refresh_interval = mkDefOpt types.str "60s" ''
+        Refresh interval to re-read the cloud instance list.
+      '';
+
+      port = mkDefOpt types.port "80" ''
+        The port to scrape metrics from. If using the public IP address, this
+        must instead be specified in the relabeling rule.
+      '';
+
+      tag_separator = mkDefOpt types.str "," ''
+        The tag separator used to separate concatenated GCE instance network tags.
+
+        See the GCP documentation on network tags for more information: <link
+        xlink:href="https://cloud.google.com/vpc/docs/add-remove-network-tags"
+        />
+      '';
+    };
+  };
+
   promTypes.relabel_config = types.submodule {
     options = {
       source_labels = mkOpt (types.listOf types.str) ''
@@ -432,10 +641,10 @@ let
         regular expression matches.
       '';
 
-      action = mkDefOpt (types.enum ["replace" "keep" "drop"]) "replace" ''
+      action =
+        mkDefOpt (types.enum ["replace" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep"]) "replace" ''
         Action to perform based on regex matching.
       '';
-
     };
   };
 
@@ -522,6 +731,45 @@ in {
       '';
     };
 
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/root/prometheus.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        Environment variables from this file will be interpolated into the
+        config file using envsubst with this syntax:
+        <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+
+        <programlisting>
+          # Example scrape config entry handling an OAuth bearer token
+          {
+            job_name = "home_assistant";
+            metrics_path = "/api/prometheus";
+            scheme = "https";
+            bearer_token = "\''${HOME_ASSISTANT_BEARER_TOKEN}";
+            [...]
+          }
+        </programlisting>
+
+        <programlisting>
+          # Content of the environment file
+          HOME_ASSISTANT_BEARER_TOKEN=someoauthbearertoken
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>Prometheus</literal> is running.
+      '';
+    };
+
     configText = mkOption {
       type = types.nullOr types.lines;
       default = null;
@@ -541,6 +789,24 @@ in {
       '';
     };
 
+    remoteRead = mkOption {
+      type = types.listOf promTypes.remote_read;
+      default = [];
+      description = ''
+        Parameters of the endpoints to query from.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_read">the official documentation</link> for more information.
+      '';
+    };
+
+    remoteWrite = mkOption {
+      type = types.listOf promTypes.remote_write;
+      default = [];
+      description = ''
+        Parameters of the endpoints to send samples to.
+        See <link xlink:href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write">the official documentation</link> for more information.
+      '';
+    };
+
     rules = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -624,12 +890,23 @@ in {
         errors, despite a correct configuration.
       '';
     };
+
+    retentionTime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "15d";
+      description = ''
+        How long to retain samples in storage.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
     assertions = [
       ( let
-          legacy = builtins.match "(.*):(.*)" cfg.listenAddress;
+          # Match something with dots (an IPv4 address) or something ending in
+          # a square bracket (an IPv6 addresses) followed by a port number.
+          legacy = builtins.match "(.*\\..*|.*]):([[:digit:]]+)" cfg.listenAddress;
         in {
           assertion = legacy == null;
           message = ''
@@ -651,14 +928,22 @@ in {
     systemd.services.prometheus = {
       wantedBy = [ "multi-user.target" ];
       after    = [ "network.target" ];
+      preStart = ''
+         ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/run/prometheus/prometheus-substituted.yaml" \
+                                                  -i "${prometheusYml}"
+      '';
       serviceConfig = {
         ExecStart = "${cfg.package}/bin/prometheus" +
           optionalString (length cmdlineArgs != 0) (" \\\n  " +
             concatStringsSep " \\\n  " cmdlineArgs);
         User = "prometheus";
         Restart  = "always";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
+        RuntimeDirectory = "prometheus";
+        RuntimeDirectoryMode = "0700";
         WorkingDirectory = workingDir;
         StateDirectory = cfg.stateDir;
+        StateDirectoryMode = "0700";
       };
     };
   };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index 59748efe0de..d648de6a414 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -3,7 +3,7 @@
 let
   inherit (lib) concatStrings foldl foldl' genAttrs literalExample maintainers
                 mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption
-                optional types;
+                optional types mkOptionDefault flip attrNames;
 
   cfg = config.services.prometheus.exporters;
 
@@ -22,14 +22,22 @@ let
 
   exporterOpts = genAttrs [
     "apcupsd"
+    "artifactory"
     "bind"
+    "bird"
+    "bitcoin"
     "blackbox"
+    "buildkite-agent"
     "collectd"
     "dnsmasq"
+    "domain"
     "dovecot"
     "fritzbox"
     "json"
+    "jitsi"
+    "kea"
     "keylight"
+    "knot"
     "lnd"
     "mail"
     "mikrotik"
@@ -37,17 +45,31 @@ let
     "modemmanager"
     "nextcloud"
     "nginx"
+    "nginxlog"
     "node"
+    "openldap"
+    "openvpn"
+    "pihole"
     "postfix"
     "postgres"
+    "process"
+    "py-air-control"
     "redis"
     "rspamd"
+    "rtl_433"
+    "script"
     "snmp"
+    "smokeping"
+    "sql"
     "surfboard"
+    "systemd"
     "tor"
+    "unbound"
     "unifi"
+    "unifi-poller"
     "varnish"
     "wireguard"
+    "flow"
   ] (name:
     import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; }
   );
@@ -55,7 +77,7 @@ let
   mkExporterOpts = ({ name, port }: {
     enable = mkEnableOption "the prometheus ${name} exporter";
     port = mkOption {
-      type = types.int;
+      type = types.port;
       default = port;
       description = ''
         Port to listen on.
@@ -83,8 +105,8 @@ let
       '';
     };
     firewallFilter = mkOption {
-      type = types.str;
-      default = "-p tcp -m tcp --dport ${toString port}";
+      type = types.nullOr types.str;
+      default = null;
       example = literalExample ''
         "-i eth0 -p tcp -m tcp --dport ${toString port}"
       '';
@@ -99,7 +121,6 @@ let
       default = "${name}-exporter";
       description = ''
         User name under which the ${name} exporter shall be run.
-        Has no effect when <option>systemd.services.prometheus-${name}-exporter.serviceConfig.DynamicUser</option> is true.
       '';
     };
     group = mkOption {
@@ -107,19 +128,20 @@ let
       default = "${name}-exporter";
       description = ''
         Group under which the ${name} exporter shall be run.
-        Has no effect when <option>systemd.services.prometheus-${name}-exporter.serviceConfig.DynamicUser</option> is true.
       '';
     };
   });
 
   mkSubModule = { name, port, extraOpts, imports }: {
     ${name} = mkOption {
-      type = types.submodule {
+      type = types.submodule [{
         inherit imports;
         options = (mkExporterOpts {
           inherit name port;
         } // extraOpts);
-      };
+      } ({ config, ... }: mkIf config.openFirewall {
+        firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}";
+      })];
       internal = true;
       default = {};
     };
@@ -159,10 +181,9 @@ let
         serviceConfig.PrivateTmp = mkDefault true;
         serviceConfig.WorkingDirectory = mkDefault /tmp;
         serviceConfig.DynamicUser = mkDefault enableDynamicUser;
-      } serviceOpts ] ++ optional (!enableDynamicUser) {
-        serviceConfig.User = conf.user;
+        serviceConfig.User = mkDefault conf.user;
         serviceConfig.Group = conf.group;
-      });
+      } serviceOpts ]);
   };
 in
 {
@@ -217,16 +238,29 @@ in
         Please specify either 'services.prometheus.exporters.mail.configuration'
           or 'services.prometheus.exporters.mail.configFile'.
       '';
-    } ];
+    } {
+      assertion = cfg.sql.enable -> (
+        (cfg.sql.configFile == null) != (cfg.sql.configuration == null)
+      );
+      message = ''
+        Please specify either 'services.prometheus.exporters.sql.configuration' or
+          'services.prometheus.exporters.sql.configFile'
+      '';
+    } ] ++ (flip map (attrNames cfg) (exporter: {
+      assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
+      message = ''
+        The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless
+        `openFirewall' is set to `true'!
+      '';
+    }));
   }] ++ [(mkIf config.services.minio.enable {
     services.prometheus.exporters.minio.minioAddress  = mkDefault "http://localhost:9000";
     services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey;
     services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey;
-  })] ++ [(mkIf config.services.rspamd.enable {
-    services.prometheus.exporters.rspamd.url = mkDefault "http://localhost:11334/stat";
-  })] ++ [(mkIf config.services.nginx.enable {
-    systemd.services.prometheus-nginx-exporter.after = [ "nginx.service" ];
-    systemd.services.prometheus-nginx-exporter.requires = [ "nginx.service" ];
+  })] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable {
+    hardware.rtl-sdr.enable = mkDefault true;
+  })] ++ [(mkIf config.services.postfix.enable {
+    services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
   })] ++ (mapAttrsToList (name: conf:
     mkExporterConf {
       inherit name;
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix b/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix
new file mode 100644
index 00000000000..2adcecc728b
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.artifactory;
+in
+{
+  port = 9531;
+  extraOpts = {
+    scrapeUri = mkOption {
+      type = types.str;
+      default = "http://localhost:8081/artifactory";
+      description = ''
+        URI on which to scrape JFrog Artifactory.
+      '';
+    };
+
+    artiUsername = mkOption {
+      type = types.str;
+      description = ''
+        Username for authentication against JFrog Artifactory API.
+      '';
+    };
+
+    artiPassword = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Password for authentication against JFrog Artifactory API.
+        One of the password or access token needs to be set.
+      '';
+    };
+
+    artiAccessToken = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Access token for authentication against JFrog Artifactory API.
+        One of the password or access token needs to be set.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-artifactory-exporter}/bin/artifactory_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --artifactory.scrape-uri ${cfg.scrapeUri} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      Environment = [
+        "ARTI_USERNAME=${cfg.artiUsername}"
+        "ARTI_PASSWORD=${cfg.artiPassword}"
+        "ARTI_ACCESS_TOKEN=${cfg.artiAccessToken}"
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bind.nix b/nixos/modules/services/monitoring/prometheus/exporters/bind.nix
index 972632b5a24..16c2920751d 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/bind.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bind.nix
@@ -41,12 +41,12 @@ in
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-bind-exporter}/bin/bind_exporter \
-          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
-          -bind.pid-file /var/run/named/named.pid \
-          -bind.timeout ${toString cfg.bindTimeout} \
-          -bind.stats-url ${cfg.bindURI} \
-          -bind.stats-version ${cfg.bindVersion} \
-          -bind.stats-groups ${concatStringsSep "," cfg.bindGroups} \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --bind.pid-file /var/run/named/named.pid \
+          --bind.timeout ${toString cfg.bindTimeout} \
+          --bind.stats-url ${cfg.bindURI} \
+          --bind.stats-version ${cfg.bindVersion} \
+          --bind.stats-groups ${concatStringsSep "," cfg.bindGroups} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bird.nix b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
new file mode 100644
index 00000000000..d8a526eafce
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bird.nix
@@ -0,0 +1,46 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.bird;
+in
+{
+  port = 9324;
+  extraOpts = {
+    birdVersion = mkOption {
+      type = types.enum [ 1 2 ];
+      default = 2;
+      description = ''
+        Specifies whether BIRD1 or BIRD2 is in use.
+      '';
+    };
+    birdSocket = mkOption {
+      type = types.path;
+      default = "/var/run/bird.ctl";
+      description = ''
+        Path to BIRD2 (or BIRD1 v4) socket.
+      '';
+    };
+    newMetricFormat = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Enable the new more-generic metric format.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      SupplementaryGroups = singleton (if cfg.birdVersion == 1 then "bird" else "bird2");
+      ExecStart = ''
+        ${pkgs.prometheus-bird-exporter}/bin/bird_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -bird.socket ${cfg.birdSocket} \
+          -bird.v2=${if cfg.birdVersion == 2 then "true" else "false"} \
+          -format.new=${if cfg.newMetricFormat then "true" else "false"} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix b/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix
new file mode 100644
index 00000000000..43721f70b49
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix
@@ -0,0 +1,82 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.bitcoin;
+in
+{
+  port = 9332;
+  extraOpts = {
+    rpcUser = mkOption {
+      type = types.str;
+      default = "bitcoinrpc";
+      description = ''
+        RPC user name.
+      '';
+    };
+
+    rpcPasswordFile = mkOption {
+      type = types.path;
+      description = ''
+        File containing RPC password.
+      '';
+    };
+
+    rpcScheme = mkOption {
+      type = types.enum [ "http" "https" ];
+      default = "http";
+      description = ''
+        Whether to connect to bitcoind over http or https.
+      '';
+    };
+
+    rpcHost = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = ''
+        RPC host.
+      '';
+    };
+
+    rpcPort = mkOption {
+      type = types.port;
+      default = 8332;
+      description = ''
+        RPC port number.
+      '';
+    };
+
+    refreshSeconds = mkOption {
+      type = types.ints.unsigned;
+      default = 300;
+      description = ''
+        How often to ask bitcoind for metrics.
+      '';
+    };
+
+    extraEnv = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      description = ''
+        Extra environment variables for the exporter.
+      '';
+    };
+  };
+  serviceOpts = {
+    script = ''
+      export BITCOIN_RPC_PASSWORD=$(cat ${cfg.rpcPasswordFile})
+      exec ${pkgs.prometheus-bitcoin-exporter}/bin/bitcoind-monitor.py
+    '';
+
+    environment = {
+      BITCOIN_RPC_USER = cfg.rpcUser;
+      BITCOIN_RPC_SCHEME = cfg.rpcScheme;
+      BITCOIN_RPC_HOST = cfg.rpcHost;
+      BITCOIN_RPC_PORT = toString cfg.rpcPort;
+      METRICS_ADDR = cfg.listenAddress;
+      METRICS_PORT = toString cfg.port;
+      REFRESH_SECONDS = toString cfg.refreshSeconds;
+    } // cfg.extraEnv;
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix b/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix
new file mode 100644
index 00000000000..7557480ac06
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.buildkite-agent;
+in
+{
+  port = 9876;
+  extraOpts = {
+    tokenPath = mkOption {
+      type = types.nullOr types.path;
+      apply = final: if final == null then null else toString final;
+      description = ''
+        The token from your Buildkite "Agents" page.
+
+        A run-time path to the token file, which is supposed to be provisioned
+        outside of Nix store.
+      '';
+    };
+    interval = mkOption {
+      type = types.str;
+      default = "30s";
+      example = "1min";
+      description = ''
+        How often to update metrics.
+      '';
+    };
+    endpoint = mkOption {
+      type = types.str;
+      default = "https://agent.buildkite.com/v3";
+      description = ''
+        The Buildkite Agent API endpoint.
+      '';
+    };
+    queues = mkOption {
+      type = with types; nullOr (listOf str);
+      default = null;
+      example = literalExample ''[ "my-queue1" "my-queue2" ]'';
+      description = ''
+        Which specific queues to process.
+      '';
+    };
+  };
+  serviceOpts = {
+    script =
+      let
+        queues = concatStringsSep " " (map (q: "-queue ${q}") cfg.queues);
+      in
+      ''
+        export BUILDKITE_AGENT_TOKEN="$(cat ${toString cfg.tokenPath})"
+        exec ${pkgs.buildkite-agent-metrics}/bin/buildkite-agent-metrics \
+          -backend prometheus \
+          -interval ${cfg.interval} \
+          -endpoint ${cfg.endpoint} \
+          ${optionalString (cfg.queues != null) queues} \
+          -prometheus-addr "${cfg.listenAddress}:${toString cfg.port}" ${concatStringsSep " " cfg.extraFlags}
+      '';
+    serviceConfig = {
+      DynamicUser = false;
+      RuntimeDirectory = "buildkite-agent-metrics";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
index 97210463027..a7f4d3e096f 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -20,7 +20,7 @@ in
       port = mkOption {
         type = types.int;
         default = 25826;
-        description = ''Network address on which to accept collectd binary network packets.'';
+        description = "Network address on which to accept collectd binary network packets.";
       };
 
       listenAddress = mkOption {
@@ -41,11 +41,11 @@ in
     };
 
     logFormat = mkOption {
-      type = types.str;
-      default = "logger:stderr";
-      example = "logger:syslog?appname=bob&local=7 or logger:stdout?json=true";
+      type = types.enum [ "logfmt" "json" ];
+      default = "logfmt";
+      example = "json";
       description = ''
-        Set the log target and format.
+        Set the log format.
       '';
     };
 
@@ -59,16 +59,16 @@ in
   };
   serviceOpts = let
     collectSettingsArgs = if (cfg.collectdBinary.enable) then ''
-      -collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \
-      -collectd.security-level ${cfg.collectdBinary.securityLevel} \
+      --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \
+      --collectd.security-level ${cfg.collectdBinary.securityLevel} \
     '' else "";
   in {
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-collectd-exporter}/bin/collectd_exporter \
-          -log.format ${escapeShellArg cfg.logFormat} \
-          -log.level ${cfg.logLevel} \
-          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --log.format ${escapeShellArg cfg.logFormat} \
+          --log.level ${cfg.logLevel} \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
           ${collectSettingsArgs} \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/domain.nix b/nixos/modules/services/monitoring/prometheus/exporters/domain.nix
new file mode 100644
index 00000000000..61e2fc80afd
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/domain.nix
@@ -0,0 +1,19 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.domain;
+in
+{
+  port = 9222;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-domain-exporter}/bin/domain_exporter \
+          --bind ${cfg.listenAddress}:${toString cfg.port} \
+          ${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 aba3533e439..472652fe8a7 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix
@@ -35,13 +35,28 @@ in
         {
           <xref linkend="opt-services.prometheus.exporters.dovecot.enable" /> = true;
           <xref linkend="opt-services.prometheus.exporters.dovecot.socketPath" /> = "/var/run/dovecot2/old-stats";
+          <xref linkend="opt-services.dovecot2.mailPlugins.globally.enable" /> = [ "old_stats" ];
           <xref linkend="opt-services.dovecot2.extraConfig" /> = '''
-            mail_plugins = $mail_plugins old_stats
             service old-stats {
               unix_listener old-stats {
                 user = dovecot-exporter
                 group = dovecot-exporter
+                mode = 0660
               }
+              fifo_listener old-stats-mail {
+                mode = 0660
+                user = dovecot
+                group = dovecot
+              }
+              fifo_listener old-stats-user {
+                mode = 0660
+                user = dovecot
+                group = dovecot
+              }
+            }
+            plugin {
+              old_stats_refresh = 30 secs
+              old_stats_track_cmds = yes
             }
           ''';
         }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/flow.nix b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix
new file mode 100644
index 00000000000..6a35f46308f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/flow.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.flow;
+in {
+  port = 9590;
+  extraOpts = {
+    brokers = mkOption {
+      type = types.listOf types.str;
+      example = literalExample ''[ "kafka.example.org:19092" ]'';
+      description = "List of Kafka brokers to connect to.";
+    };
+
+    asn = mkOption {
+      type = types.ints.positive;
+      example = 65542;
+      description = "The ASN being monitored.";
+    };
+
+    partitions = mkOption {
+      type = types.listOf types.int;
+      default = [];
+      description = ''
+        The number of the partitions to consume, none means all.
+      '';
+    };
+
+    topic = mkOption {
+      type = types.str;
+      example = "pmacct.acct";
+      description = "The Kafka topic to consume from.";
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = true;
+      ExecStart = ''
+        ${pkgs.prometheus-flow-exporter}/bin/flow-exporter \
+          -asn ${toString cfg.asn} \
+          -topic ${cfg.topic} \
+          -brokers ${concatStringsSep "," cfg.brokers} \
+          ${optionalString (cfg.partitions != []) "-partitions ${concatStringsSep "," cfg.partitions}"} \
+          -addr ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix b/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix
new file mode 100644
index 00000000000..c93a8f98e55
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix
@@ -0,0 +1,40 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.jitsi;
+in
+{
+  port = 9700;
+  extraOpts = {
+    url = mkOption {
+      type = types.str;
+      default = "http://localhost:8080/colibri/stats";
+      description = ''
+        Jitsi Videobridge metrics URL to monitor.
+        This is usually /colibri/stats on port 8080 of the jitsi videobridge host.
+      '';
+    };
+    interval = mkOption {
+      type = types.str;
+      default = "30s";
+      example = "1min";
+      description = ''
+        How often to scrape new data
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-jitsi-exporter}/bin/jitsiexporter \
+          -url ${escapeShellArg cfg.url} \
+          -host ${cfg.listenAddress} \
+          -port ${toString cfg.port} \
+          -interval ${toString cfg.interval} \
+          ${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 bd0026b55f7..1800da69a25 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/json.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/json.nix
@@ -8,28 +8,36 @@ in
 {
   port = 7979;
   extraOpts = {
-    url = mkOption {
-      type = types.str;
-      description = ''
-        URL to scrape JSON from.
-      '';
-    };
     configFile = mkOption {
       type = types.path;
       description = ''
         Path to configuration file.
       '';
     };
-    listenAddress = {}; # not used
   };
   serviceOpts = {
     serviceConfig = {
       ExecStart = ''
-        ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \
-          --port ${toString cfg.port} \
-          ${cfg.url} ${escapeShellArg cfg.configFile} \
+        ${pkgs.prometheus-json-exporter}/bin/json_exporter \
+          --config.file ${escapeShellArg cfg.configFile} \
+          --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
   };
+  imports = [
+    (mkRemovedOptionModule [ "url" ] ''
+      This option was removed. The URL of the endpoint serving JSON
+      must now be provided to the exporter by prometheus via the url
+      parameter `target'.
+
+      In prometheus a scrape URL would look like this:
+
+        http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
+
+      For more information, take a look at the official documentation
+      (https://github.com/prometheus-community/json_exporter) of the json_exporter.
+    '')
+     ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
new file mode 100644
index 00000000000..9677281f877
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix
@@ -0,0 +1,39 @@
+{ config
+, lib
+, pkgs
+, options
+}:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.kea;
+in {
+  port = 9547;
+  extraOpts = {
+    controlSocketPaths = mkOption {
+      type = types.listOf types.str;
+      example = literalExample ''
+        [
+          "/run/kea/kea-dhcp4.socket"
+          "/run/kea/kea-dhcp6.socket"
+        ]
+      '';
+      description = ''
+        Paths to kea control sockets
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      User = "kea";
+      ExecStart = ''
+        ${pkgs.prometheus-kea-exporter}/bin/kea-exporter \
+          --address ${cfg.listenAddress} \
+          --port ${toString cfg.port} \
+          ${concatStringsSep " \\n" cfg.controlSocketPaths}
+      '';
+      SupplementaryGroups = [ "kea" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/knot.nix b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix
new file mode 100644
index 00000000000..46c28fe0a57
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix
@@ -0,0 +1,50 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.knot;
+in {
+  port = 9433;
+  extraOpts = {
+    knotLibraryPath = mkOption {
+      type = types.str;
+      default = "${pkgs.knot-dns.out}/lib/libknot.so";
+      defaultText = "\${pkgs.knot-dns}/lib/libknot.so";
+      description = ''
+        Path to the library of <package>knot-dns</package>.
+      '';
+    };
+
+    knotSocketPath = mkOption {
+      type = types.str;
+      default = "/run/knot/knot.sock";
+      description = ''
+        Socket path of <citerefentry><refentrytitle>knotd</refentrytitle>
+        <manvolnum>8</manvolnum></citerefentry>.
+      '';
+    };
+
+    knotSocketTimeout = mkOption {
+      type = types.int;
+      default = 2000;
+      description = ''
+        Timeout in seconds.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-knot-exporter}/bin/knot_exporter \
+          --web-listen-addr ${cfg.listenAddress} \
+          --web-listen-port ${toString cfg.port} \
+          --knot-library-path ${cfg.knotLibraryPath} \
+          --knot-socket-path ${cfg.knotSocketPath} \
+          --knot-socket-timeout ${toString cfg.knotSocketTimeout} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      SupplementaryGroups = [ "knot" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
index 18c5c4dd162..7e196149fbb 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/mail.nix
@@ -112,6 +112,24 @@ let
       '';
       description = ''
         List of servers that should be probed.
+
+        <emphasis>Note:</emphasis> if your mailserver has <citerefentry>
+        <refentrytitle>rspamd</refentrytitle><manvolnum>8</manvolnum></citerefentry> configured,
+        it can happen that emails from this exporter are marked as spam.
+
+        It's possible to work around the issue with a config like this:
+        <programlisting>
+        {
+          <link linkend="opt-services.rspamd.locals._name_.text">services.rspamd.locals."multimap.conf".text</link> = '''
+            ALLOWLIST_PROMETHEUS {
+              filter = "email:domain:tld";
+              type = "from";
+              map = "''${pkgs.writeText "allowmap" "domain.tld"}";
+              score = -100.0;
+            }
+          ''';
+        }
+        </programlisting>
       '';
     };
   };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
index aee6bd5e66c..ce7125bf5a8 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix
@@ -46,11 +46,11 @@ in
       DynamicUser = false;
       ExecStart = ''
         ${pkgs.prometheus-nextcloud-exporter}/bin/nextcloud-exporter \
-          -a ${cfg.listenAddress}:${toString cfg.port} \
-          -u ${cfg.username} \
-          -t ${cfg.timeout} \
-          -l ${cfg.url} \
-          -p ${escapeShellArg "@${cfg.passwordFile}"} \
+          --addr ${cfg.listenAddress}:${toString cfg.port} \
+          --username ${cfg.username} \
+          --timeout ${cfg.timeout} \
+          --server ${cfg.url} \
+          --password ${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 56cddfc55b7..5ee8c346be1 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix
@@ -42,7 +42,7 @@ in
       '';
     };
   };
-  serviceOpts = {
+  serviceOpts = mkMerge ([{
     serviceConfig = {
       ExecStart = ''
         ${pkgs.prometheus-nginx-exporter}/bin/nginx-prometheus-exporter \
@@ -54,7 +54,10 @@ in
           ${concatStringsSep " \\\n  " cfg.extraFlags}
       '';
     };
-  };
+  }] ++ [(mkIf config.services.nginx.enable {
+    after = [ "nginx.service" ];
+    requires = [ "nginx.service" ];
+  })]);
   imports = [
     (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
     (mkRemovedOptionModule [ "insecure" ] ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix
new file mode 100644
index 00000000000..8c1f552d58a
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nginxlog;
+in {
+  port = 9117;
+  extraOpts = {
+    settings = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        All settings of nginxlog expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: https://github.com/martin-helmich/prometheus-nginxlog-exporter
+
+        The `listen` object is already generated by `port`, `listenAddress` and `metricsEndpoint` and
+        will be merged with the value of `settings` before writting it as JSON.
+      '';
+    };
+
+    metricsEndpoint = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+
+  serviceOpts = let
+    listenConfig = {
+      listen = {
+        port = cfg.port;
+        address = cfg.listenAddress;
+        metrics_endpoint = cfg.metricsEndpoint;
+      };
+    };
+    completeConfig = pkgs.writeText "nginxlog-exporter.yaml" (builtins.toJSON (lib.recursiveUpdate listenConfig cfg.settings));
+  in {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-nginxlog-exporter}/bin/prometheus-nginxlog-exporter -config-file ${completeConfig}
+      '';
+      Restart="always";
+      ProtectSystem="full";
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix b/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix
new file mode 100644
index 00000000000..888611ee6fa
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.openldap;
+in {
+  port = 9330;
+  extraOpts = {
+    ldapCredentialFile = mkOption {
+      type = types.path;
+      example = "/run/keys/ldap_pass";
+      description = ''
+        Environment file to contain the credentials to authenticate against
+        <package>openldap</package>.
+
+        The file should look like this:
+        <programlisting>
+        ---
+        ldapUser: "cn=monitoring,cn=Monitor"
+        ldapPass: "secret"
+        </programlisting>
+      '';
+    };
+    protocol = mkOption {
+      default = "tcp";
+      example = "udp";
+      type = types.str;
+      description = ''
+        Which protocol to use to connect against <package>openldap</package>.
+      '';
+    };
+    ldapAddr = mkOption {
+      default = "localhost:389";
+      type = types.str;
+      description = ''
+        Address of the <package>openldap</package>-instance.
+      '';
+    };
+    metricsPath = mkOption {
+      default = "/metrics";
+      type = types.str;
+      description = ''
+        URL path where metrics should be exposed.
+      '';
+    };
+    interval = mkOption {
+      default = "30s";
+      type = types.str;
+      example = "1m";
+      description = ''
+        Scrape interval of the exporter.
+      '';
+    };
+  };
+  serviceOpts.serviceConfig = {
+    ExecStart = ''
+      ${pkgs.prometheus-openldap-exporter}/bin/openldap_exporter \
+        --promAddr ${cfg.listenAddress}:${toString cfg.port} \
+        --metrPath ${cfg.metricsPath} \
+        --ldapNet ${cfg.protocol} \
+        --interval ${cfg.interval} \
+        --config ${cfg.ldapCredentialFile} \
+        ${concatStringsSep " \\\n  " cfg.extraFlags}
+    '';
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix b/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix
new file mode 100644
index 00000000000..a97a753ebc3
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/openvpn.nix
@@ -0,0 +1,39 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.openvpn;
+in {
+  port = 9176;
+  extraOpts = {
+    statusPaths = mkOption {
+      type = types.listOf types.str;
+      description = ''
+        Paths to OpenVPN status files. Please configure the OpenVPN option
+        <literal>status</literal> accordingly.
+      '';
+    };
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      PrivateDevices = true;
+      ProtectKernelModules = true;
+      NoNewPrivileges = true;
+      ExecStart = ''
+        ${pkgs.prometheus-openvpn-exporter}/bin/openvpn_exporter \
+          -openvpn.status_paths "${concatStringsSep "," cfg.statusPaths}" \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -web.telemetry-path ${cfg.telemetryPath}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
new file mode 100644
index 00000000000..21c2e5eab4c
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.pihole;
+in
+{
+  port = 9617;
+  extraOpts = {
+    apiToken = mkOption {
+      type = types.str;
+      default = "";
+      example = "580a770cb40511eb85290242ac130003580a770cb40511eb85290242ac130003";
+      description = ''
+        pi-hole API token which can be used instead of a password
+      '';
+    };
+    interval = mkOption {
+      type = types.str;
+      default = "10s";
+      example = "30s";
+      description = ''
+        How often to scrape new data
+      '';
+    };
+    password = mkOption {
+      type = types.str;
+      default = "";
+      example = "password";
+      description = ''
+        The password to login into pihole. An api token can be used instead.
+      '';
+    };
+    piholeHostname = mkOption {
+      type = types.str;
+      default = "pihole";
+      example = "127.0.0.1";
+      description = ''
+        Hostname or address where to find the pihole webinterface
+      '';
+    };
+    piholePort = mkOption {
+      type = types.port;
+      default = "80";
+      example = "443";
+      description = ''
+        The port pihole webinterface is reachable on
+      '';
+    };
+    protocol = mkOption {
+      type = types.enum [ "http" "https" ];
+      default = "http";
+      example = "https";
+      description = ''
+        The protocol which is used to connect to pihole
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.bash}/bin/bash -c "${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \
+          -interval ${cfg.interval} \
+          ${optionalString (cfg.apiToken != "") "-pihole_api_token ${cfg.apiToken}"} \
+          -pihole_hostname ${cfg.piholeHostname} \
+          ${optionalString (cfg.password != "") "-pihole_password ${cfg.password}"} \
+          -pihole_port ${toString cfg.piholePort} \
+          -pihole_protocol ${cfg.protocol} \
+          -port ${toString cfg.port}"
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
index 3b6ef1631f8..f57589a59c7 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix
@@ -8,6 +8,15 @@ in
 {
   port = 9154;
   extraOpts = {
+    group = mkOption {
+      type = types.str;
+      description = ''
+        Group under which the postfix exporter shall be run.
+        It should match the group that is allowed to access the
+        <literal>showq</literal> socket in the <literal>queue/public/</literal> directory.
+        Defaults to <literal>services.postfix.setgidGroup</literal> when postfix is enabled.
+      '';
+    };
     telemetryPath = mkOption {
       type = types.str;
       default = "/metrics";
@@ -26,16 +35,20 @@ in
     };
     showqPath = mkOption {
       type = types.path;
-      default = "/var/spool/postfix/public/showq";
-      example = "/var/lib/postfix/queue/public/showq";
+      default = "/var/lib/postfix/queue/public/showq";
+      example = "/var/spool/postfix/public/showq";
       description = ''
-        Path where Postfix places it's showq socket.
+        Path where Postfix places its showq socket.
       '';
     };
     systemd = {
-      enable = mkEnableOption ''
-        reading metrics from the systemd-journal instead of from a logfile
-      '';
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable reading metrics from the systemd journal instead of from a logfile
+        '';
+      };
       unit = mkOption {
         type = types.str;
         default = "postfix.service";
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
index 1ece73a1159..dd3bec8ec16 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix
@@ -30,12 +30,49 @@ in
         Whether to run the exporter as the local 'postgres' super user.
       '';
     };
+
+    # TODO perhaps LoadCredential would be more appropriate
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/root/prometheus-postgres-exporter.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the
+        world-readable Nix store, by specifying placeholder variables as
+        the option value in Nix and setting these variables accordingly in the
+        environment file.
+
+        Environment variables from this file will be interpolated into the
+        config file using envsubst with this syntax:
+        <literal>$ENVIRONMENT ''${VARIABLE}</literal>
+
+        The main use is to set the DATA_SOURCE_NAME that contains the
+        postgres password
+
+        note that contents from this file will override dataSourceName
+        if you have set it from nix.
+
+        <programlisting>
+          # Content of the environment file
+          DATA_SOURCE_NAME=postgresql://username:password@localhost:5432/postgres?sslmode=disable
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        this exporter is running.
+      '';
+    };
+
   };
   serviceOpts = {
     environment.DATA_SOURCE_NAME = cfg.dataSourceName;
     serviceConfig = {
       DynamicUser = false;
       User = mkIf cfg.runAsLocalSuperUser (mkForce "postgres");
+      EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
       ExecStart = ''
         ${pkgs.prometheus-postgres-exporter}/bin/postgres_exporter \
           --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/process.nix b/nixos/modules/services/monitoring/prometheus/exporters/process.nix
new file mode 100644
index 00000000000..e3b3d18367f
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/process.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.process;
+  configFile = pkgs.writeText "process-exporter.yaml" (builtins.toJSON cfg.settings);
+in
+{
+  port = 9256;
+  extraOpts = {
+    settings.process_names = mkOption {
+      type = types.listOf types.anything;
+      default = {};
+      example = literalExample ''
+        {
+          process_names = [
+            # Remove nix store path from process name
+            { name = "{{.Matches.Wrapped}} {{ .Matches.Args }}"; cmdline = [ "^/nix/store[^ ]*/(?P<Wrapped>[^ /]*) (?P<Args>.*)" ]; }
+          ];
+        }
+      '';
+      description = ''
+        All settings expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: <link xlink:href="https://github.com/ncabatoff/process-exporter" />
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      ExecStart = ''
+        ${pkgs.prometheus-process-exporter}/bin/process-exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --config.path ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      NoNewPrivileges = true;
+      ProtectHome = true;
+      ProtectSystem = true;
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectControlGroups = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix
new file mode 100644
index 00000000000..d9ab99221d9
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.py-air-control;
+
+  workingDir = "/var/lib/${cfg.stateDir}";
+
+in
+{
+  port = 9896;
+  extraOpts = {
+    deviceHostname = mkOption {
+      type = types.str;
+      example = "192.168.1.123";
+      description = ''
+        The hostname of the air purification device from which to scrape the metrics.
+      '';
+    };
+    protocol = mkOption {
+      type = types.str;
+      default = "http";
+      description = ''
+        The protocol to use when communicating with the air purification device.
+        Available: [http, coap, plain_coap]
+      '';
+    };
+    stateDir = mkOption {
+      type = types.str;
+      default = "prometheus-py-air-control-exporter";
+      description = ''
+        Directory below <literal>/var/lib</literal> to store runtime data.
+        This directory will be created automatically using systemd's StateDirectory mechanism.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      DynamicUser = false;
+      StateDirectory = cfg.stateDir;
+      WorkingDirectory = workingDir;
+      ExecStart = ''
+        ${pkgs.python3Packages.py-air-control-exporter}/bin/py-air-control-exporter \
+          --host ${cfg.deviceHostname} \
+          --protocol ${cfg.protocol} \
+          --listen-port ${toString cfg.port} \
+          --listen-address ${cfg.listenAddress}
+      '';
+      Environment = [ "HOME=${workingDir}" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
index 1f02ae20724..994670a376e 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
@@ -5,69 +5,58 @@ with lib;
 let
   cfg = config.services.prometheus.exporters.rspamd;
 
-  prettyJSON = conf:
-    pkgs.runCommand "rspamd-exporter-config.yml" { } ''
-      echo '${builtins.toJSON conf}' | ${pkgs.buildPackages.jq}/bin/jq '.' > $out
-    '';
+  mkFile = conf:
+    pkgs.writeText "rspamd-exporter-config.yml" (builtins.toJSON conf);
 
-  generateConfig = extraLabels: (map (path: {
-    name = "rspamd_${replaceStrings [ "." " " ] [ "_" "_" ] path}";
-    path = "$.${path}";
-    labels = extraLabels;
-  }) [
-    "actions.'add header'"
-    "actions.'no action'"
-    "actions.'rewrite subject'"
-    "actions.'soft reject'"
-    "actions.greylist"
-    "actions.reject"
-    "bytes_allocated"
-    "chunks_allocated"
-    "chunks_freed"
-    "chunks_oversized"
-    "connections"
-    "control_connections"
-    "ham_count"
-    "learned"
-    "pools_allocated"
-    "pools_freed"
-    "read_only"
-    "scanned"
-    "shared_chunks_allocated"
-    "spam_count"
-    "total_learns"
-  ]) ++ [{
-    name = "rspamd_statfiles";
-    type = "object";
-    path = "$.statfiles[*]";
-    labels = recursiveUpdate {
-      symbol = "$.symbol";
-      type = "$.type";
-    } extraLabels;
-    values = {
-      revision = "$.revision";
-      size = "$.size";
-      total = "$.total";
-      used = "$.used";
-      languages = "$.languages";
-      users = "$.users";
-    };
-  }];
+  generateConfig = extraLabels: {
+    metrics = (map (path: {
+      name = "rspamd_${replaceStrings [ "[" "." " " "]" "\\" "'" ] [ "_" "_" "_" "" "" "" ] path}";
+      path = "{ .${path} }";
+      labels = extraLabels;
+    }) [
+      "actions['add\\ header']"
+      "actions['no\\ action']"
+      "actions['rewrite\\ subject']"
+      "actions['soft\\ reject']"
+      "actions.greylist"
+      "actions.reject"
+      "bytes_allocated"
+      "chunks_allocated"
+      "chunks_freed"
+      "chunks_oversized"
+      "connections"
+      "control_connections"
+      "ham_count"
+      "learned"
+      "pools_allocated"
+      "pools_freed"
+      "read_only"
+      "scanned"
+      "shared_chunks_allocated"
+      "spam_count"
+      "total_learns"
+    ]) ++ [{
+      name = "rspamd_statfiles";
+      type = "object";
+      path = "{.statfiles[*]}";
+      labels = recursiveUpdate {
+        symbol = "{.symbol}";
+        type = "{.type}";
+      } extraLabels;
+      values = {
+        revision = "{.revision}";
+        size = "{.size}";
+        total = "{.total}";
+        used = "{.used}";
+        languages = "{.languages}";
+        users = "{.users}";
+      };
+    }];
+  };
 in
 {
   port = 7980;
   extraOpts = {
-    listenAddress = {}; # not used
-
-    url = mkOption {
-      type = types.str;
-      description = ''
-        URL to the rspamd metrics endpoint.
-        Defaults to http://localhost:11334/stat when
-        <option>services.rspamd.enable</option> is true.
-      '';
-    };
-
     extraLabels = mkOption {
       type = types.attrsOf types.str;
       default = {
@@ -84,9 +73,25 @@ in
     };
   };
   serviceOpts.serviceConfig.ExecStart = ''
-    ${pkgs.prometheus-json-exporter}/bin/prometheus-json-exporter \
-      --port ${toString cfg.port} \
-      ${cfg.url} ${prettyJSON (generateConfig cfg.extraLabels)} \
+    ${pkgs.prometheus-json-exporter}/bin/json_exporter \
+      --config.file ${mkFile (generateConfig cfg.extraLabels)} \
+      --web.listen-address "${cfg.listenAddress}:${toString cfg.port}" \
       ${concatStringsSep " \\\n  " cfg.extraFlags}
   '';
+
+  imports = [
+    (mkRemovedOptionModule [ "url" ] ''
+      This option was removed. The URL of the rspamd metrics endpoint
+      must now be provided to the exporter by prometheus via the url
+      parameter `target'.
+
+      In prometheus a scrape URL would look like this:
+
+        http://some.rspamd-exporter.host:7980/probe?target=http://some.rspamd.host:11334/stat
+
+      For more information, take a look at the official documentation
+      (https://github.com/prometheus-community/json_exporter) of the json_exporter.
+    '')
+     ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix
new file mode 100644
index 00000000000..01e420db389
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, options }:
+
+let
+  cfg = config.services.prometheus.exporters.rtl_433;
+in
+{
+  port = 9550;
+
+  extraOpts = let
+    mkMatcherOptionType = field: description: with lib.types;
+      listOf (submodule {
+        options = {
+          name = lib.mkOption {
+            type = str;
+            description = "Name to match.";
+          };
+          "${field}" = lib.mkOption {
+            type = int;
+            inherit description;
+          };
+          location = lib.mkOption {
+            type = str;
+            description = "Location to match.";
+          };
+        };
+      });
+  in
+  {
+    rtl433Flags = lib.mkOption {
+      type = lib.types.str;
+      default = "-C si";
+      example = "-C si -R 19";
+      description = ''
+        Flags passed verbatim to rtl_433 binary.
+        Having <literal>-C si</literal> (the default) is recommended since only Celsius temperatures are parsed.
+      '';
+    };
+    channels = lib.mkOption {
+      type = mkMatcherOptionType "channel" "Channel to match.";
+      default = [];
+      example = [
+        { name = "Acurite"; channel = 6543; location = "Kitchen"; }
+      ];
+      description = ''
+        List of channel matchers to export.
+      '';
+    };
+    ids = lib.mkOption {
+      type = mkMatcherOptionType "id" "ID to match.";
+      default = [];
+      example = [
+        { name = "Nexus"; id = 1; location = "Bedroom"; }
+      ];
+      description = ''
+        List of ID matchers to export.
+      '';
+    };
+  };
+
+  serviceOpts = {
+    serviceConfig = {
+      # rtl-sdr udev rules make supported USB devices +rw by plugdev.
+      SupplementaryGroups = "plugdev";
+      ExecStart = let
+        matchers = (map (m:
+          "--channel_matcher '${m.name},${toString m.channel},${m.location}'"
+        ) cfg.channels) ++ (map (m:
+          "--id_matcher '${m.name},${toString m.id},${m.location}'"
+        ) cfg.ids); in ''
+        ${pkgs.prometheus-rtl_433-exporter}/bin/rtl_433_prometheus \
+          -listen ${cfg.listenAddress}:${toString cfg.port} \
+          -subprocess "${pkgs.rtl_433}/bin/rtl_433 -F json ${cfg.rtl433Flags}" \
+          ${lib.concatStringsSep " \\\n  " matchers} \
+          ${lib.concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/script.nix b/nixos/modules/services/monitoring/prometheus/exporters/script.nix
new file mode 100644
index 00000000000..104ab859f2e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/script.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.script;
+  configFile = pkgs.writeText "script-exporter.yaml" (builtins.toJSON cfg.settings);
+in
+{
+  port = 9172;
+  extraOpts = {
+    settings.scripts = mkOption {
+      type = with types; listOf (submodule {
+        options = {
+          name = mkOption {
+            type = str;
+            example = "sleep";
+            description = "Name of the script.";
+          };
+          script = mkOption {
+            type = str;
+            example = "sleep 5";
+            description = "Shell script to execute when metrics are requested.";
+          };
+          timeout = mkOption {
+            type = nullOr int;
+            default = null;
+            example = 60;
+            description = "Optional timeout for the script in seconds.";
+          };
+        };
+      });
+      example = literalExample ''
+        {
+          scripts = [
+            { name = "sleep"; script = "sleep 5"; }
+          ];
+        }
+      '';
+      description = ''
+        All settings expressed as an Nix attrset.
+
+        Check the official documentation for the corresponding YAML
+        settings that can all be used here: <link xlink:href="https://github.com/adhocteam/script_exporter#sample-configuration" />
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-script-exporter}/bin/script_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --config.file ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+      NoNewPrivileges = true;
+      ProtectHome = true;
+      ProtectSystem = "strict";
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectControlGroups = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix
new file mode 100644
index 00000000000..0a7bb9c27be
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.smokeping;
+  goDuration = types.mkOptionType {
+    name = "goDuration";
+    description = "Go duration (https://golang.org/pkg/time/#ParseDuration)";
+    check = x: types.str.check x && builtins.match "(-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+" x != null;
+    inherit (types.str) merge;
+  };
+in
+{
+  port = 9374;
+  extraOpts = {
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+    pingInterval = mkOption {
+      type = goDuration;
+      default = "1s";
+      description = ''
+        Interval between pings.
+      '';
+    };
+    buckets = mkOption {
+      type = types.commas;
+      default = "5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144";
+      description = ''
+        List of buckets to use for the response duration histogram.
+      '';
+    };
+    hosts = mkOption {
+      type = with types; listOf str;
+      description = ''
+        List of endpoints to probe.
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      AmbientCapabilities = [ "CAP_NET_RAW" ];
+      ExecStart = ''
+        ${pkgs.prometheus-smokeping-prober}/bin/smokeping_prober \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          --web.telemetry-path ${cfg.telemetryPath} \
+          --buckets ${cfg.buckets} \
+          --ping.interval ${cfg.pingInterval} \
+          --privileged \
+          ${concatStringsSep " \\\n  " cfg.extraFlags} \
+          ${concatStringsSep " " cfg.hosts}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/sql.nix b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix
new file mode 100644
index 00000000000..d9be724ebc0
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/sql.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, options }:
+with lib;
+let
+  cfg = config.services.prometheus.exporters.sql;
+  cfgOptions = {
+    options = with types; {
+      jobs = mkOption {
+        type = attrsOf (submodule jobOptions);
+        default = { };
+        description = "An attrset of metrics scraping jobs to run.";
+      };
+    };
+  };
+  jobOptions = {
+    options = with types; {
+      interval = mkOption {
+        type = str;
+        description = ''
+          How often to run this job, specified in
+          <link xlink:href="https://golang.org/pkg/time/#ParseDuration">Go duration</link> format.
+        '';
+      };
+      connections = mkOption {
+        type = listOf str;
+        description = "A list of connection strings of the SQL servers to scrape metrics from";
+      };
+      startupSql = mkOption {
+        type = listOf str;
+        default = [];
+        description = "A list of SQL statements to execute once after making a connection.";
+      };
+      queries = mkOption {
+        type = attrsOf (submodule queryOptions);
+        description = "SQL queries to run.";
+      };
+    };
+  };
+  queryOptions = {
+    options = with types; {
+      help = mkOption {
+        type = nullOr str;
+        default = null;
+        description = "A human-readable description of this metric.";
+      };
+      labels = mkOption {
+        type = listOf str;
+        default = [ ];
+        description = "A set of columns that will be used as Prometheus labels.";
+      };
+      query = mkOption {
+        type = str;
+        description = "The SQL query to run.";
+      };
+      values = mkOption {
+        type = listOf str;
+        description = "A set of columns that will be used as values of this metric.";
+      };
+    };
+  };
+
+  configFile =
+    if cfg.configFile != null
+    then cfg.configFile
+    else
+      let
+        nameInline = mapAttrsToList (k: v: v // { name = k; });
+        renameStartupSql = j: removeAttrs (j // { startup_sql = j.startupSql; }) [ "startupSql" ];
+        configuration = {
+          jobs = map renameStartupSql
+            (nameInline (mapAttrs (k: v: (v // { queries = nameInline v.queries; })) cfg.configuration.jobs));
+        };
+      in
+      builtins.toFile "config.yaml" (builtins.toJSON configuration);
+in
+{
+  extraOpts = {
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        Path to configuration file.
+      '';
+    };
+    configuration = mkOption {
+      type = with types; nullOr (submodule cfgOptions);
+      default = null;
+      description = ''
+        Exporter configuration as nix attribute set. Mutually exclusive with 'configFile' option.
+      '';
+    };
+  };
+
+  port = 9237;
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-sql-exporter}/bin/sql_exporter \
+          -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+          -config.file ${configFile} \
+          ${concatStringsSep " \\\n  " cfg.extraFlags}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
new file mode 100644
index 00000000000..0514469b8a6
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix
@@ -0,0 +1,18 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let cfg = config.services.prometheus.exporters.systemd;
+
+in {
+  port = 9558;
+
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-systemd-exporter}/bin/systemd_exporter \
+          --web.listen-address ${cfg.listenAddress}:${toString cfg.port}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
new file mode 100644
index 00000000000..56a559531c1
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix
@@ -0,0 +1,59 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.unbound;
+in
+{
+  port = 9167;
+  extraOpts = {
+    fetchType = mkOption {
+      # TODO: add shm when upstream implemented it
+      type = types.enum [ "tcp" "uds" ];
+      default = "uds";
+      description = ''
+        Which methods the exporter uses to get the information from unbound.
+      '';
+    };
+
+    telemetryPath = mkOption {
+      type = types.str;
+      default = "/metrics";
+      description = ''
+        Path under which to expose metrics.
+      '';
+    };
+
+    controlInterface = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "/run/unbound/unbound.socket";
+      description = ''
+        Path to the unbound socket for uds mode or the control interface port for tcp mode.
+
+        Example:
+          uds-mode: /run/unbound/unbound.socket
+          tcp-mode: 127.0.0.1:8953
+      '';
+    };
+  };
+
+  serviceOpts = mkMerge ([{
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-unbound-exporter}/bin/unbound-telemetry \
+          ${cfg.fetchType} \
+          --bind ${cfg.listenAddress}:${toString cfg.port} \
+          --path ${cfg.telemetryPath} \
+          ${optionalString (cfg.controlInterface != null) "--control-interface ${cfg.controlInterface}"} \
+          ${toString cfg.extraFlags}
+      '';
+    };
+  }] ++ [
+    (mkIf config.services.unbound.enable {
+      after = [ "unbound.service" ];
+      requires = [ "unbound.service" ];
+    })
+  ]);
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
new file mode 100644
index 00000000000..394e6e201f0
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.unifi-poller;
+
+  configFile = pkgs.writeText "prometheus-unifi-poller-exporter.json" (generators.toJSON {} {
+    poller = { inherit (cfg.log) debug quiet; };
+    unifi = { inherit (cfg) controllers; };
+    influxdb.disable = true;
+    prometheus = {
+      http_listen = "${cfg.listenAddress}:${toString cfg.port}";
+      report_errors = cfg.log.prometheusErrors;
+    };
+  });
+
+in {
+  port = 9130;
+
+  extraOpts = {
+    inherit (options.services.unifi-poller.unifi) controllers;
+    log = {
+      debug = mkEnableOption "debug logging including line numbers, high resolution timestamps, per-device logs.";
+      quiet = mkEnableOption "startup and error logs only.";
+      prometheusErrors = mkEnableOption "emitting errors to prometheus.";
+    };
+  };
+
+  serviceOpts.serviceConfig = {
+    ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}";
+    DynamicUser = false;
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
index 44b15cb2034..980c93c9c47 100644
--- a/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
+++ b/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix
@@ -4,21 +4,29 @@ with lib;
 
 let
   cfg = config.services.prometheus.xmpp-alerts;
-
-  configFile = pkgs.writeText "prometheus-xmpp-alerts.yml" (builtins.toJSON cfg.configuration);
-
+  settingsFormat = pkgs.formats.yaml {};
+  configFile = settingsFormat.generate "prometheus-xmpp-alerts.yml" cfg.settings;
 in
-
 {
-  options.services.prometheus.xmpp-alerts = {
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "prometheus" "xmpp-alerts" "configuration" ]
+      [ "services" "prometheus" "xmpp-alerts" "settings" ])
+  ];
 
+  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";
-    };
+    settings = mkOption {
+      type = settingsFormat.type;
+      default = {};
 
+      description = ''
+        Configuration for prometheus xmpp-alerts, see
+        <link xlink:href="https://github.com/jelmer/prometheus-xmpp-alerts/blob/master/xmpp-alerts.yml.example"/>
+        for supported values.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/services/monitoring/scollector.nix b/nixos/modules/services/monitoring/scollector.nix
index 6f13ce889cb..ef535585e9b 100644
--- a/nixos/modules/services/monitoring/scollector.nix
+++ b/nixos/modules/services/monitoring/scollector.nix
@@ -113,7 +113,7 @@ in {
       description = "scollector metrics collector (part of Bosun)";
       wantedBy = [ "multi-user.target" ];
 
-      path = [ pkgs.coreutils pkgs.iproute ];
+      path = [ pkgs.coreutils pkgs.iproute2 ];
 
       serviceConfig = {
         User = cfg.user;
diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix
index c72b4abfcdc..3ea25437114 100644
--- a/nixos/modules/services/monitoring/smartd.nix
+++ b/nixos/modules/services/monitoring/smartd.nix
@@ -36,7 +36,7 @@ let
 
       $SMARTD_MESSAGE
       EOF
-      } | ${pkgs.utillinux}/bin/wall 2>/dev/null
+      } | ${pkgs.util-linux}/bin/wall 2>/dev/null
     ''}
     ${optionalString nx.enable ''
       export DISPLAY=${nx.display}
diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix
index 8d781d82d08..ce9e57a187c 100644
--- a/nixos/modules/services/monitoring/teamviewer.nix
+++ b/nixos/modules/services/monitoring/teamviewer.nix
@@ -31,14 +31,14 @@ in
       after = [ "NetworkManager-wait-online.service" "network.target" ];
       preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer";
 
+      startLimitIntervalSec = 60;
+      startLimitBurst = 10;
       serviceConfig = {
         Type = "forking";
         ExecStart = "${pkgs.teamviewer}/bin/teamviewerd -d";
         PIDFile = "/run/teamviewerd.pid";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "on-abort";
-        StartLimitInterval = "60";
-        StartLimitBurst = "10";
       };
     };
   };
diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix
index 5d131557e8b..4046260c164 100644
--- a/nixos/modules/services/monitoring/telegraf.nix
+++ b/nixos/modules/services/monitoring/telegraf.nix
@@ -5,14 +5,8 @@ with lib;
 let
   cfg = config.services.telegraf;
 
-  configFile = pkgs.runCommand "config.toml" {
-    buildInputs = [ pkgs.remarshal ];
-    preferLocalBuild = true;
-  } ''
-    remarshal -if json -of toml \
-      < ${pkgs.writeText "config.json" (builtins.toJSON cfg.extraConfig)} \
-      > $out
-  '';
+  settingsFormat = pkgs.formats.toml {};
+  configFile = settingsFormat.generate "config.toml" cfg.extraConfig;
 in {
   ###### interface
   options = {
@@ -26,22 +20,30 @@ in {
         type = types.package;
       };
 
+      environmentFiles = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        example = "/run/keys/telegraf.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</literal> or <literal>''${VARIABLE}</literal>.
+          This is useful to avoid putting secrets into the nix store.
+        '';
+      };
+
       extraConfig = mkOption {
         default = {};
         description = "Extra configuration options for telegraf";
-        type = types.attrs;
+        type = settingsFormat.type;
         example = {
-          outputs = {
-            influxdb = {
-              urls = ["http://localhost:8086"];
-              database = "telegraf";
-            };
+          outputs.influxdb = {
+            urls = ["http://localhost:8086"];
+            database = "telegraf";
           };
-          inputs = {
-            statsd = {
-              service_address = ":8125";
-              delete_timings = true;
-            };
+          inputs.statsd = {
+            service_address = ":8125";
+            delete_timings = true;
           };
         };
       };
@@ -51,21 +53,38 @@ in {
 
   ###### implementation
   config = mkIf config.services.telegraf.enable {
-    systemd.services.telegraf = {
+    systemd.services.telegraf = let
+      finalConfigFile = if config.services.telegraf.environmentFiles == []
+                        then configFile
+                        else "/var/run/telegraf/config.toml";
+    in {
       description = "Telegraf Agent";
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
-        ExecStart=''${cfg.package}/bin/telegraf -config "${configFile}"'';
+        EnvironmentFile = config.services.telegraf.environmentFiles;
+        ExecStartPre = lib.optional (config.services.telegraf.environmentFiles != [])
+          (pkgs.writeShellScript "pre-start" ''
+            umask 077
+            ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /var/run/telegraf/config.toml
+          '');
+        ExecStart="${cfg.package}/bin/telegraf -config ${finalConfigFile}";
         ExecReload="${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        RuntimeDirectory = "telegraf";
         User = "telegraf";
+        Group = "telegraf";
         Restart = "on-failure";
+        # for ping probes
+        AmbientCapabilities = [ "CAP_NET_RAW" ];
       };
     };
 
     users.users.telegraf = {
       uid = config.ids.uids.telegraf;
+      group = "telegraf";
       description = "telegraf daemon user";
     };
+
+    users.groups.telegraf = {};
   };
 }
diff --git a/nixos/modules/services/monitoring/thanos.nix b/nixos/modules/services/monitoring/thanos.nix
index 52dab28cf72..474ea4b2505 100644
--- a/nixos/modules/services/monitoring/thanos.nix
+++ b/nixos/modules/services/monitoring/thanos.nix
@@ -12,7 +12,7 @@ let
   };
 
   optionToArgs = opt: v  : optional (v != null)  ''--${opt}="${toString v}"'';
-  flagToArgs   = opt: v  : optional v            ''--${opt}'';
+  flagToArgs   = opt: v  : optional v            "--${opt}";
   listToArgs   = opt: vs : map               (v: ''--${opt}="${v}"'') vs;
   attrsToArgs  = opt: kvs: mapAttrsToList (k: v: ''--${opt}=${k}=\"${v}\"'') kvs;
 
@@ -67,7 +67,7 @@ let
     preferLocalBuild = true;
     json = builtins.toFile "${name}.json" (builtins.toJSON attrs);
     nativeBuildInputs = [ pkgs.remarshal ];
-  } ''json2yaml -i $json -o $out'';
+  } "json2yaml -i $json -o $out";
 
   thanos = cmd: "${cfg.package}/bin/thanos ${cmd}" +
     (let args = cfg.${cmd}.arguments;
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
index 8f79d916599..17c5c1f56ea 100644
--- a/nixos/modules/services/monitoring/tuptime.nix
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -34,7 +34,10 @@ in {
 
     users = {
       groups._tuptime.members = [ "_tuptime" ];
-      users._tuptime.description = "tuptime database owner";
+      users._tuptime = {
+        isSystemUser = true;
+        description = "tuptime database owner";
+      };
     };
 
     systemd = {
diff --git a/nixos/modules/services/monitoring/unifi-poller.nix b/nixos/modules/services/monitoring/unifi-poller.nix
new file mode 100644
index 00000000000..208f5e4875b
--- /dev/null
+++ b/nixos/modules/services/monitoring/unifi-poller.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.unifi-poller;
+
+  configFile = pkgs.writeText "unifi-poller.json" (generators.toJSON {} {
+    inherit (cfg) poller influxdb prometheus unifi;
+  });
+
+in {
+  options.services.unifi-poller = {
+    enable = mkEnableOption "unifi-poller";
+
+    poller = {
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Turns on line numbers, microsecond logging, and a per-device log.
+          This may be noisy if you have a lot of devices. It adds one line per device.
+        '';
+      };
+      quiet = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Turns off per-interval logs. Only startup and error logs will be emitted.
+        '';
+      };
+      plugins = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = ''
+          Load additional plugins.
+        '';
+      };
+    };
+
+    prometheus = {
+      disable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to disable the prometheus ouput plugin.
+        '';
+      };
+      http_listen = mkOption {
+        type = types.str;
+        default = "[::]:9130";
+        description = ''
+          Bind the prometheus exporter to this IP or hostname.
+        '';
+      };
+      report_errors = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to report errors.
+        '';
+      };
+    };
+
+    influxdb = {
+      disable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to disable the influxdb ouput plugin.
+        '';
+      };
+      url = mkOption {
+        type = types.str;
+        default = "http://127.0.0.1:8086";
+        description = ''
+          URL of the influxdb host.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        default = "unifipoller";
+        description = ''
+          Username for the influxdb.
+        '';
+      };
+      pass = mkOption {
+        type = types.path;
+        default = pkgs.writeText "unifi-poller-influxdb-default.password" "unifipoller";
+        defaultText = "unifi-poller-influxdb-default.password";
+        description = ''
+          Path of a file containing the password for influxdb.
+          This file needs to be readable by the unifi-poller user.
+        '';
+        apply = v: "file://${v}";
+      };
+      db = mkOption {
+        type = types.str;
+        default = "unifi";
+        description = ''
+          Database name. Database should exist.
+        '';
+      };
+      verify_ssl = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Verify the influxdb's certificate.
+        '';
+      };
+      interval = mkOption {
+        type = types.str;
+        default = "30s";
+        description = ''
+          Setting this lower than the Unifi controller's refresh
+          interval may lead to zeroes in your database.
+        '';
+      };
+    };
+
+    unifi = let
+      controllerOptions = {
+        user = mkOption {
+          type = types.str;
+          default = "unifi";
+          description = ''
+            Unifi service user name.
+          '';
+        };
+        pass = mkOption {
+          type = types.path;
+          default = pkgs.writeText "unifi-poller-unifi-default.password" "unifi";
+          defaultText = "unifi-poller-unifi-default.password";
+          description = ''
+            Path of a file containing the password for the unifi service user.
+            This file needs to be readable by the unifi-poller user.
+          '';
+          apply = v: "file://${v}";
+        };
+        url = mkOption {
+          type = types.str;
+          default = "https://unifi:8443";
+          description = ''
+            URL of the Unifi controller.
+          '';
+        };
+        sites = mkOption {
+          type = with types; either (enum [ "default" "all" ]) (listOf str);
+          default = "all";
+          description = ''
+            List of site names for which statistics should be exported.
+            Or the string "default" for the default site or the string "all" for all sites.
+          '';
+          apply = toList;
+        };
+        save_ids = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from the intrusion detection system to influxdb.
+          '';
+        };
+        save_dpi = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Collect and save data from deep packet inspection.
+            Adds around 150 data points and impacts performance.
+          '';
+        };
+        save_sites = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Collect and save site data.
+          '';
+        };
+        hash_pii = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Hash, with md5, client names and MAC addresses. This attempts
+            to protect personally identifiable information.
+          '';
+        };
+        verify_ssl = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Verify the Unifi controller's certificate.
+          '';
+        };
+      };
+
+    in {
+      dynamic = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Let prometheus select which controller to poll when scraping.
+          Use with default credentials. See unifi-poller wiki for more.
+        '';
+      };
+
+      defaults = controllerOptions;
+
+      controllers = mkOption {
+        type = with types; listOf (submodule { options = controllerOptions; });
+        default = [];
+        description = ''
+          List of Unifi controllers to poll. Use defaults if empty.
+        '';
+        apply = map (flip removeAttrs [ "_module" ]);
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.unifi-poller = { };
+    users.users.unifi-poller = {
+      description = "unifi-poller Service User";
+      group = "unifi-poller";
+      isSystemUser = true;
+    };
+
+    systemd.services.unifi-poller = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}";
+        Restart = "always";
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        DevicePolicy = "closed";
+        NoNewPrivileges = true;
+        User = "unifi-poller";
+        WorkingDirectory = "/tmp";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/ups.nix b/nixos/modules/services/monitoring/ups.nix
index a45e806d4ad..ae5097c5442 100644
--- a/nixos/modules/services/monitoring/ups.nix
+++ b/nixos/modules/services/monitoring/ups.nix
@@ -205,7 +205,7 @@ in
       after = [ "upsd.service" ];
       wantedBy = [ "multi-user.target" ];
       # TODO: replace 'root' by another username.
-      script = ''${pkgs.nut}/bin/upsdrvctl -u root start'';
+      script = "${pkgs.nut}/bin/upsdrvctl -u root start";
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
diff --git a/nixos/modules/services/monitoring/vnstat.nix b/nixos/modules/services/monitoring/vnstat.nix
index e9bedb704a4..5e19c399568 100644
--- a/nixos/modules/services/monitoring/vnstat.nix
+++ b/nixos/modules/services/monitoring/vnstat.nix
@@ -6,21 +6,21 @@ let
   cfg = config.services.vnstat;
 in {
   options.services.vnstat = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        Whether to enable update of network usage statistics via vnstatd.
-      '';
-    };
+    enable = mkEnableOption "update of network usage statistics via vnstatd";
   };
 
   config = mkIf cfg.enable {
-    users.users.vnstatd = {
-      isSystemUser = true;
-      description = "vnstat daemon user";
-      home = "/var/lib/vnstat";
-      createHome = true;
+
+    environment.systemPackages = [ pkgs.vnstat ];
+
+    users = {
+      groups.vnstatd = {};
+
+      users.vnstatd = {
+        isSystemUser = true;
+        group = "vnstatd";
+        description = "vnstat daemon user";
+      };
     };
 
     systemd.services.vnstat = {
@@ -33,7 +33,6 @@ in {
         "man:vnstat(1)"
         "man:vnstat.conf(5)"
       ];
-      preStart = "chmod 755 /var/lib/vnstat";
       serviceConfig = {
         ExecStart = "${pkgs.vnstat}/bin/vnstatd -n";
         ExecReload = "${pkgs.procps}/bin/kill -HUP $MAINPID";
@@ -52,7 +51,10 @@ in {
         RestrictNamespaces = true;
 
         User = "vnstatd";
+        Group = "vnstatd";
       };
     };
   };
+
+  meta.maintainers = [ maintainers.evils ];
 }
diff --git a/nixos/modules/services/monitoring/zabbix-agent.nix b/nixos/modules/services/monitoring/zabbix-agent.nix
index 73eed7aa66a..7eb6449e384 100644
--- a/nixos/modules/services/monitoring/zabbix-agent.nix
+++ b/nixos/modules/services/monitoring/zabbix-agent.nix
@@ -128,11 +128,16 @@ in
       {
         LogType = "console";
         Server = cfg.server;
-        ListenIP = cfg.listen.ip;
         ListenPort = cfg.listen.port;
-        LoadModule = builtins.attrNames cfg.modules;
       }
-      (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; })
+      (mkIf (cfg.modules != {}) {
+        LoadModule = builtins.attrNames cfg.modules;
+        LoadModulePath = "${moduleEnv}/lib";
+      })
+
+      # the default value for "ListenIP" is 0.0.0.0 but zabbix agent 2 cannot accept configuration files which
+      # explicitly set "ListenIP" to the default value...
+      (mkIf (cfg.listen.ip != "0.0.0.0") { ListenIP = cfg.listen.ip; })
     ];
 
     networking.firewall = mkIf cfg.openFirewall {
@@ -152,7 +157,10 @@ in
 
       wantedBy = [ "multi-user.target" ];
 
-      path = [ "/run/wrappers" ] ++ cfg.extraPackages;
+      # https://www.zabbix.com/documentation/current/manual/config/items/userparameters
+      # > User parameters are commands executed by Zabbix agent.
+      # > /bin/sh is used as a command line interpreter under UNIX operating systems.
+      path = with pkgs; [ bash "/run/wrappers" ] ++ cfg.extraPackages;
 
       serviceConfig = {
         ExecStart = "@${cfg.package}/sbin/zabbix_agentd zabbix_agentd -f --config ${configFile}";
diff --git a/nixos/modules/services/network-filesystems/cachefilesd.nix b/nixos/modules/services/network-filesystems/cachefilesd.nix
index 61981340840..229c9665419 100644
--- a/nixos/modules/services/network-filesystems/cachefilesd.nix
+++ b/nixos/modules/services/network-filesystems/cachefilesd.nix
@@ -43,17 +43,21 @@ in
 
   config = mkIf cfg.enable {
 
+    boot.kernelModules = [ "cachefiles" ];
+
     systemd.services.cachefilesd = {
       description = "Local network file caching management daemon";
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.kmod pkgs.cachefilesd ];
-      script = ''
-        modprobe -qab cachefiles
-        mkdir -p ${cfg.cacheDir}
-        chmod 700 ${cfg.cacheDir}
-        exec cachefilesd -n -f ${cfgFile}
-      '';
+      serviceConfig = {
+        Type = "exec";
+        ExecStart = "${pkgs.cachefilesd}/bin/cachefilesd -n -f ${cfgFile}";
+        Restart = "on-failure";
+        PrivateTmp = true;
+      };
     };
 
+    systemd.tmpfiles.rules = [
+      "d ${cfg.cacheDir} 0700 root root - -"
+    ];
   };
 }
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index d17959a6a30..d833062c473 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -28,6 +28,9 @@ let
 
     # Don't start services that are not yet initialized
     unitConfig.ConditionPathExists = "/var/lib/${stateDirectory}/keyring";
+    startLimitBurst =
+      if daemonType == "osd" then 30 else if lib.elem daemonType ["mgr" "mds"] then 3 else 5;
+    startLimitIntervalSec = 60 * 30;  # 30 mins
 
     serviceConfig = {
       LimitNOFILE = 1048576;
@@ -39,22 +42,17 @@ let
       ProtectHome = "true";
       ProtectSystem = "full";
       Restart = "on-failure";
-      StartLimitBurst = "5";
-      StartLimitInterval = "30min";
       StateDirectory = stateDirectory;
       User = "ceph";
       Group = if daemonType == "osd" then "disk" else "ceph";
       ExecStart = ''${ceph.out}/bin/${if daemonType == "rgw" then "radosgw" else "ceph-${daemonType}"} \
                     -f --cluster ${clusterName} --id ${daemonId}'';
     } // optionalAttrs (daemonType == "osd") {
-      ExecStartPre = ''${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}'';
-      StartLimitBurst = "30";
+      ExecStartPre = "${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}";
       RestartSec = "20s";
       PrivateDevices = "no"; # osd needs disk access
     } // optionalAttrs ( daemonType == "mon") {
       RestartSec = "10";
-    } // optionalAttrs (lib.elem daemonType ["mgr" "mds"]) {
-      StartLimitBurst = "3";
     };
   });
 
@@ -318,7 +316,7 @@ in
     client = {
       enable = mkEnableOption "Ceph client configuration";
       extraConfig = mkOption {
-        type = with types; attrsOf str;
+        type = with types; attrsOf (attrsOf str);
         default = {};
         example = ''
           {
@@ -355,7 +353,7 @@ in
     ];
 
     warnings = optional (cfg.global.monInitialMembers == null)
-      ''Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function'';
+      "Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function";
 
     environment.etc."ceph/ceph.conf".text = let
       # Merge the extraConfig set for mgr daemons, as mgr don't have their own section
diff --git a/nixos/modules/services/network-filesystems/davfs2.nix b/nixos/modules/services/network-filesystems/davfs2.nix
index 4b6f85e4a2c..8cf314fe63a 100644
--- a/nixos/modules/services/network-filesystems/davfs2.nix
+++ b/nixos/modules/services/network-filesystems/davfs2.nix
@@ -70,6 +70,24 @@ in
       };
     };
 
+    security.wrappers."mount.davfs" = {
+      program = "mount.davfs";
+      source = "${pkgs.davfs2}/bin/mount.davfs";
+      owner = "root";
+      group = cfg.davGroup;
+      setuid = true;
+      permissions = "u+rx,g+x";
+    };
+
+    security.wrappers."umount.davfs" = {
+      program = "umount.davfs";
+      source = "${pkgs.davfs2}/bin/umount.davfs";
+      owner = "root";
+      group = cfg.davGroup;
+      setuid = true;
+      permissions = "u+rx,g+x";
+    };
+
   };
 
 }
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index f298f831fa7..2748571be1f 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -44,6 +44,13 @@ in {
 
       enable = mkEnableOption "Interplanetary File System (WARNING: may cause severe network degredation)";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.ipfs;
+        defaultText = "pkgs.ipfs";
+        description = "Which IPFS package to use.";
+      };
+
       user = mkOption {
         type = types.str;
         default = "ipfs";
@@ -176,7 +183,7 @@ in {
   ###### implementation
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.ipfs ];
+    environment.systemPackages = [ cfg.package ];
     environment.variables.IPFS_PATH = cfg.dataDir;
 
     programs.fuse = mkIf cfg.autoMount {
@@ -207,16 +214,13 @@ in {
       "d '${cfg.ipnsMountDir}' - ${cfg.user} ${cfg.group} - -"
     ];
 
-    systemd.packages = [ pkgs.ipfs ];
-
-    systemd.services.ipfs-init = {
-      description = "IPFS Initializer";
+    systemd.packages = [ cfg.package ];
 
+    systemd.services.ipfs = {
+      path = [ "/run/wrappers" cfg.package ];
       environment.IPFS_PATH = cfg.dataDir;
 
-      path = [ pkgs.ipfs ];
-
-      script = ''
+      preStart = ''
         if [[ ! -f ${cfg.dataDir}/config ]]; then
           ipfs init ${optionalString cfg.emptyRepo "-e"} \
             ${optionalString (! cfg.localDiscovery) "--profile=server"}
@@ -226,29 +230,10 @@ in {
             else "ipfs config profile apply server"
           }
         fi
-      '';
-
-      wantedBy = [ "default.target" ];
-
-      serviceConfig = {
-        Type = "oneshot";
-        RemainAfterExit = true;
-        User = cfg.user;
-        Group = cfg.group;
-      };
-    };
-
-    systemd.services.ipfs = {
-      path = [ "/run/wrappers" pkgs.ipfs ];
-      environment.IPFS_PATH = cfg.dataDir;
-
-      wants = [ "ipfs-init.service" ];
-      after = [ "ipfs-init.service" ];
-
-      preStart = optionalString cfg.autoMount ''
-        ipfs --local config Mounts.FuseAllowOther --json true
-        ipfs --local config Mounts.IPFS ${cfg.ipfsMountDir}
-        ipfs --local config Mounts.IPNS ${cfg.ipnsMountDir}
+      '' + optionalString cfg.autoMount ''
+        ipfs --offline config Mounts.FuseAllowOther --json true
+        ipfs --offline config Mounts.IPFS ${cfg.ipfsMountDir}
+        ipfs --offline config Mounts.IPNS ${cfg.ipnsMountDir}
       '' + concatStringsSep "\n" (collect
             isString
             (mapAttrsRecursive
@@ -258,7 +243,7 @@ in {
                 read value <<EOF
                 ${builtins.toJSON value}
                 EOF
-                ipfs --local config --json "${concatStringsSep "." path}" "$value"
+                ipfs --offline config --json "${concatStringsSep "." path}" "$value"
               '')
               ({ Addresses.API = cfg.apiAddress;
                  Addresses.Gateway = cfg.gatewayAddress;
@@ -267,7 +252,7 @@ in {
               cfg.extraConfig))
           );
       serviceConfig = {
-        ExecStart = ["" "${pkgs.ipfs}/bin/ipfs daemon ${ipfsFlags}"];
+        ExecStart = ["" "${cfg.package}/bin/ipfs daemon ${ipfsFlags}"];
         User = cfg.user;
         Group = cfg.group;
       } // optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
@@ -289,7 +274,7 @@ in {
 
     systemd.sockets.ipfs-api = {
       wantedBy = [ "sockets.target" ];
-      # We also include "%t/ipfs.sock" because tere is no way to put the "%t"
+      # We also include "%t/ipfs.sock" because there is no way to put the "%t"
       # in the multiaddr.
       socketConfig.ListenStream = let
           fromCfg = multiaddrToListenStream cfg.apiAddress;
diff --git a/nixos/modules/services/network-filesystems/netatalk.nix b/nixos/modules/services/network-filesystems/netatalk.nix
index 7674c8f7fa8..06a36eb30c2 100644
--- a/nixos/modules/services/network-filesystems/netatalk.nix
+++ b/nixos/modules/services/network-filesystems/netatalk.nix
@@ -3,126 +3,74 @@
 with lib;
 
 let
-
   cfg = config.services.netatalk;
-
-  extmapFile = pkgs.writeText "extmap.conf" cfg.extmap;
-
-  afpToString = x: if builtins.typeOf x == "bool"
-                   then boolToString x
-                   else toString x;
-
-  volumeConfig = name:
-    let vol = getAttr name cfg.volumes; in
-    "[${name}]\n " + (toString (
-       map
-         (key: "${key} = ${afpToString (getAttr key vol)}\n")
-         (attrNames vol)
-    ));
-
-  afpConf = ''[Global]
-    extmap file = ${extmapFile}
-    afp port = ${toString cfg.port}
-
-    ${cfg.extraConfig}
-
-    ${if cfg.homes.enable then ''[Homes]
-    ${optionalString (cfg.homes.path != "") "path = ${cfg.homes.path}"}
-    basedir regex = ${cfg.homes.basedirRegex}
-    ${cfg.homes.extraConfig}
-    '' else ""}
-
-     ${toString (map volumeConfig (attrNames cfg.volumes))}
-  '';
-
-  afpConfFile = pkgs.writeText "afp.conf" afpConf;
-
-in
-
-{
+  settingsFormat = pkgs.formats.ini { };
+  afpConfFile = settingsFormat.generate "afp.conf" cfg.settings;
+in {
   options = {
     services.netatalk = {
 
       enable = mkEnableOption "the Netatalk AFP fileserver";
 
       port = mkOption {
+        type = types.port;
         default = 548;
         description = "TCP port to be used for AFP.";
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        example = "uam list = uams_guest.so";
-        description = ''
-          Lines of configuration to add to the <literal>[Global]</literal> section.
-          See <literal>man apf.conf</literal> for more information.
-        '';
-      };
-
-      homes = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = "Enable sharing of the UNIX server user home directories.";
-        };
-
-        path = mkOption {
-          default = "";
-          example = "afp-data";
-          description = "Share not the whole user home but this subdirectory path.";
-        };
-
-        basedirRegex = mkOption {
-          example = "/home";
-          description = "Regex which matches the parent directory of the user homes.";
-        };
-
-        extraConfig = mkOption {
-          type = types.lines;
-          default = "";
-          description = ''
-            Lines of configuration to add to the <literal>[Homes]</literal> section.
-            See <literal>man apf.conf</literal> for more information.
-          '';
-         };
-      };
-
-      volumes = mkOption {
+      settings = mkOption {
+        inherit (settingsFormat) type;
         default = { };
-        type = types.attrsOf (types.attrsOf types.unspecified);
-        description =
-          ''
-            Set of AFP volumes to export.
-            See <literal>man apf.conf</literal> for more information.
-          '';
-        example = literalExample ''
-          { srv =
-             { path = "/srv";
-               "read only" = true;
-               "hosts allow" = "10.1.0.0/16 10.2.1.100 2001:0db8:1234::/48";
-             };
-          }
+        example = {
+          Global = { "uam list" = "uams_guest.so"; };
+          Homes = {
+            path = "afp-data";
+            "basedir regex" = "/home";
+          };
+          example-volume = {
+            path = "/srv/volume";
+            "read only" = true;
+          };
+        };
+        description = ''
+          Configuration for Netatalk. See
+          <citerefentry><refentrytitle>afp.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
         '';
       };
 
       extmap = mkOption {
         type = types.lines;
-	default = "";
-	description = ''
-	  File name extension mappings.
-	  See <literal>man extmap.conf</literal> for more information.
+        default = "";
+        description = ''
+          File name extension mappings.
+          See <citerefentry><refentrytitle>extmap.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>. for more information.
         '';
       };
 
     };
   };
 
+  imports = (map (option:
+    mkRemovedOptionModule [ "services" "netatalk" option ]
+    "This option was removed in favor of `services.netatalk.settings`.") [
+      "extraConfig"
+      "homes"
+      "volumes"
+    ]);
+
   config = mkIf cfg.enable {
 
+    services.netatalk.settings.Global = {
+      "afp port" = toString cfg.port;
+      "extmap file" = "${pkgs.writeText "extmap.conf" cfg.extmap}";
+    };
+
     systemd.services.netatalk = {
       description = "Netatalk AFP fileserver for Macintosh clients";
-      unitConfig.Documentation = "man:afp.conf(5) man:netatalk(8) man:afpd(8) man:cnid_metad(8) man:cnid_dbd(8)";
+      unitConfig.Documentation =
+        "man:afp.conf(5) man:netatalk(8) man:afpd(8) man:cnid_metad(8) man:cnid_dbd(8)";
       after = [ "network.target" "avahi-daemon.service" ];
       wantedBy = [ "multi-user.target" ];
 
@@ -132,12 +80,12 @@ in
         Type = "forking";
         GuessMainPID = "no";
         PIDFile = "/run/lock/netatalk";
-	ExecStartPre = "${pkgs.coreutils}/bin/mkdir -m 0755 -p /var/lib/netatalk/CNID";
-        ExecStart  = "${pkgs.netatalk}/sbin/netatalk -F ${afpConfFile}";
+        ExecStart = "${pkgs.netatalk}/sbin/netatalk -F ${afpConfFile}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP  $MAINPID";
-	ExecStop   = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
         Restart = "always";
         RestartSec = 1;
+        StateDirectory = [ "netatalk/CNID" ];
       };
 
     };
diff --git a/nixos/modules/services/network-filesystems/openafs/client.nix b/nixos/modules/services/network-filesystems/openafs/client.nix
index 677111814a0..03884cb7297 100644
--- a/nixos/modules/services/network-filesystems/openafs/client.nix
+++ b/nixos/modules/services/network-filesystems/openafs/client.nix
@@ -244,7 +244,7 @@ in
       # postStop, then we get a hang + kernel oops, because AFS can't be
       # stopped simply by sending signals to processes.
       preStop = ''
-        ${pkgs.utillinux}/bin/umount ${cfg.mountPoint}
+        ${pkgs.util-linux}/bin/umount ${cfg.mountPoint}
         ${openafsBin}/sbin/afsd -shutdown
         ${pkgs.kmod}/sbin/rmmod libafs
       '';
diff --git a/nixos/modules/services/network-filesystems/openafs/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
index 095024d2c8a..4fce650b013 100644
--- a/nixos/modules/services/network-filesystems/openafs/server.nix
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -61,6 +61,7 @@ in {
       };
 
       advertisedAddresses = mkOption {
+        type = types.listOf types.str;
         default = [];
         description = "List of IP addresses this server is advertised under. See NetInfo(5)";
       };
@@ -251,7 +252,6 @@ in {
         wantedBy = [ "multi-user.target" ];
         restartIfChanged = false;
         unitConfig.ConditionPathExists = [
-          "|/etc/openafs/server/rxkad.keytab"
           "|/etc/openafs/server/KeyFileExt"
         ];
         preStart = ''
diff --git a/nixos/modules/services/network-filesystems/orangefs/server.nix b/nixos/modules/services/network-filesystems/orangefs/server.nix
index 74ebdc13402..8eb754fe611 100644
--- a/nixos/modules/services/network-filesystems/orangefs/server.nix
+++ b/nixos/modules/services/network-filesystems/orangefs/server.nix
@@ -83,14 +83,14 @@ in {
       };
 
       dataStorageSpace = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         default = null;
         example = "/data/storage";
         description = "Directory for data storage.";
       };
 
       metadataStorageSpace = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         default = null;
         example = "/data/meta";
         description = "Directory for meta data storage.";
diff --git a/nixos/modules/services/network-filesystems/rsyncd.nix b/nixos/modules/services/network-filesystems/rsyncd.nix
index fa29e18a939..edac86eb0e3 100644
--- a/nixos/modules/services/network-filesystems/rsyncd.nix
+++ b/nixos/modules/services/network-filesystems/rsyncd.nix
@@ -3,120 +3,126 @@
 with lib;
 
 let
-
   cfg = config.services.rsyncd;
-
-  motdFile = builtins.toFile "rsyncd-motd" cfg.motd;
-
-  foreach = attrs: f:
-    concatStringsSep "\n" (mapAttrsToList f attrs);
-
-  cfgFile = ''
-    ${optionalString (cfg.motd != "") "motd file = ${motdFile}"}
-    ${optionalString (cfg.address != "") "address = ${cfg.address}"}
-    ${optionalString (cfg.port != 873) "port = ${toString cfg.port}"}
-    ${cfg.extraConfig}
-    ${foreach cfg.modules (name: module: ''
-      [${name}]
-      ${foreach module (k: v:
-        "${k} = ${v}"
-      )}
-    '')}
-  '';
-in
-
-{
+  settingsFormat = pkgs.formats.ini { };
+  configFile = settingsFormat.generate "rsyncd.conf" cfg.settings;
+in {
   options = {
     services.rsyncd = {
 
       enable = mkEnableOption "the rsync daemon";
 
-      motd = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Message of the day to display to clients on each connect.
-          This usually contains site information and any legal notices.
-        '';
-      };
-
       port = mkOption {
         default = 873;
-        type = types.int;
+        type = types.port;
         description = "TCP port the daemon will listen on.";
       };
 
-      address = mkOption {
-        default = "";
-        example = "192.168.1.2";
+      settings = mkOption {
+        inherit (settingsFormat) type;
+        default = { };
+        example = {
+          global = {
+            uid = "nobody";
+            gid = "nobody";
+            "use chroot" = true;
+            "max connections" = 4;
+          };
+          ftp = {
+            path = "/var/ftp/./pub";
+            comment = "whole ftp area";
+          };
+          cvs = {
+            path = "/data/cvs";
+            comment = "CVS repository (requires authentication)";
+            "auth users" = [ "tridge" "susan" ];
+            "secrets file" = "/etc/rsyncd.secrets";
+          };
+        };
         description = ''
-          IP address the daemon will listen on; rsyncd will listen on
-          all addresses if this is not specified.
+          Configuration for rsyncd. See
+          <citerefentry><refentrytitle>rsyncd.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
         '';
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-            Lines of configuration to add to rsyncd globally.
-            See <command>man rsyncd.conf</command> for options.
-          '';
+      socketActivated = mkOption {
+        default = false;
+        type = types.bool;
+        description =
+          "If enabled Rsync will be socket-activated rather than run persistently.";
       };
 
-      modules = mkOption {
-        default = {};
-        description = ''
-            A set describing exported directories.
-            See <command>man rsyncd.conf</command> for options.
-          '';
-        type = types.attrsOf (types.attrsOf types.str);
-        example = literalExample ''
-          { srv =
-             { path = "/srv";
-               "read only" = "yes";
-               comment = "Public rsync share.";
-             };
-          }
-        '';
-      };
+    };
+  };
 
-      user = mkOption {
-        type = types.str;
-        default = "root";
-        description = ''
-          The user to run the daemon as.
-          By default the daemon runs as root.
-        '';
-      };
+  imports = (map (option:
+    mkRemovedOptionModule [ "services" "rsyncd" option ]
+    "This option was removed in favor of `services.rsyncd.settings`.") [
+      "address"
+      "extraConfig"
+      "motd"
+      "user"
+      "group"
+    ]);
 
-      group = mkOption {
-        type = types.str;
-        default = "root";
-        description = ''
-          The group to run the daemon as.
-          By default the daemon runs as root.
-        '';
+  config = mkIf cfg.enable {
+
+    services.rsyncd.settings.global.port = toString cfg.port;
+
+    systemd = let
+      serviceConfigSecurity = {
+        ProtectSystem = "full";
+        PrivateDevices = "on";
+        NoNewPrivileges = "on";
+      };
+    in {
+      services.rsync = {
+        enable = !cfg.socketActivated;
+        aliases = [ "rsyncd" ];
+
+        description = "fast remote file copy program daemon";
+        after = [ "network.target" ];
+        documentation = [ "man:rsync(1)" "man:rsyncd.conf(5)" ];
+
+        serviceConfig = serviceConfigSecurity // {
+          ExecStart =
+            "${pkgs.rsync}/bin/rsync --daemon --no-detach --config=${configFile}";
+          RestartSec = 1;
+        };
+
+        wantedBy = [ "multi-user.target" ];
       };
 
-    };
-  };
+      services."rsync@" = {
+        description = "fast remote file copy program daemon";
+        after = [ "network.target" ];
 
-  ###### implementation
+        serviceConfig = serviceConfigSecurity // {
+          ExecStart = "${pkgs.rsync}/bin/rsync --daemon --config=${configFile}";
+          StandardInput = "socket";
+          StandardOutput = "inherit";
+          StandardError = "journal";
+        };
+      };
 
-  config = mkIf cfg.enable {
+      sockets.rsync = {
+        enable = cfg.socketActivated;
 
-    environment.etc."rsyncd.conf".text = cfgFile;
+        description = "socket for fast remote file copy program daemon";
+        conflicts = [ "rsync.service" ];
 
-    systemd.services.rsyncd = {
-      description = "Rsync daemon";
-      wantedBy = [ "multi-user.target" ];
-      restartTriggers = [ config.environment.etc."rsyncd.conf".source ];
-      serviceConfig = {
-        ExecStart = "${pkgs.rsync}/bin/rsync --daemon --no-detach";
-        User = cfg.user;
-        Group = cfg.group;
+        listenStreams = [ (toString cfg.port) ];
+        socketConfig.Accept = true;
+
+        wantedBy = [ "sockets.target" ];
       };
     };
+
   };
+
+  meta.maintainers = with lib.maintainers; [ ehmry ];
+
+  # TODO: socket activated rsyncd
+
 }
diff --git a/nixos/modules/services/network-filesystems/samba-wsdd.nix b/nixos/modules/services/network-filesystems/samba-wsdd.nix
new file mode 100644
index 00000000000..800ef448d37
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/samba-wsdd.nix
@@ -0,0 +1,124 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.samba-wsdd;
+
+in {
+  options = {
+    services.samba-wsdd = {
+      enable = mkEnableOption ''
+        Enable Web Services Dynamic Discovery host daemon. This enables (Samba) hosts, like your local NAS device,
+        to be found by Web Service Discovery Clients like Windows.
+        <note>
+          <para>If you use the firewall consider adding the following:</para>
+          <programlisting>
+            networking.firewall.allowedTCPPorts = [ 5357 ];
+            networking.firewall.allowedUDPPorts = [ 3702 ];
+          </programlisting>
+        </note>
+      '';
+      interface = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "eth0";
+        description = "Interface or address to use.";
+      };
+      hoplimit = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        example = 2;
+        description = "Hop limit for multicast packets (default = 1).";
+      };
+      workgroup = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "HOME";
+        description = "Set workgroup name (default WORKGROUP).";
+      };
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "FILESERVER";
+        description = "Override (NetBIOS) hostname to be used (default hostname).";
+      };
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Set domain name (disables workgroup).";
+      };
+      discovery = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable discovery operation mode.";
+      };
+      listen = mkOption {
+        type = types.str;
+        default = "/run/wsdd/wsdd.sock";
+        description = "Listen on path or localhost port in discovery mode.";
+      };
+      extraOptions = mkOption {
+        type = types.listOf types.str;
+        default = [ "--shortlog" ];
+        example = [ "--verbose" "--no-http" "--ipv4only" "--no-host" ];
+        description = "Additional wsdd options.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.wsdd ];
+
+    systemd.services.samba-wsdd = {
+      description = "Web Services Dynamic Discovery host daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        Type = "simple";
+        ExecStart = ''
+          ${pkgs.wsdd}/bin/wsdd ${optionalString (cfg.interface != null) "--interface '${cfg.interface}'"} \
+                                ${optionalString (cfg.hoplimit != null) "--hoplimit '${toString cfg.hoplimit}'"} \
+                                ${optionalString (cfg.workgroup != null) "--workgroup '${cfg.workgroup}'"} \
+                                ${optionalString (cfg.hostname != null) "--hostname '${cfg.hostname}'"} \
+                                ${optionalString (cfg.domain != null) "--domain '${cfg.domain}'"} \
+                                ${optionalString cfg.discovery "--discovery --listen '${cfg.listen}'"} \
+                                ${escapeShellArgs cfg.extraOptions}
+        '';
+        # Runtime directory and mode
+        RuntimeDirectory = "wsdd";
+        RuntimeDirectoryMode = "0750";
+        # Access write directories
+        UMask = "0027";
+        # Capabilities
+        CapabilityBoundingSet = "";
+        # Security
+        NoNewPrivileges = true;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = false;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@cpu-emulation @debug @mount @obsolete @privileged @resources";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index 08c912e0fcd..78ea245cb35 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -26,7 +26,6 @@ let
       [global]
       security = ${cfg.securityType}
       passwd program = /run/wrappers/bin/passwd %u
-      pam password change = ${smbToString cfg.syncPasswordsByPam}
       invalid users = ${smbToString cfg.invalidUsers}
 
       ${cfg.extraConfig}
@@ -67,6 +66,7 @@ in
 {
   imports = [
     (mkRemovedOptionModule [ "services" "samba" "defaultShare" ] "")
+    (mkRemovedOptionModule [ "services" "samba" "syncPasswordsByPam" ] "This option has been removed by upstream, see https://bugzilla.samba.org/show_bug.cgi?id=10669#c10")
   ];
 
   ###### interface
@@ -124,18 +124,6 @@ in
         '';
       };
 
-      syncPasswordsByPam = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enabling this will add a line directly after pam_unix.so.
-          Whenever a password is changed the samba password will be updated as well.
-          However, you still have to add the samba password once, using smbpasswd -a user.
-          If you don't want to maintain an extra password database, you still can send plain text
-          passwords which is not secure.
-        '';
-      };
-
       invalidUsers = mkOption {
         type = types.listOf types.str;
         default = [ "root" ];
@@ -168,7 +156,6 @@ in
       securityType = mkOption {
         type = types.str;
         default = "user";
-        example = "share";
         description = "Samba security type";
       };
 
@@ -248,7 +235,7 @@ in
         };
 
         security.pam.services.samba = {};
-
+        environment.systemPackages = [ config.services.samba.package ];
       })
     ];
 
diff --git a/nixos/modules/services/network-filesystems/xtreemfs.nix b/nixos/modules/services/network-filesystems/xtreemfs.nix
index b8f8c1d7117..6cc8a05ee00 100644
--- a/nixos/modules/services/network-filesystems/xtreemfs.nix
+++ b/nixos/modules/services/network-filesystems/xtreemfs.nix
@@ -92,6 +92,7 @@ in
       enable = mkEnableOption "XtreemFS";
 
       homeDir = mkOption {
+        type = types.path;
         default = "/var/lib/xtreemfs";
         description = ''
           XtreemFS home dir for the xtreemfs user.
@@ -109,19 +110,22 @@ in
 
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e40";
+          type = types.str;
           description = ''
             Must be set to a unique identifier, preferably a UUID according to
             RFC 4122. UUIDs can be generated with `uuidgen` command, found in
-            the `utillinux` package.
+            the `util-linux` package.
           '';
         };
         port = mkOption {
           default = 32638;
+          type = types.port;
           description = ''
             The port to listen on for incoming connections (TCP).
           '';
         };
         address = mkOption {
+          type = types.str;
           example = "127.0.0.1";
           default = "";
           description = ''
@@ -131,12 +135,14 @@ in
         };
         httpPort = mkOption {
           default = 30638;
+          type = types.port;
           description = ''
             Specifies the listen port for the HTTP service that returns the
             status page.
           '';
         };
         syncMode = mkOption {
+          type = types.enum [ "ASYNC" "SYNC_WRITE_METADATA" "SYNC_WRITE" "FDATASYNC" "ASYNC" ];
           default = "FSYNC";
           example = "FDATASYNC";
           description = ''
@@ -229,20 +235,23 @@ in
 
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e41";
+          type = types.str;
           description = ''
             Must be set to a unique identifier, preferably a UUID according to
             RFC 4122. UUIDs can be generated with `uuidgen` command, found in
-            the `utillinux` package.
+            the `util-linux` package.
           '';
         };
         port = mkOption {
           default = 32636;
+          type = types.port;
           description = ''
             The port to listen on for incoming connections (TCP).
           '';
         };
         address = mkOption {
           example = "127.0.0.1";
+          type = types.str;
           default = "";
           description = ''
             If specified, it defines the interface to listen on. If not
@@ -251,6 +260,7 @@ in
         };
         httpPort = mkOption {
           default = 30636;
+          type = types.port;
           description = ''
             Specifies the listen port for the HTTP service that returns the
             status page.
@@ -258,6 +268,7 @@ in
         };
         syncMode = mkOption {
           default = "FSYNC";
+          type = types.enum [ "ASYNC" "SYNC_WRITE_METADATA" "SYNC_WRITE" "FDATASYNC" "ASYNC" ];
           example = "FDATASYNC";
           description = ''
             The sync mode influences how operations are committed to the disk
@@ -367,20 +378,23 @@ in
 
         uuid = mkOption {
           example = "eacb6bab-f444-4ebf-a06a-3f72d7465e42";
+          type = types.str;
           description = ''
             Must be set to a unique identifier, preferably a UUID according to
             RFC 4122. UUIDs can be generated with `uuidgen` command, found in
-            the `utillinux` package.
+            the `util-linux` package.
           '';
         };
         port = mkOption {
           default = 32640;
+          type = types.port;
           description = ''
             The port to listen on for incoming connections (TCP and UDP).
           '';
         };
         address = mkOption {
           example = "127.0.0.1";
+          type = types.str;
           default = "";
           description = ''
             If specified, it defines the interface to listen on. If not
@@ -389,6 +403,7 @@ in
         };
         httpPort = mkOption {
           default = 30640;
+          type = types.port;
           description = ''
             Specifies the listen port for the HTTP service that returns the
             status page.
diff --git a/nixos/modules/services/network-filesystems/yandex-disk.nix b/nixos/modules/services/network-filesystems/yandex-disk.nix
index cc73f13bf77..a5b1f9d4ab6 100644
--- a/nixos/modules/services/network-filesystems/yandex-disk.nix
+++ b/nixos/modules/services/network-filesystems/yandex-disk.nix
@@ -46,12 +46,14 @@ in
 
       user = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           The user the yandex-disk daemon should run as.
         '';
       };
 
       directory = mkOption {
+        type = types.path;
         default = "/home/Yandex.Disk";
         description = "The directory to use for Yandex.Disk storage";
       };
diff --git a/nixos/modules/services/networking/adguardhome.nix b/nixos/modules/services/networking/adguardhome.nix
new file mode 100644
index 00000000000..4388ef2b7e5
--- /dev/null
+++ b/nixos/modules/services/networking/adguardhome.nix
@@ -0,0 +1,78 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.adguardhome;
+
+  args = concatStringsSep " " ([
+    "--no-check-update"
+    "--pidfile /run/AdGuardHome/AdGuardHome.pid"
+    "--work-dir /var/lib/AdGuardHome/"
+    "--config /var/lib/AdGuardHome/AdGuardHome.yaml"
+    "--host ${cfg.host}"
+    "--port ${toString cfg.port}"
+  ] ++ cfg.extraArgs);
+
+in
+{
+  options.services.adguardhome = with types; {
+    enable = mkEnableOption "AdGuard Home network-wide ad blocker";
+
+    host = mkOption {
+      default = "0.0.0.0";
+      type = str;
+      description = ''
+        Host address to bind HTTP server to.
+      '';
+    };
+
+    port = mkOption {
+      default = 3000;
+      type = port;
+      description = ''
+        Port to serve HTTP pages on.
+      '';
+    };
+
+    openFirewall = mkOption {
+      default = false;
+      type = bool;
+      description = ''
+        Open ports in the firewall for the AdGuard Home web interface. Does not
+        open the port needed to access the DNS resolver.
+      '';
+    };
+
+    extraArgs = mkOption {
+      default = [ ];
+      type = listOf str;
+      description = ''
+        Extra command line parameters to be passed to the adguardhome binary.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.adguardhome = {
+      description = "AdGuard Home: Network-level blocker";
+      after = [ "syslog.target" "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        StartLimitIntervalSec = 5;
+        StartLimitBurst = 10;
+      };
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.adguardhome}/bin/adguardhome ${args}";
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        Restart = "always";
+        RestartSec = 10;
+        RuntimeDirectory = "AdGuardHome";
+        StateDirectory = "AdGuardHome";
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+  };
+}
diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix
index 1128ee2c3e6..39320643dd5 100644
--- a/nixos/modules/services/networking/amuled.nix
+++ b/nixos/modules/services/networking/amuled.nix
@@ -24,13 +24,15 @@ in
       };
 
       dataDir = mkOption {
-        default = ''/home/${user}/'';
+        type = types.str;
+        default = "/home/${user}/";
         description = ''
           The directory holding configuration, incoming and temporary files.
         '';
       };
 
       user = mkOption {
+        type = types.nullOr types.str;
         default = null;
         description = ''
           The user the AMule daemon should run as.
diff --git a/nixos/modules/services/networking/autossh.nix b/nixos/modules/services/networking/autossh.nix
index a8d9a027e9f..245f2bfc2cf 100644
--- a/nixos/modules/services/networking/autossh.nix
+++ b/nixos/modules/services/networking/autossh.nix
@@ -79,7 +79,7 @@ in
 
     systemd.services =
 
-      lib.fold ( s : acc : acc //
+      lib.foldr ( s : acc : acc //
         {
           "autossh-${s.name}" =
             let
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
index c876b252e8c..020a817f259 100644
--- a/nixos/modules/services/networking/avahi-daemon.nix
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -86,7 +86,8 @@ in
 
     ipv6 = mkOption {
       type = types.bool;
-      default = false;
+      default = config.networking.enableIPv6;
+      defaultText = "config.networking.enableIPv6";
       description = "Whether to use IPv6.";
     };
 
@@ -239,8 +240,8 @@ in
 
     system.nssModules = optional cfg.nssmdns pkgs.nssmdns;
     system.nssDatabases.hosts = optionals cfg.nssmdns (mkMerge [
-      [ "mdns_minimal [NOTFOUND=return]" ]
-      (mkOrder 1501 [ "mdns" ]) # 1501 to ensure it's after dns
+      (mkBefore [ "mdns_minimal [NOTFOUND=return]" ]) # before resolve
+      (mkAfter [ "mdns" ]) # after dns
     ]);
 
     environment.systemPackages = [ pkgs.avahi ];
diff --git a/nixos/modules/services/networking/babeld.nix b/nixos/modules/services/networking/babeld.nix
index e62c74d0069..aae6f1498a4 100644
--- a/nixos/modules/services/networking/babeld.nix
+++ b/nixos/modules/services/networking/babeld.nix
@@ -19,7 +19,10 @@ let
     "interface ${name} ${paramsString interface}\n";
 
   configFile = with cfg; pkgs.writeText "babeld.conf" (
-    (optionalString (cfg.interfaceDefaults != null) ''
+    ''
+      skip-kernel-setup true
+    ''
+    + (optionalString (cfg.interfaceDefaults != null) ''
       default ${paramsString cfg.interfaceDefaults}
     '')
     + (concatMapStrings interfaceConfig (attrNames cfg.interfaces))
@@ -29,6 +32,8 @@ in
 
 {
 
+  meta.maintainers = with maintainers; [ hexa ];
+
   ###### interface
 
   options = {
@@ -69,6 +74,7 @@ in
 
       extraConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Options that will be copied to babeld.conf.
           See <citerefentry><refentrytitle>babeld</refentrytitle><manvolnum>8</manvolnum></citerefentry> for details.
@@ -83,13 +89,56 @@ in
 
   config = mkIf config.services.babeld.enable {
 
+    boot.kernel.sysctl = {
+      "net.ipv6.conf.all.forwarding" = 1;
+      "net.ipv6.conf.all.accept_redirects" = 0;
+      "net.ipv4.conf.all.forwarding" = 1;
+      "net.ipv4.conf.all.rp_filter" = 0;
+    } // lib.mapAttrs' (ifname: _: lib.nameValuePair "net.ipv4.conf.${ifname}.rp_filter" (lib.mkDefault 0)) config.services.babeld.interfaces;
+
     systemd.services.babeld = {
       description = "Babel routing daemon";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${pkgs.babeld}/bin/babeld -c ${configFile}";
+      serviceConfig = {
+        ExecStart = "${pkgs.babeld}/bin/babeld -c ${configFile} -I /run/babeld/babeld.pid -S /var/lib/babeld/state";
+        AmbientCapabilities = [ "CAP_NET_ADMIN" ];
+        CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
+        DevicePolicy = "closed";
+        DynamicUser = true;
+        IPAddressAllow = [ "fe80::/64" "ff00::/8" "::1/128" "127.0.0.0/8" ];
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        NoNewPrivileges = true;
+        MemoryDenyWriteExecute = true;
+        ProtectSystem = "strict";
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_NETLINK" "AF_INET6" "AF_INET" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectProc = "invisible";
+        PrivateMounts = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = false; # kernel_route(ADD): Operation not permitted
+        ProcSubset = "pid";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged @resources"
+        ];
+        UMask = "0177";
+        RuntimeDirectory = "babeld";
+        StateDirectory = "babeld";
+      };
     };
-
   };
-
 }
diff --git a/nixos/modules/services/networking/bee-clef.nix b/nixos/modules/services/networking/bee-clef.nix
new file mode 100644
index 00000000000..719714b2898
--- /dev/null
+++ b/nixos/modules/services/networking/bee-clef.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+# NOTE for now nothing is installed into /etc/bee-clef/. the config files are used as read-only from the nix store.
+
+with lib;
+let
+  cfg = config.services.bee-clef;
+in {
+  meta = {
+    maintainers = with maintainers; [ attila-lendvai ];
+  };
+
+  ### interface
+
+  options = {
+    services.bee-clef = {
+      enable = mkEnableOption "clef external signer instance for Ethereum Swarm Bee";
+
+      dataDir = mkOption {
+        type = types.nullOr types.str;
+        default = "/var/lib/bee-clef";
+        description = ''
+          Data dir for bee-clef. Beware that some helper scripts may not work when changed!
+          The service itself should work fine, though.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.str;
+        default = "/var/lib/bee-clef/password";
+        description = "Password file for bee-clef.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bee-clef";
+        description = ''
+          User the bee-clef daemon should execute under.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bee-clef";
+        description = ''
+          Group the bee-clef daemon should execute under.
+        '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    # if we ever want to have rules.js under /etc/bee-clef/
+    # environment.etc."bee-clef/rules.js".source = ${pkgs.bee-clef}/rules.js
+
+    systemd.packages = [ pkgs.bee-clef ]; # include the upstream bee-clef.service file
+
+    systemd.tmpfiles.rules = [
+        "d '${cfg.dataDir}/'         0750 ${cfg.user} ${cfg.group}"
+        "d '${cfg.dataDir}/keystore' 0700 ${cfg.user} ${cfg.group}"
+      ];
+
+    systemd.services.bee-clef = {
+      path = [
+        # these are needed for the ensure-clef-account script
+        pkgs.coreutils
+        pkgs.gnused
+        pkgs.gawk
+      ];
+
+      wantedBy = [ "bee.service" "multi-user.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStartPre = ''${pkgs.bee-clef}/share/bee-clef/ensure-clef-account "${cfg.dataDir}" "${pkgs.bee-clef}/share/bee-clef/"'';
+        ExecStart = [
+          "" # this hides/overrides what's in the original entry
+          "${pkgs.bee-clef}/share/bee-clef/bee-clef-service start"
+        ];
+        ExecStop = [
+          "" # this hides/overrides what's in the original entry
+          "${pkgs.bee-clef}/share/bee-clef/bee-clef-service stop"
+        ];
+        Environment = [
+          "CONFIGDIR=${cfg.dataDir}"
+          "PASSWORD_FILE=${cfg.passwordFile}"
+        ];
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "bee-clef") {
+      bee-clef = {
+        group = cfg.group;
+        home = cfg.dataDir;
+        isSystemUser = true;
+        description = "Daemon user for the bee-clef service";
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "bee-clef") {
+      bee-clef = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/bee.nix b/nixos/modules/services/networking/bee.nix
new file mode 100644
index 00000000000..8a77ce23ab4
--- /dev/null
+++ b/nixos/modules/services/networking/bee.nix
@@ -0,0 +1,149 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.bee;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "bee.yaml" cfg.settings;
+in {
+  meta = {
+    # doc = ./bee.xml;
+    maintainers = with maintainers; [ attila-lendvai ];
+  };
+
+  ### interface
+
+  options = {
+    services.bee = {
+      enable = mkEnableOption "Ethereum Swarm Bee";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bee;
+        defaultText = "pkgs.bee";
+        example = "pkgs.bee-unstable";
+        description = "The package providing the bee binary for the service.";
+      };
+
+      settings = mkOption {
+        type = format.type;
+        description = ''
+          Ethereum Swarm Bee configuration. Refer to
+          <link xlink:href="https://gateway.ethswarm.org/bzz/docs.swarm.eth/docs/installation/configuration/"/>
+          for details on supported values.
+        '';
+      };
+
+      daemonNiceLevel = mkOption {
+        type = types.int;
+        default = 0;
+        description = ''
+          Daemon process priority for bee.
+          0 is the default Unix process priority, 19 is the lowest.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "bee";
+        description = ''
+          User the bee binary should execute under.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "bee";
+        description = ''
+          Group the bee binary should execute under.
+        '';
+      };
+    };
+  };
+
+  ### implementation
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = (hasAttr "password" cfg.settings) != true;
+        message = ''
+          `services.bee.settings.password` is insecure. Use `services.bee.settings.password-file` or `systemd.services.bee.serviceConfig.EnvironmentFile` instead.
+        '';
+      }
+      { assertion = (hasAttr "swap-endpoint" cfg.settings) || (cfg.settings.swap-enable or true == false);
+        message = ''
+          In a swap-enabled network a working Ethereum blockchain node is required. You must specify one using `services.bee.settings.swap-endpoint`, or disable `services.bee.settings.swap-enable` = false.
+        '';
+      }
+    ];
+
+    warnings = optional (! config.services.bee-clef.enable) "The bee service requires an external signer. Consider setting `config.services.bee-clef.enable` = true";
+
+    services.bee.settings = {
+      data-dir             = lib.mkDefault "/var/lib/bee";
+      password-file        = lib.mkDefault "/var/lib/bee/password";
+      clef-signer-enable   = lib.mkDefault true;
+      clef-signer-endpoint = lib.mkDefault "/var/lib/bee-clef/clef.ipc";
+      swap-endpoint        = lib.mkDefault "https://rpc.slock.it/goerli";
+    };
+
+    systemd.packages = [ cfg.package ]; # include the upstream bee.service file
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.settings.data-dir}' 0750 ${cfg.user} ${cfg.group}"
+    ];
+
+    systemd.services.bee = {
+      requires = optional config.services.bee-clef.enable
+        "bee-clef.service";
+
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Nice = cfg.daemonNiceLevel;
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = [
+          "" # this hides/overrides what's in the original entry
+          "${cfg.package}/bin/bee --config=${configFile} start"
+        ];
+      };
+
+      preStart = with cfg.settings; ''
+        if ! test -f ${password-file}; then
+          < /dev/urandom tr -dc _A-Z-a-z-0-9 2> /dev/null | head -c32 > ${password-file}
+          chmod 0600 ${password-file}
+          echo "Initialized ${password-file} from /dev/urandom"
+        fi
+        if [ ! -f ${data-dir}/keys/libp2p.key ]; then
+          ${cfg.package}/bin/bee init --config=${configFile} >/dev/null
+          echo "
+Logs:   journalctl -f -u bee.service
+
+Bee has SWAP enabled by default and it needs ethereum endpoint to operate.
+It is recommended to use external signer with bee.
+Check documentation for more info:
+- SWAP https://docs.ethswarm.org/docs/installation/manual#swap-bandwidth-incentives
+- External signer https://docs.ethswarm.org/docs/installation/bee-clef
+
+After you finish configuration run 'sudo bee-get-addr'."
+        fi
+      '';
+    };
+
+    users.users = optionalAttrs (cfg.user == "bee") {
+      bee = {
+        group = cfg.group;
+        home = cfg.settings.data-dir;
+        isSystemUser = true;
+        description = "Daemon user for Ethereum Swarm Bee";
+        extraGroups = optional config.services.bee-clef.enable
+          config.services.bee-clef.group;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "bee") {
+      bee = {};
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/biboumi.nix b/nixos/modules/services/networking/biboumi.nix
new file mode 100644
index 00000000000..66ddca93d81
--- /dev/null
+++ b/nixos/modules/services/networking/biboumi.nix
@@ -0,0 +1,269 @@
+{ config, lib, pkgs, options, ... }:
+with lib;
+let
+  cfg = config.services.biboumi;
+  inherit (config.environment) etc;
+  rootDir = "/run/biboumi/mnt-root";
+  stateDir = "/var/lib/biboumi";
+  settingsFile = pkgs.writeText "biboumi.cfg" (
+    generators.toKeyValue {
+      mkKeyValue = k: v:
+        if v == null then ""
+        else generators.mkKeyValueDefault {} "=" k v;
+    } cfg.settings);
+  need_CAP_NET_BIND_SERVICE = cfg.settings.identd_port != 0 && cfg.settings.identd_port < 1024;
+in
+{
+  options = {
+    services.biboumi = {
+      enable = mkEnableOption "the Biboumi XMPP gateway to IRC";
+
+      settings = mkOption {
+        description = ''
+          See <link xlink:href="https://lab.louiz.org/louiz/biboumi/blob/8.5/doc/biboumi.1.rst">biboumi 8.5</link>
+          for documentation.
+        '';
+        default = {};
+        type = types.submodule {
+          freeformType = with types;
+            (attrsOf (nullOr (oneOf [str int bool]))) // {
+              description = "settings option";
+            };
+          options.admin = mkOption {
+            type = with types; listOf str;
+            default = [];
+            example = ["admin@example.org"];
+            apply = concatStringsSep ":";
+            description = ''
+              The bare JID of the gateway administrator. This JID will have more
+              privileges than other standard users, for example some administration
+              ad-hoc commands will only be available to that JID.
+            '';
+          };
+          options.ca_file = mkOption {
+            type = types.path;
+            default = "/etc/ssl/certs/ca-certificates.crt";
+            description = ''
+              Specifies which file should be used as the list of trusted CA
+              when negociating a TLS session.
+            '';
+          };
+          options.db_name = mkOption {
+            type = with types; either path str;
+            default = "${stateDir}/biboumi.sqlite";
+            description = ''
+              The name of the database to use.
+            '';
+            example = "postgresql://user:secret@localhost";
+          };
+          options.hostname = mkOption {
+            type = types.str;
+            example = "biboumi.example.org";
+            description = ''
+              The hostname served by the XMPP gateway.
+              This domain must be configured in the XMPP server
+              as an external component.
+            '';
+          };
+          options.identd_port = mkOption {
+            type = types.port;
+            default = 113;
+            example = 0;
+            description = ''
+              The TCP port on which to listen for identd queries.
+            '';
+          };
+          options.log_level = mkOption {
+            type = types.ints.between 0 3;
+            default = 1;
+            description = ''
+              Indicate what type of log messages to write in the logs.
+              0 is debug, 1 is info, 2 is warning, 3 is error.
+            '';
+          };
+          options.password = mkOption {
+            type = with types; nullOr str;
+            description = ''
+              The password used to authenticate the XMPP component to your XMPP server.
+              This password must be configured in the XMPP server,
+              associated with the external component on
+              <link linkend="opt-services.biboumi.settings.hostname">hostname</link>.
+
+              Set it to null and use <link linkend="opt-services.biboumi.credentialsFile">credentialsFile</link>
+              if you do not want this password to go into the Nix store.
+            '';
+          };
+          options.persistent_by_default = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether all rooms will be persistent by default:
+              the value of the “persistent” option in the global configuration of each
+              user will be “true”, but the value of each individual room will still
+              default to false. This means that a user just needs to change the global
+              “persistent” configuration option to false in order to override this.
+            '';
+          };
+          options.policy_directory = mkOption {
+            type = types.path;
+            default = "${pkgs.biboumi}/etc/biboumi";
+            description = ''
+              A directory that should contain the policy files,
+              used to customize Botan’s behaviour
+              when negociating the TLS connections with the IRC servers.
+            '';
+          };
+          options.port = mkOption {
+            type = types.port;
+            default = 5347;
+            description = ''
+              The TCP port to use to connect to the local XMPP component.
+            '';
+          };
+          options.realname_customization = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether the users will be able to use
+              the ad-hoc commands that lets them configure
+              their realname and username.
+            '';
+          };
+          options.realname_from_jid = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Whether the realname and username of each biboumi
+              user will be extracted from their JID.
+              Otherwise they will be set to the nick
+              they used to connect to the IRC server.
+            '';
+          };
+          options.xmpp_server_ip = mkOption {
+            type = types.str;
+            default = "127.0.0.1";
+            description = ''
+              The IP address to connect to the XMPP server on.
+              The connection to the XMPP server is unencrypted,
+              so the biboumi instance and the server should
+              normally be on the same host.
+            '';
+          };
+        };
+      };
+
+      credentialsFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to a configuration file to be merged with the settings.
+          Beware not to surround "=" with spaces when setting biboumi's options in this file.
+          Useful to merge a file which is better kept out of the Nix store
+          because it contains sensible data like
+          <link linkend="opt-services.biboumi.settings.password">password</link>.
+        '';
+        default = "/dev/null";
+        example = "/run/keys/biboumi.cfg";
+      };
+
+      openFirewall = mkEnableOption "opening of the identd port in the firewall";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall = mkIf (cfg.openFirewall && cfg.settings.identd_port != 0)
+      { allowedTCPPorts = [ cfg.settings.identd_port ]; };
+
+    systemd.services.biboumi = {
+      description = "Biboumi, XMPP to IRC gateway";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "notify";
+        # Biboumi supports systemd's watchdog.
+        WatchdogSec = 20;
+        Restart = "always";
+        # Use "+" because credentialsFile may not be accessible to User= or Group=.
+        ExecStartPre = [("+" + pkgs.writeShellScript "biboumi-prestart" ''
+          set -eux
+          cat ${settingsFile} '${cfg.credentialsFile}' |
+          install -m 644 /dev/stdin /run/biboumi/biboumi.cfg
+        '')];
+        ExecStart = "${pkgs.biboumi}/bin/biboumi /run/biboumi/biboumi.cfg";
+        ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
+        # Firewalls needing opening for output connections can still do that
+        # selectively for biboumi with:
+        # users.users.biboumi.isSystemUser = true;
+        # and, for example:
+        # networking.nftables.ruleset = ''
+        #   add rule inet filter output meta skuid biboumi tcp accept
+        # '';
+        DynamicUser = true;
+        RootDirectory = rootDir;
+        RootDirectoryStartOnly = true;
+        InaccessiblePaths = [ "-+${rootDir}" ];
+        RuntimeDirectory = [ "biboumi" (removePrefix "/run/" rootDir) ];
+        RuntimeDirectoryMode = "700";
+        StateDirectory = "biboumi";
+        StateDirectoryMode = "700";
+        MountAPIVFS = true;
+        UMask = "0066";
+        BindPaths = [
+          stateDir
+          # This is for Type="notify"
+          # See https://github.com/systemd/systemd/issues/3544
+          "/run/systemd/notify"
+          "/run/systemd/journal/socket"
+        ];
+        BindReadOnlyPaths = [
+          builtins.storeDir
+          "/etc"
+        ];
+        # The following options are only for optimizing:
+        # systemd-analyze security biboumi
+        AmbientCapabilities = [ (optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
+        CapabilityBoundingSet = [ (optionalString need_CAP_NET_BIND_SERVICE "CAP_NET_BIND_SERVICE") ];
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        # PrivateUsers=true breaks AmbientCapabilities=CAP_NET_BIND_SERVICE
+        # See https://bugs.archlinux.org/task/65921
+        PrivateUsers = !need_CAP_NET_BIND_SERVICE;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        # AF_UNIX is for /run/systemd/notify
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall
+          # listed by perf stat -e 'syscalls:sys_enter_*' biboumi biboumi.cfg
+          # in tests, and seem likely not necessary for biboumi.
+          # To run such a perf in ExecStart=, you have to:
+          # - AmbientCapabilities="CAP_SYS_ADMIN"
+          # - mount -o remount,mode=755 /sys/kernel/debug/{,tracing}
+          "~@aio" "~@chown" "~@ipc" "~@keyring" "~@resources" "~@setuid" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ julm ];
+}
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index faad8863575..480d5a184f2 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -6,8 +6,44 @@ let
 
   cfg = config.services.bind;
 
+  bindPkg = config.services.bind.package;
+
   bindUser = "named";
 
+  bindZoneCoerce = list: builtins.listToAttrs (lib.forEach list (zone: { name = zone.name; value = zone; }));
+
+  bindZoneOptions = { name, config, ... }: {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = "Name of the zone.";
+      };
+      master = mkOption {
+        description = "Master=false means slave server";
+        type = types.bool;
+      };
+      file = mkOption {
+        type = types.either types.str types.path;
+        description = "Zone file resource records contain columns of data, separated by whitespace, that define the record.";
+      };
+      masters = mkOption {
+        type = types.listOf types.str;
+        description = "List of servers for inclusion in stub and secondary zones.";
+      };
+      slaves = mkOption {
+        type = types.listOf types.str;
+        description = "Addresses who may request zone transfers.";
+        default = [ ];
+      };
+      extraConfig = mkOption {
+        type = types.str;
+        description = "Extra zone config to be appended at the end of the zone section.";
+        default = "";
+      };
+    };
+  };
+
   confFile = pkgs.writeText "named.conf"
     ''
       include "/etc/bind/rndc.key";
@@ -25,7 +61,7 @@ let
         blackhole { badnetworks; };
         forward first;
         forwarders { ${concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
-        directory "/run/named";
+        directory "${cfg.directory}";
         pid-file "/run/named/named.pid";
         ${cfg.extraOptions}
       };
@@ -55,7 +91,7 @@ let
                 ${extraConfig}
               };
             '')
-          cfg.zones }
+          (attrValues cfg.zones) }
     '';
 
 in
@@ -70,8 +106,17 @@ in
 
       enable = mkEnableOption "BIND domain name server";
 
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.bind;
+        defaultText = "pkgs.bind";
+        description = "The BIND package to use.";
+      };
+
       cacheNetworks = mkOption {
-        default = ["127.0.0.0/24"];
+        default = [ "127.0.0.0/24" ];
+        type = types.listOf types.str;
         description = "
           What networks are allowed to use us as a resolver.  Note
           that this is for recursive queries -- all networks are
@@ -82,7 +127,8 @@ in
       };
 
       blockedNetworks = mkOption {
-        default = [];
+        default = [ ];
+        type = types.listOf types.str;
         description = "
           What networks are just blocked.
         ";
@@ -90,6 +136,7 @@ in
 
       ipv4Only = mkOption {
         default = false;
+        type = types.bool;
         description = "
           Only use ipv4, even if the host supports ipv6.
         ";
@@ -97,13 +144,14 @@ in
 
       forwarders = mkOption {
         default = config.networking.nameservers;
+        type = types.listOf types.str;
         description = "
           List of servers we should forward requests to.
         ";
       };
 
       listenOn = mkOption {
-        default = ["any"];
+        default = [ "any" ];
         type = types.listOf types.str;
         description = "
           Interfaces to listen on.
@@ -111,28 +159,34 @@ in
       };
 
       listenOnIpv6 = mkOption {
-        default = ["any"];
+        default = [ "any" ];
         type = types.listOf types.str;
         description = "
           Ipv6 interfaces to listen on.
         ";
       };
 
+      directory = mkOption {
+        type = types.str;
+        default = "/run/named";
+        description = "Working directory of BIND.";
+      };
+
       zones = mkOption {
-        default = [];
+        default = [ ];
+        type = with types; coercedTo (listOf attrs) bindZoneCoerce (attrsOf (types.submodule bindZoneOptions));
         description = "
           List of zones we claim authority over.
-            master=false means slave server; slaves means addresses
-           who may request zone transfer.
         ";
-        example = [{
-          name = "example.com";
-          master = false;
-          file = "/var/dns/example.com";
-          masters = ["192.168.0.1"];
-          slaves = [];
-          extraConfig = "";
-        }];
+        example = {
+          "example.com" = {
+            master = false;
+            file = "/var/dns/example.com";
+            masters = [ "192.168.0.1" ];
+            slaves = [ ];
+            extraConfig = "";
+          };
+        };
       };
 
       extraConfig = mkOption {
@@ -174,7 +228,8 @@ in
     networking.resolvconf.useLocalResolver = mkDefault true;
 
     users.users.${bindUser} =
-      { uid = config.ids.uids.bind;
+      {
+        uid = config.ids.uids.bind;
         description = "BIND daemon user";
       };
 
@@ -186,17 +241,20 @@ in
       preStart = ''
         mkdir -m 0755 -p /etc/bind
         if ! [ -f "/etc/bind/rndc.key" ]; then
-          ${pkgs.bind.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -u ${bindUser} -a -A hmac-sha256 2>/dev/null
+          ${bindPkg.out}/sbin/rndc-confgen -c /etc/bind/rndc.key -u ${bindUser} -a -A hmac-sha256 2>/dev/null
         fi
 
         ${pkgs.coreutils}/bin/mkdir -p /run/named
         chown ${bindUser} /run/named
+
+        ${pkgs.coreutils}/bin/mkdir -p ${cfg.directory}
+        chown ${bindUser} ${cfg.directory}
       '';
 
       serviceConfig = {
-        ExecStart  = "${pkgs.bind.out}/sbin/named -u ${bindUser} ${optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} -f";
-        ExecReload = "${pkgs.bind.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
-        ExecStop   = "${pkgs.bind.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
+        ExecStart = "${bindPkg.out}/sbin/named -u ${bindUser} ${optionalString cfg.ipv4Only "-4"} -c ${cfg.configFile} -f";
+        ExecReload = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' reload";
+        ExecStop = "${bindPkg.out}/sbin/rndc -k '/etc/bind/rndc.key' stop";
       };
 
       unitConfig.Documentation = "man:named(8)";
diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix
index 4ae35875c0f..1923afdf83f 100644
--- a/nixos/modules/services/networking/bird.nix
+++ b/nixos/modules/services/networking/bird.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mkEnableOption mkIf mkOption types;
+  inherit (lib) mkEnableOption mkIf mkOption optionalString types;
 
   generic = variant:
     let
@@ -26,6 +26,14 @@ let
               <link xlink:href='http://bird.network.cz/'/>
             '';
           };
+          checkConfig = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              Whether the config should be checked at build time.
+              Disabling this might become necessary if the config includes files not present during build time.
+            '';
+          };
         };
       };
 
@@ -36,7 +44,7 @@ let
         environment.etc."bird/${variant}.conf".source = pkgs.writeTextFile {
           name = "${variant}.conf";
           text = cfg.config;
-          checkPhase = ''
+          checkPhase = optionalString cfg.checkConfig ''
             ${pkg}/bin/${birdBin} -d -p -c $out
           '';
         };
@@ -50,7 +58,7 @@ let
             Type = "forking";
             Restart = "on-failure";
             ExecStart = "${pkg}/bin/${birdBin} -c /etc/bird/${variant}.conf -u ${variant} -g ${variant}";
-            ExecReload = "${pkg}/bin/${birdc} configure";
+            ExecReload = "/bin/sh -c '${pkg}/bin/${birdBin} -c /etc/bird/${variant}.conf -p && ${pkg}/bin/${birdc} configure'";
             ExecStop = "${pkg}/bin/${birdc} down";
             CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_SETUID" "CAP_SETGID"
                                       # see bird/sysdep/linux/syspriv.h
@@ -65,6 +73,7 @@ let
           users.${variant} = {
             description = "BIRD Internet Routing Daemon user";
             group = variant;
+            isSystemUser = true;
           };
           groups.${variant} = {};
         };
diff --git a/nixos/modules/services/networking/bitlbee.nix b/nixos/modules/services/networking/bitlbee.nix
index 9ebf382fce4..59ad9e54686 100644
--- a/nixos/modules/services/networking/bitlbee.nix
+++ b/nixos/modules/services/networking/bitlbee.nix
@@ -58,6 +58,7 @@ in
       };
 
       interface = mkOption {
+        type = types.str;
         default = "127.0.0.1";
         description = ''
           The interface the BitlBee deamon will be listening to.  If `127.0.0.1',
@@ -68,6 +69,7 @@ in
 
       portNumber = mkOption {
         default = 6667;
+        type = types.int;
         description = ''
           Number of the port BitlBee will be listening to.
         '';
@@ -142,6 +144,7 @@ in
 
       extraSettings = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Will be inserted in the Settings section of the config file.
         '';
@@ -149,6 +152,7 @@ in
 
       extraDefaults = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Will be inserted in the Default section of the config file.
         '';
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
index dde24522756..ca323e495ec 100644
--- a/nixos/modules/services/networking/blockbook-frontend.nix
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -158,15 +158,21 @@ let
         type = types.attrs;
         default = {};
         example = literalExample '' {
-          alternative_estimate_fee = "whatthefee-disabled";
-          alternative_estimate_fee_params = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
-          fiat_rates = "coingecko";
-          fiat_rates_params = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
-          coin_shortcut = "BTC";
-          coin_label = "Bitcoin";
-          xpub_magic = 76067358;
-          xpub_magic_segwit_p2sh = 77429938;
-          xpub_magic_segwit_native = 78792518;
+          "alternative_estimate_fee" = "whatthefee-disabled";
+          "alternative_estimate_fee_params" = "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}";
+          "fiat_rates" = "coingecko";
+          "fiat_rates_params" = "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 60}";
+          "coin_shortcut" = "BTC";
+          "coin_label" = "Bitcoin";
+          "parse" = true;
+          "subversion" = "";
+          "address_format" = "";
+          "xpub_magic" = 76067358;
+          "xpub_magic_segwit_p2sh" = 77429938;
+          "xpub_magic_segwit_native" = 78792518;
+          "mempool_workers" = 8;
+          "mempool_sub_workers" = 2;
+          "block_addresses_to_keep" = 300;
         }'';
         description = ''
           Additional configurations to be appended to <filename>coin.conf</filename>.
diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix
index 5f8ac96b229..f1a504b3e3f 100644
--- a/nixos/modules/services/networking/cjdns.nix
+++ b/nixos/modules/services/networking/cjdns.nix
@@ -12,8 +12,18 @@ let
   { ... }:
   { options =
     { password = mkOption {
-      type = types.str;
-      description = "Authorized password to the opposite end of the tunnel.";
+        type = types.str;
+        description = "Authorized password to the opposite end of the tunnel.";
+      };
+      login = mkOption {
+        default = "";
+        type = types.str;
+        description = "(optional) name your peer has for you";
+      };
+      peerName = mkOption {
+        default = "";
+        type = types.str;
+        description = "(optional) human-readable name for peer";
       };
       publicKey = mkOption {
         type = types.str;
@@ -245,7 +255,7 @@ in
         fi
 
         if [ -z "$CJDNS_ADMIN_PASSWORD" ]; then
-            echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 96)" \
+            echo "CJDNS_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" \
                 >> /etc/cjdns.keys
         fi
       '';
@@ -264,10 +274,10 @@ in
          ''
       );
 
+      startLimitIntervalSec = 0;
       serviceConfig = {
         Type = "forking";
         Restart = "always";
-        StartLimitInterval = 0;
         RestartSec = 1;
         CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID";
         ProtectSystem = true;
diff --git a/nixos/modules/services/networking/cntlm.nix b/nixos/modules/services/networking/cntlm.nix
index 5b5068e43d7..eea28e12ce0 100644
--- a/nixos/modules/services/networking/cntlm.nix
+++ b/nixos/modules/services/networking/cntlm.nix
@@ -36,19 +36,21 @@ in
     enable = mkEnableOption "cntlm, which starts a local proxy";
 
     username = mkOption {
+      type = types.str;
       description = ''
         Proxy account name, without the possibility to include domain name ('at' sign is interpreted literally).
       '';
     };
 
     domain = mkOption {
-      description = ''Proxy account domain/workgroup name.'';
+      type = types.str;
+      description = "Proxy account domain/workgroup name.";
     };
 
     password = mkOption {
       default = "/etc/cntlm.password";
       type = types.str;
-      description = ''Proxy account password. Note: use chmod 0600 on /etc/cntlm.password for security.'';
+      description = "Proxy account password. Note: use chmod 0600 on /etc/cntlm.password for security.";
     };
 
     netbios_hostname = mkOption {
@@ -60,6 +62,7 @@ in
     };
 
     proxy = mkOption {
+      type = types.listOf types.str;
       description = ''
         A list of NTLM/NTLMv2 authenticating HTTP proxies.
 
@@ -75,11 +78,13 @@ in
         A list of domains where the proxy is skipped.
       '';
       default = [];
+      type = types.listOf types.str;
       example = [ "*.example.com" "example.com" ];
     };
 
     port = mkOption {
       default = [3128];
+      type = types.listOf types.port;
       description = "Specifies on which ports the cntlm daemon listens.";
     };
 
diff --git a/nixos/modules/services/networking/connman.nix b/nixos/modules/services/networking/connman.nix
index 6ccc2dffb26..11f66b05df1 100644
--- a/nixos/modules/services/networking/connman.nix
+++ b/nixos/modules/services/networking/connman.nix
@@ -42,8 +42,7 @@ in {
 
       extraConfig = mkOption {
         type = types.lines;
-        default = ''
-        '';
+        default = "";
         description = ''
           Configuration lines appended to the generated connman configuration file.
         '';
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index f7d2afead06..ae7998913ee 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -99,6 +99,7 @@ in
 
       extraConfig = mkOption {
         default = { };
+        type = types.attrsOf types.anything;
         description = ''
           Extra configuration options which are serialized to json and added
           to the config.json file.
@@ -190,7 +191,7 @@ in
           ExecStop = "${cfg.package}/bin/consul leave";
         });
 
-        path = with pkgs; [ iproute gnugrep gawk consul ];
+        path = with pkgs; [ iproute2 gnugrep gawk consul ];
         preStart = ''
           mkdir -m 0700 -p ${dataDir}
           chown -R consul ${dataDir}
diff --git a/nixos/modules/services/networking/corerad.nix b/nixos/modules/services/networking/corerad.nix
index d90a5923bc6..e76ba9a2d00 100644
--- a/nixos/modules/services/networking/corerad.nix
+++ b/nixos/modules/services/networking/corerad.nix
@@ -4,13 +4,7 @@ with lib;
 
 let
   cfg = config.services.corerad;
-
-  writeTOML = name: x:
-    pkgs.runCommandNoCCLocal name {
-      passAsFile = ["config"];
-      config = builtins.toJSON x;
-      buildInputs = [ pkgs.go-toml ];
-    } "jsontoml < $configPath > $out";
+  settingsFormat = pkgs.formats.toml {};
 
 in {
   meta.maintainers = with maintainers; [ mdlayher ];
@@ -19,7 +13,7 @@ in {
     enable = mkEnableOption "CoreRAD IPv6 NDP RA daemon";
 
     settings = mkOption {
-      type = types.uniq types.attrs;
+      type = settingsFormat.type;
       example = literalExample ''
         {
           interfaces = [
@@ -43,7 +37,7 @@ in {
         }
       '';
       description = ''
-        Configuration for CoreRAD, see <link xlink:href="https://github.com/mdlayher/corerad/blob/master/internal/config/default.toml"/>
+        Configuration for CoreRAD, see <link xlink:href="https://github.com/mdlayher/corerad/blob/main/internal/config/reference.toml"/>
         for supported values. Ignored if configFile is set.
       '';
     };
@@ -64,7 +58,7 @@ in {
 
   config = mkIf cfg.enable {
     # Prefer the config file over settings if both are set.
-    services.corerad.configFile = mkDefault (writeTOML "corerad.toml" cfg.settings);
+    services.corerad.configFile = mkDefault (settingsFormat.generate "corerad.toml" cfg.settings);
 
     systemd.services.corerad = {
       description = "CoreRAD IPv6 NDP RA daemon";
diff --git a/nixos/modules/services/networking/coturn.nix b/nixos/modules/services/networking/coturn.nix
index 1bfbc307c59..5f7d2893ae2 100644
--- a/nixos/modules/services/networking/coturn.nix
+++ b/nixos/modules/services/networking/coturn.nix
@@ -16,6 +16,7 @@ ${lib.optionalString cfg.lt-cred-mech "lt-cred-mech"}
 ${lib.optionalString cfg.no-auth "no-auth"}
 ${lib.optionalString cfg.use-auth-secret "use-auth-secret"}
 ${lib.optionalString (cfg.static-auth-secret != null) ("static-auth-secret=${cfg.static-auth-secret}")}
+${lib.optionalString (cfg.static-auth-secret-file != null) ("static-auth-secret=#static-auth-secret#")}
 realm=${cfg.realm}
 ${lib.optionalString cfg.no-udp "no-udp"}
 ${lib.optionalString cfg.no-tcp "no-tcp"}
@@ -182,6 +183,13 @@ in {
           by a separate program, so this is why that other mode is 'dynamic'.
         '';
       };
+      static-auth-secret-file = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the file containing the static authentication secret.
+        '';
+      };
       realm = mkOption {
         type = types.str;
         default = config.networking.hostName;
@@ -293,42 +301,63 @@ in {
     };
   };
 
-  config = mkIf cfg.enable {
-    users.users.turnserver =
-      { uid = config.ids.uids.turnserver;
-        description = "coturn TURN server user";
-      };
-    users.groups.turnserver =
-      { gid = config.ids.gids.turnserver;
-        members = [ "turnserver" ];
-      };
+  config = mkIf cfg.enable (mkMerge ([
+    { assertions = [
+      { assertion = cfg.static-auth-secret != null -> cfg.static-auth-secret-file == null ;
+        message = "static-auth-secret and static-auth-secret-file cannot be set at the same time";
+      }
+    ];}
 
-    systemd.services.coturn = {
-      description = "coturn TURN server";
-      after = [ "network-online.target" ];
-      wants = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
+    {
+      users.users.turnserver =
+        { uid = config.ids.uids.turnserver;
+          description = "coturn TURN server user";
+        };
+      users.groups.turnserver =
+        { gid = config.ids.gids.turnserver;
+          members = [ "turnserver" ];
+        };
 
-      unitConfig = {
-        Documentation = "man:coturn(1) man:turnadmin(1) man:turnserver(1)";
-      };
+      systemd.services.coturn = let
+        runConfig = "/run/coturn/turnserver.cfg";
+      in {
+        description = "coturn TURN server";
+        after = [ "network-online.target" ];
+        wants = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
 
-      serviceConfig = {
-        Type = "simple";
-        ExecStart = "${pkgs.coturn}/bin/turnserver -c ${configFile}";
-        RuntimeDirectory = "turnserver";
-        User = "turnserver";
-        Group = "turnserver";
-        AmbientCapabilities =
-          mkIf (
-            cfg.listening-port < 1024 ||
-            cfg.alt-listening-port < 1024 ||
-            cfg.tls-listening-port < 1024 ||
-            cfg.alt-tls-listening-port < 1024 ||
-            cfg.min-port < 1024
-          ) "cap_net_bind_service";
-        Restart = "on-abort";
-      };
-    };
-  };
+        unitConfig = {
+          Documentation = "man:coturn(1) man:turnadmin(1) man:turnserver(1)";
+        };
+
+        preStart = ''
+          cat ${configFile} > ${runConfig}
+          ${optionalString (cfg.static-auth-secret-file != null) ''
+            STATIC_AUTH_SECRET="$(head -n1 ${cfg.static-auth-secret-file} || :)"
+            sed -e "s,#static-auth-secret#,$STATIC_AUTH_SECRET,g" \
+              -i ${runConfig}
+          '' }
+          chmod 640 ${runConfig}
+        '';
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${pkgs.coturn}/bin/turnserver -c ${runConfig}";
+          RuntimeDirectory = "turnserver";
+          User = "turnserver";
+          Group = "turnserver";
+          AmbientCapabilities =
+            mkIf (
+              cfg.listening-port < 1024 ||
+              cfg.alt-listening-port < 1024 ||
+              cfg.tls-listening-port < 1024 ||
+              cfg.alt-tls-listening-port < 1024 ||
+              cfg.min-port < 1024
+            ) "cap_net_bind_service";
+          Restart = "on-abort";
+        };
+      };
+    systemd.tmpfiles.rules = [
+      "d  /run/coturn 0700 turnserver turnserver - -"
+    ];
+  }]));
 }
diff --git a/nixos/modules/services/networking/croc.nix b/nixos/modules/services/networking/croc.nix
new file mode 100644
index 00000000000..9466adf71d8
--- /dev/null
+++ b/nixos/modules/services/networking/croc.nix
@@ -0,0 +1,86 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib) types;
+  cfg = config.services.croc;
+  rootDir = "/run/croc";
+in
+{
+  options.services.croc = {
+    enable = lib.mkEnableOption "croc relay";
+    ports = lib.mkOption {
+      type = with types; listOf port;
+      default = [9009 9010 9011 9012 9013];
+      description = "Ports of the relay.";
+    };
+    pass = lib.mkOption {
+      type = with types; either path str;
+      default = "pass123";
+      description = "Password or passwordfile for the relay.";
+    };
+    openFirewall = lib.mkEnableOption "opening of the peer port(s) in the firewall";
+    debug = lib.mkEnableOption "debug logs";
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.croc = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.croc}/bin/croc --pass '${cfg.pass}' ${lib.optionalString cfg.debug "--debug"} relay --ports ${lib.concatMapStringsSep "," toString cfg.ports}";
+        # The following options are only for optimizing:
+        # systemd-analyze security croc
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
+        DynamicUser = true;
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        MountAPIVFS = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = lib.mkDefault false;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "noaccess";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RootDirectory = rootDir;
+        # Avoid mounting rootDir in the own rootDir of ExecStart='s mount namespace.
+        InaccessiblePaths = [ "-+${rootDir}" ];
+        BindReadOnlyPaths = [
+          builtins.storeDir
+        ] ++ lib.optional (types.path.check cfg.pass) cfg.pass;
+        # This is for BindReadOnlyPaths=
+        # to allow traversal of directories they create in RootDirectory=.
+        UMask = "0066";
+        # Create rootDir in the host's mount namespace.
+        RuntimeDirectory = [(baseNameOf rootDir)];
+        RuntimeDirectoryMode = "700";
+        SystemCallFilter = [
+          "@system-service"
+          "~@aio" "~@keyring" "~@memlock" "~@privileged" "~@resources" "~@setuid" "~@sync" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall cfg.ports;
+  };
+
+  meta.maintainers = with lib.maintainers; [ hax404 julm ];
+}
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index 053efe71270..7820eedd932 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -18,6 +18,7 @@ let
     ${lib.optionalString (cfg.zone != "")   "zone=${cfg.zone}"}
     ssl=${boolToStr cfg.ssl}
     wildcard=YES
+    ipv6=${boolToStr cfg.ipv6}
     quiet=${boolToStr cfg.quiet}
     verbose=${boolToStr cfg.verbose}
     ${cfg.extraConfig}
@@ -116,7 +117,15 @@ with lib;
         default = true;
         type = bool;
         description = ''
-          Whether to use to use SSL/TLS to connect to dynamic DNS provider.
+          Whether to use SSL/TLS to connect to dynamic DNS provider.
+        '';
+      };
+
+      ipv6 = mkOption {
+        default = false;
+        type = bool;
+        description = ''
+          Whether to use IPv6.
         '';
       };
 
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index 0507b739d49..31e4b6ad298 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -69,6 +69,11 @@ let
         if-carrier-up = "";
       }.${cfg.wait}}
 
+      ${optionalString (config.networking.enableIPv6 == false) ''
+        # Don't solicit or accept IPv6 Router Advertisements and DHCPv6 if disabled IPv6
+        noipv6
+      ''}
+
       ${cfg.extraConfig}
     '';
 
@@ -186,9 +191,8 @@ in
       { description = "DHCP Client";
 
         wantedBy = [ "multi-user.target" ] ++ optional (!hasDefaultGatewaySet) "network-online.target";
-        wants = [ "network.target" "systemd-udev-settle.service" ];
+        wants = [ "network.target" ];
         before = [ "network-online.target" ];
-        after = [ "systemd-udev-settle.service" ];
 
         restartTriggers = [ exitHook ];
 
diff --git a/nixos/modules/services/networking/dnscrypt-proxy2.nix b/nixos/modules/services/networking/dnscrypt-proxy2.nix
index 28691e83827..72965c267a8 100644
--- a/nixos/modules/services/networking/dnscrypt-proxy2.nix
+++ b/nixos/modules/services/networking/dnscrypt-proxy2.nix
@@ -11,7 +11,7 @@ in
     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"/>
+        For available params, see: <link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/${pkgs.dnscrypt-proxy2.version}/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>
       '';
       example = literalExample ''
         {
@@ -27,6 +27,16 @@ in
       default = {};
     };
 
+    upstreamDefaults = mkOption {
+      description = ''
+        Whether to base the config declared in <literal>services.dnscrypt-proxy2.settings</literal> on the upstream example config (<link xlink:href="https://github.com/DNSCrypt/dnscrypt-proxy/blob/master/dnscrypt-proxy/example-dnscrypt-proxy.toml"/>)
+
+        Disable this if you want to declare your dnscrypt config from scratch.
+      '';
+      type = types.bool;
+      default = true;
+    };
+
     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"/>
@@ -38,7 +48,13 @@ in
         json = builtins.toJSON cfg.settings;
         passAsFile = [ "json" ];
       } ''
-        ${pkgs.remarshal}/bin/json2toml < $jsonPath > $out
+        ${if cfg.upstreamDefaults then ''
+          ${pkgs.remarshal}/bin/toml2json ${pkgs.dnscrypt-proxy2.src}/dnscrypt-proxy/example-dnscrypt-proxy.toml > example.json
+          ${pkgs.jq}/bin/jq --slurp add example.json $jsonPath > config.json # merges the two
+        '' else ''
+          cp $jsonPath config.json
+        ''}
+        ${pkgs.remarshal}/bin/json2toml < config.json > $out
       '';
       defaultText = literalExample "TOML file generated from services.dnscrypt-proxy2.settings";
     };
@@ -49,13 +65,56 @@ in
     networking.nameservers = lib.mkDefault [ "127.0.0.1" ];
 
     systemd.services.dnscrypt-proxy2 = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
+      description = "DNSCrypt-proxy client";
+      wants = [
+        "network-online.target"
+        "nss-lookup.target"
+      ];
+      before = [
+        "nss-lookup.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
       serviceConfig = {
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        CacheDirectory = "dnscrypt-proxy";
         DynamicUser = true;
         ExecStart = "${pkgs.dnscrypt-proxy2}/bin/dnscrypt-proxy -config ${cfg.configFile}";
+        LockPersonality = true;
+        LogsDirectory = "dnscrypt-proxy";
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        NonBlocking = true;
+        PrivateDevices = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
         Restart = "always";
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RuntimeDirectory = "dnscrypt-proxy";
+        StateDirectory = "dnscrypt-proxy";
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "@chown"
+          "~@aio"
+          "~@keyring"
+          "~@memlock"
+          "~@resources"
+          "~@setuid"
+          "~@timer"
+        ];
       };
     };
   };
diff --git a/nixos/modules/services/networking/dnscrypt-wrapper.nix b/nixos/modules/services/networking/dnscrypt-wrapper.nix
index b9333cd19a2..89360f4bf37 100644
--- a/nixos/modules/services/networking/dnscrypt-wrapper.nix
+++ b/nixos/modules/services/networking/dnscrypt-wrapper.nix
@@ -55,7 +55,10 @@ let
   rotateKeys = ''
     # check if keys are not expired
     keyValid() {
-      fingerprint=$(dnscrypt-wrapper --show-provider-publickey | awk '{print $(NF)}')
+      fingerprint=$(dnscrypt-wrapper \
+        --show-provider-publickey \
+        --provider-publickey-file=${publicKey} \
+        | awk '{print $(NF)}')
       dnscrypt-proxy --test=${toString (cfg.keys.checkInterval + 1)} \
         --resolver-address=127.0.0.1:${toString cfg.port} \
         --provider-name=${cfg.providerName} \
@@ -80,7 +83,7 @@ let
   # correctly implement key rotation of dnscrypt-wrapper ephemeral keys.
   dnscrypt-proxy1 = pkgs.callPackage
     ({ stdenv, fetchFromGitHub, autoreconfHook
-    , pkgconfig, libsodium, ldns, openssl, systemd }:
+    , pkg-config, libsodium, ldns, openssl, systemd }:
 
     stdenv.mkDerivation rec {
       pname = "dnscrypt-proxy";
@@ -95,7 +98,7 @@ let
 
       configureFlags = optional stdenv.isLinux "--with-systemd";
 
-      nativeBuildInputs = [ autoreconfHook pkgconfig ];
+      nativeBuildInputs = [ autoreconfHook pkg-config ];
 
       # <ldns/ldns.h> depends on <openssl/ssl.h>
       buildInputs = [ libsodium openssl.dev ldns ] ++ optional stdenv.isLinux systemd;
diff --git a/nixos/modules/services/networking/dnsdist.nix b/nixos/modules/services/networking/dnsdist.nix
index 8249da69bc1..c7c6a79864c 100644
--- a/nixos/modules/services/networking/dnsdist.nix
+++ b/nixos/modules/services/networking/dnsdist.nix
@@ -4,10 +4,10 @@ with lib;
 
 let
   cfg = config.services.dnsdist;
-  configFile = pkgs.writeText "dndist.conf" ''
+  configFile = pkgs.writeText "dnsdist.conf" ''
     setLocal('${cfg.listenAddress}:${toString cfg.listenPort}')
     ${cfg.extraConfig}
-    '';
+  '';
 in {
   options = {
     services.dnsdist = {
@@ -26,8 +26,7 @@ in {
 
       extraConfig = mkOption {
         type = types.lines;
-        default = ''
-        '';
+        default = "";
         description = ''
           Extra lines to be added verbatim to dnsdist.conf.
         '';
@@ -35,25 +34,19 @@ in {
     };
   };
 
-  config = mkIf config.services.dnsdist.enable {
+  config = mkIf cfg.enable {
+    systemd.packages = [ pkgs.dnsdist ];
+
     systemd.services.dnsdist = {
-      description = "dnsdist load balancer";
       wantedBy = [ "multi-user.target" ];
-      after = ["network.target"];
 
+      startLimitIntervalSec = 0;
       serviceConfig = {
-        Restart="on-failure";
-        RestartSec="1";
         DynamicUser = true;
-        StartLimitInterval="0";
-        PrivateDevices=true;
-        AmbientCapabilities="CAP_NET_BIND_SERVICE";
-        CapabilityBoundingSet="CAP_NET_BIND_SERVICE";
-        ExecStart = "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}";
-        ProtectHome=true;
-        RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
-        LimitNOFILE="16384";
-        TasksMax="8192";
+
+        # upstream overrides for better nixos compatibility
+        ExecStartPre = [ "" "${pkgs.dnsdist}/bin/dnsdist --check-config --config ${configFile}" ];
+        ExecStart = [ "" "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}" ];
       };
     };
   };
diff --git a/nixos/modules/services/networking/doh-proxy-rust.nix b/nixos/modules/services/networking/doh-proxy-rust.nix
new file mode 100644
index 00000000000..0e55bc38665
--- /dev/null
+++ b/nixos/modules/services/networking/doh-proxy-rust.nix
@@ -0,0 +1,60 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.doh-proxy-rust;
+
+in {
+
+  options.services.doh-proxy-rust = {
+
+    enable = mkEnableOption "doh-proxy-rust";
+
+    flags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = literalExample [ "--server-address=9.9.9.9:53" ];
+      description = ''
+        A list of command-line flags to pass to doh-proxy. For details on the
+        available options, see <link xlink:href="https://github.com/jedisct1/doh-server#usage"/>.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.doh-proxy-rust = {
+      description = "doh-proxy-rust";
+      after = [ "network.target" "nss-lookup.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.doh-proxy-rust}/bin/doh-proxy ${escapeShellArgs cfg.flags}";
+        Restart = "always";
+        RestartSec = 10;
+        DynamicUser = true;
+
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        ProtectClock = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        RemoveIPC = true;
+        RestrictAddressFamilies = "AF_INET AF_INET6";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ stephank ];
+
+}
diff --git a/nixos/modules/services/networking/epmd.nix b/nixos/modules/services/networking/epmd.nix
index 692b75e4f08..f7cdc0fe79c 100644
--- a/nixos/modules/services/networking/epmd.nix
+++ b/nixos/modules/services/networking/epmd.nix
@@ -53,4 +53,6 @@ in
       };
     };
   };
+
+  meta.maintainers = teams.beam.members;
 }
diff --git a/nixos/modules/services/networking/firefox/sync-server.nix b/nixos/modules/services/networking/firefox/sync-server.nix
index 6842aa73561..24f76864953 100644
--- a/nixos/modules/services/networking/firefox/sync-server.nix
+++ b/nixos/modules/services/networking/firefox/sync-server.nix
@@ -67,7 +67,7 @@ in
       };
 
       listen.port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 5000;
         description = ''
           Port on which the sync server listen to.
diff --git a/nixos/modules/services/networking/flannel.nix b/nixos/modules/services/networking/flannel.nix
index 4c040112d28..32a7eb3ed69 100644
--- a/nixos/modules/services/networking/flannel.nix
+++ b/nixos/modules/services/networking/flannel.nix
@@ -162,10 +162,7 @@ in {
         NODE_NAME = cfg.nodeName;
       };
       path = [ pkgs.iptables ];
-      preStart = ''
-        mkdir -p /run/flannel
-        touch /run/flannel/docker
-      '' + optionalString (cfg.storageBackend == "etcd") ''
+      preStart = optionalString (cfg.storageBackend == "etcd") ''
         echo "setting network configuration"
         until ${pkgs.etcdctl}/bin/etcdctl set /coreos.com/network/config '${builtins.toJSON networkConfig}'
         do
@@ -177,6 +174,7 @@ in {
         ExecStart = "${cfg.package}/bin/flannel";
         Restart = "always";
         RestartSec = "10s";
+        RuntimeDirectory = "flannel";
       };
     };
 
diff --git a/nixos/modules/services/networking/flashpolicyd.nix b/nixos/modules/services/networking/flashpolicyd.nix
deleted file mode 100644
index 7f25083307c..00000000000
--- a/nixos/modules/services/networking/flashpolicyd.nix
+++ /dev/null
@@ -1,85 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.flashpolicyd;
-
-  flashpolicyd = pkgs.stdenv.mkDerivation {
-    name = "flashpolicyd-0.6";
-
-    src = pkgs.fetchurl {
-      name = "flashpolicyd_v0.6.zip";
-      url = "https://download.adobe.com/pub/adobe/devnet/flashplayer/articles/socket_policy_files/flashpolicyd_v0.6.zip";
-      sha256 = "16zk237233npwfq1m4ksy4g5lzy1z9fp95w7pz0cdlpmv0fv9sm3";
-    };
-
-    buildInputs = [ pkgs.unzip pkgs.perl ];
-
-    installPhase = "mkdir $out; cp -pr * $out/; chmod +x $out/*/*.pl";
-  };
-
-  flashpolicydWrapper = pkgs.writeScriptBin "flashpolicyd"
-    ''
-      #! ${pkgs.runtimeShell}
-      exec ${flashpolicyd}/Perl_xinetd/in.flashpolicyd.pl \
-        --file=${pkgs.writeText "flashpolixy.xml" cfg.policy} \
-        2> /dev/null
-    '';
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.flashpolicyd = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description =
-          ''
-            Whether to enable the Flash Policy server.  This is
-            necessary if you want Flash applications to make
-            connections to your server.
-          '';
-      };
-
-      policy = mkOption {
-        default =
-          ''
-            <?xml version="1.0"?>
-            <!DOCTYPE cross-domain-policy SYSTEM "/xml/dtds/cross-domain-policy.dtd">
-            <cross-domain-policy>
-              <site-control permitted-cross-domain-policies="master-only"/>
-              <allow-access-from domain="*" to-ports="*" />
-            </cross-domain-policy>
-          '';
-        description = "The policy to be served.  The default is to allow connections from any domain to any port.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    services.xinetd.enable = true;
-
-    services.xinetd.services = singleton
-      { name = "flashpolicy";
-        port = 843;
-        unlisted = true;
-        server = "${flashpolicydWrapper}/bin/flashpolicyd";
-      };
-
-  };
-
-}
diff --git a/nixos/modules/services/networking/gale.nix b/nixos/modules/services/networking/gale.nix
deleted file mode 100644
index cb954fd836b..00000000000
--- a/nixos/modules/services/networking/gale.nix
+++ /dev/null
@@ -1,181 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.gale;
-  # we convert the path to a string to avoid it being copied to the nix store,
-  # otherwise users could read the private key as all files in the store are
-  # world-readable
-  keyPath = toString cfg.keyPath;
-  # ...but we refer to the pubkey file using a path so that we can ensure the
-  # config gets rebuilt if the public key changes (we can assume the private key
-  # will never change without the public key having changed)
-  gpubFile = cfg.keyPath + "/${cfg.domain}.gpub";
-  home = "/var/lib/gale";
-  keysPrepared = cfg.keyPath != null && lib.pathExists cfg.keyPath;
-in
-{
-  options = {
-    services.gale = {
-      enable = mkEnableOption "the Gale messaging daemon";
-
-      user = mkOption {
-        default = "gale";
-        type = types.str;
-        description = "Username for the Gale daemon.";
-      };
-
-      group = mkOption {
-        default = "gale";
-        type = types.str;
-        description = "Group name for the Gale daemon.";
-      };
-
-      setuidWrapper = mkOption {
-        default = null;
-        description = "Configuration for the Gale gksign setuid wrapper.";
-      };
-
-      domain = mkOption {
-        default = "";
-        type = types.str;
-        description = "Domain name for the Gale system.";
-      };
-
-      keyPath = mkOption {
-        default = null;
-        type = types.nullOr types.path;
-        description = ''
-          Directory containing the key pair for this Gale domain.  The expected
-          filename will be taken from the domain option with ".gpri" and ".gpub"
-          appended.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Additional text to be added to <filename>/etc/gale/conf</filename>.
-        '';
-      };
-    };
-  };
-
-  config = mkMerge [
-    (mkIf cfg.enable {
-       assertions = [{
-         assertion = cfg.domain != "";
-         message = "A domain must be set for Gale.";
-       }];
-
-       warnings = mkIf (!keysPrepared) [
-         "You must run gale-install in order to generate a domain key."
-       ];
-
-       system.activationScripts.gale = mkIf cfg.enable (
-         stringAfter [ "users" "groups" ] ''
-           chmod 755 ${home}
-           mkdir -m 0777 -p ${home}/auth/cache
-           mkdir -m 1777 -p ${home}/auth/local # GALE_DOMAIN.gpub
-           mkdir -m 0700 -p ${home}/auth/private # ROOT.gpub
-           mkdir -m 0755 -p ${home}/auth/trusted # ROOT
-           mkdir -m 0700 -p ${home}/.gale
-           mkdir -m 0700 -p ${home}/.gale/auth
-           mkdir -m 0700 -p ${home}/.gale/auth/private # GALE_DOMAIN.gpri
-
-           ln -sf ${pkgs.gale}/etc/gale/auth/trusted/ROOT "${home}/auth/trusted/ROOT"
-           chown ${cfg.user}:${cfg.group} ${home} ${home}/auth ${home}/auth/*
-           chown ${cfg.user}:${cfg.group} ${home}/.gale ${home}/.gale/auth ${home}/.gale/auth/private
-         ''
-       );
-
-       environment = {
-         etc = {
-           "gale/auth".source = home + "/auth"; # symlink /var/lib/gale/auth
-           "gale/conf".text = ''
-             GALE_USER ${cfg.user}
-             GALE_DOMAIN ${cfg.domain}
-             ${cfg.extraConfig}
-           '';
-         };
-
-         systemPackages = [ pkgs.gale ];
-       };
-
-       users.users.${cfg.user} = {
-         description = "Gale daemon";
-         uid = config.ids.uids.gale;
-         group = cfg.group;
-         home = home;
-         createHome = true;
-       };
-
-       users.groups = [{
-         name = cfg.group;
-         gid = config.ids.gids.gale;
-       }];
-    })
-    (mkIf (cfg.enable && keysPrepared) {
-       assertions = [
-         {
-           assertion = cfg.keyPath != null
-                    && lib.pathExists (cfg.keyPath + "/${cfg.domain}.gpub");
-           message = "Couldn't find a Gale public key for ${cfg.domain}.";
-         }
-         {
-           assertion = cfg.keyPath != null
-                    && lib.pathExists (cfg.keyPath + "/${cfg.domain}.gpri");
-           message = "Couldn't find a Gale private key for ${cfg.domain}.";
-         }
-       ];
-
-       services.gale.setuidWrapper = {
-         program = "gksign";
-         source = "${pkgs.gale}/bin/gksign";
-         owner = cfg.user;
-         group = cfg.group;
-         setuid = true;
-         setgid = false;
-       };
-
-       security.wrappers.gksign = cfg.setuidWrapper;
-
-       systemd.services.gale-galed = {
-         description = "Gale messaging daemon";
-         wantedBy = [ "multi-user.target" ];
-         wants = [ "gale-gdomain.service" ];
-         after = [ "network.target" ];
-
-         preStart = ''
-           install -m 0640 -o ${cfg.user} -g ${cfg.group} ${keyPath}/${cfg.domain}.gpri "${home}/.gale/auth/private/"
-           install -m 0644 -o ${cfg.user} -g ${cfg.group} ${gpubFile} "${home}/.gale/auth/private/${cfg.domain}.gpub"
-           install -m 0644 -o ${cfg.user} -g ${cfg.group} ${gpubFile} "${home}/auth/local/${cfg.domain}.gpub"
-         '';
-
-         serviceConfig = {
-           Type = "forking";
-           ExecStart = "@${pkgs.gale}/bin/galed galed";
-           User = cfg.user;
-           Group = cfg.group;
-           PermissionsStartOnly = true;
-         };
-       };
-
-       systemd.services.gale-gdomain = {
-         description = "Gale AKD daemon";
-         wantedBy = [ "multi-user.target" ];
-         requires = [ "gale-galed.service" ];
-         after = [ "gale-galed.service" ];
-
-         serviceConfig = {
-           Type = "forking";
-           ExecStart = "@${pkgs.gale}/bin/gdomain gdomain";
-           User = cfg.user;
-           Group = cfg.group;
-         };
-       };
-    })
-  ];
-}
diff --git a/nixos/modules/services/networking/gateone.nix b/nixos/modules/services/networking/gateone.nix
index 56f2ba21a12..3e3a3c1aa94 100644
--- a/nixos/modules/services/networking/gateone.nix
+++ b/nixos/modules/services/networking/gateone.nix
@@ -10,12 +10,12 @@ options = {
       pidDir = mkOption {
         default = "/run/gateone";
         type = types.path;
-        description = ''Path of pid files for GateOne.'';
+        description = "Path of pid files for GateOne.";
       };
       settingsDir = mkOption {
         default = "/var/lib/gateone";
         type = types.path;
-        description = ''Path of configuration files for GateOne.'';
+        description = "Path of configuration files for GateOne.";
       };
     };
 };
diff --git a/nixos/modules/services/networking/ghostunnel.nix b/nixos/modules/services/networking/ghostunnel.nix
new file mode 100644
index 00000000000..58a51df6cca
--- /dev/null
+++ b/nixos/modules/services/networking/ghostunnel.nix
@@ -0,0 +1,242 @@
+{ config, lib, pkgs, ... }:
+let
+  inherit (lib)
+    attrValues
+    concatMap
+    concatStringsSep
+    escapeShellArg
+    literalExample
+    mapAttrs'
+    mkDefault
+    mkEnableOption
+    mkIf
+    mkOption
+    nameValuePair
+    optional
+    types
+    ;
+
+  mainCfg = config.services.ghostunnel;
+
+  module = { config, name, ... }:
+    {
+      options = {
+
+        listen = mkOption {
+          description = ''
+            Address and port to listen on (can be HOST:PORT, unix:PATH).
+          '';
+          type = types.str;
+        };
+
+        target = mkOption {
+          description = ''
+            Address to forward connections to (can be HOST:PORT or unix:PATH).
+          '';
+          type = types.str;
+        };
+
+        keystore = mkOption {
+          description = ''
+            Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
+
+            NB: storepass is not supported because it would expose credentials via <code>/proc/*/cmdline</code>.
+
+            Specify this or <code>cert</code> and <code>key</code>.
+          '';
+          type = types.nullOr types.str;
+          default = null;
+        };
+
+        cert = mkOption {
+          description = ''
+            Path to certificate (PEM with certificate chain).
+
+            Not required if <code>keystore</code> is set.
+          '';
+          type = types.nullOr types.str;
+          default = null;
+        };
+
+        key = mkOption {
+          description = ''
+            Path to certificate private key (PEM with private key).
+
+            Not required if <code>keystore</code> is set.
+          '';
+          type = types.nullOr types.str;
+          default = null;
+        };
+
+        cacert = mkOption {
+          description = ''
+            Path to CA bundle file (PEM/X509). Uses system trust store if <code>null</code>.
+          '';
+          type = types.nullOr types.str;
+        };
+
+        disableAuthentication = mkOption {
+          description = ''
+            Disable client authentication, no client certificate will be required.
+          '';
+          type = types.bool;
+          default = false;
+        };
+
+        allowAll = mkOption {
+          description = ''
+            If true, allow all clients, do not check client cert subject.
+          '';
+          type = types.bool;
+          default = false;
+        };
+
+        allowCN = mkOption {
+          description = ''
+            Allow client if common name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        allowOU = mkOption {
+          description = ''
+            Allow client if organizational unit name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        allowDNS = mkOption {
+          description = ''
+            Allow client if DNS subject alternative name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        allowURI = mkOption {
+          description = ''
+            Allow client if URI subject alternative name appears in the list.
+          '';
+          type = types.listOf types.str;
+          default = [];
+        };
+
+        extraArguments = mkOption {
+          description = "Extra arguments to pass to <code>ghostunnel server</code>";
+          type = types.separatedString " ";
+          default = "";
+        };
+
+        unsafeTarget = mkOption {
+          description = ''
+            If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
+
+            This is meant to protect against accidental unencrypted traffic on
+            untrusted networks.
+          '';
+          type = types.bool;
+          default = false;
+        };
+
+        # Definitions to apply at the root of the NixOS configuration.
+        atRoot = mkOption {
+          internal = true;
+        };
+      };
+
+      # Clients should not be authenticated with the public root certificates
+      # (afaict, it doesn't make sense), so we only provide that default when
+      # client cert auth is disabled.
+      config.cacert = mkIf config.disableAuthentication (mkDefault null);
+
+      config.atRoot = {
+        assertions = [
+          { message = ''
+              services.ghostunnel.servers.${name}: At least one access control flag is required.
+              Set at least one of:
+                - services.ghostunnel.servers.${name}.disableAuthentication
+                - services.ghostunnel.servers.${name}.allowAll
+                - services.ghostunnel.servers.${name}.allowCN
+                - services.ghostunnel.servers.${name}.allowOU
+                - services.ghostunnel.servers.${name}.allowDNS
+                - services.ghostunnel.servers.${name}.allowURI
+            '';
+            assertion = config.disableAuthentication
+              || config.allowAll
+              || config.allowCN != []
+              || config.allowOU != []
+              || config.allowDNS != []
+              || config.allowURI != []
+              ;
+          }
+        ];
+
+        systemd.services."ghostunnel-server-${name}" = {
+          after = [ "network.target" ];
+          wants = [ "network.target" ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            Restart = "always";
+            AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+            DynamicUser = true;
+            LoadCredential = optional (config.keystore != null) "keystore:${config.keystore}"
+              ++ optional (config.cert != null) "cert:${config.cert}"
+              ++ optional (config.key != null) "key:${config.key}"
+              ++ optional (config.cacert != null) "cacert:${config.cacert}";
+           };
+          script = concatStringsSep " " (
+            [ "${mainCfg.package}/bin/ghostunnel" ]
+            ++ optional (config.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
+            ++ optional (config.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
+            ++ optional (config.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
+            ++ optional (config.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
+            ++ [
+              "server"
+              "--listen ${config.listen}"
+              "--target ${config.target}"
+            ] ++ optional config.allowAll "--allow-all"
+              ++ map (v: "--allow-cn=${escapeShellArg v}") config.allowCN
+              ++ map (v: "--allow-ou=${escapeShellArg v}") config.allowOU
+              ++ map (v: "--allow-dns=${escapeShellArg v}") config.allowDNS
+              ++ map (v: "--allow-uri=${escapeShellArg v}") config.allowURI
+              ++ optional config.disableAuthentication "--disable-authentication"
+              ++ optional config.unsafeTarget "--unsafe-target"
+              ++ [ config.extraArguments ]
+          );
+        };
+      };
+    };
+
+in
+{
+
+  options = {
+    services.ghostunnel.enable = mkEnableOption "ghostunnel";
+
+    services.ghostunnel.package = mkOption {
+      description = "The ghostunnel package to use.";
+      type = types.package;
+      default = pkgs.ghostunnel;
+      defaultText = literalExample ''pkgs.ghostunnel'';
+    };
+
+    services.ghostunnel.servers = mkOption {
+      description = ''
+        Server mode ghostunnels (TLS listener -> plain TCP/UNIX target)
+      '';
+      type = types.attrsOf (types.submodule module);
+      default = {};
+    };
+  };
+
+  config = mkIf mainCfg.enable {
+    assertions = lib.mkMerge (map (v: v.atRoot.assertions) (attrValues mainCfg.servers));
+    systemd = lib.mkMerge (map (v: v.atRoot.systemd) (attrValues mainCfg.servers));
+  };
+
+  meta.maintainers = with lib.maintainers; [
+    roberth
+  ];
+}
diff --git a/nixos/modules/services/networking/git-daemon.nix b/nixos/modules/services/networking/git-daemon.nix
index 52c895215fb..98f80dd4bc4 100644
--- a/nixos/modules/services/networking/git-daemon.nix
+++ b/nixos/modules/services/networking/git-daemon.nix
@@ -74,7 +74,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 9418;
         description = "Port to listen on.";
       };
diff --git a/nixos/modules/services/networking/globalprotect-vpn.nix b/nixos/modules/services/networking/globalprotect-vpn.nix
new file mode 100644
index 00000000000..367a42687e1
--- /dev/null
+++ b/nixos/modules/services/networking/globalprotect-vpn.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.globalprotect;
+
+  execStart = if cfg.csdWrapper == null then
+      "${pkgs.globalprotect-openconnect}/bin/gpservice"
+    else
+      "${pkgs.globalprotect-openconnect}/bin/gpservice --csd-wrapper=${cfg.csdWrapper}";
+in
+
+{
+  options.services.globalprotect = {
+    enable = mkEnableOption "globalprotect";
+
+    csdWrapper = mkOption {
+      description = ''
+        A script that will produce a Host Integrity Protection (HIP) report,
+        as described at <link xlink:href="https://www.infradead.org/openconnect/hip.html" />
+      '';
+      default = null;
+      example = literalExample "\${pkgs.openconnect}/libexec/openconnect/hipreport.sh";
+      type = types.nullOr types.path;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.dbus.packages = [ pkgs.globalprotect-openconnect ];
+
+    systemd.services.gpservice = {
+      description = "GlobalProtect openconnect DBus service";
+      serviceConfig = {
+        Type="dbus";
+        BusName="com.yuezk.qt.GPService";
+        ExecStart=execStart;
+      };
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix
index 991ae38f30a..765834fad83 100644
--- a/nixos/modules/services/networking/go-neb.nix
+++ b/nixos/modules/services/networking/go-neb.nix
@@ -5,7 +5,8 @@ with lib;
 let
   cfg = config.services.go-neb;
 
-  configFile = pkgs.writeText "config.yml" (builtins.toJSON cfg.config);
+  settingsFormat = pkgs.formats.yaml {};
+  configFile = settingsFormat.generate "config.yaml" cfg.config;
 in {
   options.services.go-neb = {
     enable = mkEnableOption "Extensible matrix bot written in Go";
@@ -16,13 +17,26 @@ in {
       default = ":4050";
     };
 
+    secretFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/run/keys/go-neb.env";
+      description = ''
+        Environment variables from this file will be interpolated into the
+        final config file using envsubst with this syntax: <literal>$ENVIRONMENT</literal>
+        or <literal>''${VARIABLE}</literal>.
+        The file should contain lines formatted as <literal>SECRET_VAR=SECRET_VALUE</literal>.
+        This is useful to avoid putting secrets into the nix store.
+      '';
+    };
+
     baseUrl = mkOption {
       type = types.str;
       description = "Public-facing endpoint that can receive webhooks.";
     };
 
     config = mkOption {
-      type = types.uniq types.attrs;
+      inherit (settingsFormat) type;
       description = ''
         Your <filename>config.yaml</filename> as a Nix attribute set.
         See <link xlink:href="https://github.com/matrix-org/go-neb/blob/master/config.sample.yaml">config.sample.yaml</link>
@@ -32,18 +46,30 @@ in {
   };
 
   config = mkIf cfg.enable {
-    systemd.services.go-neb = {
+    systemd.services.go-neb = let
+      finalConfigFile = if cfg.secretFile == null then configFile else "/var/run/go-neb/config.yaml";
+    in {
       description = "Extensible matrix bot written in Go";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       environment = {
         BASE_URL = cfg.baseUrl;
         BIND_ADDRESS = cfg.bindAddress;
-        CONFIG_FILE = configFile;
+        CONFIG_FILE = finalConfigFile;
       };
 
       serviceConfig = {
+        ExecStartPre = lib.optional (cfg.secretFile != null)
+          (pkgs.writeShellScript "pre-start" ''
+            umask 077
+            export $(xargs < ${cfg.secretFile})
+            ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > ${finalConfigFile}
+            chown go-neb ${finalConfigFile}
+          '');
+        PermissionsStartOnly = true;
+        RuntimeDirectory = "go-neb";
         ExecStart = "${pkgs.go-neb}/bin/go-neb";
+        User = "go-neb";
         DynamicUser = true;
       };
     };
diff --git a/nixos/modules/services/networking/gobgpd.nix b/nixos/modules/services/networking/gobgpd.nix
new file mode 100644
index 00000000000..d3b03471f4e
--- /dev/null
+++ b/nixos/modules/services/networking/gobgpd.nix
@@ -0,0 +1,64 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gobgpd;
+  format = pkgs.formats.toml { };
+  confFile = format.generate "gobgpd.conf" cfg.settings;
+in {
+  options.services.gobgpd = {
+    enable = mkEnableOption "GoBGP Routing Daemon";
+
+    settings = mkOption {
+      type = format.type;
+      default = { };
+      description = ''
+        GoBGP configuration. Refer to
+        <link xlink:href="https://github.com/osrg/gobgp#documentation"/>
+        for details on supported values.
+      '';
+      example = literalExample ''
+        {
+          global = {
+            config = {
+              as = 64512;
+              router-id = "192.168.255.1";
+            };
+          };
+          neighbors = [
+            {
+              config = {
+                neighbor-address = "10.0.255.1";
+                peer-as = 65001;
+              };
+            }
+            {
+              config = {
+                neighbor-address = "10.0.255.2";
+                peer-as = 65002;
+              };
+            }
+          ];
+        }
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.gobgpd ];
+    systemd.services.gobgpd = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      description = "GoBGP Routing Daemon";
+      serviceConfig = {
+        Type = "notify";
+        ExecStartPre = "${pkgs.gobgpd}/bin/gobgpd -f ${confFile} -d";
+        ExecStart = "${pkgs.gobgpd}/bin/gobgpd -f ${confFile} --sdnotify";
+        ExecReload = "${pkgs.gobgpd}/bin/gobgpd -r";
+        DynamicUser = true;
+        AmbientCapabilities = "cap_net_bind_service";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/gogoclient.nix b/nixos/modules/services/networking/gogoclient.nix
index 99455b18314..1205321818b 100644
--- a/nixos/modules/services/networking/gogoclient.nix
+++ b/nixos/modules/services/networking/gogoclient.nix
@@ -28,6 +28,7 @@ in
 
       username = mkOption {
         default = "";
+        type = types.str;
         description = ''
           Your Gateway6 login name, if any.
         '';
@@ -42,6 +43,7 @@ in
       };
 
       server = mkOption {
+        type = types.str;
         default = "anonymous.freenet6.net";
         example = "broker.freenet6.net";
         description = "The Gateway6 server to be used.";
diff --git a/nixos/modules/services/networking/gvpe.nix b/nixos/modules/services/networking/gvpe.nix
index 92e87cd4640..4fad37ba15e 100644
--- a/nixos/modules/services/networking/gvpe.nix
+++ b/nixos/modules/services/networking/gvpe.nix
@@ -3,7 +3,7 @@
 {config, pkgs, lib, ...}:
 
 let
-  inherit (lib) mkOption mkIf;
+  inherit (lib) mkOption mkIf types;
 
   cfg = config.services.gvpe;
 
@@ -27,7 +27,7 @@ let
     text = ''
       #! /bin/sh
 
-      export PATH=$PATH:${pkgs.iproute}/sbin
+      export PATH=$PATH:${pkgs.iproute2}/sbin
 
       ip link set $IFNAME up
       ip address add ${cfg.ipAddress} dev $IFNAME
@@ -46,12 +46,14 @@ in
 
       nodename = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description =''
           GVPE node name
         '';
       };
       configText = mkOption {
         default = null;
+        type = types.nullOr types.lines;
         example = ''
           tcp-port = 655
           udp-port = 655
@@ -72,6 +74,7 @@ in
       };
       configFile = mkOption {
         default = null;
+        type = types.nullOr types.path;
         example = "/root/my-gvpe-conf";
         description = ''
           GVPE config file, if already present
@@ -79,12 +82,14 @@ in
       };
       ipAddress = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           IP address to assign to GVPE interface
         '';
       };
       subnet = mkOption {
         default = null;
+        type = types.nullOr types.str;
         example = "10.0.0.0/8";
         description = ''
           IP subnet assigned to GVPE network
@@ -92,6 +97,7 @@ in
       };
       customIFSetup = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           Additional commands to apply in ifup script
         '';
diff --git a/nixos/modules/services/networking/hans.nix b/nixos/modules/services/networking/hans.nix
index 8334dc68d62..84147db00f6 100644
--- a/nixos/modules/services/networking/hans.nix
+++ b/nixos/modules/services/networking/hans.nix
@@ -141,5 +141,5 @@ in
     };
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 }
diff --git a/nixos/modules/services/networking/heyefi.nix b/nixos/modules/services/networking/heyefi.nix
deleted file mode 100644
index fc2b5a84857..00000000000
--- a/nixos/modules/services/networking/heyefi.nix
+++ /dev/null
@@ -1,82 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.heyefi;
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.heyefi = {
-
-      enable = mkEnableOption "heyefi";
-
-      cardMacaddress = mkOption {
-        default = "";
-        description = ''
-          An Eye-Fi card MAC address.
-          '';
-      };
-
-      uploadKey = mkOption {
-        default = "";
-        description = ''
-          An Eye-Fi card's upload key.
-          '';
-      };
-
-      uploadDir = mkOption {
-        example = "/home/username/pictures";
-        description = ''
-          The directory to upload the files to.
-          '';
-      };
-
-      user = mkOption {
-        default = "root";
-        description = ''
-          heyefi will be run under this user (user must exist,
-          this can be your user name).
-        '';
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    systemd.services.heyefi =
-      {
-        description = "heyefi service";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = "${cfg.user}";
-          Restart = "always";
-          ExecStart = "${pkgs.heyefi}/bin/heyefi";
-        };
-
-      };
-
-    environment.etc."heyefi/heyefi.config".text =
-      ''
-        # /etc/heyefi/heyefi.conf: DO NOT EDIT -- this file has been generated automatically.
-        cards = [["${config.services.heyefi.cardMacaddress}","${config.services.heyefi.uploadKey}"]]
-        upload_dir = "${toString config.services.heyefi.uploadDir}"
-      '';
-
-    environment.systemPackages = [ pkgs.heyefi ];
-
-  };
-
-}
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 5d73038363a..f719ff59cc7 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -20,8 +20,8 @@ let
     ssid=${cfg.ssid}
     hw_mode=${cfg.hwMode}
     channel=${toString cfg.channel}
-    ${optionalString (cfg.countryCode != null) ''country_code=${cfg.countryCode}''}
-    ${optionalString (cfg.countryCode != null) ''ieee80211d=1''}
+    ${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
+    ${optionalString (cfg.countryCode != null) "ieee80211d=1"}
 
     # logging (debug level)
     logger_syslog=-1
@@ -68,6 +68,7 @@ in
       interface = mkOption {
         default = "";
         example = "wlp2s0";
+        type = types.str;
         description = ''
           The interfaces <command>hostapd</command> will use.
         '';
diff --git a/nixos/modules/services/networking/hylafax/faxq-wait.sh b/nixos/modules/services/networking/hylafax/faxq-wait.sh
index 8c39e9d20c1..1826aa30e62 100755
--- a/nixos/modules/services/networking/hylafax/faxq-wait.sh
+++ b/nixos/modules/services/networking/hylafax/faxq-wait.sh
@@ -1,4 +1,4 @@
-#! @shell@ -e
+#! @runtimeShell@ -e
 
 # skip this if there are no modems at all
 if ! stat -t "@spoolAreaPath@"/etc/config.* >/dev/null 2>&1
diff --git a/nixos/modules/services/networking/hylafax/modem-default.nix b/nixos/modules/services/networking/hylafax/modem-default.nix
index 7529b5b0aaf..707b8209282 100644
--- a/nixos/modules/services/networking/hylafax/modem-default.nix
+++ b/nixos/modules/services/networking/hylafax/modem-default.nix
@@ -5,7 +5,7 @@
 {
 
   TagLineFont = "etc/LiberationSans-25.pcf";
-  TagLineLocale = ''en_US.UTF-8'';
+  TagLineLocale = "en_US.UTF-8";
 
   AdminGroup = "root";  # groups that can change server config
   AnswerRotary = "fax";  # don't accept anything else but faxes
@@ -16,7 +16,7 @@
   SessionTracing = "0x78701";
   UUCPLockDir = "/var/lock";
 
-  SendPageCmd = ''${pkgs.coreutils}/bin/false'';  # prevent pager transmit
-  SendUUCPCmd = ''${pkgs.coreutils}/bin/false'';  # prevent UUCP transmit
+  SendPageCmd = "${pkgs.coreutils}/bin/false";  # prevent pager transmit
+  SendUUCPCmd = "${pkgs.coreutils}/bin/false";  # prevent UUCP transmit
 
 }
diff --git a/nixos/modules/services/networking/hylafax/options.nix b/nixos/modules/services/networking/hylafax/options.nix
index 4ac6d3fa843..74960e69b9a 100644
--- a/nixos/modules/services/networking/hylafax/options.nix
+++ b/nixos/modules/services/networking/hylafax/options.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib.options) literalExample mkEnableOption mkOption;
-  inherit (lib.types) bool enum int lines loaOf nullOr path str submodule;
+  inherit (lib.types) bool enum ints lines attrsOf nullOr path str submodule;
   inherit (lib.modules) mkDefault mkIf mkMerge;
 
   commonDescr = ''
@@ -18,7 +18,6 @@ let
   '';
 
   str1 = lib.types.addCheck str (s: s!="");  # non-empty string
-  int1 = lib.types.addCheck int (i: i>0);  # positive integer
 
   configAttrType =
     # Options in HylaFAX configuration files can be
@@ -27,7 +26,7 @@ let
     # This type definition resolves all
     # those types into a list of strings.
     let
-      inherit (lib.types) attrsOf coercedTo listOf;
+      inherit (lib.types) attrsOf coercedTo int listOf;
       innerType = coercedTo bool (x: if x then "Yes" else "No")
         (coercedTo int (toString) str);
     in
@@ -85,8 +84,8 @@ let
       # Otherwise, we use `false` to provoke
       # an error if hylafax tries to use it.
       c.sendmailPath = mkMerge [
-        (mkIfDefault noWrapper ''${pkgs.coreutils}/bin/false'')
-        (mkIfDefault (!noWrapper) ''${wrapperDir}/${program}'')
+        (mkIfDefault noWrapper "${pkgs.coreutils}/bin/false")
+        (mkIfDefault (!noWrapper) "${wrapperDir}/${program}")
       ];
       importDefaultConfig = file:
         lib.attrsets.mapAttrs
@@ -121,7 +120,7 @@ in
 
   options.services.hylafax = {
 
-    enable = mkEnableOption ''HylaFAX server'';
+    enable = mkEnableOption "HylaFAX server";
 
     autostart = mkOption {
       type = bool;
@@ -139,28 +138,28 @@ in
       type = nullOr str1;
       default = null;
       example = "49";
-      description = ''Country code for server and all modems.'';
+      description = "Country code for server and all modems.";
     };
 
     areaCode = mkOption {
       type = nullOr str1;
       default = null;
       example = "30";
-      description = ''Area code for server and all modems.'';
+      description = "Area code for server and all modems.";
     };
 
     longDistancePrefix = mkOption {
       type = nullOr str;
       default = null;
       example = "0";
-      description = ''Long distance prefix for server and all modems.'';
+      description = "Long distance prefix for server and all modems.";
     };
 
     internationalPrefix = mkOption {
       type = nullOr str;
       default = null;
       example = "00";
-      description = ''International prefix for server and all modems.'';
+      description = "International prefix for server and all modems.";
     };
 
     spoolAreaPath = mkOption {
@@ -248,7 +247,7 @@ in
     };
 
     modems = mkOption {
-      type = loaOf (submodule [ modemConfigOptions ]);
+      type = attrsOf (submodule [ modemConfigOptions ]);
       default = {};
       example.ttyS1 = {
         type = "cirrus";
@@ -267,7 +266,7 @@ in
     spoolExtraInit = mkOption {
       type = lines;
       default = "";
-      example = ''chmod 0755 .  # everyone may read my faxes'';
+      example = "chmod 0755 .  # everyone may read my faxes";
       description = ''
         Additional shell code that is executed within the
         spooling area directory right after its setup.
@@ -290,7 +289,7 @@ in
       '';
     };
     faxcron.infoDays = mkOption {
-      type = int1;
+      type = ints.positive;
       default = 30;
       description = ''
         Set the expiration time for data in the
@@ -298,7 +297,7 @@ in
       '';
     };
     faxcron.logDays = mkOption {
-      type = int1;
+      type = ints.positive;
       default = 30;
       description = ''
         Set the expiration time for
@@ -306,7 +305,7 @@ in
       '';
     };
     faxcron.rcvDays = mkOption {
-      type = int1;
+      type = ints.positive;
       default = 7;
       description = ''
         Set the expiration time for files in
@@ -343,9 +342,9 @@ in
       '';
     };
     faxqclean.doneqMinutes = mkOption {
-      type = int1;
+      type = ints.positive;
       default = 15;
-      example = literalExample ''24*60'';
+      example = literalExample "24*60";
       description = ''
         Set the job
         age threshold (in minutes) that controls how long
@@ -353,9 +352,9 @@ in
       '';
     };
     faxqclean.docqMinutes = mkOption {
-      type = int1;
+      type = ints.positive;
       default = 60;
-      example = literalExample ''24*60'';
+      example = literalExample "24*60";
       description = ''
         Set the document
         age threshold (in minutes) that controls how long
diff --git a/nixos/modules/services/networking/hylafax/spool.sh b/nixos/modules/services/networking/hylafax/spool.sh
index 31e930e8c59..8b723df77df 100755
--- a/nixos/modules/services/networking/hylafax/spool.sh
+++ b/nixos/modules/services/networking/hylafax/spool.sh
@@ -1,4 +1,4 @@
-#! @shell@ -e
+#! @runtimeShell@ -e
 
 # The following lines create/update the HylaFAX spool directory:
 # Subdirectories/files with persistent data are kept,
@@ -80,7 +80,7 @@ touch clientlog faxcron.lastrun xferfaxlog
 chown @faxuser@:@faxgroup@ clientlog faxcron.lastrun xferfaxlog
 
 # create symlinks for frozen directories/files
-lnsym --target-directory=. "@hylafax@"/spool/{COPYRIGHT,bin,config}
+lnsym --target-directory=. "@hylafaxplus@"/spool/{COPYRIGHT,bin,config}
 
 # create empty temporary directories
 update --mode=0700 -d client dev status
@@ -93,7 +93,7 @@ install -d "@spoolAreaPath@/etc"
 cd "@spoolAreaPath@/etc"
 
 # create symlinks to all files in template's etc
-lnsym --target-directory=. "@hylafax@/spool/etc"/*
+lnsym --target-directory=. "@hylafaxplus@/spool/etc"/*
 
 # set LOCKDIR in setup.cache
 sed --regexp-extended 's|^(UUCP_LOCKDIR=).*$|\1'"'@lockPath@'|g" --in-place setup.cache
diff --git a/nixos/modules/services/networking/hylafax/systemd.nix b/nixos/modules/services/networking/hylafax/systemd.nix
index b9b9b9dca4f..4506bbbc5eb 100644
--- a/nixos/modules/services/networking/hylafax/systemd.nix
+++ b/nixos/modules/services/networking/hylafax/systemd.nix
@@ -13,15 +13,14 @@ let
     # creates hylafax config file,
     # makes sure "Include" is listed *first*
     let
-      mkLines = conf:
-        (lib.concatLists
-        (lib.flip lib.mapAttrsToList conf
-        (k: map (v: ''${k}: ${v}'')
-      )));
+      mkLines = lib.flip lib.pipe [
+        (lib.mapAttrsToList (key: map (val: "${key}: ${val}")))
+        lib.concatLists
+      ];
       include = mkLines { Include = conf.Include or []; };
       other = mkLines ( conf // { Include = []; } );
     in
-      pkgs.writeText ''hylafax-config${name}''
+      pkgs.writeText "hylafax-config${name}"
       (concatStringsSep "\n" (include ++ other));
 
   globalConfigPath = mkConfigFile "" cfg.faxqConfig;
@@ -29,7 +28,7 @@ let
   modemConfigPath =
     let
       mkModemConfigFile = { config, name, ... }:
-        mkConfigFile ''.${name}''
+        mkConfigFile ".${name}"
         (cfg.commonModemConfig // config);
       mkLine = { name, type, ... }@modem: ''
         # check if modem config file exists:
@@ -48,13 +47,12 @@ let
     name = "hylafax-setup-spool.sh";
     src = ./spool.sh;
     isExecutable = true;
-    inherit (pkgs.stdenv) shell;
-    hylafax = pkgs.hylafaxplus;
     faxuser = "uucp";
     faxgroup = "uucp";
     lockPath = "/var/lock";
     inherit globalConfigPath modemConfigPath;
     inherit (cfg) sendmailPath spoolAreaPath userAccessFile;
+    inherit (pkgs) hylafaxplus runtimeShell;
   };
 
   waitFaxqScript = pkgs.substituteAll {
@@ -64,8 +62,8 @@ let
     src = ./faxq-wait.sh;
     isExecutable = true;
     timeoutSec = toString 10;
-    inherit (pkgs.stdenv) shell;
     inherit (cfg) spoolAreaPath;
+    inherit (pkgs) runtimeShell;
   };
 
   sockets.hylafax-hfaxd = {
@@ -81,7 +79,7 @@ let
     description = "HylaFAX queue manager sendq watch";
     documentation = [ "man:faxq(8)" "man:sendq(5)" ];
     wantedBy = [ "multi-user.target" ];
-    pathConfig.PathExistsGlob = [ ''${cfg.spoolAreaPath}/sendq/q*'' ];
+    pathConfig.PathExistsGlob = [ "${cfg.spoolAreaPath}/sendq/q*" ];
   };
 
   timers = mkMerge [
@@ -108,8 +106,10 @@ let
         PrivateDevices = true;  # breaks /dev/tty...
         PrivateNetwork = true;
         PrivateTmp = true;
+        #ProtectClock = true;  # breaks /dev/tty... (why?)
         ProtectControlGroups = true;
         #ProtectHome = true;  # breaks custom spool dirs
+        ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         #ProtectSystem = "strict";  # breaks custom spool dirs
@@ -134,7 +134,7 @@ let
         exit 1
       fi
     '';
-    serviceConfig.ExecStop = ''${setupSpoolScript}'';
+    serviceConfig.ExecStop = "${setupSpoolScript}";
     serviceConfig.RemainAfterExit = true;
     serviceConfig.Type = "oneshot";
     unitConfig.RequiresMountsFor = [ cfg.spoolAreaPath ];
@@ -145,7 +145,7 @@ let
     documentation = [ "man:faxq(8)" ];
     requires = [ "hylafax-spool.service" ];
     after = [ "hylafax-spool.service" ];
-    wants = mapModems ( { name, ... }: ''hylafax-faxgetty@${name}.service'' );
+    wants = mapModems ( { name, ... }: "hylafax-faxgetty@${name}.service" );
     wantedBy = mkIf cfg.autostart [ "multi-user.target" ];
     serviceConfig.Type = "forking";
     serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/faxq -q "${cfg.spoolAreaPath}"'';
@@ -155,7 +155,7 @@ let
     # stopped will always yield a failed send attempt:
     # The fax service is started when the job is created with
     # `sendfax`, but modems need some time to initialize.
-    serviceConfig.ExecStartPost = [ ''${waitFaxqScript}'' ];
+    serviceConfig.ExecStartPost = [ "${waitFaxqScript}" ];
     # faxquit fails if the pipe is already gone
     # (e.g. the service is already stopping)
     serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}"'';
@@ -186,7 +186,7 @@ let
     wantedBy = mkIf cfg.faxcron.enable.spoolInit requires;
     startAt = mkIf (cfg.faxcron.enable.frequency!=null) cfg.faxcron.enable.frequency;
     serviceConfig.ExecStart = concatStringsSep " " [
-      ''${pkgs.hylafaxplus}/spool/bin/faxcron''
+      "${pkgs.hylafaxplus}/spool/bin/faxcron"
       ''-q "${cfg.spoolAreaPath}"''
       ''-info ${toString cfg.faxcron.infoDays}''
       ''-log  ${toString cfg.faxcron.logDays}''
@@ -202,18 +202,18 @@ let
     wantedBy = mkIf cfg.faxqclean.enable.spoolInit requires;
     startAt = mkIf (cfg.faxqclean.enable.frequency!=null) cfg.faxqclean.enable.frequency;
     serviceConfig.ExecStart = concatStringsSep " " [
-      ''${pkgs.hylafaxplus}/spool/bin/faxqclean''
+      "${pkgs.hylafaxplus}/spool/bin/faxqclean"
       ''-q "${cfg.spoolAreaPath}"''
-      ''-v''
-      (optionalString (cfg.faxqclean.archiving!="never") ''-a'')
-      (optionalString (cfg.faxqclean.archiving=="always")  ''-A'')
+      "-v"
+      (optionalString (cfg.faxqclean.archiving!="never") "-a")
+      (optionalString (cfg.faxqclean.archiving=="always")  "-A")
       ''-j ${toString (cfg.faxqclean.doneqMinutes*60)}''
       ''-d ${toString (cfg.faxqclean.docqMinutes*60)}''
     ];
   };
 
   mkFaxgettyService = { name, ... }:
-    lib.nameValuePair ''hylafax-faxgetty@${name}'' rec {
+    lib.nameValuePair "hylafax-faxgetty@${name}" rec {
       description = "HylaFAX faxgetty for %I";
       documentation = [ "man:faxgetty(8)" ];
       bindsTo = [ "dev-%i.device" ];
@@ -221,7 +221,7 @@ let
       after = bindsTo ++ requires;
       before = [ "hylafax-faxq.service" "getty.target" ];
       unitConfig.StopWhenUnneeded = true;
-      unitConfig.AssertFileNotEmpty = ''${cfg.spoolAreaPath}/etc/config.%I'';
+      unitConfig.AssertFileNotEmpty = "${cfg.spoolAreaPath}/etc/config.%I";
       serviceConfig.UtmpIdentifier = "%I";
       serviceConfig.TTYPath = "/dev/%I";
       serviceConfig.Restart = "always";
diff --git a/nixos/modules/services/networking/icecream/daemon.nix b/nixos/modules/services/networking/icecream/daemon.nix
new file mode 100644
index 00000000000..2975696f9c2
--- /dev/null
+++ b/nixos/modules/services/networking/icecream/daemon.nix
@@ -0,0 +1,155 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecream.daemon;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecream.daemon = {
+
+     enable = mkEnableOption "Icecream Daemon";
+
+      openFirewall = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open receive port in the firewall.
+        '';
+      };
+
+      openBroadcast = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open the firewall for scheduler discovery.
+        '';
+      };
+
+      cacheLimit = mkOption {
+        type = types.ints.u16;
+        default = 256;
+        description = ''
+          Maximum size in Megabytes of cache used to store compile environments of compile clients.
+        '';
+      };
+
+      netName = mkOption {
+        type = types.str;
+        default = "ICECREAM";
+        description = ''
+          Network name to connect to. A scheduler with the same name needs to be running.
+        '';
+      };
+
+      noRemote = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Prevent jobs from other nodes being scheduled on this daemon.
+        '';
+      };
+
+      schedulerHost = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Explicit scheduler hostname, useful in firewalled environments.
+
+          Uses scheduler autodiscovery via broadcast if set to null.
+        '';
+      };
+
+      maxProcesses = mkOption {
+        type = types.nullOr types.ints.u16;
+        default = null;
+        description = ''
+          Maximum number of compile jobs started in parallel for this daemon.
+
+          Uses the number of CPUs if set to null.
+        '';
+      };
+
+      nice = mkOption {
+        type = types.int;
+        default = 5;
+        description = ''
+          The level of niceness to use.
+        '';
+      };
+
+      hostname = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Hostname of the daemon in the icecream infrastructure.
+
+          Uses the hostname retrieved via uname if set to null.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "icecc";
+        description = ''
+          User to run the icecream daemon as. Set to root to enable receive of
+          remote compile environments.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.icecream;
+        defaultText = "pkgs.icecream";
+        type = types.package;
+        description = "Icecream package to use.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line parameters.";
+        example = [ "-v" ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 10245 ];
+    networking.firewall.allowedUDPPorts = mkIf cfg.openBroadcast [ 8765 ];
+
+    systemd.services.icecc-daemon = {
+      description = "Icecream compile daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs ([
+          "${getBin cfg.package}/bin/iceccd"
+          "-b" "$STATE_DIRECTORY"
+          "-u" "icecc"
+          (toString cfg.nice)
+        ]
+        ++ optionals (cfg.schedulerHost != null) ["-s" cfg.schedulerHost]
+        ++ optionals (cfg.netName != null) [ "-n" cfg.netName ]
+        ++ optionals (cfg.cacheLimit != null) [ "--cache-limit" (toString cfg.cacheLimit) ]
+        ++ optionals (cfg.maxProcesses != null) [ "-m" (toString cfg.maxProcesses) ]
+        ++ optionals (cfg.hostname != null) [ "-N" (cfg.hostname) ]
+        ++ optional  cfg.noRemote "--no-remote"
+        ++ cfg.extraArgs);
+        DynamicUser = true;
+        User = "icecc";
+        Group = "icecc";
+        StateDirectory = "icecc";
+        RuntimeDirectory = "icecc";
+        AmbientCapabilities = "CAP_SYS_CHROOT";
+        CapabilityBoundingSet = "CAP_SYS_CHROOT";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ emantor ];
+}
diff --git a/nixos/modules/services/networking/icecream/scheduler.nix b/nixos/modules/services/networking/icecream/scheduler.nix
new file mode 100644
index 00000000000..4ccbf27015d
--- /dev/null
+++ b/nixos/modules/services/networking/icecream/scheduler.nix
@@ -0,0 +1,101 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.icecream.scheduler;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.icecream.scheduler = {
+      enable = mkEnableOption "Icecream Scheduler";
+
+      netName = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Network name for the icecream scheduler.
+
+          Uses the default ICECREAM if null.
+        '';
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 8765;
+        description = ''
+          Server port to listen for icecream daemon requests.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        description = ''
+          Whether to automatically open the daemon port in the firewall.
+        '';
+      };
+
+      openTelnet = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the telnet TCP port on 8766.
+        '';
+      };
+
+      persistentClientConnection = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to prevent clients from connecting to a better scheduler.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.icecream;
+        defaultText = "pkgs.icecream";
+        type = types.package;
+        description = "Icecream package to use.";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Additional command line parameters";
+        example = [ "-v" ];
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkMerge [
+      (mkIf cfg.openFirewall [ cfg.port ])
+      (mkIf cfg.openTelnet [ 8766 ])
+    ];
+
+    systemd.services.icecc-scheduler = {
+      description = "Icecream scheduling server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = escapeShellArgs ([
+          "${getBin cfg.package}/bin/icecc-scheduler"
+          "-p" (toString cfg.port)
+        ]
+        ++ optionals (cfg.netName != null) [ "-n" (toString cfg.netName) ]
+        ++ optional cfg.persistentClientConnection "-r"
+        ++ cfg.extraArgs);
+
+        DynamicUser = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ emantor ];
+}
diff --git a/nixos/modules/services/networking/inspircd.nix b/nixos/modules/services/networking/inspircd.nix
new file mode 100644
index 00000000000..8cb2b406ee2
--- /dev/null
+++ b/nixos/modules/services/networking/inspircd.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.inspircd;
+
+  configFile = pkgs.writeText "inspircd.conf" cfg.config;
+
+in {
+  meta = {
+    maintainers = [ lib.maintainers.sternenseemann ];
+  };
+
+  options = {
+    services.inspircd = {
+      enable = lib.mkEnableOption "InspIRCd";
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.inspircd;
+        defaultText = lib.literalExample "pkgs.inspircd";
+        example = lib.literalExample "pkgs.inspircdMinimal";
+        description = ''
+          The InspIRCd package to use. This is mainly useful
+          to specify an overridden version of the
+          <literal>pkgs.inspircd</literal> dervivation, for
+          example if you want to use a more minimal InspIRCd
+          distribution with less modules enabled or with
+          modules enabled which can't be distributed in binary
+          form due to licensing issues.
+        '';
+      };
+
+      config = lib.mkOption {
+        type = lib.types.lines;
+        description = ''
+          Verbatim <literal>inspircd.conf</literal> file.
+          For a list of options, consult the
+          <link xlink:href="https://docs.inspircd.org/3/configuration/">InspIRCd documentation</link>, the
+          <link xlink:href="https://docs.inspircd.org/3/modules/">Module documentation</link>
+          and the example configuration files distributed
+          with <literal>pkgs.inspircd.doc</literal>
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.inspircd = {
+      description = "InspIRCd - the stable, high-performance and modular Internet Relay Chat Daemon";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "network.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = ''
+          ${lib.getBin cfg.package}/bin/inspircd start --config ${configFile} --nofork --nopid
+        '';
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/ircd-hybrid/default.nix b/nixos/modules/services/networking/ircd-hybrid/default.nix
index 91d0bf437d6..1f5636e4e3a 100644
--- a/nixos/modules/services/networking/ircd-hybrid/default.nix
+++ b/nixos/modules/services/networking/ircd-hybrid/default.nix
@@ -10,7 +10,7 @@ let
     name = "ircd-hybrid-service";
     scripts = [ "=>/bin" ./control.in ];
     substFiles = [ "=>/conf" ./ircd.conf ];
-    inherit (pkgs) ircdHybrid coreutils su iproute gnugrep procps;
+    inherit (pkgs) ircdHybrid coreutils su iproute2 gnugrep procps;
 
     ipv6Enabled = boolToString config.networking.enableIPv6;
 
@@ -40,6 +40,7 @@ in
 
       serverName = mkOption {
         default = "hades.arpa";
+        type = types.str;
         description = "
           IRCD server name.
         ";
@@ -47,6 +48,7 @@ in
 
       sid = mkOption {
         default = "0NL";
+        type = types.str;
         description = "
           IRCD server unique ID in a net of servers.
         ";
@@ -54,6 +56,7 @@ in
 
       description = mkOption {
         default = "Hybrid-7 IRC server.";
+        type = types.str;
         description = "
           IRCD server description.
         ";
@@ -62,6 +65,7 @@ in
       rsaKey = mkOption {
         default = null;
         example = literalExample "/root/certificates/irc.key";
+        type = types.nullOr types.path;
         description = "
           IRCD server RSA key.
         ";
@@ -70,6 +74,7 @@ in
       certificate = mkOption {
         default = null;
         example = literalExample "/root/certificates/irc.pem";
+        type = types.nullOr types.path;
         description = "
           IRCD server SSL certificate. There are some limitations - read manual.
         ";
@@ -77,6 +82,7 @@ in
 
       adminEmail = mkOption {
         default = "<bit-bucket@example.com>";
+        type = types.str;
         example = "<name@domain.tld>";
         description = "
           IRCD server administrator e-mail.
@@ -86,6 +92,7 @@ in
       extraIPs = mkOption {
         default = [];
         example = ["127.0.0.1"];
+        type = types.listOf types.str;
         description = "
           Extra IP's to bind.
         ";
@@ -93,6 +100,7 @@ in
 
       extraPort = mkOption {
         default = "7117";
+        type = types.str;
         description = "
           Extra port to avoid filtering.
         ";
diff --git a/nixos/modules/services/networking/iscsi/initiator.nix b/nixos/modules/services/networking/iscsi/initiator.nix
new file mode 100644
index 00000000000..cbc919a2f76
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/initiator.nix
@@ -0,0 +1,84 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.services.openiscsi;
+in
+{
+  options.services.openiscsi = with types; {
+    enable = mkEnableOption "the openiscsi iscsi daemon";
+    enableAutoLoginOut = mkEnableOption ''
+      automatic login and logout of all automatic targets.
+      You probably do not want this.
+    '';
+    discoverPortal = mkOption {
+      type = nullOr str;
+      default = null;
+      description = "Portal to discover targets on";
+    };
+    name = mkOption {
+      type = str;
+      description = "Name of this iscsi initiator";
+      example = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
+    };
+    package = mkOption {
+      type = package;
+      description = "openiscsi package to use";
+      default = pkgs.openiscsi;
+      defaultText = "pkgs.openiscsi";
+    };
+
+    extraConfig = mkOption {
+      type = str;
+      default = "";
+      description = "Lines to append to default iscsid.conf";
+    };
+
+    extraConfigFile = mkOption {
+      description = ''
+        Append an additional file's contents to /etc/iscsid.conf. Use a non-store path
+        and store passwords in this file.
+      '';
+      default = null;
+      type = nullOr str;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."iscsi/iscsid.conf.fragment".source = pkgs.runCommand "iscsid.conf" {} ''
+      cat "${cfg.package}/etc/iscsi/iscsid.conf" > $out
+      cat << 'EOF' >> $out
+      ${cfg.extraConfig}
+      ${optionalString cfg.enableAutoLoginOut "node.startup = automatic"}
+      EOF
+    '';
+    environment.etc."iscsi/initiatorname.iscsi".text = "InitiatorName=${cfg.name}";
+
+    system.activationScripts.iscsid = let
+      extraCfgDumper = optionalString (cfg.extraConfigFile != null) ''
+        if [ -f "${cfg.extraConfigFile}" ]; then
+          printf "\n# The following is from ${cfg.extraConfigFile}:\n"
+          cat "${cfg.extraConfigFile}"
+        else
+          echo "Warning: services.openiscsi.extraConfigFile ${cfg.extraConfigFile} does not exist!" >&2
+        fi
+      '';
+    in ''
+      (
+        cat ${config.environment.etc."iscsi/iscsid.conf.fragment".source}
+        ${extraCfgDumper}
+      ) > /etc/iscsi/iscsid.conf
+    '';
+
+    systemd.packages = [ cfg.package ];
+
+    systemd.services."iscsid".wantedBy = [ "multi-user.target" ];
+    systemd.sockets."iscsid".wantedBy = [ "sockets.target" ];
+
+    systemd.services."iscsi" = mkIf cfg.enableAutoLoginOut {
+      wantedBy = [ "remote-fs.target" ];
+      serviceConfig.ExecStartPre = mkIf (cfg.discoverPortal != null) "${cfg.package}/bin/iscsiadm --mode discoverydb --type sendtargets --portal ${escapeShellArg cfg.discoverPortal} --discover";
+    };
+
+    environment.systemPackages = [ cfg.package ];
+    boot.kernelModules = [ "iscsi_tcp" ];
+  };
+}
diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix
new file mode 100644
index 00000000000..3274878c4fa
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/root-initiator.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }: with lib;
+let
+  cfg = config.boot.iscsi-initiator;
+in
+{
+  # If you're booting entirely off another machine you may want to add
+  # this snippet to always boot the latest "system" version. It is not
+  # enabled by default in case you have an initrd on a local disk:
+  #
+  #     boot.initrd.postMountCommands = ''
+  #       ln -sfn /nix/var/nix/profiles/system/init /mnt-root/init
+  #       stage2Init=/init
+  #     '';
+  #
+  # Note: Theoretically you might want to connect to multiple portals and
+  # log in to multiple targets, however the authors of this module so far
+  # don't have the need or expertise to reasonably implement it. Also,
+  # consider carefully before making your boot chain depend on multiple
+  # machines to be up.
+  options.boot.iscsi-initiator = with types; {
+    name = mkOption {
+      description = ''
+        Name of the iSCSI initiator to boot from. Note, booting from iscsi
+        requires networkd based networking.
+      '';
+      default = null;
+      example = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
+      type = nullOr str;
+    };
+
+    discoverPortal = mkOption {
+      description = ''
+        iSCSI portal to boot from.
+      '';
+      default = null;
+      example = "192.168.1.1:3260";
+      type = nullOr str;
+    };
+
+    target = mkOption {
+      description = ''
+        Name of the iSCSI target to boot from.
+      '';
+      default = null;
+      example = "iqn.2020-08.org.linux-iscsi.targethost:example";
+      type = nullOr str;
+    };
+
+    logLevel = mkOption {
+      description = ''
+        Higher numbers elicits more logs.
+      '';
+      default = 1;
+      example = 8;
+      type = int;
+    };
+
+    loginAll = mkOption {
+      description = ''
+        Do not log into a specific target on the portal, but to all that we discover.
+        This overrides setting target.
+      '';
+      type = bool;
+      default = false;
+    };
+
+    extraConfig = mkOption {
+      description = "Extra lines to append to /etc/iscsid.conf";
+      default = null;
+      type = nullOr lines;
+    };
+
+    extraConfigFile = mkOption {
+      description = ''
+        Append an additional file's contents to `/etc/iscsid.conf`. Use a non-store path
+        and store passwords in this file. Note: the file specified here must be available
+        in the initrd, see: `boot.initrd.secrets`.
+      '';
+      default = null;
+      type = nullOr str;
+    };
+  };
+
+  config = mkIf (cfg.name != null) {
+    # The "scripted" networking configuration (ie: non-networkd)
+    # doesn't properly order the start and stop of the interfaces, and the
+    # network interfaces are torn down before unmounting disks. Since this
+    # module is specifically for very-early-boot network mounts, we need
+    # the network to stay on.
+    #
+    # We could probably fix the scripted options to properly order, but I'm
+    # not inclined to invest that time today. Hopefully this gets users far
+    # enough along and they can just use networkd.
+    networking.useNetworkd = true;
+    networking.useDHCP = false; # Required to set useNetworkd = true
+
+    boot.initrd = {
+      network.enable = true;
+
+      # By default, the stage-1 disables the network and resets the interfaces
+      # on startup. Since our startup disks are on the network, we can't let
+      # the network not work.
+      network.flushBeforeStage2 = false;
+
+      kernelModules = [ "iscsi_tcp" ];
+
+      extraUtilsCommands = ''
+        copy_bin_and_libs ${pkgs.openiscsi}/bin/iscsid
+        copy_bin_and_libs ${pkgs.openiscsi}/bin/iscsiadm
+        ${optionalString (!config.boot.initrd.network.ssh.enable) "cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib"}
+
+        mkdir -p $out/etc/iscsi
+        cp ${config.environment.etc.hosts.source} $out/etc/hosts
+        cp ${pkgs.openiscsi}/etc/iscsi/iscsid.conf $out/etc/iscsi/iscsid.fragment.conf
+        chmod +w $out/etc/iscsi/iscsid.fragment.conf
+        cat << 'EOF' >> $out/etc/iscsi/iscsid.fragment.conf
+        ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+        EOF
+      '';
+
+      extraUtilsCommandsTest = ''
+        $out/bin/iscsiadm --version
+      '';
+
+      preLVMCommands = let
+        extraCfgDumper = optionalString (cfg.extraConfigFile != null) ''
+          if [ -f "${cfg.extraConfigFile}" ]; then
+            printf "\n# The following is from ${cfg.extraConfigFile}:\n"
+            cat "${cfg.extraConfigFile}"
+          else
+            echo "Warning: boot.iscsi-initiator.extraConfigFile ${cfg.extraConfigFile} does not exist!" >&2
+          fi
+        '';
+      in ''
+        ${optionalString (!config.boot.initrd.network.ssh.enable) ''
+        # stolen from initrd-ssh.nix
+        echo 'root:x:0:0:root:/root:/bin/ash' > /etc/passwd
+        echo 'passwd: files' > /etc/nsswitch.conf
+      ''}
+
+        cp -f $extraUtils/etc/hosts /etc/hosts
+
+        mkdir -p /etc/iscsi /run/lock/iscsi
+        echo "InitiatorName=${cfg.name}" > /etc/iscsi/initiatorname.iscsi
+
+        (
+          cat "$extraUtils/etc/iscsi/iscsid.fragment.conf"
+          printf "\n"
+          ${optionalString cfg.loginAll ''echo "node.startup = automatic"''}
+          ${extraCfgDumper}
+        ) > /etc/iscsi/iscsid.conf
+
+        iscsid --foreground --no-pid-file --debug ${toString cfg.logLevel} &
+        iscsiadm --mode discoverydb \
+          --type sendtargets \
+          --discover \
+          --portal ${escapeShellArg cfg.discoverPortal} \
+          --debug ${toString cfg.logLevel}
+
+        ${if cfg.loginAll then ''
+        iscsiadm --mode node --loginall all
+      '' else ''
+        iscsiadm --mode node --targetname ${escapeShellArg cfg.target} --login
+      ''}
+        pkill -9 iscsid
+      '';
+    };
+
+    services.openiscsi = {
+      enable = true;
+      inherit (cfg) name;
+    };
+
+    assertions = [
+      {
+        assertion = cfg.loginAll -> cfg.target == null;
+        message = "iSCSI target name is set while login on all portals is enabled.";
+      }
+    ];
+  };
+}
diff --git a/nixos/modules/services/networking/iscsi/target.nix b/nixos/modules/services/networking/iscsi/target.nix
new file mode 100644
index 00000000000..8a10e7d346a
--- /dev/null
+++ b/nixos/modules/services/networking/iscsi/target.nix
@@ -0,0 +1,53 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.target;
+in
+{
+  ###### interface
+  options = {
+    services.target = with types; {
+      enable = mkEnableOption "the kernel's LIO iscsi target";
+
+      config = mkOption {
+        type = attrs;
+        default = {};
+        description = ''
+          Content of /etc/target/saveconfig.json
+          This file is normally read and written by targetcli
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    environment.etc."target/saveconfig.json" = {
+      text = builtins.toJSON cfg.config;
+      mode = "0600";
+    };
+
+    environment.systemPackages = with pkgs; [ targetcli ];
+
+    boot.kernelModules = [ "configfs" "target_core_mod" "iscsi_target_mod" ];
+
+    systemd.services.iscsi-target = {
+      enable = true;
+      after = [ "network.target" "local-fs.target" ];
+      requires = [ "sys-kernel-config.mount" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.python3.pkgs.rtslib}/bin/targetctl restore";
+        ExecStop = "${pkgs.python3.pkgs.rtslib}/bin/targetctl clear";
+        RemainAfterExit = "yes";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /etc/target 0700 root root - -"
+    ];
+  };
+}
diff --git a/nixos/modules/services/networking/iwd.nix b/nixos/modules/services/networking/iwd.nix
index 6be67a8b96f..8835f7f9372 100644
--- a/nixos/modules/services/networking/iwd.nix
+++ b/nixos/modules/services/networking/iwd.nix
@@ -4,8 +4,31 @@ with lib;
 
 let
   cfg = config.networking.wireless.iwd;
+  ini = pkgs.formats.ini { };
+  configFile = ini.generate "main.conf" cfg.settings;
 in {
-  options.networking.wireless.iwd.enable = mkEnableOption "iwd";
+  options.networking.wireless.iwd = {
+    enable = mkEnableOption "iwd";
+
+    settings = mkOption {
+      type = ini.type;
+      default = { };
+
+      example = {
+        Settings.AutoConnect = true;
+
+        Network = {
+          EnableIPv6 = true;
+          RoutePriorityOffset = 300;
+        };
+      };
+
+      description = ''
+        Options passed to iwd.
+        See <link xlink:href="https://iwd.wiki.kernel.org/networkconfigurationsettings">here</link> for supported options.
+      '';
+    };
+  };
 
   config = mkIf cfg.enable {
     assertions = [{
@@ -15,6 +38,8 @@ in {
       '';
     }];
 
+    environment.etc."iwd/main.conf".source = configFile;
+
     # for iwctl
     environment.systemPackages =  [ pkgs.iwd ];
 
@@ -22,7 +47,15 @@ in {
 
     systemd.packages = [ pkgs.iwd ];
 
-    systemd.services.iwd.wantedBy = [ "multi-user.target" ];
+    systemd.network.links."80-iwd" = {
+      matchConfig.Type = "wlan";
+      linkConfig.NamePolicy = "keep kernel";
+    };
+
+    systemd.services.iwd = {
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ configFile ];
+    };
   };
 
   meta.maintainers = with lib.maintainers; [ mic92 dtzWill ];
diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix
index 5482e997a40..80f35d56e2d 100644
--- a/nixos/modules/services/networking/jitsi-videobridge.nix
+++ b/nixos/modules/services/networking/jitsi-videobridge.nix
@@ -191,6 +191,16 @@ in
         Whether to open ports in the firewall for the videobridge.
       '';
     };
+
+    apis = mkOption {
+      type = with types; listOf str;
+      description = ''
+        What is passed as --apis= parameter. If this is empty, "none" is passed.
+        Needed for monitoring jitsi.
+      '';
+      default = [];
+      example = literalExample "[ \"colibri\" \"rest\" ]";
+    };
   };
 
   config = mkIf cfg.enable {
@@ -221,7 +231,7 @@ in
         "export ${toVarName name}=$(cat ${xmppConfig.passwordFile})\n"
       ) cfg.xmppConfigs))
       + ''
-        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=none
+        ${pkgs.jitsi-videobridge}/bin/jitsi-videobridge --apis=${if (cfg.apis == []) then "none" else concatStringsSep "," cfg.apis}
       '';
 
       serviceConfig = {
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
new file mode 100644
index 00000000000..72773b83a49
--- /dev/null
+++ b/nixos/modules/services/networking/kea.nix
@@ -0,0 +1,361 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+with lib;
+
+let
+  cfg = config.services.kea;
+
+  format = pkgs.formats.json {};
+
+  ctrlAgentConfig = format.generate "kea-ctrl-agent.conf" {
+    Control-agent = cfg.ctrl-agent.settings;
+  };
+  dhcp4Config = format.generate "kea-dhcp4.conf" {
+    Dhcp4 = cfg.dhcp4.settings;
+  };
+  dhcp6Config = format.generate "kea-dhcp6.conf" {
+    Dhcp6 = cfg.dhcp6.settings;
+  };
+  dhcpDdnsConfig = format.generate "kea-dhcp-ddns.conf" {
+    DhcpDdns = cfg.dhcp-ddns.settings;
+  };
+
+  package = pkgs.kea;
+in
+{
+  options.services.kea = with types; {
+    ctrl-agent = mkOption {
+      description = ''
+        Kea Control Agent configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea Control Agent";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            description = ''
+              Kea Control Agent configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/agent.html"/>.
+            '';
+          };
+        };
+      };
+    };
+
+    dhcp4 = mkOption {
+      description = ''
+        DHCP4 Server configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea DHCP4 server";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            example = {
+              valid-lifetime = 4000;
+              renew-timer = 1000;
+              rebind-timer = 2000;
+              interfaces-config = {
+                interfaces = [
+                  "eth0"
+                ];
+              };
+              lease-database = {
+                type = "memfile";
+                persist = true;
+                name = "/var/lib/kea/dhcp4.leases";
+              };
+              subnet4 = [ {
+                subnet = "192.0.2.0/24";
+                pools = [ {
+                  pool = "192.0.2.100 - 192.0.2.240";
+                } ];
+              } ];
+            };
+            description = ''
+              Kea DHCP4 configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html"/>.
+            '';
+          };
+        };
+      };
+    };
+
+    dhcp6 = mkOption {
+      description = ''
+        DHCP6 Server configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea DHCP6 server";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            example = {
+              valid-lifetime = 4000;
+              renew-timer = 1000;
+              rebind-timer = 2000;
+              preferred-lifetime = 3000;
+              interfaces-config = {
+                interfaces = [
+                  "eth0"
+                ];
+              };
+              lease-database = {
+                type = "memfile";
+                persist = true;
+                name = "/var/lib/kea/dhcp6.leases";
+              };
+              subnet6 = [ {
+                subnet = "2001:db8:1::/64";
+                pools = [ {
+                  pool = "2001:db8:1::1-2001:db8:1::ffff";
+                } ];
+              } ];
+            };
+            description = ''
+              Kea DHCP6 configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html"/>.
+            '';
+          };
+        };
+      };
+    };
+
+    dhcp-ddns = mkOption {
+      description = ''
+        Kea DHCP-DDNS configuration
+      '';
+      default = {};
+      type = submodule {
+        options = {
+          enable = mkEnableOption "Kea DDNS server";
+
+          extraArgs = mkOption {
+            type = listOf str;
+            default = [];
+            description = ''
+              List of additonal arguments to pass to the daemon.
+            '';
+          };
+
+          settings = mkOption {
+            type = format.type;
+            default = null;
+            example = {
+              ip-address = "127.0.0.1";
+              port = 53001;
+              dns-server-timeout = 100;
+              ncr-protocol = "UDP";
+              ncr-format = "JSON";
+              tsig-keys = [ ];
+              forward-ddns = {
+                ddns-domains = [ ];
+              };
+              reverse-ddns = {
+                ddns-domains = [ ];
+              };
+            };
+            description = ''
+              Kea DHCP-DDNS configuration as an attribute set, see <link xlink:href="https://kea.readthedocs.io/en/kea-${package.version}/arm/ddns.html"/>.
+            '';
+          };
+        };
+      };
+    };
+  };
+
+  config = let
+    commonServiceConfig = {
+      ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      DynamicUser = true;
+      User = "kea";
+      ConfigurationDirectory = "kea";
+      RuntimeDirectory = "kea";
+      StateDirectory = "kea";
+      UMask = "0077";
+    };
+  in mkIf (cfg.ctrl-agent.enable || cfg.dhcp4.enable || cfg.dhcp6.enable || cfg.dhcp-ddns.enable) (mkMerge [
+  {
+    environment.systemPackages = [ package ];
+  }
+
+  (mkIf cfg.ctrl-agent.enable {
+
+    environment.etc."kea/ctrl-agent.conf".source = ctrlAgentConfig;
+
+    systemd.services.kea-ctrl-agent = {
+      description = "Kea Control Agent";
+      documentation = [
+        "man:kea-ctrl-agent(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/agent.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "kea-dhcp4-server.service"
+        "kea-dhcp6-server.service"
+        "kea-dhcp-ddns-server.service"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+      };
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-ctrl-agent -c /etc/kea/ctrl-agent.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
+        KillMode = "process";
+        Restart = "on-failure";
+      } // commonServiceConfig;
+    };
+  })
+
+  (mkIf cfg.dhcp4.enable {
+
+    environment.etc."kea/dhcp4-server.conf".source = dhcp4Config;
+
+    systemd.services.kea-dhcp4-server = {
+      description = "Kea DHCP4 Server";
+      documentation = [
+        "man:kea-dhcp4(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp4-srv.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+      };
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-dhcp4 -c /etc/kea/dhcp4-server.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
+        # Kea does not request capabilities by itself
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+        ];
+      } // commonServiceConfig;
+    };
+  })
+
+  (mkIf cfg.dhcp6.enable {
+
+    environment.etc."kea/dhcp6-server.conf".source = dhcp6Config;
+
+    systemd.services.kea-dhcp6-server = {
+      description = "Kea DHCP6 Server";
+      documentation = [
+        "man:kea-dhcp6(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/dhcp6-srv.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+      };
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-dhcp6 -c /etc/kea/dhcp6-server.conf ${lib.escapeShellArgs cfg.dhcp6.extraArgs}";
+        # Kea does not request capabilities by itself
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+      } // commonServiceConfig;
+    };
+  })
+
+  (mkIf cfg.dhcp-ddns.enable {
+
+    environment.etc."kea/dhcp-ddns.conf".source = dhcpDdnsConfig;
+
+    systemd.services.kea-dhcp-ddns-server = {
+      description = "Kea DHCP-DDNS Server";
+      documentation = [
+        "man:kea-dhcp-ddns(8)"
+        "https://kea.readthedocs.io/en/kea-${package.version}/arm/ddns.html"
+      ];
+
+      after = [
+        "network-online.target"
+        "time-sync.target"
+      ];
+      wantedBy = [
+        "multi-user.target"
+      ];
+
+      environment = {
+        KEA_PIDFILE_DIR = "/run/kea";
+      };
+
+      serviceConfig = {
+        ExecStart = "${package}/bin/kea-dhcp-ddns -c /etc/kea/dhcp-ddns.conf ${lib.escapeShellArgs cfg.dhcp-ddns.extraArgs}";
+        AmbientCapabilites = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+        CapabilityBoundingSet = [
+          "CAP_NET_BIND_SERVICE"
+        ];
+      } // commonServiceConfig;
+    };
+  })
+
+  ]);
+
+  meta.maintainers = with maintainers; [ hexa ];
+}
diff --git a/nixos/modules/services/networking/kippo.nix b/nixos/modules/services/networking/kippo.nix
index 553415a2f32..6fedb0a270f 100644
--- a/nixos/modules/services/networking/kippo.nix
+++ b/nixos/modules/services/networking/kippo.nix
@@ -17,37 +17,37 @@ in
       enable = mkOption {
         default = false;
         type = types.bool;
-        description = ''Enable the kippo honeypot ssh server.'';
+        description = "Enable the kippo honeypot ssh server.";
       };
       port = mkOption {
         default = 2222;
         type = types.int;
-        description = ''TCP port number for kippo to bind to.'';
+        description = "TCP port number for kippo to bind to.";
       };
       hostname = mkOption {
         default = "nas3";
         type = types.str;
-        description = ''Hostname for kippo to present to SSH login'';
+        description = "Hostname for kippo to present to SSH login";
       };
       varPath = mkOption {
         default = "/var/lib/kippo";
         type = types.path;
-        description = ''Path of read/write files needed for operation and configuration.'';
+        description = "Path of read/write files needed for operation and configuration.";
       };
       logPath = mkOption {
         default = "/var/log/kippo";
         type = types.path;
-        description = ''Path of log files needed for operation and configuration.'';
+        description = "Path of log files needed for operation and configuration.";
       };
       pidPath = mkOption {
         default = "/run/kippo";
         type = types.path;
-        description = ''Path of pid files needed for operation.'';
+        description = "Path of pid files needed for operation.";
       };
       extraConfig = mkOption {
         default = "";
         type = types.lines;
-        description = ''Extra verbatim configuration added to the end of kippo.cfg.'';
+        description = "Extra verbatim configuration added to the end of kippo.cfg.";
       };
     };
 
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index ccb34163d5f..6882a315f61 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -8,14 +8,14 @@ let
   # 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_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'}";
+    addrSpec = if al_portOnly == null then "'${head al}'" else "{'::', '0.0.0.0'}";
     in # freebind is set for compatibility with earlier kresd services;
        # it could be configurable, for example.
       ''
@@ -23,18 +23,12 @@ let
       '';
 
   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
+    + concatMapStrings (mkListen "doh2") 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 */ ];
 
@@ -62,6 +56,15 @@ in {
         and give commands interactively to kresd@1.service.
       '';
     };
+    package = mkOption {
+      type = types.package;
+      description = "
+        knot-resolver package to use.
+      ";
+      default = pkgs.knot-resolver;
+      defaultText = "pkgs.knot-resolver";
+      example = literalExample "pkgs.knot-resolver.override { extraFeatures = true; }";
+    };
     extraConfig = mkOption {
       type = types.lines;
       default = "";
@@ -92,7 +95,7 @@ in {
       default = [];
       example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
       description = ''
-        Addresses and ports on which kresd should provide DNS over HTTPS (see RFC 8484).
+        Addresses and ports on which kresd should provide DNS over HTTPS/2 (see RFC 8484).
         For detailed syntax see ListenStream in man systemd.socket.
       '';
     };
@@ -112,6 +115,8 @@ in {
   config = mkIf cfg.enable {
     environment.etc."knot-resolver/kresd.conf".source = configFile; # not required
 
+    networking.resolvconf.useLocalResolver = mkDefault true;
+
     users.users.knot-resolver =
       { isSystemUser = true;
         group = "knot-resolver";
@@ -119,7 +124,7 @@ in {
       };
     users.groups.knot-resolver.gid = null;
 
-    systemd.packages = [ package ]; # the units are patched inside the package a bit
+    systemd.packages = [ cfg.package ]; # the units are patched inside the package a bit
 
     systemd.targets.kresd = { # configure units started by default
       wantedBy = [ "multi-user.target" ];
@@ -127,8 +132,8 @@ in {
         ++ map (i: "kresd@${toString i}.service") (range 1 cfg.instances);
     };
     systemd.services."kresd@".serviceConfig = {
-      ExecStart = "${package}/bin/kresd --noninteractive "
-        + "-c ${package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
+      ExecStart = "${cfg.package}/bin/kresd --noninteractive "
+        + "-c ${cfg.package}/lib/knot-resolver/distro-preconfig.lua -c ${configFile}";
       # Ensure /run/knot-resolver exists
       RuntimeDirectory = "knot-resolver";
       RuntimeDirectoryMode = "0770";
@@ -139,10 +144,7 @@ in {
       CacheDirectory = "knot-resolver";
       CacheDirectoryMode = "0770";
     };
-
-    # 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" ];
+    # We don't mind running stop phase from wrong version.  It seems less racy.
+    systemd.services."kresd@".stopIfChanged = false;
   };
 }
diff --git a/nixos/modules/services/networking/libreswan.nix b/nixos/modules/services/networking/libreswan.nix
index 280158b89f6..1f0423ac3d8 100644
--- a/nixos/modules/services/networking/libreswan.nix
+++ b/nixos/modules/services/networking/libreswan.nix
@@ -9,21 +9,22 @@ let
   libexec = "${pkgs.libreswan}/libexec/ipsec";
   ipsec = "${pkgs.libreswan}/sbin/ipsec";
 
-  trim = chars: str: let
-      nonchars = filter (x : !(elem x.value chars))
-                  (imap0 (i: v: {ind = i; value = v;}) (stringToCharacters str));
-    in
-      if length nonchars == 0 then ""
-      else substring (head nonchars).ind (add 1 (sub (last nonchars).ind (head nonchars).ind)) str;
+  trim = chars: str:
+  let
+    nonchars = filter (x : !(elem x.value chars))
+               (imap0 (i: v: {ind = i; value = v;}) (stringToCharacters str));
+  in
+    if length nonchars == 0 then ""
+    else substring (head nonchars).ind (add 1 (sub (last nonchars).ind (head nonchars).ind)) str;
   indent = str: concatStrings (concatMap (s: ["  " (trim [" " "\t"] s) "\n"]) (splitString "\n" str));
   configText = indent (toString cfg.configSetup);
   connectionText = concatStrings (mapAttrsToList (n: v:
     ''
       conn ${n}
       ${indent v}
-
     '') cfg.connections);
-  configFile = pkgs.writeText "ipsec.conf"
+
+  configFile = pkgs.writeText "ipsec-nixos.conf"
     ''
       config setup
       ${configText}
@@ -31,6 +32,11 @@ let
       ${connectionText}
     '';
 
+  policyFiles = mapAttrs' (name: text:
+    { name = "ipsec.d/policies/${name}";
+      value.source = pkgs.writeText "ipsec-policy-${name}" text;
+    }) cfg.policies;
+
 in
 
 {
@@ -41,41 +47,71 @@ in
 
     services.libreswan = {
 
-      enable = mkEnableOption "libreswan ipsec service";
+      enable = mkEnableOption "Libreswan IPsec service";
 
       configSetup = mkOption {
         type = types.lines;
         default = ''
             protostack=netkey
-            nat_traversal=yes
             virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v4:100.64.0.0/10,%v6:fd00::/8,%v6:fe80::/10
         '';
         example = ''
             secretsfile=/root/ipsec.secrets
             protostack=netkey
-            nat_traversal=yes
             virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:25.0.0.0/8,%v4:100.64.0.0/10,%v6:fd00::/8,%v6:fe80::/10
         '';
-        description = "Options to go in the 'config setup' section of the libreswan ipsec configuration";
+        description = "Options to go in the 'config setup' section of the Libreswan IPsec configuration";
       };
 
       connections = mkOption {
         type = types.attrsOf types.lines;
         default = {};
-        example = {
-          myconnection = ''
-            auto=add
-            left=%defaultroute
-            leftid=@user
-
-            right=my.vpn.com
-
-            ikev2=no
-            ikelifetime=8h
-          '';
-        };
-        description = "A set of connections to define for the libreswan ipsec service";
+        example = literalExample ''
+          { myconnection = '''
+              auto=add
+              left=%defaultroute
+              leftid=@user
+
+              right=my.vpn.com
+
+              ikev2=no
+              ikelifetime=8h
+            ''';
+          }
+        '';
+        description = "A set of connections to define for the Libreswan IPsec service";
+      };
+
+      policies = mkOption {
+        type = types.attrsOf types.lines;
+        default = {};
+        example = literalExample ''
+          { private-or-clear = '''
+              # Attempt opportunistic IPsec for the entire Internet
+              0.0.0.0/0
+              ::/0
+            ''';
+          }
+        '';
+        description = ''
+          A set of policies to apply to the IPsec connections.
+
+          <note><para>
+            The policy name must match the one of connection it needs to apply to.
+          </para></note>
+        '';
       };
+
+      disableRedirects = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to disable send and accept redirects for all nework interfaces.
+          See the Libreswan <link xlink:href="https://libreswan.org/wiki/FAQ#Why_is_it_recommended_to_disable_send_redirects_in_.2Fproc.2Fsys.2Fnet_.3F">
+          FAQ</link> page for why this is recommended.
+        '';
+      };
+
     };
 
   };
@@ -85,43 +121,38 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.libreswan pkgs.iproute ];
+    # Install package, systemd units, etc.
+    environment.systemPackages = [ pkgs.libreswan pkgs.iproute2 ];
+    systemd.packages = [ pkgs.libreswan ];
+    systemd.tmpfiles.packages = [ pkgs.libreswan ];
+
+    # Install configuration files
+    environment.etc = {
+      "ipsec.secrets".source = "${pkgs.libreswan}/etc/ipsec.secrets";
+      "ipsec.conf".source = "${pkgs.libreswan}/etc/ipsec.conf";
+      "ipsec.d/01-nixos.conf".source = configFile;
+    } // policyFiles;
+
+    # Create NSS database directory
+    systemd.tmpfiles.rules = [ "d /var/lib/ipsec/nss 755 root root -" ];
 
     systemd.services.ipsec = {
       description = "Internet Key Exchange (IKE) Protocol Daemon for IPsec";
-      path = [
-        "${pkgs.libreswan}"
-        "${pkgs.iproute}"
-        "${pkgs.procps}"
-        "${pkgs.nssTools}"
-        "${pkgs.iptables}"
-        "${pkgs.nettools}"
-      ];
-
-      wants = [ "network-online.target" ];
-      after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
-
-      serviceConfig = {
-        Type = "simple";
-        Restart = "always";
-        EnvironmentFile = "-${pkgs.libreswan}/etc/sysconfig/pluto";
-        ExecStartPre = [
-          "${libexec}/addconn --config ${configFile} --checkconfig"
-          "${libexec}/_stackmanager start"
-          "${ipsec} --checknss"
-          "${ipsec} --checknflog"
-        ];
-        ExecStart = "${libexec}/pluto --config ${configFile} --nofork \$PLUTO_OPTIONS";
-        ExecStop = "${libexec}/whack --shutdown";
-        ExecStopPost = [
-          "${pkgs.iproute}/bin/ip xfrm policy flush"
-          "${pkgs.iproute}/bin/ip xfrm state flush"
-          "${ipsec} --stopnflog"
-        ];
-        ExecReload = "${libexec}/whack --listen";
-      };
-
+      restartTriggers = [ configFile ] ++ mapAttrsToList (n: v: v.source) policyFiles;
+      path = with pkgs; [
+        libreswan
+        iproute2
+        procps
+        nssTools
+        iptables
+        nettools
+      ];
+      preStart = optionalString cfg.disableRedirects ''
+        # Disable send/receive redirects
+        echo 0 | tee /proc/sys/net/ipv4/conf/*/send_redirects
+        echo 0 | tee /proc/sys/net/ipv{4,6}/conf/*/accept_redirects
+      '';
     };
 
   };
diff --git a/nixos/modules/services/networking/mailpile.nix b/nixos/modules/services/networking/mailpile.nix
index b79ee11d17d..4673a2580b6 100644
--- a/nixos/modules/services/networking/mailpile.nix
+++ b/nixos/modules/services/networking/mailpile.nix
@@ -21,11 +21,13 @@ in
       enable = mkEnableOption "Mailpile the mail client";
 
       hostname = mkOption {
+        type = types.str;
         default = "localhost";
         description = "Listen to this hostname or ip.";
       };
       port = mkOption {
-        default = "33411";
+        type = types.port;
+        default = 33411;
         description = "Listen on this port.";
       };
     };
diff --git a/nixos/modules/services/networking/matterbridge.nix b/nixos/modules/services/networking/matterbridge.nix
index b8b4f37c84a..9186eee26ab 100644
--- a/nixos/modules/services/networking/matterbridge.nix
+++ b/nixos/modules/services/networking/matterbridge.nix
@@ -38,8 +38,8 @@ in
           # Use services.matterbridge.configPath instead.
 
           [irc]
-              [irc.freenode]
-              Server="irc.freenode.net:6667"
+              [irc.libera]
+              Server="irc.libera.chat:6667"
               Nick="matterbot"
 
           [mattermost]
@@ -55,7 +55,7 @@ in
           name="gateway1"
           enable=true
               [[gateway.inout]]
-              account="irc.freenode"
+              account="irc.libera"
               channel="#testing"
 
               [[gateway.inout]]
diff --git a/nixos/modules/services/networking/monero.nix b/nixos/modules/services/networking/monero.nix
index 97af2997839..9a9084e4ce1 100644
--- a/nixos/modules/services/networking/monero.nix
+++ b/nixos/modules/services/networking/monero.nix
@@ -4,7 +4,6 @@ with lib;
 
 let
   cfg     = config.services.monero;
-  dataDir = "/var/lib/monero";
 
   listToConf = option: list:
     concatMapStrings (value: "${option}=${value}\n") list;
@@ -53,11 +52,19 @@ in
 
       enable = mkEnableOption "Monero node daemon";
 
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/monero";
+        description = ''
+          The directory where Monero stores its data files.
+        '';
+      };
+
       mining.enable = mkOption {
         type = types.bool;
         default = false;
         description = ''
-          Whether to mine moneroj.
+          Whether to mine monero.
         '';
       };
 
@@ -87,7 +94,7 @@ in
       };
 
       rpc.password = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         default = null;
         description = ''
           Password for RPC connections.
@@ -103,7 +110,7 @@ in
       };
 
       rpc.port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 18081;
         description = ''
           Port the RPC server will bind to.
@@ -198,15 +205,14 @@ in
   config = mkIf cfg.enable {
 
     users.users.monero = {
-      uid  = config.ids.uids.monero;
+      isSystemUser = true;
+      group = "monero";
       description = "Monero daemon user";
-      home = dataDir;
+      home = cfg.dataDir;
       createHome = true;
     };
 
-    users.groups.monero = {
-      gid = config.ids.gids.monero;
-    };
+    users.groups.monero = { };
 
     systemd.services.monero = {
       description = "monero daemon";
diff --git a/nixos/modules/services/networking/morty.nix b/nixos/modules/services/networking/morty.nix
index e3a6444c116..e110a5c8610 100644
--- a/nixos/modules/services/networking/morty.nix
+++ b/nixos/modules/services/networking/morty.nix
@@ -29,9 +29,11 @@ in
       key = mkOption {
         type = types.str;
         default = "";
-        description = "HMAC url validation key (hexadecimal encoded).
-	Leave blank to disable. Without validation key, anyone can
-	submit proxy requests. Leave blank to disable.";
+        description = ''
+          HMAC url validation key (hexadecimal encoded).
+          Leave blank to disable. Without validation key, anyone can
+          submit proxy requests. Leave blank to disable.
+        '';
         defaultText = "No HMAC url validation. Generate with echo -n somevalue | openssl dgst -sha1 -hmac somekey";
       };
 
@@ -85,10 +87,10 @@ in
         serviceConfig = {
           User = "morty";
           ExecStart = ''${cfg.package}/bin/morty              \
-	    -listen ${cfg.listenAddress}:${toString cfg.port} \
-	    ${optionalString cfg.ipv6 "-ipv6"}                \
-	    ${optionalString (cfg.key != "") "-key " + cfg.key} \
-	  '';
+            -listen ${cfg.listenAddress}:${toString cfg.port} \
+            ${optionalString cfg.ipv6 "-ipv6"}                \
+            ${optionalString (cfg.key != "") "-key " + cfg.key} \
+          '';
         };
       };
     environment.systemPackages = [ cfg.package ];
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index d2feb93e2b7..8e814ffd0b9 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -20,8 +20,7 @@ let
     acl_file ${aclFile}
     persistence true
     allow_anonymous ${boolToString cfg.allowAnonymous}
-    bind_address ${cfg.host}
-    port ${toString cfg.port}
+    listener ${toString cfg.port} ${cfg.host}
     ${passwordConf}
     ${listenerConf}
     ${cfg.extraConf}
@@ -123,12 +122,33 @@ in
               '';
             };
 
+            passwordFile = mkOption {
+              type = with types; uniq (nullOr str);
+              example = "/path/to/file";
+              default = null;
+              description = ''
+                Specifies the path to a file containing the
+                clear text password for the MQTT user.
+              '';
+            };
+
             hashedPassword = mkOption {
               type = with types; uniq (nullOr str);
               default = null;
               description = ''
                 Specifies the hashed password for the MQTT User.
-                <option>hashedPassword</option> overrides <option>password</option>.
+                To generate hashed password install <literal>mosquitto</literal>
+                package and use <literal>mosquitto_passwd</literal>.
+              '';
+            };
+
+            hashedPasswordFile = mkOption {
+              type = with types; uniq (nullOr str);
+              example = "/path/to/file";
+              default = null;
+              description = ''
+                Specifies the path to a file containing the
+                hashed password for the MQTT user.
                 To generate hashed password install <literal>mosquitto</literal>
                 package and use <literal>mosquitto_passwd</literal>.
               '';
@@ -190,6 +210,13 @@ in
 
   config = mkIf cfg.enable {
 
+    assertions = mapAttrsToList (name: cfg: {
+      assertion = length (filter (s: s != null) (with cfg; [
+        password passwordFile hashedPassword hashedPasswordFile
+      ])) <= 1;
+      message = "Cannot set more than one password option";
+    }) cfg.users;
+
     systemd.services.mosquitto = {
       description = "Mosquitto MQTT Broker Daemon";
       wantedBy = [ "multi-user.target" ];
@@ -204,13 +231,62 @@ in
         Restart = "on-failure";
         ExecStart = "${pkgs.mosquitto}/bin/mosquitto -c ${mosquittoConf}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        ReadWritePaths = [
+          cfg.dataDir
+          "/tmp"  # mosquitto_passwd creates files in /tmp before moving them
+        ];
+        ReadOnlyPaths = with cfg.ssl; lib.optionals (enable) [
+          certfile
+          keyfile
+          cafile
+        ];
+        RemoveIPC = true;
+        RestrictAddressFamilies = [
+          "AF_UNIX"  # for sd_notify() call
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+          "~@resources"
+        ];
+        UMask = "0077";
       };
       preStart = ''
         rm -f ${cfg.dataDir}/passwd
         touch ${cfg.dataDir}/passwd
       '' + concatStringsSep "\n" (
         mapAttrsToList (n: c:
-          if c.hashedPassword != null then
+          if c.hashedPasswordFile != null then
+            "echo '${n}:'$(cat '${c.hashedPasswordFile}') >> ${cfg.dataDir}/passwd"
+          else if c.passwordFile != null then
+            "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} $(cat '${c.passwordFile}')"
+          else if c.hashedPassword != null then
             "echo '${n}:${c.hashedPassword}' >> ${cfg.dataDir}/passwd"
           else optionalString (c.password != null)
             "${pkgs.mosquitto}/bin/mosquitto_passwd -b ${cfg.dataDir}/passwd ${n} '${c.password}'"
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
index cc98414257c..8ce71f26b3e 100644
--- a/nixos/modules/services/networking/mullvad-vpn.nix
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -15,6 +15,9 @@ with lib;
   config = mkIf cfg.enable {
     boot.kernelModules = [ "tun" ];
 
+    # mullvad-daemon writes to /etc/iproute2/rt_tables
+    networking.iproute2.enable = true;
+
     systemd.services.mullvad-daemon = {
       description = "Mullvad VPN daemon";
       wantedBy = [ "multi-user.target" ];
@@ -25,13 +28,13 @@ with lib;
         "systemd-resolved.service"
       ];
       path = [
-        pkgs.iproute
+        pkgs.iproute2
         # Needed for ping
         "/run/wrappers"
       ];
+      startLimitBurst = 5;
+      startLimitIntervalSec = 20;
       serviceConfig = {
-        StartLimitBurst = 5;
-        StartLimitIntervalSec = 20;
         ExecStart = "${pkgs.mullvad-vpn}/bin/mullvad-daemon -v --disable-stdout-timestamps";
         Restart = "always";
         RestartSec = 1;
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index 3054ae1b201..f8bb878ec65 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -98,7 +98,7 @@ in
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 64738;
         description = "Ports to bind to (UDP and TCP).";
       };
@@ -109,6 +109,13 @@ in
         description = "Host to bind to. Defaults binding on all addresses.";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.murmur;
+        defaultText = "pkgs.murmur";
+        description = "Overridable attribute of the murmur package to use.";
+      };
+
       password = mkOption {
         type = types.str;
         default = "";
@@ -241,6 +248,34 @@ in
         default = "";
         description = "Extra configuration to put into murmur.ini.";
       };
+
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/murmur/murmurd.env";
+        description = ''
+          Environment file as defined in <citerefentry>
+          <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+          </citerefentry>.
+
+          Secrets may be passed to the service without adding them to the world-readable
+          Nix store, by specifying placeholder variables as the option value in Nix and
+          setting these variables accordingly in the environment file.
+
+          <programlisting>
+            # snippet of murmur-related config
+            services.murmur.password = "$MURMURD_PASSWORD";
+          </programlisting>
+
+          <programlisting>
+            # content of the environment file
+            MURMURD_PASSWORD=verysecretpassword
+          </programlisting>
+
+          Note that this file needs to be available on the host on which
+          <literal>murmur</literal> is running.
+        '';
+      };
     };
   };
 
@@ -250,20 +285,33 @@ in
       home            = "/var/lib/murmur";
       createHome      = true;
       uid             = config.ids.uids.murmur;
+      group           = "murmur";
+    };
+    users.groups.murmur = {
+      gid             = config.ids.gids.murmur;
     };
 
     systemd.services.murmur = {
       description = "Murmur Chat Service";
       wantedBy    = [ "multi-user.target" ];
       after       = [ "network-online.target "];
+      preStart    = ''
+        ${pkgs.envsubst}/bin/envsubst \
+          -o /run/murmur/murmurd.ini \
+          -i ${configFile}
+      '';
 
       serviceConfig = {
         # murmurd doesn't fork when logging to the console.
-        Type      = if forking then "forking" else "simple";
-        PIDFile   = mkIf forking "/run/murmur/murmurd.pid";
-        RuntimeDirectory = mkIf forking "murmur";
-        User      = "murmur";
-        ExecStart = "${pkgs.murmur}/bin/murmurd -ini ${configFile}";
+        Type = if forking then "forking" else "simple";
+        PIDFile = mkIf forking "/run/murmur/murmurd.pid";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${cfg.package}/bin/murmurd -ini /run/murmur/murmurd.ini";
+        Restart = "always";
+        RuntimeDirectory = "murmur";
+        RuntimeDirectoryMode = "0700";
+        User = "murmur";
+        Group = "murmur";
       };
     };
   };
diff --git a/nixos/modules/services/networking/mxisd.nix b/nixos/modules/services/networking/mxisd.nix
index 482d6ff456b..f29d190c626 100644
--- a/nixos/modules/services/networking/mxisd.nix
+++ b/nixos/modules/services/networking/mxisd.nix
@@ -41,8 +41,8 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.mxisd;
-        defaultText = "pkgs.mxisd";
+        default = pkgs.ma1sd;
+        defaultText = "pkgs.ma1sd";
         description = "The mxisd/ma1sd package to use";
       };
 
diff --git a/nixos/modules/services/networking/namecoind.nix b/nixos/modules/services/networking/namecoind.nix
index 6ca99e1321b..8f7a5123f7e 100644
--- a/nixos/modules/services/networking/namecoind.nix
+++ b/nixos/modules/services/networking/namecoind.nix
@@ -89,7 +89,7 @@ in
       };
 
       rpc.password = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         default = null;
         description = ''
           Password for RPC connections.
@@ -105,7 +105,7 @@ in
       };
 
       rpc.port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 8332;
         description = ''
           Port the RPC server will bind to.
@@ -165,6 +165,8 @@ in
       after    = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
+      startLimitIntervalSec = 120;
+      startLimitBurst = 5;
       serviceConfig = {
         User  = "namecoin";
         Group = "namecoin";
@@ -176,8 +178,6 @@ in
         TimeoutStopSec     = "60s";
         TimeoutStartSec    = "2s";
         Restart            = "always";
-        StartLimitInterval = "120s";
-        StartLimitBurst    = "5";
       };
 
       preStart = optionalString (cfg.wallet != "${dataDir}/wallet.dat")  ''
diff --git a/nixos/modules/services/networking/nar-serve.nix b/nixos/modules/services/networking/nar-serve.nix
new file mode 100644
index 00000000000..745138186a2
--- /dev/null
+++ b/nixos/modules/services/networking/nar-serve.nix
@@ -0,0 +1,55 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.nar-serve;
+in
+{
+  meta = {
+    maintainers = [ maintainers.rizary ];
+  };
+  options = {
+    services.nar-serve = {
+      enable = mkEnableOption "Serve NAR file contents via HTTP";
+
+      port = mkOption {
+        type = types.port;
+        default = 8383;
+        description = ''
+          Port number where nar-serve will listen on.
+        '';
+      };
+
+      cacheURL = mkOption {
+        type = types.str;
+        default = "https://cache.nixos.org/";
+        description = ''
+          Binary cache URL to connect to.
+
+          The URL format is compatible with the nix remote url style, such as:
+          - http://, https:// for binary caches via HTTP or HTTPS
+          - s3:// for binary caches stored in Amazon S3
+          - gs:// for binary caches stored in Google Cloud Storage
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nar-serve = {
+      description = "NAR server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment.PORT = toString cfg.port;
+      environment.NAR_CACHE_URL = cfg.cacheURL;
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "5s";
+        ExecStart = "${pkgs.nar-serve}/bin/nar-serve";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index 21ae9eb8b6d..45eb500fe8c 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -9,7 +9,14 @@ with lib;
 let
   cfg = config.networking.nat;
 
-  dest = if cfg.externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${cfg.externalIP}";
+  mkDest = externalIP: if externalIP == null
+                       then "-j MASQUERADE"
+                       else "-j SNAT --to-source ${externalIP}";
+  dest = mkDest cfg.externalIP;
+  destIPv6 = mkDest cfg.externalIPv6;
+
+  # Whether given IP (plus optional port) is an IPv6.
+  isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
 
   helpers = import ./helpers.nix { inherit config lib; };
 
@@ -28,63 +35,80 @@ let
     ${cfg.extraStopCommands}
   '';
 
-  setupNat = ''
-    ${helpers}
-    # Create subchain where we store rules
-    ip46tables -w -t nat -N nixos-nat-pre
-    ip46tables -w -t nat -N nixos-nat-post
-    ip46tables -w -t nat -N nixos-nat-out
-
+  mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
     # We can't match on incoming interface in POSTROUTING, so
     # mark packets coming from the internal interfaces.
     ${concatMapStrings (iface: ''
-      iptables -w -t nat -A nixos-nat-pre \
+      ${iptables} -w -t nat -A nixos-nat-pre \
         -i '${iface}' -j MARK --set-mark 1
     '') cfg.internalInterfaces}
 
     # NAT the marked packets.
     ${optionalString (cfg.internalInterfaces != []) ''
-      iptables -w -t nat -A nixos-nat-post -m mark --mark 1 \
+      ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
         ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
     ''}
 
     # NAT packets coming from the internal IPs.
     ${concatMapStrings (range: ''
-      iptables -w -t nat -A nixos-nat-post \
+      ${iptables} -w -t nat -A nixos-nat-post \
         -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
-    '') cfg.internalIPs}
+    '') internalIPs}
 
     # NAT from external ports to internal ports.
     ${concatMapStrings (fwd: ''
-      iptables -w -t nat -A nixos-nat-pre \
+      ${iptables} -w -t nat -A nixos-nat-pre \
         -i ${toString cfg.externalInterface} -p ${fwd.proto} \
         --dport ${builtins.toString fwd.sourcePort} \
         -j DNAT --to-destination ${fwd.destination}
 
       ${concatMapStrings (loopbackip:
         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 builtins.replaceStrings ["-"] [":"] (elemAt m 1);
+          matchIP          = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+          m                = builtins.match "${matchIP}:([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 builtins.replaceStrings ["-"] [":"] (elemAt m 1);
         in ''
           # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
-          iptables -w -t nat -A nixos-nat-out \
+          ${iptables} -w -t nat -A nixos-nat-out \
             -d ${loopbackip} -p ${fwd.proto} \
             --dport ${builtins.toString fwd.sourcePort} \
             -j DNAT --to-destination ${fwd.destination}
 
           # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
-          iptables -w -t nat -A nixos-nat-pre \
+          ${iptables} -w -t nat -A nixos-nat-pre \
             -d ${loopbackip} -p ${fwd.proto} \
             --dport ${builtins.toString fwd.sourcePort} \
             -j DNAT --to-destination ${fwd.destination}
 
-          iptables -w -t nat -A nixos-nat-post \
+          ${iptables} -w -t nat -A nixos-nat-post \
             -d ${destinationIP} -p ${fwd.proto} \
             --dport ${destinationPorts} \
             -j SNAT --to-source ${loopbackip}
         '') fwd.loopbackIPs}
-    '') cfg.forwardPorts}
+    '') forwardPorts}
+  '';
+
+  setupNat = ''
+    ${helpers}
+    # Create subchains where we store rules
+    ip46tables -w -t nat -N nixos-nat-pre
+    ip46tables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-out
+
+    ${mkSetupNat {
+      iptables = "iptables";
+      inherit dest;
+      inherit (cfg) internalIPs;
+      forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+    }}
+
+    ${optionalString cfg.enableIPv6 (mkSetupNat {
+      iptables = "ip6tables";
+      dest = destIPv6;
+      internalIPs = cfg.internalIPv6s;
+      forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+    })}
 
     ${optionalString (cfg.dmzHost != null) ''
       iptables -w -t nat -A nixos-nat-pre \
@@ -117,6 +141,15 @@ in
         '';
     };
 
+    networking.nat.enableIPv6 = mkOption {
+      type = types.bool;
+      default = false;
+      description =
+        ''
+          Whether to enable IPv6 NAT.
+        '';
+    };
+
     networking.nat.internalInterfaces = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -141,6 +174,18 @@ in
         '';
     };
 
+    networking.nat.internalIPv6s = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "fc00::/64" ];
+      description =
+        ''
+          The IPv6 address ranges for which to perform NAT.  Packets
+          coming from these addresses (on any interface) and destined
+          for the external interface will be rewritten.
+        '';
+    };
+
     networking.nat.externalInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -164,6 +209,19 @@ in
         '';
     };
 
+    networking.nat.externalIPv6 = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      example = "2001:dc0:2001:11::175";
+      description =
+        ''
+          The public IPv6 address to which packets from the local
+          network are to be rewritten.  If this is left empty, the
+          IP address associated with the external interface will be
+          used.
+        '';
+    };
+
     networking.nat.forwardPorts = mkOption {
       type = with types; listOf (submodule {
         options = {
@@ -176,7 +234,7 @@ in
           destination = mkOption {
             type = types.str;
             example = "10.0.0.1:80";
-            description = "Forward connection to destination ip:port; to specify a port range, use ip:start-end";
+            description = "Forward connection to destination ip:port (or [ipv6]:port); to specify a port range, use ip:start-end";
           };
 
           proto = mkOption {
@@ -195,11 +253,15 @@ in
         };
       });
       default = [];
-      example = [ { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; } ];
+      example = [
+        { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
+        { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
+      ];
       description =
         ''
           List of forwarded ports from the external interface to
-          internal destinations by using DNAT.
+          internal destinations by using DNAT. Destination can be
+          IPv6 if IPv6 NAT is enabled.
         '';
     };
 
@@ -246,6 +308,9 @@ in
     (mkIf config.networking.nat.enable {
 
       assertions = [
+        { assertion = cfg.enableIPv6           -> config.networking.enableIPv6;
+          message = "networking.nat.enableIPv6 requires networking.enableIPv6";
+        }
         { assertion = (cfg.dmzHost != null)    -> (cfg.externalInterface != null);
           message = "networking.nat.dmzHost requires networking.nat.externalInterface";
         }
@@ -261,6 +326,15 @@ in
         kernel.sysctl = {
           "net.ipv4.conf.all.forwarding" = mkOverride 99 true;
           "net.ipv4.conf.default.forwarding" = mkOverride 99 true;
+        } // optionalAttrs cfg.enableIPv6 {
+          # Do not prevent IPv6 autoconfiguration.
+          # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
+          "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
+          "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
+
+          # Forward IPv6 packets.
+          "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
+          "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
         };
       };
 
diff --git a/nixos/modules/services/networking/ncdns.nix b/nixos/modules/services/networking/ncdns.nix
index c1832ad1752..d30fe0f6f6d 100644
--- a/nixos/modules/services/networking/ncdns.nix
+++ b/nixos/modules/services/networking/ncdns.nix
@@ -243,8 +243,10 @@ in
         xlog.journal = true;
     };
 
-    users.users.ncdns =
-      { description = "ncdns daemon user"; };
+    users.users.ncdns = {
+      isSystemUser = true;
+      description = "ncdns daemon user";
+    };
 
     systemd.services.ncdns = {
       description = "ncdns daemon";
diff --git a/nixos/modules/services/networking/nebula.nix b/nixos/modules/services/networking/nebula.nix
new file mode 100644
index 00000000000..e7ebfe1b4db
--- /dev/null
+++ b/nixos/modules/services/networking/nebula.nix
@@ -0,0 +1,219 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.nebula;
+  enabledNetworks = filterAttrs (n: v: v.enable) cfg.networks;
+
+  format = pkgs.formats.yaml {};
+
+  nameToId = netName: "nebula-${netName}";
+in
+{
+  # Interface
+
+  options = {
+    services.nebula = {
+      networks = mkOption {
+        description = "Nebula network definitions.";
+        default = {};
+        type = types.attrsOf (types.submodule {
+          options = {
+            enable = mkOption {
+              type = types.bool;
+              default = true;
+              description = "Enable or disable this network.";
+            };
+
+            package = mkOption {
+              type = types.package;
+              default = pkgs.nebula;
+              defaultText = "pkgs.nebula";
+              description = "Nebula derivation to use.";
+            };
+
+            ca = mkOption {
+              type = types.path;
+              description = "Path to the certificate authority certificate.";
+              example = "/etc/nebula/ca.crt";
+            };
+
+            cert = mkOption {
+              type = types.path;
+              description = "Path to the host certificate.";
+              example = "/etc/nebula/host.crt";
+            };
+
+            key = mkOption {
+              type = types.path;
+              description = "Path to the host key.";
+              example = "/etc/nebula/host.key";
+            };
+
+            staticHostMap = mkOption {
+              type = types.attrsOf (types.listOf (types.str));
+              default = {};
+              description = ''
+                The static host map defines a set of hosts with fixed IP addresses on the internet (or any network).
+                A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
+              '';
+              example = literalExample ''
+                { "192.168.100.1" = [ "100.64.22.11:4242" ]; }
+              '';
+            };
+
+            isLighthouse = mkOption {
+              type = types.bool;
+              default = false;
+              description = "Whether this node is a lighthouse.";
+            };
+
+            lighthouses = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = ''
+                List of IPs of lighthouse hosts this node should report to and query from. This should be empty on lighthouse
+                nodes. The IPs should be the lighthouse's Nebula IPs, not their external IPs.
+              '';
+              example = ''[ "192.168.100.1" ]'';
+            };
+
+            listen.host = mkOption {
+              type = types.str;
+              default = "0.0.0.0";
+              description = "IP address to listen on.";
+            };
+
+            listen.port = mkOption {
+              type = types.port;
+              default = 4242;
+              description = "Port number to listen on.";
+            };
+
+            tun.disable = mkOption {
+              type = types.bool;
+              default = false;
+              description = ''
+                When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root).
+              '';
+            };
+
+            tun.device = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = "Name of the tun device. Defaults to nebula.\${networkName}.";
+            };
+
+            firewall.outbound = mkOption {
+              type = types.listOf types.attrs;
+              default = [];
+              description = "Firewall rules for outbound traffic.";
+              example = ''[ { port = "any"; proto = "any"; host = "any"; } ]'';
+            };
+
+            firewall.inbound = mkOption {
+              type = types.listOf types.attrs;
+              default = [];
+              description = "Firewall rules for inbound traffic.";
+              example = ''[ { port = "any"; proto = "any"; host = "any"; } ]'';
+            };
+
+            settings = mkOption {
+              type = format.type;
+              default = {};
+              description = ''
+                Nebula configuration. Refer to
+                <link xlink:href="https://github.com/slackhq/nebula/blob/master/examples/config.yml"/>
+                for details on supported values.
+              '';
+              example = literalExample ''
+                {
+                  lighthouse.dns = {
+                    host = "0.0.0.0";
+                    port = 53;
+                  };
+                }
+              '';
+            };
+          };
+        });
+      };
+    };
+  };
+
+  # Implementation
+  config = mkIf (enabledNetworks != {}) {
+    systemd.services = mkMerge (mapAttrsToList (netName: netCfg:
+      let
+        networkId = nameToId netName;
+        settings = recursiveUpdate {
+          pki = {
+            ca = netCfg.ca;
+            cert = netCfg.cert;
+            key = netCfg.key;
+          };
+          static_host_map = netCfg.staticHostMap;
+          lighthouse = {
+            am_lighthouse = netCfg.isLighthouse;
+            hosts = netCfg.lighthouses;
+          };
+          listen = {
+            host = netCfg.listen.host;
+            port = netCfg.listen.port;
+          };
+          tun = {
+            disabled = netCfg.tun.disable;
+            dev = if (netCfg.tun.device != null) then netCfg.tun.device else "nebula.${netName}";
+          };
+          firewall = {
+            inbound = netCfg.firewall.inbound;
+            outbound = netCfg.firewall.outbound;
+          };
+        } netCfg.settings;
+        configFile = format.generate "nebula-config-${netName}.yml" settings;
+        in
+        {
+          # Create systemd service for Nebula.
+          "nebula@${netName}" = {
+            description = "Nebula VPN service for ${netName}";
+            wants = [ "basic.target" ];
+            after = [ "basic.target" "network.target" ];
+            before = [ "sshd.service" ];
+            wantedBy = [ "multi-user.target" ];
+            serviceConfig = mkMerge [
+              {
+                Type = "simple";
+                Restart = "always";
+                ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
+              }
+              # The service needs to launch as root to access the tun device, if it's enabled.
+              (mkIf netCfg.tun.disable {
+                User = networkId;
+                Group = networkId;
+              })
+            ];
+          };
+        }) enabledNetworks);
+
+    # Open the chosen ports for UDP.
+    networking.firewall.allowedUDPPorts =
+      unique (mapAttrsToList (netName: netCfg: netCfg.listen.port) enabledNetworks);
+
+    # Create the service users and groups.
+    users.users = mkMerge (mapAttrsToList (netName: netCfg:
+      mkIf netCfg.tun.disable {
+        ${nameToId netName} = {
+          group = nameToId netName;
+          description = "Nebula service user for network ${netName}";
+          isSystemUser = true;
+        };
+      }) enabledNetworks);
+
+    users.groups = mkMerge (mapAttrsToList (netName: netCfg:
+      mkIf netCfg.tun.disable {
+        ${nameToId netName} = {};
+      }) enabledNetworks);
+  };
+}
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index cc789897b29..064018057cd 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -15,42 +15,58 @@ let
     networkmanager-openconnect
     networkmanager-openvpn
     networkmanager-vpnc
+    networkmanager-sstp
    ] ++ optional (!delegateWireless && !enableIwd) wpa_supplicant;
 
   delegateWireless = config.networking.wireless.enable == true && cfg.unmanaged != [];
 
   enableIwd = cfg.wifi.backend == "iwd";
 
-  configFile = pkgs.writeText "NetworkManager.conf" ''
-    [main]
-    plugins=keyfile
-    dhcp=${cfg.dhcp}
-    dns=${cfg.dns}
-    # If resolvconf is disabled that means that resolv.conf is managed by some other module.
-    rc-manager=${if config.networking.resolvconf.enable then "resolvconf" else "unmanaged"}
-
-    [keyfile]
-    ${optionalString (cfg.unmanaged != [])
-      ''unmanaged-devices=${lib.concatStringsSep ";" cfg.unmanaged}''}
-
-    [logging]
-    level=${cfg.logLevel}
-    audit=${lib.boolToString config.security.audit.enable}
-
-    [connection]
-    ipv6.ip6-privacy=2
-    ethernet.cloned-mac-address=${cfg.ethernet.macAddress}
-    wifi.cloned-mac-address=${cfg.wifi.macAddress}
-    ${optionalString (cfg.wifi.powersave != null)
-      ''wifi.powersave=${if cfg.wifi.powersave then "3" else "2"}''}
-
-    [device]
-    wifi.scan-rand-mac-address=${if cfg.wifi.scanRandMacAddress then "yes" else "no"}
-    wifi.backend=${cfg.wifi.backend}
-
-    ${cfg.extraConfig}
+  mkValue = v:
+    if v == true then "yes"
+    else if v == false then "no"
+    else if lib.isInt v then toString v
+    else v;
+
+  mkSection = name: attrs: ''
+    [${name}]
+    ${
+      lib.concatStringsSep "\n"
+        (lib.mapAttrsToList
+          (k: v: "${k}=${mkValue v}")
+          (lib.filterAttrs
+            (k: v: v != null)
+            attrs))
+    }
   '';
 
+  configFile = pkgs.writeText "NetworkManager.conf" (lib.concatStringsSep "\n" [
+    (mkSection "main" {
+      plugins = "keyfile";
+      dhcp = cfg.dhcp;
+      dns = cfg.dns;
+      # If resolvconf is disabled that means that resolv.conf is managed by some other module.
+      rc-manager =
+        if config.networking.resolvconf.enable then "resolvconf"
+        else "unmanaged";
+    })
+    (mkSection "keyfile" {
+      unmanaged-devices =
+        if cfg.unmanaged == [] then null
+        else lib.concatStringsSep ";" cfg.unmanaged;
+    })
+    (mkSection "logging" {
+      audit = config.security.audit.enable;
+      level = cfg.logLevel;
+    })
+    (mkSection "connection" cfg.connectionConfig)
+    (mkSection "device" {
+      "wifi.scan-rand-mac-address" = cfg.wifi.scanRandMacAddress;
+      "wifi.backend" = cfg.wifi.backend;
+    })
+    cfg.extraConfig
+  ]);
+
   /*
     [network-manager]
     Identity=unix-group:networkmanager
@@ -153,6 +169,28 @@ in {
         '';
       };
 
+      connectionConfig = mkOption {
+        type = with types; attrsOf (nullOr (oneOf [
+          bool
+          int
+          str
+        ]));
+        default = {};
+        description = ''
+          Configuration for the [connection] section of NetworkManager.conf.
+          Refer to
+          <link xlink:href="https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html">
+            https://developer.gnome.org/NetworkManager/stable/NetworkManager.conf.html#id-1.2.3.11
+          </link>
+          or
+          <citerefentry>
+            <refentrytitle>NetworkManager.conf</refentrytitle>
+            <manvolnum>5</manvolnum>
+          </citerefentry>
+          for more information.
+        '';
+      };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "";
@@ -386,6 +424,9 @@ in {
 
       "NetworkManager/VPN/nm-iodine-service.name".source =
         "${networkmanager-iodine}/lib/NetworkManager/VPN/nm-iodine-service.name";
+
+      "NetworkManager/VPN/nm-sstp-service.name".source =
+        "${networkmanager-sstp}/lib/NetworkManager/VPN/nm-sstp-service.name";
       }
       // optionalAttrs (cfg.appendNameservers != [] || cfg.insertNameservers != [])
          {
@@ -458,10 +499,10 @@ in {
 
     systemd.services.NetworkManager-dispatcher = {
       wantedBy = [ "network.target" ];
-      restartTriggers = [ configFile ];
+      restartTriggers = [ configFile overrideNameserversScript ];
 
       # useful binaries for user-specified hooks
-      path = [ pkgs.iproute pkgs.utillinux pkgs.coreutils ];
+      path = [ pkgs.iproute2 pkgs.util-linux pkgs.coreutils ];
       aliases = [ "dbus-org.freedesktop.nm-dispatcher.service" ];
     };
 
@@ -478,8 +519,22 @@ in {
       (mkIf enableIwd {
         wireless.iwd.enable = true;
       })
+
+      {
+        networkmanager.connectionConfig = {
+          "ipv6.ip6-privacy" = 2;
+          "ethernet.cloned-mac-address" = cfg.ethernet.macAddress;
+          "wifi.cloned-mac-address" = cfg.wifi.macAddress;
+          "wifi.powersave" =
+            if cfg.wifi.powersave == null then null
+            else if cfg.wifi.powersave then 3
+            else 2;
+        };
+      }
     ];
 
+    boot.kernelModules = [ "ctr" ];
+
     security.polkit.extraConfig = polkitConf;
 
     services.dbus.packages = cfg.packages
diff --git a/nixos/modules/services/networking/nextdns.nix b/nixos/modules/services/networking/nextdns.nix
index a633bff62ec..b070eeec894 100644
--- a/nixos/modules/services/networking/nextdns.nix
+++ b/nixos/modules/services/networking/nextdns.nix
@@ -28,9 +28,9 @@ in {
       environment = {
         SERVICE_RUN_MODE = "1";
       };
+      startLimitIntervalSec = 5;
+      startLimitBurst = 10;
       serviceConfig = {
-        StartLimitInterval = 5;
-        StartLimitBurst = 10;
         ExecStart = "${pkgs.nextdns}/bin/nextdns run ${escapeShellArgs config.services.nextdns.arguments}";
         RestartSec = 120;
         LimitMEMLOCK = "infinity";
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index ec9d9753cfe..cb75142965e 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -99,7 +99,7 @@ in
   config = mkIf cfg.enable {
     assertions = [{
       assertion = config.networking.firewall.enable == false;
-      message = "You can not use nftables with services.networking.firewall.";
+      message = "You can not use nftables and iptables at the same time. networking.firewall.enable must be set to false.";
     }];
     boot.blacklistedKernelModules = [ "ip_tables" ];
     environment.systemPackages = [ pkgs.nftables ];
diff --git a/nixos/modules/services/networking/nix-serve.nix b/nixos/modules/services/networking/nix-serve.nix
index 347d87b3f38..7fc145f2303 100644
--- a/nixos/modules/services/networking/nix-serve.nix
+++ b/nixos/modules/services/networking/nix-serve.nix
@@ -11,7 +11,7 @@ in
       enable = mkEnableOption "nix-serve, the standalone Nix binary cache server";
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 5000;
         description = ''
           Port number where nix-serve will listen on.
@@ -69,13 +69,9 @@ in
         ExecStart = "${pkgs.nix-serve}/bin/nix-serve " +
           "--listen ${cfg.bindAddress}:${toString cfg.port} ${cfg.extraParams}";
         User = "nix-serve";
-        Group = "nogroup";
+        Group = "nix-serve";
+        DynamicUser = true;
       };
     };
-
-    users.users.nix-serve = {
-      description = "Nix-serve user";
-      uid = config.ids.uids.nix-serve;
-    };
   };
 }
diff --git a/nixos/modules/services/networking/nix-store-gcs-proxy.nix b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
index 3f2ce5bca4d..0012302db2e 100644
--- a/nixos/modules/services/networking/nix-store-gcs-proxy.nix
+++ b/nixos/modules/services/networking/nix-store-gcs-proxy.nix
@@ -42,9 +42,9 @@ in
       description = "A HTTP nix store that proxies requests to Google Storage";
       wantedBy = ["multi-user.target"];
 
+      startLimitIntervalSec = 10;
       serviceConfig = {
         RestartSec = 5;
-        StartLimitInterval = 10;
         ExecStart = ''
           ${pkgs.nix-store-gcs-proxy}/bin/nix-store-gcs-proxy \
             --bucket-name ${cfg.bucketName} \
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
new file mode 100644
index 00000000000..48689f1195c
--- /dev/null
+++ b/nixos/modules/services/networking/nomad.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.nomad;
+  format = pkgs.formats.json { };
+in
+{
+  ##### interface
+  options = {
+    services.nomad = {
+      enable = mkEnableOption "Nomad, a distributed, highly available, datacenter-aware scheduler";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.nomad;
+        defaultText = "pkgs.nomad";
+        description = ''
+          The package used for the Nomad agent and CLI.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = types.listOf types.package;
+        default = [ ];
+        description = ''
+          Extra packages to add to <envar>PATH</envar> for the Nomad agent process.
+        '';
+        example = literalExample ''
+          with pkgs; [ cni-plugins ]
+        '';
+      };
+
+      dropPrivileges = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether the nomad agent should be run as a non-root nomad user.
+        '';
+      };
+
+      enableDocker = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable Docker support. Needed for Nomad's docker driver.
+
+          Note that the docker group membership is effectively equivalent
+          to being root, see https://github.com/moby/moby/issues/9976.
+        '';
+      };
+
+      extraSettingsPaths = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Additional settings paths used to configure nomad. These can be files or directories.
+        '';
+        example = literalExample ''
+          [ "/etc/nomad-mutable.json" "/run/keys/nomad-with-secrets.json" "/etc/nomad/config.d" ]
+        '';
+      };
+
+      settings = mkOption {
+        type = format.type;
+        default = {};
+        description = ''
+          Configuration for Nomad. See the <link xlink:href="https://www.nomadproject.io/docs/configuration">documentation</link>
+          for supported values.
+
+          Notes about <literal>data_dir</literal>:
+
+          If <literal>data_dir</literal> is set to a value other than the
+          default value of <literal>"/var/lib/nomad"</literal> it is the Nomad
+          cluster manager's responsibility to make sure that this directory
+          exists and has the appropriate permissions.
+
+          Additionally, if <literal>dropPrivileges</literal> is
+          <literal>true</literal> then <literal>data_dir</literal>
+          <emphasis>cannot</emphasis> be customized. Setting
+          <literal>dropPrivileges</literal> to <literal>true</literal> enables
+          the <literal>DynamicUser</literal> feature of systemd which directly
+          manages and operates on <literal>StateDirectory</literal>.
+        '';
+        example = literalExample ''
+          {
+            # A minimal config example:
+            server = {
+              enabled = true;
+              bootstrap_expect = 1; # for demo; no fault tolerance
+            };
+            client = {
+              enabled = true;
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  ##### implementation
+  config = mkIf cfg.enable {
+    services.nomad.settings = {
+      # Agrees with `StateDirectory = "nomad"` set below.
+      data_dir = mkDefault "/var/lib/nomad";
+    };
+
+    environment = {
+      etc."nomad.json".source = format.generate "nomad.json" cfg.settings;
+      systemPackages = [ cfg.package ];
+    };
+
+    systemd.services.nomad = {
+      description = "Nomad";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+      restartTriggers = [ config.environment.etc."nomad.json".source ];
+
+      path = cfg.extraPackages ++ (with pkgs; [
+        # Client mode requires at least the following:
+        coreutils
+        iproute2
+        iptables
+      ]);
+
+      serviceConfig = mkMerge [
+        {
+          DynamicUser = cfg.dropPrivileges;
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          ExecStart = "${cfg.package}/bin/nomad agent -config=/etc/nomad.json" +
+            concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths;
+          KillMode = "process";
+          KillSignal = "SIGINT";
+          LimitNOFILE = 65536;
+          LimitNPROC = "infinity";
+          OOMScoreAdjust = -1000;
+          Restart = "on-failure";
+          RestartSec = 2;
+          TasksMax = "infinity";
+        }
+        (mkIf cfg.enableDocker {
+          SupplementaryGroups = "docker"; # space-separated string
+        })
+        (mkIf (cfg.settings.data_dir == "/var/lib/nomad") {
+          StateDirectory = "nomad";
+        })
+      ];
+
+      unitConfig = {
+        StartLimitIntervalSec = 10;
+        StartLimitBurst = 3;
+      };
+    };
+
+    assertions = [
+      {
+        assertion = cfg.dropPrivileges -> cfg.settings.data_dir == "/var/lib/nomad";
+        message = "settings.data_dir must be equal to \"/var/lib/nomad\" if dropPrivileges is true";
+      }
+    ];
+
+    # Docker support requires the Docker daemon to be running.
+    virtualisation.docker.enable = mkIf cfg.enableDocker true;
+  };
+}
diff --git a/nixos/modules/services/networking/nsd.nix b/nixos/modules/services/networking/nsd.nix
index 3ecbd06ee41..2ac0a8c7922 100644
--- a/nixos/modules/services/networking/nsd.nix
+++ b/nixos/modules/services/networking/nsd.nix
@@ -20,6 +20,15 @@ let
 
   mkZoneFileName = name: if name == "." then "root" else name;
 
+  # replaces include: directives for keys with fake keys for nsd-checkconf
+  injectFakeKeys = keys: concatStrings
+    (mapAttrsToList
+      (keyName: keyOptions: ''
+        fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${escapeShellArgs [ keyOptions.algorithm keyName ]} | grep -oP "\s*secret \"\K.*(?=\";)")"
+        sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf
+      '')
+      keys);
+
   nsdEnv = pkgs.buildEnv {
     name = "nsd-env";
 
@@ -34,9 +43,9 @@ let
         echo "|- checking zone '$out/zones/$zoneFile'"
         ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
           if grep -q \\\\\\$ "$zoneFile"; then
-            echo zone "$zoneFile" contains escaped dollar signes \\\$
-            echo Escaping them is not needed any more. Please make shure \
-                 to unescape them where they prefix a variable name
+            echo zone "$zoneFile" contains escaped dollar signs \\\$
+            echo Escaping them is not needed any more. Please make sure \
+                 to unescape them where they prefix a variable name.
           fi
 
           exit 1
@@ -44,7 +53,14 @@ let
       done
 
       echo "checking configuration file"
+      # Save original config file including key references...
+      cp $out/nsd.conf{,.orig}
+      # ...inject mock keys into config
+      ${injectFakeKeys cfg.keys}
+      # ...do the checkconf
       ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf
+      # ... and restore original config file.
+      mv $out/nsd.conf{.orig,}
     '';
   };
 
@@ -916,14 +932,14 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
+      startLimitBurst = 4;
+      startLimitIntervalSec = 5 * 60;  # 5 mins
       serviceConfig = {
         ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
         StandardError = "null";
         PIDFile = pidFile;
         Restart = "always";
         RestartSec = "4s";
-        StartLimitBurst = 4;
-        StartLimitInterval = "5min";
       };
 
       preStart = ''
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
index b7e4c89a155..96c6444c23a 100644
--- a/nixos/modules/services/networking/ntp/chrony.nix
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -4,20 +4,23 @@ with lib;
 
 let
   cfg = config.services.chrony;
+  chronyPkg = cfg.package;
 
-  stateDir = "/var/lib/chrony";
+  stateDir = cfg.directory;
+  driftFile = "${stateDir}/chrony.drift";
   keyFile = "${stateDir}/chrony.keys";
 
   configFile = pkgs.writeText "chrony.conf" ''
-    ${concatMapStringsSep "\n" (server: "server " + server + " iburst") cfg.servers}
+    ${concatMapStringsSep "\n" (server: "server " + server + " " + cfg.serverOption + optionalString (cfg.enableNTS) " nts") cfg.servers}
 
     ${optionalString
       (cfg.initstepslew.enabled && (cfg.servers != []))
       "initstepslew ${toString cfg.initstepslew.threshold} ${concatStringsSep " " cfg.servers}"
     }
 
-    driftfile ${stateDir}/chrony.drift
+    driftfile ${driftFile}
     keyfile ${keyFile}
+    ${optionalString (cfg.enableNTS) "ntsdumpdir ${stateDir}"}
 
     ${optionalString (!config.time.hardwareClockInLocalTime) "rtconutc"}
 
@@ -38,14 +41,48 @@ in
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.chrony;
+        defaultText = "pkgs.chrony";
+        description = ''
+          Which chrony package to use.
+        '';
+      };
+
       servers = mkOption {
         default = config.networking.timeServers;
+        type = types.listOf types.str;
         description = ''
           The set of NTP servers from which to synchronise.
         '';
       };
 
+      serverOption = mkOption {
+        default = "iburst";
+        type = types.enum [ "iburst" "offline" ];
+        description = ''
+          Set option for server directives.
+
+          Use "iburst" to rapidly poll on startup. Recommended if your machine
+          is consistently online.
+
+          Use "offline" to prevent polling on startup. Recommended if your
+          machine boots offline or is otherwise frequently offline.
+        '';
+      };
+
+      enableNTS = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Network Time Security authentication.
+          Make sure it is supported by your selected NTP server(s).
+        '';
+      };
+
       initstepslew = mkOption {
+        type = types.attrsOf (types.either types.bool types.int);
         default = {
           enabled = true;
           threshold = 1000; # by default, same threshold as 'ntpd -g' (1000s)
@@ -57,6 +94,12 @@ in
         '';
       };
 
+      directory = mkOption {
+        type = types.str;
+        default = "/var/lib/chrony";
+        description = "Directory where chrony state is stored.";
+      };
+
       extraConfig = mkOption {
         type = types.lines;
         default = "";
@@ -78,7 +121,7 @@ in
   config = mkIf cfg.enable {
     meta.maintainers = with lib.maintainers; [ thoughtpolice ];
 
-    environment.systemPackages = [ pkgs.chrony ];
+    environment.systemPackages = [ chronyPkg ];
 
     users.groups.chrony.gid = config.ids.gids.chrony;
 
@@ -95,6 +138,7 @@ in
 
     systemd.tmpfiles.rules = [
       "d ${stateDir} 0755 chrony chrony - -"
+      "f ${driftFile} 0640 chrony chrony -"
       "f ${keyFile} 0640 chrony chrony -"
     ];
 
@@ -107,17 +151,16 @@ in
         after    = [ "network.target" ];
         conflicts = [ "ntpd.service" "systemd-timesyncd.service" ];
 
-        path = [ pkgs.chrony ];
+        path = [ chronyPkg ];
 
         unitConfig.ConditionCapability = "CAP_SYS_TIME";
         serviceConfig =
           { Type = "simple";
-            ExecStart = "${pkgs.chrony}/bin/chronyd ${chronyFlags}";
+            ExecStart = "${chronyPkg}/bin/chronyd ${chronyFlags}";
 
             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 51398851adc..861b0db01a4 100644
--- a/nixos/modules/services/networking/ntp/ntpd.nix
+++ b/nixos/modules/services/networking/ntp/ntpd.nix
@@ -79,6 +79,7 @@ in
 
       servers = mkOption {
         default = config.networking.timeServers;
+        type = types.listOf types.str;
         description = ''
           The set of NTP servers from which to synchronise.
         '';
diff --git a/nixos/modules/services/networking/nylon.nix b/nixos/modules/services/networking/nylon.nix
index 7c171281a92..a20fa615af8 100644
--- a/nixos/modules/services/networking/nylon.nix
+++ b/nixos/modules/services/networking/nylon.nix
@@ -140,7 +140,7 @@ in
     services.nylon = mkOption {
       default = {};
       description = "Collection of named nylon instances";
-      type = with types; loaOf (submodule nylonOpts);
+      type = with types; attrsOf (submodule nylonOpts);
       internal = true;
     };
 
@@ -160,7 +160,7 @@ in
 
     users.groups.nylon.gid = config.ids.gids.nylon;
 
-    systemd.services = fold (a: b: a // b) {} nylonUnits;
+    systemd.services = foldr (a: b: a // b) {} nylonUnits;
 
   };
 }
diff --git a/nixos/modules/services/networking/onedrive.nix b/nixos/modules/services/networking/onedrive.nix
index 210d2217b27..c52f920bae2 100644
--- a/nixos/modules/services/networking/onedrive.nix
+++ b/nixos/modules/services/networking/onedrive.nix
@@ -53,7 +53,7 @@ in {
       serviceConfig = {
         Type = "simple";
         ExecStart = ''
-          ${cfg.package}/bin/onedrive --monitor --verbose --confdir=%h/.config/%i
+          ${cfg.package}/bin/onedrive --monitor --confdir=%h/.config/%i
         '';
         Restart="on-failure";
         RestartSec=3;
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
index dcd7e9e5fa4..b4c2c944b6e 100644
--- a/nixos/modules/services/networking/openvpn.nix
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -11,7 +11,7 @@ let
   makeOpenVPNJob = cfg: name:
     let
 
-      path = (getAttr "openvpn-${name}" config.systemd.services).path;
+      path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
 
       upScript = ''
         #! /bin/sh
@@ -63,7 +63,7 @@ let
       wantedBy = optional cfg.autoStart "multi-user.target";
       after = [ "network.target" ];
 
-      path = [ pkgs.iptables pkgs.iproute pkgs.nettools ];
+      path = [ pkgs.iptables pkgs.iproute2 pkgs.nettools ];
 
       serviceConfig.ExecStart = "@${openvpn}/sbin/openvpn openvpn --suppress-timestamps --config ${configFile}";
       serviceConfig.Restart = "always";
diff --git a/nixos/modules/services/networking/owamp.nix b/nixos/modules/services/networking/owamp.nix
index 637ed618b89..baf64347b09 100644
--- a/nixos/modules/services/networking/owamp.nix
+++ b/nixos/modules/services/networking/owamp.nix
@@ -10,7 +10,7 @@ in
   ###### interface
 
   options = {
-    services.owamp.enable = mkEnableOption ''Enable OWAMP server'';
+    services.owamp.enable = mkEnableOption "Enable OWAMP server";
   };
 
 
diff --git a/nixos/modules/services/networking/pdns-recursor.nix b/nixos/modules/services/networking/pdns-recursor.nix
index 6ff181377fc..a326eccfd65 100644
--- a/nixos/modules/services/networking/pdns-recursor.nix
+++ b/nixos/modules/services/networking/pdns-recursor.nix
@@ -3,9 +3,6 @@
 with lib;
 
 let
-  dataDir  = "/var/lib/pdns-recursor";
-  username = "pdns-recursor";
-
   cfg = config.services.pdns-recursor;
 
   oneOrMore  = type: with types; either type (listOf type);
@@ -21,7 +18,7 @@ let
     else if builtins.isList val then (concatMapStringsSep "," serialize val)
     else "";
 
-  configFile = pkgs.writeText "recursor.conf"
+  configDir = pkgs.writeTextDir "recursor.conf"
     (concatStringsSep "\n"
       (flip mapAttrsToList cfg.settings
         (name: val: "${name}=${serialize val}")));
@@ -173,45 +170,30 @@ in {
       serve-rfc1918    = cfg.serveRFC1918;
       lua-config-file  = pkgs.writeText "recursor.lua" cfg.luaConfig;
 
+      daemon         = false;
+      write-pid      = false;
       log-timestamp  = false;
       disable-syslog = true;
     };
 
-    users.users.${username} = {
-      home = dataDir;
-      createHome = true;
-      uid = config.ids.uids.pdns-recursor;
-      description = "PowerDNS Recursor daemon user";
-    };
+    systemd.packages = [ pkgs.pdns-recursor ];
 
     systemd.services.pdns-recursor = {
-      unitConfig.Documentation = "man:pdns_recursor(1) man:rec_control(1)";
-      description = "PowerDNS recursive server";
       wantedBy = [ "multi-user.target" ];
-      after    = [ "network.target" ];
 
       serviceConfig = {
-        User = username;
-        Restart    ="on-failure";
-        RestartSec = "5";
-        PrivateTmp = true;
-        PrivateDevices = true;
-        AmbientCapabilities = "cap_net_bind_service";
-        ExecStart = ''${pkgs.pdns-recursor}/bin/pdns_recursor \
-          --config-dir=${dataDir} \
-          --socket-dir=${dataDir}
-        '';
+        ExecStart = [ "" "${pkgs.pdns-recursor}/bin/pdns_recursor --config-dir=${configDir}" ];
       };
+    };
 
-      preStart = ''
-        # Link configuration file into recursor home directory
-        configPath=${dataDir}/recursor.conf
-        if [ "$(realpath $configPath)" != "${configFile}" ]; then
-          rm -f $configPath
-          ln -s ${configFile} $configPath
-        fi
-      '';
+    users.users.pdns-recursor = {
+      isSystemUser = true;
+      group = "pdns-recursor";
+      description = "PowerDNS Recursor daemon user";
     };
+
+    users.groups.pdns-recursor = {};
+
   };
 
   imports = [
diff --git a/nixos/modules/services/networking/pixiecore.nix b/nixos/modules/services/networking/pixiecore.nix
index 85aa40784af..d2642c82c2d 100644
--- a/nixos/modules/services/networking/pixiecore.nix
+++ b/nixos/modules/services/networking/pixiecore.nix
@@ -93,6 +93,7 @@ in
     users.users.pixiecore = {
       description = "Pixiecore daemon user";
       group = "pixiecore";
+      isSystemUser = true;
     };
 
     networking.firewall = mkIf cfg.openFirewall {
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
new file mode 100644
index 00000000000..bd75083a4a7
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -0,0 +1,141 @@
+{ config, options, lib, pkgs, stdenv, ... }:
+let
+  cfg = config.services.pleroma;
+in {
+  options = {
+    services.pleroma = with lib; {
+      enable = mkEnableOption "pleroma";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.pleroma;
+        description = "Pleroma package to use.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "pleroma";
+        description = "User account under which pleroma runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "pleroma";
+        description = "Group account under which pleroma runs.";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/lib/pleroma";
+        readOnly = true;
+        description = "Directory where the pleroma service will save the uploads and static files.";
+      };
+
+      configs = mkOption {
+        type = with types; listOf str;
+        description = ''
+          Pleroma public configuration.
+
+          This list gets appended from left to
+          right into /etc/pleroma/config.exs. Elixir evaluates its
+          configuration imperatively, meaning you can override a
+          setting by appending a new str to this NixOS option list.
+
+          <emphasis>DO NOT STORE ANY PLEROMA SECRET
+          HERE</emphasis>, use
+          <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>
+          instead.
+
+          This setting is going to be stored in a file part of
+          the Nix store. The Nix store being world-readable, it's not
+          the right place to store any secret
+
+          Have a look to Pleroma section in the NixOS manual for more
+          informations.
+          '';
+      };
+
+      secretConfigFile = mkOption {
+        type = types.str;
+        default = "/var/lib/pleroma/secrets.exs";
+        description = ''
+          Path to the file containing your secret pleroma configuration.
+
+          <emphasis>DO NOT POINT THIS OPTION TO THE NIX
+          STORE</emphasis>, the store being world-readable, it'll
+          compromise all your secrets.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users = {
+      users."${cfg.user}" = {
+        description = "Pleroma user";
+        home = cfg.stateDir;
+        extraGroups = [ cfg.group ];
+        isSystemUser = true;
+      };
+      groups."${cfg.group}" = {};
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc."/pleroma/config.exs".text = ''
+      ${lib.concatMapStrings (x: "${x}") cfg.configs}
+
+      # The lau/tzdata library is trying to download the latest
+      # timezone database in the OTP priv directory by default.
+      # This directory being in the store, it's read-only.
+      # Setting that up to a more appropriate location.
+      config :tzdata, :data_dir, "/var/lib/pleroma/elixir_tzdata_data"
+
+      import_config "${cfg.secretConfigFile}"
+    '';
+
+    systemd.services.pleroma = {
+      description = "Pleroma social network";
+      after = [ "network-online.target" "postgresql.service" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."/pleroma/config.exs".source ];
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        Type = "exec";
+        WorkingDirectory = "~";
+        StateDirectory = "pleroma pleroma/static pleroma/uploads";
+        StateDirectoryMode = "700";
+
+        # Checking the conf file is there then running the database
+        # migration before each service start, just in case there are
+        # some pending ones.
+        #
+        # It's sub-optimal as we'll always run this, even if pleroma
+        # has not been updated. But the no-op process is pretty fast.
+        # Better be safe than sorry migration-wise.
+        ExecStartPre =
+          let preScript = pkgs.writers.writeBashBin "pleromaStartPre"
+            "${cfg.package}/bin/pleroma_ctl migrate";
+          in "${preScript}/bin/pleromaStartPre";
+
+        ExecStart = "${cfg.package}/bin/pleroma start";
+        ExecStop = "${cfg.package}/bin/pleroma stop";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+
+        # Systemd sandboxing directives.
+        # Taken from the upstream contrib systemd service at
+        # pleroma/installation/pleroma.service
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+        PrivateDevices = false;
+        NoNewPrivileges = true;
+        CapabilityBoundingSet = "~CAP_SYS_ADMIN";
+      };
+    };
+
+  };
+  meta.maintainers = with lib.maintainers; [ ninjatrappeur ];
+  meta.doc = ./pleroma.xml;
+}
diff --git a/nixos/modules/services/networking/pleroma.xml b/nixos/modules/services/networking/pleroma.xml
new file mode 100644
index 00000000000..9ab0be3d947
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.xml
@@ -0,0 +1,132 @@
+<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-pleroma">
+ <title>Pleroma</title>
+ <para><link xlink:href="https://pleroma.social/">Pleroma</link> is a lightweight activity pub server.</para>
+ <section xml:id="module-services-pleroma-getting-started">
+   <title>Quick Start</title>
+   <para>To get quickly started, you can use this sample NixOS configuration and adapt it to your use case.</para>
+   <para><programlisting>
+    {
+      security.acme = {
+        email = "root@tld";
+        acceptTerms = true;
+        certs = {
+          "social.tld.com" = {
+            webroot = "/var/www/social.tld.com";
+            email = "root@tld";
+            group = "nginx";
+          };
+        };
+      };
+      services = {
+        pleroma = {
+          enable = true;
+          secretConfigFile = "/var/lib/pleroma/secrets.exs";
+          configs = [
+          ''
+            import Config
+
+            config :pleroma, Pleroma.Web.Endpoint,
+            url: [host: "social.tld.com", scheme: "https", port: 443],
+            http: [ip: {127, 0, 0, 1}, port: 4000]
+
+            config :pleroma, :instance,
+            name: "NixOS test pleroma server",
+            email: "pleroma@social.tld.com",
+            notify_email: "pleroma@social.tld.com",
+            limit: 5000,
+            registrations_open: true
+
+            config :pleroma, :media_proxy,
+            enabled: false,
+            redirect_on_failure: true
+            #base_url: "https://cache.pleroma.social"
+
+            config :pleroma, Pleroma.Repo,
+            adapter: Ecto.Adapters.Postgres,
+            username: "pleroma",
+            password: "${test-db-passwd}",
+            database: "pleroma",
+            hostname: "localhost",
+            pool_size: 10,
+            prepare: :named,
+            parameters: [
+                plan_cache_mode: "force_custom_plan"
+            ]
+
+            config :pleroma, :database, rum_enabled: false
+            config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
+            config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
+            config :pleroma, configurable_from_database: false
+          ''
+          ];
+        };
+        postgresql = {
+          enable = true;
+          package = pkgs.postgresql_12;
+        };
+        nginx = {
+          enable = true;
+          addSSL = true;
+          sslCertificate = "/var/lib/acme/social.tld.com/fullchain.pem";
+          sslCertificateKey = "/var/lib/acme/social.tld.com/key.pem";
+          root = "/var/www/social.tld.com";
+          # ACME endpoint
+          locations."/.well-known/acme-challenge" = {
+              root = "/var/www/social.tld.com/";
+          };
+          virtualHosts."social.tld.com" = {
+            addSSL = true;
+            locations."/" = {
+              proxyPass = "http://127.0.0.1:4000";
+              extraConfig = ''
+                add_header 'Access-Control-Allow-Origin' '*' always;
+                add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
+                add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
+                add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
+                if ($request_method = OPTIONS) {
+                    return 204;
+                }
+                add_header X-XSS-Protection "1; mode=block";
+                add_header X-Permitted-Cross-Domain-Policies none;
+                add_header X-Frame-Options DENY;
+                add_header X-Content-Type-Options nosniff;
+                add_header Referrer-Policy same-origin;
+                add_header X-Download-Options noopen;
+                proxy_http_version 1.1;
+                proxy_set_header Upgrade $http_upgrade;
+                proxy_set_header Connection "upgrade";
+                proxy_set_header Host $host;
+                client_max_body_size 16m;
+              '';
+            };
+          };
+        };
+      };
+    };
+   </programlisting></para>
+   <para>Note that you'll need to seed your database and upload your pleroma secrets to the path pointed by <literal>config.pleroma.secretConfigFile</literal>. You can find more informations about how to do that in the <link linkend="module-services-pleroma-generate-config">next</link> section.</para>
+ </section>
+ <section xml:id="module-services-pleroma-generate-config">
+   <title>Generating the Pleroma Config and Seed the Database</title>
+
+   <para>Before using this service, you'll need to generate your
+server configuration and its associated database seed. The
+<literal>pleroma_ctl</literal> CLI utility can help you with that. You
+can start with <literal>pleroma_ctl instance gen --output config.exs
+--output-psql setup.psql</literal>, this will prompt you some
+questions and will generate both your config file and database initial
+migration. </para>
+<para>For more details about this configuration format, please have a look at the <link xlink:href="https://docs-develop.pleroma.social/backend/configuration/cheatsheet/">upstream documentation</link>.</para>
+<para>To seed your database, you can use the <literal>setup.psql</literal> file you just generated by running
+<programlisting>
+    sudo -u postgres psql -f setup.psql
+</programlisting></para>
+   <para>In regard of the pleroma service configuration you also just generated, you'll need to split it in two parts. The "public" part, which do not contain any secrets and thus can be safely stored in the Nix store and its "private" counterpart containing some secrets (database password, endpoint secret key, salts, etc.).</para>
+
+   <para>The public part will live in your NixOS machine configuration in the <link linkend="opt-services.pleroma.configs">services.pleroma.configs</link> option. However, it's up to you to upload the secret pleroma configuration to the path pointed by <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>. You can do that manually or rely on a third party tool such as <link xlink:href="https://github.com/DBCDK/morph">Morph</link> or <link xlink:href="https://github.com/NixOS/nixops">NixOps</link>.</para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/networking/powerdns.nix b/nixos/modules/services/networking/powerdns.nix
index ba05e15389f..8cae61b8354 100644
--- a/nixos/modules/services/networking/powerdns.nix
+++ b/nixos/modules/services/networking/powerdns.nix
@@ -8,42 +8,40 @@ let
 in {
   options = {
     services.powerdns = {
-      enable = mkEnableOption "Powerdns domain name server";
+      enable = mkEnableOption "PowerDNS domain name server";
 
       extraConfig = mkOption {
         type = types.lines;
         default = "launch=bind";
         description = ''
-          Extra lines to be added verbatim to pdns.conf.
-          Powerdns will chroot to /var/lib/powerdns.
-          So any file, powerdns is supposed to be read,
-          should be in /var/lib/powerdns and needs to specified
-          relative to the chroot.
+          PowerDNS configuration. Refer to
+          <link xlink:href="https://doc.powerdns.com/authoritative/settings.html"/>
+          for details on supported values.
         '';
       };
     };
   };
 
-  config = mkIf config.services.powerdns.enable {
+  config = mkIf cfg.enable {
+
+    systemd.packages = [ pkgs.powerdns ];
+
     systemd.services.pdns = {
-      unitConfig.Documentation = "man:pdns_server(1) man:pdns_control(1)";
-      description = "Powerdns name server";
       wantedBy = [ "multi-user.target" ];
-      after = ["network.target" "mysql.service" "postgresql.service" "openldap.service"];
+      after = [ "network.target" "mysql.service" "postgresql.service" "openldap.service" ];
 
       serviceConfig = {
-        Restart="on-failure";
-        RestartSec="1";
-        StartLimitInterval="0";
-        PrivateDevices=true;
-        CapabilityBoundingSet="CAP_CHOWN CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID CAP_SYS_CHROOT";
-        NoNewPrivileges=true;
-        ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/lib/powerdns";
-        ExecStart = "${pkgs.powerdns}/bin/pdns_server --setuid=nobody --setgid=nogroup --chroot=/var/lib/powerdns --socket-dir=/ --daemon=no --guardian=no --disable-syslog --write-pid=no --config-dir=${configDir}";
-        ProtectSystem="full";
-        ProtectHome=true;
-        RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
+        ExecStart = [ "" "${pkgs.powerdns}/bin/pdns_server --config-dir=${configDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
       };
     };
+
+    users.users.pdns = {
+      isSystemUser = true;
+      group = "pdns";
+      description = "PowerDNS";
+    };
+
+    users.groups.pdns = {};
+
   };
 }
diff --git a/nixos/modules/services/networking/pppd.nix b/nixos/modules/services/networking/pppd.nix
index c1cbdb46176..37f44f07ac4 100644
--- a/nixos/modules/services/networking/pppd.nix
+++ b/nixos/modules/services/networking/pppd.nix
@@ -82,13 +82,21 @@ in
           LD_PRELOAD = "${pkgs.libredirect}/lib/libredirect.so";
           NIX_REDIRECTS = "/var/run=/run/pppd";
         };
-        serviceConfig = {
+        serviceConfig = let
+          capabilities = [
+            "CAP_BPF"
+            "CAP_SYS_TTY_CONFIG"
+            "CAP_NET_ADMIN"
+            "CAP_NET_RAW"
+          ];
+        in
+        {
           ExecStart = "${getBin cfg.package}/sbin/pppd call ${peerCfg.name} nodetach nolog";
           Restart = "always";
           RestartSec = 5;
 
-          AmbientCapabilities = "CAP_SYS_TTY_CONFIG CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_ADMIN";
-          CapabilityBoundingSet = "CAP_SYS_TTY_CONFIG CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_ADMIN";
+          AmbientCapabilities = capabilities;
+          CapabilityBoundingSet = capabilities;
           KeyringMode = "private";
           LockPersonality = true;
           MemoryDenyWriteExecute = true;
@@ -103,7 +111,17 @@ in
           ProtectKernelTunables = false;
           ProtectSystem = "strict";
           RemoveIPC = true;
-          RestrictAddressFamilies = "AF_PACKET AF_UNIX AF_PPPOX AF_ATMPVC AF_ATMSVC AF_INET AF_INET6 AF_IPX";
+          RestrictAddressFamilies = [
+            "AF_ATMPVC"
+            "AF_ATMSVC"
+            "AF_INET"
+            "AF_INET6"
+            "AF_IPX"
+            "AF_NETLINK"
+            "AF_PACKET"
+            "AF_PPPOX"
+            "AF_UNIX"
+          ];
           RestrictNamespaces = true;
           RestrictRealtime = true;
           RestrictSUIDSGID = true;
diff --git a/nixos/modules/services/networking/prayer.nix b/nixos/modules/services/networking/prayer.nix
index f04dac01d9b..ae9258b2712 100644
--- a/nixos/modules/services/networking/prayer.nix
+++ b/nixos/modules/services/networking/prayer.nix
@@ -44,7 +44,8 @@ in
       enable = mkEnableOption "the prayer webmail http server";
 
       port = mkOption {
-        default = "2080";
+        default = 2080;
+        type = types.port;
         description = ''
           Port the prayer http server is listening to.
         '';
diff --git a/nixos/modules/services/networking/privoxy.nix b/nixos/modules/services/networking/privoxy.nix
index 1f41c720adf..df818baa465 100644
--- a/nixos/modules/services/networking/privoxy.nix
+++ b/nixos/modules/services/networking/privoxy.nix
@@ -4,19 +4,46 @@ with lib;
 
 let
 
-  inherit (pkgs) privoxy;
-
   cfg = config.services.privoxy;
 
-  confFile = pkgs.writeText "privoxy.conf" ''
-    user-manual ${privoxy}/share/doc/privoxy/user-manual
-    confdir ${privoxy}/etc/
-    listen-address  ${cfg.listenAddress}
-    enable-edit-actions ${if (cfg.enableEditActions == true) then "1" else "0"}
-    ${concatMapStrings (f: "actionsfile ${f}\n") cfg.actionsFiles}
-    ${concatMapStrings (f: "filterfile ${f}\n") cfg.filterFiles}
-    ${cfg.extraConfig}
-  '';
+  serialise = name: val:
+         if isList val then concatMapStrings (serialise name) val
+    else if isBool val then serialise name (if val then "1" else "0")
+    else "${name} ${toString val}\n";
+
+  configType = with types;
+    let atom = oneOf [ int bool string path ];
+    in attrsOf (either atom (listOf atom))
+    // { description = ''
+          privoxy configuration type. The format consists of an attribute
+          set of settings. Each setting can be either a value (integer, string,
+          boolean or path) or a list of such values.
+        '';
+       };
+
+  ageType = types.str // {
+    check = x:
+      isString x &&
+      (builtins.match "([0-9]+([smhdw]|min|ms|us)*)+" x != null);
+    description = "tmpfiles.d(5) age format";
+  };
+
+  configFile = pkgs.writeText "privoxy.conf"
+    (concatStrings (
+      # Relative paths in some options are relative to confdir. Privoxy seems
+      # to parse the options in order of appearance, so this must come first.
+      # Nix however doesn't preserve the order in attrsets, so we have to
+      # hardcode confdir here.
+      [ "confdir ${pkgs.privoxy}/etc\n" ]
+      ++ mapAttrsToList serialise cfg.settings
+    ));
+
+  inspectAction = pkgs.writeText "inspect-all-https.action"
+    ''
+      # Enable HTTPS inspection for all requests
+      {+https-inspection}
+      /
+    '';
 
 in
 
@@ -24,61 +51,144 @@ in
 
   ###### interface
 
-  options = {
+  options.services.privoxy = {
 
-    services.privoxy = {
+    enable = mkEnableOption "Privoxy, non-caching filtering proxy";
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable the Privoxy non-caching filtering proxy.
-        '';
-      };
+    enableTor = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to configure Privoxy to use Tor's faster SOCKS port,
+        suitable for HTTP.
+      '';
+    };
 
-      listenAddress = mkOption {
-        type = types.str;
-        default = "127.0.0.1:8118";
-        description = ''
-          Address the proxy server is listening to.
-        '';
-      };
+    inspectHttps = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to configure Privoxy to inspect HTTPS requests, meaning all
+        encrypted traffic will be filtered as well. This works by decrypting
+        and re-encrypting the requests using a per-domain generated certificate.
 
-      actionsFiles = mkOption {
-        type = types.listOf types.str;
-        example = [ "match-all.action" "default.action" "/etc/privoxy/user.action" ];
-        default = [ "match-all.action" "default.action" ];
-        description = ''
-          List of paths to Privoxy action files.
-          These paths may either be absolute or relative to the privoxy configuration directory.
-        '';
-      };
+        To issue per-domain certificates, Privoxy must be provided with a CA
+        certificate, using the <literal>ca-cert-file</literal>,
+        <literal>ca-key-file</literal> settings.
 
-      filterFiles = mkOption {
-        type = types.listOf types.str;
-        example = [ "default.filter" "/etc/privoxy/user.filter" ];
-        default = [ "default.filter" ];
-        description = ''
-          List of paths to Privoxy filter files.
-          These paths may either be absolute or relative to the privoxy configuration directory.
-        '';
-      };
+        <warning><para>
+          The CA certificate must also be added to the system trust roots,
+          otherwise browsers will reject all Privoxy certificates as invalid.
+          You can do so by using the option
+          <option>security.pki.certificateFiles</option>.
+        </para></warning>
+      '';
+    };
 
-      enableEditActions = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether or not the web-based actions file editor may be used.
-        '';
-      };
+    certsLifetime = mkOption {
+      type = ageType;
+      default = "10d";
+      example = "12h";
+      description = ''
+        If <literal>inspectHttps</literal> is enabled, the time generated HTTPS
+        certificates will be stored in a temporary directory for reuse. Once
+        the lifetime has expired the directory will cleared and the certificate
+        will have to be generated again, on-demand.
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "" ;
-        description = ''
-          Extra configuration. Contents will be added verbatim to the configuration file.
-        '';
+        Depending on the traffic, you may want to reduce the lifetime to limit
+        the disk usage, since Privoxy itself never deletes the certificates.
+
+        <note><para>The format is that of the <literal>tmpfiles.d(5)</literal>
+        Age parameter.</para></note>
+      '';
+    };
+
+    userActions = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Actions to be included in a <literal>user.action</literal> file. This
+        will have a higher priority and can be used to override all other
+        actions.
+      '';
+    };
+
+    userFilters = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Filters to be included in a <literal>user.filter</literal> file. This
+        will have a higher priority and can be used to override all other
+        filters definitions.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = configType;
+
+        options.listen-address = mkOption {
+          type = types.str;
+          default = "127.0.0.1:8118";
+          description = "Pair of address:port the proxy server is listening to.";
+        };
+
+        options.enable-edit-actions = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Whether the web-based actions file editor may be used.";
+        };
+
+        options.actionsfile = mkOption {
+          type = types.listOf types.str;
+          # This must come after all other entries, in order to override the
+          # other actions/filters installed by Privoxy or the user.
+          apply = x: x ++ optional (cfg.userActions != "")
+            (toString (pkgs.writeText "user.actions" cfg.userActions));
+          default = [ "match-all.action" "default.action" ];
+          description = ''
+            List of paths to Privoxy action files. These paths may either be
+            absolute or relative to the privoxy configuration directory.
+          '';
+        };
+
+        options.filterfile = mkOption {
+          type = types.listOf types.str;
+          default = [ "default.filter" ];
+          apply = x: x ++ optional (cfg.userFilters != "")
+            (toString (pkgs.writeText "user.filter" cfg.userFilters));
+          description = ''
+            List of paths to Privoxy filter files. These paths may either be
+            absolute or relative to the privoxy configuration directory.
+          '';
+        };
       };
+      default = {};
+      example = literalExample ''
+        { # Listen on IPv6 only
+          listen-address = "[::]:8118";
+
+          # Forward .onion requests to Tor
+          forward-socks5 = ".onion localhost:9050 .";
+
+          # Log redirects and filters
+          debug = [ 128 64 ];
+          # This is equivalent to writing these lines
+          # in the Privoxy configuration file:
+          # debug 128
+          # debug 64
+        }
+      '';
+      description = ''
+        This option is mapped to the main Privoxy configuration file.
+        Check out the Privoxy user manual at
+        <link xlink:href="https://www.privoxy.org/user-manual/config.html"/>
+        for available settings and documentation.
+
+        <note><para>
+          Repeated settings can be represented by using a list.
+        </para></note>
+      '';
     };
 
   };
@@ -88,27 +198,82 @@ in
   config = mkIf cfg.enable {
 
     users.users.privoxy = {
+      description = "Privoxy daemon user";
       isSystemUser = true;
-      home = "/var/empty";
       group = "privoxy";
     };
 
     users.groups.privoxy = {};
 
+    systemd.tmpfiles.rules = optional cfg.inspectHttps
+      "d ${cfg.settings.certificate-directory} 0770 privoxy privoxy ${cfg.certsLifetime}";
+
     systemd.services.privoxy = {
       description = "Filtering web proxy";
       after = [ "network.target" "nss-lookup.target" ];
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${privoxy}/bin/privoxy --no-daemon --user privoxy ${confFile}";
-
-      serviceConfig.PrivateDevices = true;
-      serviceConfig.PrivateTmp = true;
-      serviceConfig.ProtectHome = true;
-      serviceConfig.ProtectSystem = "full";
+      serviceConfig = {
+        User = "privoxy";
+        Group = "privoxy";
+        ExecStart = "${pkgs.privoxy}/bin/privoxy --no-daemon ${configFile}";
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectSystem = "full";
+      };
+      unitConfig =  mkIf cfg.inspectHttps {
+        ConditionPathExists = with cfg.settings;
+          [ ca-cert-file ca-key-file ];
+      };
     };
 
+    services.tor.settings.SOCKSPort = mkIf cfg.enableTor [
+      # Route HTTP traffic over a faster port (without IsolateDestAddr).
+      { addr = "127.0.0.1"; port = 9063; IsolateDestAddr = false; }
+    ];
+
+    services.privoxy.settings = {
+      user-manual = "${pkgs.privoxy}/share/doc/privoxy/user-manual";
+      # This is needed for external filters
+      temporary-directory = "/tmp";
+      filterfile = [ "default.filter" ];
+      actionsfile =
+        [ "match-all.action"
+          "default.action"
+        ] ++ optional cfg.inspectHttps (toString inspectAction);
+    } // (optionalAttrs cfg.enableTor {
+      forward-socks5 = "/ 127.0.0.1:9063 .";
+      toggle = true;
+      enable-remote-toggle = false;
+      enable-edit-actions = false;
+      enable-remote-http-toggle = false;
+    }) // (optionalAttrs cfg.inspectHttps {
+      # This allows setting absolute key/crt paths
+      ca-directory = "/var/empty";
+      certificate-directory = "/run/privoxy/certs";
+      trusted-cas-file = "/etc/ssl/certs/ca-certificates.crt";
+    });
+
   };
 
+  imports =
+    let
+      top = x: [ "services" "privoxy" x ];
+      setting = x: [ "services" "privoxy" "settings" x ];
+    in
+    [ (mkRenamedOptionModule (top "enableEditActions") (setting "enable-edit-actions"))
+      (mkRenamedOptionModule (top "listenAddress") (setting "listen-address"))
+      (mkRenamedOptionModule (top "actionsFiles") (setting "actionsfile"))
+      (mkRenamedOptionModule (top "filterFiles") (setting "filterfile"))
+      (mkRemovedOptionModule (top "extraConfig")
+      ''
+        Use services.privoxy.settings instead.
+        This is part of the general move to use structured settings instead of raw
+        text for config as introduced by RFC0042:
+        https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
+      '')
+    ];
+
   meta.maintainers = with lib.maintainers; [ rnhmjoj ];
 
 }
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index e53d7093be8..e7a7aa700be 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -261,7 +261,7 @@ let
 
   toLua = x:
     if builtins.isString x then ''"${x}"''
-    else if builtins.isBool x then (if x == true then "true" else "false")
+    else if builtins.isBool x then boolToString x
     else if builtins.isInt x then toString x
     else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
     else throw "Invalid Lua value";
@@ -655,7 +655,7 @@ in
 
         description = "Define the virtual hosts";
 
-        type = with types; loaOf (submodule vHostOpts);
+        type = with types; attrsOf (submodule vHostOpts);
 
         example = {
           myhost = {
diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml
index 7859cb1578b..471240cd147 100644
--- a/nixos/modules/services/networking/prosody.xml
+++ b/nixos/modules/services/networking/prosody.xml
@@ -43,10 +43,10 @@ services.prosody = {
   <link linkend="opt-services.prosody.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
   <link linkend="opt-services.prosody.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
   <link linkend="opt-services.prosody.virtualHosts">virtualHosts</link>."example.org" = {
-      <link linkend="opt-services.prosody.virtualHosts._name__.enabled">enabled</link> = true;
-      <link linkend="opt-services.prosody.virtualHosts._name__.domain">domain</link> = "example.org";
-      <link linkend="opt-services.prosody.virtualHosts._name__.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
-      <link linkend="opt-services.prosody.virtualHosts._name__.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
+      <link linkend="opt-services.prosody.virtualHosts._name_.enabled">enabled</link> = true;
+      <link linkend="opt-services.prosody.virtualHosts._name_.domain">domain</link> = "example.org";
+      <link linkend="opt-services.prosody.virtualHosts._name_.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
+      <link linkend="opt-services.prosody.virtualHosts._name_.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
   };
   <link linkend="opt-services.prosody.muc">muc</link> = [ {
       <link linkend="opt-services.prosody.muc">domain</link> = "conference.example.org";
@@ -65,7 +65,7 @@ services.prosody = {
    you'll need a single TLS certificate covering your main endpoint,
    the MUC one as well as the HTTP Upload one. We can generate such a
    certificate by leveraging the ACME
-   <link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains</link> module option.
+   <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> module option.
  </para>
  <para>
    Provided the setup detailed in the previous section, you'll need the following acme configuration to generate
@@ -78,8 +78,7 @@ security.acme = {
     "example.org" = {
       <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/www/example.org";
       <link linkend="opt-security.acme.certs._name_.email">email</link> = "root@example.org";
-      <link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains."conference.example.org"</link> = null;
-      <link linkend="opt-security.acme.certs._name_.extraDomains">extraDomains."upload.example.org"</link> = null;
+      <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "conference.example.org" "upload.example.org" ];
     };
   };
 };</programlisting>
diff --git a/nixos/modules/services/networking/quagga.nix b/nixos/modules/services/networking/quagga.nix
deleted file mode 100644
index 5acdd5af8f8..00000000000
--- a/nixos/modules/services/networking/quagga.nix
+++ /dev/null
@@ -1,185 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.quagga;
-
-  services = [ "babel" "bgp" "isis" "ospf6" "ospf" "pim" "rip" "ripng" ];
-  allServices = services ++ [ "zebra" ];
-
-  isEnabled = service: cfg.${service}.enable;
-
-  daemonName = service: if service == "zebra" then service else "${service}d";
-
-  configFile = service:
-    let
-      scfg = cfg.${service};
-    in
-      if scfg.configFile != null then scfg.configFile
-      else pkgs.writeText "${daemonName service}.conf"
-        ''
-          ! Quagga ${daemonName service} configuration
-          !
-          hostname ${config.networking.hostName}
-          log syslog
-          service password-encryption
-          !
-          ${scfg.config}
-          !
-          end
-        '';
-
-  serviceOptions = service:
-    {
-      enable = mkEnableOption "the Quagga ${toUpper service} routing protocol";
-
-      configFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        example = "/etc/quagga/${daemonName service}.conf";
-        description = ''
-          Configuration file to use for Quagga ${daemonName service}.
-          By default the NixOS generated files are used.
-        '';
-      };
-
-      config = mkOption {
-        type = types.lines;
-        default = "";
-        example =
-          let
-            examples = {
-              rip = ''
-                router rip
-                  network 10.0.0.0/8
-              '';
-
-              ospf = ''
-                router ospf
-                  network 10.0.0.0/8 area 0
-              '';
-
-              bgp = ''
-                router bgp 65001
-                  neighbor 10.0.0.1 remote-as 65001
-              '';
-            };
-          in
-            examples.${service} or "";
-        description = ''
-          ${daemonName service} configuration statements.
-        '';
-      };
-
-      vtyListenAddress = mkOption {
-        type = types.str;
-        default = "127.0.0.1";
-        description = ''
-          Address to bind to for the VTY interface.
-        '';
-      };
-
-      vtyListenPort = mkOption {
-        type = types.nullOr types.int;
-        default = null;
-        description = ''
-          TCP Port to bind to for the VTY interface.
-        '';
-      };
-    };
-
-in
-
-{
-
-  ###### interface
-  imports = [
-    {
-      options.services.quagga = {
-        zebra = (serviceOptions "zebra") // {
-          enable = mkOption {
-            type = types.bool;
-            default = any isEnabled services;
-            description = ''
-              Whether to enable the Zebra routing manager.
-
-              The Zebra routing manager is automatically enabled
-              if any routing protocols are configured.
-            '';
-          };
-        };
-      };
-    }
-    { options.services.quagga = (genAttrs services serviceOptions); }
-  ];
-
-  ###### implementation
-
-  config = mkIf (any isEnabled allServices) {
-
-    environment.systemPackages = [
-      pkgs.quagga               # for the vtysh tool
-    ];
-
-    users.users.quagga = {
-      description = "Quagga daemon user";
-      isSystemUser = true;
-      group = "quagga";
-    };
-
-    users.groups = {
-      quagga = {};
-      # Members of the quaggavty group can use vtysh to inspect the Quagga daemons
-      quaggavty = { members = [ "quagga" ]; };
-    };
-
-    systemd.services =
-      let
-        quaggaService = service:
-          let
-            scfg = cfg.${service};
-            daemon = daemonName service;
-          in
-            nameValuePair daemon ({
-              wantedBy = [ "multi-user.target" ];
-              restartTriggers = [ (configFile service) ];
-
-              serviceConfig = {
-                Type = "forking";
-                PIDFile = "/run/quagga/${daemon}.pid";
-                ExecStart = "@${pkgs.quagga}/libexec/quagga/${daemon} ${daemon} -d -f ${configFile service}"
-                  + optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}"
-                  + optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}";
-                ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-                Restart = "on-abort";
-              };
-            } // (
-              if service == "zebra" then
-                {
-                  description = "Quagga Zebra routing manager";
-                  unitConfig.Documentation = "man:zebra(8)";
-                  after = [ "network.target" ];
-                  preStart = ''
-                    install -m 0755 -o quagga -g quagga -d /run/quagga
-
-                    ${pkgs.iproute}/bin/ip route flush proto zebra
-                  '';
-                }
-              else
-                {
-                  description = "Quagga ${toUpper service} routing daemon";
-                  unitConfig.Documentation = "man:${daemon}(8) man:zebra(8)";
-                  bindsTo = [ "zebra.service" ];
-                  after = [ "network.target" "zebra.service" ];
-                }
-            ));
-       in
-         listToAttrs (map quaggaService (filter isEnabled allServices));
-
-  };
-
-  meta.maintainers = with lib.maintainers; [ tavyc ];
-
-}
diff --git a/nixos/modules/services/networking/quassel.nix b/nixos/modules/services/networking/quassel.nix
index da723ec86ad..bfbd3b46ab4 100644
--- a/nixos/modules/services/networking/quassel.nix
+++ b/nixos/modules/services/networking/quassel.nix
@@ -45,6 +45,7 @@ in
       };
 
       interfaces = mkOption {
+        type = types.listOf types.str;
         default = [ "127.0.0.1" ];
         description = ''
           The interfaces the Quassel daemon will be listening to.  If `[ 127.0.0.1 ]',
@@ -54,6 +55,7 @@ in
       };
 
       portNumber = mkOption {
+        type = types.port;
         default = 4242;
         description = ''
           The port number the Quassel daemon will be listening to.
@@ -61,7 +63,8 @@ in
       };
 
       dataDir = mkOption {
-        default = ''/home/${user}/.config/quassel-irc.org'';
+        default = "/home/${user}/.config/quassel-irc.org";
+        type = types.str;
         description = ''
           The directory holding configuration files, the SQlite database and the SSL Cert.
         '';
@@ -69,6 +72,7 @@ in
 
       user = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           The existing user the Quassel daemon should run as. If left empty, a default "quassel" user will be created.
         '';
diff --git a/nixos/modules/services/networking/quicktun.nix b/nixos/modules/services/networking/quicktun.nix
index fb783c83646..438e67d5ebb 100644
--- a/nixos/modules/services/networking/quicktun.nix
+++ b/nixos/modules/services/networking/quicktun.nix
@@ -87,7 +87,7 @@ with lib;
   };
 
   config = mkIf (cfg != []) {
-    systemd.services = fold (a: b: a // b) {} (
+    systemd.services = foldr (a: b: a // b) {} (
       mapAttrsToList (name: qtcfg: {
         "quicktun-${name}" = {
           wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 5af035fd59e..8c632c319d3 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -3,56 +3,103 @@
 with lib;
 
 let
-
   cfg = config.services.radicale;
 
-  confFile = pkgs.writeText "radicale.conf" cfg.config;
-
-  defaultPackage = if versionAtLeast config.system.stateVersion "20.09" then {
-    pkg = pkgs.radicale3;
-    text = "pkgs.radicale3";
-  } else if versionAtLeast config.system.stateVersion "17.09" then {
-    pkg = pkgs.radicale2;
-    text = "pkgs.radicale2";
-  } else {
-    pkg = pkgs.radicale1;
-    text = "pkgs.radicale1";
+  format = pkgs.formats.ini {
+    listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
   };
-in
 
-{
+  pkg = if isNull cfg.package then
+    pkgs.radicale
+  else
+    cfg.package;
+
+  confFile = if cfg.settings == { } then
+    pkgs.writeText "radicale.conf" cfg.config
+  else
+    format.generate "radicale.conf" cfg.settings;
+
+  rightsFile = format.generate "radicale.rights" cfg.rights;
 
-  options = {
-    services.radicale.enable = mkOption {
-      type = types.bool;
-      default = false;
+  bindLocalhost = cfg.settings != { } && !hasAttrByPath [ "server" "hosts" ] cfg.settings;
+
+in {
+  options.services.radicale = {
+    enable = mkEnableOption "Radicale CalDAV and CardDAV server";
+
+    package = mkOption {
+      description = "Radicale package to use.";
+      # Default cannot be pkgs.radicale because non-null values suppress
+      # warnings about incompatible configuration and storage formats.
+      type = with types; nullOr package // { inherit (package) description; };
+      default = null;
+      defaultText = "pkgs.radicale";
+    };
+
+    config = mkOption {
+      type = types.str;
+      default = "";
       description = ''
-          Enable Radicale CalDAV and CardDAV server.
+        Radicale configuration, this will set the service
+        configuration file.
+        This option is mutually exclusive with <option>settings</option>.
+        This option is deprecated.  Use <option>settings</option> instead.
       '';
     };
 
-    services.radicale.package = mkOption {
-      type = types.package;
-      default = defaultPackage.pkg;
-      defaultText = defaultPackage.text;
+    settings = mkOption {
+      type = format.type;
+      default = { };
       description = ''
-        Radicale package to use. This defaults to version 1.x if
-        <literal>system.stateVersion &lt; 17.09</literal>, version 2.x if
-        <literal>17.09 ≤ system.stateVersion &lt; 20.09</literal>, and
-        version 3.x otherwise.
+        Configuration for Radicale. See
+        <link xlink:href="https://radicale.org/3.0.html#documentation/configuration" />.
+        This option is mutually exclusive with <option>config</option>.
+      '';
+      example = literalExample ''
+        server = {
+          hosts = [ "0.0.0.0:5232" "[::]:5232" ];
+        };
+        auth = {
+          type = "htpasswd";
+          htpasswd_filename = "/etc/radicale/users";
+          htpasswd_encryption = "bcrypt";
+        };
+        storage = {
+          filesystem_folder = "/var/lib/radicale/collections";
+        };
       '';
     };
 
-    services.radicale.config = mkOption {
-      type = types.str;
-      default = "";
+    rights = mkOption {
+      type = format.type;
       description = ''
-        Radicale configuration, this will set the service
-        configuration file.
+        Configuration for Radicale's rights file. See
+        <link xlink:href="https://radicale.org/3.0.html#documentation/authentication-and-rights" />.
+        This option only works in conjunction with <option>settings</option>.
+        Setting this will also set <option>settings.rights.type</option> and
+        <option>settings.rights.file</option> to approriate values.
+      '';
+      default = { };
+      example = literalExample ''
+        root = {
+          user = ".+";
+          collection = "";
+          permissions = "R";
+        };
+        principal = {
+          user = ".+";
+          collection = "{user}";
+          permissions = "RW";
+        };
+        calendars = {
+          user = ".+";
+          collection = "{user}/[^/]+";
+          permissions = "rw";
+        };
       '';
     };
 
-    services.radicale.extraArgs = mkOption {
+    extraArgs = mkOption {
       type = types.listOf types.str;
       default = [];
       description = "Extra arguments passed to the Radicale daemon.";
@@ -60,33 +107,94 @@ in
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ cfg.package ];
+    assertions = [
+      {
+        assertion = cfg.settings == { } || cfg.config == "";
+        message = ''
+          The options services.radicale.config and services.radicale.settings
+          are mutually exclusive.
+        '';
+      }
+    ];
 
-    users.users.radicale =
-      { uid = config.ids.uids.radicale;
-        description = "radicale user";
-        home = "/var/lib/radicale";
-        createHome = true;
-      };
+    warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
+      The configuration and storage formats of your existing Radicale
+      installation might be incompatible with the newest version.
+      For upgrade instructions see
+      https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
+      Set services.radicale.package to suppress this warning.
+    '' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
+      The configuration format of your existing Radicale installation might be
+      incompatible with the newest version.  For upgrade instructions see
+      https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
+      Set services.radicale.package to suppress this warning.
+    '' ++ optional (cfg.config != "") ''
+      The option services.radicale.config is deprecated.
+      Use services.radicale.settings instead.
+    '';
+
+    services.radicale.settings.rights = mkIf (cfg.rights != { }) {
+      type = "from_file";
+      file = toString rightsFile;
+    };
+
+    environment.systemPackages = [ pkg ];
+
+    users.users.radicale.uid = config.ids.uids.radicale;
 
-    users.groups.radicale =
-      { gid = config.ids.gids.radicale; };
+    users.groups.radicale.gid = config.ids.gids.radicale;
 
     systemd.services.radicale = {
       description = "A Simple Calendar and Contact Server";
       after = [ "network.target" ];
+      requires = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         ExecStart = concatStringsSep " " ([
-          "${cfg.package}/bin/radicale" "-C" confFile
+          "${pkg}/bin/radicale" "-C" confFile
         ] ++ (
           map escapeShellArg cfg.extraArgs
         ));
         User = "radicale";
         Group = "radicale";
+        StateDirectory = "radicale/collections";
+        StateDirectoryMode = "0750";
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DeviceAllow = [ "/dev/stdin" ];
+        DevicePolicy = "strict";
+        IPAddressAllow = mkIf bindLocalhost "localhost";
+        IPAddressDeny = mkIf bindLocalhost "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ReadWritePaths = lib.optional
+          (hasAttrByPath [ "storage" "filesystem_folder" ] cfg.settings)
+          cfg.settings.storage.filesystem_folder;
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0027";
       };
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ];
+  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
 }
diff --git a/nixos/modules/services/networking/radvd.nix b/nixos/modules/services/networking/radvd.nix
index f4b00c9b356..53fac4b7b72 100644
--- a/nixos/modules/services/networking/radvd.nix
+++ b/nixos/modules/services/networking/radvd.nix
@@ -33,6 +33,7 @@ in
     };
 
     services.radvd.config = mkOption {
+      type = types.lines;
       example =
         ''
           interface eth0 {
diff --git a/nixos/modules/services/networking/resilio.nix b/nixos/modules/services/networking/resilio.nix
index 6193d7340fc..4701b0e8143 100644
--- a/nixos/modules/services/networking/resilio.nix
+++ b/nixos/modules/services/networking/resilio.nix
@@ -183,6 +183,7 @@ in
 
       sharedFolders = mkOption {
         default = [];
+        type = types.listOf (types.attrsOf types.anything);
         example =
           [ { secret         = "AHMYFPCQAHBM7LQPFXQ7WV6Y42IGUXJ5Y";
               directory      = "/home/user/sync_test";
diff --git a/nixos/modules/services/networking/robustirc-bridge.nix b/nixos/modules/services/networking/robustirc-bridge.nix
new file mode 100644
index 00000000000..255af79ec04
--- /dev/null
+++ b/nixos/modules/services/networking/robustirc-bridge.nix
@@ -0,0 +1,47 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.robustirc-bridge;
+in
+{
+  options = {
+    services.robustirc-bridge = {
+      enable = mkEnableOption "RobustIRC bridge";
+
+      extraFlags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''Extra flags passed to the <command>robustirc-bridge</command> command. See <link xlink:href="https://robustirc.net/docs/adminguide.html#_bridge">RobustIRC Documentation</link> or robustirc-bridge(1) for details.'';
+        example = [
+          "-network robustirc.net"
+        ];
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.robustirc-bridge = {
+      description = "RobustIRC bridge";
+      documentation = [
+        "man:robustirc-bridge(1)"
+        "https://robustirc.net/"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        ExecStart = "${pkgs.robustirc-bridge}/bin/robustirc-bridge ${concatStringsSep " " cfg.extraFlags}";
+        Restart = "on-failure";
+
+        # Hardening
+        PrivateDevices = true;
+        ProtectSystem = true;
+        ProtectHome = true;
+        PrivateTmp = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/rxe.nix b/nixos/modules/services/networking/rxe.nix
index c7d174a00de..868e2c81ccb 100644
--- a/nixos/modules/services/networking/rxe.nix
+++ b/nixos/modules/services/networking/rxe.nix
@@ -39,11 +39,11 @@ in {
         Type = "oneshot";
         RemainAfterExit = true;
         ExecStart = map ( x:
-          "${pkgs.iproute}/bin/rdma link add rxe_${x} type rxe netdev ${x}"
+          "${pkgs.iproute2}/bin/rdma link add rxe_${x} type rxe netdev ${x}"
           ) cfg.interfaces;
 
         ExecStop = map ( x:
-          "${pkgs.iproute}/bin/rdma link delete rxe_${x}"
+          "${pkgs.iproute2}/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 ff5aef7d1cb..43566dfd25c 100644
--- a/nixos/modules/services/networking/sabnzbd.nix
+++ b/nixos/modules/services/networking/sabnzbd.nix
@@ -18,16 +18,19 @@ in
       enable = mkEnableOption "the sabnzbd server";
 
       configFile = mkOption {
+        type = types.path;
         default = "/var/lib/sabnzbd/sabnzbd.ini";
         description = "Path to config file.";
       };
 
       user = mkOption {
         default = "sabnzbd";
+        type = types.str;
         description = "User to run the service as";
       };
 
       group = mkOption {
+        type = types.str;
         default = "sabnzbd";
         description = "Group to run the service as";
       };
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index 60fb3d5d6d4..04f7d7e31f4 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -1,34 +1,116 @@
-{ config, lib, pkgs, ... }:
+{ options, config, lib, pkgs, ... }:
 
 with lib;
 
 let
+  runDir = "/run/searx";
 
   cfg = config.services.searx;
 
-  configFile = cfg.configFile;
+  settingsFile = pkgs.writeText "settings.yml"
+    (builtins.toJSON cfg.settings);
+
+  generateConfig = ''
+    cd ${runDir}
+
+    # write NixOS settings as JSON
+    (
+      umask 077
+      cp --no-preserve=mode ${settingsFile} settings.yml
+    )
+
+    # substitute environment variables
+    env -0 | while IFS='=' read -r -d ''' n v; do
+      sed "s#@$n@#$v#g" -i settings.yml
+    done
+  '';
+
+  settingType = with types; (oneOf
+    [ bool int float str
+      (listOf settingType)
+      (attrsOf settingType)
+    ]) // { description = "JSON value"; };
 
 in
 
 {
 
+  imports = [
+    (mkRenamedOptionModule
+      [ "services" "searx" "configFile" ]
+      [ "services" "searx" "settingsFile" ])
+  ];
+
   ###### interface
 
   options = {
 
     services.searx = {
 
-      enable = mkEnableOption
-        "the searx server. See https://github.com/asciimoo/searx";
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        relatedPackages = [ "searx" ];
+        description = "Whether to enable Searx, the meta search engine.";
+      };
 
-      configFile = mkOption {
+      environmentFile = mkOption {
         type = types.nullOr types.path;
         default = null;
-        description = "
-          The path of the Searx server configuration file. If no file
-          is specified, a default file is used (default config file has
-          debug mode enabled).
-        ";
+        description = ''
+          Environment file (see <literal>systemd.exec(5)</literal>
+          "EnvironmentFile=" section for the syntax) to define variables for
+          Searx. This option can be used to safely include secret keys into the
+          Searx configuration.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.attrsOf settingType;
+        default = { };
+        example = literalExample ''
+          { server.port = 8080;
+            server.bind_address = "0.0.0.0";
+            server.secret_key = "@SEARX_SECRET_KEY@";
+
+            engines = lib.singleton
+              { name = "wolframalpha";
+                shortcut = "wa";
+                api_key = "@WOLFRAM_API_KEY@";
+                engine = "wolframalpha_api";
+              };
+          }
+        '';
+        description = ''
+          Searx settings. These will be merged with (taking precedence over)
+          the default configuration. It's also possible to refer to
+          environment variables
+          (defined in <xref linkend="opt-services.searx.environmentFile"/>)
+          using the syntax <literal>@VARIABLE_NAME@</literal>.
+          <note>
+            <para>
+              For available settings, see the Searx
+              <link xlink:href="https://searx.github.io/searx/admin/settings.html">docs</link>.
+            </para>
+          </note>
+        '';
+      };
+
+      settingsFile = mkOption {
+        type = types.path;
+        default = "${runDir}/settings.yml";
+        description = ''
+          The path of the Searx server settings.yml file. If no file is
+          specified, a default file is used (default config file has debug mode
+          enabled). Note: setting this options overrides
+          <xref linkend="opt-services.searx.settings"/>.
+          <warning>
+            <para>
+              This file, along with any secret key it contains, will be copied
+              into the world-readable Nix store.
+            </para>
+          </warning>
+        '';
       };
 
       package = mkOption {
@@ -38,6 +120,38 @@ in
         description = "searx package to use.";
       };
 
+      runInUwsgi = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run searx in uWSGI as a "vassal", instead of using its
+          built-in HTTP server. This is the recommended mode for public or
+          large instances, but is unecessary for LAN or local-only use.
+          <warning>
+            <para>
+              The built-in HTTP server logs all queries by default.
+            </para>
+          </warning>
+        '';
+      };
+
+      uwsgiConfig = mkOption {
+        type = options.services.uwsgi.instance.type;
+        default = { http = ":8080"; };
+        example = literalExample ''
+          {
+            disable-logging = true;
+            http = ":8080";                   # serve via HTTP...
+            socket = "/run/searx/searx.sock"; # ...or UNIX socket
+          }
+        '';
+        description = ''
+          Additional configuration of the uWSGI vassal running searx. It
+          should notably specify on which interfaces and ports the vassal
+          should listen.
+        '';
+      };
+
     };
 
   };
@@ -45,36 +159,74 @@ in
 
   ###### implementation
 
-  config = mkIf config.services.searx.enable {
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
 
     users.users.searx =
-      { uid = config.ids.uids.searx;
-        description = "Searx user";
-        createHome = true;
-        home = "/var/lib/searx";
+      { description = "Searx daemon user";
+        group = "searx";
+        isSystemUser = true;
       };
 
-    users.groups.searx =
-      { gid = config.ids.gids.searx;
+    users.groups.searx = { };
+
+    systemd.services.searx-init = {
+      description = "Initialise Searx settings";
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = "searx";
+        RuntimeDirectory = "searx";
+        RuntimeDirectoryMode = "750";
+      } // optionalAttrs (cfg.environmentFile != null)
+        { EnvironmentFile = builtins.toPath cfg.environmentFile; };
+      script = generateConfig;
+    };
+
+    systemd.services.searx = mkIf (!cfg.runInUwsgi) {
+      description = "Searx server, the meta search engine.";
+      wantedBy = [ "network.target" "multi-user.target" ];
+      requires = [ "searx-init.service" ];
+      after = [ "searx-init.service" ];
+      serviceConfig = {
+        User  = "searx";
+        Group = "searx";
+        ExecStart = "${cfg.package}/bin/searx-run";
+      } // optionalAttrs (cfg.environmentFile != null)
+        { EnvironmentFile = builtins.toPath cfg.environmentFile; };
+      environment.SEARX_SETTINGS_PATH = cfg.settingsFile;
+    };
+
+    systemd.services.uwsgi = mkIf (cfg.runInUwsgi)
+      { requires = [ "searx-init.service" ];
+        after = [ "searx-init.service" ];
       };
 
-    systemd.services.searx =
-      {
-        description = "Searx server, the meta search engine.";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = "searx";
-          ExecStart = "${cfg.package}/bin/searx-run";
-        };
-      } // (optionalAttrs (configFile != null) {
-        environment.SEARX_SETTINGS_PATH = configFile;
-      });
+    services.searx.settings = {
+      # merge NixOS settings with defaults settings.yml
+      use_default_settings = mkDefault true;
+    };
 
-    environment.systemPackages = [ cfg.package ];
+    services.uwsgi = mkIf (cfg.runInUwsgi) {
+      enable = true;
+      plugins = [ "python3" ];
+
+      instance.type = "emperor";
+      instance.vassals.searx = {
+        type = "normal";
+        strict = true;
+        immediate-uid = "searx";
+        immediate-gid = "searx";
+        lazy-apps = true;
+        enable-threads = true;
+        module = "searx.webapp";
+        env = [ "SEARX_SETTINGS_PATH=${cfg.settingsFile}" ];
+        pythonPackages = self: [ cfg.package ];
+      } // cfg.uwsgiConfig;
+    };
 
   };
 
-  meta.maintainers = with lib.maintainers; [ rnhmjoj ];
+  meta.maintainers = with maintainers; [ rnhmjoj ];
 
 }
diff --git a/nixos/modules/services/networking/seeks.nix b/nixos/modules/services/networking/seeks.nix
deleted file mode 100644
index 40729225b6d..00000000000
--- a/nixos/modules/services/networking/seeks.nix
+++ /dev/null
@@ -1,75 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.seeks;
-
-  confDir = cfg.confDir;
-
-  seeks = pkgs.seeks.override { seeks_confDir = confDir; };
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.seeks = {
-
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = "
-          Whether to enable the Seeks server.
-        ";
-      };
-
-      confDir = mkOption {
-        default = "";
-        type = types.str;
-        description = "
-          The Seeks server configuration. If it is not specified,
-          a default configuration is used.
-        ";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.services.seeks.enable {
-
-    users.users.seeks =
-      { uid = config.ids.uids.seeks;
-        description = "Seeks user";
-        createHome = true;
-        home = "/var/lib/seeks";
-      };
-
-    users.groups.seeks =
-      { gid = config.ids.gids.seeks;
-      };
-
-    systemd.services.seeks =
-      {
-        description = "Seeks server, the p2p search engine.";
-        after = [ "network.target" ];
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = "seeks";
-          ExecStart = "${seeks}/bin/seeks";
-        };
-      };
-
-    environment.systemPackages = [ seeks ];
-
-  };
-
-}
diff --git a/nixos/modules/services/networking/shadowsocks.nix b/nixos/modules/services/networking/shadowsocks.nix
index af12db590f0..d2541f9a6df 100644
--- a/nixos/modules/services/networking/shadowsocks.nix
+++ b/nixos/modules/services/networking/shadowsocks.nix
@@ -11,8 +11,13 @@ let
     method = cfg.encryptionMethod;
     mode = cfg.mode;
     user = "nobody";
-    fast_open = true;
-  } // optionalAttrs (cfg.password != null) { password = cfg.password; };
+    fast_open = cfg.fastOpen;
+  } // optionalAttrs (cfg.plugin != null) {
+    plugin = cfg.plugin;
+    plugin_opts = cfg.pluginOpts;
+  } // optionalAttrs (cfg.password != null) {
+    password = cfg.password;
+  } // cfg.extraConfig;
 
   configFile = pkgs.writeText "shadowsocks.json" (builtins.toJSON opts);
 
@@ -74,6 +79,14 @@ in
         '';
       };
 
+      fastOpen = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          use TCP fast-open
+        '';
+      };
+
       encryptionMethod = mkOption {
         type = types.str;
         default = "chacha20-ietf-poly1305";
@@ -82,6 +95,41 @@ in
         '';
       };
 
+      plugin = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "\${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin";
+        description = ''
+          SIP003 plugin for shadowsocks
+        '';
+      };
+
+      pluginOpts = mkOption {
+        type = types.str;
+        default = "";
+        example = "server;host=example.com";
+        description = ''
+          Options to pass to the plugin if one was specified
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.attrs;
+        default = {};
+        example = ''
+          {
+            nameserver = "8.8.8.8";
+          }
+        '';
+        description = ''
+          Additional configuration for shadowsocks that is not covered by the
+          provided options. The provided attrset will be serialized to JSON and
+          has to contain valid shadowsocks options. Unfortunately most
+          additional options are undocumented but it's easy to find out what is
+          available by looking into the source code of
+          <link xlink:href="https://github.com/shadowsocks/shadowsocks-libev/blob/master/src/jconf.c"/>
+        '';
+      };
     };
 
   };
@@ -99,7 +147,7 @@ in
       description = "shadowsocks-libev Daemon";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.shadowsocks-libev ] ++ optional (cfg.passwordFile != null) pkgs.jq;
+      path = [ pkgs.shadowsocks-libev ] ++ optional (cfg.plugin != null) cfg.plugin ++ optional (cfg.passwordFile != null) pkgs.jq;
       serviceConfig.PrivateTmp = true;
       script = ''
         ${optionalString (cfg.passwordFile != null) ''
diff --git a/nixos/modules/services/networking/shairport-sync.nix b/nixos/modules/services/networking/shairport-sync.nix
index b4b86a2d55b..ac526c0e9f6 100644
--- a/nixos/modules/services/networking/shairport-sync.nix
+++ b/nixos/modules/services/networking/shairport-sync.nix
@@ -28,6 +28,7 @@ in
       };
 
       arguments = mkOption {
+        type = types.str;
         default = "-v -o pa";
         description = ''
           Arguments to pass to the daemon. Defaults to a local pulseaudio
@@ -36,6 +37,7 @@ in
       };
 
       user = mkOption {
+        type = types.str;
         default = "shairport";
         description = ''
           User account name under which to run shairport-sync. The account
diff --git a/nixos/modules/services/networking/shellhub-agent.nix b/nixos/modules/services/networking/shellhub-agent.nix
new file mode 100644
index 00000000000..4ce4b8250bc
--- /dev/null
+++ b/nixos/modules/services/networking/shellhub-agent.nix
@@ -0,0 +1,91 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.shellhub-agent;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.shellhub-agent = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the ShellHub Agent daemon, which allows
+          secure remote logins.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.shellhub-agent;
+        defaultText = "pkgs.shellhub-agent";
+        description = ''
+          Which ShellHub Agent package to use.
+        '';
+      };
+
+      tenantId = mkOption {
+        type = types.str;
+        example = "ba0a880c-2ada-11eb-a35e-17266ef329d6";
+        description = ''
+          The tenant ID to use when connecting to the ShellHub
+          Gateway.
+        '';
+      };
+
+      server = mkOption {
+        type = types.str;
+        default = "https://cloud.shellhub.io";
+        description = ''
+          Server address of ShellHub Gateway to connect.
+        '';
+      };
+
+      privateKey = mkOption {
+        type = types.path;
+        default = "/var/lib/shellhub-agent/private.key";
+        description = ''
+          Location where to store the ShellHub Agent private
+          key.
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    systemd.services.shellhub-agent = {
+      description = "ShellHub Agent";
+
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "local-fs.target" ];
+      wants = [ "network-online.target" ];
+      after = [
+        "local-fs.target"
+        "network.target"
+        "network-online.target"
+        "time-sync.target"
+      ];
+
+      environment.SERVER_ADDRESS = cfg.server;
+      environment.PRIVATE_KEY = cfg.privateKey;
+      environment.TENANT_ID = cfg.tenantId;
+
+      serviceConfig = {
+        # The service starts sessions for different users.
+        User = "root";
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/agent";
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/networking/smartdns.nix b/nixos/modules/services/networking/smartdns.nix
index f1888af7041..f84c727f034 100644
--- a/nixos/modules/services/networking/smartdns.nix
+++ b/nixos/modules/services/networking/smartdns.nix
@@ -54,6 +54,7 @@ in {
 
     systemd.packages = [ pkgs.smartdns ];
     systemd.services.smartdns.wantedBy = [ "multi-user.target" ];
+    systemd.services.smartdns.restartTriggers = [ confFile ];
     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 37ee2a80389..4470c18fd53 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -124,7 +124,8 @@ in
       };
       hostName = mkOption {
         type = types.str;
-        default = config.networking.hostName;
+        default = config.networking.fqdn;
+        defaultText = "\${config.networking.fqdn}";
         example = "somewhere.example.com";
         description = "DNS name for the urls generated in the cgi.";
       };
@@ -156,6 +157,7 @@ in
       ownerEmail = mkOption {
         type = types.str;
         default = "no-reply@${cfg.hostName}";
+        defaultText = "no-reply@\${hostName}";
         example = "no-reply@yourdomain.com";
         description = "Email contact for owner";
       };
@@ -239,18 +241,18 @@ in
       targetConfig = mkOption {
         type = types.lines;
         default = ''
-					probe = FPing
-					menu = Top
-					title = Network Latency Grapher
-					remark = Welcome to the SmokePing website of xxx Company. \
-									 Here you will learn all about the latency of our network.
-					+ Local
-					menu = Local
-					title = Local Network
-					++ LocalMachine
-					menu = Local Machine
-					title = This host
-					host = localhost
+          probe = FPing
+          menu = Top
+          title = Network Latency Grapher
+          remark = Welcome to the SmokePing website of xxx Company. \
+                   Here you will learn all about the latency of our network.
+          + Local
+          menu = Local
+          title = Local Network
+          ++ LocalMachine
+          menu = Local Machine
+          title = This host
+          host = localhost
         '';
         description = "Target configuration";
       };
@@ -303,7 +305,7 @@ in
         ${cfg.package}/bin/smokeping --check --config=${configPath}
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
-      script = ''${cfg.package}/bin/smokeping --config=${configPath} --nodaemon'';
+      script = "${cfg.package}/bin/smokeping --config=${configPath} --nodaemon";
     };
     systemd.services.thttpd = mkIf cfg.webService {
       wantedBy = [ "multi-user.target"];
diff --git a/nixos/modules/services/networking/solanum.nix b/nixos/modules/services/networking/solanum.nix
new file mode 100644
index 00000000000..dc066a24549
--- /dev/null
+++ b/nixos/modules/services/networking/solanum.nix
@@ -0,0 +1,109 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkEnableOption mkIf mkOption types;
+  inherit (pkgs) solanum util-linux;
+  cfg = config.services.solanum;
+
+  configFile = pkgs.writeText "solanum.conf" cfg.config;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.solanum = {
+
+      enable = mkEnableOption "Solanum IRC daemon";
+
+      config = mkOption {
+        type = types.str;
+        default = ''
+          serverinfo {
+            name = "irc.example.com";
+            sid = "1ix";
+            description = "irc!";
+
+            vhost = "0.0.0.0";
+            vhost6 = "::";
+          };
+
+          listen {
+            host = "0.0.0.0";
+            port = 6667;
+          };
+
+          auth {
+            user = "*@*";
+            class = "users";
+            flags = exceed_limit;
+          };
+          channel {
+            default_split_user_count = 0;
+          };
+        '';
+        description = ''
+          Solanum IRC daemon configuration file.
+          check <link xlink:href="https://github.com/solanum-ircd/solanum/blob/main/doc/reference.conf"/> for all options.
+        '';
+      };
+
+      openFilesLimit = mkOption {
+        type = types.int;
+        default = 1024;
+        description = ''
+          Maximum number of open files. Limits the clients and server connections.
+        '';
+      };
+
+      motd = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Solanum MOTD text.
+
+          Solanum will read its MOTD from <literal>/etc/solanum/ircd.motd</literal>.
+          If set, the value of this option will be written to this path.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (lib.mkMerge [
+    {
+
+      environment.etc."solanum/ircd.conf".source = configFile;
+
+      systemd.services.solanum = {
+        description = "Solanum IRC daemon";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        reloadIfChanged = true;
+        restartTriggers = [
+          configFile
+        ];
+        serviceConfig = {
+          ExecStart = "${solanum}/bin/solanum -foreground -logfile /dev/stdout -configfile /etc/solanum/ircd.conf -pidfile /run/solanum/ircd.pid";
+          ExecReload = "${util-linux}/bin/kill -HUP $MAINPID";
+          DynamicUser = true;
+          User = "solanum";
+          StateDirectory = "solanum";
+          RuntimeDirectory = "solanum";
+          LimitNOFILE = "${toString cfg.openFilesLimit}";
+        };
+      };
+
+    }
+
+    (mkIf (cfg.motd != null) {
+      environment.etc."solanum/ircd.motd".text = cfg.motd;
+    })
+  ]);
+}
diff --git a/nixos/modules/services/networking/spacecookie.nix b/nixos/modules/services/networking/spacecookie.nix
index c4d06df6ad4..e0bef9e9628 100644
--- a/nixos/modules/services/networking/spacecookie.nix
+++ b/nixos/modules/services/networking/spacecookie.nix
@@ -4,10 +4,22 @@ with lib;
 
 let
   cfg = config.services.spacecookie;
-  configFile = pkgs.writeText "spacecookie.json" (lib.generators.toJSON {} {
-    inherit (cfg) hostname port root;
-  });
+
+  spacecookieConfig = {
+    listen = {
+      inherit (cfg) port;
+    };
+  } // cfg.settings;
+
+  format = pkgs.formats.json {};
+
+  configFile = format.generate "spacecookie.json" spacecookieConfig;
+
 in {
+  imports = [
+    (mkRenamedOptionModule [ "services" "spacecookie" "root" ] [ "services" "spacecookie" "settings" "root" ])
+    (mkRenamedOptionModule [ "services" "spacecookie" "hostname" ] [ "services" "spacecookie" "settings" "hostname" ])
+  ];
 
   options = {
 
@@ -15,32 +27,149 @@ in {
 
       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.";
+      package = mkOption {
+        type = types.package;
+        default = pkgs.spacecookie;
+        defaultText = literalExample "pkgs.spacecookie";
+        example = literalExample "pkgs.haskellPackages.spacecookie";
+        description = ''
+          The spacecookie derivation to use. This can be used to
+          override the used package or to use another version.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to open the necessary port in the firewall for spacecookie.
+        '';
       };
 
       port = mkOption {
         type = types.port;
         default = 70;
-        description = "Port the gopher service should be exposed on.";
+        description = ''
+          Port the gopher service should be exposed on.
+        '';
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "[::]";
+        description = ''
+          Address to listen on. Must be in the
+          <literal>ListenStream=</literal> syntax of
+          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.socket.html">systemd.socket(5)</link>.
+        '';
       };
 
-      root = mkOption {
-        type = types.path;
-        default = "/srv/gopher";
-        description = "The root directory spacecookie serves via gopher.";
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+
+          options.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.
+            '';
+          };
+
+          options.root = mkOption {
+            type = types.path;
+            default = "/srv/gopher";
+            description = ''
+              The directory spacecookie should serve via gopher.
+              Files in there need to be world-readable since
+              the spacecookie service file sets
+              <literal>DynamicUser=true</literal>.
+            '';
+          };
+
+          options.log = {
+            enable = mkEnableOption "logging for spacecookie"
+              // { default = true; example = false; };
+
+            hide-ips = mkOption {
+              type = types.bool;
+              default = true;
+              description = ''
+                If enabled, spacecookie will hide personal
+                information of users like IP addresses from
+                log output.
+              '';
+            };
+
+            hide-time = mkOption {
+              type = types.bool;
+              # since we are starting with systemd anyways
+              # we deviate from the default behavior here:
+              # journald will add timestamps, so no need
+              # to double up.
+              default = true;
+              description = ''
+                If enabled, spacecookie will not print timestamps
+                at the beginning of every log line.
+              '';
+            };
+
+            level = mkOption {
+              type = types.enum [
+                "info"
+                "warn"
+                "error"
+              ];
+              default = "info";
+              description = ''
+                Log level for the spacecookie service.
+              '';
+            };
+          };
+        };
+
+        description = ''
+          Settings for spacecookie. The settings set here are
+          directly translated to the spacecookie JSON config
+          file. See
+          <link xlink:href="https://sternenseemann.github.io/spacecookie/spacecookie.json.5.html">spacecookie.json(5)</link>
+          for explanations of all options.
+        '';
       };
     };
   };
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !(cfg.settings ? user);
+        message = ''
+          spacecookie is started as a normal user, so the setuid
+          feature doesn't work. If you want to run spacecookie as
+          a specific user, set:
+          systemd.services.spacecookie.serviceConfig = {
+            DynamicUser = false;
+            User = "youruser";
+            Group = "yourgroup";
+          }
+        '';
+      }
+      {
+        assertion = !(cfg.settings ? listen || cfg.settings ? port);
+        message = ''
+          The NixOS spacecookie module uses socket activation,
+          so the listen options have no effect. Use the port
+          and address options in services.spacecookie instead.
+        '';
+      }
+    ];
 
     systemd.sockets.spacecookie = {
       description = "Socket for the Spacecookie Gopher Server";
       wantedBy = [ "sockets.target" ];
-      listenStreams = [ "[::]:${toString cfg.port}" ];
+      listenStreams = [ "${cfg.address}:${toString cfg.port}" ];
       socketConfig = {
         BindIPv6Only = "both";
       };
@@ -53,7 +182,7 @@ in {
 
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${pkgs.haskellPackages.spacecookie}/bin/spacecookie ${configFile}";
+        ExecStart = "${lib.getBin cfg.package}/bin/spacecookie ${configFile}";
         FileDescriptorStoreMax = 1;
 
         DynamicUser = true;
@@ -79,5 +208,9 @@ in {
         RestrictAddressFamilies = "AF_UNIX AF_INET6";
       };
     };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
   };
 }
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index 41d0584080e..862ff7df054 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -29,6 +29,7 @@ in
 
       portNumber = mkOption {
         default = 22;
+        type = types.port;
         description = ''
           The port on which to listen for connections.
         '';
@@ -36,6 +37,7 @@ in
 
       interfaces = mkOption {
         default = [];
+        type = types.listOf types.str;
         description = ''
           List of network interfaces where listening for connections.
           When providing the empty list, `[]', lshd listens on all
@@ -46,6 +48,7 @@ in
 
       hostKey = mkOption {
         default = "/etc/lsh/host-key";
+        type = types.str;
         description = ''
           Path to the server's private key.  Note that this key must
           have been created, e.g., using "lsh-keygen --server |
@@ -56,29 +59,30 @@ in
       syslog = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable syslog output.'';
+        description = "Whether to enable syslog output.";
       };
 
       passwordAuthentication = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable password authentication.'';
+        description = "Whether to enable password authentication.";
       };
 
       publicKeyAuthentication = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable public key authentication.'';
+        description = "Whether to enable public key authentication.";
       };
 
       rootLogin = mkOption {
         type = types.bool;
         default = false;
-        description = ''Whether to enable remote root login.'';
+        description = "Whether to enable remote root login.";
       };
 
       loginShell = mkOption {
         default = null;
+        type = types.nullOr types.str;
         description = ''
           If non-null, override the default login shell with the
           specified value.
@@ -88,6 +92,7 @@ in
 
       srpKeyExchange = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Whether to enable SRP key exchange and user authentication.
         '';
@@ -96,16 +101,17 @@ in
       tcpForwarding = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable TCP/IP forwarding.'';
+        description = "Whether to enable TCP/IP forwarding.";
       };
 
       x11Forwarding = mkOption {
         type = types.bool;
         default = true;
-        description = ''Whether to enable X11 forwarding.'';
+        description = "Whether to enable X11 forwarding.";
       };
 
       subsystems = mkOption {
+        type = types.listOf types.path;
         description = ''
           List of subsystem-path pairs, where the head of the pair
           denotes the subsystem name, and the tail denotes the path to
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 17f31e3a488..2c96b94ca43 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -41,6 +41,10 @@ let
           Warning: If you are using <literal>NixOps</literal> then don't use this
           option since it will replace the key required for deployment via ssh.
         '';
+        example = [
+          "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
+          "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
+        ];
       };
 
       keyFiles = mkOption {
@@ -122,6 +126,15 @@ in
         '';
       };
 
+      sftpServerExecutable = mkOption {
+        type = types.str;
+        example = "internal-sftp";
+        description = ''
+          The sftp server executable.  Can be a path or "internal-sftp" to use
+          the sftp server built into the sshd binary.
+        '';
+      };
+
       sftpFlags = mkOption {
         type = with types; listOf str;
         default = [];
@@ -232,10 +245,28 @@ in
         '';
       };
 
+      banner = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Message to display to the remote user before authentication is allowed.
+        '';
+      };
+
       authorizedKeysFiles = mkOption {
         type = types.listOf types.str;
         default = [];
-        description = "Files from which authorized keys are read.";
+        description = ''
+          Specify the rules for which files to read on the host.
+
+          This is an advanced option. If you're looking to configure user
+          keys, you can generally use <xref linkend="opt-users.users._name_.openssh.authorizedKeys.keys"/>
+          or <xref linkend="opt-users.users._name_.openssh.authorizedKeys.keyFiles"/>.
+
+          These are paths relative to the host root file system or home
+          directories and they are subject to certain token expansion rules.
+          See AuthorizedKeysFile in man sshd_config for details.
+        '';
       };
 
       authorizedKeysCommand = mkOption {
@@ -261,6 +292,7 @@ in
       kexAlgorithms = mkOption {
         type = types.listOf types.str;
         default = [
+          "curve25519-sha256"
           "curve25519-sha256@libssh.org"
           "diffie-hellman-group-exchange-sha256"
         ];
@@ -271,7 +303,7 @@ in
           Defaults to recommended settings from both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
-          <link xlink:href="https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29" />
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
         '';
       };
 
@@ -292,7 +324,7 @@ in
           Defaults to recommended settings from both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
-          <link xlink:href="https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29" />
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
         '';
       };
 
@@ -313,21 +345,18 @@ in
           Defaults to recommended settings from both
           <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html" />
           and
-          <link xlink:href="https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29" />
+          <link xlink:href="https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67" />
         '';
       };
 
       logLevel = mkOption {
         type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
-        default = "VERBOSE";
+        default = "INFO"; # upstream default
         description = ''
           Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
-          QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is VERBOSE. DEBUG and DEBUG1
+          QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is INFO. DEBUG and DEBUG1
           are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
           violates the privacy of users and is not recommended.
-
-          LogLevel VERBOSE logs user's key fingerprint on login.
-          Needed to have a clear audit track of which key was used to log in.
         '';
       };
 
@@ -361,7 +390,7 @@ in
     };
 
     users.users = mkOption {
-      type = with types; loaOf (submodule userOptions);
+      type = with types; attrsOf (submodule userOptions);
     };
 
   };
@@ -377,6 +406,7 @@ in
       };
 
     services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
+    services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server";
 
     environment.etc = authKeysFiles //
       { "ssh/moduli".source = cfg.moduliFile;
@@ -423,6 +453,7 @@ in
               { ExecStart =
                   (optionalString cfg.startWhenNeeded "-") +
                   "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
+                  "-D " +  # don't detach into a daemon process
                   "-f /etc/ssh/sshd_config";
                 KillMode = "process";
               } // (if cfg.startWhenNeeded then {
@@ -468,12 +499,14 @@ in
     # https://github.com/NixOS/nixpkgs/pull/10155
     # https://github.com/NixOS/nixpkgs/pull/41745
     services.openssh.authorizedKeysFiles =
-      [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
+      [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
 
     services.openssh.extraConfig = mkOrder 0
       ''
         UsePAM yes
 
+        Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner}
+
         AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
         ${concatMapStrings (port: ''
           Port ${toString port}
@@ -494,7 +527,7 @@ in
         ''}
 
         ${optionalString cfg.allowSFTP ''
-          Subsystem sftp ${cfgc.package}/libexec/sftp-server ${concatStringsSep " " cfg.sftpFlags}
+          Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags}
         ''}
 
         PermitRootLogin ${cfg.permitRootLogin}
diff --git a/nixos/modules/services/networking/sslh.nix b/nixos/modules/services/networking/sslh.nix
index 0921febba66..abe96f60f81 100644
--- a/nixos/modules/services/networking/sslh.nix
+++ b/nixos/modules/services/networking/sslh.nix
@@ -31,7 +31,7 @@ let
       { name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; },
       { name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; },
       { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
-      { name: "ssl"; host: "localhost"; port: "443"; probe: "builtin"; },
+      { name: "tls"; host: "localhost"; port: "443"; probe: "builtin"; },
       { name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; }
     );
   '';
@@ -132,7 +132,7 @@ in
           { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
         ];
       in {
-        path = [ pkgs.iptables pkgs.iproute pkgs.procps ];
+        path = [ pkgs.iptables pkgs.iproute2 pkgs.procps ];
 
         preStart = ''
           # Cleanup old iptables entries which might be still there
diff --git a/nixos/modules/services/networking/strongswan-swanctl/module.nix b/nixos/modules/services/networking/strongswan-swanctl/module.nix
index 0fec3ef00ad..6e619f22546 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/module.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/module.nix
@@ -63,7 +63,7 @@ in  {
       description = "strongSwan IPsec IKEv1/IKEv2 daemon using swanctl";
       wantedBy = [ "multi-user.target" ];
       after    = [ "network-online.target" ];
-      path     = with pkgs; [ kmod iproute iptables utillinux ];
+      path     = with pkgs; [ kmod iproute2 iptables util-linux ];
       environment = {
         STRONGSWAN_CONF = pkgs.writeTextFile {
           name = "strongswan.conf";
diff --git a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
index 808cb863a9c..8ae62931a8f 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
@@ -1173,20 +1173,20 @@ in {
 
     ppk = mkPrefixedAttrsOfParams {
       secret = mkOptionalStrParam ''
-	      Value of the PPK. It may either be an ASCII string, a hex encoded string
-	      if it has a <literal>0x</literal> prefix or a Base64 encoded string if
-	      it has a <literal>0s</literal> prefix in its value. Should have at least
-	      256 bits of entropy for 128-bit security.
+        Value of the PPK. It may either be an ASCII string, a hex encoded string
+        if it has a <literal>0x</literal> prefix or a Base64 encoded string if
+        it has a <literal>0s</literal> prefix in its value. Should have at least
+        256 bits of entropy for 128-bit security.
       '';
 
       id = mkPrefixedAttrsOfParam (mkOptionalStrParam "") ''
-	      PPK identity the PPK belongs to. Multiple unique identities may be
-	      specified, each having an <literal>id</literal> prefix, if a secret is
-	      shared between multiple peers.
+        PPK identity the PPK belongs to. Multiple unique identities may be
+        specified, each having an <literal>id</literal> prefix, if a secret is
+        shared between multiple peers.
       '';
     } ''
-	    Postquantum Preshared Key (PPK) section for a specific secret. Each PPK is
-	    defined in a unique section having the <literal>ppk</literal> prefix.
+      Postquantum Preshared Key (PPK) section for a specific secret. Each PPK is
+      defined in a unique section having the <literal>ppk</literal> prefix.
     '';
 
     private = mkPrefixedAttrsOfParams {
@@ -1273,7 +1273,7 @@ in {
         provided the user is prompted during an interactive
         <literal>--load-creds</literal> call.
       '';
-    } ''Definition for a private key that's stored on a token/smartcard/TPM.'';
+    } "Definition for a private key that's stored on a token/smartcard/TPM.";
 
   };
 
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 13a1a897c5e..401f7be4028 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -152,7 +152,7 @@ in
     systemd.services.strongswan = {
       description = "strongSwan IPSec Service";
       wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ kmod iproute iptables utillinux ]; # XXX Linux
+      path = with pkgs; [ kmod iproute2 iptables util-linux ]; # XXX Linux
       after = [ "network-online.target" ];
       environment = {
         STRONGSWAN_CONF = strongswanConf { inherit setup connections ca secretsFile managePlugins enabledPlugins; };
diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix
index ab51bba2f6a..fe1616f411f 100644
--- a/nixos/modules/services/networking/stunnel.nix
+++ b/nixos/modules/services/networking/stunnel.nix
@@ -16,8 +16,12 @@ let
   serverConfig = {
     options = {
       accept = mkOption {
-        type = types.int;
-        description = "On which port stunnel should listen for incoming TLS connections.";
+        type = types.either types.str types.int;
+        description = ''
+          On which [host:]port stunnel should listen for incoming TLS connections.
+          Note that unlike other softwares stunnel ipv6 address need no brackets,
+          so to listen on all IPv6 addresses on port 1234 one would use ':::1234'.
+        '';
       };
 
       connect = mkOption {
@@ -129,7 +133,6 @@ in
         type = with types; attrsOf (submodule serverConfig);
         example = {
           fancyWebserver = {
-            enable = true;
             accept = 443;
             connect = 8080;
             cert = "/path/to/pem/file";
diff --git a/nixos/modules/services/networking/supplicant.nix b/nixos/modules/services/networking/supplicant.nix
index 20704be9b36..4f4b5cef374 100644
--- a/nixos/modules/services/networking/supplicant.nix
+++ b/nixos/modules/services/networking/supplicant.nix
@@ -44,19 +44,10 @@ let
 
         preStart = ''
           ${optionalString (suppl.configFile.path!=null) ''
-            touch -a ${suppl.configFile.path}
-            chmod 600 ${suppl.configFile.path}
+            (umask 077 && touch -a "${suppl.configFile.path}")
           ''}
           ${optionalString suppl.userControlled.enable ''
-            if ! test -e ${suppl.userControlled.socketDir}; then
-                mkdir -m 0770 -p ${suppl.userControlled.socketDir}
-                chgrp ${suppl.userControlled.group} ${suppl.userControlled.socketDir}
-            fi
-
-            if test "$(stat --printf '%G' ${suppl.userControlled.socketDir})" != "${suppl.userControlled.group}"; then
-                echo "ERROR: bad ownership on ${suppl.userControlled.socketDir}" >&2
-                exit 1
-            fi
+            install -dm770 -g "${suppl.userControlled.group}" "${suppl.userControlled.socketDir}"
           ''}
         '';
 
diff --git a/nixos/modules/services/networking/supybot.nix b/nixos/modules/services/networking/supybot.nix
index dc9fb31ffd0..332c3ced06f 100644
--- a/nixos/modules/services/networking/supybot.nix
+++ b/nixos/modules/services/networking/supybot.nix
@@ -64,13 +64,14 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         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 ]'';
+        example = literalExample "p: [ p.lxml p.requests ]";
       };
 
     };
@@ -103,6 +104,8 @@ in
         rm -f '${cfg.stateDir}/supybot.cfg.bak'
       '';
 
+      startLimitIntervalSec = 5 * 60;  # 5 min
+      startLimitBurst = 1;
       serviceConfig = {
         ExecStart = "${pyEnv}/bin/supybot ${cfg.stateDir}/supybot.cfg";
         PIDFile = "/run/supybot.pid";
@@ -110,8 +113,6 @@ in
         Group = "supybot";
         UMask = "0007";
         Restart = "on-abort";
-        StartLimitInterval = "5m";
-        StartLimitBurst = "1";
 
         NoNewPrivileges = true;
         PrivateDevices = true;
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index e717d78feed..28348c7893a 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -18,6 +18,7 @@ let
     fsWatcherEnabled = folder.watch;
     fsWatcherDelayS = folder.watchDelay;
     ignorePerms = folder.ignorePerms;
+    ignoreDelete = folder.ignoreDelete;
     versioning = folder.versioning;
   }) (filterAttrs (
     _: folder:
@@ -284,8 +285,6 @@ in {
                 });
               };
 
-
-
               rescanInterval = mkOption {
                 type = types.int;
                 default = 3600;
@@ -327,6 +326,16 @@ in {
                 '';
               };
 
+              ignoreDelete = mkOption {
+                type = types.bool;
+                default = false;
+                description = ''
+                  Whether to delete files in destination. See <link
+                  xlink:href="https://docs.syncthing.net/advanced/folder-ignoredelete.html">
+                  upstream's docs</link>.
+                '';
+              };
+
             };
           }));
         };
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index 4d6aeb75ebd..3f88ff53dff 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -14,36 +14,31 @@ in {
       default = 41641;
       description = "The port to listen on for tunnel traffic (0=autoselect).";
     };
+
+    interfaceName = mkOption {
+      type = types.str;
+      default = "tailscale0";
+      description = ''The interface name for tunnel traffic. Use "userspace-networking" (beta) to not use TUN.'';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.tailscale;
+      defaultText = "pkgs.tailscale";
+      description = "The package to use for tailscale";
+    };
   };
 
   config = mkIf cfg.enable {
-    systemd.services.tailscale = {
-      description = "Tailscale client daemon";
-
-      after = [ "network-pre.target" ];
-      wants = [ "network-pre.target" ];
+    environment.systemPackages = [ cfg.package ]; # for the CLI
+    systemd.packages = [ cfg.package ];
+    systemd.services.tailscaled = {
       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";
-      };
+      path = [ pkgs.openresolv pkgs.procps ];
+      serviceConfig.Environment = [
+        "PORT=${toString cfg.port}"
+        ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName}"''
+      ];
     };
   };
 }
diff --git a/nixos/modules/services/networking/tinc.nix b/nixos/modules/services/networking/tinc.nix
index 725bd9bf940..9e433ad1a98 100644
--- a/nixos/modules/services/networking/tinc.nix
+++ b/nixos/modules/services/networking/tinc.nix
@@ -1,13 +1,156 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.tinc;
 
-in
+  mkValueString = value:
+    if value == true then "yes"
+    else if value == false then "no"
+    else generators.mkValueStringDefault { } value;
+
+  toTincConf = generators.toKeyValue {
+    listsAsDuplicateKeys = true;
+    mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } "=";
+  };
+
+  tincConfType = with types;
+    let
+      valueType = oneOf [ bool str int ];
+    in
+    attrsOf (either valueType (listOf valueType));
+
+  addressSubmodule = {
+    options = {
+      address = mkOption {
+        type = types.str;
+        description = "The external IP address or hostname where the host can be reached.";
+      };
+
+      port = mkOption {
+        type = types.nullOr types.port;
+        default = null;
+        description = ''
+          The port where the host can be reached.
+
+          If no port is specified, the default Port is used.
+        '';
+      };
+    };
+  };
+
+  subnetSubmodule = {
+    options = {
+      address = mkOption {
+        type = types.str;
+        description = ''
+          The subnet of this host.
+
+          Subnets can either be single MAC, IPv4 or IPv6 addresses, in which case
+          a subnet consisting of only that single address is assumed, or they can
+          be a IPv4 or IPv6 network address with a prefix length.
+
+          IPv4 subnets are notated like 192.168.1.0/24, IPv6 subnets are notated
+          like fec0:0:0:1::/64. MAC addresses are notated like 0:1a:2b:3c:4d:5e.
+
+          Note that subnets like 192.168.1.1/24 are invalid.
+        '';
+      };
+
+      prefixLength = mkOption {
+        type = with types; nullOr (addCheck int (n: n >= 0 && n <= 128));
+        default = null;
+        description = ''
+          The prefix length of the subnet.
+
+          If null, a subnet consisting of only that single address is assumed.
+
+          This conforms to standard CIDR notation as described in RFC1519.
+        '';
+      };
+
+      weight = mkOption {
+        type = types.ints.unsigned;
+        default = 10;
+        description = ''
+          Indicates the priority over identical Subnets owned by different nodes.
+
+          Lower values indicate higher priority. Packets will be sent to the
+          node with the highest priority, unless that node is not reachable, in
+          which case the node with the next highest priority will be tried, and
+          so on.
+        '';
+      };
+    };
+  };
+
+  hostSubmodule = { config, ... }: {
+    options = {
+      addresses = mkOption {
+        type = types.listOf (types.submodule addressSubmodule);
+        default = [ ];
+        description = ''
+          The external address where the host can be reached. This will set this
+          host's <option>settings.Address</option> option.
+
+          This variable is only required if you want to connect to this host.
+        '';
+      };
+
+      subnets = mkOption {
+        type = types.listOf (types.submodule subnetSubmodule);
+        default = [ ];
+        description = ''
+          The subnets which this tinc daemon will serve. This will set this
+          host's <option>settings.Subnet</option> option.
 
+          Tinc tries to look up which other daemon it should send a packet to by
+          searching the appropriate subnet. If the packet matches a subnet, it
+          will be sent to the daemon who has this subnet in his host
+          configuration file.
+        '';
+      };
+
+      rsaPublicKey = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Legacy RSA public key of the host in PEM format, including start and
+          end markers.
+
+          This will be appended as-is in the host's configuration file.
+
+          The ed25519 public key can be specified using the
+          <option>settings.Ed25519PublicKey</option> option instead.
+        '';
+      };
+
+      settings = mkOption {
+        default = { };
+        type = types.submodule { freeformType = tincConfType; };
+        description = ''
+          Configuration for this host.
+
+          See <link xlink:href="https://tinc-vpn.org/documentation-1.1/Host-configuration-variables.html"/>
+          for supported values.
+        '';
+      };
+    };
+
+    config.settings = {
+      Address = mkDefault (map
+        (address: "${address.address} ${toString address.port}")
+        config.addresses);
+
+      Subnet = mkDefault (map
+        (subnet:
+          if subnet.prefixLength == null then "${subnet.address}#${toString subnet.weight}"
+          else "${subnet.address}/${toString subnet.prefixLength}#${toString subnet.weight}")
+        config.subnets);
+    };
+  };
+
+in
 {
 
   ###### interface
@@ -18,7 +161,7 @@ in
 
       networks = mkOption {
         default = { };
-        type = with types; attrsOf (submodule {
+        type = with types; attrsOf (submodule ({ config, ... }: {
           options = {
 
             extraConfig = mkOption {
@@ -26,6 +169,9 @@ in
               type = types.lines;
               description = ''
                 Extra lines to add to the tinc service configuration file.
+
+                Note that using the declarative <option>service.tinc.networks.&lt;name&gt;.settings</option>
+                option is preferred.
               '';
             };
 
@@ -72,6 +218,40 @@ in
               description = ''
                 The name of the host in the network as well as the configuration for that host.
                 This name should only contain alphanumerics and underscores.
+
+                Note that using the declarative <option>service.tinc.networks.&lt;name&gt;.hostSettings</option>
+                option is preferred.
+              '';
+            };
+
+            hostSettings = mkOption {
+              default = { };
+              example = literalExample ''
+                {
+                  host1 = {
+                    addresses = [
+                      { address = "192.168.1.42"; }
+                      { address = "192.168.1.42"; port = 1655; }
+                    ];
+                    subnets = [ { address = "10.0.0.42"; } ];
+                    rsaPublicKey = "...";
+                    settings = {
+                      Ed25519PublicKey = "...";
+                    };
+                  };
+                  host2 = {
+                    subnets = [ { address = "10.0.1.0"; prefixLength = 24; weight = 2; } ];
+                    rsaPublicKey = "...";
+                    settings = {
+                      Compression = 10;
+                    };
+                  };
+                }
+              '';
+              type = types.attrsOf (types.submodule hostSubmodule);
+              description = ''
+                The name of the host in the network as well as the configuration for that host.
+                This name should only contain alphanumerics and underscores.
               '';
             };
 
@@ -79,7 +259,7 @@ in
               default = "tun";
               type = types.enum [ "tun" "tap" ];
               description = ''
-                The type of virtual interface used for the network connection
+                The type of virtual interface used for the network connection.
               '';
             };
 
@@ -118,8 +298,44 @@ in
                 Note that tinc can't run scripts anymore (such as tinc-down or host-up), unless it is setup to be runnable inside chroot environment.
               '';
             };
+
+            settings = mkOption {
+              default = { };
+              type = types.submodule { freeformType = tincConfType; };
+              example = literalExample ''
+                {
+                  Interface = "custom.interface";
+                  DirectOnly = true;
+                  Mode = "switch";
+                }
+              '';
+              description = ''
+                Configuration of the Tinc daemon for this network.
+
+                See <link xlink:href="https://tinc-vpn.org/documentation-1.1/Main-configuration-variables.html"/>
+                for supported values.
+              '';
+            };
+          };
+
+          config = {
+            hosts = mapAttrs
+              (hostname: host: ''
+                ${toTincConf host.settings}
+                ${host.rsaPublicKey}
+              '')
+              config.hostSettings;
+
+            settings = {
+              DeviceType = mkDefault config.interfaceType;
+              Name = mkDefault (if config.name == null then "$HOST" else config.name);
+              Ed25519PrivateKeyFile = mkIf (config.ed25519PrivateKeyFile != null) (mkDefault config.ed25519PrivateKeyFile);
+              PrivateKeyFile = mkIf (config.rsaPrivateKeyFile != null) (mkDefault config.rsaPrivateKeyFile);
+              ListenAddress = mkIf (config.listenAddress != null) (mkDefault config.listenAddress);
+              BindToAddress = mkIf (config.bindToAddress != null) (mkDefault config.bindToAddress);
+            };
           };
-        });
+        }));
 
         description = ''
           Defines the tinc networks which will be started.
@@ -135,7 +351,7 @@ in
 
   config = mkIf (cfg.networks != { }) {
 
-    environment.etc = fold (a: b: a // b) { }
+    environment.etc = foldr (a: b: a // b) { }
       (flip mapAttrsToList cfg.networks (network: data:
         flip mapAttrs' data.hosts (host: text: nameValuePair
           ("tinc/${network}/hosts/${host}")
@@ -144,13 +360,7 @@ in
           "tinc/${network}/tinc.conf" = {
             mode = "0444";
             text = ''
-              Name = ${if data.name == null then "$HOST" else data.name}
-              DeviceType = ${data.interfaceType}
-              ${optionalString (data.ed25519PrivateKeyFile != null) "Ed25519PrivateKeyFile = ${data.ed25519PrivateKeyFile}"}
-              ${optionalString (data.rsaPrivateKeyFile != null) "PrivateKeyFile = ${data.rsaPrivateKeyFile}"}
-              ${optionalString (data.listenAddress != null) "ListenAddress = ${data.listenAddress}"}
-              ${optionalString (data.bindToAddress != null) "BindToAddress = ${data.bindToAddress}"}
-              Interface = tinc.${network}
+              ${toTincConf ({ Interface = "tinc.${network}"; } // data.settings)}
               ${data.extraConfig}
             '';
           };
@@ -168,6 +378,7 @@ in
           Type = "simple";
           Restart = "always";
           RestartSec = "3";
+          ExecReload = mkIf (versionAtLeast (getVersion data.package) "1.1pre") "${data.package}/bin/tinc -n ${network} reload";
           ExecStart = "${data.package}/bin/tincd -D -U tinc.${network} -n ${network} ${optionalString (data.chroot) "-R"} --pidfile /run/tinc.${network}.pid -d ${toString data.debugLevel}";
         };
         preStart = ''
@@ -221,4 +432,5 @@ in
 
   };
 
+  meta.maintainers = with maintainers; [ minijackson ];
 }
diff --git a/nixos/modules/services/networking/ucarp.nix b/nixos/modules/services/networking/ucarp.nix
new file mode 100644
index 00000000000..9b19a19687b
--- /dev/null
+++ b/nixos/modules/services/networking/ucarp.nix
@@ -0,0 +1,183 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.ucarp;
+
+  ucarpExec = concatStringsSep " " (
+    [
+      "${cfg.package}/bin/ucarp"
+      "--interface=${cfg.interface}"
+      "--srcip=${cfg.srcIp}"
+      "--vhid=${toString cfg.vhId}"
+      "--passfile=${cfg.passwordFile}"
+      "--addr=${cfg.addr}"
+      "--advbase=${toString cfg.advBase}"
+      "--advskew=${toString cfg.advSkew}"
+      "--upscript=${cfg.upscript}"
+      "--downscript=${cfg.downscript}"
+      "--deadratio=${toString cfg.deadratio}"
+    ]
+    ++ (optional cfg.preempt "--preempt")
+    ++ (optional cfg.neutral "--neutral")
+    ++ (optional cfg.shutdown "--shutdown")
+    ++ (optional cfg.ignoreIfState "--ignoreifstate")
+    ++ (optional cfg.noMcast "--nomcast")
+    ++ (optional (cfg.extraParam != null) "--xparam=${cfg.extraParam}")
+  );
+in {
+  options.networking.ucarp = {
+    enable = mkEnableOption "ucarp, userspace implementation of CARP";
+
+    interface = mkOption {
+      type = types.str;
+      description = "Network interface to bind to.";
+      example = "eth0";
+    };
+
+    srcIp = mkOption {
+      type = types.str;
+      description = "Source (real) IP address of this host.";
+    };
+
+    vhId = mkOption {
+      type = types.ints.between 1 255;
+      description = "Virtual IP identifier shared between CARP hosts.";
+      example = 1;
+    };
+
+    passwordFile = mkOption {
+      type = types.str;
+      description = "File containing shared password between CARP hosts.";
+      example = "/run/keys/ucarp-password";
+    };
+
+    preempt = mkOption {
+      type = types.bool;
+      description = ''
+        Enable preemptive failover.
+        Thus, this host becomes the CARP master as soon as possible.
+      '';
+      default = false;
+    };
+
+    neutral = mkOption {
+      type = types.bool;
+      description = "Do not run downscript at start if the host is the backup.";
+      default = false;
+    };
+
+    addr = mkOption {
+      type = types.str;
+      description = "Virtual shared IP address.";
+    };
+
+    advBase = mkOption {
+      type = types.ints.unsigned;
+      description = "Advertisement frequency in seconds.";
+      default = 1;
+    };
+
+    advSkew = mkOption {
+      type = types.ints.unsigned;
+      description = "Advertisement skew in seconds.";
+      default = 0;
+    };
+
+    upscript = mkOption {
+      type = types.path;
+      description = ''
+        Command to run after become master, the interface name, virtual address
+        and optional extra parameters are passed as arguments.
+      '';
+      example = ''
+        pkgs.writeScript "upscript" '''
+          #!/bin/sh
+          $\{pkgs.iproute2\}/bin/ip addr add "$2"/24 dev "$1"
+        ''';
+      '';
+    };
+
+    downscript = mkOption {
+      type = types.path;
+      description = ''
+        Command to run after become backup, the interface name, virtual address
+        and optional extra parameters are passed as arguments.
+      '';
+      example = ''
+        pkgs.writeScript "downscript" '''
+          #!/bin/sh
+          $\{pkgs.iproute2\}/bin/ip addr del "$2"/24 dev "$1"
+        ''';
+      '';
+    };
+
+    deadratio = mkOption {
+      type = types.ints.unsigned;
+      description = "Ratio to consider a host as dead.";
+      default = 3;
+    };
+
+    shutdown = mkOption {
+      type = types.bool;
+      description = "Call downscript at exit.";
+      default = false;
+    };
+
+    ignoreIfState = mkOption {
+      type = types.bool;
+      description = "Ignore interface state, e.g., down or no carrier.";
+      default = false;
+    };
+
+    noMcast = mkOption {
+      type = types.bool;
+      description = "Use broadcast instead of multicast advertisements.";
+      default = false;
+    };
+
+    extraParam = mkOption {
+      type = types.nullOr types.str;
+      description = "Extra parameter to pass to the up/down scripts.";
+      default = null;
+    };
+
+    package = mkOption {
+      type = types.package;
+      description = ''
+        Package that should be used for ucarp.
+
+        Please note that the default package, pkgs.ucarp, has not received any
+        upstream updates for a long time and can be considered as unmaintained.
+      '';
+      default = pkgs.ucarp;
+      defaultText = "pkgs.ucarp";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.ucarp = {
+      description = "ucarp, userspace implementation of CARP";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        Type = "exec";
+        ExecStart = ucarpExec;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ oxzi ];
+}
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index baed83591e1..6d7178047ea 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -1,50 +1,39 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.unbound;
 
-  stateDir = "/var/lib/unbound";
-
-  access = concatMapStringsSep "\n  " (x: "access-control: ${x} allow") cfg.allowedAccess;
-
-  interfaces = concatMapStringsSep "\n  " (x: "interface: ${x}") cfg.interfaces;
-
-  isLocalAddress = x: substring 0 3 x == "::1" || substring 0 9 x == "127.0.0.1";
+  yesOrNo = v: if v then "yes" else "no";
 
-  forward =
-    optionalString (any isLocalAddress cfg.forwardAddresses) ''
-      do-not-query-localhost: no
-    '' +
-    optionalString (cfg.forwardAddresses != []) ''
-      forward-zone:
-        name: .
-    '' +
-    concatMapStringsSep "\n" (x: "    forward-addr: ${x}") cfg.forwardAddresses;
+  toOption = indent: n: v: "${indent}${toString n}: ${v}";
 
-  rootTrustAnchorFile = "${stateDir}/root.key";
+  toConf = indent: n: v:
+    if builtins.isFloat v then (toOption indent n (builtins.toJSON v))
+    else if isInt v       then (toOption indent n (toString v))
+    else if isBool v      then (toOption indent n (yesOrNo v))
+    else if isString v    then (toOption indent n v)
+    else if isList v      then (concatMapStringsSep "\n" (toConf indent n) v)
+    else if isAttrs v     then (concatStringsSep "\n" (
+                                  ["${indent}${n}:"] ++ (
+                                    mapAttrsToList (toConf "${indent}  ") v
+                                  )
+                                ))
+    else throw (traceSeq v "services.unbound.settings: unexpected type");
 
-  trustAnchor = optionalString cfg.enableRootTrustAnchor
-    "auto-trust-anchor-file: ${rootTrustAnchorFile}";
+  confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]);
+  confServer = concatStringsSep "\n" (mapAttrsToList (toConf "  ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ]));
 
   confFile = pkgs.writeText "unbound.conf" ''
     server:
-      directory: "${stateDir}"
-      username: unbound
-      chroot: "${stateDir}"
-      pidfile: ""
-      ${interfaces}
-      ${access}
-      ${trustAnchor}
-    ${cfg.extraConfig}
-    ${forward}
+    ${optionalString (cfg.settings.server.define-tag != "") (toOption "  " "define-tag" cfg.settings.server.define-tag)}
+    ${confServer}
+    ${confNoServer}
   '';
 
-in
+  rootTrustAnchorFile = "${cfg.stateDir}/root.key";
 
-{
+in {
 
   ###### interface
 
@@ -55,27 +44,35 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.unbound;
-        defaultText = "pkgs.unbound";
+        default = pkgs.unbound-with-systemd;
+        defaultText = "pkgs.unbound-with-systemd";
         description = "The unbound package to use";
       };
 
-      allowedAccess = mkOption {
-        default = [ "127.0.0.0/24" ];
-        type = types.listOf types.str;
-        description = "What networks are allowed to use unbound as a resolver.";
+      user = mkOption {
+        type = types.str;
+        default = "unbound";
+        description = "User account under which unbound runs.";
       };
 
-      interfaces = mkOption {
-        default = [ "127.0.0.1" ] ++ optional config.networking.enableIPv6 "::1";
-        type = types.listOf types.str;
-        description = "What addresses the server should listen on.";
+      group = mkOption {
+        type = types.str;
+        default = "unbound";
+        description = "Group under which unbound runs.";
       };
 
-      forwardAddresses = mkOption {
-        default = [ ];
-        type = types.listOf types.str;
-        description = "What servers to forward queries to.";
+      stateDir = mkOption {
+        default = "/var/lib/unbound";
+        description = "Directory holding all state for unbound to run.";
+      };
+
+      resolveLocalQueries = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
+          /etc/resolv.conf).
+        '';
       };
 
       enableRootTrustAnchor = mkOption {
@@ -84,16 +81,81 @@ in
         description = "Use and update root trust anchor for DNSSEC validation.";
       };
 
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
+      localControlSocketPath = mkOption {
+        default = null;
+        # FIXME: What is the proper type here so users can specify strings,
+        # paths and null?
+        # My guess would be `types.nullOr (types.either types.str types.path)`
+        # but I haven't verified yet.
+        type = types.nullOr types.str;
+        example = "/run/unbound/unbound.ctl";
         description = ''
-          Extra unbound config. See
-          <citerefentry><refentrytitle>unbound.conf</refentrytitle><manvolnum>8
-          </manvolnum></citerefentry>.
+          When not set to <literal>null</literal> this option defines the path
+          at which the unbound remote control socket should be created at. The
+          socket will be owned by the unbound user (<literal>unbound</literal>)
+          and group will be <literal>nogroup</literal>.
+
+          Users that should be permitted to access the socket must be in the
+          <literal>config.services.unbound.group</literal> group.
+
+          If this option is <literal>null</literal> remote control will not be
+          enabled. Unbounds default values apply.
         '';
       };
 
+      settings = mkOption {
+        default = {};
+        type = with types; submodule {
+
+          freeformType = let
+            validSettingsPrimitiveTypes = oneOf [ int str bool float ];
+            validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
+            settingsType = oneOf [ str (attrsOf validSettingsTypes) ];
+          in attrsOf (oneOf [ settingsType (listOf settingsType) ])
+              // { description = ''
+                unbound.conf configuration type. The format consist of an attribute
+                set of settings. Each settings can be either one value, a list of
+                values or an attribute set. The allowed values are integers,
+                strings, booleans or floats.
+              '';
+            };
+
+          options = {
+            remote-control.control-enable = mkOption {
+              type = bool;
+              default = false;
+              internal = true;
+            };
+          };
+        };
+        example = literalExample ''
+          {
+            server = {
+              interface = [ "127.0.0.1" ];
+            };
+            forward-zone = [
+              {
+                name = ".";
+                forward-addr = "1.1.1.1@853#cloudflare-dns.com";
+              }
+              {
+                name = "example.org.";
+                forward-addr = [
+                  "1.1.1.1@853#cloudflare-dns.com"
+                  "1.0.0.1@853#cloudflare-dns.com"
+                ];
+              }
+            ];
+            remote-control.control-enable = true;
+          };
+        '';
+        description = ''
+          Declarative Unbound configuration
+          See the <citerefentry><refentrytitle>unbound.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> manpage for a list of
+          available options.
+        '';
+      };
     };
   };
 
@@ -101,48 +163,151 @@ in
 
   config = mkIf cfg.enable {
 
+    services.unbound.settings = {
+      server = {
+        directory = mkDefault cfg.stateDir;
+        username = cfg.user;
+        chroot = ''""'';
+        pidfile = ''""'';
+        # when running under systemd there is no need to daemonize
+        do-daemonize = false;
+        interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
+        access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
+        auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
+        tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
+        # prevent race conditions on system startup when interfaces are not yet
+        # configured
+        ip-freebind = mkDefault true;
+        define-tag = mkDefault "";
+      };
+      remote-control = {
+        control-enable = mkDefault false;
+        control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
+        server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
+        server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
+        control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
+        control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
+      } // optionalAttrs (cfg.localControlSocketPath != null) {
+        control-enable = true;
+        control-interface = cfg.localControlSocketPath;
+      };
+    };
+
     environment.systemPackages = [ cfg.package ];
 
-    users.users.unbound = {
-      description = "unbound daemon user";
-      isSystemUser = true;
+    users.users = mkIf (cfg.user == "unbound") {
+      unbound = {
+        description = "unbound daemon user";
+        isSystemUser = true;
+        group = cfg.group;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "unbound") {
+      unbound = {};
+    };
+
+    networking = mkIf cfg.resolveLocalQueries {
+      resolvconf = {
+        useLocalResolver = mkDefault true;
+      };
+
+      networkmanager.dns = "unbound";
     };
 
-    networking.resolvconf.useLocalResolver = mkDefault true;
+    environment.etc."unbound/unbound.conf".source = confFile;
 
     systemd.services.unbound = {
       description = "Unbound recursive Domain Name Server";
       after = [ "network.target" ];
       before = [ "nss-lookup.target" ];
-      wants = [ "nss-lookup.target" ];
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "multi-user.target" "nss-lookup.target" ];
+
+      path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];
 
       preStart = ''
-        mkdir -m 0755 -p ${stateDir}/dev/
-        cp ${confFile} ${stateDir}/unbound.conf
         ${optionalString cfg.enableRootTrustAnchor ''
           ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
-          chown unbound ${stateDir} ${rootTrustAnchorFile}
         ''}
-        touch ${stateDir}/dev/random
-        ${pkgs.utillinux}/bin/mount --bind -n /dev/urandom ${stateDir}/dev/random
+        ${optionalString cfg.settings.remote-control.control-enable ''
+          ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
+        ''}
       '';
 
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/unbound -d -c ${stateDir}/unbound.conf";
-        ExecStopPost="${pkgs.utillinux}/bin/umount ${stateDir}/dev/random";
+      restartTriggers = [
+        confFile
+      ];
 
-        ProtectSystem = true;
-        ProtectHome = true;
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
+        ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
+
+        NotifyAccess = "main";
+        Type = "notify";
+
+        # FIXME: Which of these do we actualy need, can we drop the chroot flag?
+        AmbientCapabilities = [
+          "CAP_NET_BIND_SERVICE"
+          "CAP_NET_RAW"
+          "CAP_SETGID"
+          "CAP_SETUID"
+          "CAP_SYS_CHROOT"
+          "CAP_SYS_RESOURCE"
+        ];
+
+        User = cfg.user;
+        Group = cfg.group;
+
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
         PrivateDevices = true;
-        Restart = "always";
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectSystem = "strict";
+        RuntimeDirectory = "unbound";
+        ConfigurationDirectory = "unbound";
+        StateDirectory = "unbound";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "~@clock"
+          "@cpu-emulation"
+          "@debug"
+          "@keyring"
+          "@module"
+          "mount"
+          "@obsolete"
+          "@resources"
+        ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictSUIDSGID = true;
+
+        Restart = "on-failure";
         RestartSec = "5s";
       };
     };
-
-    # If networkmanager is enabled, ask it to interface with unbound.
-    networking.networkmanager.dns = "unbound";
-
   };
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
+    (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
+      config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
+    ))
+    (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
+      Add a new setting:
+      services.unbound.settings.forward-zone = [{
+        name = ".";
+        forward-addr = [ # Your current services.unbound.forwardAddresses ];
+      }];
+      If any of those addresses are local addresses (127.0.0.1 or ::1), you must
+      also set services.unbound.settings.server.do-not-query-localhost to false.
+    '')
+    (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
+      You can use services.unbound.settings to add any configuration you want.
+    '')
+  ];
 }
diff --git a/nixos/modules/services/networking/wakeonlan.nix b/nixos/modules/services/networking/wakeonlan.nix
index ebfba263cd8..f41b6ec2740 100644
--- a/nixos/modules/services/networking/wakeonlan.nix
+++ b/nixos/modules/services/networking/wakeonlan.nix
@@ -19,7 +19,7 @@ let
     ${ethtool} -s ${interface} ${methodParameter {inherit method password;}}
   '';
 
-  concatStrings = fold (x: y: x + y) "";
+  concatStrings = foldr (x: y: x + y) "";
   lines = concatStrings (map (l: line l) interfaces);
 
 in
@@ -51,6 +51,6 @@ in
 
   ###### implementation
 
-  config.powerManagement.powerDownCommands = lines;
+  config.powerManagement.powerUpCommands = lines;
 
 }
diff --git a/nixos/modules/services/networking/wasabibackend.nix b/nixos/modules/services/networking/wasabibackend.nix
index 6eacffe709b..8482823e197 100644
--- a/nixos/modules/services/networking/wasabibackend.nix
+++ b/nixos/modules/services/networking/wasabibackend.nix
@@ -21,7 +21,7 @@ let
       RegTestBitcoinCoreRpcEndPoint = "${cfg.rpc.ip}:${toString cfg.rpc.port}";
   };
 
-	configFile = pkgs.writeText "wasabibackend.conf" (builtins.toJSON confOptions);
+  configFile = pkgs.writeText "wasabibackend.conf" (builtins.toJSON confOptions);
 
 in {
 
diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
index 02fe40a22a1..3b76de58548 100644
--- a/nixos/modules/services/networking/wg-quick.nix
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -57,7 +57,7 @@ let
 
       preUp = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns add foo
+          ${pkgs.iproute2}/bin/ip netns add foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -68,7 +68,7 @@ let
 
       preDown = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns del foo
+          ${pkgs.iproute2}/bin/ip netns del foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -79,7 +79,7 @@ let
 
       postUp = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns add foo
+          ${pkgs.iproute2}/bin/ip netns add foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -90,7 +90,7 @@ let
 
       postDown = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns del foo
+          ${pkgs.iproute2}/bin/ip netns del foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index e07020349cf..2b51770a5aa 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -63,7 +63,7 @@ let
 
       preSetup = mkOption {
         example = literalExample ''
-          ${pkgs.iproute}/bin/ip netns add foo
+          ${pkgs.iproute2}/bin/ip netns add foo
         '';
         default = "";
         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
@@ -198,7 +198,32 @@ let
         example = "demo.wireguard.io:12913";
         type = with types; nullOr str;
         description = ''Endpoint IP or hostname of the peer, followed by a colon,
-        and then a port number of the peer.'';
+        and then a port number of the peer.
+
+        Warning for endpoints with changing IPs:
+        The WireGuard kernel side cannot perform DNS resolution.
+        Thus DNS resolution is done once by the <literal>wg</literal> userspace
+        utility, when setting up WireGuard. Consequently, if the IP address
+        behind the name changes, WireGuard will not notice.
+        This is especially common for dynamic-DNS setups, but also applies to
+        any other DNS-based setup.
+        If you do not use IP endpoints, you likely want to set
+        <option>networking.wireguard.dynamicEndpointRefreshSeconds</option>
+        to refresh the IPs periodically.
+        '';
+      };
+
+      dynamicEndpointRefreshSeconds = mkOption {
+        default = 0;
+        example = 5;
+        type = with types; int;
+        description = ''
+          Periodically re-execute the <literal>wg</literal> utility every
+          this many seconds in order to let WireGuard notice DNS / hostname
+          changes.
+
+          Setting this to <literal>0</literal> disables periodic reexecution.
+        '';
       };
 
       persistentKeepalive = mkOption {
@@ -219,17 +244,6 @@ let
 
   };
 
-  generatePathUnit = name: values:
-    assert (values.privateKey == null);
-    assert (values.privateKeyFile != null);
-    nameValuePair "wireguard-${name}"
-      {
-        description = "WireGuard Tunnel - ${name} - Private Key";
-        requiredBy = [ "wireguard-${name}.service" ];
-        before = [ "wireguard-${name}.service" ];
-        pathConfig.PathExists = values.privateKeyFile;
-      };
-
   generateKeyServiceUnit = name: values:
     assert values.generatePrivateKeyFile;
     nameValuePair "wireguard-${name}-key"
@@ -238,7 +252,7 @@ let
         wantedBy = [ "wireguard-${name}.service" ];
         requiredBy = [ "wireguard-${name}.service" ];
         before = [ "wireguard-${name}.service" ];
-        path = with pkgs; [ wireguard ];
+        path = with pkgs; [ wireguard-tools ];
 
         serviceConfig = {
           Type = "oneshot";
@@ -246,22 +260,31 @@ let
         };
 
         script = ''
-          mkdir --mode 0644 -p "${dirOf values.privateKeyFile}"
+          set -e
+
+          # If the parent dir does not already exist, create it.
+          # Otherwise, does nothing, keeping existing permisions intact.
+          mkdir -p --mode 0755 "${dirOf values.privateKeyFile}"
+
           if [ ! -f "${values.privateKeyFile}" ]; then
-            touch "${values.privateKeyFile}"
-            chmod 0600 "${values.privateKeyFile}"
-            wg genkey > "${values.privateKeyFile}"
-            chmod 0400 "${values.privateKeyFile}"
+            # Write private key file with atomically-correct permissions.
+            (set -e; umask 077; wg genkey > "${values.privateKeyFile}")
           fi
         '';
       };
 
-  generatePeerUnit = { interfaceName, interfaceCfg, peer }:
+  peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
     let
       keyToUnitName = replaceChars
         [ "/" "-"    " "     "+"     "="      ]
         [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
-      unitName = keyToUnitName peer.publicKey;
+      unitName = keyToUnitName publicKey;
+      refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
+    in
+      "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
+
+  generatePeerUnit = { interfaceName, interfaceCfg, peer }:
+    let
       psk =
         if peer.presharedKey != null
           then pkgs.writeText "wg-psk" peer.presharedKey
@@ -270,7 +293,12 @@ let
       dst = interfaceCfg.interfaceNamespace;
       ip = nsWrap "ip" src dst;
       wg = nsWrap "wg" src dst;
-    in nameValuePair "wireguard-${interfaceName}-peer-${unitName}"
+      dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
+      # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds`
+      # to avoid that the same service switches `Type` (`oneshot` vs `simple`),
+      # with the intent to make scripting more obvious.
+      serviceName = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
+    in nameValuePair serviceName
       {
         description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}";
         requires = [ "wireguard-${interfaceName}.service" ];
@@ -278,38 +306,61 @@ let
         wantedBy = [ "multi-user.target" "wireguard-${interfaceName}.service" ];
         environment.DEVICE = interfaceName;
         environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity";
-        path = with pkgs; [ iproute wireguard-tools ];
-
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-        };
+        path = with pkgs; [ iproute2 wireguard-tools ];
+
+        serviceConfig =
+          if !dynamicRefreshEnabled
+            then
+              {
+                Type = "oneshot";
+                RemainAfterExit = true;
+              }
+            else
+              {
+                Type = "simple"; # re-executes 'wg' indefinitely
+                # Note that `Type = "oneshot"` services with `RemainAfterExit = true`
+                # cannot be used with systemd timers (see `man systemd.timer`),
+                # which is why `simple` with a loop is the best choice here.
+                # It also makes starting and stopping easiest.
+              };
 
         script = let
-          wg_setup = "${wg} set ${interfaceName} peer ${peer.publicKey}" +
-            optionalString (psk != null) " preshared-key ${psk}" +
-            optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
-            optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
-            optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}";
+          wg_setup = concatStringsSep " " (
+            [ ''${wg} set ${interfaceName} peer "${peer.publicKey}"'' ]
+            ++ optional (psk != null) ''preshared-key "${psk}"''
+            ++ optional (peer.endpoint != null) ''endpoint "${peer.endpoint}"''
+            ++ optional (peer.persistentKeepalive != null) ''persistent-keepalive "${toString peer.persistentKeepalive}"''
+            ++ optional (peer.allowedIPs != []) ''allowed-ips "${concatStringsSep "," peer.allowedIPs}"''
+          );
           route_setup =
             optionalString interfaceCfg.allowedIPsAsRoutes
               (concatMapStringsSep "\n"
                 (allowedIP:
-                  "${ip} route replace ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}"
+                  ''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
                 ) peer.allowedIPs);
         in ''
           ${wg_setup}
           ${route_setup}
+
+          ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) ''
+            # Re-execute 'wg' periodically to notice DNS / hostname changes.
+            # Note this will not time out on transient DNS failures such as DNS names
+            # because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'.
+            # Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing.
+            while ${wg_setup}; do
+              sleep "${toString peer.dynamicEndpointRefreshSeconds}";
+            done
+          ''}
         '';
 
         postStop = let
           route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes
             (concatMapStringsSep "\n"
               (allowedIP:
-                "${ip} route delete ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}"
+                ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
               ) peer.allowedIPs);
         in ''
-          ${wg} set ${interfaceName} peer ${peer.publicKey} remove
+          ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove
           ${route_destroy}
         '';
       };
@@ -333,7 +384,7 @@ let
         after = [ "network.target" "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         environment.DEVICE = name;
-        path = with pkgs; [ kmod iproute wireguard-tools ];
+        path = with pkgs; [ kmod iproute2 wireguard-tools ];
 
         serviceConfig = {
           Type = "oneshot";
@@ -345,23 +396,25 @@ let
 
           ${values.preSetup}
 
-          ${ipPreMove} link add dev ${name} type wireguard
-          ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) "${ipPreMove} link set ${name} netns ${ns}"}
+          ${ipPreMove} link add dev "${name}" type wireguard
+          ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''}
 
           ${concatMapStringsSep "\n" (ip:
-            "${ipPostMove} address add ${ip} dev ${name}"
+            ''${ipPostMove} address add "${ip}" dev "${name}"''
           ) values.ips}
 
-          ${wg} set ${name} private-key ${privKey} ${
-            optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}"}
+          ${concatStringsSep " " (
+            [ ''${wg} set "${name}" private-key "${privKey}"'' ]
+            ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"''
+          )}
 
-          ${ipPostMove} link set up dev ${name}
+          ${ipPostMove} link set up dev "${name}"
 
           ${values.postSetup}
         '';
 
         postStop = ''
-          ${ipPostMove} link del dev ${name}
+          ${ipPostMove} link del dev "${name}"
           ${values.postShutdown}
         '';
       };
@@ -371,7 +424,7 @@ let
       nsList = filter (ns: ns != null) [ src dst ];
       ns = last nsList;
     in
-      if (length nsList > 0 && ns != "init") then "ip netns exec ${ns} ${cmd}" else cmd;
+      if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd;
 in
 
 {
@@ -445,9 +498,6 @@ in
       // (mapAttrs' generateKeyServiceUnit
       (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces));
 
-    systemd.paths = mapAttrs' generatePathUnit
-      (filterAttrs (name: value: value.privateKeyFile != null) cfg.interfaces);
-
   });
 
 }
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 08a17d20ed7..c0a4ce40760 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -3,6 +3,10 @@
 with lib;
 
 let
+  package = if cfg.allowAuxiliaryImperativeNetworks
+    then pkgs.wpa_supplicant_ro_ssids
+    else pkgs.wpa_supplicant;
+
   cfg = config.networking.wireless;
   configFile = if cfg.networks != {} || cfg.extraConfig != "" || cfg.userControlled.enable then pkgs.writeText "wpa_supplicant.conf" ''
     ${optionalString cfg.userControlled.enable ''
@@ -14,8 +18,8 @@ let
         then ''"${psk}"''
         else pskRaw;
       baseAuth = if key != null
-        then ''psk=${key}''
-        else ''key_mgmt=NONE'';
+        then "psk=${key}"
+        else "key_mgmt=NONE";
     in ''
       network={
         ssid="${ssid}"
@@ -38,6 +42,11 @@ in {
         description = ''
           The interfaces <command>wpa_supplicant</command> will use. If empty, it will
           automatically use all wireless interfaces.
+          <warning><para>
+            The automatic discovery of interfaces does not work reliably on boot:
+            it may fail and leave the system without network. When possible, specify
+            a known interface name.
+          </para></warning>
         '';
       };
 
@@ -47,6 +56,16 @@ in {
         description = "Force a specific wpa_supplicant driver.";
       };
 
+      allowAuxiliaryImperativeNetworks = mkEnableOption "support for imperative & declarative networks" // {
+        description = ''
+          Whether to allow configuring networks "imperatively" (e.g. via
+          <package>wpa_supplicant_gui</package>) and declaratively via
+          <xref linkend="opt-networking.wireless.networks" />.
+
+          Please note that this adds a custom patch to <package>wpa_supplicant</package>.
+        '';
+      };
+
       networks = mkOption {
         type = types.attrsOf (types.submodule {
           options = {
@@ -211,9 +230,17 @@ in {
       message = ''options networking.wireless."${name}".{psk,pskRaw,auth} are mutually exclusive'';
     });
 
-    environment.systemPackages =  [ pkgs.wpa_supplicant ];
+    warnings =
+      optional (cfg.interfaces == [] && config.systemd.services.wpa_supplicant.wantedBy != [])
+      ''
+        No network interfaces for wpa_supplicant have been configured: the service
+        may randomly fail to start at boot. You should specify at least one using the option
+        networking.wireless.interfaces.
+      '';
+
+    environment.systemPackages = [ package ];
 
-    services.dbus.packages = [ pkgs.wpa_supplicant ];
+    services.dbus.packages = [ package ];
     services.udev.packages = [ pkgs.crda ];
 
     # FIXME: start a separate wpa_supplicant instance per interface.
@@ -230,10 +257,17 @@ in {
       wantedBy = [ "multi-user.target" ];
       stopIfChanged = false;
 
-      path = [ pkgs.wpa_supplicant ];
+      path = [ package ];
 
-      script = ''
-        iface_args="-s -u -D${cfg.driver} -c ${configFile}"
+      script = let
+        configStr = if cfg.allowAuxiliaryImperativeNetworks
+          then "-c /etc/wpa_supplicant.conf -I ${configFile}"
+          else "-c ${configFile}";
+      in ''
+        if [ -f /etc/wpa_supplicant.conf -a "/etc/wpa_supplicant.conf" != "${configFile}" ]
+        then echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead."
+        fi
+        iface_args="-s -u -D${cfg.driver} ${configStr}"
         ${if ifaces == [] then ''
           for i in $(cd /sys/class/net && echo *); do
             DEVTYPE=
diff --git a/nixos/modules/services/networking/x2goserver.nix b/nixos/modules/services/networking/x2goserver.nix
new file mode 100644
index 00000000000..48020fc1cec
--- /dev/null
+++ b/nixos/modules/services/networking/x2goserver.nix
@@ -0,0 +1,161 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.x2goserver;
+
+  defaults = {
+    superenicer = { enable = cfg.superenicer.enable; };
+  };
+  confText = generators.toINI {} (recursiveUpdate defaults cfg.settings);
+  x2goServerConf = pkgs.writeText "x2goserver.conf" confText;
+
+  x2goAgentOptions = pkgs.writeText "x2goagent.options" ''
+    X2GO_NXOPTIONS=""
+    X2GO_NXAGENT_DEFAULT_OPTIONS="${concatStringsSep " " cfg.nxagentDefaultOptions}"
+  '';
+
+in {
+  imports = [
+    (mkRenamedOptionModule [ "programs" "x2goserver" ] [ "services" "x2goserver" ])
+  ];
+
+  options.services.x2goserver = {
+    enable = mkEnableOption "x2goserver" // {
+      description = ''
+        Enables the x2goserver module.
+        NOTE: This will create a good amount of symlinks in `/usr/local/bin`
+      '';
+    };
+
+    superenicer = {
+      enable = mkEnableOption "superenicer" // {
+        description = ''
+          Enables the SupeReNicer code in x2gocleansessions, this will renice
+          suspended sessions to nice level 19 and renice them to level 0 if the
+          session becomes marked as running again
+        '';
+      };
+    };
+
+    nxagentDefaultOptions = mkOption {
+      type = types.listOf types.str;
+      default = [ "-extension GLX" "-nolisten tcp" ];
+      example = [ "-extension GLX" "-nolisten tcp" ];
+      description = ''
+        List of default nx agent options.
+      '';
+    };
+
+    settings = mkOption {
+      type = types.attrsOf types.attrs;
+      default = {};
+      description = ''
+        x2goserver.conf ini configuration as nix attributes. See
+        `x2goserver.conf(5)` for details
+      '';
+      example = literalExample ''
+        superenicer = {
+          "enable" = "yes";
+          "idle-nice-level" = 19;
+        };
+        telekinesis = { "enable" = "no"; };
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    # x2goserver can run X11 program even if "services.xserver.enable = false"
+    xdg = {
+      autostart.enable = true;
+      menus.enable = true;
+      mime.enable = true;
+      icons.enable = true;
+    };
+
+    environment.systemPackages = [ pkgs.x2goserver ];
+
+    users.groups.x2go = {};
+    users.users.x2go = {
+      home = "/var/lib/x2go/db";
+      group = "x2go";
+      isSystemUser = true;
+    };
+
+    security.wrappers.x2gosqliteWrapper = {
+      source = "${pkgs.x2goserver}/lib/x2go/libx2go-server-db-sqlite3-wrapper.pl";
+      owner = "x2go";
+      group = "x2go";
+      setgid = true;
+    };
+    security.wrappers.x2goprintWrapper = {
+      source = "${pkgs.x2goserver}/bin/x2goprint";
+      owner = "x2go";
+      group = "x2go";
+      setgid = true;
+    };
+
+    systemd.tmpfiles.rules = with pkgs; [
+      "d /var/lib/x2go/ - x2go x2go - -"
+      "d /var/lib/x2go/db - x2go x2go - -"
+      "d /var/lib/x2go/conf - x2go x2go - -"
+      "d /run/x2go 0755 x2go x2go - -"
+    ] ++
+    # x2goclient sends SSH commands with preset PATH set to
+    # "/usr/local/bin;/usr/bin;/bin". Since we cannot filter arbitrary ssh
+    # commands, we have to make the following executables available.
+    map (f: "L+ /usr/local/bin/${f} - - - - ${x2goserver}/bin/${f}") [
+      "x2goagent" "x2gobasepath" "x2gocleansessions" "x2gocmdexitmessage"
+      "x2godbadmin" "x2gofeature" "x2gofeaturelist" "x2gofm" "x2gogetapps"
+      "x2gogetservers" "x2golistdesktops" "x2golistmounts" "x2golistsessions"
+      "x2golistsessions_root" "x2golistshadowsessions" "x2gomountdirs"
+      "x2gopath" "x2goprint" "x2goresume-desktopsharing" "x2goresume-session"
+      "x2goruncommand" "x2goserver-run-extensions" "x2gosessionlimit"
+      "x2gosetkeyboard" "x2goshowblocks" "x2gostartagent"
+      "x2gosuspend-desktopsharing" "x2gosuspend-session"
+      "x2goterminate-desktopsharing" "x2goterminate-session"
+      "x2goumount-session" "x2goversion"
+    ] ++ [
+      "L+ /usr/local/bin/awk - - - - ${gawk}/bin/awk"
+      "L+ /usr/local/bin/chmod - - - - ${coreutils}/bin/chmod"
+      "L+ /usr/local/bin/cp - - - - ${coreutils}/bin/cp"
+      "L+ /usr/local/bin/sed - - - - ${gnused}/bin/sed"
+      "L+ /usr/local/bin/setsid - - - - ${util-linux}/bin/setsid"
+      "L+ /usr/local/bin/xrandr - - - - ${xorg.xrandr}/bin/xrandr"
+      "L+ /usr/local/bin/xmodmap - - - - ${xorg.xmodmap}/bin/xmodmap"
+    ];
+
+    systemd.services.x2goserver = {
+      description = "X2Go Server Daemon";
+      wantedBy = [ "multi-user.target" ];
+      unitConfig.Documentation = "man:x2goserver.conf(5)";
+      serviceConfig = {
+        Type = "forking";
+        ExecStart = "${pkgs.x2goserver}/bin/x2gocleansessions";
+        PIDFile = "/run/x2go/x2goserver.pid";
+        User = "x2go";
+        Group = "x2go";
+        RuntimeDirectory = "x2go";
+        StateDirectory = "x2go";
+      };
+      preStart = ''
+        if [ ! -e /var/lib/x2go/setup_ran ]
+        then
+          mkdir -p /var/lib/x2go/conf
+          cp -r ${pkgs.x2goserver}/etc/x2go/* /var/lib/x2go/conf/
+          ln -sf ${x2goServerConf} /var/lib/x2go/conf/x2goserver.conf
+          ln -sf ${x2goAgentOptions} /var/lib/x2go/conf/x2goagent.options
+          ${pkgs.x2goserver}/bin/x2godbadmin --createdb
+          touch /var/lib/x2go/setup_ran
+        fi
+      '';
+    };
+
+    # https://bugs.x2go.org/cgi-bin/bugreport.cgi?bug=276
+    security.sudo.extraConfig = ''
+      Defaults  env_keep+=QT_GRAPHICSSYSTEM
+    '';
+  };
+}
diff --git a/nixos/modules/services/networking/xrdp.nix b/nixos/modules/services/networking/xrdp.nix
index b7dd1c5d99d..9be7c3233e2 100644
--- a/nixos/modules/services/networking/xrdp.nix
+++ b/nixos/modules/services/networking/xrdp.nix
@@ -61,6 +61,12 @@ in
         '';
       };
 
+      openFirewall = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to open the firewall for the specified RDP port.";
+      };
+
       sslKey = mkOption {
         type = types.str;
         default = "/etc/xrdp/key.pem";
@@ -99,6 +105,8 @@ in
 
   config = mkIf cfg.enable {
 
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
     # xrdp can run X11 program even if "services.xserver.enable = false"
     xdg = {
       autostart.enable = true;
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index a71c635c9f6..47a7152f6fe 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -64,7 +64,7 @@ in {
         type = types.str;
         default = "root";
         example = "wheel";
-        description = "Group to grant acces to the Yggdrasil control socket.";
+        description = "Group to grant access to the Yggdrasil control socket.";
       };
 
       openMulticastPort = mkOption {
@@ -122,12 +122,11 @@ in {
     system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
       if [ ! -e ${keysPath} ]
       then
-        mkdir -p ${builtins.dirOf keysPath}
+        mkdir --mode=700 -p ${builtins.dirOf keysPath}
         ${binYggdrasil} -genconf -json \
           | ${pkgs.jq}/bin/jq \
               'to_entries|map(select(.key|endswith("Key")))|from_entries' \
           > ${keysPath}
-        chmod 600 ${keysPath}
       fi
     '';
 
diff --git a/nixos/modules/services/networking/zerobin.nix b/nixos/modules/services/networking/zerobin.nix
index 78de246a816..16db25d6230 100644
--- a/nixos/modules/services/networking/zerobin.nix
+++ b/nixos/modules/services/networking/zerobin.nix
@@ -88,7 +88,7 @@ in
         enable = true;
         after = [ "network.target" ];
         wantedBy = [ "multi-user.target" ];
-        serviceConfig.ExecStart = "${pkgs.pythonPackages.zerobin}/bin/zerobin ${cfg.listenAddress} ${toString cfg.listenPort} false ${cfg.user} ${cfg.group} ${zerobin_config}";
+        serviceConfig.ExecStart = "${pkgs.zerobin}/bin/zerobin ${cfg.listenAddress} ${toString cfg.listenPort} false ${cfg.user} ${cfg.group} ${zerobin_config}";
         serviceConfig.PrivateTmp="yes";
         serviceConfig.User = cfg.user;
         serviceConfig.Group = cfg.group;
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
index a7315896c50..b872b99976c 100644
--- a/nixos/modules/services/networking/znc/default.nix
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -103,8 +103,8 @@ in
       };
 
       dataDir = mkOption {
-        default = "/var/lib/znc/";
-        example = "/home/john/.znc/";
+        default = "/var/lib/znc";
+        example = "/home/john/.znc";
         type = types.path;
         description = ''
           The state directory for ZNC. The config and the modules will be linked
@@ -133,8 +133,8 @@ in
               Nick = "paul";
               AltNick = "paul1";
               LoadModule = [ "chansaver" "controlpanel" ];
-              Network.freenode = {
-                Server = "chat.freenode.net +6697";
+              Network.libera = {
+                Server = "irc.libera.chat +6697";
                 LoadModule = [ "simple_away" ];
                 Chan = {
                   "#nixos" = { Detached = false; };
@@ -258,6 +258,34 @@ in
         ExecStart = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${escapeShellArgs cfg.extraFlags}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+        # Hardening
+        CapabilityBoundingSet = [ "" ];
+        DevicePolicy = "closed";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        PrivateUsers = true;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ReadWritePaths = [ cfg.dataDir ];
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        UMask = "0027";
       };
       preStart = ''
         mkdir -p ${cfg.dataDir}/configs
@@ -271,9 +299,8 @@ in
         # Ensure essential files exist.
         if [[ ! -f ${cfg.dataDir}/configs/znc.conf ]]; then
             echo "No znc.conf file found in ${cfg.dataDir}. Creating one now."
-            cp --no-clobber ${cfg.configFile} ${cfg.dataDir}/configs/znc.conf
+            cp --no-preserve=ownership --no-clobber ${cfg.configFile} ${cfg.dataDir}/configs/znc.conf
             chmod u+rw ${cfg.dataDir}/configs/znc.conf
-            chown ${cfg.user} ${cfg.dataDir}/configs/znc.conf
         fi
 
         if [[ ! -f ${cfg.dataDir}/znc.pem ]]; then
diff --git a/nixos/modules/services/networking/znc/options.nix b/nixos/modules/services/networking/znc/options.nix
index 048dbd73863..be9dc78c86d 100644
--- a/nixos/modules/services/networking/znc/options.nix
+++ b/nixos/modules/services/networking/znc/options.nix
@@ -11,7 +11,7 @@ let
 
       server = mkOption {
         type = types.str;
-        example = "chat.freenode.net";
+        example = "irc.libera.chat";
         description = ''
           IRC server address.
         '';
@@ -44,7 +44,7 @@ let
       modules = mkOption {
         type = types.listOf types.str;
         default = [ "simple_away" ];
-        example = literalExample "[ simple_away sasl ]";
+        example = literalExample ''[ "simple_away" "sasl" ]'';
         description = ''
           ZNC network modules to load.
         '';
@@ -150,8 +150,8 @@ in
           '';
           example = literalExample ''
             {
-              "freenode" = {
-                server = "chat.freenode.net";
+              "libera" = {
+                server = "irc.libera.chat";
                 port = 6697;
                 useSSL = true;
                 modules = [ "simple_away" ];
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index e67badfcd29..d2b36d9e754 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -104,7 +104,7 @@ let
     ignoreCollisions = true;
   };
 
-  filterGutenprint = pkgs: filter (pkg: pkg.meta.isGutenprint or false == true) pkgs;
+  filterGutenprint = filter (pkg: pkg.meta.isGutenprint or false == true);
   containsGutenprint = pkgs: length (filterGutenprint pkgs) > 0;
   getGutenprint = pkgs: head (filterGutenprint pkgs);
 
@@ -270,7 +270,7 @@ in
       drivers = mkOption {
         type = types.listOf types.path;
         default = [];
-        example = literalExample "with pkgs; [ gutenprint hplip splix cups-googlecloudprint ]";
+        example = literalExample "with pkgs; [ gutenprint hplip splix ]";
         description = ''
           CUPS drivers to use. Drivers provided by CUPS, cups-filters,
           Ghostscript and Samba are added unconditionally. If this list contains
diff --git a/nixos/modules/services/scheduling/atd.nix b/nixos/modules/services/scheduling/atd.nix
index cefe72b0e99..37f6651ec4c 100644
--- a/nixos/modules/services/scheduling/atd.nix
+++ b/nixos/modules/services/scheduling/atd.nix
@@ -81,14 +81,9 @@ in
         jobdir=/var/spool/atjobs
         etcdir=/etc/at
 
-        for dir in "$spooldir" "$jobdir" "$etcdir"; do
-          if [ ! -d "$dir" ]; then
-              mkdir -p "$dir"
-              chown atd:atd "$dir"
-          fi
-        done
-        chmod 1770 "$spooldir" "$jobdir"
-        ${if cfg.allowEveryone then ''chmod a+rwxt "$spooldir" "$jobdir" '' else ""}
+        install -dm755 -o atd -g atd "$etcdir"
+        spool_and_job_dir_perms=${if cfg.allowEveryone then "1777" else "1770"}
+        install -dm"$spool_and_job_dir_perms" -o atd -g atd "$spooldir" "$jobdir"
         if [ ! -f "$etcdir"/at.deny ]; then
             touch "$etcdir"/at.deny
             chown root:atd "$etcdir"/at.deny
diff --git a/nixos/modules/services/search/elasticsearch-curator.nix b/nixos/modules/services/search/elasticsearch-curator.nix
index 9620c3e0b6d..bb2612322bb 100644
--- a/nixos/modules/services/search/elasticsearch-curator.nix
+++ b/nixos/modules/services/search/elasticsearch-curator.nix
@@ -55,6 +55,7 @@ in {
     };
     actionYAML = mkOption {
       description = "curator action.yaml file contents, alternatively use curator-cli which takes a simple action command";
+      type = types.lines;
       example = ''
         ---
         actions:
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
index aaf6fb0479b..340cbbf02fb 100644
--- a/nixos/modules/services/security/clamav.nix
+++ b/nixos/modules/services/security/clamav.nix
@@ -8,30 +8,19 @@ let
   cfg = config.services.clamav;
   pkg = pkgs.clamav;
 
-  clamdConfigFile = pkgs.writeText "clamd.conf" ''
-    DatabaseDirectory ${stateDir}
-    LocalSocket ${runDir}/clamd.ctl
-    PidFile ${runDir}/clamd.pid
-    TemporaryDirectory /tmp
-    User clamav
-    Foreground yes
-
-    ${cfg.daemon.extraConfig}
-  '';
-
-  freshclamConfigFile = pkgs.writeText "freshclam.conf" ''
-    DatabaseDirectory ${stateDir}
-    Foreground yes
-    Checks ${toString cfg.updater.frequency}
-
-    ${cfg.updater.extraConfig}
-
-    DatabaseMirror database.clamav.net
-  '';
+  toKeyValue = generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {} " ";
+    listsAsDuplicateKeys = true;
+  };
+
+  clamdConfigFile = pkgs.writeText "clamd.conf" (toKeyValue cfg.daemon.settings);
+  freshclamConfigFile = pkgs.writeText "freshclam.conf" (toKeyValue cfg.updater.settings);
 in
 {
   imports = [
-    (mkRenamedOptionModule [ "services" "clamav" "updater" "config" ] [ "services" "clamav" "updater" "extraConfig" ])
+    (mkRemovedOptionModule [ "services" "clamav" "updater" "config" ] "Use services.clamav.updater.settings instead.")
+    (mkRemovedOptionModule [ "services" "clamav" "updater" "extraConfig" ] "Use services.clamav.updater.settings instead.")
+    (mkRemovedOptionModule [ "services" "clamav" "daemon" "extraConfig" ] "Use services.clamav.daemon.settings instead.")
   ];
 
   options = {
@@ -39,12 +28,12 @@ in
       daemon = {
         enable = mkEnableOption "ClamAV clamd daemon";
 
-        extraConfig = mkOption {
-          type = types.lines;
-          default = "";
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+          default = {};
           description = ''
-            Extra configuration for clamd. Contents will be added verbatim to the
-            configuration file.
+            ClamAV configuration. Refer to <link xlink:href="https://linux.die.net/man/5/clamd.conf"/>,
+            for details on supported values.
           '';
         };
       };
@@ -68,12 +57,12 @@ in
           '';
         };
 
-        extraConfig = mkOption {
-          type = types.lines;
-          default = "";
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+          default = {};
           description = ''
-            Extra configuration for freshclam. Contents will be added verbatim to the
-            configuration file.
+            freshclam configuration. Refer to <link xlink:href="https://linux.die.net/man/5/freshclam.conf"/>,
+            for details on supported values.
           '';
         };
       };
@@ -93,6 +82,22 @@ in
     users.groups.${clamavGroup} =
       { gid = config.ids.gids.clamav; };
 
+    services.clamav.daemon.settings = {
+      DatabaseDirectory = stateDir;
+      LocalSocket = "${runDir}/clamd.ctl";
+      PidFile = "${runDir}/clamd.pid";
+      TemporaryDirectory = "/tmp";
+      User = "clamav";
+      Foreground = true;
+    };
+
+    services.clamav.updater.settings = {
+      DatabaseDirectory = stateDir;
+      Foreground = true;
+      Checks = cfg.updater.frequency;
+      DatabaseMirror = [ "database.clamav.net" ];
+    };
+
     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 3f84f9c2560..499d3466750 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -45,7 +45,12 @@ in
       enable = mkOption {
         default = false;
         type = types.bool;
-        description = "Whether to enable the fail2ban service.";
+        description = ''
+          Whether to enable the fail2ban service.
+
+          See the documentation of <option>services.fail2ban.jails</option>
+          for what jails are enabled by default.
+        '';
       };
 
       package = mkOption {
@@ -62,6 +67,22 @@ in
         description = "The firewall package used by fail2ban service.";
       };
 
+      extraPackages = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = lib.literalExample "[ pkgs.ipset ]";
+        description = ''
+          Extra packages to be made available to the fail2ban service. The example contains
+          the packages needed by the `iptables-ipset-proto6` action.
+        '';
+      };
+
+      maxretry = mkOption {
+        default = 3;
+        type = types.ints.unsigned;
+        description = "Number of failures before a host gets banned.";
+      };
+
       banaction = mkOption {
         default = "iptables-multiport";
         type = types.str;
@@ -205,6 +226,15 @@ in
           defined in <filename>/etc/fail2ban/action.d</filename>,
           while filters are defined in
           <filename>/etc/fail2ban/filter.d</filename>.
+
+          NixOS comes with a default <literal>sshd</literal> jail;
+          for it to work well,
+          <option>services.openssh.logLevel</option> should be set to
+          <literal>"VERBOSE"</literal> or higher so that fail2ban
+          can observe failed login attempts.
+          This module sets it to <literal>"VERBOSE"</literal> if
+          not set otherwise, so enabling fail2ban can make SSH logs
+          more verbose.
         '';
       };
 
@@ -241,9 +271,8 @@ in
       partOf = optional config.networking.firewall.enable "firewall.service";
 
       restartTriggers = [ fail2banConf jailConf pathsConf ];
-      reloadIfChanged = true;
 
-      path = [ cfg.package cfg.packageFirewall pkgs.iproute ];
+      path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
 
       unitConfig.Documentation = "man:fail2ban(1)";
 
@@ -282,22 +311,25 @@ in
     services.fail2ban.jails.DEFAULT = ''
       ${optionalString cfg.bantime-increment.enable ''
         # Bantime incremental
-        bantime.increment    = ${if cfg.bantime-increment.enable then "true" else "false"}
+        bantime.increment    = ${boolToString cfg.bantime-increment.enable}
         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"}
+        bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}
       ''}
       # Miscellaneous options
       ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
-      maxretry    = 3
+      maxretry    = ${toString cfg.maxretry}
       backend     = systemd
       # Actions
       banaction   = ${cfg.banaction}
       banaction_allports = ${cfg.banaction-allports}
     '';
     # Block SSH if there are too many failing connection attempts.
+    # Benefits from verbose sshd logging to observe failed login attempts,
+    # so we set that here unless the user overrode it.
+    services.openssh.logLevel = lib.mkDefault "VERBOSE";
     services.fail2ban.jails.sshd = mkDefault ''
       enabled = true
       port    = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
diff --git a/nixos/modules/services/security/fprintd.nix b/nixos/modules/services/security/fprintd.nix
index cbac4ef05b8..fe0fba5b45d 100644
--- a/nixos/modules/services/security/fprintd.nix
+++ b/nixos/modules/services/security/fprintd.nix
@@ -5,6 +5,7 @@ with lib;
 let
 
   cfg = config.services.fprintd;
+  fprintdPkg = if cfg.tod.enable then pkgs.fprintd-tod else pkgs.fprintd;
 
 in
 
@@ -17,25 +18,30 @@ in
 
     services.fprintd = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to enable fprintd daemon and PAM module for fingerprint readers handling.
-        '';
-      };
+      enable = mkEnableOption "fprintd daemon and PAM module for fingerprint readers handling";
 
       package = mkOption {
         type = types.package;
-        default = pkgs.fprintd;
-        defaultText = "pkgs.fprintd";
+        default = fprintdPkg;
+        defaultText = "if cfg.tod.enable then pkgs.fprintd-tod else pkgs.fprintd";
         description = ''
           fprintd package to use.
         '';
       };
 
-    };
+      tod = {
+
+        enable = mkEnableOption "Touch OEM Drivers library support";
 
+        driver = mkOption {
+          type = types.package;
+          example = literalExample "pkgs.libfprint-2-tod1-goodix";
+          description = ''
+            Touch OEM Drivers (TOD) package to use.
+          '';
+        };
+      };
+    };
   };
 
 
@@ -43,12 +49,16 @@ in
 
   config = mkIf cfg.enable {
 
-    services.dbus.packages = [ pkgs.fprintd ];
+    services.dbus.packages = [ cfg.package ];
 
-    environment.systemPackages = [ pkgs.fprintd ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.packages = [ cfg.package ];
 
+    systemd.services.fprintd.environment = mkIf cfg.tod.enable {
+      FP_TOD_DRIVERS_DIR = "${cfg.tod.driver}${cfg.tod.driver.driverPath}";
+    };
+
   };
 
 }
diff --git a/nixos/modules/services/security/fprot.nix b/nixos/modules/services/security/fprot.nix
index 3a0b08b3c6d..df60d553e85 100644
--- a/nixos/modules/services/security/fprot.nix
+++ b/nixos/modules/services/security/fprot.nix
@@ -16,16 +16,19 @@ in {
           description = ''
             product.data file. Defaults to the one supplied with installation package.
           '';
+          type = types.path;
         };
 
         frequency = mkOption {
           default = 30;
+          type = types.int;
           description = ''
             Update virus definitions every X minutes.
           '';
         };
 
         licenseKeyfile = mkOption {
+          type = types.path;
           description = ''
             License keyfile. Defaults to the one supplied with installation package.
           '';
diff --git a/nixos/modules/services/security/hockeypuck.nix b/nixos/modules/services/security/hockeypuck.nix
new file mode 100644
index 00000000000..686634c8add
--- /dev/null
+++ b/nixos/modules/services/security/hockeypuck.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.hockeypuck;
+  settingsFormat = pkgs.formats.toml { };
+in {
+  meta.maintainers = with lib.maintainers; [ etu ];
+
+  options.services.hockeypuck = {
+    enable = lib.mkEnableOption "Hockeypuck OpenPGP Key Server";
+
+    port = lib.mkOption {
+      default = 11371;
+      type = lib.types.port;
+      description = "HKP port to listen on.";
+    };
+
+    settings = lib.mkOption {
+      type = settingsFormat.type;
+      default = { };
+      example = lib.literalExample ''
+        {
+          hockeypuck = {
+            loglevel = "INFO";
+            logfile = "/var/log/hockeypuck/hockeypuck.log";
+            indexTemplate = "''${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+            vindexTemplate = "''${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+            statsTemplate = "''${pkgs.hockeypuck-web}/share/templates/stats.html.tmpl";
+            webroot = "''${pkgs.hockeypuck-web}/share/webroot";
+
+            hkp.bind = ":''${toString cfg.port}";
+
+            openpgp.db = {
+              driver = "postgres-jsonb";
+              dsn = "database=hockeypuck host=/var/run/postgresql sslmode=disable";
+            };
+          };
+        }
+      '';
+      description = ''
+        Configuration file for hockeypuck, here you can override
+        certain settings (<literal>loglevel</literal> and
+        <literal>openpgp.db.dsn</literal>) by just setting those values.
+
+        For other settings you need to use lib.mkForce to override them.
+
+        This service doesn't provision or enable postgres on your
+        system, it rather assumes that you enable postgres and create
+        the database yourself.
+
+        Example:
+        <literal>
+          services.postgresql = {
+            enable = true;
+            ensureDatabases = [ "hockeypuck" ];
+            ensureUsers = [{
+              name = "hockeypuck";
+              ensurePermissions."DATABASE hockeypuck" = "ALL PRIVILEGES";
+            }];
+          };
+        </literal>
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.hockeypuck.settings.hockeypuck = {
+      loglevel = lib.mkDefault "INFO";
+      logfile = "/var/log/hockeypuck/hockeypuck.log";
+      indexTemplate = "${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+      vindexTemplate = "${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
+      statsTemplate = "${pkgs.hockeypuck-web}/share/templates/stats.html.tmpl";
+      webroot = "${pkgs.hockeypuck-web}/share/webroot";
+
+      hkp.bind = ":${toString cfg.port}";
+
+      openpgp.db = {
+        driver = "postgres-jsonb";
+        dsn = lib.mkDefault "database=hockeypuck host=/var/run/postgresql sslmode=disable";
+      };
+    };
+
+    users.users.hockeypuck = {
+      isSystemUser = true;
+      description = "Hockeypuck user";
+    };
+
+    systemd.services.hockeypuck = {
+      description = "Hockeypuck OpenPGP Key Server";
+      after = [ "network.target" "postgresql.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        WorkingDirectory = "/var/lib/hockeypuck";
+        User = "hockeypuck";
+        ExecStart = "${pkgs.hockeypuck}/bin/hockeypuck -config ${settingsFormat.generate "config.toml" cfg.settings}";
+        Restart = "always";
+        RestartSec = "5s";
+        LogsDirectory = "hockeypuck";
+        LogsDirectoryMode = "0755";
+        StateDirectory = "hockeypuck";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/hologram-agent.nix b/nixos/modules/services/security/hologram-agent.nix
index e37334b3cf5..e29267e5000 100644
--- a/nixos/modules/services/security/hologram-agent.nix
+++ b/nixos/modules/services/security/hologram-agent.nix
@@ -54,5 +54,5 @@ in {
 
   };
 
-  meta.maintainers = with lib.maintainers; [ nand0p ];
+  meta.maintainers = with lib.maintainers; [ ];
 }
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index 2f9e94bd77b..e85fd4b75df 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -90,10 +90,10 @@ in
 
     package = mkOption {
       type = types.package;
-      default = pkgs.oauth2_proxy;
-      defaultText = "pkgs.oauth2_proxy";
+      default = pkgs.oauth2-proxy;
+      defaultText = "pkgs.oauth2-proxy";
       description = ''
-        The package that provides oauth2_proxy.
+        The package that provides oauth2-proxy.
       '';
     };
 
@@ -448,7 +448,7 @@ in
       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
+        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.
       '';
@@ -524,7 +524,7 @@ in
       type = types.nullOr types.str;
       default = null;
       description = ''
-      	Profile access endpoint.
+        Profile access endpoint.
       '';
     };
 
@@ -538,6 +538,7 @@ in
 
     extraConfig = mkOption {
       default = {};
+      type = types.attrsOf types.anything;
       description = ''
         Extra config to pass to oauth2-proxy.
       '';
diff --git a/nixos/modules/services/security/oauth2_proxy_nginx.nix b/nixos/modules/services/security/oauth2_proxy_nginx.nix
index be6734f439f..d82ddb894ea 100644
--- a/nixos/modules/services/security/oauth2_proxy_nginx.nix
+++ b/nixos/modules/services/security/oauth2_proxy_nginx.nix
@@ -23,7 +23,8 @@ in
   config.services.oauth2_proxy = mkIf (cfg.virtualHosts != [] && (hasPrefix "127.0.0.1:" cfg.proxy)) {
     enable = true;
   };
-  config.services.nginx = mkMerge ((optional (cfg.virtualHosts != []) {
+  config.services.nginx = mkIf config.services.oauth2_proxy.enable (mkMerge
+  ((optional (cfg.virtualHosts != []) {
     recommendedProxySettings = true; # needed because duplicate headers
   }) ++ (map (vhost: {
     virtualHosts.${vhost} = {
@@ -31,7 +32,7 @@ in
         proxyPass = cfg.proxy;
         extraConfig = ''
           proxy_set_header X-Scheme                $scheme;
-          proxy_set_header X-Auth-Request-Redirect $request_uri;
+          proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
         '';
       };
       locations."/oauth2/auth" = {
@@ -60,5 +61,5 @@ in
       '';
 
     };
-  }) cfg.virtualHosts));
+  }) cfg.virtualHosts)));
 }
diff --git a/nixos/modules/services/security/physlock.nix b/nixos/modules/services/security/physlock.nix
index 690eb70079d..da5c22a90a0 100644
--- a/nixos/modules/services/security/physlock.nix
+++ b/nixos/modules/services/security/physlock.nix
@@ -52,6 +52,14 @@ in
         '';
       };
 
+      lockMessage = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Message to show on physlock login terminal.
+        '';
+      };
+
       lockOn = {
 
         suspend = mkOption {
@@ -111,7 +119,7 @@ in
                 ++ cfg.lockOn.extraTargets;
         serviceConfig = {
           Type = "forking";
-          ExecStart = "${pkgs.physlock}/bin/physlock -d${optionalString cfg.disableSysRq "s"}";
+          ExecStart = "${pkgs.physlock}/bin/physlock -d${optionalString cfg.disableSysRq "s"}${optionalString (cfg.lockMessage != "") " -p \"${cfg.lockMessage}\""}";
         };
       };
 
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
index c2988858e56..63271848e94 100644
--- a/nixos/modules/services/security/privacyidea.nix
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -7,7 +7,7 @@ let
 
   uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; };
   python = uwsgi.python3;
-  penv = python.withPackages (ps: [ ps.privacyidea ]);
+  penv = python.withPackages (const [ pkgs.privacyidea ]);
   logCfg = pkgs.writeText "privacyidea-log.cfg" ''
     [formatters]
     keys=detail
@@ -57,6 +57,26 @@ in
     services.privacyidea = {
       enable = mkEnableOption "PrivacyIDEA";
 
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/root/privacyidea.env";
+        description = ''
+          File to load as environment file. Environment variables
+          from this file will be interpolated into the config file
+          using <package>envsubst</package> which is helpful for specifying
+          secrets:
+          <programlisting>
+          { <xref linkend="opt-services.privacyidea.secretKey" /> = "$SECRET"; }
+          </programlisting>
+
+          The environment-file can now specify the actual secret key:
+          <programlisting>
+          SECRET=veryverytopsecret
+          </programlisting>
+        '';
+      };
+
       stateDir = mkOption {
         type = types.str;
         default = "/var/lib/privacyidea";
@@ -174,7 +194,7 @@ in
 
     (mkIf cfg.enable {
 
-      environment.systemPackages = [ python.pkgs.privacyidea ];
+      environment.systemPackages = [ pkgs.privacyidea ];
 
       services.postgresql.enable = mkDefault true;
 
@@ -206,7 +226,7 @@ in
         wantedBy = [ "multi-user.target" ];
         after = [ "postgresql.service" ];
         path = with pkgs; [ openssl ];
-        environment.PRIVACYIDEA_CONFIGFILE = piCfgFile;
+        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
         preStart = let
           pi-manage = "${pkgs.sudo}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
           pgsu = config.services.postgresql.superUser;
@@ -214,6 +234,10 @@ in
         in ''
           mkdir -p ${cfg.stateDir} /run/privacyidea
           chown ${cfg.user}:${cfg.group} -R ${cfg.stateDir} /run/privacyidea
+          umask 077
+          ${lib.getBin pkgs.envsubst}/bin/envsubst -o ${cfg.stateDir}/privacyidea.cfg \
+                                                   -i "${piCfgFile}"
+          chown ${cfg.user}:${cfg.group} ${cfg.stateDir}/privacyidea.cfg
           if ! test -e "${cfg.stateDir}/db-created"; then
             ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
             ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
@@ -231,6 +255,7 @@ in
           Type = "notify";
           ExecStart = "${uwsgi}/bin/uwsgi --json ${piuwsgi}";
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
           ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
           NotifyAccess = "main";
           KillSignal = "SIGQUIT";
@@ -239,6 +264,7 @@ in
 
       users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
         group = cfg.group;
+        isSystemUser = true;
       };
 
       users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
@@ -269,6 +295,7 @@ in
 
       users.users.pi-ldap-proxy = mkIf (cfg.ldap-proxy.user == "pi-ldap-proxy") {
         group = cfg.ldap-proxy.group;
+        isSystemUser = true;
       };
 
       users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {};
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
index e7a9cefdef3..53bd9efa5ac 100644
--- a/nixos/modules/services/security/sshguard.nix
+++ b/nixos/modules/services/security/sshguard.nix
@@ -5,6 +5,21 @@ with lib;
 let
   cfg = config.services.sshguard;
 
+  configFile = let
+    args = lib.concatStringsSep " " ([
+      "-afb"
+      "-p info"
+      "-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 pkgs.writeText "sshguard.conf" ''
+    BACKEND="${pkgs.sshguard}/libexec/${backend}"
+    LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
+  '';
+
 in {
 
   ###### interface
@@ -85,20 +100,7 @@ in {
 
   config = mkIf cfg.enable {
 
-    environment.etc."sshguard.conf".text = let
-      args = lib.concatStringsSep " " ([
-        "-afb"
-        "-p info"
-        "-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/${backend}"
-      LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}"
-    '';
+    environment.etc."sshguard.conf".source = configFile;
 
     systemd.services.sshguard = {
       description = "SSHGuard brute-force attacks protection system";
@@ -107,9 +109,11 @@ in {
       after = [ "network.target" ];
       partOf = optional config.networking.firewall.enable "firewall.service";
 
+      restartTriggers = [ configFile ];
+
       path = with pkgs; if config.networking.nftables.enable
-        then [ nftables iproute systemd ]
-        else [ iptables ipset iproute systemd ];
+        then [ nftables iproute2 systemd ]
+        else [ iptables ipset iproute2 systemd ];
 
       # The sshguard ipsets must exist before we invoke
       # iptables. sshguard creates the ipsets after startup if
@@ -119,15 +123,17 @@ in {
       # firewall rules before sshguard starts.
       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
+      '' + optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6
         ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
       '';
 
       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
+      '' + optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
+        ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
         ${pkgs.ipset}/bin/ipset -quiet destroy sshguard6
       '';
 
diff --git a/nixos/modules/services/security/step-ca.nix b/nixos/modules/services/security/step-ca.nix
new file mode 100644
index 00000000000..64eee11f588
--- /dev/null
+++ b/nixos/modules/services/security/step-ca.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.step-ca;
+  settingsFormat = (pkgs.formats.json { });
+in
+{
+  meta.maintainers = with lib.maintainers; [ mohe2015 ];
+
+  options = {
+    services.step-ca = {
+      enable = lib.mkEnableOption "the smallstep certificate authority server";
+      openFirewall = lib.mkEnableOption "opening the certificate authority server port";
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.step-ca;
+        description = "Which step-ca package to use.";
+      };
+      address = lib.mkOption {
+        type = lib.types.str;
+        example = "127.0.0.1";
+        description = ''
+          The address (without port) the certificate authority should listen at.
+          This combined with <option>services.step-ca.port</option> overrides <option>services.step-ca.settings.address</option>.
+        '';
+      };
+      port = lib.mkOption {
+        type = lib.types.port;
+        example = 8443;
+        description = ''
+          The port the certificate authority should listen on.
+          This combined with <option>services.step-ca.address</option> overrides <option>services.step-ca.settings.address</option>.
+        '';
+      };
+      settings = lib.mkOption {
+        type = with lib.types; attrsOf anything;
+        description = ''
+          Settings that go into <filename>ca.json</filename>. See
+          <link xlink:href="https://smallstep.com/docs/step-ca/configuration">
+          the step-ca manual</link> for more information. The easiest way to
+          configure this module would be to run <literal>step ca init</literal>
+          to generate <filename>ca.json</filename> and then import it using
+          <literal>builtins.fromJSON</literal>.
+          <link xlink:href="https://smallstep.com/docs/step-cli/basic-crypto-operations#run-an-offline-x509-certificate-authority">This article</link>
+          may also be useful if you want to customize certain aspects of
+          certificate generation for your CA.
+          You need to change the database storage path to <filename>/var/lib/step-ca/db</filename>.
+
+          <warning>
+            <para>
+              The <option>services.step-ca.settings.address</option> option
+              will be ignored and overwritten by
+              <option>services.step-ca.address</option> and
+              <option>services.step-ca.port</option>.
+            </para>
+          </warning>
+        '';
+      };
+      intermediatePasswordFile = lib.mkOption {
+        type = lib.types.path;
+        example = "/run/keys/smallstep-password";
+        description = ''
+          Path to the file containing the password for the intermediate
+          certificate private key.
+
+          <warning>
+            <para>
+              Make sure to use a quoted absolute path instead of a path literal
+              to prevent it from being copied to the globally readable Nix
+              store.
+            </para>
+          </warning>
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf config.services.step-ca.enable (
+    let
+      configFile = settingsFormat.generate "ca.json" (cfg.settings // {
+        address = cfg.address + ":" + toString cfg.port;
+      });
+    in
+    {
+      assertions =
+        [
+          {
+            assertion = !lib.isStorePath cfg.intermediatePasswordFile;
+            message = ''
+              <option>services.step-ca.intermediatePasswordFile</option> points to
+              a file in the Nix store. You should use a quoted absolute path to
+              prevent this.
+            '';
+          }
+        ];
+
+      systemd.packages = [ cfg.package ];
+
+      # configuration file indirection is needed to support reloading
+      environment.etc."smallstep/ca.json".source = configFile;
+
+      systemd.services."step-ca" = {
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ configFile ];
+        unitConfig = {
+          ConditionFileNotEmpty = ""; # override upstream
+        };
+        serviceConfig = {
+          Environment = "HOME=%S/step-ca";
+          WorkingDirectory = ""; # override upstream
+          ReadWriteDirectories = ""; # override upstream
+
+          # LocalCredential handles file permission problems arising from the use of DynamicUser.
+          LoadCredential = "intermediate_password:${cfg.intermediatePasswordFile}";
+
+          ExecStart = [
+            "" # override upstream
+            "${cfg.package}/bin/step-ca /etc/smallstep/ca.json --password-file \${CREDENTIALS_DIRECTORY}/intermediate_password"
+          ];
+
+          # ProtectProc = "invisible"; # not supported by upstream yet
+          # ProcSubset = "pid"; # not supported by upstream upstream yet
+          # PrivateUsers = true; # doesn't work with privileged ports therefore not supported by upstream
+
+          DynamicUser = true;
+          StateDirectory = "step-ca";
+        };
+      };
+
+      networking.firewall = lib.mkIf cfg.openFirewall {
+        allowedTCPPorts = [ cfg.port ];
+      };
+    }
+  );
+}
diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix
index b33e905c67d..9e8f18e93c8 100644
--- a/nixos/modules/services/security/tor.nix
+++ b/nixos/modules/services/security/tor.nix
@@ -1,301 +1,300 @@
 { config, lib, pkgs, ... }:
 
+with builtins;
 with lib;
 
 let
   cfg = config.services.tor;
-  torDirectory = "/var/lib/tor";
-  torRunDirectory = "/run/tor";
-
-  opt    = name: value: optionalString (value != null) "${name} ${value}";
-  optint = name: value: optionalString (value != null && value != 0)    "${name} ${toString value}";
-
-  isolationOptions = {
-    type = types.listOf (types.enum [
-      "IsolateClientAddr"
-      "IsolateSOCKSAuth"
-      "IsolateClientProtocol"
-      "IsolateDestPort"
-      "IsolateDestAddr"
+  stateDir = "/var/lib/tor";
+  runDir = "/run/tor";
+  descriptionGeneric = option: ''
+    See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en#${option}">torrc manual</link>.
+  '';
+  bindsPrivilegedPort =
+    any (p0:
+      let p1 = if p0 ? "port" then p0.port else p0; in
+      if p1 == "auto" then false
+      else let p2 = if isInt p1 then p1 else toInt p1; in
+        p1 != null && 0 < p2 && p2 < 1024)
+    (flatten [
+      cfg.settings.ORPort
+      cfg.settings.DirPort
+      cfg.settings.DNSPort
+      cfg.settings.ExtORPort
+      cfg.settings.HTTPTunnelPort
+      cfg.settings.NATDPort
+      cfg.settings.SOCKSPort
+      cfg.settings.TransPort
     ]);
+  optionBool = optionName: mkOption {
+    type = with types; nullOr bool;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionInt = optionName: mkOption {
+    type = with types; nullOr int;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionString = optionName: mkOption {
+    type = with types; nullOr str;
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionStrings = optionName: mkOption {
+    type = with types; listOf str;
     default = [];
-    example = [
-      "IsolateClientAddr"
-      "IsolateSOCKSAuth"
-      "IsolateClientProtocol"
-      "IsolateDestPort"
-      "IsolateDestAddr"
+    description = descriptionGeneric optionName;
+  };
+  optionAddress = mkOption {
+    type = with types; nullOr str;
+    default = null;
+    example = "0.0.0.0";
+    description = ''
+      IPv4 or IPv6 (if between brackets) address.
+    '';
+  };
+  optionUnix = mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = ''
+      Unix domain socket path to use.
+    '';
+  };
+  optionPort = mkOption {
+    type = with types; nullOr (oneOf [port (enum ["auto"])]);
+    default = null;
+  };
+  optionPorts = optionName: mkOption {
+    type = with types; listOf port;
+    default = [];
+    description = descriptionGeneric optionName;
+  };
+  optionIsolablePort = with types; oneOf [
+    port (enum ["auto"])
+    (submodule ({config, ...}: {
+      options = {
+        addr = optionAddress;
+        port = optionPort;
+        flags = optionFlags;
+        SessionGroup = mkOption { type = nullOr int; default = null; };
+      } // genAttrs isolateFlags (name: mkOption { type = types.bool; default = false; });
+      config = {
+        flags = filter (name: config.${name} == true) isolateFlags ++
+                optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+      };
+    }))
+  ];
+  optionIsolablePorts = optionName: mkOption {
+    default = [];
+    type = with types; either optionIsolablePort (listOf optionIsolablePort);
+    description = descriptionGeneric optionName;
+  };
+  isolateFlags = [
+    "IsolateClientAddr"
+    "IsolateClientProtocol"
+    "IsolateDestAddr"
+    "IsolateDestPort"
+    "IsolateSOCKSAuth"
+    "KeepAliveIsolateSOCKSAuth"
+  ];
+  optionSOCKSPort = doConfig: let
+    flags = [
+      "CacheDNS" "CacheIPv4DNS" "CacheIPv6DNS" "GroupWritable" "IPv6Traffic"
+      "NoDNSRequest" "NoIPv4Traffic" "NoOnionTraffic" "OnionTrafficOnly"
+      "PreferIPv6" "PreferIPv6Automap" "PreferSOCKSNoAuth" "UseDNSCache"
+      "UseIPv4Cache" "UseIPv6Cache" "WorldWritable"
+    ] ++ isolateFlags;
+    in with types; oneOf [
+      port (submodule ({config, ...}: {
+        options = {
+          unix = optionUnix;
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+          SessionGroup = mkOption { type = nullOr int; default = null; };
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = mkIf doConfig { # Only add flags in SOCKSPort to avoid duplicates
+          flags = filter (name: config.${name} == true) flags ++
+                  optional (config.SessionGroup != null) "SessionGroup=${toString config.SessionGroup}";
+        };
+      }))
     ];
-    description = "Tor isolation options";
+  optionFlags = mkOption {
+    type = with types; listOf str;
+    default = [];
+  };
+  optionORPort = optionName: mkOption {
+    default = [];
+    example = 443;
+    type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+      port
+      (enum ["auto"])
+      (submodule ({config, ...}:
+        let flags = [ "IPv4Only" "IPv6Only" "NoAdvertise" "NoListen" ];
+        in {
+        options = {
+          addr = optionAddress;
+          port = optionPort;
+          flags = optionFlags;
+        } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+        config = {
+          flags = filter (name: config.${name} == true) flags;
+        };
+      }))
+    ]))];
+    description = descriptionGeneric optionName;
+  };
+  optionBandwith = optionName: mkOption {
+    type = with types; nullOr (either int str);
+    default = null;
+    description = descriptionGeneric optionName;
+  };
+  optionPath = optionName: mkOption {
+    type = with types; nullOr path;
+    default = null;
+    description = descriptionGeneric optionName;
   };
 
-
-  torRc = ''
-    User tor
-    DataDirectory ${torDirectory}
-    ${optionalString cfg.enableGeoIP ''
-      GeoIPFile ${pkgs.tor.geoip}/share/tor/geoip
-      GeoIPv6File ${pkgs.tor.geoip}/share/tor/geoip6
-    ''}
-
-    ${optint "ControlPort" cfg.controlPort}
-    ${optionalString cfg.controlSocket.enable "ControlPort unix:${torRunDirectory}/control GroupWritable RelaxDirModeCheck"}
-  ''
-  # Client connection config
-  + optionalString cfg.client.enable ''
-    SOCKSPort ${cfg.client.socksListenAddress} ${toString cfg.client.socksIsolationOptions}
-    SOCKSPort ${cfg.client.socksListenAddressFaster}
-    ${opt "SocksPolicy" cfg.client.socksPolicy}
-
-    ${optionalString cfg.client.transparentProxy.enable ''
-    TransPort ${cfg.client.transparentProxy.listenAddress} ${toString cfg.client.transparentProxy.isolationOptions}
-    ''}
-
-    ${optionalString cfg.client.dns.enable ''
-    DNSPort ${cfg.client.dns.listenAddress} ${toString cfg.client.dns.isolationOptions}
-    AutomapHostsOnResolve 1
-    AutomapHostsSuffixes ${concatStringsSep "," cfg.client.dns.automapHostsSuffixes}
-    ''}
-  ''
-  # Explicitly disable the SOCKS server if the client is disabled.  In
-  # particular, this makes non-anonymous hidden services possible.
-  + optionalString (! cfg.client.enable) ''
-  SOCKSPort 0
-  ''
-  # Relay config
-  + optionalString cfg.relay.enable ''
-    ORPort ${toString cfg.relay.port}
-    ${opt "Address" cfg.relay.address}
-    ${opt "Nickname" cfg.relay.nickname}
-    ${opt "ContactInfo" cfg.relay.contactInfo}
-
-    ${optint "RelayBandwidthRate" cfg.relay.bandwidthRate}
-    ${optint "RelayBandwidthBurst" cfg.relay.bandwidthBurst}
-    ${opt "AccountingMax" cfg.relay.accountingMax}
-    ${opt "AccountingStart" cfg.relay.accountingStart}
-
-    ${if (cfg.relay.role == "exit") then
-        opt "ExitPolicy" cfg.relay.exitPolicy
-      else
-        "ExitPolicy reject *:*"}
-
-    ${optionalString (elem cfg.relay.role ["bridge" "private-bridge"]) ''
-      BridgeRelay 1
-      ServerTransportPlugin ${concatStringsSep "," cfg.relay.bridgeTransports} exec ${pkgs.obfs4}/bin/obfs4proxy managed
-      ExtORPort auto
-      ${optionalString (cfg.relay.role == "private-bridge") ''
-        ExtraInfoStatistics 0
-        PublishServerDescriptor 0
-      ''}
-    ''}
-  ''
-  # Hidden services
-  + concatStrings (flip mapAttrsToList cfg.hiddenServices (n: v: ''
-    HiddenServiceDir ${torDirectory}/onion/${v.name}
-    ${optionalString (v.version != null) "HiddenServiceVersion ${toString v.version}"}
-    ${flip concatMapStrings v.map (p: ''
-      HiddenServicePort ${toString p.port} ${p.destination}
-    '')}
-    ${optionalString (v.authorizeClient != null) ''
-      HiddenServiceAuthorizeClient ${v.authorizeClient.authType} ${concatStringsSep "," v.authorizeClient.clientNames}
-    ''}
-  ''))
-  + cfg.extraConfig;
-
-  torRcFile = pkgs.writeText "torrc" torRc;
-
+  mkValueString = k: v:
+    if v == null then ""
+    else if isBool v then
+      (if v then "1" else "0")
+    else if v ? "unix" && v.unix != null then
+      "unix:"+v.unix +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if v ? "port" && v.port != null then
+      optionalString (v ? "addr" && v.addr != null) "${v.addr}:" +
+      toString v.port +
+      optionalString (v ? "flags") (" " + concatStringsSep " " v.flags)
+    else if k == "ServerTransportPlugin" then
+      optionalString (v.transports != []) "${concatStringsSep "," v.transports} exec ${v.exec}"
+    else if k == "HidServAuth" then
+      v.onion + " " + v.auth
+    else generators.mkValueStringDefault {} v;
+  genTorrc = settings:
+    generators.toKeyValue {
+      listsAsDuplicateKeys = true;
+      mkKeyValue = k: generators.mkKeyValueDefault { mkValueString = mkValueString k; } " " k;
+    }
+    (lib.mapAttrs (k: v:
+      # Not necesssary, but prettier rendering
+      if elem k [ "AutomapHostsSuffixes" "DirPolicy" "ExitPolicy" "SocksPolicy" ]
+      && v != []
+      then concatStringsSep "," v
+      else v)
+    (lib.filterAttrs (k: v: !(v == null || v == ""))
+    settings));
+  torrc = pkgs.writeText "torrc" (
+    genTorrc cfg.settings +
+    concatStrings (mapAttrsToList (name: onion:
+      "HiddenServiceDir ${onion.path}\n" +
+      genTorrc onion.settings) cfg.relay.onionServices)
+  );
 in
 {
   imports = [
-    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "relay" "port" ])
+    (mkRenamedOptionModule [ "services" "tor" "client" "dns" "automapHostsSuffixes" ] [ "services" "tor" "settings" "AutomapHostsSuffixes" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "isolationOptions" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "dns" "listenAddress" ] "Use services.tor.settings.DNSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "privoxy" "enable" ] "Use services.privoxy.enable and services.privoxy.enableTor instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksIsolationOptions" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "socksListenAddressFaster" ] "Use services.tor.settings.SOCKSPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "client" "socksPolicy" ] [ "services" "tor" "settings" "SocksPolicy" ])
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "isolationOptions" ] "Use services.tor.settings.TransPort instead.")
+    (mkRemovedOptionModule [ "services" "tor" "client" "transparentProxy" "listenAddress" ] "Use services.tor.settings.TransPort instead.")
+    (mkRenamedOptionModule [ "services" "tor" "controlPort" ] [ "services" "tor" "settings" "ControlPort" ])
+    (mkRemovedOptionModule [ "services" "tor" "extraConfig" ] "Plese use services.tor.settings instead.")
+    (mkRenamedOptionModule [ "services" "tor" "hiddenServices" ] [ "services" "tor" "relay" "onionServices" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingMax" ] [ "services" "tor" "settings" "AccountingMax" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "accountingStart" ] [ "services" "tor" "settings" "AccountingStart" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "address" ] [ "services" "tor" "settings" "Address" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthBurst" ] [ "services" "tor" "settings" "BandwidthBurst" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bandwidthRate" ] [ "services" "tor" "settings" "BandwidthRate" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "bridgeTransports" ] [ "services" "tor" "settings" "ServerTransportPlugin" "transports" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "contactInfo" ] [ "services" "tor" "settings" "ContactInfo" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "exitPolicy" ] [ "services" "tor" "settings" "ExitPolicy" ])
     (mkRemovedOptionModule [ "services" "tor" "relay" "isBridge" ] "Use services.tor.relay.role instead.")
     (mkRemovedOptionModule [ "services" "tor" "relay" "isExit" ] "Use services.tor.relay.role instead.")
+    (mkRenamedOptionModule [ "services" "tor" "relay" "nickname" ] [ "services" "tor" "settings" "Nickname" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "port" ] [ "services" "tor" "settings" "ORPort" ])
+    (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "settings" "ORPort" ])
   ];
 
   options = {
     services.tor = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable the Tor daemon. By default, the daemon is run without
-          relay, exit, bridge or client connectivity.
-        '';
-      };
-
-      enableGeoIP = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whenever to configure Tor daemon to use GeoIP databases.
-
-          Disabling this will disable by-country statistics for
-          bridges and relays and some client and third-party software
-          functionality.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Extra configuration. Contents will be added verbatim to the
-          configuration file at the end.
-        '';
+      enable = mkEnableOption ''Tor daemon.
+        By default, the daemon is run without
+        relay, exit, bridge or client connectivity'';
+
+      openFirewall = mkEnableOption "opening of the relay port(s) in the firewall";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.tor;
+        defaultText = "pkgs.tor";
+        example = literalExample "pkgs.tor";
+        description = "Tor package to use.";
       };
 
-      controlPort = mkOption {
-        type = types.nullOr (types.either types.int types.str);
-        default = null;
-        example = 9051;
-        description = ''
-          If set, Tor will accept connections on the specified port
-          and allow them to control the tor process.
-        '';
-      };
+      enableGeoIP = mkEnableOption ''use of GeoIP databases.
+        Disabling this will disable by-country statistics for bridges and relays
+        and some client and third-party software functionality'' // { default = true; };
 
-      controlSocket = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable Tor control socket. Control socket is created
-            in <literal>${torRunDirectory}/control</literal>
-          '';
-        };
-      };
+      controlSocket.enable = mkEnableOption ''control socket,
+        created in <literal>${runDir}/control</literal>'';
 
       client = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable Tor daemon to route application
-            connections.  You might want to disable this if you plan
-            running a dedicated Tor relay.
-          '';
-        };
+        enable = mkEnableOption ''the routing of application connections.
+          You might want to disable this if you plan running a dedicated Tor relay'';
 
-        socksListenAddress = mkOption {
-          type = types.str;
-          default = "127.0.0.1:9050";
-          example = "192.168.0.1:9100";
-          description = ''
-            Bind to this address to listen for connections from
-            Socks-speaking applications. Provides strong circuit
-            isolation, separate circuit per IP address.
-          '';
-        };
+        transparentProxy.enable = mkEnableOption "transparent proxy";
+        dns.enable = mkEnableOption "DNS resolver";
 
-        socksListenAddressFaster = mkOption {
-          type = types.str;
-          default = "127.0.0.1:9063";
-          example = "192.168.0.1:9101";
+        socksListenAddress = mkOption {
+          type = optionSOCKSPort false;
+          default = {addr = "127.0.0.1"; port = 9050; IsolateDestAddr = true;};
+          example = {addr = "192.168.0.1"; port = 9090; IsolateDestAddr = true;};
           description = ''
             Bind to this address to listen for connections from
-            Socks-speaking applications. Same as
-            <option>socksListenAddress</option> but uses weaker
-            circuit isolation to provide performance suitable for a
-            web browser.
-           '';
-         };
-
-        socksPolicy = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "accept 192.168.0.0/16, reject *";
-          description = ''
-            Entry policies to allow/deny SOCKS requests based on IP
-            address. First entry that matches wins. If no SocksPolicy
-            is set, we accept all (and only) requests from
-            <option>socksListenAddress</option>.
+            Socks-speaking applications.
           '';
         };
 
-        socksIsolationOptions = mkOption (isolationOptions // {
-          default = ["IsolateDestAddr"];
-        });
-
-        transparentProxy = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = "Whether to enable tor transparent proxy";
-          };
-
-          listenAddress = mkOption {
-            type = types.str;
-            default = "127.0.0.1:9040";
-            example = "192.168.0.1:9040";
-            description = ''
-              Bind transparent proxy to this address.
-            '';
-          };
-
-          isolationOptions = mkOption isolationOptions;
-        };
-
-        dns = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = "Whether to enable tor dns resolver";
-          };
-
-          listenAddress = mkOption {
-            type = types.str;
-            default = "127.0.0.1:9053";
-            example = "192.168.0.1:9053";
-            description = ''
-              Bind tor dns to this address.
-            '';
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" = {
+              clientAuthorizations = ["/run/keys/tor/alice.prv.x25519"];
+            };
           };
-
-          isolationOptions = mkOption isolationOptions;
-
-          automapHostsSuffixes = mkOption {
-            type = types.listOf types.str;
-            default = [".onion" ".exit"];
-            example = [".onion"];
-            description = "List of suffixes to use with automapHostsOnResolve";
-          };
-        };
-
-        privoxy.enable = mkOption {
-          type = types.bool;
-          default = true;
-          description = ''
-            Whether to enable and configure the system Privoxy to use Tor's
-            faster port, suitable for HTTP.
-
-            To have anonymity, protocols need to be scrubbed of identifying
-            information, and this can be accomplished for HTTP by Privoxy.
-
-            Privoxy can also be useful for KDE torification. A good setup would be:
-            setting SOCKS proxy to the default Tor port, providing maximum
-            circuit isolation where possible; and setting HTTP proxy to Privoxy
-            to route HTTP traffic over faster, but less isolated port.
-          '';
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.clientAuthorizations = mkOption {
+              description = ''
+                Clients' authorizations for a v3 onion service,
+                as a list of files containing each one private key, in the format:
+                <screen>descriptor:x25519:&lt;base32-private-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf path;
+              default = [];
+              example = ["/run/keys/tor/alice.prv.x25519"];
+            };
+          }));
         };
       };
 
       relay = {
-        enable = mkOption {
-          type = types.bool;
-          default = false;
-          description = ''
-            Whether to enable relaying TOR traffic for others.
+        enable = mkEnableOption ''relaying of Tor traffic for others.
 
-            See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
-            for details.
+          See <link xlink:href="https://www.torproject.org/docs/tor-doc-relay" />
+          for details.
 
-            Setting this to true requires setting
-            <option>services.tor.relay.role</option>
-            and
-            <option>services.tor.relay.port</option>
-            options.
-          '';
-        };
+          Setting this to true requires setting
+          <option>services.tor.relay.role</option>
+          and
+          <option>services.tor.settings.ORPort</option>
+          options'';
 
         role = mkOption {
           type = types.enum [ "exit" "relay" "bridge" "private-bridge" ];
@@ -314,13 +313,13 @@ in
                 <important><para>
                   Running an exit relay may expose you to abuse
                   complaints. See
-                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies" />
+                  <link xlink:href="https://www.torproject.org/faq.html.en#ExitPolicies"/>
                   for more info.
                 </para></important>
 
                 <para>
                   You can specify which services Tor users may access via
-                  your exit relay using <option>exitPolicy</option> option.
+                  your exit relay using <option>settings.ExitPolicy</option> option.
                 </para>
               </listitem>
             </varlistentry>
@@ -373,15 +372,14 @@ in
                 <important>
                   <para>
                     WARNING: THE FOLLOWING PARAGRAPH IS NOT LEGAL ADVICE.
-                    Consult with your lawer when in doubt.
+                    Consult with your lawyer when in doubt.
                   </para>
 
                   <para>
                     This role should be safe to use in most situations
                     (unless the act of forwarding traffic for others is
                     a punishable offence under your local laws, which
-                    would be pretty insane as it would make ISP
-                    illegal).
+                    would be pretty insane as it would make ISP illegal).
                   </para>
                 </important>
 
@@ -408,7 +406,7 @@ in
 
                 <para>
                   Use this if you want to run a private bridge, for
-                  example because you'll give out your bridge address
+                  example because you'll give out your bridge addr
                   manually to your friends.
                 </para>
 
@@ -430,269 +428,399 @@ in
           '';
         };
 
-        bridgeTransports = mkOption {
-          type = types.listOf types.str;
-          default = ["obfs4"];
-          example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
-          description = "List of pluggable transports";
-        };
-
-        nickname = mkOption {
-          type = types.str;
-          default = "anonymous";
-          description = ''
-            A unique handle for your TOR relay.
-          '';
-        };
-
-        contactInfo = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "admin@relay.com";
-          description = ''
-            Contact information for the relay owner (e.g. a mail
-            address and GPG key ID).
-          '';
-        };
-
-        accountingMax = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "450 GBytes";
-          description = ''
-            Specify maximum bandwidth allowed during an accounting period. This
-            allows you to limit overall tor bandwidth over some time period.
-            See the <literal>AccountingMax</literal> option by looking at the
-            tor manual <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more.
-
-            Note this limit applies individually to upload and
-            download; if you specify <literal>"500 GBytes"</literal>
-            here, then you may transfer up to 1 TBytes of overall
-            bandwidth (500 GB upload, 500 GB download).
-          '';
-        };
-
-        accountingStart = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "month 1 1:00";
-          description = ''
-            Specify length of an accounting period. This allows you to limit
-            overall tor bandwidth over some time period. See the
-            <literal>AccountingStart</literal> option by looking at the tor
-            manual <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more.
-          '';
-        };
-
-        bandwidthRate = mkOption {
-          type = types.nullOr types.int;
-          default = null;
-          example = 100;
-          description = ''
-            Specify this to limit the bandwidth usage of relayed (server)
-            traffic. Your own traffic is still unthrottled. Units: bytes/second.
-          '';
-        };
-
-        bandwidthBurst = mkOption {
-          type = types.nullOr types.int;
-          default = cfg.relay.bandwidthRate;
-          example = 200;
-          description = ''
-            Specify this to allow bursts of the bandwidth usage of relayed (server)
-            traffic. The average usage will still be as specified in relayBandwidthRate.
-            Your own traffic is still unthrottled. Units: bytes/second.
-          '';
-        };
-
-        address = mkOption {
-          type    = types.nullOr types.str;
-          default = null;
-          example = "noname.example.com";
-          description = ''
-            The IP address or full DNS name for advertised address of your relay.
-            Leave unset and Tor will guess.
-          '';
-        };
-
-        port = mkOption {
-          type    = types.either types.int types.str;
-          example = 143;
-          description = ''
-            What port to advertise for Tor connections. This corresponds to the
-            <literal>ORPort</literal> section in the Tor manual; see
-            <citerefentry><refentrytitle>tor</refentrytitle>
-            <manvolnum>1</manvolnum></citerefentry> for more details.
-
-            At a minimum, you should just specify the port for the
-            relay to listen on; a common one like 143, 22, 80, or 443
-            to help Tor users who may have very restrictive port-based
-            firewalls.
-          '';
-        };
-
-        exitPolicy = mkOption {
-          type    = types.nullOr types.str;
-          default = null;
-          example = "accept *:6660-6667,reject *:*";
-          description = ''
-            A comma-separated list of exit policies. They're
-            considered first to last, and the first match wins. If you
-            want to _replace_ the default exit policy, end this with
-            either a reject *:* or an accept *:*. Otherwise, you're
-            _augmenting_ (prepending to) the default exit policy.
-            Leave commented to just use the default, which is
-            available in the man page or at
-            <link xlink:href="https://www.torproject.org/documentation.html" />.
-
-            Look at
-            <link xlink:href="https://www.torproject.org/faq-abuse.html#TypicalAbuses" />
-            for issues you might encounter if you use the default
-            exit policy.
-
-            If certain IPs and ports are blocked externally, e.g. by
-            your firewall, you should update your exit policy to
-            reflect this -- otherwise Tor users will be told that
-            those destinations are down.
-          '';
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "example.org/www" = {
+              map = [ 80 ];
+              authorizedClients = [
+                "descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+              ];
+            };
+          };
+          type = types.attrsOf (types.submodule ({name, config, ...}: {
+            options.path = mkOption {
+              type = types.path;
+              description = ''
+                Path where to store the data files of the hidden service.
+                If the <option>secretKey</option> is null
+                this defaults to <literal>${stateDir}/onion/$onion</literal>,
+                otherwise to <literal>${runDir}/onion/$onion</literal>.
+              '';
+            };
+            options.secretKey = mkOption {
+              type = with types; nullOr path;
+              default = null;
+              example = "/run/keys/tor/onion/expyuzz4wqqyqhjn/hs_ed25519_secret_key";
+              description = ''
+                Secret key of the onion service.
+                If null, Tor reuses any preexisting secret key (in <option>path</option>)
+                or generates a new one.
+                The associated public key and hostname are deterministically regenerated
+                from this file if they do not exist.
+              '';
+            };
+            options.authorizeClient = mkOption {
+              description = descriptionGeneric "HiddenServiceAuthorizeClient";
+              default = null;
+              type = types.nullOr (types.submodule ({...}: {
+                options = {
+                  authType = mkOption {
+                    type = types.enum [ "basic" "stealth" ];
+                    description = ''
+                      Either <literal>"basic"</literal> for a general-purpose authorization protocol
+                      or <literal>"stealth"</literal> for a less scalable protocol
+                      that also hides service activity from unauthorized clients.
+                    '';
+                  };
+                  clientNames = mkOption {
+                    type = with types; nonEmptyListOf (strMatching "[A-Za-z0-9+-_]+");
+                    description = ''
+                      Only clients that are listed here are authorized to access the hidden service.
+                      Generated authorization data can be found in <filename>${stateDir}/onion/$name/hostname</filename>.
+                      Clients need to put this authorization data in their configuration file using
+                      <xref linkend="opt-services.tor.settings.HidServAuth"/>.
+                    '';
+                  };
+                };
+              }));
+            };
+            options.authorizedClients = mkOption {
+              description = ''
+                Authorized clients for a v3 onion service,
+                as a list of public key, in the format:
+                <screen>descriptor:x25519:&lt;base32-public-key&gt;</screen>
+              '' + descriptionGeneric "_client_authorization";
+              type = with types; listOf str;
+              default = [];
+              example = ["descriptor:x25519:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"];
+            };
+            options.map = mkOption {
+              description = descriptionGeneric "HiddenServicePort";
+              type = with types; listOf (oneOf [
+                port (submodule ({...}: {
+                  options = {
+                    port = optionPort;
+                    target = mkOption {
+                      default = null;
+                      type = nullOr (submodule ({...}: {
+                        options = {
+                          unix = optionUnix;
+                          addr = optionAddress;
+                          port = optionPort;
+                        };
+                      }));
+                    };
+                  };
+                }))
+              ]);
+              apply = map (v: if isInt v then {port=v; target=null;} else v);
+            };
+            options.version = mkOption {
+              description = descriptionGeneric "HiddenServiceVersion";
+              type = with types; nullOr (enum [2 3]);
+              default = null;
+            };
+            options.settings = mkOption {
+              description = ''
+                Settings of the onion service.
+              '' + descriptionGeneric "_hidden_service_options";
+              default = {};
+              type = types.submodule {
+                freeformType = with types;
+                  (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+                    description = "settings option";
+                  };
+                options.HiddenServiceAllowUnknownPorts = optionBool "HiddenServiceAllowUnknownPorts";
+                options.HiddenServiceDirGroupReadable = optionBool "HiddenServiceDirGroupReadable";
+                options.HiddenServiceExportCircuitID = mkOption {
+                  description = descriptionGeneric "HiddenServiceExportCircuitID";
+                  type = with types; nullOr (enum ["haproxy"]);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreams = mkOption {
+                  description = descriptionGeneric "HiddenServiceMaxStreams";
+                  type = with types; nullOr (ints.between 0 65535);
+                  default = null;
+                };
+                options.HiddenServiceMaxStreamsCloseCircuit = optionBool "HiddenServiceMaxStreamsCloseCircuit";
+                options.HiddenServiceNumIntroductionPoints = mkOption {
+                  description = descriptionGeneric "HiddenServiceNumIntroductionPoints";
+                  type = with types; nullOr (ints.between 0 20);
+                  default = null;
+                };
+                options.HiddenServiceSingleHopMode = optionBool "HiddenServiceSingleHopMode";
+                options.RendPostPeriod = optionString "RendPostPeriod";
+              };
+            };
+            config = {
+              path = mkDefault ((if config.secretKey == null then stateDir else runDir) + "/onion/${name}");
+              settings.HiddenServiceVersion = config.version;
+              settings.HiddenServiceAuthorizeClient =
+                if config.authorizeClient != null then
+                  config.authorizeClient.authType + " " +
+                  concatStringsSep "," config.authorizeClient.clientNames
+                else null;
+              settings.HiddenServicePort = map (p: mkValueString "" p.port + " " + mkValueString "" p.target) config.map;
+            };
+          }));
         };
       };
 
-      hiddenServices = mkOption {
+      settings = mkOption {
         description = ''
-          A set of static hidden services that terminate their Tor
-          circuits at this node.
-
-          Every element in this set declares a virtual onion host.
-
-          You can specify your onion address by putting corresponding
-          private key to an appropriate place in ${torDirectory}.
-
-          For services without private keys in ${torDirectory} Tor
-          daemon will generate random key pairs (which implies random
-          onion addresses) on restart. The latter could take a while,
-          please be patient.
-
-          <note><para>
-            Hidden services can be useful even if you don't intend to
-            actually <emphasis>hide</emphasis> them, since they can
-            also be seen as a kind of NAT traversal mechanism.
-
-            E.g. the example will make your sshd, whatever runs on
-            "8080" and your mail server available from anywhere where
-            the Tor network is available (which, with the help from
-            bridges, is pretty much everywhere), even if both client
-            and server machines are behind NAT you have no control
-            over.
-          </para></note>
+          See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en">torrc manual</link>
+          for documentation.
         '';
         default = {};
-        example = literalExample ''
-          { "my-hidden-service-example".map = [
-              { port = 22; }                # map ssh port to this machine's ssh
-              { port = 80; toPort = 8080; } # map http port to whatever runs on 8080
-              { port = "sip"; toHost = "mail.example.com"; toPort = "imap"; } # because we can
+        type = types.submodule {
+          freeformType = with types;
+            (attrsOf (nullOr (oneOf [str int bool (listOf str)]))) // {
+              description = "settings option";
+            };
+          options.Address = optionString "Address";
+          options.AssumeReachable = optionBool "AssumeReachable";
+          options.AccountingMax = optionBandwith "AccountingMax";
+          options.AccountingStart = optionString "AccountingStart";
+          options.AuthDirHasIPv6Connectivity = optionBool "AuthDirHasIPv6Connectivity";
+          options.AuthDirListBadExits = optionBool "AuthDirListBadExits";
+          options.AuthDirPinKeys = optionBool "AuthDirPinKeys";
+          options.AuthDirSharedRandomness = optionBool "AuthDirSharedRandomness";
+          options.AuthDirTestEd25519LinkKeys = optionBool "AuthDirTestEd25519LinkKeys";
+          options.AuthoritativeDirectory = optionBool "AuthoritativeDirectory";
+          options.AutomapHostsOnResolve = optionBool "AutomapHostsOnResolve";
+          options.AutomapHostsSuffixes = optionStrings "AutomapHostsSuffixes" // {
+            default = [".onion" ".exit"];
+            example = [".onion"];
+          };
+          options.BandwidthBurst = optionBandwith "BandwidthBurst";
+          options.BandwidthRate = optionBandwith "BandwidthRate";
+          options.BridgeAuthoritativeDir = optionBool "BridgeAuthoritativeDir";
+          options.BridgeRecordUsageByCountry = optionBool "BridgeRecordUsageByCountry";
+          options.BridgeRelay = optionBool "BridgeRelay" // { default = false; };
+          options.CacheDirectory = optionPath "CacheDirectory";
+          options.CacheDirectoryGroupReadable = optionBool "CacheDirectoryGroupReadable"; # default is null and like "auto"
+          options.CellStatistics = optionBool "CellStatistics";
+          options.ClientAutoIPv6ORPort = optionBool "ClientAutoIPv6ORPort";
+          options.ClientDNSRejectInternalAddresses = optionBool "ClientDNSRejectInternalAddresses";
+          options.ClientOnionAuthDir = mkOption {
+            description = descriptionGeneric "ClientOnionAuthDir";
+            default = null;
+            type = with types; nullOr path;
+          };
+          options.ClientPreferIPv6DirPort = optionBool "ClientPreferIPv6DirPort"; # default is null and like "auto"
+          options.ClientPreferIPv6ORPort = optionBool "ClientPreferIPv6ORPort"; # default is null and like "auto"
+          options.ClientRejectInternalAddresses = optionBool "ClientRejectInternalAddresses";
+          options.ClientUseIPv4 = optionBool "ClientUseIPv4";
+          options.ClientUseIPv6 = optionBool "ClientUseIPv6";
+          options.ConnDirectionStatistics = optionBool "ConnDirectionStatistics";
+          options.ConstrainedSockets = optionBool "ConstrainedSockets";
+          options.ContactInfo = optionString "ContactInfo";
+          options.ControlPort = mkOption rec {
+            description = descriptionGeneric "ControlPort";
+            default = [];
+            example = [{port = 9051;}];
+            type = with types; oneOf [port (enum ["auto"]) (listOf (oneOf [
+              port (enum ["auto"]) (submodule ({config, ...}: let
+                flags = ["GroupWritable" "RelaxDirModeCheck" "WorldWritable"];
+                in {
+                options = {
+                  unix = optionUnix;
+                  flags = optionFlags;
+                  addr = optionAddress;
+                  port = optionPort;
+                } // genAttrs flags (name: mkOption { type = types.bool; default = false; });
+                config = {
+                  flags = filter (name: config.${name} == true) flags;
+                };
+              }))
+            ]))];
+          };
+          options.ControlPortFileGroupReadable= optionBool "ControlPortFileGroupReadable";
+          options.ControlPortWriteToFile = optionPath "ControlPortWriteToFile";
+          options.ControlSocket = optionPath "ControlSocket";
+          options.ControlSocketsGroupWritable = optionBool "ControlSocketsGroupWritable";
+          options.CookieAuthFile = optionPath "CookieAuthFile";
+          options.CookieAuthFileGroupReadable = optionBool "CookieAuthFileGroupReadable";
+          options.CookieAuthentication = optionBool "CookieAuthentication";
+          options.DataDirectory = optionPath "DataDirectory" // { default = stateDir; };
+          options.DataDirectoryGroupReadable = optionBool "DataDirectoryGroupReadable";
+          options.DirPortFrontPage = optionPath "DirPortFrontPage";
+          options.DirAllowPrivateAddresses = optionBool "DirAllowPrivateAddresses";
+          options.DormantCanceledByStartup = optionBool "DormantCanceledByStartup";
+          options.DormantOnFirstStartup = optionBool "DormantOnFirstStartup";
+          options.DormantTimeoutDisabledByIdleStreams = optionBool "DormantTimeoutDisabledByIdleStreams";
+          options.DirCache = optionBool "DirCache";
+          options.DirPolicy = mkOption {
+            description = descriptionGeneric "DirPolicy";
+            type = with types; listOf str;
+            default = [];
+            example = ["accept *:*"];
+          };
+          options.DirPort = optionORPort "DirPort";
+          options.DirReqStatistics = optionBool "DirReqStatistics";
+          options.DisableAllSwap = optionBool "DisableAllSwap";
+          options.DisableDebuggerAttachment = optionBool "DisableDebuggerAttachment";
+          options.DisableNetwork = optionBool "DisableNetwork";
+          options.DisableOOSCheck = optionBool "DisableOOSCheck";
+          options.DNSPort = optionIsolablePorts "DNSPort";
+          options.DoSCircuitCreationEnabled = optionBool "DoSCircuitCreationEnabled";
+          options.DoSConnectionEnabled = optionBool "DoSConnectionEnabled"; # default is null and like "auto"
+          options.DoSRefuseSingleHopClientRendezvous = optionBool "DoSRefuseSingleHopClientRendezvous";
+          options.DownloadExtraInfo = optionBool "DownloadExtraInfo";
+          options.EnforceDistinctSubnets = optionBool "EnforceDistinctSubnets";
+          options.EntryStatistics = optionBool "EntryStatistics";
+          options.ExitPolicy = optionStrings "ExitPolicy" // {
+            default = ["reject *:*"];
+            example = ["accept *:*"];
+          };
+          options.ExitPolicyRejectLocalInterfaces = optionBool "ExitPolicyRejectLocalInterfaces";
+          options.ExitPolicyRejectPrivate = optionBool "ExitPolicyRejectPrivate";
+          options.ExitPortStatistics = optionBool "ExitPortStatistics";
+          options.ExitRelay = optionBool "ExitRelay"; # default is null and like "auto"
+          options.ExtORPort = mkOption {
+            description = descriptionGeneric "ExtORPort";
+            default = null;
+            type = with types; nullOr (oneOf [
+              port (enum ["auto"]) (submodule ({...}: {
+                options = {
+                  addr = optionAddress;
+                  port = optionPort;
+                };
+              }))
+            ]);
+            apply = p: if isInt p || isString p then { port = p; } else p;
+          };
+          options.ExtORPortCookieAuthFile = optionPath "ExtORPortCookieAuthFile";
+          options.ExtORPortCookieAuthFileGroupReadable = optionBool "ExtORPortCookieAuthFileGroupReadable";
+          options.ExtendAllowPrivateAddresses = optionBool "ExtendAllowPrivateAddresses";
+          options.ExtraInfoStatistics = optionBool "ExtraInfoStatistics";
+          options.FascistFirewall = optionBool "FascistFirewall";
+          options.FetchDirInfoEarly = optionBool "FetchDirInfoEarly";
+          options.FetchDirInfoExtraEarly = optionBool "FetchDirInfoExtraEarly";
+          options.FetchHidServDescriptors = optionBool "FetchHidServDescriptors";
+          options.FetchServerDescriptors = optionBool "FetchServerDescriptors";
+          options.FetchUselessDescriptors = optionBool "FetchUselessDescriptors";
+          options.ReachableAddresses = optionStrings "ReachableAddresses";
+          options.ReachableDirAddresses = optionStrings "ReachableDirAddresses";
+          options.ReachableORAddresses = optionStrings "ReachableORAddresses";
+          options.GeoIPFile = optionPath "GeoIPFile";
+          options.GeoIPv6File = optionPath "GeoIPv6File";
+          options.GuardfractionFile = optionPath "GuardfractionFile";
+          options.HidServAuth = mkOption {
+            description = descriptionGeneric "HidServAuth";
+            default = [];
+            type = with types; listOf (oneOf [
+              (submodule {
+                options = {
+                  onion = mkOption {
+                    type = strMatching "[a-z2-7]{16}\\.onion";
+                    description = "Onion address.";
+                    example = "xxxxxxxxxxxxxxxx.onion";
+                  };
+                  auth = mkOption {
+                    type = strMatching "[A-Za-z0-9+/]{22}";
+                    description = "Authentication cookie.";
+                  };
+                };
+              })
+            ]);
+            example = [
+              {
+                onion = "xxxxxxxxxxxxxxxx.onion";
+                auth = "xxxxxxxxxxxxxxxxxxxxxx";
+              }
             ];
-          }
-        '';
-        type = types.loaOf (types.submodule ({name, ...}: {
-          options = {
-
-             name = mkOption {
-               type = types.str;
-               description = ''
-                 Name of this tor hidden service.
-
-                 This is purely descriptive.
-
-                 After restarting Tor daemon you should be able to
-                 find your .onion address in
-                 <literal>${torDirectory}/onion/$name/hostname</literal>.
-               '';
-             };
-
-             map = mkOption {
-               default = [];
-               description = "Port mapping for this hidden service.";
-               type = types.listOf (types.submodule ({config, ...}: {
-                 options = {
-
-                   port = mkOption {
-                     type = types.either types.int types.str;
-                     example = 80;
-                     description = ''
-                       Hidden service port to "bind to".
-                     '';
-                   };
-
-                   destination = mkOption {
-                     internal = true;
-                     type = types.str;
-                     description = "Forward these connections where?";
-                   };
-
-                   toHost = mkOption {
-                     type = types.str;
-                     default = "127.0.0.1";
-                     description = "Mapping destination host.";
-                   };
-
-                   toPort = mkOption {
-                     type = types.either types.int types.str;
-                     example = 8080;
-                     description = "Mapping destination port.";
-                   };
-
-                 };
-
-                 config = {
-                   toPort = mkDefault config.port;
-                   destination = mkDefault "${config.toHost}:${toString config.toPort}";
-                 };
-               }));
-             };
-
-             authorizeClient = mkOption {
-               default = null;
-               description = "If configured, the hidden service is accessible for authorized clients only.";
-               type = types.nullOr (types.submodule ({...}: {
-
-                 options = {
-
-                   authType = mkOption {
-                     type = types.enum [ "basic" "stealth" ];
-                     description = ''
-                       Either <literal>"basic"</literal> for a general-purpose authorization protocol
-                       or <literal>"stealth"</literal> for a less scalable protocol
-                       that also hides service activity from unauthorized clients.
-                     '';
-                   };
-
-                   clientNames = mkOption {
-                     type = types.nonEmptyListOf (types.strMatching "[A-Za-z0-9+-_]+");
-                     description = ''
-                       Only clients that are listed here are authorized to access the hidden service.
-                       Generated authorization data can be found in <filename>${torDirectory}/onion/$name/hostname</filename>.
-                       Clients need to put this authorization data in their configuration file using <literal>HidServAuth</literal>.
-                     '';
-                   };
-                 };
-               }));
-             };
-
-             version = mkOption {
-               default = null;
-               description = "Rendezvous service descriptor version to publish for the hidden service. Currently, versions 2 and 3 are supported. (Default: 2)";
-               type = types.nullOr (types.enum [ 2 3 ]);
-             };
           };
-
-          config = {
-            name = mkDefault name;
+          options.HiddenServiceNonAnonymousMode = optionBool "HiddenServiceNonAnonymousMode";
+          options.HiddenServiceStatistics = optionBool "HiddenServiceStatistics";
+          options.HSLayer2Nodes = optionStrings "HSLayer2Nodes";
+          options.HSLayer3Nodes = optionStrings "HSLayer3Nodes";
+          options.HTTPTunnelPort = optionIsolablePorts "HTTPTunnelPort";
+          options.IPv6Exit = optionBool "IPv6Exit";
+          options.KeyDirectory = optionPath "KeyDirectory";
+          options.KeyDirectoryGroupReadable = optionBool "KeyDirectoryGroupReadable";
+          options.LogMessageDomains = optionBool "LogMessageDomains";
+          options.LongLivedPorts = optionPorts "LongLivedPorts";
+          options.MainloopStats = optionBool "MainloopStats";
+          options.MaxAdvertisedBandwidth = optionBandwith "MaxAdvertisedBandwidth";
+          options.MaxCircuitDirtiness = optionInt "MaxCircuitDirtiness";
+          options.MaxClientCircuitsPending = optionInt "MaxClientCircuitsPending";
+          options.NATDPort = optionIsolablePorts "NATDPort";
+          options.NewCircuitPeriod = optionInt "NewCircuitPeriod";
+          options.Nickname = optionString "Nickname";
+          options.ORPort = optionORPort "ORPort";
+          options.OfflineMasterKey = optionBool "OfflineMasterKey";
+          options.OptimisticData = optionBool "OptimisticData"; # default is null and like "auto"
+          options.PaddingStatistics = optionBool "PaddingStatistics";
+          options.PerConnBWBurst = optionBandwith "PerConnBWBurst";
+          options.PerConnBWRate = optionBandwith "PerConnBWRate";
+          options.PidFile = optionPath "PidFile";
+          options.ProtocolWarnings = optionBool "ProtocolWarnings";
+          options.PublishHidServDescriptors = optionBool "PublishHidServDescriptors";
+          options.PublishServerDescriptor = mkOption {
+            description = descriptionGeneric "PublishServerDescriptor";
+            type = with types; nullOr (enum [false true 0 1 "0" "1" "v3" "bridge"]);
+            default = null;
+          };
+          options.ReducedExitPolicy = optionBool "ReducedExitPolicy";
+          options.RefuseUnknownExits = optionBool "RefuseUnknownExits"; # default is null and like "auto"
+          options.RejectPlaintextPorts = optionPorts "RejectPlaintextPorts";
+          options.RelayBandwidthBurst = optionBandwith "RelayBandwidthBurst";
+          options.RelayBandwidthRate = optionBandwith "RelayBandwidthRate";
+          #options.RunAsDaemon
+          options.Sandbox = optionBool "Sandbox";
+          options.ServerDNSAllowBrokenConfig = optionBool "ServerDNSAllowBrokenConfig";
+          options.ServerDNSAllowNonRFC953Hostnames = optionBool "ServerDNSAllowNonRFC953Hostnames";
+          options.ServerDNSDetectHijacking = optionBool "ServerDNSDetectHijacking";
+          options.ServerDNSRandomizeCase = optionBool "ServerDNSRandomizeCase";
+          options.ServerDNSResolvConfFile = optionPath "ServerDNSResolvConfFile";
+          options.ServerDNSSearchDomains = optionBool "ServerDNSSearchDomains";
+          options.ServerTransportPlugin = mkOption {
+            description = descriptionGeneric "ServerTransportPlugin";
+            default = null;
+            type = with types; nullOr (submodule ({...}: {
+              options = {
+                transports = mkOption {
+                  description = "List of pluggable transports.";
+                  type = listOf str;
+                  example = ["obfs2" "obfs3" "obfs4" "scramblesuit"];
+                };
+                exec = mkOption {
+                  type = types.str;
+                  description = "Command of pluggable transport.";
+                };
+              };
+            }));
+          };
+          options.SocksPolicy = optionStrings "SocksPolicy" // {
+            example = ["accept *:*"];
+          };
+          options.SOCKSPort = mkOption {
+            description = descriptionGeneric "SOCKSPort";
+            default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            example = [{port = 9090;}];
+            type = types.listOf (optionSOCKSPort true);
           };
-        }));
+          options.TestingTorNetwork = optionBool "TestingTorNetwork";
+          options.TransPort = optionIsolablePorts "TransPort";
+          options.TransProxyType = mkOption {
+            description = descriptionGeneric "TransProxyType";
+            type = with types; nullOr (enum ["default" "TPROXY" "ipfw" "pf-divert"]);
+            default = null;
+          };
+          #options.TruncateLogFile
+          options.UnixSocksGroupWritable = optionBool "UnixSocksGroupWritable";
+          options.UseDefaultFallbackDirs = optionBool "UseDefaultFallbackDirs";
+          options.UseMicrodescriptors = optionBool "UseMicrodescriptors";
+          options.V3AuthUseLegacyKey = optionBool "V3AuthUseLegacyKey";
+          options.V3AuthoritativeDirectory = optionBool "V3AuthoritativeDirectory";
+          options.VersioningAuthoritativeDirectory = optionBool "VersioningAuthoritativeDirectory";
+          options.VirtualAddrNetworkIPv4 = optionString "VirtualAddrNetworkIPv4";
+          options.VirtualAddrNetworkIPv6 = optionString "VirtualAddrNetworkIPv6";
+          options.WarnPlaintextPorts = optionPorts "WarnPlaintextPorts";
+        };
       };
     };
   };
@@ -700,90 +828,219 @@ in
   config = mkIf cfg.enable {
     # Not sure if `cfg.relay.role == "private-bridge"` helps as tor
     # sends a lot of stats
-    warnings = optional (cfg.relay.enable && cfg.hiddenServices != {})
+    warnings = optional (cfg.settings.BridgeRelay &&
+      flatten (mapAttrsToList (n: o: o.map) cfg.relay.onionServices) != [])
       ''
         Running Tor hidden services on a public relay makes the
         presence of hidden services visible through simple statistical
         analysis of publicly available data.
+        See https://trac.torproject.org/projects/tor/ticket/8742
 
         You can safely ignore this warning if you don't intend to
         actually hide your hidden services. In either case, you can
         always create a container/VM with a separate Tor daemon instance.
-      '';
+      '' ++
+      flatten (mapAttrsToList (n: o:
+        optional (o.settings.HiddenServiceVersion == 2) [
+          (optional (o.settings.HiddenServiceExportCircuitID != null) ''
+            HiddenServiceExportCircuitID is used in the HiddenService: ${n}
+            but this option is only for v3 hidden services.
+          '')
+        ] ++
+        optional (o.settings.HiddenServiceVersion != 2) [
+          (optional (o.settings.HiddenServiceAuthorizeClient != null) ''
+            HiddenServiceAuthorizeClient is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+          (optional (o.settings.RendPostPeriod != null) ''
+            RendPostPeriod is used in the HiddenService: ${n}
+            but this option is only for v2 hidden services.
+          '')
+        ]
+      ) cfg.relay.onionServices);
 
     users.groups.tor.gid = config.ids.gids.tor;
     users.users.tor =
       { description = "Tor Daemon User";
         createHome  = true;
-        home        = torDirectory;
+        home        = stateDir;
         group       = "tor";
         uid         = config.ids.uids.tor;
       };
 
-    # We have to do this instead of using RuntimeDirectory option in
-    # the service below because systemd has no way to set owners of
-    # RuntimeDirectory and putting this into the service below
-    # requires that service to relax it's sandbox since this needs
-    # writable /run
-    systemd.services.tor-init =
-      { description = "Tor Daemon Init";
-        wantedBy = [ "tor.service" ];
-        script = ''
-          install -m 0700 -o tor -g tor -d ${torDirectory} ${torDirectory}/onion
-          install -m 0750 -o tor -g tor -d ${torRunDirectory}
-        '';
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-        };
-      };
+    services.tor.settings = mkMerge [
+      (mkIf cfg.enableGeoIP {
+        GeoIPFile = "${cfg.package.geoip}/share/tor/geoip";
+        GeoIPv6File = "${cfg.package.geoip}/share/tor/geoip6";
+      })
+      (mkIf cfg.controlSocket.enable {
+        ControlPort = [ { unix = runDir + "/control"; GroupWritable=true; RelaxDirModeCheck=true; } ];
+      })
+      (mkIf cfg.relay.enable (
+        optionalAttrs (cfg.relay.role != "exit") {
+          ExitPolicy = mkForce ["reject *:*"];
+        } //
+        optionalAttrs (elem cfg.relay.role ["bridge" "private-bridge"]) {
+          BridgeRelay = true;
+          ExtORPort.port = mkDefault "auto";
+          ServerTransportPlugin.transports = mkDefault ["obfs4"];
+          ServerTransportPlugin.exec = mkDefault "${pkgs.obfs4}/bin/obfs4proxy managed";
+        } // optionalAttrs (cfg.relay.role == "private-bridge") {
+          ExtraInfoStatistics = false;
+          PublishServerDescriptor = false;
+        }
+      ))
+      (mkIf (!cfg.relay.enable) {
+        # Avoid surprises when leaving ORPort/DirPort configurations in cfg.settings,
+        # because it would still enable Tor as a relay,
+        # which can trigger all sort of problems when not carefully done,
+        # like the blocklisting of the machine's IP addresses
+        # by some hosting providers...
+        DirPort = mkForce [];
+        ORPort = mkForce [];
+        PublishServerDescriptor = mkForce false;
+      })
+      (mkIf cfg.client.enable (
+        { SOCKSPort = [ cfg.client.socksListenAddress ];
+        } // optionalAttrs cfg.client.transparentProxy.enable {
+          TransPort = [{ addr = "127.0.0.1"; port = 9040; }];
+        } // optionalAttrs cfg.client.dns.enable {
+          DNSPort = [{ addr = "127.0.0.1"; port = 9053; }];
+          AutomapHostsOnResolve = true;
+        } // optionalAttrs (flatten (mapAttrsToList (n: o: o.clientAuthorizations) cfg.client.onionServices) != []) {
+          ClientOnionAuthDir = runDir + "/ClientOnionAuthDir";
+        }
+      ))
+    ];
 
-    systemd.services.tor =
-      { description = "Tor Daemon";
-        path = [ pkgs.tor ];
-
-        wantedBy = [ "multi-user.target" ];
-        after    = [ "tor-init.service" "network.target" ];
-        restartTriggers = [ torRcFile ];
-
-        serviceConfig =
-          { Type         = "simple";
-            # Translated from the upstream contrib/dist/tor.service.in
-            ExecStartPre = "${pkgs.tor}/bin/tor -f ${torRcFile} --verify-config";
-            ExecStart    = "${pkgs.tor}/bin/tor -f ${torRcFile}";
-            ExecReload   = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-            KillSignal   = "SIGINT";
-            TimeoutSec   = 30;
-            Restart      = "on-failure";
-            LimitNOFILE  = 32768;
-
-            # Hardening
-            # this seems to unshare /run despite what systemd.exec(5) says
-            PrivateTmp              = mkIf (!cfg.controlSocket.enable) "yes";
-            PrivateDevices          = "yes";
-            ProtectHome             = "yes";
-            ProtectSystem           = "strict";
-            InaccessiblePaths       = "/home";
-            ReadOnlyPaths           = "/";
-            ReadWritePaths          = [ torDirectory torRunDirectory ];
-            NoNewPrivileges         = "yes";
-
-            # tor.service.in has this in, but this line it fails to spawn a namespace when using hidden services
-            #CapabilityBoundingSet   = "CAP_SETUID CAP_SETGID CAP_NET_BIND_SERVICE";
-          };
-      };
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts =
+        concatMap (o:
+          if isInt o && o > 0 then [o]
+          else if o ? "port" && isInt o.port && o.port > 0 then [o.port]
+          else []
+        ) (flatten [
+          cfg.settings.ORPort
+          cfg.settings.DirPort
+        ]);
+    };
 
-    environment.systemPackages = [ pkgs.tor ];
-
-    services.privoxy = mkIf (cfg.client.enable && cfg.client.privoxy.enable) {
-      enable = true;
-      extraConfig = ''
-        forward-socks4a / ${cfg.client.socksListenAddressFaster} .
-        toggle  1
-        enable-remote-toggle 0
-        enable-edit-actions 0
-        enable-remote-http-toggle 0
-      '';
+    systemd.services.tor = {
+      description = "Tor Daemon";
+      path = [ pkgs.tor ];
+
+      wantedBy = [ "multi-user.target" ];
+      after    = [ "network.target" ];
+      restartTriggers = [ torrc ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "tor";
+        Group = "tor";
+        ExecStartPre = [
+          "${cfg.package}/bin/tor -f ${torrc} --verify-config"
+          # DOC: Appendix G of https://spec.torproject.org/rend-spec-v3
+          ("+" + pkgs.writeShellScript "ExecStartPre" (concatStringsSep "\n" (flatten (["set -eu"] ++
+            mapAttrsToList (name: onion:
+              optional (onion.authorizedClients != []) ''
+                rm -rf ${escapeShellArg onion.path}/authorized_clients
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path} ${escapeShellArg onion.path}/authorized_clients
+              '' ++
+              imap0 (i: pubKey: ''
+                echo ${pubKey} |
+                install -o tor -g tor -m 0400 /dev/stdin ${escapeShellArg onion.path}/authorized_clients/${toString i}.auth
+              '') onion.authorizedClients ++
+              optional (onion.secretKey != null) ''
+                install -d -o tor -g tor -m 0700 ${escapeShellArg onion.path}
+                key="$(cut -f1 -d: ${escapeShellArg onion.secretKey})"
+                case "$key" in
+                 ("== ed25519v"*"-secret")
+                  install -o tor -g tor -m 0400 ${escapeShellArg onion.secretKey} ${escapeShellArg onion.path}/hs_ed25519_secret_key;;
+                 (*) echo >&2 "NixOS does not (yet) support secret key type for onion: ${name}"; exit 1;;
+                esac
+              ''
+            ) cfg.relay.onionServices ++
+            mapAttrsToList (name: onion: imap0 (i: prvKeyPath:
+              let hostname = removeSuffix ".onion" name; in ''
+              printf "%s:" ${escapeShellArg hostname} | cat - ${escapeShellArg prvKeyPath} |
+              install -o tor -g tor -m 0700 /dev/stdin \
+               ${runDir}/ClientOnionAuthDir/${escapeShellArg hostname}.${toString i}.auth_private
+            '') onion.clientAuthorizations)
+            cfg.client.onionServices
+          ))))
+        ];
+        ExecStart = "${cfg.package}/bin/tor -f ${torrc}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        KillSignal = "SIGINT";
+        TimeoutSec = 30;
+        Restart = "on-failure";
+        LimitNOFILE = 32768;
+        RuntimeDirectory = [
+          # g+x allows access to the control socket
+          "tor"
+          "tor/root"
+          # g+x can't be removed in ExecStart=, but will be removed by Tor
+          "tor/ClientOnionAuthDir"
+        ];
+        RuntimeDirectoryMode = "0710";
+        StateDirectoryMode = "0700";
+        StateDirectory = [
+            "tor"
+            "tor/onion"
+          ] ++
+          flatten (mapAttrsToList (name: onion:
+            optional (onion.secretKey == null) "tor/onion/${name}"
+          ) cfg.relay.onionServices);
+        # The following options are only to optimize:
+        # systemd-analyze security tor
+        RootDirectory = runDir + "/root";
+        RootDirectoryStartOnly = true;
+        #InaccessiblePaths = [ "-+${runDir}/root" ];
+        UMask = "0066";
+        BindPaths = [ stateDir ];
+        BindReadOnlyPaths = [ storeDir "/etc" ];
+        AmbientCapabilities   = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
+        # ProtectClock= adds DeviceAllow=char-rtc r
+        DeviceAllow = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateNetwork = mkDefault false;
+        PrivateTmp = true;
+        # Tor cannot currently bind privileged port when PrivateUsers=true,
+        # see https://gitlab.torproject.org/legacy/trac/-/issues/20930
+        PrivateUsers = !bindsPrivilegedPort;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        # See also the finer but experimental option settings.Sandbox
+        SystemCallFilter = [
+          "@system-service"
+          # Groups in @system-service which do not contain a syscall listed by:
+          # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' tor
+          # in tests, and seem likely not necessary for tor.
+          "~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources" "~@setuid" "~@timer"
+        ];
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+      };
     };
+
+    environment.systemPackages = [ cfg.package ];
   };
+
+  meta.maintainers = with lib.maintainers; [ julm ];
 }
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
index 16a90da5231..4cdb3a041b5 100644
--- a/nixos/modules/services/security/usbguard.nix
+++ b/nixos/modules/services/security/usbguard.nix
@@ -19,13 +19,13 @@ let
     PresentDevicePolicy=${cfg.presentDevicePolicy}
     PresentControllerPolicy=${cfg.presentControllerPolicy}
     InsertedDevicePolicy=${cfg.insertedDevicePolicy}
-    RestoreControllerDeviceState=${if cfg.restoreControllerDeviceState then "true" else "false"}
+    RestoreControllerDeviceState=${boolToString cfg.restoreControllerDeviceState}
     # this does not seem useful for endusers to change
     DeviceManagerBackend=uevent
     IPCAllowedUsers=${concatStringsSep " " cfg.IPCAllowedUsers}
     IPCAllowedGroups=${concatStringsSep " " cfg.IPCAllowedGroups}
     IPCAccessControlFiles=/var/lib/usbguard/IPCAccessControl.d/
-    DeviceRulesWithPort=${if cfg.deviceRulesWithPort then "true" else "false"}
+    DeviceRulesWithPort=${boolToString cfg.deviceRulesWithPort}
     # HACK: that way audit logs still land in the journal
     AuditFilePath=/dev/null
   '';
@@ -173,7 +173,7 @@ in
 
       serviceConfig = {
         Type = "simple";
-        ExecStart = ''${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}'';
+        ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
         Restart = "on-failure";
 
         StateDirectory = [
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
index 6a8a3a93327..5a20f6413b1 100644
--- a/nixos/modules/services/security/vault.nix
+++ b/nixos/modules/services/security/vault.nix
@@ -27,6 +27,11 @@ let
       ''}
     ${cfg.extraConfig}
   '';
+
+  allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
+
+  configOptions = escapeShellArgs (concatMap (p: ["-config" p]) allConfigPaths);
+
 in
 
 {
@@ -84,7 +89,14 @@ in
       storageConfig = mkOption {
         type = types.nullOr types.lines;
         default = null;
-        description = "Storage configuration";
+        description = ''
+          HCL configuration to insert in the storageBackend section.
+
+          Confidential values should not be specified here because this option's
+          value is written to the Nix store, which is publicly readable.
+          Provide credentials and such in a separate file using
+          <xref linkend="opt-services.vault.extraSettingsPaths"/>.
+        '';
       };
 
       telemetryConfig = mkOption {
@@ -98,6 +110,36 @@ in
         default = "";
         description = "Extra text appended to <filename>vault.hcl</filename>.";
       };
+
+      extraSettingsPaths = mkOption {
+        type = types.listOf types.path;
+        default = [];
+        description = ''
+          Configuration files to load besides the immutable one defined by the NixOS module.
+          This can be used to avoid putting credentials in the Nix store, which can be read by any user.
+
+          Each path can point to a JSON- or HCL-formatted file, or a directory
+          to be scanned for files with <literal>.hcl</literal> or
+          <literal>.json</literal> extensions.
+
+          To upload the confidential file with NixOps, use for example:
+
+          <programlisting><![CDATA[
+          # https://releases.nixos.org/nixops/latest/manual/manual.html#opt-deployment.keys
+          deployment.keys."vault.hcl" = let db = import ./db-credentials.nix; in {
+            text = ${"''"}
+              storage "postgresql" {
+                connection_url = "postgres://''${db.username}:''${db.password}@host.example.com/exampledb?sslmode=verify-ca"
+              }
+            ${"''"};
+            user = "vault";
+          };
+          services.vault.extraSettingsPaths = ["/run/keys/vault.hcl"];
+          services.vault.storageBackend = "postgresql";
+          users.users.vault.extraGroups = ["keys"];
+          ]]></programlisting>
+        '';
+      };
     };
   };
 
@@ -131,10 +173,12 @@ in
 
       restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
 
+      startLimitIntervalSec = 60;
+      startLimitBurst = 3;
       serviceConfig = {
         User = "vault";
         Group = "vault";
-        ExecStart = "${cfg.package}/bin/vault server -config ${configFile}";
+        ExecStart = "${cfg.package}/bin/vault server ${configOptions}";
         ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
         PrivateDevices = true;
         PrivateTmp = true;
@@ -145,8 +189,6 @@ in
         KillSignal = "SIGINT";
         TimeoutStopSec = "30s";
         Restart = "on-failure";
-        StartLimitInterval = "60s";
-        StartLimitBurst = 3;
       };
 
       unitConfig.RequiresMountsFor = optional (cfg.storagePath != null) cfg.storagePath;
diff --git a/nixos/modules/services/security/bitwarden_rs/backup.sh b/nixos/modules/services/security/vaultwarden/backup.sh
index 264a7da9cbb..2a3de0ab1de 100644
--- a/nixos/modules/services/security/bitwarden_rs/backup.sh
+++ b/nixos/modules/services/security/vaultwarden/backup.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-# Based on: https://github.com/dani-garcia/bitwarden_rs/wiki/Backing-up-your-vault
+# Based on: https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault
 if ! mkdir -p "$BACKUP_FOLDER"; then
   echo "Could not create backup folder '$BACKUP_FOLDER'" >&2
   exit 1
diff --git a/nixos/modules/services/security/bitwarden_rs/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index 903a5327037..d28ea61e66a 100644
--- a/nixos/modules/services/security/bitwarden_rs/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -3,9 +3,9 @@
 with lib;
 
 let
-  cfg = config.services.bitwarden_rs;
-  user = config.users.users.bitwarden_rs.name;
-  group = config.users.groups.bitwarden_rs.name;
+  cfg = config.services.vaultwarden;
+  user = config.users.users.vaultwarden.name;
+  group = config.users.groups.vaultwarden.name;
 
   # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
   nameToEnvVar = name:
@@ -26,22 +26,26 @@ let
         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";
+      WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
     } // configEnv;
 
-  configFile = pkgs.writeText "bitwarden_rs.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
+  configFile = pkgs.writeText "vaultwarden.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
 
-  bitwarden_rs = pkgs.bitwarden_rs.override { inherit (cfg) dbBackend; };
+  vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
 
 in {
-  options.services.bitwarden_rs = with types; {
-    enable = mkEnableOption "bitwarden_rs";
+  imports = [
+    (mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
+  ];
+
+  options.services.vaultwarden = with types; {
+    enable = mkEnableOption "vaultwarden";
 
     dbBackend = mkOption {
       type = enum [ "sqlite" "mysql" "postgresql" ];
       default = "sqlite";
       description = ''
-        Which database backend bitwarden_rs will be using.
+        Which database backend vaultwarden will be using.
       '';
     };
 
@@ -49,7 +53,7 @@ in {
       type = nullOr str;
       default = null;
       description = ''
-        The directory under which bitwarden_rs will backup its persistent data.
+        The directory under which vaultwarden will backup its persistent data.
       '';
     };
 
@@ -65,7 +69,7 @@ in {
         }
       '';
       description = ''
-        The configuration of bitwarden_rs is done through environment variables,
+        The configuration of vaultwarden is done through environment variables,
         therefore the names are converted from camel case (e.g. disable2FARemember)
         to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
         In this conversion digits (0-9) are handled just like upper case characters,
@@ -75,12 +79,43 @@ in {
         This allows working around any potential future conflicting naming conventions.
 
         Based on the attributes passed to this config option an environment file will be generated
-        that is passed to bitwarden_rs's systemd service.
+        that is passed to vaultwarden's systemd service.
 
         The available configuration options can be found in
-        <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/${bitwarden_rs.version}/.env.template">the environment template file</link>.
+        <link xlink:href="https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template">the environment template file</link>.
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/root/vaultwarden.env";
+      description = ''
+        Additional environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets like <envar>ADMIN_TOKEN</envar> and <envar>SMTP_PASSWORD</envar>
+        may be passed to the service without adding them to the world-readable Nix store.
+
+        Note that this file needs to be available on the host on which
+        <literal>vaultwarden</literal> is running.
       '';
     };
+
+    package = mkOption {
+      type = package;
+      default = pkgs.vaultwarden;
+      defaultText = "pkgs.vaultwarden";
+      description = "Vaultwarden package to use.";
+    };
+
+    webVaultPackage = mkOption {
+      type = package;
+      default = pkgs.vaultwarden-vault;
+      defaultText = "pkgs.vaultwarden-vault";
+      description = "Web vault package to use.";
+    };
   };
 
   config = mkIf cfg.enable {
@@ -89,22 +124,22 @@ in {
       message = "Backups for database backends other than sqlite will need customization";
     } ];
 
-    users.users.bitwarden_rs = {
+    users.users.vaultwarden = {
       inherit group;
       isSystemUser = true;
     };
-    users.groups.bitwarden_rs = { };
+    users.groups.vaultwarden = { };
 
-    systemd.services.bitwarden_rs = {
+    systemd.services.vaultwarden = {
+      aliases = [ "bitwarden_rs" ];
       after = [ "network.target" ];
       path = with pkgs; [ openssl ];
       serviceConfig = {
         User = user;
         Group = group;
-        EnvironmentFile = configFile;
-        ExecStart = "${bitwarden_rs}/bin/bitwarden_rs";
+        EnvironmentFile = [ configFile ] ++ optional (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${vaultwarden}/bin/vaultwarden";
         LimitNOFILE = "1048576";
-        LimitNPROC = "64";
         PrivateTmp = "true";
         PrivateDevices = "true";
         ProtectHome = "true";
@@ -115,15 +150,16 @@ in {
       wantedBy = [ "multi-user.target" ];
     };
 
-    systemd.services.backup-bitwarden_rs = mkIf (cfg.backupDir != null) {
-      description = "Backup bitwarden_rs";
+    systemd.services.backup-vaultwarden = mkIf (cfg.backupDir != null) {
+      aliases = [ "backup-bitwarden_rs" ];
+      description = "Backup vaultwarden";
       environment = {
         DATA_FOLDER = "/var/lib/bitwarden_rs";
         BACKUP_FOLDER = cfg.backupDir;
       };
       path = with pkgs; [ sqlite ];
       serviceConfig = {
-        SyslogIdentifier = "backup-bitwarden_rs";
+        SyslogIdentifier = "backup-vaultwarden";
         Type = "oneshot";
         User = mkDefault user;
         Group = mkDefault group;
@@ -132,12 +168,13 @@ in {
       wantedBy = [ "multi-user.target" ];
     };
 
-    systemd.timers.backup-bitwarden_rs = mkIf (cfg.backupDir != null) {
-      description = "Backup bitwarden_rs on time";
+    systemd.timers.backup-vaultwarden = mkIf (cfg.backupDir != null) {
+      aliases = [ "backup-bitwarden_rs" ];
+      description = "Backup vaultwarden on time";
       timerConfig = {
         OnCalendar = mkDefault "23:00";
         Persistent = "true";
-        Unit = "backup-bitwarden_rs.service";
+        Unit = "backup-vaultwarden.service";
       };
       wantedBy = [ "multi-user.target" ];
     };
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index 15fe822aec6..eb82b738e49 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -5,11 +5,11 @@ with lib;
 let cfg = config.services.cloud-init;
     path = with pkgs; [
       cloud-init
-      iproute
+      iproute2
       nettools
       openssh
       shadow
-      utillinux
+      util-linux
     ] ++ optional cfg.btrfs.enable btrfs-progs
       ++ optional cfg.ext4.enable e2fsprogs
     ;
@@ -98,7 +98,7 @@ in
            - final-message
            - power-state-change
           '';
-        description = ''cloud-init configuration.'';
+        description = "cloud-init configuration.";
       };
 
     };
diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix
index 4a60fec1ca8..d4cacb85694 100644
--- a/nixos/modules/services/system/dbus.nix
+++ b/nixos/modules/services/system/dbus.nix
@@ -1,6 +1,6 @@
 # D-Bus configuration and system bus daemon.
 
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -11,6 +11,7 @@ let
   homeDir = "/run/dbus";
 
   configDir = pkgs.makeDBusConf {
+    inherit (cfg) apparmor;
     suidHelper = "${config.security.wrapperDir}/dbus-daemon-launch-helper";
     serviceDirectories = cfg.packages;
   };
@@ -18,7 +19,6 @@ let
 in
 
 {
-
   ###### interface
 
   options = {
@@ -52,11 +52,26 @@ in
         '';
       };
 
+      apparmor = mkOption {
+        type = types.enum [ "enabled" "disabled" "required" ];
+        description = ''
+          AppArmor mode for dbus.
+
+          <literal>enabled</literal> enables mediation when it's
+          supported in the kernel, <literal>disabled</literal>
+          always disables AppArmor even with kernel support, and
+          <literal>required</literal> fails when AppArmor was not found
+          in the kernel.
+        '';
+        default = "disabled";
+      };
+
       socketActivated = mkOption {
-        type = types.bool;
-        default = false;
+        type = types.nullOr types.bool;
+        default = null;
+        visible = false;
         description = ''
-          Make the user instance socket activated.
+          Removed option, do not use.
         '';
       };
     };
@@ -65,6 +80,14 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    warnings = optional (cfg.socketActivated != null) (
+      let
+        files = showFiles options.services.dbus.socketActivated.files;
+      in
+        "The option 'services.dbus.socketActivated' in ${files} no longer has"
+        + " any effect and can be safely removed: the user D-Bus session is"
+        + " now always socket activated."
+    );
 
     environment.systemPackages = [ pkgs.dbus.daemon pkgs.dbus ];
 
@@ -108,7 +131,7 @@ in
         reloadIfChanged = true;
         restartTriggers = [ configDir ];
       };
-      sockets.dbus.wantedBy = mkIf cfg.socketActivated [ "sockets.target" ];
+      sockets.dbus.wantedBy = [ "sockets.target" ];
     };
 
     environment.pathsToLink = [ "/etc/dbus-1" "/share/dbus-1" ];
diff --git a/nixos/modules/services/system/localtime.nix b/nixos/modules/services/system/localtime.nix
index 8f8e2e2e933..bb99e5e36ff 100644
--- a/nixos/modules/services/system/localtime.nix
+++ b/nixos/modules/services/system/localtime.nix
@@ -29,15 +29,14 @@ in {
       };
     };
 
-    # We use the 'out' output, since localtime has its 'bin' output
-    # first, so that is what we get if we use the derivation bare.
     # Install the polkit rules.
-    environment.systemPackages = [ pkgs.localtime.out ];
+    environment.systemPackages = [ pkgs.localtime ];
     # Install the systemd unit.
-    systemd.packages = [ pkgs.localtime.out ];
+    systemd.packages = [ pkgs.localtime ];
 
     users.users.localtimed = {
-      description = "Taskserver user";
+      description = "localtime daemon";
+      isSystemUser = true;
     };
 
     systemd.services.localtime = {
diff --git a/nixos/modules/services/system/self-deploy.nix b/nixos/modules/services/system/self-deploy.nix
new file mode 100644
index 00000000000..33d15e08f4a
--- /dev/null
+++ b/nixos/modules/services/system/self-deploy.nix
@@ -0,0 +1,172 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.self-deploy;
+
+  workingDirectory = "/var/lib/nixos-self-deploy";
+  repositoryDirectory = "${workingDirectory}/repo";
+  outPath = "${workingDirectory}/system";
+
+  gitWithRepo = "git -C ${repositoryDirectory}";
+
+  renderNixArgs = args:
+    let
+      toArg = key: value:
+        if builtins.isString value
+        then " --argstr ${lib.escapeShellArg key} ${lib.escapeShellArg value}"
+        else " --arg ${lib.escapeShellArg key} ${lib.escapeShellArg (toString value)}";
+    in
+    lib.concatStrings (lib.mapAttrsToList toArg args);
+
+  isPathType = x: lib.strings.isCoercibleToString x && builtins.substring 0 1 (toString x) == "/";
+
+in
+{
+  options.services.self-deploy = {
+    enable = lib.mkEnableOption "self-deploy";
+
+    nixFile = lib.mkOption {
+      type = lib.types.path;
+
+      default = "/default.nix";
+
+      description = ''
+        Path to nix file in repository. Leading '/' refers to root of
+        git repository.
+      '';
+    };
+
+    nixAttribute = lib.mkOption {
+      type = with lib.types; nullOr str;
+
+      default = null;
+
+      description = ''
+        Attribute of `nixFile` that builds the current system.
+      '';
+    };
+
+    nixArgs = lib.mkOption {
+      type = lib.types.attrs;
+
+      default = { };
+
+      description = ''
+        Arguments to `nix-build` passed as `--argstr` or `--arg` depending on
+        the type.
+      '';
+    };
+
+    switchCommand = lib.mkOption {
+      type = lib.types.enum [ "boot" "switch" "dry-activate" "test" ];
+
+      default = "switch";
+
+      description = ''
+        The `switch-to-configuration` subcommand used.
+      '';
+    };
+
+    repository = lib.mkOption {
+      type = with lib.types; oneOf [ path str ];
+
+      description = ''
+        The repository to fetch from. Must be properly formatted for git.
+
+        If this value is set to a path (must begin with `/`) then it's
+        assumed that the repository is local and the resulting service
+        won't wait for the network to be up.
+
+        If the repository will be fetched over SSH, you must add an
+        entry to `programs.ssh.knownHosts` for the SSH host for the fetch
+        to be successful.
+      '';
+    };
+
+    sshKeyFile = lib.mkOption {
+      type = with lib.types; nullOr path;
+
+      default = null;
+
+      description = ''
+        Path to SSH private key used to fetch private repositories over
+        SSH.
+      '';
+    };
+
+    branch = lib.mkOption {
+      type = lib.types.str;
+
+      default = "master";
+
+      description = ''
+        Branch to track
+
+        Technically speaking any ref can be specified here, as this is
+        passed directly to a `git fetch`, but for the use-case of
+        continuous deployment you're likely to want to specify a branch.
+      '';
+    };
+
+    startAt = lib.mkOption {
+      type = with lib.types; either str (listOf str);
+
+      default = "hourly";
+
+      description = ''
+        The schedule on which to run the `self-deploy` service. Format
+        specified by `systemd.time 7`.
+
+        This value can also be a list of `systemd.time 7` formatted
+        strings, in which case the service will be started on multiple
+        schedules.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.self-deploy = {
+      wantedBy = [ "multi-user.target" ];
+
+      requires = lib.mkIf (!(isPathType cfg.repository)) [ "network-online.target" ];
+
+      environment.GIT_SSH_COMMAND = lib.mkIf (!(isNull cfg.sshKeyFile))
+        "${pkgs.openssh}/bin/ssh -i ${lib.escapeShellArg cfg.sshKeyFile}";
+
+      restartIfChanged = false;
+
+      path = with pkgs; [
+        git
+        nix
+        systemd
+      ];
+
+      script = ''
+        if [ ! -e ${repositoryDirectory} ]; then
+          mkdir --parents ${repositoryDirectory}
+          git init ${repositoryDirectory}
+        fi
+
+        ${gitWithRepo} fetch ${lib.escapeShellArg cfg.repository} ${lib.escapeShellArg cfg.branch}
+
+        ${gitWithRepo} checkout FETCH_HEAD
+
+        nix-build${renderNixArgs cfg.nixArgs} ${lib.cli.toGNUCommandLineShell { } {
+          attr = cfg.nixAttribute;
+          out-link = outPath;
+        }} ${lib.escapeShellArg "${repositoryDirectory}${cfg.nixFile}"}
+
+        ${lib.optionalString (cfg.switchCommand != "test")
+          "nix-env --profile /nix/var/nix/profiles/system --set ${outPath}"}
+
+        ${outPath}/bin/switch-to-configuration ${cfg.switchCommand}
+
+        rm ${outPath}
+
+        ${gitWithRepo} gc --prune=all
+
+        ${lib.optionalString (cfg.switchCommand == "boot") "systemctl reboot"}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/torrent/deluge.nix b/nixos/modules/services/torrent/deluge.nix
index 45398cb2613..7ca4fdcf64d 100644
--- a/nixos/modules/services/torrent/deluge.nix
+++ b/nixos/modules/services/torrent/deluge.nix
@@ -41,6 +41,7 @@ in {
 
         openFilesLimit = mkOption {
           default = openFilesLimit;
+          type = types.either types.int types.str;
           description = ''
             Number of files to allow deluged to open.
           '';
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 014a22bb5a8..34a5219c959 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.transmission;
   inherit (config.environment) etc;
-  apparmor = config.security.apparmor.enable;
+  apparmor = config.security.apparmor;
   rootDir = "/run/transmission";
   homeDir = "/var/lib/transmission";
   settingsDir = ".config/transmission-daemon";
@@ -184,8 +184,8 @@ in
 
     systemd.services.transmission = {
       description = "Transmission BitTorrent Service";
-      after = [ "network.target" ] ++ optional apparmor "apparmor.service";
-      requires = optional apparmor "apparmor.service";
+      after = [ "network.target" ] ++ optional apparmor.enable "apparmor.service";
+      requires = optional apparmor.enable "apparmor.service";
       wantedBy = [ "multi-user.target" ];
       environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
 
@@ -197,7 +197,7 @@ in
           install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' /dev/stdin \
            '${cfg.home}/${settingsDir}/settings.json'
         '')];
-        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f";
+        ExecStart="${pkgs.transmission}/bin/transmission-daemon -f -g ${cfg.home}/${settingsDir}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = cfg.user;
         Group = cfg.group;
@@ -236,6 +236,7 @@ in
           # an AppArmor profile is provided to get a confinement based upon paths and rights.
           builtins.storeDir
           "/etc"
+          "/run"
           ] ++
           optional (cfg.settings.script-torrent-done-enabled &&
                     cfg.settings.script-torrent-done-filename != "")
@@ -357,94 +358,39 @@ in
       })
     ];
 
-    security.apparmor.profiles = mkIf apparmor [
-      (pkgs.writeText "apparmor-transmission-daemon" ''
-        include <tunables/global>
-
-        ${pkgs.transmission}/bin/transmission-daemon {
-          include <abstractions/base>
-          include <abstractions/nameservice>
-
-          # NOTE: https://github.com/NixOS/nixpkgs/pull/93457
-          # will remove the need for these by fixing <abstractions/base>
-          r ${etc."hosts".source},
-          r /etc/ld-nix.so.preload,
-          ${lib.optionalString (builtins.hasAttr "ld-nix.so.preload" etc) ''
-            r ${etc."ld-nix.so.preload".source},
-            ${concatMapStrings (p: optionalString (p != "") ("mr ${p},\n"))
-              (splitString "\n" config.environment.etc."ld-nix.so.preload".text)}
-          ''}
-          r ${etc."ssl/certs/ca-certificates.crt".source},
-          r ${pkgs.tzdata}/share/zoneinfo/**,
-          r ${pkgs.stdenv.cc.libc}/share/i18n/**,
-          r ${pkgs.stdenv.cc.libc}/share/locale/**,
-
-          mr ${getLib pkgs.stdenv.cc.cc}/lib/*.so*,
-          mr ${getLib pkgs.stdenv.cc.libc}/lib/*.so*,
-          mr ${getLib pkgs.attr}/lib/libattr*.so*,
-          mr ${getLib pkgs.c-ares}/lib/libcares*.so*,
-          mr ${getLib pkgs.curl}/lib/libcurl*.so*,
-          mr ${getLib pkgs.keyutils}/lib/libkeyutils*.so*,
-          mr ${getLib pkgs.libcap}/lib/libcap*.so*,
-          mr ${getLib pkgs.libevent}/lib/libevent*.so*,
-          mr ${getLib pkgs.libgcrypt}/lib/libgcrypt*.so*,
-          mr ${getLib pkgs.libgpgerror}/lib/libgpg-error*.so*,
-          mr ${getLib pkgs.libkrb5}/lib/lib*.so*,
-          mr ${getLib pkgs.libssh2}/lib/libssh2*.so*,
-          mr ${getLib pkgs.lz4}/lib/liblz4*.so*,
-          mr ${getLib pkgs.nghttp2}/lib/libnghttp2*.so*,
-          mr ${getLib pkgs.openssl}/lib/libcrypto*.so*,
-          mr ${getLib pkgs.openssl}/lib/libssl*.so*,
-          mr ${getLib pkgs.systemd}/lib/libsystemd*.so*,
-          mr ${getLib pkgs.utillinuxMinimal.out}/lib/libblkid.so*,
-          mr ${getLib pkgs.utillinuxMinimal.out}/lib/libmount.so*,
-          mr ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so*,
-          mr ${getLib pkgs.xz}/lib/liblzma*.so*,
-          mr ${getLib pkgs.zlib}/lib/libz*.so*,
-
-          r @{PROC}/sys/kernel/random/uuid,
-          r @{PROC}/sys/vm/overcommit_memory,
-          # @{pid} is not a kernel variable yet but a regexp
-          #r @{PROC}/@{pid}/environ,
-          r @{PROC}/@{pid}/mounts,
-          rwk /tmp/tr_session_id_*,
-
-          r ${pkgs.openssl.out}/etc/**,
-          r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
-          r ${pkgs.transmission}/share/transmission/**,
-
-          owner rw ${cfg.home}/${settingsDir}/**,
-          rw ${cfg.settings.download-dir}/**,
-          ${optionalString cfg.settings.incomplete-dir-enabled ''
-            rw ${cfg.settings.incomplete-dir}/**,
-          ''}
-          ${optionalString cfg.settings.watch-dir-enabled ''
-            rw ${cfg.settings.watch-dir}/**,
-          ''}
-          profile dirs {
-            rw ${cfg.settings.download-dir}/**,
-            ${optionalString cfg.settings.incomplete-dir-enabled ''
-              rw ${cfg.settings.incomplete-dir}/**,
-            ''}
-            ${optionalString cfg.settings.watch-dir-enabled ''
-              rw ${cfg.settings.watch-dir}/**,
-            ''}
-          }
-
-          ${optionalString (cfg.settings.script-torrent-done-enabled &&
-                            cfg.settings.script-torrent-done-filename != "") ''
-            # Stack transmission_directories profile on top of
-            # any existing profile for script-torrent-done-filename
-            # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
-            # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
-            px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
-          ''}
+    security.apparmor.policies."bin.transmission-daemon".profile = ''
+      include "${pkgs.transmission.apparmor}/bin.transmission-daemon"
+    '';
+    security.apparmor.includes."local/bin.transmission-daemon" = ''
+      r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
+
+      owner rw ${cfg.home}/${settingsDir}/**,
+      rw ${cfg.settings.download-dir}/**,
+      ${optionalString cfg.settings.incomplete-dir-enabled ''
+        rw ${cfg.settings.incomplete-dir}/**,
+      ''}
+      ${optionalString cfg.settings.watch-dir-enabled ''
+        rw ${cfg.settings.watch-dir}/**,
+      ''}
+      profile dirs {
+        rw ${cfg.settings.download-dir}/**,
+        ${optionalString cfg.settings.incomplete-dir-enabled ''
+          rw ${cfg.settings.incomplete-dir}/**,
+        ''}
+        ${optionalString cfg.settings.watch-dir-enabled ''
+          rw ${cfg.settings.watch-dir}/**,
+        ''}
+      }
 
-          # FIXME: enable customizing using https://github.com/NixOS/nixpkgs/pull/93457
-          # include <local/transmission-daemon>
-        }
-      '')
-    ];
+      ${optionalString (cfg.settings.script-torrent-done-enabled &&
+                        cfg.settings.script-torrent-done-filename != "") ''
+        # Stack transmission_directories profile on top of
+        # any existing profile for script-torrent-done-filename
+        # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
+        # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
+        px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
+      ''}
+    '';
   };
 
   meta.maintainers = with lib.maintainers; [ julm ];
diff --git a/nixos/modules/services/ttys/agetty.nix b/nixos/modules/services/ttys/getty.nix
index f3a629f7af7..7cf2ff87da2 100644
--- a/nixos/modules/services/ttys/agetty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -3,9 +3,18 @@
 with lib;
 
 let
+  cfg = config.services.getty;
 
-  autologinArg = optionalString (config.services.mingetty.autologinUser != null) "--autologin ${config.services.mingetty.autologinUser}";
-  gettyCmd = extraArgs: "@${pkgs.utillinux}/sbin/agetty agetty --login-program ${pkgs.shadow}/bin/login ${autologinArg} ${extraArgs}";
+  baseArgs = [
+    "--login-program" "${cfg.loginProgram}"
+  ] ++ optionals (cfg.autologinUser != null) [
+    "--autologin" cfg.autologinUser
+  ] ++ optionals (cfg.loginOptions != null) [
+    "--login-options" cfg.loginOptions
+  ] ++ cfg.extraArgs;
+
+  gettyCmd = args:
+    "@${pkgs.util-linux}/sbin/agetty agetty ${escapeShellArgs baseArgs} ${args}";
 
 in
 
@@ -13,9 +22,13 @@ in
 
   ###### interface
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "mingetty" ] [ "services" "getty" ])
+  ];
+
   options = {
 
-    services.mingetty = {
+    services.getty = {
 
       autologinUser = mkOption {
         type = types.nullOr types.str;
@@ -26,10 +39,44 @@ in
         '';
       };
 
+      loginProgram = mkOption {
+        type = types.path;
+        default = "${pkgs.shadow}/bin/login";
+        description = ''
+          Path to the login binary executed by agetty.
+        '';
+      };
+
+      loginOptions = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Template for arguments to be passed to
+          <citerefentry><refentrytitle>login</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry>.
+
+          See <citerefentry><refentrytitle>agetty</refentrytitle>
+          <manvolnum>1</manvolnum></citerefentry> for details,
+          including security considerations.  If unspecified, agetty
+          will not be invoked with a <option>--login-options</option>
+          option.
+        '';
+        example = "-h darkstar -- \\u";
+      };
+
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        description = ''
+          Additional arguments passed to agetty.
+        '';
+        example = [ "--nohostname" ];
+      };
+
       greetingLine = mkOption {
         type = types.str;
         description = ''
-          Welcome line printed by mingetty.
+          Welcome line printed by agetty.
           The default shows current NixOS version label, machine type and tty.
         '';
       };
@@ -38,7 +85,7 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Help line printed by mingetty below the welcome line.
+          Help line printed by agetty below the welcome line.
           Used by the installation CD to give some hints on
           how to proceed.
         '';
@@ -65,7 +112,7 @@ in
   config = {
     # Note: this is set here rather than up there so that changing
     # nixos.label would not rebuild manual pages
-    services.mingetty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
+    services.getty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
 
     systemd.services."getty@" =
       { serviceConfig.ExecStart = [
@@ -76,10 +123,10 @@ in
       };
 
     systemd.services."serial-getty@" =
-      let speeds = concatStringsSep "," (map toString config.services.mingetty.serialSpeed); in
+      let speeds = concatStringsSep "," (map toString config.services.getty.serialSpeed); in
       { serviceConfig.ExecStart = [
           "" # override upstream default with an empty ExecStart
-          (gettyCmd "%I ${speeds} $TERM")
+          (gettyCmd "%I --keep-baud ${speeds} $TERM")
         ];
         restartIfChanged = false;
       };
@@ -106,8 +153,8 @@ in
       { # Friendly greeting on the virtual consoles.
         source = pkgs.writeText "issue" ''
 
-          ${config.services.mingetty.greetingLine}
-          ${config.services.mingetty.helpLine}
+          ${config.services.getty.greetingLine}
+          ${config.services.getty.helpLine}
 
         '';
       };
diff --git a/nixos/modules/services/ttys/kmscon.nix b/nixos/modules/services/ttys/kmscon.nix
index dc37f9bee4b..4fe720bf044 100644
--- a/nixos/modules/services/ttys/kmscon.nix
+++ b/nixos/modules/services/ttys/kmscon.nix
@@ -82,11 +82,8 @@ in {
       X-RestartIfChanged=false
     '';
 
-    systemd.units."autovt@.service".unit = pkgs.runCommand "unit" { preferLocalBuild = true; }
-        ''
-          mkdir -p $out
-          ln -s ${config.systemd.units."kmsconvt@.service".unit}/kmsconvt@.service $out/autovt@.service
-        '';
+    systemd.suppressedSystemUnits = [ "autovt@.service" ];
+    systemd.units."kmsconvt@.service".aliases = [ "autovt@.service" ];
 
     systemd.services.systemd-vconsole-setup.enable = false;
 
diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix
new file mode 100644
index 00000000000..b13393c8983
--- /dev/null
+++ b/nixos/modules/services/video/epgstation/default.nix
@@ -0,0 +1,295 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.epgstation;
+
+  username = config.users.users.epgstation.name;
+  groupname = config.users.users.epgstation.group;
+
+  settingsFmt = pkgs.formats.json {};
+  settingsTemplate = settingsFmt.generate "config.json" cfg.settings;
+  preStartScript = pkgs.writeScript "epgstation-prestart" ''
+    #!${pkgs.runtimeShell}
+
+    PASSWORD="$(head -n1 "${cfg.basicAuth.passwordFile}")"
+    DB_PASSWORD="$(head -n1 "${cfg.database.passwordFile}")"
+
+    # setup configuration
+    touch /etc/epgstation/config.json
+    chmod 640 /etc/epgstation/config.json
+    sed \
+      -e "s,@password@,$PASSWORD,g" \
+      -e "s,@dbPassword@,$DB_PASSWORD,g" \
+      ${settingsTemplate} > /etc/epgstation/config.json
+    chown "${username}:${groupname}" /etc/epgstation/config.json
+
+    # NOTE: Use password authentication, since mysqljs does not yet support auth_socket
+    if [ ! -e /var/lib/epgstation/db-created ]; then
+      ${pkgs.mariadb}/bin/mysql -e \
+        "GRANT ALL ON \`${cfg.database.name}\`.* TO '${username}'@'localhost' IDENTIFIED by '$DB_PASSWORD';"
+      touch /var/lib/epgstation/db-created
+    fi
+  '';
+
+  streamingConfig = builtins.fromJSON (builtins.readFile ./streaming.json);
+  logConfig = {
+    appenders.stdout.type = "stdout";
+    categories = {
+      default = { appenders = [ "stdout" ]; level = "info"; };
+      system = { appenders = [ "stdout" ]; level = "info"; };
+      access = { appenders = [ "stdout" ]; level = "info"; };
+      stream = { appenders = [ "stdout" ]; level = "info"; };
+    };
+  };
+
+  defaultPassword = "INSECURE_GO_CHECK_CONFIGURATION_NIX\n";
+in
+{
+  options.services.epgstation = {
+    enable = mkEnableOption pkgs.epgstation.meta.description;
+
+    usePreconfiguredStreaming = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Use preconfigured default streaming options.
+
+        Upstream defaults:
+        <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.sample.json"/>
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 20772;
+      description = ''
+        HTTP port for EPGStation to listen on.
+      '';
+    };
+
+    socketioPort = mkOption {
+      type = types.port;
+      default = cfg.port + 1;
+      description = ''
+        Socket.io port for EPGStation to listen on.
+      '';
+    };
+
+    clientSocketioPort = mkOption {
+      type = types.port;
+      default = cfg.socketioPort;
+      description = ''
+        Socket.io port that the web client is going to connect to. This may be
+        different from <option>socketioPort</option> if EPGStation is hidden
+        behind a reverse proxy.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Open ports in the firewall for the EPGStation web interface.
+
+        <warning>
+          <para>
+            Exposing EPGStation to the open internet is generally advised
+            against. Only use it inside a trusted local network, or consider
+            putting it behind a VPN if you want remote access.
+          </para>
+        </warning>
+      '';
+    };
+
+    basicAuth = {
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "epgstation";
+        description = ''
+          Basic auth username for EPGStation. If <literal>null</literal>, basic
+          auth will be disabled.
+
+          <warning>
+            <para>
+              Basic authentication has known weaknesses, the most critical being
+              that it sends passwords over the network in clear text. Use this
+              feature to control access to EPGStation within your family and
+              friends, but don't rely on it for security.
+            </para>
+          </warning>
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.path;
+        default = pkgs.writeText "epgstation-password" defaultPassword;
+        example = "/run/keys/epgstation-password";
+        description = ''
+          A file containing the password for <option>basicAuth.user</option>.
+        '';
+      };
+    };
+
+    database =  {
+      name = mkOption {
+        type = types.str;
+        default = "epgstation";
+        description = ''
+          Name of the MySQL database that holds EPGStation's data.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.path;
+        default = pkgs.writeText "epgstation-db-password" defaultPassword;
+        example = "/run/keys/epgstation-db-password";
+        description = ''
+          A file containing the password for the database named
+          <option>database.name</option>.
+        '';
+      };
+    };
+
+    settings = mkOption {
+      description = ''
+        Options to add to config.json.
+
+        Documentation:
+        <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/>
+      '';
+
+      default = {};
+      example = {
+        recPriority = 20;
+        conflictPriority = 10;
+      };
+
+      type = types.submodule {
+        freeformType = settingsFmt.type;
+
+        options.readOnlyOnce = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Don't reload configuration files at runtime.";
+        };
+
+        options.mirakurunPath = mkOption (let
+          sockPath = config.services.mirakurun.unixSocket;
+        in {
+          type = types.str;
+          default = "http+unix://${replaceStrings ["/"] ["%2F"] sockPath}";
+          example = "http://localhost:40772";
+          description = "URL to connect to Mirakurun.";
+        });
+
+        options.encode = mkOption {
+          type = with types; listOf attrs;
+          description = "Encoding presets for recorded videos.";
+          default = [
+            { name = "H264";
+              cmd = "${pkgs.epgstation}/libexec/enc.sh main";
+              suffix = ".mp4";
+              default = true; }
+            { name = "H264-sub";
+              cmd = "${pkgs.epgstation}/libexec/enc.sh sub";
+              suffix = "-sub.mp4"; }
+          ];
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc = {
+      "epgstation/operatorLogConfig.json".text = builtins.toJSON logConfig;
+      "epgstation/serviceLogConfig.json".text = builtins.toJSON logConfig;
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = with cfg; [ port socketioPort ];
+    };
+
+    users.users.epgstation = {
+      description = "EPGStation user";
+      group = config.users.groups.epgstation.name;
+      isSystemUser = true;
+    };
+
+    users.groups.epgstation = {};
+
+    services.mirakurun.enable = mkDefault true;
+
+    services.mysql = {
+      enable = mkDefault true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      # FIXME: enable once mysqljs supports auth_socket
+      # ensureUsers = [ {
+      #   name = username;
+      #   ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+      # } ];
+    };
+
+    services.epgstation.settings = let
+      defaultSettings = {
+        serverPort = cfg.port;
+        socketioPort = cfg.socketioPort;
+        clientSocketioPort = cfg.clientSocketioPort;
+
+        dbType = mkDefault "mysql";
+        mysql = {
+          user = username;
+          database = cfg.database.name;
+          socketPath = mkDefault "/run/mysqld/mysqld.sock";
+          password = mkDefault "@dbPassword@";
+          connectTimeout = mkDefault 1000;
+          connectionLimit = mkDefault 10;
+        };
+
+        basicAuth = mkIf (cfg.basicAuth.user != null) {
+          user = mkDefault cfg.basicAuth.user;
+          password = mkDefault "@password@";
+        };
+
+        ffmpeg = mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg";
+        ffprobe = mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe";
+
+        fileExtension = mkDefault ".m2ts";
+        maxEncode = mkDefault 2;
+        maxStreaming = mkDefault 2;
+      };
+    in
+    mkMerge [
+      defaultSettings
+      (mkIf cfg.usePreconfiguredStreaming streamingConfig)
+    ];
+
+    systemd.tmpfiles.rules = [
+      "d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/recorded' - ${username} ${groupname} - -"
+      "d '/var/lib/epgstation/thumbnail' - ${username} ${groupname} - -"
+    ];
+
+    systemd.services.epgstation = {
+      description = pkgs.epgstation.meta.description;
+      wantedBy = [ "multi-user.target" ];
+      after = [
+        "network.target"
+      ] ++ optional config.services.mirakurun.enable "mirakurun.service"
+        ++ optional config.services.mysql.enable "mysql.service";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.epgstation}/bin/epgstation start";
+        ExecStartPre = "+${preStartScript}";
+        User = username;
+        Group = groupname;
+        StateDirectory = "epgstation";
+        LogsDirectory = "epgstation";
+        ConfigurationDirectory = "epgstation";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json
new file mode 100644
index 00000000000..8eb99cf8558
--- /dev/null
+++ b/nixos/modules/services/video/epgstation/streaming.json
@@ -0,0 +1,119 @@
+{
+  "liveHLS": [
+    {
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+    },
+    {
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
+    },
+    {
+      "name": "180p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 48k -ac 2 -c:v libx264 -vf yadif,scale=-2:180 -b:v 100k -preset veryfast -maxrate 110k -bufsize 1000k -flags +loop-global_header %OUTPUT%"
+    }
+  ],
+  "liveMP4": [
+    {
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+    },
+    {
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1"
+    }
+  ],
+  "liveWebM": [
+    {
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+    },
+    {
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1"
+    }
+  ],
+  "mpegTsStreaming": [
+    {
+      "name": "720p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1"
+    },
+    {
+      "name": "480p",
+      "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1"
+    },
+    {
+      "name": "Original"
+    }
+  ],
+  "mpegTsViewer": {
+    "ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS",
+    "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end"
+  },
+  "recordedDownloader": {
+    "ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME",
+    "android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end"
+  },
+  "recordedStreaming": {
+    "webm": [
+      {
+        "name": "720p",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
+        "vb": "3000k",
+        "ab": "192k"
+      },
+      {
+        "name": "360p",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1",
+        "vb": "1500k",
+        "ab": "128k"
+      }
+    ],
+    "mp4": [
+      {
+        "name": "720p",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
+        "vb": "3000k",
+        "ab": "192k"
+      },
+      {
+        "name": "360p",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1",
+        "vb": "1500k",
+        "ab": "128k"
+      }
+    ],
+    "mpegTs": [
+      {
+        "name": "720p (H.264)",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
+        "vb": "3000k",
+        "ab": "192k"
+      },
+      {
+        "name": "360p (H.264)",
+        "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1",
+        "vb": "1500k",
+        "ab": "128k"
+      }
+    ]
+  },
+  "recordedHLS": [
+    {
+      "name": "720p",
+      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%"
+    },
+    {
+      "name": "480p",
+      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%"
+    },
+    {
+      "name": "480p(h265)",
+      "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_type fmp4 -hls_fmp4_init_filename stream%streamNum%-init.mp4 -hls_segment_filename stream%streamNum%-%09d.m4s -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx265 -vf yadif,scale=-2:480 -b:v 350k -preset veryfast -tag:v hvc1 %OUTPUT%"
+    }
+  ],
+  "recordedViewer": {
+    "ios": "infuse://x-callback-url/play?url=http://ADDRESS",
+    "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end"
+  }
+}
diff --git a/nixos/modules/services/video/mirakurun.nix b/nixos/modules/services/video/mirakurun.nix
index 675b67f6ebf..6ea73fa5c67 100644
--- a/nixos/modules/services/video/mirakurun.nix
+++ b/nixos/modules/services/video/mirakurun.nix
@@ -8,6 +8,18 @@ let
   username = config.users.users.mirakurun.name;
   groupname = config.users.users.mirakurun.group;
   settingsFmt = pkgs.formats.yaml {};
+
+  polkitRule = pkgs.writeTextDir "share/polkit-1/rules.d/10-mirakurun.rules" ''
+    polkit.addRule(function (action, subject) {
+      if (
+        (action.id == "org.debian.pcsc-lite.access_pcsc" ||
+          action.id == "org.debian.pcsc-lite.access_card") &&
+        subject.user == "${username}"
+      ) {
+        return polkit.Result.YES;
+      }
+    });
+  '';
 in
   {
     options = {
@@ -18,7 +30,8 @@ in
           type = with types; nullOr port;
           default = 40772;
           description = ''
-            Port to listen on. If null, it won't listen on any port.
+            Port to listen on. If <literal>null</literal>, it won't listen on
+            any port.
           '';
         };
 
@@ -27,6 +40,32 @@ in
           default = false;
           description = ''
             Open ports in the firewall for Mirakurun.
+
+            <warning>
+              <para>
+                Exposing Mirakurun to the open internet is generally advised
+                against. Only use it inside a trusted local network, or
+                consider putting it behind a VPN if you want remote access.
+              </para>
+            </warning>
+          '';
+        };
+
+        unixSocket = mkOption {
+          type = with types; nullOr path;
+          default = "/var/run/mirakurun/mirakurun.sock";
+          description = ''
+            Path to unix socket to listen on. If <literal>null</literal>, it
+            won't listen on any unix sockets.
+          '';
+        };
+
+        allowSmartCardAccess = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Install polkit rules to allow Mirakurun to access smart card readers
+            which is commonly used along with tuner devices.
           '';
         };
 
@@ -92,7 +131,7 @@ in
     };
 
     config = mkIf cfg.enable {
-      environment.systemPackages = [ mirakurun ];
+      environment.systemPackages = [ mirakurun ] ++ optional cfg.allowSmartCardAccess polkitRule;
       environment.etc = {
         "mirakurun/server.yml".source = settingsFmt.generate "server.yml" cfg.serverSettings;
         "mirakurun/tuners.yml" = mkIf (cfg.tunerSettings != null) {
@@ -121,8 +160,8 @@ in
 
       services.mirakurun.serverSettings = {
         logLevel = mkDefault 2;
-        path = mkDefault "/var/run/mirakurun/mirakurun.sock";
-        port = mkIf (cfg.port != null) (mkDefault cfg.port);
+        path = mkIf (cfg.unixSocket != null) cfg.unixSocket;
+        port = mkIf (cfg.port != null) cfg.port;
       };
 
       systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
new file mode 100644
index 00000000000..d4c0268ed66
--- /dev/null
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, utils, ... }:
+with lib;
+let
+  cfg = config.services.unifi-video;
+  mainClass = "com.ubnt.airvision.Main";
+  cmd = ''
+    ${pkgs.jsvc}/bin/jsvc \
+    -cwd ${stateDir} \
+    -debug \
+    -verbose:class \
+    -nodetach \
+    -user unifi-video \
+    -home ${cfg.jrePackage}/lib/openjdk \
+    -cp ${pkgs.commonsDaemon}/share/java/commons-daemon-1.2.4.jar:${stateDir}/lib/airvision.jar \
+    -pidfile ${cfg.pidFile} \
+    -procname unifi-video \
+    -Djava.security.egd=file:/dev/./urandom \
+    -Xmx${cfg.maximumJavaHeapSize}M \
+    -Xss512K \
+    -XX:+UseG1GC \
+    -XX:+UseStringDeduplication \
+    -XX:MaxMetaspaceSize=768M \
+    -Djava.library.path=${stateDir}/lib \
+    -Djava.awt.headless=true \
+    -Djavax.net.ssl.trustStore=${stateDir}/etc/ufv-truststore \
+    -Dfile.encoding=UTF-8 \
+    -Dav.tempdir=/var/cache/unifi-video
+  '';
+
+  mongoConf = pkgs.writeTextFile {
+    name = "mongo.conf";
+    executable = false;
+    text = ''
+      # for documentation of all options, see http://docs.mongodb.org/manual/reference/configuration-options/
+
+      storage:
+         dbPath: ${cfg.dataDir}/db
+         journal:
+            enabled: true
+         syncPeriodSecs: 60
+
+      systemLog:
+         destination: file
+         logAppend: true
+         path: ${stateDir}/logs/mongod.log
+
+      net:
+         port: 7441
+         bindIp: 127.0.0.1
+         http:
+            enabled: false
+
+      operationProfiling:
+         slowOpThresholdMs: 500
+         mode: off
+    '';
+  };
+
+
+  mongoWtConf = pkgs.writeTextFile {
+    name = "mongowt.conf";
+    executable = false;
+    text = ''
+      # for documentation of all options, see:
+      #   http://docs.mongodb.org/manual/reference/configuration-options/
+
+      storage:
+         dbPath: ${cfg.dataDir}/db-wt
+         journal:
+            enabled: true
+         wiredTiger:
+            engineConfig:
+               cacheSizeGB: 1
+
+      systemLog:
+         destination: file
+         logAppend: true
+         path: logs/mongod.log
+
+      net:
+         port: 7441
+         bindIp: 127.0.0.1
+
+      operationProfiling:
+         slowOpThresholdMs: 500
+         mode: off
+    '';
+  };
+
+  stateDir = "/var/lib/unifi-video";
+
+in
+  {
+
+    options.services.unifi-video = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether or not to enable the unifi-video service.
+        '';
+      };
+
+      jrePackage = mkOption {
+        type = types.package;
+        default = pkgs.jre8;
+        defaultText = "pkgs.jre8";
+        description = ''
+          The JRE package to use. Check the release notes to ensure it is supported.
+        '';
+      };
+
+      unifiVideoPackage = mkOption {
+        type = types.package;
+        default = pkgs.unifi-video;
+        defaultText = "pkgs.unifi-video";
+        description = ''
+          The unifi-video package to use.
+        '';
+      };
+
+      mongodbPackage = mkOption {
+        type = types.package;
+        default = pkgs.mongodb-4_0;
+        defaultText = "pkgs.mongodb";
+        description = ''
+          The mongodb package to use.
+        '';
+      };
+
+      logDir = mkOption {
+        type = types.str;
+        default = "${stateDir}/logs";
+        description = ''
+          Where to store the logs.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "${stateDir}/data";
+        description = ''
+          Where to store the database and other data.
+        '';
+      };
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether or not to open the required ports on the firewall.
+        '';
+      };
+
+      maximumJavaHeapSize = mkOption {
+        type = types.nullOr types.int;
+        default = 1024;
+        example = 4096;
+        description = ''
+          Set the maximimum heap size for the JVM in MB.
+        '';
+      };
+
+      pidFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/unifi-video.pid";
+        description = "Location of unifi-video pid file.";
+      };
+
+};
+
+config = mkIf cfg.enable {
+  users = {
+    users.unifi-video = {
+      description = "UniFi Video controller daemon user";
+      home = stateDir;
+      group = "unifi-video";
+      isSystemUser = true;
+    };
+    groups.unifi-video = {};
+  };
+
+  networking.firewall = mkIf cfg.openPorts {
+      # https://help.ui.com/hc/en-us/articles/217875218-UniFi-Video-Ports-Used
+      allowedTCPPorts = [
+        7080 # HTTP portal
+        7443 # HTTPS portal
+        7445 # Video over HTTP (mobile app)
+        7446 # Video over HTTPS (mobile app)
+        7447 # RTSP via the controller
+        7442 # Camera management from cameras to NVR over WAN
+      ];
+      allowedUDPPorts = [
+        6666 # Inbound camera streams sent over WAN
+      ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${stateDir}' 0700 unifi-video unifi-video - -"
+      "d '/var/cache/unifi-video' 0700 unifi-video unifi-video - -"
+
+      "d '${stateDir}/logs' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/etc' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc"
+      "C '${stateDir}/webapps' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/webapps"
+      "C '${stateDir}/email' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/email"
+      "C '${stateDir}/fw' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/fw"
+      "C '${stateDir}/lib' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/lib"
+
+      "d '${stateDir}/data' 0700 unifi-video unifi-video - -"
+      "d '${stateDir}/data/db' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/data/system.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc/system.properties"
+
+      "d '${stateDir}/bin' 0700 unifi-video unifi-video - -"
+      "f '${stateDir}/bin/evostreamms' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/evostreamms"
+      "f '${stateDir}/bin/libavcodec.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavcodec.so.54"
+      "f '${stateDir}/bin/libavformat.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavformat.so.54"
+      "f '${stateDir}/bin/libavutil.so.52' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavutil.so.52"
+      "f '${stateDir}/bin/ubnt.avtool' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.avtool"
+      "f '${stateDir}/bin/ubnt.updater' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.updater"
+      "C '${stateDir}/bin/mongo' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongo"
+      "C '${stateDir}/bin/mongod' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongod"
+      "C '${stateDir}/bin/mongoperf' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongoperf"
+      "C '${stateDir}/bin/mongos' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongos"
+
+      "d '${stateDir}/conf' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/evostream"
+      "Z '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - -"
+      "L+ '${stateDir}/conf/mongodv3.0+.conf' 0700 unifi-video unifi-video - ${mongoConf}"
+      "L+ '${stateDir}/conf/mongodv3.6+.conf' 0700 unifi-video unifi-video - ${mongoConf}"
+      "L+ '${stateDir}/conf/mongod-wt.conf' 0700 unifi-video unifi-video - ${mongoWtConf}"
+      "L+ '${stateDir}/conf/catalina.policy' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.policy"
+      "L+ '${stateDir}/conf/catalina.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.properties"
+      "L+ '${stateDir}/conf/context.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/context.xml"
+      "L+ '${stateDir}/conf/logging.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/logging.properties"
+      "L+ '${stateDir}/conf/server.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/server.xml"
+      "L+ '${stateDir}/conf/tomcat-users.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/tomcat-users.xml"
+      "L+ '${stateDir}/conf/web.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/web.xml"
+
+    ];
+
+    systemd.services.unifi-video = {
+      description = "UniFi Video NVR daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ] ;
+      unitConfig.RequiresMountsFor = stateDir;
+      # Make sure package upgrades trigger a service restart
+      restartTriggers = [ cfg.unifiVideoPackage cfg.mongodbPackage ];
+      path = with pkgs; [ gawk coreutils busybox which jre8 lsb-release libcap util-linux ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${(removeSuffix "\n" cmd)} ${mainClass} start";
+        ExecStop = "${(removeSuffix "\n" cmd)} stop ${mainClass} stop";
+        Restart = "on-failure";
+        UMask = "0077";
+        User = "unifi-video";
+        WorkingDirectory = "${stateDir}";
+      };
+    };
+
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ rsynnest ];
+  };
+}
diff --git a/nixos/modules/services/wayland/cage.nix b/nixos/modules/services/wayland/cage.nix
index 50e424fccbf..2e71abb69fc 100644
--- a/nixos/modules/services/wayland/cage.nix
+++ b/nixos/modules/services/wayland/cage.nix
@@ -82,6 +82,7 @@ in {
       auth    required pam_unix.so nullok
       account required pam_unix.so
       session required pam_unix.so
+      session required pam_env.so conffile=${config.system.build.pamEnvironment} readenv=0
       session required ${pkgs.systemd}/lib/security/pam_systemd.so
     '';
 
@@ -92,6 +93,6 @@ in {
     systemd.defaultUnit = "graphical.target";
   };
 
-  meta.maintainers = with lib.maintainers; [ matthewbauer flokli ];
+  meta.maintainers = with lib.maintainers; [ matthewbauer ];
 
 }
diff --git a/nixos/modules/services/web-apps/bookstack.nix b/nixos/modules/services/web-apps/bookstack.nix
new file mode 100644
index 00000000000..34a31af9c9d
--- /dev/null
+++ b/nixos/modules/services/web-apps/bookstack.nix
@@ -0,0 +1,368 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.bookstack;
+  bookstack = pkgs.bookstack.override {
+    dataDir = cfg.dataDir;
+  };
+  db = cfg.database;
+  mail = cfg.mail;
+
+  user = cfg.user;
+  group = cfg.group;
+
+  # shell script for local administration
+  artisan = pkgs.writeScriptBin "bookstack" ''
+    #! ${pkgs.runtimeShell}
+    cd ${bookstack}
+    sudo=exec
+    if [[ "$USER" != ${user} ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u ${user}'
+    fi
+    $sudo ${pkgs.php}/bin/php artisan $*
+  '';
+
+
+in {
+  options.services.bookstack = {
+
+    enable = mkEnableOption "BookStack";
+
+    user = mkOption {
+      default = "bookstack";
+      description = "User bookstack runs as.";
+      type = types.str;
+    };
+
+    group = mkOption {
+      default = "bookstack";
+      description = "Group bookstack runs as.";
+      type = types.str;
+    };
+
+    appKeyFile = mkOption {
+      description = ''
+        A file containing the AppKey.
+        Used for encryption where needed. Can be generated with <code>head -c 32 /dev/urandom| base64</code> and must be prefixed with <literal>base64:</literal>.
+      '';
+      example = "/run/keys/bookstack-appkey";
+      type = types.path;
+    };
+
+    appURL = mkOption {
+      description = ''
+        The root URL that you want to host BookStack on. All URLs in BookStack will be generated using this value.
+        If you change this in the future you may need to run a command to update stored URLs in the database. Command example: <code>php artisan bookstack:update-url https://old.example.com https://new.example.com</code>
+      '';
+      example = "https://example.com";
+      type = types.str;
+    };
+
+    cacheDir = mkOption {
+      description = "BookStack cache directory";
+      default = "/var/cache/bookstack";
+      type = types.path;
+    };
+
+    dataDir = mkOption {
+      description = "BookStack data directory";
+      default = "/var/lib/bookstack";
+      type = types.path;
+    };
+
+    database = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = "Database host port.";
+      };
+      name = mkOption {
+        type = types.str;
+        default = "bookstack";
+        description = "Database name.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = user;
+        defaultText = "\${user}";
+        description = "Database username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/bookstack-dbpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Create the database and database user locally.";
+      };
+    };
+
+    mail = {
+      driver = mkOption {
+        type = types.enum [ "smtp" "sendmail" ];
+        default = "smtp";
+        description = "Mail driver to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Mail host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "Mail host port.";
+      };
+      fromName = mkOption {
+        type = types.str;
+        default = "BookStack";
+        description = "Mail \"from\" name.";
+      };
+      from = mkOption {
+        type = types.str;
+        default = "mail@bookstackapp.com";
+        description = "Mail \"from\" email.";
+      };
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "bookstack";
+        description = "Mail username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/bookstack-mailpassword";
+        description = ''
+          A file containing the password corresponding to
+          <option>mail.user</option>.
+        '';
+      };
+      encryption = mkOption {
+        type = with types; nullOr (enum [ "tls" ]);
+        default = null;
+        description = "SMTP encryption mechanism to use.";
+      };
+    };
+
+    maxUploadSize = mkOption {
+      type = types.str;
+      default = "18M";
+      example = "1G";
+      description = "The maximum size for uploads (e.g. images).";
+    };
+
+    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 bookstack 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; }) {}
+      );
+      default = {};
+      example = {
+        serverAliases = [
+          "bookstack.\${config.networking.domain}"
+        ];
+        # To enable encryption and let let's encrypt take care of certificate
+        forceSSL = true;
+        enableACME = true;
+      };
+      description = ''
+        With this option, you can customize the nginx virtualHost settings.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      example = ''
+        ALLOWED_IFRAME_HOSTS="https://example.com"
+        WKHTMLTOPDF=/home/user/bins/wkhtmltopdf
+      '';
+      description = ''
+        Lines to be appended verbatim to the BookStack configuration.
+        Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/> for details on supported values.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = db.createLocally -> db.user == user;
+        message = "services.bookstack.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true.";
+      }
+      { assertion = db.createLocally -> db.passwordFile == null;
+        message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true.";
+      }
+    ];
+
+    environment.systemPackages = [ artisan ];
+
+    services.mysql = mkIf db.createLocally {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ db.name ];
+      ensureUsers = [
+        { name = db.user;
+          ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
+        }
+      ];
+    };
+
+    services.phpfpm.pools.bookstack = {
+      inherit user;
+      inherit group;
+      phpOptions = ''
+        log_errors = on
+        post_max_size = ${cfg.maxUploadSize}
+        upload_max_filesize = ${cfg.maxUploadSize}
+      '';
+      settings = {
+        "listen.mode" = "0660";
+        "listen.owner" = user;
+        "listen.group" = group;
+      } // cfg.poolConfig;
+    };
+
+    services.nginx = {
+      enable = mkDefault true;
+      virtualHosts.bookstack = mkMerge [ cfg.nginx {
+        root = mkForce "${bookstack}/public";
+        extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
+        locations = {
+          "/" = {
+            index = "index.php";
+            extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
+          };
+          "~ \.php$" = {
+            extraConfig = ''
+              try_files $uri $uri/ /index.php?$query_string;
+              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."bookstack".socket};
+              ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
+            '';
+          };
+          "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+            extraConfig = "expires 365d;";
+          };
+        };
+      }];
+    };
+
+    systemd.services.bookstack-setup = {
+      description = "Preperation tasks for BookStack";
+      before = [ "phpfpm-bookstack.service" ];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        WorkingDirectory = "${bookstack}";
+      };
+      script = ''
+        # set permissions
+        umask 077
+        # create .env file
+        echo "
+        APP_KEY=base64:$(head -n1 ${cfg.appKeyFile})
+        APP_URL=${cfg.appURL}
+        DB_HOST=${db.host}
+        DB_PORT=${toString db.port}
+        DB_DATABASE=${db.name}
+        DB_USERNAME=${db.user}
+        MAIL_DRIVER=${mail.driver}
+        MAIL_FROM_NAME=\"${mail.fromName}\"
+        MAIL_FROM=${mail.from}
+        MAIL_HOST=${mail.host}
+        MAIL_PORT=${toString mail.port}
+        ${optionalString (mail.user != null) "MAIL_USERNAME=${mail.user};"}
+        ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption};"}
+        ${optionalString (db.passwordFile != null) "DB_PASSWORD=$(head -n1 ${db.passwordFile})"}
+        ${optionalString (mail.passwordFile != null) "MAIL_PASSWORD=$(head -n1 ${mail.passwordFile})"}
+        APP_SERVICES_CACHE=${cfg.cacheDir}/services.php
+        APP_PACKAGES_CACHE=${cfg.cacheDir}/packages.php
+        APP_CONFIG_CACHE=${cfg.cacheDir}/config.php
+        APP_ROUTES_CACHE=${cfg.cacheDir}/routes-v7.php
+        APP_EVENTS_CACHE=${cfg.cacheDir}/events.php
+        ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "SESSION_SECURE_COOKIE=true"}
+        ${toString cfg.extraConfig}
+        " > "${cfg.dataDir}/.env"
+
+        # migrate db
+        ${pkgs.php}/bin/php artisan migrate --force
+
+        # clear & create caches (needed in case of update)
+        ${pkgs.php}/bin/php artisan cache:clear
+        ${pkgs.php}/bin/php artisan config:clear
+        ${pkgs.php}/bin/php artisan view:clear
+        ${pkgs.php}/bin/php artisan config:cache
+        ${pkgs.php}/bin/php artisan route:cache
+        ${pkgs.php}/bin/php artisan view:cache
+      '';
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.cacheDir}                           0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}                            0710 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public                     0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/public/uploads             0750 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage                    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/app                0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/fonts              0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework          0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/cache    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/framework/views    0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/logs               0700 ${user} ${group} - -"
+      "d ${cfg.dataDir}/storage/uploads            0700 ${user} ${group} - -"
+    ];
+
+    users = {
+      users = mkIf (user == "bookstack") {
+        bookstack = {
+          inherit group;
+          isSystemUser = true;
+        };
+        "${config.services.nginx.user}".extraGroups = [ group ];
+      };
+      groups = mkIf (group == "bookstack") {
+        bookstack = {};
+      };
+    };
+
+  };
+
+  meta.maintainers = with maintainers; [ ymarkus ];
+}
diff --git a/nixos/modules/services/web-apps/calibre-web.nix b/nixos/modules/services/web-apps/calibre-web.nix
new file mode 100644
index 00000000000..704cd2cfa8a
--- /dev/null
+++ b/nixos/modules/services/web-apps/calibre-web.nix
@@ -0,0 +1,165 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.calibre-web;
+
+  inherit (lib) concatStringsSep mkEnableOption mkIf mkOption optional optionalString types;
+in
+{
+  options = {
+    services.calibre-web = {
+      enable = mkEnableOption "Calibre-Web";
+
+      listen = {
+        ip = mkOption {
+          type = types.str;
+          default = "::1";
+          description = ''
+            IP address that Calibre-Web should listen on.
+          '';
+        };
+
+        port = mkOption {
+          type = types.port;
+          default = 8083;
+          description = ''
+            Listen port for Calibre-Web.
+          '';
+        };
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "calibre-web";
+        description = ''
+          The directory below <filename>/var/lib</filename> where Calibre-Web stores its data.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "calibre-web";
+        description = "User account under which Calibre-Web runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "calibre-web";
+        description = "Group account under which Calibre-Web runs.";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Open ports in the firewall for the server.
+        '';
+      };
+
+      options = {
+        calibreLibrary = mkOption {
+          type = types.nullOr types.path;
+          default = null;
+          description = ''
+            Path to Calibre library.
+          '';
+        };
+
+        enableBookConversion = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Configure path to the Calibre's ebook-convert in the DB.
+          '';
+        };
+
+        enableBookUploading = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Allow books to be uploaded via Calibre-Web UI.
+          '';
+        };
+
+        reverseProxyAuth = {
+          enable = mkOption {
+            type = types.bool;
+            default = false;
+            description = ''
+              Enable authorization using auth proxy.
+            '';
+          };
+
+          header = mkOption {
+            type = types.str;
+            default = "";
+            description = ''
+              Auth proxy header name.
+            '';
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.calibre-web = let
+      appDb = "/var/lib/${cfg.dataDir}/app.db";
+      gdriveDb = "/var/lib/${cfg.dataDir}/gdrive.db";
+      calibreWebCmd = "${pkgs.calibre-web}/bin/calibre-web -p ${appDb} -g ${gdriveDb}";
+
+      settings = concatStringsSep ", " (
+        [
+          "config_port = ${toString cfg.listen.port}"
+          "config_uploading = ${if cfg.options.enableBookUploading then "1" else "0"}"
+          "config_allow_reverse_proxy_header_login = ${if cfg.options.reverseProxyAuth.enable then "1" else "0"}"
+          "config_reverse_proxy_login_header_name = '${cfg.options.reverseProxyAuth.header}'"
+        ]
+        ++ optional (cfg.options.calibreLibrary != null) "config_calibre_dir = '${cfg.options.calibreLibrary}'"
+        ++ optional cfg.options.enableBookConversion "config_converterpath = '${pkgs.calibre}/bin/ebook-convert'"
+      );
+    in
+      {
+        description = "Web app for browsing, reading and downloading eBooks stored in a Calibre database";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          User = cfg.user;
+          Group = cfg.group;
+
+          StateDirectory = cfg.dataDir;
+          ExecStartPre = pkgs.writeShellScript "calibre-web-pre-start" (
+            ''
+              __RUN_MIGRATIONS_AND_EXIT=1 ${calibreWebCmd}
+
+              ${pkgs.sqlite}/bin/sqlite3 ${appDb} "update settings set ${settings}"
+            '' + optionalString (cfg.options.calibreLibrary != null) ''
+              test -f ${cfg.options.calibreLibrary}/metadata.db || { echo "Invalid Calibre library"; exit 1; }
+            ''
+          );
+
+          ExecStart = "${calibreWebCmd} -i ${cfg.listen.ip}";
+          Restart = "on-failure";
+        };
+      };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.listen.port ];
+    };
+
+    users.users = mkIf (cfg.user == "calibre-web") {
+      calibre-web = {
+        isSystemUser = true;
+        group = cfg.group;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "calibre-web") {
+      calibre-web = {};
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ pborzenkov ];
+}
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
new file mode 100644
index 00000000000..8d5302ba267
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -0,0 +1,1064 @@
+{ config, options, lib, pkgs, utils, ... }:
+
+let
+  json = pkgs.formats.json {};
+
+  cfg = config.services.discourse;
+
+  # Keep in sync with https://github.com/discourse/discourse_docker/blob/master/image/base/Dockerfile#L5
+  upstreamPostgresqlVersion = lib.getVersion pkgs.postgresql_13;
+
+  postgresqlPackage = if config.services.postgresql.enable then
+                        config.services.postgresql.package
+                      else
+                        pkgs.postgresql;
+
+  postgresqlVersion = lib.getVersion postgresqlPackage;
+
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == null;
+
+  tlsEnabled = (cfg.enableACME
+                || cfg.sslCertificate != null
+                || cfg.sslCertificateKey != null);
+in
+{
+  options = {
+    services.discourse = {
+      enable = lib.mkEnableOption "Discourse, an open source discussion platform";
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.discourse;
+        apply = p: p.override {
+          plugins = lib.unique (p.enabledPlugins ++ cfg.plugins);
+        };
+        defaultText = "pkgs.discourse";
+        description = ''
+          The discourse package to use.
+        '';
+      };
+
+      hostname = lib.mkOption {
+        type = lib.types.str;
+        default = if config.networking.domain != null then
+                    config.networking.fqdn
+                  else
+                    config.networking.hostName;
+        defaultText = "config.networking.fqdn";
+        example = "discourse.example.com";
+        description = ''
+          The hostname to serve Discourse on.
+        '';
+      };
+
+      secretKeyBaseFile = lib.mkOption {
+        type = with lib.types; nullOr path;
+        default = null;
+        example = "/run/keys/secret_key_base";
+        description = ''
+          The path to a file containing the
+          <literal>secret_key_base</literal> secret.
+
+          Discourse uses <literal>secret_key_base</literal> to encrypt
+          the cookie store, which contains session data, and to digest
+          user auth tokens.
+
+          Needs to be a 64 byte long string of hexadecimal
+          characters. You can generate one by running
+
+          <screen>
+          <prompt>$ </prompt>openssl rand -hex 64 >/path/to/secret_key_base_file
+          </screen>
+
+          This should be a string, not a nix path, since nix paths are
+          copied into the world-readable nix store.
+        '';
+      };
+
+      sslCertificate = lib.mkOption {
+        type = with lib.types; nullOr path;
+        default = null;
+        example = "/run/keys/ssl.cert";
+        description = ''
+          The path to the server SSL certificate. Set this to enable
+          SSL.
+        '';
+      };
+
+      sslCertificateKey = lib.mkOption {
+        type = with lib.types; nullOr path;
+        default = null;
+        example = "/run/keys/ssl.key";
+        description = ''
+          The path to the server SSL certificate key. Set this to
+          enable SSL.
+        '';
+      };
+
+      enableACME = lib.mkOption {
+        type = lib.types.bool;
+        default = cfg.sslCertificate == null && cfg.sslCertificateKey == null;
+        defaultText = "true, unless services.discourse.sslCertificate and services.discourse.sslCertificateKey are set.";
+        description = ''
+          Whether an ACME certificate should be used to secure
+          connections to the server.
+        '';
+      };
+
+      backendSettings = lib.mkOption {
+        type = with lib.types; attrsOf (nullOr (oneOf [ str int bool float ]));
+        default = {};
+        example = lib.literalExample ''
+          {
+            max_reqs_per_ip_per_minute = 300;
+            max_reqs_per_ip_per_10_seconds = 60;
+            max_asset_reqs_per_ip_per_10_seconds = 250;
+            max_reqs_per_ip_mode = "warn+block";
+          };
+        '';
+        description = ''
+          Additional settings to put in the
+          <filename>discourse.conf</filename> file.
+
+          Look in the
+          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/discourse_defaults.conf">discourse_defaults.conf</link>
+          file in the upstream distribution to find available options.
+
+          Setting an option to <literal>null</literal> means
+          <quote>define variable, but leave right-hand side
+          empty</quote>.
+        '';
+      };
+
+      siteSettings = lib.mkOption {
+        type = json.type;
+        default = {};
+        example = lib.literalExample ''
+          {
+            required = {
+              title = "My Cats";
+              site_description = "Discuss My Cats (and be nice plz)";
+            };
+            login = {
+              enable_github_logins = true;
+              github_client_id = "a2f6dfe838cb3206ce20";
+              github_client_secret._secret = /run/keys/discourse_github_client_secret;
+            };
+          };
+        '';
+        description = ''
+          Discourse site settings. These are the settings that can be
+          changed from the UI. This only defines their default values:
+          they can still be overridden from the UI.
+
+          Available settings can be found by looking in the
+          <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">site_settings.yml</link>
+          file of the upstream distribution. To find a setting's path,
+          you only need to care about the first two levels; i.e. its
+          category and name. See the example.
+
+          Settings containing secret data should be set to an
+          attribute set containing the attribute
+          <literal>_secret</literal> - a string pointing to a file
+          containing the value the option should be set to. See the
+          example to get a better picture of this: in the resulting
+          <filename>config/nixos_site_settings.json</filename> file,
+          the <literal>login.github_client_secret</literal> key will
+          be set to the contents of the
+          <filename>/run/keys/discourse_github_client_secret</filename>
+          file.
+        '';
+      };
+
+      admin = {
+        email = lib.mkOption {
+          type = lib.types.str;
+          example = "admin@example.com";
+          description = ''
+            The admin user email address.
+          '';
+        };
+
+        username = lib.mkOption {
+          type = lib.types.str;
+          example = "admin";
+          description = ''
+            The admin user username.
+          '';
+        };
+
+        fullName = lib.mkOption {
+          type = lib.types.str;
+          description = ''
+            The admin user's full name.
+          '';
+        };
+
+        passwordFile = lib.mkOption {
+          type = lib.types.path;
+          description = ''
+            A path to a file containing the admin user's password.
+
+            This should be a string, not a nix path, since nix paths are
+            copied into the world-readable nix store.
+          '';
+        };
+      };
+
+      nginx.enable = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Whether an <literal>nginx</literal> virtual host should be
+          set up to serve Discourse. Only disable if you're planning
+          to use a different web server, which is not recommended.
+        '';
+      };
+
+      database = {
+        pool = lib.mkOption {
+          type = lib.types.int;
+          default = 8;
+          description = ''
+            Database connection pool size.
+          '';
+        };
+
+        host = lib.mkOption {
+          type = with lib.types; nullOr str;
+          default = null;
+          description = ''
+            Discourse database hostname. <literal>null</literal> means <quote>prefer
+            local unix socket connection</quote>.
+          '';
+        };
+
+        passwordFile = lib.mkOption {
+          type = with lib.types; nullOr path;
+          default = null;
+          description = ''
+            File containing the Discourse database user password.
+
+            This should be a string, not a nix path, since nix paths are
+            copied into the world-readable nix store.
+          '';
+        };
+
+        createLocally = lib.mkOption {
+          type = lib.types.bool;
+          default = true;
+          description = ''
+            Whether a database should be automatically created on the
+            local host. Set this to <literal>false</literal> if you plan
+            on provisioning a local database yourself. This has no effect
+            if <option>services.discourse.database.host</option> is customized.
+          '';
+        };
+
+        name = lib.mkOption {
+          type = lib.types.str;
+          default = "discourse";
+          description = ''
+            Discourse database name.
+          '';
+        };
+
+        username = lib.mkOption {
+          type = lib.types.str;
+          default = "discourse";
+          description = ''
+            Discourse database user.
+          '';
+        };
+
+        ignorePostgresqlVersion = lib.mkOption {
+          type = lib.types.bool;
+          default = false;
+          description = ''
+            Whether to allow other versions of PostgreSQL than the
+            recommended one. Only effective when
+            <option>services.discourse.database.createLocally</option>
+            is enabled.
+          '';
+        };
+      };
+
+      redis = {
+        host = lib.mkOption {
+          type = lib.types.str;
+          default = "localhost";
+          description = ''
+            Redis server hostname.
+          '';
+        };
+
+        passwordFile = lib.mkOption {
+          type = with lib.types; nullOr path;
+          default = null;
+          description = ''
+            File containing the Redis password.
+
+            This should be a string, not a nix path, since nix paths are
+            copied into the world-readable nix store.
+          '';
+        };
+
+        dbNumber = lib.mkOption {
+          type = lib.types.int;
+          default = 0;
+          description = ''
+            Redis database number.
+          '';
+        };
+
+        useSSL = lib.mkOption {
+          type = lib.types.bool;
+          default = cfg.redis.host != "localhost";
+          description = ''
+            Connect to Redis with SSL.
+          '';
+        };
+      };
+
+      mail = {
+        notificationEmailAddress = lib.mkOption {
+          type = lib.types.str;
+          default = "${if cfg.mail.incoming.enable then "notifications" else "noreply"}@${cfg.hostname}";
+          defaultText = ''
+            "notifications@`config.services.discourse.hostname`" if
+            config.services.discourse.mail.incoming.enable is "true",
+            otherwise "noreply`config.services.discourse.hostname`"
+          '';
+          description = ''
+            The <literal>from:</literal> email address used when
+            sending all essential system emails. The domain specified
+            here must have SPF, DKIM and reverse PTR records set
+            correctly for email to arrive.
+          '';
+        };
+
+        contactEmailAddress = lib.mkOption {
+          type = lib.types.str;
+          default = "";
+          description = ''
+            Email address of key contact responsible for this
+            site. Used for critical notifications, as well as on the
+            <literal>/about</literal> contact form for urgent matters.
+          '';
+        };
+
+        outgoing = {
+          serverAddress = lib.mkOption {
+            type = lib.types.str;
+            default = "localhost";
+            description = ''
+              The address of the SMTP server Discourse should use to
+              send email.
+            '';
+          };
+
+          port = lib.mkOption {
+            type = lib.types.port;
+            default = 25;
+            description = ''
+              The port of the SMTP server Discourse should use to
+              send email.
+            '';
+          };
+
+          username = lib.mkOption {
+            type = with lib.types; nullOr str;
+            default = null;
+            description = ''
+              The username of the SMTP server.
+            '';
+          };
+
+          passwordFile = lib.mkOption {
+            type = lib.types.nullOr lib.types.path;
+            default = null;
+            description = ''
+              A file containing the password of the SMTP server account.
+
+              This should be a string, not a nix path, since nix paths
+              are copied into the world-readable nix store.
+            '';
+          };
+
+          domain = lib.mkOption {
+            type = lib.types.str;
+            default = cfg.hostname;
+            description = ''
+              HELO domain to use for outgoing mail.
+            '';
+          };
+
+          authentication = lib.mkOption {
+            type = with lib.types; nullOr (enum ["plain" "login" "cram_md5"]);
+            default = null;
+            description = ''
+              Authentication type to use, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
+            '';
+          };
+
+          enableStartTLSAuto = lib.mkOption {
+            type = lib.types.bool;
+            default = true;
+            description = ''
+              Whether to try to use StartTLS.
+            '';
+          };
+
+          opensslVerifyMode = lib.mkOption {
+            type = lib.types.str;
+            default = "peer";
+            description = ''
+              How OpenSSL checks the certificate, see http://api.rubyonrails.org/classes/ActionMailer/Base.html
+            '';
+          };
+
+          forceTLS = lib.mkOption {
+            type = lib.types.bool;
+            default = false;
+            description = ''
+              Force implicit TLS as per RFC 8314 3.3.
+            '';
+          };
+        };
+
+        incoming = {
+          enable = lib.mkOption {
+            type = lib.types.bool;
+            default = false;
+            description = ''
+              Whether to set up Postfix to receive incoming mail.
+            '';
+          };
+
+          replyEmailAddress = lib.mkOption {
+            type = lib.types.str;
+            default = "%{reply_key}@${cfg.hostname}";
+            defaultText = "%{reply_key}@`config.services.discourse.hostname`";
+            description = ''
+              Template for reply by email incoming email address, for
+              example: %{reply_key}@reply.example.com or
+              replies+%{reply_key}@example.com
+            '';
+          };
+
+          mailReceiverPackage = lib.mkOption {
+            type = lib.types.package;
+            default = pkgs.discourse-mail-receiver;
+            defaultText = "pkgs.discourse-mail-receiver";
+            description = ''
+              The discourse-mail-receiver package to use.
+            '';
+          };
+
+          apiKeyFile = lib.mkOption {
+            type = lib.types.nullOr lib.types.path;
+            default = null;
+            description = ''
+              A file containing the Discourse API key used to add
+              posts and messages from mail. If left at its default
+              value <literal>null</literal>, one will be automatically
+              generated.
+
+              This should be a string, not a nix path, since nix paths
+              are copied into the world-readable nix store.
+            '';
+          };
+        };
+      };
+
+      plugins = lib.mkOption {
+        type = lib.types.listOf lib.types.package;
+        default = [];
+        example = lib.literalExample ''
+          with config.services.discourse.package.plugins; [
+            discourse-canned-replies
+            discourse-github
+          ];
+        '';
+        description = ''
+          Plugins to install as part of
+          <productname>Discourse</productname>, expressed as a list of
+          derivations.
+        '';
+      };
+
+      sidekiqProcesses = lib.mkOption {
+        type = lib.types.int;
+        default = 1;
+        description = ''
+          How many Sidekiq processes should be spawned.
+        '';
+      };
+
+      unicornTimeout = lib.mkOption {
+        type = lib.types.int;
+        default = 30;
+        description = ''
+          Time in seconds before a request to Unicorn times out.
+
+          This can be raised if the system Discourse is running on is
+          too slow to handle many requests within 30 seconds.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.database.host != null) -> (cfg.database.passwordFile != null);
+        message = "When services.gitlab.database.host is customized, services.discourse.database.passwordFile must be set!";
+      }
+      {
+        assertion = cfg.hostname != "";
+        message = "Could not automatically determine hostname, set service.discourse.hostname manually.";
+      }
+      {
+        assertion = cfg.database.ignorePostgresqlVersion || (databaseActuallyCreateLocally -> upstreamPostgresqlVersion == postgresqlVersion);
+        message = "The PostgreSQL version recommended for use with Discourse is ${upstreamPostgresqlVersion}, you're using ${postgresqlVersion}. "
+                  + "Either update your PostgreSQL package to the correct version or set services.discourse.database.ignorePostgresqlVersion. "
+                  + "See https://nixos.org/manual/nixos/stable/index.html#module-postgresql for details on how to upgrade PostgreSQL.";
+      }
+    ];
+
+
+    # Default config values are from `config/discourse_defaults.conf`
+    # upstream.
+    services.discourse.backendSettings = lib.mapAttrs (_: lib.mkDefault) {
+      db_pool = cfg.database.pool;
+      db_timeout = 5000;
+      db_connect_timeout = 5;
+      db_socket = null;
+      db_host = cfg.database.host;
+      db_backup_host = null;
+      db_port = null;
+      db_backup_port = 5432;
+      db_name = cfg.database.name;
+      db_username = if databaseActuallyCreateLocally then "discourse" else cfg.database.username;
+      db_password = cfg.database.passwordFile;
+      db_prepared_statements = false;
+      db_replica_host = null;
+      db_replica_port = null;
+      db_advisory_locks = true;
+
+      inherit (cfg) hostname;
+      backup_hostname = null;
+
+      smtp_address = cfg.mail.outgoing.serverAddress;
+      smtp_port = cfg.mail.outgoing.port;
+      smtp_domain = cfg.mail.outgoing.domain;
+      smtp_user_name = cfg.mail.outgoing.username;
+      smtp_password = cfg.mail.outgoing.passwordFile;
+      smtp_authentication = cfg.mail.outgoing.authentication;
+      smtp_enable_start_tls = cfg.mail.outgoing.enableStartTLSAuto;
+      smtp_openssl_verify_mode = cfg.mail.outgoing.opensslVerifyMode;
+      smtp_force_tls = cfg.mail.outgoing.forceTLS;
+
+      load_mini_profiler = true;
+      mini_profiler_snapshots_period = 0;
+      mini_profiler_snapshots_transport_url = null;
+      mini_profiler_snapshots_transport_auth_key = null;
+
+      cdn_url = null;
+      cdn_origin_hostname = null;
+      developer_emails = null;
+
+      redis_host = cfg.redis.host;
+      redis_port = 6379;
+      redis_replica_host = null;
+      redis_replica_port = 6379;
+      redis_db = cfg.redis.dbNumber;
+      redis_password = cfg.redis.passwordFile;
+      redis_skip_client_commands = false;
+      redis_use_ssl = cfg.redis.useSSL;
+
+      message_bus_redis_enabled = false;
+      message_bus_redis_host = "localhost";
+      message_bus_redis_port = 6379;
+      message_bus_redis_replica_host = null;
+      message_bus_redis_replica_port = 6379;
+      message_bus_redis_db = 0;
+      message_bus_redis_password = null;
+      message_bus_redis_skip_client_commands = false;
+
+      enable_cors = false;
+      cors_origin = "";
+      serve_static_assets = false;
+      sidekiq_workers = 5;
+      rtl_css = false;
+      connection_reaper_age = 30;
+      connection_reaper_interval = 30;
+      relative_url_root = null;
+      message_bus_max_backlog_size = 100;
+      secret_key_base = cfg.secretKeyBaseFile;
+      fallback_assets_path = null;
+
+      s3_bucket = null;
+      s3_region = null;
+      s3_access_key_id = null;
+      s3_secret_access_key = null;
+      s3_use_iam_profile = null;
+      s3_cdn_url = null;
+      s3_endpoint = null;
+      s3_http_continue_timeout = null;
+      s3_install_cors_rule = null;
+
+      max_user_api_reqs_per_minute = 20;
+      max_user_api_reqs_per_day = 2880;
+      max_admin_api_reqs_per_key_per_minute = 60;
+      max_reqs_per_ip_per_minute = 200;
+      max_reqs_per_ip_per_10_seconds = 50;
+      max_asset_reqs_per_ip_per_10_seconds = 200;
+      max_reqs_per_ip_mode = "block";
+      max_reqs_rate_limit_on_private = false;
+      force_anonymous_min_queue_seconds = 1;
+      force_anonymous_min_per_10_seconds = 3;
+      background_requests_max_queue_length = 0.5;
+      reject_message_bus_queue_seconds = 0.1;
+      disable_search_queue_threshold = 1;
+      max_old_rebakes_per_15_minutes = 300;
+      max_logster_logs = 1000;
+      refresh_maxmind_db_during_precompile_days = 2;
+      maxmind_backup_path = null;
+      maxmind_license_key = null;
+      enable_performance_http_headers = false;
+      enable_js_error_reporting = true;
+      mini_scheduler_workers = 5;
+      compress_anon_cache = false;
+      anon_cache_store_threshold = 2;
+      allowed_theme_repos = null;
+      enable_email_sync_demon = false;
+      max_digests_enqueued_per_30_mins_per_site = 10000;
+      cluster_name = null;
+    };
+
+    services.redis.enable = lib.mkDefault (cfg.redis.host == "localhost");
+
+    services.postgresql = lib.mkIf databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = [{ name = "discourse"; }];
+    };
+
+    # The postgresql module doesn't currently support concepts like
+    # objects owners and extensions; for now we tack on what's needed
+    # here.
+    systemd.services.discourse-postgresql =
+      let
+        pgsql = config.services.postgresql;
+      in
+        lib.mkIf databaseActuallyCreateLocally {
+          after = [ "postgresql.service" ];
+          bindsTo = [ "postgresql.service" ];
+          wantedBy = [ "discourse.service" ];
+          partOf = [ "discourse.service" ];
+          path = [
+            pgsql.package
+          ];
+          script = ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'discourse'" | grep -q 1 || psql -tAc 'CREATE DATABASE "discourse" OWNER "discourse"'
+            psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS pg_trgm"
+            psql '${cfg.database.name}' -tAc "CREATE EXTENSION IF NOT EXISTS hstore"
+          '';
+
+          serviceConfig = {
+            User = pgsql.superUser;
+            Type = "oneshot";
+            RemainAfterExit = true;
+          };
+        };
+
+    systemd.services.discourse = {
+      wantedBy = [ "multi-user.target" ];
+      after = [
+        "redis.service"
+        "postgresql.service"
+        "discourse-postgresql.service"
+      ];
+      bindsTo = [
+        "redis.service"
+      ] ++ lib.optionals (cfg.database.host == null) [
+        "postgresql.service"
+        "discourse-postgresql.service"
+      ];
+      path = cfg.package.runtimeDeps ++ [
+        postgresqlPackage
+        pkgs.replace-secret
+        cfg.package.rake
+      ];
+      environment = cfg.package.runtimeEnv // {
+        UNICORN_TIMEOUT = builtins.toString cfg.unicornTimeout;
+        UNICORN_SIDEKIQS = builtins.toString cfg.sidekiqProcesses;
+        MALLOC_ARENA_MAX = "2";
+      };
+
+      preStart =
+        let
+          discourseKeyValue = lib.generators.toKeyValue {
+            mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " = " {
+              mkValueString = v: with builtins;
+                if isInt           v then toString v
+                else if isString   v then ''"${v}"''
+                else if true  ==   v then "true"
+                else if false ==   v then "false"
+                else if null  ==   v then ""
+                else if isFloat    v then lib.strings.floatToString v
+                else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+            };
+          };
+
+          discourseConf = pkgs.writeText "discourse.conf" (discourseKeyValue cfg.backendSettings);
+
+          mkSecretReplacement = file:
+            lib.optionalString (file != null) ''
+              replace-secret '${file}' '${file}' /run/discourse/config/discourse.conf
+            '';
+        in ''
+          set -o errexit -o pipefail -o nounset -o errtrace
+          shopt -s inherit_errexit
+
+          umask u=rwx,g=rx,o=
+
+          cp -r ${cfg.package}/share/discourse/config.dist/* /run/discourse/config/
+          cp -r ${cfg.package}/share/discourse/public.dist/* /run/discourse/public/
+          ln -sf /var/lib/discourse/uploads /run/discourse/public/uploads
+          ln -sf /var/lib/discourse/backups /run/discourse/public/backups
+
+          (
+              umask u=rwx,g=,o=
+
+              ${utils.genJqSecretsReplacementSnippet
+                  cfg.siteSettings
+                  "/run/discourse/config/nixos_site_settings.json"
+              }
+              install -T -m 0600 -o discourse ${discourseConf} /run/discourse/config/discourse.conf
+              ${mkSecretReplacement cfg.database.passwordFile}
+              ${mkSecretReplacement cfg.mail.outgoing.passwordFile}
+              ${mkSecretReplacement cfg.redis.passwordFile}
+              ${mkSecretReplacement cfg.secretKeyBaseFile}
+              chmod 0400 /run/discourse/config/discourse.conf
+          )
+
+          discourse-rake db:migrate >>/var/log/discourse/db_migration.log
+          chmod -R u+w /run/discourse/tmp/
+
+          export ADMIN_EMAIL="${cfg.admin.email}"
+          export ADMIN_NAME="${cfg.admin.fullName}"
+          export ADMIN_USERNAME="${cfg.admin.username}"
+          ADMIN_PASSWORD="$(<${cfg.admin.passwordFile})"
+          export ADMIN_PASSWORD
+          discourse-rake admin:create_noninteractively
+
+          discourse-rake themes:update
+          discourse-rake uploads:regenerate_missing_optimized
+        '';
+
+      serviceConfig = {
+        Type = "simple";
+        User = "discourse";
+        Group = "discourse";
+        RuntimeDirectory = map (p: "discourse/" + p) [
+          "config"
+          "home"
+          "tmp"
+          "assets/javascripts/plugins"
+          "public"
+          "plugins"
+          "sockets"
+        ];
+        RuntimeDirectoryMode = 0750;
+        StateDirectory = map (p: "discourse/" + p) [
+          "uploads"
+          "backups"
+        ];
+        StateDirectoryMode = 0750;
+        LogsDirectory = "discourse";
+        TimeoutSec = "infinity";
+        Restart = "on-failure";
+        WorkingDirectory = "${cfg.package}/share/discourse";
+
+        RemoveIPC = true;
+        PrivateTmp = true;
+        NoNewPrivileges = true;
+        RestrictSUIDSGID = true;
+        ProtectSystem = "strict";
+        ProtectHome = "read-only";
+
+        ExecStart = "${cfg.package.rubyEnv}/bin/bundle exec config/unicorn_launcher -E production -c config/unicorn.conf.rb";
+      };
+    };
+
+    services.nginx = lib.mkIf cfg.nginx.enable {
+      enable = true;
+      additionalModules = [ pkgs.nginxModules.brotli ];
+
+      recommendedTlsSettings = true;
+      recommendedOptimisation = true;
+      recommendedGzipSettings = true;
+      recommendedProxySettings = true;
+
+      upstreams.discourse.servers."unix:/run/discourse/sockets/unicorn.sock" = {};
+
+      appendHttpConfig = ''
+        # inactive means we keep stuff around for 1440m minutes regardless of last access (1 week)
+        # levels means it is a 2 deep heirarchy cause we can have lots of files
+        # max_size limits the size of the cache
+        proxy_cache_path /var/cache/nginx inactive=1440m levels=1:2 keys_zone=discourse:10m max_size=600m;
+
+        # see: https://meta.discourse.org/t/x/74060
+        proxy_buffer_size 8k;
+      '';
+
+      virtualHosts.${cfg.hostname} = {
+        inherit (cfg) sslCertificate sslCertificateKey enableACME;
+        forceSSL = lib.mkDefault tlsEnabled;
+
+        root = "/run/discourse/public";
+
+        locations =
+          let
+            proxy = { extraConfig ? "" }: {
+              proxyPass = "http://discourse";
+              extraConfig = extraConfig + ''
+                proxy_set_header X-Request-Start "t=''${msec}";
+              '';
+            };
+            cache = time: ''
+              expires ${time};
+              add_header Cache-Control public,immutable;
+            '';
+            cache_1y = cache "1y";
+            cache_1d = cache "1d";
+          in
+            {
+              "/".tryFiles = "$uri @discourse";
+              "@discourse" = proxy {};
+              "^~ /backups/".extraConfig = ''
+                internal;
+              '';
+              "/favicon.ico" = {
+                return = "204";
+                extraConfig = ''
+                  access_log off;
+                  log_not_found off;
+                '';
+              };
+              "~ ^/uploads/short-url/" = proxy {};
+              "~ ^/secure-media-uploads/" = proxy {};
+              "~* (fonts|assets|plugins|uploads)/.*\.(eot|ttf|woff|woff2|ico|otf)$".extraConfig = cache_1y + ''
+                add_header Access-Control-Allow-Origin *;
+              '';
+              "/srv/status" = proxy {
+                extraConfig = ''
+                  access_log off;
+                  log_not_found off;
+                '';
+              };
+              "~ ^/javascripts/".extraConfig = cache_1d;
+              "~ ^/assets/(?<asset_path>.+)$".extraConfig = cache_1y + ''
+                # asset pipeline enables this
+                brotli_static on;
+                gzip_static on;
+              '';
+              "~ ^/plugins/".extraConfig = cache_1y;
+              "~ /images/emoji/".extraConfig = cache_1y;
+              "~ ^/uploads/" = proxy {
+                extraConfig = cache_1y + ''
+                  proxy_set_header X-Sendfile-Type X-Accel-Redirect;
+                  proxy_set_header X-Accel-Mapping /run/discourse/public/=/downloads/;
+
+                  # custom CSS
+                  location ~ /stylesheet-cache/ {
+                      try_files $uri =404;
+                  }
+                  # this allows us to bypass rails
+                  location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp)$ {
+                      try_files $uri =404;
+                  }
+                  # SVG needs an extra header attached
+                  location ~* \.(svg)$ {
+                  }
+                  # thumbnails & optimized images
+                  location ~ /_?optimized/ {
+                      try_files $uri =404;
+                  }
+                '';
+              };
+              "~ ^/admin/backups/" = proxy {
+                extraConfig = ''
+                  proxy_set_header X-Sendfile-Type X-Accel-Redirect;
+                  proxy_set_header X-Accel-Mapping /run/discourse/public/=/downloads/;
+                '';
+              };
+              "~ ^/(svg-sprite/|letter_avatar/|letter_avatar_proxy/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker)" = proxy {
+                extraConfig = ''
+                  # if Set-Cookie is in the response nothing gets cached
+                  # this is double bad cause we are not passing last modified in
+                  proxy_ignore_headers "Set-Cookie";
+                  proxy_hide_header "Set-Cookie";
+                  proxy_hide_header "X-Discourse-Username";
+                  proxy_hide_header "X-Runtime";
+
+                  # note x-accel-redirect can not be used with proxy_cache
+                  proxy_cache discourse;
+                  proxy_cache_key "$scheme,$host,$request_uri";
+                  proxy_cache_valid 200 301 302 7d;
+                  proxy_cache_valid any 1m;
+                '';
+              };
+              "/message-bus/" = proxy {
+                extraConfig = ''
+                  proxy_http_version 1.1;
+                  proxy_buffering off;
+                '';
+              };
+              "/downloads/".extraConfig = ''
+                internal;
+                alias /run/discourse/public/;
+              '';
+            };
+      };
+    };
+
+    systemd.services.discourse-mail-receiver-setup = lib.mkIf cfg.mail.incoming.enable (
+      let
+        mail-receiver-environment = {
+          MAIL_DOMAIN = cfg.hostname;
+          DISCOURSE_BASE_URL = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
+          DISCOURSE_API_KEY = "@api-key@";
+          DISCOURSE_API_USERNAME = "system";
+        };
+        mail-receiver-json = json.generate "mail-receiver.json" mail-receiver-environment;
+      in
+        {
+          before = [ "postfix.service" ];
+          after = [ "discourse.service" ];
+          wantedBy = [ "discourse.service" ];
+          partOf = [ "discourse.service" ];
+          path = [
+            cfg.package.rake
+            pkgs.jq
+          ];
+          preStart = lib.optionalString (cfg.mail.incoming.apiKeyFile == null) ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            if [[ ! -e /var/lib/discourse-mail-receiver/api_key ]]; then
+                discourse-rake api_key:create_master[email-receiver] >/var/lib/discourse-mail-receiver/api_key
+            fi
+          '';
+          script =
+            let
+              apiKeyPath =
+                if cfg.mail.incoming.apiKeyFile == null then
+                  "/var/lib/discourse-mail-receiver/api_key"
+                else
+                  cfg.mail.incoming.apiKeyFile;
+            in ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+
+              api_key=$(<'${apiKeyPath}')
+              export api_key
+
+              jq <${mail-receiver-json} \
+                 '.DISCOURSE_API_KEY = $ENV.api_key' \
+                 >'/run/discourse-mail-receiver/mail-receiver-environment.json'
+            '';
+
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            RuntimeDirectory = "discourse-mail-receiver";
+            RuntimeDirectoryMode = "0700";
+            StateDirectory = "discourse-mail-receiver";
+            User = "discourse";
+            Group = "discourse";
+          };
+        });
+
+    services.discourse.siteSettings = {
+      required = {
+        notification_email = cfg.mail.notificationEmailAddress;
+        contact_email = cfg.mail.contactEmailAddress;
+      };
+      email = {
+        manual_polling_enabled = cfg.mail.incoming.enable;
+        reply_by_email_enabled = cfg.mail.incoming.enable;
+        reply_by_email_address = cfg.mail.incoming.replyEmailAddress;
+      };
+    };
+
+    services.postfix = lib.mkIf cfg.mail.incoming.enable {
+      enable = true;
+      sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else "";
+      sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else "";
+
+      origin = cfg.hostname;
+      relayDomains = [ cfg.hostname ];
+      config = {
+        smtpd_recipient_restrictions = "check_policy_service unix:private/discourse-policy";
+        append_dot_mydomain = lib.mkDefault false;
+        compatibility_level = "2";
+        smtputf8_enable = false;
+        smtpd_banner = lib.mkDefault "ESMTP server";
+        myhostname = lib.mkDefault cfg.hostname;
+        mydestination = lib.mkDefault "localhost";
+      };
+      transport = ''
+        ${cfg.hostname} discourse-mail-receiver:
+      '';
+      masterConfig = {
+        "discourse-mail-receiver" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "pipe";
+          args = [
+            "user=discourse"
+            "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/receive-mail"
+            "\${recipient}"
+          ];
+        };
+        "discourse-policy" = {
+          type = "unix";
+          privileged = true;
+          chroot = false;
+          command = "spawn";
+          args = [
+            "user=discourse"
+            "argv=${cfg.mail.incoming.mailReceiverPackage}/bin/discourse-smtp-fast-rejection"
+          ];
+        };
+      };
+    };
+
+    users.users = {
+      discourse = {
+        group = "discourse";
+        isSystemUser = true;
+      };
+    } // (lib.optionalAttrs cfg.nginx.enable {
+      ${config.services.nginx.user}.extraGroups = [ "discourse" ];
+    });
+
+    users.groups = {
+      discourse = {};
+    };
+
+    environment.systemPackages = [
+      cfg.package.rake
+    ];
+  };
+
+  meta.doc = ./discourse.xml;
+  meta.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
new file mode 100644
index 00000000000..1d6866e7b35
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.xml
@@ -0,0 +1,344 @@
+<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-discourse">
+ <title>Discourse</title>
+ <para>
+   <link xlink:href="https://www.discourse.org/">Discourse</link> is a
+   modern and open source discussion platform.
+ </para>
+
+ <section xml:id="module-services-discourse-basic-usage">
+   <title>Basic usage</title>
+   <para>
+     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+<programlisting>
+services.discourse = {
+  <link linkend="opt-services.discourse.enable">enable</link> = true;
+  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
+  admin = {
+    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
+    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
+    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
+    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+  };
+  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+};
+<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
+</programlisting>
+   </para>
+
+   <para>
+     Provided a proper DNS setup, you'll be able to connect to the
+     instance at <literal>discourse.example.com</literal> and log in
+     using the credentials provided in
+     <literal>services.discourse.admin</literal>.
+   </para>
+ </section>
+
+ <section xml:id="module-services-discourse-tls">
+   <title>Using a regular TLS certificate</title>
+   <para>
+     To set up TLS using a regular certificate and key on file, use
+     the <xref linkend="opt-services.discourse.sslCertificate" />
+     and <xref linkend="opt-services.discourse.sslCertificateKey" />
+     options:
+
+<programlisting>
+services.discourse = {
+  <link linkend="opt-services.discourse.enable">enable</link> = true;
+  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
+  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
+  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  admin = {
+    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
+    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
+    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
+    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+  };
+  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+};
+</programlisting>
+
+   </para>
+ </section>
+
+ <section xml:id="module-services-discourse-database">
+   <title>Database access</title>
+   <para>
+     <productname>Discourse</productname> uses
+     <productname>PostgreSQL</productname> to store most of its
+     data. A database will automatically be enabled and a database
+     and role created unless <xref
+     linkend="opt-services.discourse.database.host" /> is changed from
+     its default of <literal>null</literal> or <xref
+     linkend="opt-services.discourse.database.createLocally" /> is set
+     to <literal>false</literal>.
+   </para>
+
+   <para>
+     External database access can also be configured by setting
+     <xref linkend="opt-services.discourse.database.host" />, <xref
+     linkend="opt-services.discourse.database.username" /> and <xref
+     linkend="opt-services.discourse.database.passwordFile" /> as
+     appropriate. Note that you need to manually create a database
+     called <literal>discourse</literal> (or the name you chose in
+     <xref linkend="opt-services.discourse.database.name" />) and
+     allow the configured database user full access to it.
+   </para>
+ </section>
+
+ <section xml:id="module-services-discourse-mail">
+   <title>Email</title>
+   <para>
+     In addition to the basic setup, you'll want to configure an SMTP
+     server <productname>Discourse</productname> can use to send user
+     registration and password reset emails, among others. You can
+     also optionally let <productname>Discourse</productname> receive
+     email, which enables people to reply to threads and conversations
+     via email.
+   </para>
+
+   <para>
+     A basic setup which assumes you want to use your configured <link
+     linkend="opt-services.discourse.hostname">hostname</link> as
+     email domain can be done like this:
+
+<programlisting>
+services.discourse = {
+  <link linkend="opt-services.discourse.enable">enable</link> = true;
+  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
+  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
+  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  admin = {
+    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
+    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
+    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
+    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
+    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
+    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
+    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
+  };
+  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
+  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+};
+</programlisting>
+
+     This assumes you have set up an MX record for the address you've
+     set in <link linkend="opt-services.discourse.hostname">hostname</link> and
+     requires proper SPF, DKIM and DMARC configuration to be done for
+     the domain you're sending from, in order for email to be reliably delivered.
+   </para>
+
+   <para>
+     If you want to use a different domain for your outgoing email
+     (for example <literal>example.com</literal> instead of
+     <literal>discourse.example.com</literal>) you should set
+     <xref linkend="opt-services.discourse.mail.notificationEmailAddress" /> and
+     <xref linkend="opt-services.discourse.mail.contactEmailAddress" /> manually.
+   </para>
+
+   <note>
+     <para>
+       Setup of TLS for incoming email is currently only configured
+       automatically when a regular TLS certificate is used, i.e. when
+       <xref linkend="opt-services.discourse.sslCertificate" /> and
+       <xref linkend="opt-services.discourse.sslCertificateKey" /> are
+       set.
+     </para>
+   </note>
+
+ </section>
+
+ <section xml:id="module-services-discourse-settings">
+   <title>Additional settings</title>
+   <para>
+     Additional site settings and backend settings, for which no
+     explicit <productname>NixOS</productname> options are provided,
+     can be set in <xref linkend="opt-services.discourse.siteSettings" /> and
+     <xref linkend="opt-services.discourse.backendSettings" /> respectively.
+   </para>
+
+   <section xml:id="module-services-discourse-site-settings">
+     <title>Site settings</title>
+     <para>
+       <quote>Site settings</quote> are the settings that can be
+       changed through the <productname>Discourse</productname>
+       UI. Their <emphasis>default</emphasis> values can be set using
+       <xref linkend="opt-services.discourse.siteSettings" />.
+     </para>
+
+     <para>
+       Settings are expressed as a Nix attribute set which matches the
+       structure of the configuration in
+       <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">config/site_settings.yml</link>.
+       To find a setting's path, you only need to care about the first
+       two levels; i.e. its category (e.g. <literal>login</literal>)
+       and name (e.g. <literal>invite_only</literal>).
+     </para>
+
+     <para>
+       Settings containing secret data should be set to an attribute
+       set containing the attribute <literal>_secret</literal> - a
+       string pointing to a file containing the value the option
+       should be set to. See the example.
+     </para>
+   </section>
+
+   <section xml:id="module-services-discourse-backend-settings">
+     <title>Backend settings</title>
+     <para>
+       Settings are expressed as a Nix attribute set which matches the
+       structure of the configuration in
+       <link xlink:href="https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf">config/discourse.conf</link>.
+       Empty parameters can be defined by setting them to
+       <literal>null</literal>.
+     </para>
+   </section>
+
+   <section xml:id="module-services-discourse-settings-example">
+     <title>Example</title>
+     <para>
+       The following example sets the title and description of the
+       <productname>Discourse</productname> instance and enables
+       <productname>GitHub</productname> login in the site settings,
+       and changes a few request limits in the backend settings:
+<programlisting>
+services.discourse = {
+  <link linkend="opt-services.discourse.enable">enable</link> = true;
+  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
+  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
+  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  admin = {
+    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
+    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
+    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
+    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
+    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
+    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
+    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
+  };
+  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
+  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
+    required = {
+      title = "My Cats";
+      site_description = "Discuss My Cats (and be nice plz)";
+    };
+    login = {
+      enable_github_logins = true;
+      github_client_id = "a2f6dfe838cb3206ce20";
+      github_client_secret._secret = /run/keys/discourse_github_client_secret;
+    };
+  };
+  <link linkend="opt-services.discourse.backendSettings">backendSettings</link> = {
+    max_reqs_per_ip_per_minute = 300;
+    max_reqs_per_ip_per_10_seconds = 60;
+    max_asset_reqs_per_ip_per_10_seconds = 250;
+    max_reqs_per_ip_mode = "warn+block";
+  };
+  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+};
+</programlisting>
+     </para>
+     <para>
+       In the resulting site settings file, the
+       <literal>login.github_client_secret</literal> key will be set
+       to the contents of the
+       <filename>/run/keys/discourse_github_client_secret</filename>
+       file.
+     </para>
+   </section>
+ </section>
+  <section xml:id="module-services-discourse-plugins">
+    <title>Plugins</title>
+    <para>
+      You can install <productname>Discourse</productname> plugins
+      using the <xref linkend="opt-services.discourse.plugins" />
+      option. Pre-packaged plugins are provided in
+      <literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
+      you want the full suite of plugins provided through
+      <literal>nixpkgs</literal>, you can also set the <xref
+      linkend="opt-services.discourse.package" /> option to
+      <literal>pkgs.discourseAllPlugins</literal>.
+    </para>
+
+    <para>
+      Plugins can be built with the
+      <literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
+      function. Normally, it should suffice to provide a
+      <literal>name</literal> and <literal>src</literal> attribute. If
+      the plugin has Ruby dependencies, however, they need to be
+      packaged in accordance with the <link
+      xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
+      with Ruby</link> section of the Nixpkgs manual and the
+      appropriate gem options set in <literal>bundlerEnvArgs</literal>
+      (normally <literal>gemdir</literal> is sufficient). A plugin's
+      Ruby dependencies are listed in its
+      <filename>plugin.rb</filename> file as function calls to
+      <literal>gem</literal>. To construct the corresponding
+      <filename>Gemfile</filename>, run <command>bundle
+      init</command>, then add the <literal>gem</literal> lines to it
+      verbatim.
+    </para>
+
+    <para>
+      Some plugins provide <link
+      linkend="module-services-discourse-site-settings">site
+      settings</link>. Their defaults can be configured using <xref
+      linkend="opt-services.discourse.siteSettings" />, just like
+      regular site settings. To find the names of these settings, look
+      in the <literal>config/settings.yml</literal> file of the plugin
+      repo.
+    </para>
+
+    <para>
+      For example, to add the <link
+      xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
+      and <link
+      xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
+      plugins, and disable <literal>discourse-spoiler-alert</literal>
+      by default:
+
+<programlisting>
+services.discourse = {
+  <link linkend="opt-services.discourse.enable">enable</link> = true;
+  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
+  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
+  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
+  admin = {
+    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
+    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
+    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
+    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
+    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
+    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
+    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
+  };
+  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
+  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
+    discourse-spoiler-alert
+    discourse-solved
+  ];
+  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
+    plugins = {
+      spoiler_enabled = false;
+    };
+  };
+  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
+};
+</programlisting>
+
+    </para>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index d9ebb3a9880..685cb496703 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -193,7 +193,7 @@ let
                 };
                 sourceRoot = ".";
                 # We need unzip to build this package
-                buildInputs = [ pkgs.unzip ];
+                nativeBuildInputs = [ pkgs.unzip ];
                 # Installing simply means copying all files to the output directory
                 installPhase = "mkdir -p $out; cp -R * $out/";
               };
@@ -220,7 +220,7 @@ let
                   sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
                 };
                 # We need unzip to build this package
-                buildInputs = [ pkgs.unzip ];
+                nativeBuildInputs = [ pkgs.unzip ];
                 # Installing simply means copying all files to the output directory
                 installPhase = "mkdir -p $out; cp -R * $out/";
               };
@@ -329,14 +329,14 @@ in
           extraConfig = "internal;";
         };
 
-        locations."~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
+        locations."~ ^/lib.*\\.(js|css|gif|png|ico|jpg|jpeg)$" = {
           extraConfig = "expires 365d;";
         };
 
         locations."/" = {
           priority = 1;
           index = "doku.php";
-          extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
+          extraConfig = "try_files $uri $uri/ @dokuwiki;";
         };
 
         locations."@dokuwiki" = {
@@ -349,7 +349,7 @@ in
           '';
         };
 
-        locations."~ \.php$" = {
+        locations."~ \\.php$" = {
           extraConfig = ''
               try_files $uri $uri/ /doku.php;
               include ${pkgs.nginx}/conf/fastcgi_params;
diff --git a/nixos/modules/services/web-apps/engelsystem.nix b/nixos/modules/services/web-apps/engelsystem.nix
index 899582a2030..b87fecae65f 100644
--- a/nixos/modules/services/web-apps/engelsystem.nix
+++ b/nixos/modules/services/web-apps/engelsystem.nix
@@ -10,7 +10,7 @@ in {
         default = false;
         example = true;
         description = ''
-          Whether to enable engelsystem, an online tool for coordinating helpers
+          Whether to enable engelsystem, an online tool for coordinating volunteers
           and shifts on large events.
         '';
         type = lib.types.bool;
@@ -89,7 +89,7 @@ in {
     # create database
     services.mysql = mkIf cfg.createDatabase {
       enable = true;
-      package = mkDefault pkgs.mysql;
+      package = mkDefault pkgs.mariadb;
       ensureUsers = [{
         name = "engelsystem";
         ensurePermissions = { "engelsystem.*" = "ALL PRIVILEGES"; };
diff --git a/nixos/modules/services/web-apps/frab.nix b/nixos/modules/services/web-apps/frab.nix
deleted file mode 100644
index 1b5890d6b0c..00000000000
--- a/nixos/modules/services/web-apps/frab.nix
+++ /dev/null
@@ -1,222 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.frab;
-
-  package = pkgs.frab;
-
-  databaseConfig = builtins.toJSON { production = cfg.database; };
-
-  frabEnv = {
-    RAILS_ENV = "production";
-    RACK_ENV = "production";
-    SECRET_KEY_BASE = cfg.secretKeyBase;
-    FRAB_HOST = cfg.host;
-    FRAB_PROTOCOL = cfg.protocol;
-    FROM_EMAIL = cfg.fromEmail;
-    RAILS_SERVE_STATIC_FILES = "1";
-  } // cfg.extraEnvironment;
-
-  frab-rake = pkgs.stdenv.mkDerivation {
-    name = "frab-rake";
-    buildInputs = [ package.env pkgs.makeWrapper ];
-    phases = "installPhase fixupPhase";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${package.env}/bin/bundle $out/bin/frab-bundle \
-          ${concatStrings (mapAttrsToList (name: value: "--set ${name} '${value}' ") frabEnv)} \
-          --set PATH '${lib.makeBinPath (with pkgs; [ nodejs file imagemagick ])}:$PATH' \
-          --set RAKEOPT '-f ${package}/share/frab/Rakefile' \
-          --run 'cd ${package}/share/frab'
-      makeWrapper $out/bin/frab-bundle $out/bin/frab-rake \
-          --add-flags "exec rake"
-     '';
-  };
-
-in
-
-{
-  options = {
-    services.frab = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Enable the frab service.
-        '';
-      };
-
-      host = mkOption {
-        type = types.str;
-        example = "frab.example.com";
-        description = ''
-          Hostname under which this frab instance can be reached.
-        '';
-      };
-
-      protocol = mkOption {
-        type = types.str;
-        default = "https";
-        example = "http";
-        description = ''
-          Either http or https, depending on how your Frab instance
-          will be exposed to the public.
-        '';
-      };
-
-      fromEmail = mkOption {
-        type = types.str;
-        default = "frab@localhost";
-        description = ''
-          Email address used by frab.
-        '';
-      };
-
-      listenAddress = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = ''
-          Address or hostname frab should listen on.
-        '';
-      };
-
-      listenPort = mkOption {
-        type = types.int;
-        default = 3000;
-        description = ''
-          Port frab should listen on.
-        '';
-      };
-
-      statePath = mkOption {
-        type = types.str;
-        default = "/var/lib/frab";
-        description = ''
-          Directory where frab keeps its state.
-        '';
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "frab";
-        description = ''
-          User to run frab.
-        '';
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "frab";
-        description = ''
-          Group to run frab.
-        '';
-      };
-
-      secretKeyBase = mkOption {
-        type = types.str;
-        description = ''
-          Your secret key is used for verifying the integrity of signed cookies.
-          If you change this key, all old signed cookies will become invalid!
-
-          Make sure the secret is at least 30 characters and all random,
-          no regular words or you'll be exposed to dictionary attacks.
-        '';
-      };
-
-      database = mkOption {
-        type = types.attrs;
-        default = {
-          adapter = "sqlite3";
-          database = "/var/lib/frab/db.sqlite3";
-          pool = 5;
-          timeout = 5000;
-        };
-        example = {
-          adapter = "postgresql";
-          database = "frab";
-          host = "localhost";
-          username = "frabuser";
-          password = "supersecret";
-          encoding = "utf8";
-          pool = 5;
-        };
-        description = ''
-          Rails database configuration for Frab as Nix attribute set.
-        '';
-      };
-
-      extraEnvironment = mkOption {
-        type = types.attrs;
-        default = {};
-        example = {
-          FRAB_CURRENCY_UNIT = "€";
-          FRAB_CURRENCY_FORMAT = "%n%u";
-          EXCEPTION_EMAIL = "frab-owner@example.com";
-          SMTP_ADDRESS = "localhost";
-          SMTP_PORT = "587";
-          SMTP_DOMAIN = "localdomain";
-          SMTP_USER_NAME = "root";
-          SMTP_PASSWORD = "toor";
-          SMTP_AUTHENTICATION = "1";
-          SMTP_NOTLS = "1";
-        };
-        description = ''
-          Additional environment variables to set for frab for further
-          configuration. See the frab documentation for more information.
-        '';
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ frab-rake ];
-
-    users.users.${cfg.user} =
-      { group = cfg.group;
-        home = "${cfg.statePath}";
-        isSystemUser = true;
-      };
-
-    users.groups.${cfg.group} = { };
-
-    systemd.tmpfiles.rules = [
-      "d '${cfg.statePath}/system/attachments' - ${cfg.user} ${cfg.group} - -"
-    ];
-
-    systemd.services.frab = {
-      after = [ "network.target" "gitlab.service" ];
-      wantedBy = [ "multi-user.target" ];
-      environment = frabEnv;
-
-      preStart = ''
-        ln -sf ${pkgs.writeText "frab-database.yml" databaseConfig} /run/frab/database.yml
-        ln -sf ${cfg.statePath}/system /run/frab/system
-
-        if ! test -e "${cfg.statePath}/db-setup-done"; then
-          ${frab-rake}/bin/frab-rake db:setup
-          touch ${cfg.statePath}/db-setup-done
-        else
-          ${frab-rake}/bin/frab-rake db:migrate
-        fi
-      '';
-
-      serviceConfig = {
-        PrivateTmp = true;
-        PrivateDevices = true;
-        Type = "simple";
-        User = cfg.user;
-        Group = cfg.group;
-        TimeoutSec = "300s";
-        Restart = "on-failure";
-        RestartSec = "10s";
-        RuntimeDirectory = "frab";
-        WorkingDirectory = "${package}/share/frab";
-        ExecStart = "${frab-rake}/bin/frab-bundle exec rails server " +
-          "--binding=${cfg.listenAddress} --port=${toString cfg.listenPort}";
-      };
-    };
-
-  };
-}
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
new file mode 100644
index 00000000000..dd63857a55c
--- /dev/null
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -0,0 +1,180 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.galene;
+  defaultstateDir = "/var/lib/galene";
+  defaultrecordingsDir = "${cfg.stateDir}/recordings";
+  defaultgroupsDir = "${cfg.stateDir}/groups";
+  defaultdataDir = "${cfg.stateDir}/data";
+in
+{
+  options = {
+    services.galene = {
+      enable = mkEnableOption "Galene Service.";
+
+      stateDir = mkOption {
+        default = defaultstateDir;
+        type = types.str;
+        description = ''
+          The directory where Galene stores its internal state. If left as the default
+          value this directory will automatically be created before the Galene server
+          starts, otherwise the sysadmin is responsible for ensuring the directory
+          exists with appropriate ownership and permissions.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "galene";
+        description = "User account under which galene runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "galene";
+        description = "Group under which galene runs.";
+      };
+
+      insecure = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether Galene should listen in http or in https. If left as the default
+          value (false), Galene needs to be fed a private key and a certificate.
+        '';
+      };
+
+      certFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/cert.pem";
+        description = ''
+          Path to the server's certificate. The file is copied at runtime to
+          Galene's data directory where it needs to reside.
+        '';
+      };
+
+      keyFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/key.pem";
+        description = ''
+          Path to the server's private key. The file is copied at runtime to
+          Galene's data directory where it needs to reside.
+        '';
+      };
+
+      httpAddress = mkOption {
+        type = types.str;
+        default = "";
+        description = "HTTP listen address for galene.";
+      };
+
+      httpPort = mkOption {
+        type = types.port;
+        default = 8443;
+        description = "HTTP listen port.";
+      };
+
+      staticDir = mkOption {
+        type = types.str;
+        default = "${cfg.package.static}/static";
+        example = "/var/lib/galene/static";
+        description = "Web server directory.";
+      };
+
+      recordingsDir = mkOption {
+        type = types.str;
+        default = defaultrecordingsDir;
+        example = "/var/lib/galene/recordings";
+        description = "Recordings directory.";
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = defaultdataDir;
+        example = "/var/lib/galene/data";
+        description = "Data directory.";
+      };
+
+      groupsDir = mkOption {
+        type = types.str;
+        default = defaultgroupsDir;
+        example = "/var/lib/galene/groups";
+        description = "Web server directory.";
+      };
+
+      package = mkOption {
+        default = pkgs.galene;
+        defaultText = "pkgs.galene";
+        type = types.package;
+        description = ''
+          Package for running Galene.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.insecure || (cfg.certFile != null && cfg.keyFile != null);
+        message = ''
+          Galene needs both certFile and keyFile defined for encryption, or
+          the insecure flag.
+        '';
+      }
+    ];
+
+    systemd.services.galene = {
+      description = "galene";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        ${optionalString (cfg.insecure != true) ''
+           install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.certFile} ${cfg.dataDir}/cert.pem
+           install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.keyFile} ${cfg.dataDir}/key.pem
+        ''}
+      '';
+
+      serviceConfig = mkMerge [
+        {
+          Type = "simple";
+          User = cfg.user;
+          Group = cfg.group;
+          WorkingDirectory = cfg.stateDir;
+          ExecStart = ''${cfg.package}/bin/galene \
+          ${optionalString (cfg.insecure) "-insecure"} \
+          -data ${cfg.dataDir} \
+          -groups ${cfg.groupsDir} \
+          -recordings ${cfg.recordingsDir} \
+          -static ${cfg.staticDir}'';
+          Restart = "always";
+          # Upstream Requirements
+          LimitNOFILE = 65536;
+          StateDirectory = [ ] ++
+            optional (cfg.stateDir == defaultstateDir) "galene" ++
+            optional (cfg.dataDir == defaultdataDir) "galene/data" ++
+            optional (cfg.groupsDir == defaultgroupsDir) "galene/groups" ++
+            optional (cfg.recordingsDir == defaultrecordingsDir) "galene/recordings";
+        }
+      ];
+    };
+
+    users.users = mkIf (cfg.user == "galene")
+      {
+        galene = {
+          description = "galene Service";
+          group = cfg.group;
+          isSystemUser = true;
+        };
+      };
+
+    users.groups = mkIf (cfg.group == "galene") {
+      galene = { };
+    };
+  };
+  meta.maintainers = with lib.maintainers; [ rgrunbla ];
+}
diff --git a/nixos/modules/services/web-apps/gerrit.nix b/nixos/modules/services/web-apps/gerrit.nix
index 657b1a4fc5b..864587aea56 100644
--- a/nixos/modules/services/web-apps/gerrit.nix
+++ b/nixos/modules/services/web-apps/gerrit.nix
@@ -143,7 +143,7 @@ in
           Set a UUID that uniquely identifies the server.
 
           This can be generated with
-          <literal>nix-shell -p utillinux --run uuidgen</literal>.
+          <literal>nix-shell -p util-linux --run uuidgen</literal>.
         '';
       };
     };
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
index 568bdfd0c42..be2de638dd9 100644
--- a/nixos/modules/services/web-apps/grocy.nix
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -115,9 +115,9 @@ in {
       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;
+      # PHP 7.4 is the only version which is supported/tested by upstream:
+      # https://github.com/grocy/grocy/blob/v3.0.0/README.md#how-to-install
+      phpPackage = pkgs.php74;
 
       inherit (cfg.phpfpm) settings;
 
diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index ab922a38e5c..d940f3d3dae 100644
--- a/nixos/modules/services/web-apps/codimd.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -3,31 +3,45 @@
 with lib;
 
 let
-  cfg = config.services.codimd;
+  cfg = config.services.hedgedoc;
+
+  # 21.03 will not be an official release - it was instead 21.05.  This
+  # versionAtLeast statement remains set to 21.03 for backwards compatibility.
+  # See https://github.com/NixOS/nixpkgs/pull/108899 and
+  # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
+  name = if versionAtLeast config.system.stateVersion "21.03"
+    then "hedgedoc"
+    else "codimd";
 
   prettyJSON = conf:
-    pkgs.runCommand "codimd-config.json" { preferLocalBuild = true; } ''
-      echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq \
+    pkgs.runCommandLocal "hedgedoc-config.json" {
+      nativeBuildInputs = [ pkgs.jq ];
+    } ''
+      echo '${builtins.toJSON conf}' | jq \
         '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out
     '';
 in
 {
-  options.services.codimd = {
-    enable = mkEnableOption "the CodiMD Markdown Editor";
+  imports = [
+    (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ])
+  ];
+
+  options.services.hedgedoc = {
+    enable = mkEnableOption "the HedgeDoc Markdown Editor";
 
     groups = mkOption {
       type = types.listOf types.str;
       default = [];
       description = ''
-        Groups to which the codimd user should be added.
+        Groups to which the user ${name} should be added.
       '';
     };
 
     workDir = mkOption {
       type = types.path;
-      default = "/var/lib/codimd";
+      default = "/var/lib/${name}";
       description = ''
-        Working directory for the CodiMD service.
+        Working directory for the HedgeDoc service.
       '';
     };
 
@@ -36,17 +50,17 @@ in
       domain = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "codimd.org";
+        example = "hedgedoc.org";
         description = ''
-          Domain name for the CodiMD instance.
+          Domain name for the HedgeDoc instance.
         '';
       };
       urlPath = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/url/path/to/codimd";
+        example = "/url/path/to/hedgedoc";
         description = ''
-          Path under which CodiMD is accessible.
+          Path under which HedgeDoc is accessible.
         '';
       };
       host = mkOption {
@@ -67,7 +81,7 @@ in
       path = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/run/codimd.sock";
+        example = "/run/hedgedoc.sock";
         description = ''
           Specify where a UNIX domain socket should be placed.
         '';
@@ -75,7 +89,7 @@ in
       allowOrigin = mkOption {
         type = types.listOf types.str;
         default = [];
-        example = [ "localhost" "codimd.org" ];
+        example = [ "localhost" "hedgedoc.org" ];
         description = ''
           List of domains to whitelist.
         '';
@@ -199,7 +213,7 @@ in
         '';
         description = ''
           Specify which database to use.
-          CodiMD supports mysql, postgres, sqlite and mssql.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
           See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
           https://sequelize.readthedocs.io/en/v3/</link> for more information.
           Note: This option overrides <option>db</option>.
@@ -211,12 +225,12 @@ in
         example = literalExample ''
           {
             dialect = "sqlite";
-            storage = "/var/lib/codimd/db.codimd.sqlite";
+            storage = "/var/lib/${name}/db.${name}.sqlite";
           }
         '';
         description = ''
           Specify the configuration for sequelize.
-          CodiMD supports mysql, postgres, sqlite and mssql.
+          HedgeDoc supports mysql, postgres, sqlite and mssql.
           See <link xlink:href="https://sequelize.readthedocs.io/en/v3/">
           https://sequelize.readthedocs.io/en/v3/</link> for more information.
           Note: This option overrides <option>db</option>.
@@ -225,7 +239,7 @@ in
       sslKeyPath= mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/codimd/codimd.key";
+        example = "/var/lib/hedgedoc/hedgedoc.key";
         description = ''
           Path to the SSL key. Needed when <option>useSSL</option> is enabled.
         '';
@@ -233,7 +247,7 @@ in
       sslCertPath = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/codimd/codimd.crt";
+        example = "/var/lib/hedgedoc/hedgedoc.crt";
         description = ''
           Path to the SSL cert. Needed when <option>useSSL</option> is enabled.
         '';
@@ -241,7 +255,7 @@ in
       sslCAPath = mkOption {
         type = types.listOf types.str;
         default = [];
-        example = [ "/var/lib/codimd/ca.crt" ];
+        example = [ "/var/lib/hedgedoc/ca.crt" ];
         description = ''
           SSL ca chain. Needed when <option>useSSL</option> is enabled.
         '';
@@ -249,7 +263,7 @@ in
       dhParamPath = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "/var/lib/codimd/dhparam.pem";
+        example = "/var/lib/hedgedoc/dhparam.pem";
         description = ''
           Path to the SSL dh params. Needed when <option>useSSL</option> is enabled.
         '';
@@ -258,10 +272,10 @@ in
         type = types.str;
         default = "/tmp";
         description = ''
-          Path to the temp directory CodiMD should use.
+          Path to the temp directory HedgeDoc should use.
           Note that <option>serviceConfig.PrivateTmp</option> is enabled for
-          the CodiMD systemd service by default.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          the HedgeDoc systemd service by default.
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       defaultNotePath = mkOption {
@@ -269,7 +283,7 @@ in
         default = "./public/default.md";
         description = ''
           Path to the default Note file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       docsPath = mkOption {
@@ -277,7 +291,7 @@ in
         default = "./public/docs";
         description = ''
           Path to the docs directory.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       indexPath = mkOption {
@@ -285,7 +299,7 @@ in
         default = "./public/views/index.ejs";
         description = ''
           Path to the index template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       hackmdPath = mkOption {
@@ -293,7 +307,7 @@ in
         default = "./public/views/hackmd.ejs";
         description = ''
           Path to the hackmd template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       errorPath = mkOption {
@@ -302,7 +316,7 @@ in
         defaultText = "./public/views/error.ejs";
         description = ''
           Path to the error template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       prettyPath = mkOption {
@@ -311,7 +325,7 @@ in
         defaultText = "./public/views/pretty.ejs";
         description = ''
           Path to the pretty template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       slidePath = mkOption {
@@ -320,13 +334,13 @@ in
         defaultText = "./public/views/slide.hbs";
         description = ''
           Path to the slide template file.
-          (Non-canonical paths are relative to CodiMD's base directory)
+          (Non-canonical paths are relative to HedgeDoc's base directory)
         '';
       };
       uploadsPath = mkOption {
         type = types.str;
         default = "${cfg.workDir}/uploads";
-        defaultText = "/var/lib/codimd/uploads";
+        defaultText = "/var/lib/${name}/uploads";
         description = ''
           Path under which uploaded files are saved.
         '';
@@ -764,7 +778,7 @@ in
               type = types.str;
               default = "";
               description = ''
-                LDAP field which is used as the username on CodiMD.
+                LDAP field which is used as the username on HedgeDoc.
                 By default <option>useridField</option> is used.
               '';
             };
@@ -772,7 +786,7 @@ in
               type = types.str;
               example = "uid";
               description = ''
-                LDAP field which is a unique identifier for users on CodiMD.
+                LDAP field which is a unique identifier for users on HedgeDoc.
               '';
             };
             tlsca = mkOption {
@@ -838,7 +852,7 @@ in
             requiredGroups = mkOption {
               type = types.listOf types.str;
               default = [];
-              example = [ "Hackmd-users" "Codimd-users" ];
+              example = [ "Hedgedoc-Users" ];
               description = ''
                 Required group names.
               '';
@@ -877,6 +891,44 @@ in
         description = "Configure the SAML integration.";
       };
     };
+
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/var/lib/hedgedoc/hedgedoc.env";
+      description = ''
+        Environment file as defined in <citerefentry>
+        <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+        </citerefentry>.
+
+        Secrets may be passed to the service without adding them to the world-readable
+        Nix store, by specifying placeholder variables as the option value in Nix and
+        setting these variables accordingly in the environment file.
+
+        <programlisting>
+          # snippet of HedgeDoc-related config
+          services.hedgedoc.configuration.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb";
+          services.hedgedoc.configuration.minio.secretKey = "$MINIO_SECRET_KEY";
+        </programlisting>
+
+        <programlisting>
+          # content of the environment file
+          DB_PASSWORD=verysecretdbpassword
+          MINIO_SECRET_KEY=verysecretminiokey
+        </programlisting>
+
+        Note that this file needs to be available on the host on which
+        <literal>HedgeDoc</literal> is running.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.hedgedoc;
+      description = ''
+        Package that provides HedgeDoc.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
@@ -884,31 +936,37 @@ in
       { assertion = cfg.configuration.db == {} -> (
           cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null
         );
-        message = "Database configuration for CodiMD missing."; }
+        message = "Database configuration for HedgeDoc missing."; }
     ];
-    users.groups.codimd = {};
-    users.users.codimd = {
-      description = "CodiMD service user";
-      group = "codimd";
+    users.groups.${name} = {};
+    users.users.${name} = {
+      description = "HedgeDoc service user";
+      group = name;
       extraGroups = cfg.groups;
       home = cfg.workDir;
       createHome = true;
       isSystemUser = true;
     };
 
-    systemd.services.codimd = {
-      description = "CodiMD Service";
+    systemd.services.hedgedoc = {
+      description = "HedgeDoc Service";
       wantedBy = [ "multi-user.target" ];
       after = [ "networking.target" ];
+      preStart = ''
+        ${pkgs.envsubst}/bin/envsubst \
+          -o ${cfg.workDir}/config.json \
+          -i ${prettyJSON cfg.configuration}
+      '';
       serviceConfig = {
         WorkingDirectory = cfg.workDir;
-        ExecStart = "${pkgs.codimd}/bin/codimd";
+        ExecStart = "${cfg.package}/bin/hedgedoc";
+        EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
         Environment = [
-          "CMD_CONFIG_FILE=${prettyJSON cfg.configuration}"
+          "CMD_CONFIG_FILE=${cfg.workDir}/config.json"
           "NODE_ENV=production"
         ];
         Restart = "always";
-        User = "codimd";
+        User = name;
         PrivateTmp = true;
       };
     };
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
new file mode 100644
index 00000000000..a69767194c3
--- /dev/null
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -0,0 +1,142 @@
+{ lib, pkgs, config, ... }:
+with lib;
+let
+  cfg = config.services.hledger-web;
+in {
+  options.services.hledger-web = {
+
+    enable = mkEnableOption "hledger-web service";
+
+    serveApi = mkEnableOption "Serve only the JSON web API, without the web UI.";
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      description = ''
+        Address to listen on.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5000;
+      example = "80";
+      description = ''
+        Port to listen on.
+      '';
+    };
+
+    capabilities = {
+      view = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable the view capability.
+        '';
+      };
+      add = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the add capability.
+        '';
+      };
+      manage = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the manage capability.
+        '';
+      };
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/hledger-web";
+      description = ''
+        Path the service has access to. If left as the default value this
+        directory will automatically be created before the hledger-web server
+        starts, otherwise the sysadmin is responsible for ensuring the
+        directory exists with appropriate ownership and permissions.
+      '';
+    };
+
+    journalFiles = mkOption {
+      type = types.listOf types.str;
+      default = [ ".hledger.journal" ];
+      description = ''
+        Paths to journal files relative to <option>services.hledger-web.stateDir</option>.
+      '';
+    };
+
+    baseUrl = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "https://example.org";
+      description = ''
+        Base URL, when sharing over a network.
+      '';
+    };
+
+    extraOptions = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [ "--forecast" ];
+      description = ''
+        Extra command line arguments to pass to hledger-web.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    users.users.hledger = {
+      name = "hledger";
+      group = "hledger";
+      isSystemUser = true;
+      home = cfg.stateDir;
+      useDefaultShell = true;
+    };
+
+    users.groups.hledger = {};
+
+    systemd.services.hledger-web = let
+      capabilityString = with cfg.capabilities; concatStringsSep "," (
+        (optional view "view")
+        ++ (optional add "add")
+        ++ (optional manage "manage")
+      );
+      serverArgs = with cfg; escapeShellArgs ([
+        "--serve"
+        "--host=${host}"
+        "--port=${toString port}"
+        "--capabilities=${capabilityString}"
+        (optionalString (cfg.baseUrl != null) "--base-url=${cfg.baseUrl}")
+        (optionalString (cfg.serveApi) "--serve-api")
+      ] ++ (map (f: "--file=${stateDir}/${f}") cfg.journalFiles)
+        ++ extraOptions);
+    in {
+      description = "hledger-web - web-app for the hledger accounting tool.";
+      documentation = [ https://hledger.org/hledger-web.html ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "networking.target" ];
+      serviceConfig = mkMerge [
+        {
+          ExecStart = "${pkgs.hledger-web}/bin/hledger-web ${serverArgs}";
+          Restart = "always";
+          WorkingDirectory = cfg.stateDir;
+          User = "hledger";
+          Group = "hledger";
+          PrivateTmp = true;
+        }
+        (mkIf (cfg.stateDir == "/var/lib/hledger-web") {
+          StateDirectory = "hledger-web";
+        })
+      ];
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ marijanp erictapen ];
+}
diff --git a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
index d9ad7e9e3d3..f8f0854f1bc 100644
--- a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
+++ b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -23,6 +23,16 @@ in {
       '';
     };
 
+    libraryPaths = mkOption {
+      type = attrsOf package;
+      default = { };
+      description = ''
+        Libraries to add to the Icingaweb2 library path.
+        The name of the attribute is the name of the library, the value
+        is the package to add.
+      '';
+    };
+
     virtualHost = mkOption {
       type = nullOr str;
       default = "icingaweb2";
@@ -167,8 +177,11 @@ in {
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         user = "icingaweb2";
+        phpEnv = {
+          ICINGAWEB_LIBDIR = toString (pkgs.linkFarm "icingaweb2-libdir" (mapAttrsToList (name: path: { inherit name path; }) cfg.libraryPaths));
+        };
+        phpPackage = pkgs.php.withExtensions ({ enabled, all }: [ all.imagick ] ++ enabled);
         phpOptions = ''
-          extension = ${pkgs.phpPackages.imagick}/lib/php/extensions/imagick.so
           date.timezone = "${cfg.timezone}"
         '';
         settings = mapAttrs (name: mkDefault) {
@@ -184,6 +197,11 @@ in {
       };
     };
 
+    services.icingaweb2.libraryPaths = {
+      ipl = pkgs.icingaweb2-ipl;
+      thirdparty = pkgs.icingaweb2-thirdparty;
+    };
+
     systemd.services."phpfpm-${poolName}".serviceConfig.ReadWritePaths = [ "/etc/icingaweb2" ];
 
     services.nginx = {
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
index 68769ac8c03..b4987fa4702 100644
--- a/nixos/modules/services/web-apps/ihatemoney/default.nix
+++ b/nixos/modules/services/web-apps/ihatemoney/default.nix
@@ -44,7 +44,7 @@ let
 in
   {
     options.services.ihatemoney = {
-      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode running as root";
+      enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode";
       backend = mkOption {
         type = types.enum [ "sqlite" "postgresql" ];
         default = "sqlite";
@@ -116,16 +116,13 @@ in
       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;
+            immediate-uid = user;
+            immediate-gid = group;
             # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
             enable-threads = true;
             module = "wsgi:application";
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
index 3b2b2440491..997604754e4 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -186,9 +186,10 @@ in
         }
       ];
       extraModules = [ "pubsub" ];
+      extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ];
       extraConfig = mkAfter ''
-        Component "focus.${cfg.hostName}"
-          component_secret = os.getenv("JICOFO_COMPONENT_SECRET")
+        Component "focus.${cfg.hostName}" "client_proxy"
+          target_address = "focus@auth.${cfg.hostName}"
       '';
       virtualHosts.${cfg.hostName} = {
         enabled = true;
@@ -254,6 +255,7 @@ in
       + optionalString cfg.prosody.enable ''
         ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
         ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
+        ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
 
         # generate self-signed certificates
         if [ ! -f /var/lib/jitsi-meet.crt ]; then
@@ -329,5 +331,6 @@ in
     };
   };
 
+  meta.doc = ./jitsi-meet.xml;
   meta.maintainers = lib.teams.jitsi.members;
 }
diff --git a/nixos/modules/services/web-apps/jitsi-meet.xml b/nixos/modules/services/web-apps/jitsi-meet.xml
new file mode 100644
index 00000000000..97373bc6d9a
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.xml
@@ -0,0 +1,55 @@
+<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-jitsi-meet">
+ <title>Jitsi Meet</title>
+ <para>
+   With Jitsi Meet on NixOS you can quickly configure a complete,
+   private, self-hosted video conferencing solution.
+ </para>
+
+ <section xml:id="module-services-jitsi-basic-usage">
+ <title>Basic usage</title>
+   <para>
+     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+<programlisting>{
+  services.jitsi-meet = {
+    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
+    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
+  };
+  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
+  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
+  <link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
+}</programlisting>
+   </para>
+ </section>
+
+ <section xml:id="module-services-jitsi-configuration">
+ <title>Configuration</title>
+   <para>
+     Here is the minimal configuration with additional configurations:
+<programlisting>{
+  services.jitsi-meet = {
+    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
+    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
+    <link linkend="opt-services.jitsi-meet.config">config</link> = {
+      enableWelcomePage = false;
+      prejoinPageEnabled = true;
+      defaultLang = "fi";
+    };
+    <link linkend="opt-services.jitsi-meet.interfaceConfig">interfaceConfig</link> = {
+      SHOW_JITSI_WATERMARK = false;
+      SHOW_WATERMARK_FOR_GUESTS = false;
+    };
+  };
+  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
+  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
+  <link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
+  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
+}</programlisting>
+   </para>
+ </section>
+
+</chapter>
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
new file mode 100644
index 00000000000..dc66c296656
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -0,0 +1,736 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.keycloak;
+in
+{
+  options.services.keycloak = {
+
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether to enable the Keycloak identity and access management
+        server.
+      '';
+    };
+
+    bindAddress = lib.mkOption {
+      type = lib.types.str;
+      default = "\${jboss.bind.address:0.0.0.0}";
+      example = "127.0.0.1";
+      description = ''
+        On which address Keycloak should accept new connections.
+
+        A special syntax can be used to allow command line Java system
+        properties to override the value: ''${property.name:value}
+      '';
+    };
+
+    httpPort = lib.mkOption {
+      type = lib.types.str;
+      default = "\${jboss.http.port:80}";
+      example = "8080";
+      description = ''
+        On which port Keycloak should listen for new HTTP connections.
+
+        A special syntax can be used to allow command line Java system
+        properties to override the value: ''${property.name:value}
+      '';
+    };
+
+    httpsPort = lib.mkOption {
+      type = lib.types.str;
+      default = "\${jboss.https.port:443}";
+      example = "8443";
+      description = ''
+        On which port Keycloak should listen for new HTTPS connections.
+
+        A special syntax can be used to allow command line Java system
+        properties to override the value: ''${property.name:value}
+      '';
+    };
+
+    frontendUrl = lib.mkOption {
+      type = lib.types.str;
+      apply = x: if lib.hasSuffix "/" x then x else x + "/";
+      example = "keycloak.example.com/auth";
+      description = ''
+        The public URL used as base for all frontend requests. Should
+        normally include a trailing <literal>/auth</literal>.
+
+        See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
+        Hostname section of the Keycloak server installation
+        manual</link> for more information.
+      '';
+    };
+
+    forceBackendUrlToFrontendUrl = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      example = true;
+      description = ''
+        Whether Keycloak should force all requests to go through the
+        frontend URL configured in <xref
+        linkend="opt-services.keycloak.frontendUrl" />. By default,
+        Keycloak allows backend requests to instead use its local
+        hostname or IP address and may also advertise it to clients
+        through its OpenID Connect Discovery endpoint.
+
+        See <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
+        Hostname section of the Keycloak server installation
+        manual</link> for more information.
+      '';
+    };
+
+    sslCertificate = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/run/keys/ssl_cert";
+      description = ''
+        The path to a PEM formatted certificate to use for TLS/SSL
+        connections.
+
+        This should be a string, not a Nix path, since Nix paths are
+        copied into the world-readable Nix store.
+      '';
+    };
+
+    sslCertificateKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/run/keys/ssl_key";
+      description = ''
+        The path to a PEM formatted private key to use for TLS/SSL
+        connections.
+
+        This should be a string, not a Nix path, since Nix paths are
+        copied into the world-readable Nix store.
+      '';
+    };
+
+    database = {
+      type = lib.mkOption {
+        type = lib.types.enum [ "mysql" "postgresql" ];
+        default = "postgresql";
+        example = "mysql";
+        description = ''
+          The type of database Keycloak should connect to.
+        '';
+      };
+
+      host = lib.mkOption {
+        type = lib.types.str;
+        default = "localhost";
+        description = ''
+          Hostname of the database to connect to.
+        '';
+      };
+
+      port =
+        let
+          dbPorts = {
+            postgresql = 5432;
+            mysql = 3306;
+          };
+        in
+          lib.mkOption {
+            type = lib.types.port;
+            default = dbPorts.${cfg.database.type};
+            description = ''
+              Port of the database to connect to.
+            '';
+          };
+
+      useSSL = lib.mkOption {
+        type = lib.types.bool;
+        default = cfg.database.host != "localhost";
+        description = ''
+          Whether the database connection should be secured by SSL /
+          TLS.
+        '';
+      };
+
+      caCert = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        description = ''
+          The SSL / TLS CA certificate that verifies the identity of the
+          database server.
+
+          Required when PostgreSQL is used and SSL is turned on.
+
+          For MySQL, if left at <literal>null</literal>, the default
+          Java keystore is used, which should suffice if the server
+          certificate is issued by an official CA.
+        '';
+      };
+
+      createLocally = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Whether a database should be automatically created on the
+          local host. Set this to false if you plan on provisioning a
+          local database yourself. This has no effect if
+          services.keycloak.database.host is customized.
+        '';
+      };
+
+      username = lib.mkOption {
+        type = lib.types.str;
+        default = "keycloak";
+        description = ''
+          Username to use when connecting to an external or manually
+          provisioned database; has no effect when a local database is
+          automatically provisioned.
+
+          To use this with a local database, set <xref
+          linkend="opt-services.keycloak.database.createLocally" /> to
+          <literal>false</literal> and create the database and user
+          manually. The database should be called
+          <literal>keycloak</literal>.
+        '';
+      };
+
+      passwordFile = lib.mkOption {
+        type = lib.types.path;
+        example = "/run/keys/db_password";
+        description = ''
+          File containing the database password.
+
+          This should be a string, not a Nix path, since Nix paths are
+          copied into the world-readable Nix store.
+        '';
+      };
+    };
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.keycloak;
+      description = ''
+        Keycloak package to use.
+      '';
+    };
+
+    initialAdminPassword = lib.mkOption {
+      type = lib.types.str;
+      default = "changeme";
+      description = ''
+        Initial password set for the <literal>admin</literal>
+        user. The password is not stored safely and should be changed
+        immediately in the admin panel.
+      '';
+    };
+
+    extraConfig = lib.mkOption {
+      type = lib.types.attrs;
+      default = { };
+      example = lib.literalExample ''
+        {
+          "subsystem=keycloak-server" = {
+            "spi=hostname" = {
+              "provider=default" = null;
+              "provider=fixed" = {
+                enabled = true;
+                properties.hostname = "keycloak.example.com";
+              };
+              default-provider = "fixed";
+            };
+          };
+        }
+      '';
+      description = ''
+        Additional Keycloak configuration options to set in
+        <literal>standalone.xml</literal>.
+
+        Options are expressed as a Nix attribute set which matches the
+        structure of the jboss-cli configuration. The configuration is
+        effectively overlayed on top of the default configuration
+        shipped with Keycloak. To remove existing nodes and undefine
+        attributes from the default configuration, set them to
+        <literal>null</literal>.
+
+        The example configuration does the equivalent of the following
+        script, which removes the hostname provider
+        <literal>default</literal>, adds the deprecated hostname
+        provider <literal>fixed</literal> and defines it the default:
+
+        <programlisting>
+        /subsystem=keycloak-server/spi=hostname/provider=default:remove()
+        /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
+        /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
+        </programlisting>
+
+        You can discover available options by using the <link
+        xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
+        program and by referring to the <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
+        Server Installation and Configuration Guide</link>.
+      '';
+    };
+
+  };
+
+  config =
+    let
+      # We only want to create a database if we're actually going to connect to it.
+      databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
+      createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
+      createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql";
+
+      mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} ''
+        ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
+      '';
+
+      keycloakConfig' = builtins.foldl' lib.recursiveUpdate {
+        "interface=public".inet-address = cfg.bindAddress;
+        "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort;
+        "subsystem=keycloak-server"."spi=hostname" = {
+          "provider=default" = {
+            enabled = true;
+            properties = {
+              inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl;
+            };
+          };
+        };
+        "subsystem=datasources"."data-source=KeycloakDS" = {
+          max-pool-size = "20";
+          user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
+          password = "@db-password@";
+        };
+      } [
+        (lib.optionalAttrs (cfg.database.type == "postgresql") {
+          "subsystem=datasources" = {
+            "jdbc-driver=postgresql" = {
+              driver-module-name = "org.postgresql";
+              driver-name = "postgresql";
+              driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource";
+            };
+            "data-source=KeycloakDS" = {
+              connection-url = "jdbc:postgresql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak";
+              driver-name = "postgresql";
+              "connection-properties=ssl".value = lib.boolToString cfg.database.useSSL;
+            } // (lib.optionalAttrs (cfg.database.caCert != null) {
+              "connection-properties=sslrootcert".value = cfg.database.caCert;
+              "connection-properties=sslmode".value = "verify-ca";
+            });
+          };
+        })
+        (lib.optionalAttrs (cfg.database.type == "mysql") {
+          "subsystem=datasources" = {
+            "jdbc-driver=mysql" = {
+              driver-module-name = "com.mysql";
+              driver-name = "mysql";
+              driver-class-name = "com.mysql.jdbc.Driver";
+            };
+            "data-source=KeycloakDS" = {
+              connection-url = "jdbc:mysql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak";
+              driver-name = "mysql";
+              "connection-properties=useSSL".value = lib.boolToString cfg.database.useSSL;
+              "connection-properties=requireSSL".value = lib.boolToString cfg.database.useSSL;
+              "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.database.useSSL;
+              "connection-properties=characterEncoding".value = "UTF-8";
+              valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker";
+              validate-on-match = true;
+              exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter";
+            } // (lib.optionalAttrs (cfg.database.caCert != null) {
+              "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
+              "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
+            });
+          };
+        })
+        (lib.optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
+          "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
+          "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = {
+            keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
+            keystore-password = "notsosecretpassword";
+          };
+          "subsystem=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm";
+        })
+        cfg.extraConfig
+      ];
+
+
+      /* Produces a JBoss CLI script that creates paths and sets
+         attributes matching those described by `attrs`. When the
+         script is run, the existing settings are effectively overlayed
+         by those from `attrs`. Existing attributes can be unset by
+         defining them `null`.
+
+         JBoss paths and attributes / maps are distinguished by their
+         name, where paths follow a `key=value` scheme.
+
+         Example:
+           mkJbossScript {
+             "subsystem=keycloak-server"."spi=hostname" = {
+               "provider=fixed" = null;
+               "provider=default" = {
+                 enabled = true;
+                 properties = {
+                   inherit frontendUrl;
+                   forceBackendUrlToFrontendUrl = false;
+                 };
+               };
+             };
+           }
+           => ''
+             if (outcome != success) of /:read-resource()
+                 /:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server:read-resource()
+                 /subsystem=keycloak-server:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource()
+                 /subsystem=keycloak-server/spi=hostname:add()
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource()
+                 /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" })
+             end-if
+             if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
+             end-if
+             if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
+             end-if
+             if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
+               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
+             end-if
+             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource()
+                 /subsystem=keycloak-server/spi=hostname/provider=fixed:remove()
+             end-if
+           ''
+      */
+      mkJbossScript = attrs:
+        let
+          /* From a JBoss path and an attrset, produces a JBoss CLI
+             snippet that writes the corresponding attributes starting
+             at `path`. Recurses down into subattrsets as necessary,
+             producing the variable name from its full path in the
+             attrset.
+
+             Example:
+               writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" {
+                 enabled = true;
+                 properties = {
+                   forceBackendUrlToFrontendUrl = false;
+                   frontendUrl = "https://keycloak.example.com/auth";
+                 };
+               }
+               => ''
+                 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
+                 end-if
+                 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
+                 end-if
+                 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
+                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
+                 end-if
+               ''
+          */
+          writeAttributes = path: set:
+            let
+              # JBoss expressions like `${var}` need to be prefixed
+              # with `expression` to evaluate.
+              prefixExpression = string:
+                let
+                  match = (builtins.match ''"\$\{.*}"'' string);
+                in
+                  if match != null then
+                    "expression " + string
+                  else
+                    string;
+
+              writeAttribute = attribute: value:
+                let
+                  type = builtins.typeOf value;
+                in
+                  if type == "set" then
+                    let
+                      names = builtins.attrNames value;
+                    in
+                      builtins.foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names
+                  else if value == null then ''
+                    if (outcome == success) of ${path}:read-attribute(name="${attribute}")
+                        ${path}:undefine-attribute(name="${attribute}")
+                    end-if
+                  ''
+                  else if builtins.elem type [ "string" "path" "bool" ] then
+                    let
+                      value' = if type == "bool" then lib.boolToString value else ''"${value}"'';
+                    in ''
+                      if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}")
+                        ${path}:write-attribute(name=${attribute}, value=${value'})
+                      end-if
+                    ''
+                  else throw "Unsupported type '${type}' for path '${path}'!";
+            in
+              lib.concatStrings
+                (lib.mapAttrsToList
+                  (attribute: value: (writeAttribute attribute value))
+                  set);
+
+
+          /* Produces an argument list for the JBoss `add()` function,
+             which adds a JBoss path and takes as its arguments the
+             required subpaths and attributes.
+
+             Example:
+               makeArgList {
+                 enabled = true;
+                 properties = {
+                   forceBackendUrlToFrontendUrl = false;
+                   frontendUrl = "https://keycloak.example.com/auth";
+                 };
+               }
+               => ''
+                 enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }
+               ''
+          */
+          makeArgList = set:
+            let
+              makeArg = attribute: value:
+                let
+                  type = builtins.typeOf value;
+                in
+                  if type == "set" then
+                    "${attribute} = { " + (makeArgList value) + " }"
+                  else if builtins.elem type [ "string" "path" "bool" ] then
+                    "${attribute} = ${if type == "bool" then lib.boolToString value else ''"${value}"''}"
+                  else if value == null then
+                    ""
+                  else
+                    throw "Unsupported type '${type}' for attribute '${attribute}'!";
+            in
+              lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set);
+
+
+          /* Recurses into the `attrs` attrset, beginning at the path
+             resolved from `state.path ++ node`; if `node` is `null`,
+             starts from `state.path`. Only subattrsets that are JBoss
+             paths, i.e. follows the `key=value` format, are recursed
+             into - the rest are considered JBoss attributes / maps.
+          */
+          recurse = state: node:
+            let
+              path = state.path ++ (lib.optional (node != null) node);
+              isPath = name:
+                let
+                  value = lib.getAttrFromPath (path ++ [ name ]) attrs;
+                in
+                  if (builtins.match ".*([=]).*" name) == [ "=" ] then
+                    if builtins.isAttrs value || value == null then
+                      true
+                    else
+                      throw "Parsing path '${lib.concatStringsSep "." (path ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
+                  else
+                    false;
+              jbossPath = "/" + (lib.concatStringsSep "/" path);
+              nodeValue = lib.getAttrFromPath path attrs;
+              children = if !builtins.isAttrs nodeValue then {} else nodeValue;
+              subPaths = builtins.filter isPath (builtins.attrNames children);
+              jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children;
+            in
+              state // {
+                text = state.text + (
+                  if nodeValue != null then ''
+                    if (outcome != success) of ${jbossPath}:read-resource()
+                        ${jbossPath}:add(${makeArgList jbossAttrs})
+                    end-if
+                  '' + (writeAttributes jbossPath jbossAttrs)
+                  else ''
+                    if (outcome == success) of ${jbossPath}:read-resource()
+                        ${jbossPath}:remove()
+                    end-if
+                  '') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text;
+              };
+        in
+          (recurse { text = ""; path = []; } null).text;
+
+
+      jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
+
+      keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {
+        nativeBuildInputs = [ cfg.package ];
+      } ''
+        export JBOSS_BASE_DIR="$(pwd -P)";
+        export JBOSS_MODULEPATH="${cfg.package}/modules";
+        export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log";
+
+        cp -r ${cfg.package}/standalone/configuration .
+        chmod -R u+rwX ./configuration
+
+        mkdir -p {deployments,ssl}
+
+        standalone.sh&
+
+        attempt=1
+        max_attempts=30
+        while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do
+            if [[ "$attempt" == "$max_attempts" ]]; then
+                echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2
+                exit 1
+            fi
+            echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)"
+            sleep 1
+            (( attempt++ ))
+        done
+
+        jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
+
+        cp configuration/standalone.xml $out
+      '';
+    in
+      lib.mkIf cfg.enable {
+
+        assertions = [
+          {
+            assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null);
+            message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL";
+          }
+        ];
+
+        environment.systemPackages = [ cfg.package ];
+
+        systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL {
+          after = [ "postgresql.service" ];
+          before = [ "keycloak.service" ];
+          bindsTo = [ "postgresql.service" ];
+          path = [ config.services.postgresql.package ];
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            User = "postgres";
+            Group = "postgres";
+          };
+          script = ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            create_role="$(mktemp)"
+            trap 'rm -f "$create_role"' ERR EXIT
+
+            echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.database.passwordFile}')' CREATEDB" > "$create_role"
+            psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
+            psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
+          '';
+        };
+
+        systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL {
+          after = [ "mysql.service" ];
+          before = [ "keycloak.service" ];
+          bindsTo = [ "mysql.service" ];
+          path = [ config.services.mysql.package ];
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            User = config.services.mysql.user;
+            Group = config.services.mysql.group;
+          };
+          script = ''
+            set -o errexit -o pipefail -o nounset -o errtrace
+            shopt -s inherit_errexit
+
+            db_password="$(<'${cfg.database.passwordFile}')"
+            ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
+              echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
+              echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
+            ) | mysql -N
+          '';
+        };
+
+        systemd.services.keycloak =
+          let
+            databaseServices =
+              if createLocalPostgreSQL then [
+                "keycloakPostgreSQLInit.service" "postgresql.service"
+              ]
+              else if createLocalMySQL then [
+                "keycloakMySQLInit.service" "mysql.service"
+              ]
+              else [ ];
+          in {
+            after = databaseServices;
+            bindsTo = databaseServices;
+            wantedBy = [ "multi-user.target" ];
+            path = with pkgs; [
+              cfg.package
+              openssl
+              replace-secret
+            ];
+            environment = {
+              JBOSS_LOG_DIR = "/var/log/keycloak";
+              JBOSS_BASE_DIR = "/run/keycloak";
+              JBOSS_MODULEPATH = "${cfg.package}/modules";
+            };
+            serviceConfig = {
+              ExecStartPre = let
+                startPreFullPrivileges = ''
+                  set -o errexit -o pipefail -o nounset -o errtrace
+                  shopt -s inherit_errexit
+
+                  umask u=rwx,g=,o=
+
+                  install -T -m 0400 -o keycloak -g keycloak '${cfg.database.passwordFile}' /run/keycloak/secrets/db_password
+                '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
+                  install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificate}' /run/keycloak/secrets/ssl_cert
+                  install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificateKey}' /run/keycloak/secrets/ssl_key
+                '';
+                startPre = ''
+                  set -o errexit -o pipefail -o nounset -o errtrace
+                  shopt -s inherit_errexit
+
+                  umask u=rwx,g=,o=
+
+                  install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
+                  install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
+
+                  replace-secret '@db-password@' '/run/keycloak/secrets/db_password' /run/keycloak/configuration/standalone.xml
+
+                  export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
+                  add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
+                '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
+                  pushd /run/keycloak/ssl/
+                  cat /run/keycloak/secrets/ssl_cert <(echo) \
+                      /run/keycloak/secrets/ssl_key <(echo) \
+                      /etc/ssl/certs/ca-certificates.crt \
+                      > allcerts.pem
+                  openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert -inkey /run/keycloak/secrets/ssl_key -chain \
+                                 -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
+                                 -CAfile allcerts.pem -passout pass:notsosecretpassword
+                  popd
+                '';
+              in [
+                "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}"
+                "${pkgs.writeShellScript "keycloak-start-pre" startPre}"
+              ];
+              ExecStart = "${cfg.package}/bin/standalone.sh";
+              User = "keycloak";
+              Group = "keycloak";
+              DynamicUser = true;
+              RuntimeDirectory = map (p: "keycloak/" + p) [
+                "secrets"
+                "configuration"
+                "deployments"
+                "data"
+                "ssl"
+                "log"
+                "tmp"
+              ];
+              RuntimeDirectoryMode = 0700;
+              LogsDirectory = "keycloak";
+              AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+            };
+          };
+
+        services.postgresql.enable = lib.mkDefault createLocalPostgreSQL;
+        services.mysql.enable = lib.mkDefault createLocalMySQL;
+        services.mysql.package = lib.mkIf createLocalMySQL pkgs.mariadb;
+      };
+
+  meta.doc = ./keycloak.xml;
+  meta.maintainers = [ lib.maintainers.talyz ];
+}
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
new file mode 100644
index 00000000000..7ba656c20f1
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -0,0 +1,206 @@
+<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-keycloak">
+ <title>Keycloak</title>
+ <para>
+   <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
+   open source identity and access management server with support for
+   <link xlink:href="https://openid.net/connect/">OpenID
+   Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
+   2.0</link> and <link
+   xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
+   2.0</link>.
+ </para>
+   <section xml:id="module-services-keycloak-admin">
+     <title>Administration</title>
+     <para>
+       An administrative user with the username
+       <literal>admin</literal> is automatically created in the
+       <literal>master</literal> realm. Its initial password can be
+       configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
+       and defaults to <literal>changeme</literal>. The password is
+       not stored safely and should be changed immediately in the
+       admin panel.
+     </para>
+
+     <para>
+       Refer to the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console">Admin
+       Console section of the Keycloak Server Administration Guide</link> for
+       information on how to administer your
+       <productname>Keycloak</productname> instance.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-database">
+     <title>Database access</title>
+     <para>
+       <productname>Keycloak</productname> can be used with either
+       <productname>PostgreSQL</productname> or
+       <productname>MySQL</productname>. Which one is used can be
+       configured in <xref
+       linkend="opt-services.keycloak.database.type" />. The selected
+       database will automatically be enabled and a database and role
+       created unless <xref
+       linkend="opt-services.keycloak.database.host" /> is changed from
+       its default of <literal>localhost</literal> or <xref
+       linkend="opt-services.keycloak.database.createLocally" /> is set
+       to <literal>false</literal>.
+     </para>
+
+     <para>
+       External database access can also be configured by setting
+       <xref linkend="opt-services.keycloak.database.host" />, <xref
+       linkend="opt-services.keycloak.database.username" />, <xref
+       linkend="opt-services.keycloak.database.useSSL" /> and <xref
+       linkend="opt-services.keycloak.database.caCert" /> as
+       appropriate. Note that you need to manually create a database
+       called <literal>keycloak</literal> and allow the configured
+       database user full access to it.
+     </para>
+
+     <para>
+       <xref linkend="opt-services.keycloak.database.passwordFile" />
+       must be set to the path to a file containing the password used
+       to log in to the database. If <xref linkend="opt-services.keycloak.database.host" />
+       and <xref linkend="opt-services.keycloak.database.createLocally" />
+       are kept at their defaults, the database role
+       <literal>keycloak</literal> with that password is provisioned
+       on the local database instance.
+     </para>
+
+     <warning>
+       <para>
+         The path should be provided as a string, not a Nix path, since Nix
+         paths are copied into the world readable Nix store.
+       </para>
+     </warning>
+   </section>
+
+   <section xml:id="module-services-keycloak-frontendurl">
+     <title>Frontend URL</title>
+     <para>
+       The frontend URL is used as base for all frontend requests and
+       must be configured through <xref linkend="opt-services.keycloak.frontendUrl" />.
+       It should normally include a trailing <literal>/auth</literal>
+       (the default web context).
+     </para>
+
+     <para>
+       <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
+       determines whether Keycloak should force all requests to go
+       through the frontend URL. By default,
+       <productname>Keycloak</productname> allows backend requests to
+       instead use its local hostname or IP address and may also
+       advertise it to clients through its OpenID Connect Discovery
+       endpoint.
+     </para>
+
+     <para>
+       See the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">Hostname
+       section of the Keycloak Server Installation and Configuration
+       Guide</link> for more information.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-tls">
+     <title>Setting up TLS/SSL</title>
+     <para>
+       By default, <productname>Keycloak</productname> won't accept
+       unsecured HTTP connections originating from outside its local
+       network.
+     </para>
+
+     <para>
+       HTTPS support requires a TLS/SSL certificate and a private key,
+       both <link
+       xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
+       formatted</link>. Their paths should be set through <xref
+       linkend="opt-services.keycloak.sslCertificate" /> and <xref
+       linkend="opt-services.keycloak.sslCertificateKey" />.
+     </para>
+
+     <warning>
+       <para>
+         The paths should be provided as a strings, not a Nix paths,
+         since Nix paths are copied into the world readable Nix store.
+       </para>
+     </warning>
+   </section>
+
+   <section xml:id="module-services-keycloak-extra-config">
+     <title>Additional configuration</title>
+     <para>
+       Additional Keycloak configuration options, for which no
+       explicit <productname>NixOS</productname> options are provided,
+       can be set in <xref linkend="opt-services.keycloak.extraConfig" />.
+     </para>
+
+     <para>
+       Options are expressed as a Nix attribute set which matches the
+       structure of the jboss-cli configuration. The configuration is
+       effectively overlayed on top of the default configuration
+       shipped with Keycloak. To remove existing nodes and undefine
+       attributes from the default configuration, set them to
+       <literal>null</literal>.
+     </para>
+     <para>
+       For example, the following script, which removes the hostname
+       provider <literal>default</literal>, adds the deprecated
+       hostname provider <literal>fixed</literal> and defines it the
+       default:
+
+<programlisting>
+/subsystem=keycloak-server/spi=hostname/provider=default:remove()
+/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
+/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
+</programlisting>
+
+       would be expressed as
+
+<programlisting>
+services.keycloak.extraConfig = {
+  "subsystem=keycloak-server" = {
+    "spi=hostname" = {
+      "provider=default" = null;
+      "provider=fixed" = {
+        enabled = true;
+        properties.hostname = "keycloak.example.com";
+      };
+      default-provider = "fixed";
+    };
+  };
+};
+</programlisting>
+     </para>
+     <para>
+       You can discover available options by using the <link
+       xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
+       program and by referring to the <link
+       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
+       Server Installation and Configuration Guide</link>.
+     </para>
+   </section>
+
+   <section xml:id="module-services-keycloak-example-config">
+     <title>Example configuration</title>
+     <para>
+       A basic configuration with some custom settings could look like this:
+<programlisting>
+services.keycloak = {
+  <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
+  <link linkend="opt-services.keycloak.frontendUrl">frontendUrl</link> = "https://keycloak.example.com/auth";
+  <link linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl">forceBackendUrlToFrontendUrl</link> = true;
+  <link linkend="opt-services.keycloak.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
+  <link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
+  <link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
+};
+</programlisting>
+     </para>
+
+   </section>
+ </chapter>
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
new file mode 100644
index 00000000000..5e24bd06ffd
--- /dev/null
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -0,0 +1,599 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.mastodon;
+  # We only want to create a database if we're actually going to connect to it.
+  databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql";
+
+  env = {
+    RAILS_ENV = "production";
+    NODE_ENV = "production";
+
+    DB_USER = cfg.database.user;
+
+    REDIS_HOST = cfg.redis.host;
+    REDIS_PORT = toString(cfg.redis.port);
+    DB_HOST = cfg.database.host;
+    DB_PORT = toString(cfg.database.port);
+    DB_NAME = cfg.database.name;
+    LOCAL_DOMAIN = cfg.localDomain;
+    SMTP_SERVER = cfg.smtp.host;
+    SMTP_PORT = toString(cfg.smtp.port);
+    SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
+    PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
+    PAPERCLIP_ROOT_URL = "/system";
+    ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
+    ES_HOST = cfg.elasticsearch.host;
+    ES_PORT = toString(cfg.elasticsearch.port);
+
+    TRUSTED_PROXY_IP = cfg.trustedProxy;
+  }
+  // (if cfg.smtp.authenticate then { SMTP_LOGIN  = cfg.smtp.user; } else {})
+  // cfg.extraConfig;
+
+  systemCallsList = [ "@clock" "@cpu-emulation" "@debug" "@keyring" "@module" "@mount" "@obsolete" "@raw-io" "@reboot" "@setuid" "@swap" ];
+
+  cfgService = {
+    # User and group
+    User = cfg.user;
+    Group = cfg.group;
+    # State directory and mode
+    StateDirectory = "mastodon";
+    StateDirectoryMode = "0750";
+    # Logs directory and mode
+    LogsDirectory = "mastodon";
+    LogsDirectoryMode = "0750";
+    # Access write directories
+    UMask = "0027";
+    # Capabilities
+    CapabilityBoundingSet = "";
+    # Security
+    NoNewPrivileges = true;
+    # Sandboxing
+    ProtectSystem = "strict";
+    ProtectHome = true;
+    PrivateTmp = true;
+    PrivateDevices = true;
+    PrivateUsers = true;
+    ProtectClock = true;
+    ProtectHostname = true;
+    ProtectKernelLogs = true;
+    ProtectKernelModules = true;
+    ProtectKernelTunables = true;
+    ProtectControlGroups = true;
+    RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+    RestrictNamespaces = true;
+    LockPersonality = true;
+    MemoryDenyWriteExecute = false;
+    RestrictRealtime = true;
+    RestrictSUIDSGID = true;
+    PrivateMounts = true;
+    # System Call Filtering
+    SystemCallArchitectures = "native";
+  };
+
+  envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
+    (lib.concatLists (lib.mapAttrsToList (name: value:
+      if value != null then [
+        "${name}=\"${toString value}\""
+      ] else []
+    ) env))));
+
+  mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" ''
+    set -a
+    source "${envFile}"
+    source /var/lib/mastodon/.secrets_env
+    eval -- "\$@"
+  '';
+
+in {
+
+  options = {
+    services.mastodon = {
+      enable = lib.mkEnableOption "Mastodon, a federated social network server";
+
+      configureNginx = lib.mkOption {
+        description = ''
+          Configure nginx as a reverse proxy for mastodon.
+          Note that this makes some assumptions on your setup, and sets settings that will
+          affect other virtualHosts running on your nginx instance, if any.
+          Alternatively you can configure a reverse-proxy of your choice to serve these paths:
+
+          <code>/ -> $(nix-instantiate --eval '&lt;nixpkgs&gt;' -A mastodon.outPath)/public</code>
+
+          <code>/ -> 127.0.0.1:{{ webPort }} </code>(If there was no file in the directory above.)
+
+          <code>/system/ -> /var/lib/mastodon/public-system/</code>
+
+          <code>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</code>
+
+          Make sure that websockets are forwarded properly. You might want to set up caching
+          of some requests. Take a look at mastodon's provided nginx configuration at
+          <code>https://github.com/tootsuite/mastodon/blob/master/dist/nginx.conf</code>.
+        '';
+        type = lib.types.bool;
+        default = false;
+      };
+
+      user = lib.mkOption {
+        description = ''
+          User under which mastodon runs. If it is set to "mastodon",
+          that user will be created, otherwise it should be set to the
+          name of a user created elsewhere.  In both cases,
+          <package>mastodon</package> and a package containing only
+          the shell script <code>mastodon-env</code> will be added to
+          the user's package set. To run a command from
+          <package>mastodon</package> such as <code>tootctl</code>
+          with the environment configured by this module use
+          <code>mastodon-env</code>, as in:
+
+          <code>mastodon-env tootctl accounts create newuser --email newuser@example.com</code>
+        '';
+        type = lib.types.str;
+        default = "mastodon";
+      };
+
+      group = lib.mkOption {
+        description = ''
+          Group under which mastodon runs.
+        '';
+        type = lib.types.str;
+        default = "mastodon";
+      };
+
+      streamingPort = lib.mkOption {
+        description = "TCP port used by the mastodon-streaming service.";
+        type = lib.types.port;
+        default = 55000;
+      };
+
+      webPort = lib.mkOption {
+        description = "TCP port used by the mastodon-web service.";
+        type = lib.types.port;
+        default = 55001;
+      };
+
+      sidekiqPort = lib.mkOption {
+        description = "TCP port used by the mastodon-sidekiq service";
+        type = lib.types.port;
+        default = 55002;
+      };
+
+      vapidPublicKeyFile = lib.mkOption {
+        description = ''
+          Path to file containing the public key used for Web Push
+          Voluntary Application Server Identification.  A new keypair can
+          be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+
+          If <option>mastodon.vapidPrivateKeyFile</option>does not
+          exist, it and this file will be created with a new keypair.
+        '';
+        default = "/var/lib/mastodon/secrets/vapid-public-key";
+        type = lib.types.str;
+      };
+
+      localDomain = lib.mkOption {
+        description = "The domain serving your Mastodon instance.";
+        example = "social.example.org";
+        type = lib.types.str;
+      };
+
+      secretKeyBaseFile = lib.mkOption {
+        description = ''
+          Path to file containing the secret key base.
+          A new secret key base can be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+
+          If this file does not exist, it will be created with a new secret key base.
+        '';
+        default = "/var/lib/mastodon/secrets/secret-key-base";
+        type = lib.types.str;
+      };
+
+      otpSecretFile = lib.mkOption {
+        description = ''
+          Path to file containing the OTP secret.
+          A new OTP secret can be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code>
+
+          If this file does not exist, it will be created with a new OTP secret.
+        '';
+        default = "/var/lib/mastodon/secrets/otp-secret";
+        type = lib.types.str;
+      };
+
+      vapidPrivateKeyFile = lib.mkOption {
+        description = ''
+          Path to file containing the private key used for Web Push
+          Voluntary Application Server Identification.  A new keypair can
+          be generated by running:
+
+          <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code>
+
+          If this file does not exist, it will be created with a new
+          private key.
+        '';
+        default = "/var/lib/mastodon/secrets/vapid-private-key";
+        type = lib.types.str;
+      };
+
+      trustedProxy = lib.mkOption {
+        description = ''
+          You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
+          otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
+          bad because IP addresses are used for important rate limits and security functions.
+        '';
+        type = lib.types.str;
+        default = "127.0.0.1";
+      };
+
+      enableUnixSocket = lib.mkOption {
+        description = ''
+          Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
+          is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
+          processes and streaming API (Node.js) processes.
+        '';
+        type = lib.types.bool;
+        default = true;
+      };
+
+      redis = {
+        createLocally = lib.mkOption {
+          description = "Configure local Redis server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          description = "Redis host.";
+          type = lib.types.str;
+          default = "127.0.0.1";
+        };
+
+        port = lib.mkOption {
+          description = "Redis port.";
+          type = lib.types.port;
+          default = 6379;
+        };
+      };
+
+      database = {
+        createLocally = lib.mkOption {
+          description = "Configure local PostgreSQL database server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          type = lib.types.str;
+          default = "/run/postgresql";
+          example = "192.168.23.42";
+          description = "Database host address or unix socket.";
+        };
+
+        port = lib.mkOption {
+          type = lib.types.int;
+          default = 5432;
+          description = "Database host port.";
+        };
+
+        name = lib.mkOption {
+          type = lib.types.str;
+          default = "mastodon";
+          description = "Database name.";
+        };
+
+        user = lib.mkOption {
+          type = lib.types.str;
+          default = "mastodon";
+          description = "Database user.";
+        };
+
+        passwordFile = lib.mkOption {
+          type = lib.types.nullOr lib.types.path;
+          default = "/var/lib/mastodon/secrets/db-password";
+          example = "/run/keys/mastodon-db-password";
+          description = ''
+            A file containing the password corresponding to
+            <option>database.user</option>.
+          '';
+        };
+      };
+
+      smtp = {
+        createLocally = lib.mkOption {
+          description = "Configure local Postfix SMTP server for Mastodon.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        authenticate = lib.mkOption {
+          description = "Authenticate with the SMTP server using username and password.";
+          type = lib.types.bool;
+          default = true;
+        };
+
+        host = lib.mkOption {
+          description = "SMTP host used when sending emails to users.";
+          type = lib.types.str;
+          default = "127.0.0.1";
+        };
+
+        port = lib.mkOption {
+          description = "SMTP port used when sending emails to users.";
+          type = lib.types.port;
+          default = 25;
+        };
+
+        fromAddress = lib.mkOption {
+          description = ''"From" address used when sending Emails to users.'';
+          type = lib.types.str;
+        };
+
+        user = lib.mkOption {
+          description = "SMTP login name.";
+          type = lib.types.str;
+        };
+
+        passwordFile = lib.mkOption {
+          description = ''
+            Path to file containing the SMTP password.
+          '';
+          default = "/var/lib/mastodon/secrets/smtp-password";
+          example = "/run/keys/mastodon-smtp-password";
+          type = lib.types.str;
+        };
+      };
+
+      elasticsearch = {
+        host = lib.mkOption {
+          description = ''
+            Elasticsearch host.
+            If it is not null, Elasticsearch full text search will be enabled.
+          '';
+          type = lib.types.nullOr lib.types.str;
+          default = null;
+        };
+
+        port = lib.mkOption {
+          description = "Elasticsearch port.";
+          type = lib.types.port;
+          default = 9200;
+        };
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.mastodon;
+        defaultText = "pkgs.mastodon";
+        description = "Mastodon package to use.";
+      };
+
+      extraConfig = lib.mkOption {
+        type = lib.types.attrs;
+        default = {};
+        description = ''
+          Extra environment variables to pass to all mastodon services.
+        '';
+      };
+
+      automaticMigrations = lib.mkOption {
+        type = lib.types.bool;
+        default = true;
+        description = ''
+          Do automatic database migrations.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
+        message = ''For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user and services.mastodon.database.user must be identical.'';
+      }
+    ];
+
+    systemd.services.mastodon-init-dirs = {
+      script = ''
+        umask 077
+
+        if ! test -f ${cfg.secretKeyBaseFile}; then
+          mkdir -p $(dirname ${cfg.secretKeyBaseFile})
+          bin/rake secret > ${cfg.secretKeyBaseFile}
+        fi
+        if ! test -f ${cfg.otpSecretFile}; then
+          mkdir -p $(dirname ${cfg.otpSecretFile})
+          bin/rake secret > ${cfg.otpSecretFile}
+        fi
+        if ! test -f ${cfg.vapidPrivateKeyFile}; then
+          mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
+          keypair=$(bin/rake webpush:generate_keys)
+          echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
+          echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
+        fi
+
+        cat > /var/lib/mastodon/.secrets_env <<EOF
+        SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
+        OTP_SECRET="$(cat ${cfg.otpSecretFile})"
+        VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
+        VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
+        DB_PASS="$(cat ${cfg.database.passwordFile})"
+      '' + (if cfg.smtp.authenticate then ''
+        SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
+      '' else "") + ''
+        EOF
+      '';
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
+      } // cfgService;
+
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
+      script = ''
+        if [ `psql ${cfg.database.name} -c \
+                "select count(*) from pg_class c \
+                join pg_namespace s on s.oid = c.relnamespace \
+                where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
+                and s.nspname not like 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
+          SAFETY_ASSURED=1 rails db:schema:load
+          rails db:seed
+        else
+          rails db:migrate
+        fi
+      '';
+      path = [ cfg.package pkgs.postgresql ];
+      environment = env;
+      serviceConfig = {
+        Type = "oneshot";
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
+      } // cfgService;
+      after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []);
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.mastodon-streaming = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon streaming";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // (if cfg.enableUnixSocket
+        then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
+        else { PORT = toString(cfg.streamingPort); }
+      );
+      serviceConfig = {
+        ExecStart = "${cfg.package}/run-streaming.sh";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # Runtime directory and mode
+        RuntimeDirectory = "mastodon-streaming";
+        RuntimeDirectoryMode = "0750";
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@privileged" "@resources" ]);
+      } // cfgService;
+    };
+
+    systemd.services.mastodon-web = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon web";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // (if cfg.enableUnixSocket
+        then { SOCKET = "/run/mastodon-web/web.socket"; }
+        else { PORT = toString(cfg.webPort); }
+      );
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # Runtime directory and mode
+        RuntimeDirectory = "mastodon-web";
+        RuntimeDirectoryMode = "0750";
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    };
+
+    systemd.services.mastodon-sidekiq = {
+      after = [ "network.target" ]
+        ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
+        ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
+      description = "Mastodon sidekiq";
+      wantedBy = [ "multi-user.target" ];
+      environment = env // {
+        PORT = toString(cfg.sidekiqPort);
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/sidekiq -c 25 -r ${cfg.package}";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = "/var/lib/mastodon/.secrets_env";
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = "~" + lib.concatStringsSep " " systemCallsList;
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    };
+
+    services.nginx = lib.mkIf cfg.configureNginx {
+      enable = true;
+      recommendedProxySettings = true; # required for redirections to work
+      virtualHosts."${cfg.localDomain}" = {
+        root = "${cfg.package}/public/";
+        forceSSL = true; # mastodon only supports https
+        enableACME = true;
+
+        locations."/system/".alias = "/var/lib/mastodon/public-system/";
+
+        locations."/" = {
+          tryFiles = "$uri @proxy";
+        };
+
+        locations."@proxy" = {
+          proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString(cfg.webPort)}");
+          proxyWebsockets = true;
+        };
+
+        locations."/api/v1/streaming/" = {
+          proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-streaming/streaming.socket" else "http://127.0.0.1:${toString(cfg.streamingPort)}/");
+          proxyWebsockets = true;
+        };
+      };
+    };
+
+    services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
+      enable = true;
+    };
+    services.redis = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
+      enable = true;
+    };
+    services.postgresql = lib.mkIf databaseActuallyCreateLocally {
+      enable = true;
+      ensureUsers = [
+        {
+          name = cfg.database.user;
+          ensurePermissions."DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
+        }
+      ];
+      ensureDatabases = [ cfg.database.name ];
+    };
+
+    users.users = lib.mkMerge [
+      (lib.mkIf (cfg.user == "mastodon") {
+        mastodon = {
+          isSystemUser = true;
+          home = cfg.package;
+          inherit (cfg) group;
+        };
+      })
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package mastodonEnv ])
+    ];
+
+    users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
+  };
+
+  meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
+
+}
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 75da474dc44..79a0354e22b 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -77,6 +77,16 @@ in {
         '';
       };
 
+      periodicArchiveProcessingUrl = mkOption {
+        type = types.str;
+        default = "${user}.${fqdn}";
+        example = "matomo.yourdomain.org";
+        description = ''
+          URL of the host, without https prefix. By default, this is ${user}.${fqdn}, but you may want to change it if you
+          run Matomo on a different URL than matomo.yourdomain.
+        '';
+      };
+
       nginx = mkOption {
         type = types.nullOr (types.submodule (
           recursiveUpdate
@@ -190,7 +200,7 @@ in {
         UMask = "0007";
         CPUSchedulingPolicy = "idle";
         IOSchedulingClass = "idle";
-        ExecStart = "${cfg.package}/bin/matomo-console core:archive --url=https://${user}.${fqdn}";
+        ExecStart = "${cfg.package}/bin/matomo-console core:archive --url=https://${cfg.periodicArchiveProcessingUrl}";
       };
     };
 
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
index 0a5b6047bb5..1db1652022a 100644
--- a/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -180,6 +180,7 @@ in
       };
 
       name = mkOption {
+        type = types.str;
         default = "MediaWiki";
         example = "Foobar Wiki";
         description = "Name of the wiki.";
diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix
index 304712d0efc..01710b1bd59 100644
--- a/nixos/modules/services/web-apps/miniflux.nix
+++ b/nixos/modules/services/web-apps/miniflux.nix
@@ -14,17 +14,16 @@ let
     ADMIN_PASSWORD=password
   '';
 
-  pgsu = "${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser}";
   pgbin = "${config.services.postgresql.package}/bin";
   preStart = pkgs.writeScript "miniflux-pre-start" ''
     #!${pkgs.runtimeShell}
     db_exists() {
-      [ "$(${pgsu} ${pgbin}/psql -Atc "select 1 from pg_database where datname='$1'")" == "1" ]
+      [ "$(${pgbin}/psql -Atc "select 1 from pg_database where datname='$1'")" == "1" ]
     }
     if ! db_exists "${dbName}"; then
-      ${pgsu} ${pgbin}/psql postgres -c "CREATE ROLE ${dbUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${dbPassword}'"
-      ${pgsu} ${pgbin}/createdb --owner "${dbUser}" "${dbName}"
-      ${pgsu} ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
+      ${pgbin}/psql postgres -c "CREATE ROLE ${dbUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${dbPassword}'"
+      ${pgbin}/createdb --owner "${dbUser}" "${dbName}"
+      ${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
     fi
   '';
 in
@@ -44,7 +43,7 @@ in
         '';
         description = ''
           Configuration for Miniflux, refer to
-          <link xlink:href="http://docs.miniflux.app/en/latest/configuration.html"/>
+          <link xlink:href="https://miniflux.app/docs/configuration.html"/>
           for documentation on the supported values.
         '';
       };
@@ -73,15 +72,26 @@ in
 
     services.postgresql.enable = true;
 
+    systemd.services.miniflux-dbsetup = {
+      description = "Miniflux database setup";
+      wantedBy = [ "multi-user.target" ];
+      requires = [ "postgresql.service" ];
+      after = [ "network.target" "postgresql.service" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = config.services.postgresql.superUser;
+        ExecStart = preStart;
+      };
+    };
+
     systemd.services.miniflux = {
       description = "Miniflux service";
       wantedBy = [ "multi-user.target" ];
       requires = [ "postgresql.service" ];
-      after = [ "network.target" "postgresql.service" ];
+      after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ];
 
       serviceConfig = {
         ExecStart = "${pkgs.miniflux}/bin/miniflux";
-        ExecStartPre = "+${preStart}";
         DynamicUser = true;
         RuntimeDirectory = "miniflux";
         RuntimeDirectoryMode = "0700";
diff --git a/nixos/modules/services/web-apps/moinmoin.nix b/nixos/modules/services/web-apps/moinmoin.nix
index dc7abce2a5c..7a54255a46e 100644
--- a/nixos/modules/services/web-apps/moinmoin.nix
+++ b/nixos/modules/services/web-apps/moinmoin.nix
@@ -211,7 +211,7 @@ in
             environment = let
               penv = python.buildEnv.override {
                 # setuptools: https://github.com/benoitc/gunicorn/issues/1716
-                extraLibs = [ python.pkgs.gevent python.pkgs.setuptools pkg ];
+                extraLibs = [ python.pkgs.eventlet python.pkgs.setuptools pkg ];
               };
             in {
               PYTHONPATH = "${dataDir}/${wikiIdent}/config:${penv}/${python.sitePackages}";
@@ -224,6 +224,8 @@ in
               chmod -R u+w ${dataDir}/${wikiIdent}/underlay
             '';
 
+            startLimitIntervalSec = 30;
+
             serviceConfig = {
               User = user;
               Group = group;
@@ -231,13 +233,12 @@ in
               ExecStart = ''${python.pkgs.gunicorn}/bin/gunicorn moin_wsgi \
                 --name gunicorn-${wikiIdent} \
                 --workers ${toString cfg.gunicorn.workers} \
-                --worker-class gevent \
+                --worker-class eventlet \
                 --bind unix:/run/moin/${wikiIdent}/gunicorn.sock
               '';
 
               Restart = "on-failure";
               RestartSec = "2s";
-              StartLimitIntervalSec = "30s";
 
               StateDirectory = "moin/${wikiIdent}";
               StateDirectoryMode = "0750";
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index f45eaa24d54..ad1e55d62d1 100644
--- a/nixos/modules/services/web-apps/moodle.nix
+++ b/nixos/modules/services/web-apps/moodle.nix
@@ -57,7 +57,7 @@ let
   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
 
   phpExt = pkgs.php.withExtensions
-        ({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom  intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo ]);
+        ({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom  intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter ]);
 in
 {
   # interface
@@ -84,7 +84,7 @@ in
       type = mkOption {
         type = types.enum [ "mysql" "pgsql" ];
         default = "mysql";
-        description = ''Database engine to use.'';
+        description = "Database engine to use.";
       };
 
       host = mkOption {
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 7da119758fc..5e15aaba096 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -6,17 +6,19 @@ let
   cfg = config.services.nextcloud;
   fpm = config.services.phpfpm.pools.nextcloud;
 
-  phpPackage =
-    let
-      base = pkgs.php74;
-    in
-      base.buildEnv {
-        extensions = { enabled, all }: with all;
-          enabled ++ [
-            apcu redis memcached imagick
-          ];
-        extraConfig = phpOptionsStr;
-      };
+  phpPackage = pkgs.php74.buildEnv {
+    extensions = { enabled, all }:
+      (with all;
+        enabled
+        ++ optional cfg.enableImagemagick imagick
+        # Optionally enabled depending on caching settings
+        ++ optional cfg.caching.apcu apcu
+        ++ optional cfg.caching.redis redis
+        ++ optional cfg.caching.memcached memcached
+      )
+      ++ cfg.phpExtraExtensions all; # Enabled by user
+    extraConfig = toKeyValue phpOptions;
+  };
 
   toKeyValue = generators.toKeyValue {
     mkKeyValue = generators.mkKeyValueDefault {} " = ";
@@ -26,8 +28,10 @@ let
     upload_max_filesize = cfg.maxUploadSize;
     post_max_size = cfg.maxUploadSize;
     memory_limit = cfg.maxUploadSize;
-  } // cfg.phpOptions;
-  phpOptionsStr = toKeyValue phpOptions;
+  } // cfg.phpOptions
+    // optionalAttrs cfg.caching.apcu {
+      "apc.enable_cli" = "1";
+    };
 
   occ = pkgs.writeScriptBin "nextcloud-occ" ''
     #! ${pkgs.runtimeShell}
@@ -39,7 +43,7 @@ let
     export NEXTCLOUD_CONFIG_DIR="${cfg.home}/config"
     $sudo \
       ${phpPackage}/bin/php \
-      occ $*
+      occ "$@"
   '';
 
   inherit (config.system) stateVersion;
@@ -59,6 +63,9 @@ in {
       Further details about this can be found in the `Nextcloud`-section of the NixOS-manual
       (which can be openend e.g. by running `nixos-help`).
     '')
+    (mkRemovedOptionModule [ "services" "nextcloud" "disableImagemagick" ] ''
+      Use services.nextcloud.nginx.enableImagemagick instead.
+    '')
   ];
 
   options.services.nextcloud = {
@@ -85,7 +92,7 @@ in {
     package = mkOption {
       type = types.package;
       description = "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud17" "nextcloud18" "nextcloud19" ];
+      relatedPackages = [ "nextcloud20" "nextcloud21" "nextcloud22" ];
     };
 
     maxUploadSize = mkOption {
@@ -116,6 +123,21 @@ in {
       '';
     };
 
+    phpExtraExtensions = mkOption {
+      type = with types; functionTo (listOf package);
+      default = all: [];
+      defaultText = "all: []";
+      description = ''
+        Additional PHP extensions to use for nextcloud.
+        By default, only extensions necessary for a vanilla nextcloud installation are enabled,
+        but you may choose from the list of available extensions and add further ones.
+        This is sometimes necessary to be able to install a certain nextcloud app that has additional requirements.
+      '';
+      example = literalExample ''
+        all: [ all.pdlib all.bz2 ]
+      '';
+    };
+
     phpOptions = mkOption {
       type = types.attrsOf types.str;
       default = {
@@ -228,7 +250,8 @@ in {
         type = types.nullOr types.str;
         default = null;
         description = ''
-          The full path to a file that contains the admin's password.
+          The full path to a file that contains the admin's password. Must be
+          readable by user <literal>nextcloud</literal>.
         '';
       };
 
@@ -263,6 +286,34 @@ in {
           may be served via HTTPS.
         '';
       };
+
+      defaultPhoneRegion = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "DE";
+        description = ''
+          <warning>
+           <para>This option exists since Nextcloud 21! If older versions are used,
+            this will throw an eval-error!</para>
+          </warning>
+
+          <link xlink:href="https://www.iso.org/iso-3166-country-codes.html">ISO 3611-1</link>
+          country codes for automatic phone-number detection without a country code.
+
+          With e.g. <literal>DE</literal> set, the <literal>+49</literal> can be omitted for
+          phone-numbers.
+        '';
+      };
+    };
+
+    enableImagemagick = mkEnableOption ''
+        Whether to load the ImageMagick module into PHP.
+        This is used by the theming app and for generating previews of certain images (e.g. SVG and HEIF).
+        You may want to disable it for increased security. In that case, previews will still be available
+        for some images (e.g. JPEG and PNG).
+        See https://github.com/nextcloud/server/issues/13099
+    '' // {
+      default = true;
     };
 
     caching = {
@@ -328,39 +379,33 @@ in {
             && !(acfg.adminpass != null && acfg.adminpassFile != null));
           message = "Please specify exactly one of adminpass or adminpassFile";
         }
+        { assertion = versionOlder cfg.package.version "21" -> cfg.config.defaultPhoneRegion == null;
+          message = "The `defaultPhoneRegion'-setting is only supported for Nextcloud >=21!";
+        }
       ];
 
-      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.
+      warnings = let
+        latest = 22;
+        upgradeWarning = major: nixos:
+          ''
+            A legacy Nextcloud install (from before NixOS ${nixos}) 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).
+            After nextcloud${toString major} is installed successfully, you can safely upgrade
+            to ${toString (major + 1)}. The latest version available is nextcloud${toString latest}.
 
-          It is assumed that Nextcloud will be upgraded from version 16 to 17.
+            Please note that Nextcloud doesn't support upgrades across multiple major versions
+            (i.e. an upgrade from 16 is possible to 17, but not 16 to 18).
 
-           * 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. Please note that Nextcloud 19 is already out and it's
-          recommended to upgrade to nextcloud19 after that.
+            The package can be upgraded by explicitly declaring the service-option
+            `services.nextcloud.package`.
+          '';
+      in (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 "19") ''
-          A legacy Nextcloud install (from before NixOS 20.09/unstable) may be installed.
-
-          If/After nextcloud18 is installed successfully, you can safely upgrade to
-          nextcloud19. If not, please upgrade to nextcloud18 first since Nextcloud doesn't
-          support upgrades that skip multiple versions (i.e. an upgrade from 17 to 19 isn't
-          possible, but an upgrade from 18 to 19).
-        '');
+        ++ (optional (versionOlder cfg.package.version "20") (upgradeWarning 19 "21.05"))
+        ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"))
+        ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"));
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -370,9 +415,13 @@ in {
               nextcloud defined in an overlay, please set `services.nextcloud.package` to
               `pkgs.nextcloud`.
             ''
-          else if versionOlder stateVersion "20.03" then nextcloud17
-          else if versionOlder stateVersion "20.09" then nextcloud18
-          else nextcloud19
+          # 21.03 will not be an official release - it was instead 21.05.
+          # This versionOlder statement remains set to 21.03 for backwards compatibility.
+          # See https://github.com/NixOS/nixpkgs/pull/108899 and
+          # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
+          else if versionOlder stateVersion "21.03" then nextcloud19
+          else if versionOlder stateVersion "21.11" then nextcloud21
+          else nextcloud22
         );
     }
 
@@ -399,7 +448,9 @@ in {
                 $file = "${c.dbpassFile}";
                 if (!file_exists($file)) {
                   throw new \RuntimeException(sprintf(
-                    "Cannot start Nextcloud, dbpass file %s set by NixOS doesn't exist!",
+                    "Cannot start Nextcloud, dbpass file %s set by NixOS doesn't seem to "
+                    . "exist! Please make sure that the file exists and has appropriate "
+                    . "permissions for user & group 'nextcloud'!",
                     $file
                   ));
                 }
@@ -428,6 +479,7 @@ in {
               'dbtype' => '${c.dbtype}',
               'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
               'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
+              ${optionalString (c.defaultPhoneRegion != null) "'default_phone_region' => '${c.defaultPhoneRegion}',"}
             ];
           '';
           occInstallCmd = let
@@ -435,7 +487,7 @@ in {
               then ''"$(<"${toString c.dbpassFile}")"''
               else if c.dbpass != null
               then ''"${toString c.dbpass}"''
-              else null;
+              else ''""'';
             adminpass = if c.adminpassFile != null
               then ''"$(<"${toString c.adminpassFile}")"''
               else ''"${toString c.adminpass}"'';
@@ -449,8 +501,7 @@ in {
               ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"'';
               ${if c.dbport != null then "--database-port" else null} = ''"${toString c.dbport}"'';
               ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"'';
-              ${if (any (x: x != null) [c.dbpass c.dbpassFile])
-                 then "--database-pass" else null} = dbpass;
+              "--database-pass" = dbpass;
               ${if c.dbtableprefix != null
                 then "--database-table-prefix" else null} = ''"${toString c.dbtableprefix}"'';
               "--admin-user" = ''"${c.adminuser}"'';
@@ -473,6 +524,28 @@ in {
           path = [ occ ];
           script = ''
             chmod og+x ${cfg.home}
+
+            ${optionalString (c.dbpassFile != null) ''
+              if [ ! -r "${c.dbpassFile}" ]; then
+                echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..."
+                exit 1
+              fi
+              if [ -z "$(<${c.dbpassFile})" ]; then
+                echo "dbpassFile ${c.dbpassFile} is empty!"
+                exit 1
+              fi
+            ''}
+            ${optionalString (c.adminpassFile != null) ''
+              if [ ! -r "${c.adminpassFile}" ]; then
+                echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..."
+                exit 1
+              fi
+              if [ -z "$(<${c.adminpassFile})" ]; then
+                echo "adminpassFile ${c.adminpassFile} is empty!"
+                exit 1
+              fi
+            ''}
+
             ln -sf ${cfg.package}/apps ${cfg.home}/
 
             # create nextcloud directories.
@@ -518,7 +591,6 @@ in {
         pools.nextcloud = {
           user = "nextcloud";
           group = "nextcloud";
-          phpOptions = phpOptionsStr;
           phpPackage = phpPackage;
           phpEnv = {
             NEXTCLOUD_CONFIG_DIR = "${cfg.home}/config";
@@ -536,12 +608,14 @@ in {
         home = "${cfg.home}";
         group = "nextcloud";
         createHome = true;
+        isSystemUser = true;
       };
       users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
 
       environment.systemPackages = [ occ ];
 
       services.nginx.enable = mkDefault true;
+
       services.nginx.virtualHosts.${cfg.hostName} = {
         root = cfg.package;
         locations = {
@@ -553,9 +627,17 @@ in {
               access_log off;
             '';
           };
+          "= /" = {
+            priority = 100;
+            extraConfig = ''
+              if ( $http_user_agent ~ ^DavClnt ) {
+                return 302 /remote.php/webdav/$is_args$args;
+              }
+            '';
+          };
           "/" = {
             priority = 900;
-            extraConfig = "try_files $uri $uri/ /index.php$request_uri;";
+            extraConfig = "rewrite ^ /index.php;";
           };
           "~ ^/store-apps" = {
             priority = 201;
@@ -564,11 +646,15 @@ in {
           "^~ /.well-known" = {
             priority = 210;
             extraConfig = ''
+              absolute_redirect off;
               location = /.well-known/carddav {
-                return 301 $scheme://$host/remote.php/dav;
+                return 301 /remote.php/dav;
               }
               location = /.well-known/caldav {
-                return 301 $scheme://$host/remote.php/dav;
+                return 301 /remote.php/dav;
+              }
+              location ~ ^/\.well-known/(?!acme-challenge|pki-validation) {
+                return 301 /index.php$request_uri;
               }
               try_files $uri $uri/ =404;
             '';
@@ -576,10 +662,10 @@ in {
           "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)".extraConfig = ''
             return 404;
           '';
-          "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)".extraConfig = ''
+          "~ ^/(?:\\.(?!well-known)|autotest|occ|issue|indie|db_|console)".extraConfig = ''
             return 404;
           '';
-          "~ \\.php(?:$|/)" = {
+          "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" = {
             priority = 500;
             extraConfig = ''
               include ${config.services.nginx.package}/conf/fastcgi.conf;
@@ -597,24 +683,22 @@ in {
               fastcgi_read_timeout 120s;
             '';
           };
-          "~ \\.(?:css|js|svg|gif|map)$".extraConfig = ''
+          "~ \\.(?:css|js|woff2?|svg|gif|map)$".extraConfig = ''
             try_files $uri /index.php$request_uri;
             expires 6M;
             access_log off;
           '';
-          "~ \\.woff2?$".extraConfig = ''
-            try_files $uri /index.php$request_uri;
-            expires 7d;
-            access_log off;
-          '';
           "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
             try_files $uri/ =404;
             index index.php;
           '';
+          "~ \\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$".extraConfig = ''
+            try_files $uri /index.php$request_uri;
+            access_log off;
+          '';
         };
         extraConfig = ''
           index index.php index.html /index.php$request_uri;
-          expires 1m;
           add_header X-Content-Type-Options nosniff;
           add_header X-XSS-Protection "1; mode=block";
           add_header X-Robots-Tag none;
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
index 02e4dba2861..3af37b15dd5 100644
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ b/nixos/modules/services/web-apps/nextcloud.xml
@@ -10,6 +10,10 @@
   <link linkend="opt-services.nextcloud.enable">services.nextcloud</link>. A
   desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
  </para>
+ <para>
+  The current default by NixOS is <package>nextcloud22</package> which is also the latest
+  major version available.
+ </para>
  <section xml:id="module-services-nextcloud-basic-usage">
   <title>Basic usage</title>
 
@@ -178,6 +182,17 @@
   </para>
  </section>
 
+ <section xml:id="installing-apps-php-extensions-nextcloud">
+  <title>Installing Apps and PHP extensions</title>
+
+  <para>
+   Nextcloud apps are installed statefully through the web interface.
+
+   Some apps may require extra PHP extensions to be installed.
+   This can be configured with the <xref linkend="opt-services.nextcloud.phpExtraExtensions" /> setting.
+  </para>
+ </section>
+
  <section xml:id="module-services-nextcloud-maintainer-info">
   <title>Maintainer information</title>
 
@@ -210,7 +225,7 @@
   nextcloud17 = generic {
     version = "17.0.x";
     sha256 = "0000000000000000000000000000000000000000000000000000";
-    insecure = true;
+    eol = true;
   };
 }</programlisting>
   </para>
diff --git a/nixos/modules/services/web-apps/plantuml-server.nix b/nixos/modules/services/web-apps/plantuml-server.nix
new file mode 100644
index 00000000000..a39f594c274
--- /dev/null
+++ b/nixos/modules/services/web-apps/plantuml-server.nix
@@ -0,0 +1,123 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.plantuml-server;
+
+in
+
+{
+  options = {
+    services.plantuml-server = {
+      enable = mkEnableOption "PlantUML server";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.plantuml-server;
+        description = "PlantUML server package to use";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "plantuml";
+        description = "User which runs PlantUML server.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "plantuml";
+        description = "Group which runs PlantUML server.";
+      };
+
+      home = mkOption {
+        type = types.str;
+        default = "/var/lib/plantuml";
+        description = "Home directory of the PlantUML server instance.";
+      };
+
+      listenHost = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Host to listen on.";
+      };
+
+      listenPort = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Port to listen on.";
+      };
+
+      plantumlLimitSize = mkOption {
+        type = types.int;
+        default = 4096;
+        description = "Limits image width and height.";
+      };
+
+      graphvizPackage = mkOption {
+        type = types.package;
+        default = pkgs.graphviz_2_32;
+        description = "Package containing the dot executable.";
+      };
+
+      plantumlStats = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Set it to on to enable statistics report (https://plantuml.com/statistics-report).";
+      };
+
+      httpAuthorization = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "When calling the proxy endpoint, the value of HTTP_AUTHORIZATION will be used to set the HTTP Authorization header.";
+      };
+
+      allowPlantumlInclude = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enables !include processing which can read files from the server into diagrams. Files are read relative to the current working directory.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.users.${cfg.user} = {
+      isSystemUser = true;
+      group = cfg.group;
+      home = cfg.home;
+      createHome = true;
+    };
+
+    users.groups.${cfg.group} = {};
+
+    systemd.services.plantuml-server = {
+      description = "PlantUML server";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.home ];
+      environment = {
+        PLANTUML_LIMIT_SIZE = builtins.toString cfg.plantumlLimitSize;
+        GRAPHVIZ_DOT = "${cfg.graphvizPackage}/bin/dot";
+        PLANTUML_STATS = if cfg.plantumlStats then "on" else "off";
+        HTTP_AUTHORIZATION = cfg.httpAuthorization;
+        ALLOW_PLANTUML_INCLUDE = if cfg.allowPlantumlInclude then "true" else "false";
+      };
+      script = ''
+      ${pkgs.jre}/bin/java \
+        -jar ${pkgs.jetty}/start.jar \
+          --module=deploy,http,jsp \
+          jetty.home=${pkgs.jetty} \
+          jetty.base=${cfg.package} \
+          jetty.http.host=${cfg.listenHost} \
+          jetty.http.port=${builtins.toString cfg.listenPort}
+      '';
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        PrivateTmp = true;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ truh ];
+}
diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix
new file mode 100644
index 00000000000..b56848b79d2
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.nix
@@ -0,0 +1,285 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.plausible;
+
+  # FIXME consider using LoadCredential as soon as it actually works.
+  envSecrets = ''
+    ADMIN_USER_PWD="$(<${cfg.adminUser.passwordFile})"
+    export ADMIN_USER_PWD # separate export to make `set -e` work
+
+    SECRET_KEY_BASE="$(<${cfg.server.secretKeybaseFile})"
+    export SECRET_KEY_BASE # separate export to make `set -e` work
+
+    ${optionalString (cfg.mail.smtp.passwordFile != null) ''
+      SMTP_USER_PWD="$(<${cfg.mail.smtp.passwordFile})"
+      export SMTP_USER_PWD # separate export to make `set -e` work
+    ''}
+  '';
+in {
+  options.services.plausible = {
+    enable = mkEnableOption "plausible";
+
+    adminUser = {
+      name = mkOption {
+        default = "admin";
+        type = types.str;
+        description = ''
+          Name of the admin user that plausible will created on initial startup.
+        '';
+      };
+
+      email = mkOption {
+        type = types.str;
+        example = "admin@localhost";
+        description = ''
+          Email-address of the admin-user.
+        '';
+      };
+
+      passwordFile = mkOption {
+        type = types.either types.str types.path;
+        description = ''
+          Path to the file which contains the password of the admin user.
+        '';
+      };
+
+      activate = mkEnableOption "activating the freshly created admin-user";
+    };
+
+    database = {
+      clickhouse = {
+        setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
+        url = mkOption {
+          default = "http://localhost:8123/default";
+          type = types.str;
+          description = ''
+            The URL to be used to connect to <package>clickhouse</package>.
+          '';
+        };
+      };
+      postgres = {
+        setup = mkEnableOption "creating a postgresql instance" // { default = true; };
+        dbname = mkOption {
+          default = "plausible";
+          type = types.str;
+          description = ''
+            Name of the database to use.
+          '';
+        };
+        socket = mkOption {
+          default = "/run/postgresql";
+          type = types.str;
+          description = ''
+            Path to the UNIX domain-socket to communicate with <package>postgres</package>.
+          '';
+        };
+      };
+    };
+
+    server = {
+      disableRegistration = mkOption {
+        default = true;
+        type = types.bool;
+        description = ''
+          Whether to prohibit creating an account in plausible's UI.
+        '';
+      };
+      secretKeybaseFile = mkOption {
+        type = types.either types.path types.str;
+        description = ''
+          Path to the secret used by the <literal>phoenix</literal>-framework. Instructions
+          how to generate one are documented in the
+          <link xlink:href="https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content">
+          framework docs</link>.
+        '';
+      };
+      port = mkOption {
+        default = 8000;
+        type = types.port;
+        description = ''
+          Port where the service should be available.
+        '';
+      };
+      baseUrl = mkOption {
+        type = types.str;
+        description = ''
+          Public URL where plausible is available.
+
+          Note that <literal>/path</literal> components are currently ignored:
+          <link xlink:href="https://github.com/plausible/analytics/issues/1182">
+            https://github.com/plausible/analytics/issues/1182
+          </link>.
+        '';
+      };
+    };
+
+    mail = {
+      email = mkOption {
+        default = "hello@plausible.local";
+        type = types.str;
+        description = ''
+          The email id to use for as <emphasis>from</emphasis> address of all communications
+          from Plausible.
+        '';
+      };
+      smtp = {
+        hostAddr = mkOption {
+          default = "localhost";
+          type = types.str;
+          description = ''
+            The host address of your smtp server.
+          '';
+        };
+        hostPort = mkOption {
+          default = 25;
+          type = types.port;
+          description = ''
+            The port of your smtp server.
+          '';
+        };
+        user = mkOption {
+          default = null;
+          type = types.nullOr types.str;
+          description = ''
+            The username/email in case SMTP auth is enabled.
+          '';
+        };
+        passwordFile = mkOption {
+          default = null;
+          type = with types; nullOr (either str path);
+          description = ''
+            The path to the file with the password in case SMTP auth is enabled.
+          '';
+        };
+        enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
+        retries = mkOption {
+          type = types.ints.unsigned;
+          default = 2;
+          description = ''
+            Number of retries to make until mailer gives up.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
+        message = ''
+          Unable to automatically activate the admin-user if no locally managed DB for
+          postgres (`services.plausible.database.postgres.setup') is enabled!
+        '';
+      }
+    ];
+
+    services.postgresql = mkIf cfg.database.postgres.setup {
+      enable = true;
+    };
+
+    services.clickhouse = mkIf cfg.database.clickhouse.setup {
+      enable = true;
+    };
+
+    systemd.services = mkMerge [
+      {
+        plausible = {
+          inherit (pkgs.plausible.meta) description;
+          documentation = [ "https://plausible.io/docs/self-hosting" ];
+          wantedBy = [ "multi-user.target" ];
+          after = optional cfg.database.postgres.setup "plausible-postgres.service";
+          requires = optional cfg.database.clickhouse.setup "clickhouse.service"
+            ++ optionals cfg.database.postgres.setup [
+              "postgresql.service"
+              "plausible-postgres.service"
+            ];
+
+          environment = {
+            # NixOS specific option to avoid that it's trying to write into its store-path.
+            # See also https://github.com/lau/tzdata#data-directory-and-releases
+            TZDATA_DIR = "/var/lib/plausible/elixir_tzdata";
+
+            # Configuration options from
+            # https://plausible.io/docs/self-hosting-configuration
+            PORT = toString cfg.server.port;
+            DISABLE_REGISTRATION = boolToString cfg.server.disableRegistration;
+
+            RELEASE_TMP = "/var/lib/plausible/tmp";
+
+            ADMIN_USER_NAME = cfg.adminUser.name;
+            ADMIN_USER_EMAIL = cfg.adminUser.email;
+
+            DATABASE_SOCKET_DIR = cfg.database.postgres.socket;
+            DATABASE_NAME = cfg.database.postgres.dbname;
+            CLICKHOUSE_DATABASE_URL = cfg.database.clickhouse.url;
+
+            BASE_URL = cfg.server.baseUrl;
+
+            MAILER_EMAIL = cfg.mail.email;
+            SMTP_HOST_ADDR = cfg.mail.smtp.hostAddr;
+            SMTP_HOST_PORT = toString cfg.mail.smtp.hostPort;
+            SMTP_RETRIES = toString cfg.mail.smtp.retries;
+            SMTP_HOST_SSL_ENABLED = boolToString cfg.mail.smtp.enableSSL;
+
+            SELFHOST = "true";
+          } // (optionalAttrs (cfg.mail.smtp.user != null) {
+            SMTP_USER_NAME = cfg.mail.smtp.user;
+          });
+
+          path = [ pkgs.plausible ]
+            ++ optional cfg.database.postgres.setup config.services.postgresql.package;
+
+          serviceConfig = {
+            DynamicUser = true;
+            PrivateTmp = true;
+            WorkingDirectory = "/var/lib/plausible";
+            StateDirectory = "plausible";
+            ExecStartPre = "@${pkgs.writeShellScript "plausible-setup" ''
+              set -eu -o pipefail
+              ${envSecrets}
+              ${pkgs.plausible}/createdb.sh
+              ${pkgs.plausible}/migrate.sh
+              ${optionalString cfg.adminUser.activate ''
+                if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
+                  psql -d plausible <<< "UPDATE users SET email_verified=true;"
+                fi
+              ''}
+            ''} plausible-setup";
+            ExecStart = "@${pkgs.writeShellScript "plausible" ''
+              set -eu -o pipefail
+              ${envSecrets}
+              plausible start
+            ''} plausible";
+          };
+        };
+      }
+      (mkIf cfg.database.postgres.setup {
+        # `plausible' requires the `citext'-extension.
+        plausible-postgres = {
+          after = [ "postgresql.service" ];
+          bindsTo = [ "postgresql.service" ];
+          requiredBy = [ "plausible.service" ];
+          partOf = [ "plausible.service" ];
+          serviceConfig.Type = "oneshot";
+          unitConfig.ConditionPathExists = "!/var/lib/plausible/.db-setup";
+          script = ''
+            mkdir -p /var/lib/plausible/
+            PSQL() {
+              /run/wrappers/bin/sudo -Hu postgres ${config.services.postgresql.package}/bin/psql --port=5432 "$@"
+            }
+            PSQL -tAc "CREATE ROLE plausible WITH LOGIN;"
+            PSQL -tAc "CREATE DATABASE plausible WITH OWNER plausible;"
+            PSQL -d plausible -tAc "CREATE EXTENSION IF NOT EXISTS citext;"
+            touch /var/lib/plausible/.db-setup
+          '';
+        };
+      })
+    ];
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+  meta.doc = ./plausible.xml;
+}
diff --git a/nixos/modules/services/web-apps/plausible.xml b/nixos/modules/services/web-apps/plausible.xml
new file mode 100644
index 00000000000..92a571b9fbd
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.xml
@@ -0,0 +1,51 @@
+<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-plausible">
+ <title>Plausible</title>
+ <para>
+  <link xlink:href="https://plausible.io/">Plausible</link> is a privacy-friendly alternative to
+  Google analytics.
+ </para>
+ <section xml:id="module-services-plausible-basic-usage">
+  <title>Basic Usage</title>
+  <para>
+   At first, a secret key is needed to be generated. This can be done with e.g.
+   <screen><prompt>$ </prompt>openssl rand -base64 64</screen>
+  </para>
+  <para>
+   After that, <package>plausible</package> can be deployed like this:
+<programlisting>{
+  services.plausible = {
+    <link linkend="opt-services.plausible.enable">enable</link> = true;
+    adminUser = {
+      <link linkend="opt-services.plausible.adminUser.activate">activate</link> = true; <co xml:id='ex-plausible-cfg-activate' />
+      <link linkend="opt-services.plausible.adminUser.email">email</link> = "admin@localhost";
+      <link linkend="opt-services.plausible.adminUser.passwordFile">passwordFile</link> = "/run/secrets/plausible-admin-pwd";
+    };
+    server = {
+      <link linkend="opt-services.plausible.server.baseUrl">baseUrl</link> = "http://analytics.example.org";
+      <link linkend="opt-services.plausible.server.secretKeybaseFile">secretKeybaseFile</link> = "/run/secrets/plausible-secret-key-base"; <co xml:id='ex-plausible-cfg-secretbase' />
+    };
+  };
+}</programlisting>
+   <calloutlist>
+    <callout arearefs='ex-plausible-cfg-activate'>
+     <para>
+      <varname>activate</varname> is used to skip the email verification of the admin-user that's
+      automatically created by <package>plausible</package>. This is only supported if
+      <package>postgresql</package> is configured by the module. This is done by default, but
+      can be turned off with <xref linkend="opt-services.plausible.database.postgres.setup" />.
+     </para>
+    </callout>
+    <callout arearefs='ex-plausible-cfg-secretbase'>
+     <para>
+      <varname>secretKeybaseFile</varname> is a path to the file which contains the secret generated
+      with <package>openssl</package> as described above.
+     </para>
+    </callout>
+   </calloutlist>
+  </para>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/web-apps/shiori.nix b/nixos/modules/services/web-apps/shiori.nix
index 1817a203935..a15bb9744a9 100644
--- a/nixos/modules/services/web-apps/shiori.nix
+++ b/nixos/modules/services/web-apps/shiori.nix
@@ -37,11 +37,57 @@ in {
       description = "Shiori simple bookmarks manager";
       wantedBy = [ "multi-user.target" ];
 
+      environment.SHIORI_DIR = "/var/lib/shiori";
+
       serviceConfig = {
         ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}'";
+
         DynamicUser = true;
-        Environment = "SHIORI_DIR=/var/lib/shiori";
         StateDirectory = "shiori";
+        # As the RootDirectory
+        RuntimeDirectory = "shiori";
+
+        # Security options
+
+        BindReadOnlyPaths = [
+          "/nix/store"
+
+          # For SSL certificates, and the resolv.conf
+          "/etc"
+        ];
+
+        CapabilityBoundingSet = "";
+
+        DeviceAllow = "";
+
+        LockPersonality = true;
+
+        MemoryDenyWriteExecute = true;
+
+        PrivateDevices = true;
+        PrivateUsers = true;
+
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+
+        RestrictNamespaces = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        RootDirectory = "/run/shiori";
+
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@resources" "~@setuid"
+        ];
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/sogo.nix b/nixos/modules/services/web-apps/sogo.nix
index 5f30124dd68..4610bb96cb5 100644
--- a/nixos/modules/services/web-apps/sogo.nix
+++ b/nixos/modules/services/web-apps/sogo.nix
@@ -77,7 +77,6 @@ in {
         // Paths
         WOSendMail = "/run/wrappers/bin/sendmail";
         SOGoMailSpoolPath = "/var/lib/sogo/spool";
-        SOGoZipPath = "${pkgs.zip}/bin/zip";
         // Enable CSRF protection
         SOGoXSRFValidationEnabled = YES;
         // Remove dates from log (jornald does that)
diff --git a/nixos/modules/services/web-apps/trilium.nix b/nixos/modules/services/web-apps/trilium.nix
index 3fa8dad0490..35383c992fe 100644
--- a/nixos/modules/services/web-apps/trilium.nix
+++ b/nixos/modules/services/web-apps/trilium.nix
@@ -9,6 +9,7 @@ let
 
     # Disable automatically generating desktop icon
     noDesktopIcon=true
+    noBackup=${lib.boolToString cfg.noBackup}
 
     [Network]
     # host setting is relevant only for web deployments - set the host on which the server will listen
@@ -28,7 +29,7 @@ in
       type = types.str;
       default = "/var/lib/trilium";
       description = ''
-        The directory storing the nodes database and the configuration.
+        The directory storing the notes database and the configuration.
       '';
     };
 
@@ -40,6 +41,14 @@ in
       '';
     };
 
+    noBackup = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Disable periodic database backups.
+      '';
+    };
+
     host = mkOption {
       type = types.str;
       default = "127.0.0.1";
@@ -85,7 +94,7 @@ in
 
   config = lib.mkIf cfg.enable (lib.mkMerge [
   {
-    meta.maintainers = with lib.maintainers; [ kampka ];
+    meta.maintainers = with lib.maintainers; [ fliegendewurst ];
 
     users.groups.trilium = {};
     users.users.trilium = {
diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix
index 6a29f10d119..b78487cc928 100644
--- a/nixos/modules/services/web-apps/tt-rss.nix
+++ b/nixos/modules/services/web-apps/tt-rss.nix
@@ -644,7 +644,7 @@ let
 
     services.mysql = mkIf mysqlLocal {
       enable = true;
-      package = mkDefault pkgs.mysql;
+      package = mkDefault pkgs.mariadb;
       ensureDatabases = [ cfg.database.name ];
       ensureUsers = [
         {
diff --git a/nixos/modules/services/web-apps/vikunja.nix b/nixos/modules/services/web-apps/vikunja.nix
new file mode 100644
index 00000000000..b0b6eb6df17
--- /dev/null
+++ b/nixos/modules/services/web-apps/vikunja.nix
@@ -0,0 +1,145 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vikunja;
+  format = pkgs.formats.yaml {};
+  configFile = format.generate "config.yaml" cfg.settings;
+  useMysql = cfg.database.type == "mysql";
+  usePostgresql = cfg.database.type == "postgres";
+in {
+  options.services.vikunja = with lib; {
+    enable = mkEnableOption "vikunja service";
+    package-api = mkOption {
+      default = pkgs.vikunja-api;
+      type = types.package;
+      defaultText = "pkgs.vikunja-api";
+      description = "vikunja-api derivation to use.";
+    };
+    package-frontend = mkOption {
+      default = pkgs.vikunja-frontend;
+      type = types.package;
+      defaultText = "pkgs.vikunja-frontend";
+      description = "vikunja-frontend derivation to use.";
+    };
+    environmentFiles = mkOption {
+      type = types.listOf types.path;
+      default = [ ];
+      description = ''
+        List of environment files set in the vikunja systemd service.
+        For example passwords should be set in one of these files.
+      '';
+    };
+    setupNginx = mkOption {
+      type = types.bool;
+      default = config.services.nginx.enable;
+      defaultText = "config.services.nginx.enable";
+      description = ''
+        Whether to setup NGINX.
+        Further nginx configuration can be done by changing
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;</option>.
+        This does not enable TLS or ACME by default. To enable this, set the
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.enableACME</option> to
+        <literal>true</literal> and if appropriate do the same for
+        <option>services.nginx.virtualHosts.&lt;frontendHostname&gt;.forceSSL</option>.
+      '';
+    };
+    frontendScheme = mkOption {
+      type = types.enum [ "http" "https" ];
+      description = ''
+        Whether the site is available via http or https.
+        This does not configure https or ACME in nginx!
+      '';
+    };
+    frontendHostname = mkOption {
+      type = types.str;
+      description = "The Hostname under which the frontend is running.";
+    };
+
+    settings = mkOption {
+      type = format.type;
+      default = {};
+      description = ''
+        Vikunja configuration. Refer to
+        <link xlink:href="https://vikunja.io/docs/config-options/"/>
+        for details on supported values.
+        '';
+    };
+    database = {
+      type = mkOption {
+        type = types.enum [ "sqlite" "mysql" "postgres" ];
+        example = "postgres";
+        default = "sqlite";
+        description = "Database engine to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Database host address. Can also be a socket.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = "vikunja";
+        description = "Database user.";
+      };
+      database = mkOption {
+        type = types.str;
+        default = "vikunja";
+        description = "Database name.";
+      };
+      path = mkOption {
+        type = types.str;
+        default = "/var/lib/vikunja/vikunja.db";
+        description = "Path to the sqlite3 database file.";
+      };
+    };
+  };
+  config = lib.mkIf cfg.enable {
+    services.vikunja.settings = {
+      database = {
+        inherit (cfg.database) type host user database path;
+      };
+      service = {
+        frontendurl = "${cfg.frontendScheme}://${cfg.frontendHostname}/";
+      };
+      files = {
+        basepath = "/var/lib/vikunja/files";
+      };
+    };
+
+    systemd.services.vikunja-api = {
+      description = "vikunja-api";
+      after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+      path = [ cfg.package-api ];
+      restartTriggers = [ configFile ];
+
+      serviceConfig = {
+        Type = "simple";
+        DynamicUser = true;
+        StateDirectory = "vikunja";
+        ExecStart = "${cfg.package-api}/bin/vikunja";
+        Restart = "always";
+        EnvironmentFile = cfg.environmentFiles;
+      };
+    };
+
+    services.nginx.virtualHosts."${cfg.frontendHostname}" = mkIf cfg.setupNginx {
+      locations = {
+        "/" = {
+          root = cfg.package-frontend;
+          tryFiles = "try_files $uri $uri/ /";
+        };
+        "~* ^/(api|dav|\\.well-known)/" = {
+          proxyPass = "http://localhost:3456";
+          extraConfig = ''
+            client_max_body_size 20M;
+          '';
+        };
+      };
+    };
+
+    environment.etc."vikunja/config.yaml".source = configFile;
+  };
+}
diff --git a/nixos/modules/services/web-apps/whitebophir.nix b/nixos/modules/services/web-apps/whitebophir.nix
new file mode 100644
index 00000000000..b265296d5c1
--- /dev/null
+++ b/nixos/modules/services/web-apps/whitebophir.nix
@@ -0,0 +1,52 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.whitebophir;
+in {
+  options = {
+    services.whitebophir = {
+      enable = mkEnableOption "whitebophir, an online collaborative whiteboard server (persistent state will be maintained under <filename>/var/lib/whitebophir</filename>)";
+
+      package = mkOption {
+        default = pkgs.whitebophir;
+        defaultText = "pkgs.whitebophir";
+        type = types.package;
+        description = "Whitebophir package to use.";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 5001;
+        description = "Port to bind to.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.whitebophir = {
+      description = "Whitebophir Service";
+      wantedBy    = [ "multi-user.target" ];
+      after       = [ "network.target" ];
+      environment = {
+        PORT            = toString cfg.port;
+        HOST            = toString cfg.listenAddress;
+        WBO_HISTORY_DIR = "/var/lib/whitebophir";
+      };
+
+      serviceConfig = {
+        DynamicUser    = true;
+        ExecStart      = "${cfg.package}/bin/whitebophir";
+        Restart        = "always";
+        StateDirectory = "whitebophir";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/wiki-js.nix b/nixos/modules/services/web-apps/wiki-js.nix
new file mode 100644
index 00000000000..1a6259dffee
--- /dev/null
+++ b/nixos/modules/services/web-apps/wiki-js.nix
@@ -0,0 +1,139 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+
+let
+  cfg = config.services.wiki-js;
+
+  format = pkgs.formats.json { };
+
+  configFile = format.generate "wiki-js.yml" cfg.settings;
+in {
+  options.services.wiki-js = {
+    enable = mkEnableOption "wiki-js";
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/root/wiki-js.env";
+      description = ''
+        Environment fiel to inject e.g. secrets into the configuration.
+      '';
+    };
+
+    stateDirectoryName = mkOption {
+      default = "wiki-js";
+      type = types.str;
+      description = ''
+        Name of the directory in <filename>/var/lib</filename>.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      type = types.submodule {
+        freeformType = format.type;
+        options = {
+          port = mkOption {
+            type = types.port;
+            default = 3000;
+            description = ''
+              TCP port the process should listen to.
+            '';
+          };
+
+          bindIP = mkOption {
+            default = "0.0.0.0";
+            type = types.str;
+            description = ''
+              IPs the service should listen to.
+            '';
+          };
+
+          db = {
+            type = mkOption {
+              default = "postgres";
+              type = types.enum [ "postgres" "mysql" "mariadb" "mssql" ];
+              description = ''
+                Database driver to use for persistence. Please note that <literal>sqlite</literal>
+                is currently not supported as the build process for it is currently not implemented
+                in <package>pkgs.wiki-js</package> and it's not recommended by upstream for
+                production use.
+              '';
+            };
+            host = mkOption {
+              type = types.str;
+              example = "/run/postgresql";
+              description = ''
+                Hostname or socket-path to connect to.
+              '';
+            };
+            db = mkOption {
+              default = "wiki";
+              type = types.str;
+              description = ''
+                Name of the database to use.
+              '';
+            };
+          };
+
+          logLevel = mkOption {
+            default = "info";
+            type = types.enum [ "error" "warn" "info" "verbose" "debug" "silly" ];
+            description = ''
+              Define how much detail is supposed to be logged at runtime.
+            '';
+          };
+
+          offline = mkEnableOption "offline mode" // {
+            description = ''
+              Disable latest file updates and enable
+              <link xlink:href="https://docs.requarks.io/install/sideload">sideloading</link>.
+            '';
+          };
+        };
+      };
+      description = ''
+        Settings to configure <package>wiki-js</package>. This directly
+        corresponds to <link xlink:href="https://docs.requarks.io/install/config">the upstream
+        configuration options</link>.
+
+        Secrets can be injected via the environment by
+        <itemizedlist>
+          <listitem><para>specifying <xref linkend="opt-services.wiki-js.environmentFile" />
+          to contain secrets</para></listitem>
+          <listitem><para>and setting sensitive values to <literal>$(ENVIRONMENT_VAR)</literal>
+          with this value defined in the environment-file.</para></listitem>
+        </itemizedlist>
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.wiki-js.settings.dataPath = "/var/lib/${cfg.stateDirectoryName}";
+    systemd.services.wiki-js = {
+      description = "A modern and powerful wiki app built on Node.js";
+      documentation = [ "https://docs.requarks.io/" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = with pkgs; [ coreutils ];
+      preStart = ''
+        ln -sf ${configFile} /var/lib/${cfg.stateDirectoryName}/config.yml
+        ln -sf ${pkgs.wiki-js}/server /var/lib/${cfg.stateDirectoryName}
+        ln -sf ${pkgs.wiki-js}/assets /var/lib/${cfg.stateDirectoryName}
+        ln -sf ${pkgs.wiki-js}/package.json /var/lib/${cfg.stateDirectoryName}/package.json
+      '';
+
+      serviceConfig = {
+        EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+        StateDirectory = cfg.stateDirectoryName;
+        WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
+        DynamicUser = true;
+        PrivateTmp = true;
+        ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.wiki-js}/server";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ma27 ];
+}
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index 5fbe53221ae..6f1ef815bc4 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -3,13 +3,18 @@
 let
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
   inherit (lib) any attrValues concatMapStringsSep flatten literalExample;
-  inherit (lib) mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
+  inherit (lib) filterAttrs mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
 
-  eachSite = config.services.wordpress;
+  cfg = migrateOldAttrs config.services.wordpress;
+  eachSite = cfg.sites;
   user = "wordpress";
-  group = config.services.httpd.group;
+  webserver = config.services.${cfg.webserver};
   stateDir = hostName: "/var/lib/wordpress/${hostName}";
 
+  # Migrate config.services.wordpress.<hostName> to config.services.wordpress.sites.<hostName>
+  oldSites = filterAttrs (o: _: o != "sites" && o != "webserver");
+  migrateOldAttrs = cfg: cfg // { sites = cfg.sites // oldSites cfg; };
+
   pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
     pname = "wordpress-${hostName}";
     version = src.version;
@@ -61,8 +66,10 @@ let
     ?>
   '';
 
-  secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ];
+  secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ];
   secretsScript = hostStateDir: ''
+    # The match in this line is not a typo, see https://github.com/NixOS/nixpkgs/pull/124839
+    grep -q "LOOGGED_IN_KEY" "${hostStateDir}/secret-keys.php" && rm "${hostStateDir}/secret-keys.php"
     if ! test -e "${hostStateDir}/secret-keys.php"; then
       umask 0177
       echo "<?php" >> "${hostStateDir}/secret-keys.php"
@@ -109,7 +116,7 @@ let
                 sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
               };
               # We need unzip to build this package
-              buildInputs = [ pkgs.unzip ];
+              nativeBuildInputs = [ pkgs.unzip ];
               # Installing simply means copying all files to the output directory
               installPhase = "mkdir -p $out; cp -R * $out/";
             };
@@ -136,7 +143,7 @@ let
                 sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
               };
               # We need unzip to build this package
-              buildInputs = [ pkgs.unzip ];
+              nativeBuildInputs = [ pkgs.unzip ];
               # Installing simply means copying all files to the output directory
               installPhase = "mkdir -p $out; cp -R * $out/";
             };
@@ -259,21 +266,48 @@ in
   # interface
   options = {
     services.wordpress = mkOption {
-      type = types.attrsOf (types.submodule siteOpts);
+      type = types.submodule {
+        # Used to support old interface
+        freeformType = types.attrsOf (types.submodule siteOpts);
+
+        # New interface
+        options.sites = mkOption {
+          type = types.attrsOf (types.submodule siteOpts);
+          default = {};
+          description = "Specification of one or more WordPress sites to serve";
+        };
+
+        options.webserver = mkOption {
+          type = types.enum [ "httpd" "nginx" ];
+          default = "httpd";
+          description = ''
+            Whether to use apache2 or nginx for virtual host management.
+
+            Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+            See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+
+            Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+            See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+          '';
+        };
+      };
       default = {};
-      description = "Specification of one or more WordPress sites to serve via Apache.";
+      description = "Wordpress configuration";
     };
+
   };
 
   # implementation
-  config = mkIf (eachSite != {}) {
+  config = mkIf (eachSite != {}) (mkMerge [{
 
     assertions = mapAttrsToList (hostName: cfg:
       { assertion = cfg.database.createLocally -> cfg.database.user == user;
-        message = "services.wordpress.${hostName}.database.user must be ${user} if the database is to be automatically provisioned";
+        message = ''services.wordpress.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
       }
     ) eachSite;
 
+    warnings = mapAttrsToList (hostName: _: ''services.wordpress."${hostName}" is deprecated use services.wordpress.sites."${hostName}"'') (oldSites cfg);
+
     services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
       enable = true;
       package = mkDefault pkgs.mariadb;
@@ -287,14 +321,18 @@ in
 
     services.phpfpm.pools = mapAttrs' (hostName: cfg: (
       nameValuePair "wordpress-${hostName}" {
-        inherit user group;
+        inherit user;
+        group = webserver.group;
         settings = {
-          "listen.owner" = config.services.httpd.user;
-          "listen.group" = config.services.httpd.group;
+          "listen.owner" = webserver.user;
+          "listen.group" = webserver.group;
         } // cfg.poolConfig;
       }
     )) eachSite;
 
+  }
+
+  (mkIf (cfg.webserver == "httpd") {
     services.httpd = {
       enable = true;
       extraModules = [ "proxy_fcgi" ];
@@ -330,11 +368,13 @@ in
         '';
       } ]) eachSite;
     };
+  })
 
+  {
     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
-      "d '${stateDir hostName}' 0750 ${user} ${group} - -"
-      "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
-      "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
+      "d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -"
+      "d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
+      "Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
     ]) eachSite);
 
     systemd.services = mkMerge [
@@ -348,7 +388,7 @@ in
           serviceConfig = {
             Type = "oneshot";
             User = user;
-            Group = group;
+            Group = webserver.group;
           };
       })) eachSite)
 
@@ -358,9 +398,65 @@ in
     ];
 
     users.users.${user} = {
-      group = group;
+      group = webserver.group;
       isSystemUser = true;
     };
+  }
 
-  };
+  (mkIf (cfg.webserver == "nginx") {
+    services.nginx = {
+      enable = true;
+      virtualHosts = mapAttrs (hostName: cfg: {
+        serverName = mkDefault hostName;
+        root = "${pkg hostName cfg}/share/wordpress";
+        extraConfig = ''
+          index index.php;
+        '';
+        locations = {
+          "/" = {
+            priority = 200;
+            extraConfig = ''
+              try_files $uri $uri/ /index.php$is_args$args;
+            '';
+          };
+          "~ \\.php$" = {
+            priority = 500;
+            extraConfig = ''
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket};
+              fastcgi_index index.php;
+              include "${config.services.nginx.package}/conf/fastcgi.conf";
+              fastcgi_param PATH_INFO $fastcgi_path_info;
+              fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
+              # Mitigate https://httpoxy.org/ vulnerabilities
+              fastcgi_param HTTP_PROXY "";
+              fastcgi_intercept_errors off;
+              fastcgi_buffer_size 16k;
+              fastcgi_buffers 4 16k;
+              fastcgi_connect_timeout 300;
+              fastcgi_send_timeout 300;
+              fastcgi_read_timeout 300;
+            '';
+          };
+          "~ /\\." = {
+            priority = 800;
+            extraConfig = "deny all;";
+          };
+          "~* /(?:uploads|files)/.*\\.php$" = {
+            priority = 900;
+            extraConfig = "deny all;";
+          };
+          "~* \\.(js|css|png|jpg|jpeg|gif|ico)$" = {
+            priority = 1000;
+            extraConfig = ''
+              expires max;
+              log_not_found off;
+            '';
+          };
+        };
+      }) eachSite;
+    };
+  })
+
+  ]);
 }
diff --git a/nixos/modules/services/web-apps/zabbix.nix b/nixos/modules/services/web-apps/zabbix.nix
index 00719512834..e94861a90b5 100644
--- a/nixos/modules/services/web-apps/zabbix.nix
+++ b/nixos/modules/services/web-apps/zabbix.nix
@@ -3,7 +3,7 @@
 let
 
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
-  inherit (lib) literalExample mapAttrs optionalString;
+  inherit (lib) literalExample mapAttrs optionalString versionAtLeast;
 
   cfg = config.services.zabbixWeb;
   fpm = config.services.phpfpm.pools.zabbix;
@@ -28,6 +28,8 @@ let
     $ZBX_SERVER_PORT = '${toString cfg.server.port}';
     $ZBX_SERVER_NAME = ''';
     $IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;
+
+    ${cfg.extraConfig}
   '';
 
 in
@@ -143,6 +145,14 @@ in
         '';
       };
 
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Additional configuration to be copied verbatim into <filename>zabbix.conf.php</filename>.
+        '';
+      };
+
     };
   };
 
@@ -150,6 +160,10 @@ in
 
   config = mkIf cfg.enable {
 
+    services.zabbixWeb.extraConfig = optionalString ((versionAtLeast config.system.stateVersion "20.09") && (versionAtLeast cfg.package.version "5.0.0")) ''
+      $DB['DOUBLE_IEEE754'] = 'true';
+    '';
+
     systemd.tmpfiles.rules = [
       "d '${stateDir}' 0750 ${user} ${group} - -"
       "d '${stateDir}/session' 0750 ${user} ${config.services.httpd.group} - -"
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index fc4c2945394..df7035c03cc 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -6,6 +6,8 @@ let
 
   cfg = config.services.httpd;
 
+  certs = config.security.acme.certs;
+
   runtimeDir = "/run/httpd";
 
   pkg = cfg.package.out;
@@ -13,19 +15,26 @@ let
   apachectl = pkgs.runCommand "apachectl" { meta.priority = -1; } ''
     mkdir -p $out/bin
     cp ${pkg}/bin/apachectl $out/bin/apachectl
-    sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f ${httpdConf}|'
+    sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f /etc/httpd/httpd.conf|'
   '';
 
-  httpdConf = cfg.configFile;
-
   php = cfg.phpPackage.override { apacheHttpd = pkg; };
 
-  phpMajorVersion = lib.versions.major (lib.getVersion php);
+  phpModuleName = let
+    majorVersion = lib.versions.major (lib.getVersion php);
+  in (if majorVersion == "8" then "php" else "php${majorVersion}");
 
   mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
 
   vhosts = attrValues cfg.virtualHosts;
 
+  # certName is used later on to determine systemd service names.
+  acmeEnabledVhosts = map (hostOpts: hostOpts // {
+    certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName;
+  }) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts);
+
+  dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
+
   mkListenInfo = hostOpts:
     if hostOpts.listen != [] then hostOpts.listen
     else (
@@ -54,7 +63,7 @@ let
     ++ optional enableSSL "ssl"
     ++ optional enableUserDir "userdir"
     ++ 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.enablePHP { name = phpModuleName; path = "${php}/modules/lib${phpModuleName}.so"; }
     ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
     ++ cfg.extraModules;
 
@@ -117,6 +126,17 @@ let
     </IfModule>
   '';
 
+  luaSetPaths = let
+    # support both lua and lua.withPackages derivations
+    luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion;
+    in
+  ''
+    <IfModule mod_lua.c>
+      LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so
+      LuaPackagePath  ${cfg.package.lua5}/share/lua/${luaversion}/?.lua
+    </IfModule>
+  '';
+
   mkVHostConf = hostOpts:
     let
       adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
@@ -125,13 +145,13 @@ let
 
       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
+        if hostOpts.enableACME then certs.${hostOpts.hostName}.directory
+        else if hostOpts.useACMEHost != null then certs.${hostOpts.useACMEHost}.directory
         else abort "This case should never happen.";
 
-      sslServerCert = if useACME then "${sslCertDir}/full.pem" else hostOpts.sslServerCert;
+      sslServerCert = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerCert;
       sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
-      sslServerChain = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerChain;
+      sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain;
 
       acmeChallenge = optionalString useACME ''
         Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
@@ -182,7 +202,7 @@ let
     let
       documentRoot = if hostOpts.documentRoot != null
         then hostOpts.documentRoot
-        else pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"
+        else pkgs.emptyDirectory
       ;
 
       mkLocations = locations: concatStringsSep "\n" (map (config: ''
@@ -317,6 +337,8 @@ let
 
     ${sslConf}
 
+    ${optionalString cfg.package.luaSupport luaSetPaths}
+
     # Fascist default - deny access to everything.
     <Directory />
         Options FollowSymLinks
@@ -347,7 +369,6 @@ let
       cat ${php.phpIni} > $out
       echo "$options" >> $out
     '';
-
 in
 
 
@@ -647,15 +668,20 @@ in
       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);
-
+    security.acme.certs = let
+      acmePairs = map (hostOpts: nameValuePair hostOpts.hostName {
+        group = mkDefault cfg.group;
+        webroot = hostOpts.acmeRoot;
+        extraDomainNames = hostOpts.serverAliases;
+        # Use the vhost-specific email address if provided, otherwise let
+        # security.acme.email or security.acme.certs.<cert>.email be used.
+        email = mkOverride 2000 (if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr);
+      # Filter for enableACME-only vhosts. Don't want to create dud certs
+      }) (filter (hostOpts: hostOpts.useACMEHost == null) acmeEnabledVhosts);
+    in listToAttrs acmePairs;
+
+    # httpd requires a stable path to the configuration file for reloads
+    environment.etc."httpd/httpd.conf".source = cfg.configFile;
     environment.systemPackages = [
       apachectl
       pkg
@@ -682,9 +708,6 @@ in
 
     services.httpd.phpOptions =
       ''
-        ; Needed for PHP's mail() function.
-        sendmail_path = ${pkgs.system-sendmail}/bin/sendmail -t -i
-
         ; Don't advertise PHP
         expose_php = off
       '' + optionalString (config.time.timeZone != null) ''
@@ -724,16 +747,13 @@ in
           "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
         ];
 
-    systemd.services.httpd =
-      let
-        vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts;
-      in
-      { description = "Apache HTTPD";
-
+    systemd.services.httpd = {
+        description = "Apache HTTPD";
         wantedBy = [ "multi-user.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;
-        before = map (hostOpts: "acme-${hostOpts.hostName}.service") vhostsACME;
+        wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
+        after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
+        before = map (certName: "acme-${certName}.service") dependentCertNames;
+        restartTriggers = [ cfg.configFile ];
 
         path = [ pkg pkgs.coreutils pkgs.gnugrep ];
 
@@ -746,15 +766,15 @@ in
             # 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 ' ${cfg.user} ' | cut -f2 -d ' '); do
-                ${pkgs.utillinux}/bin/ipcrm -s $i
+            for i in $(${pkgs.util-linux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
+                ${pkgs.util-linux}/bin/ipcrm -s $i
             done
           '';
 
         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";
+          ExecStart = "@${pkg}/bin/httpd httpd -f /etc/httpd/httpd.conf";
+          ExecStop = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful-stop";
+          ExecReload = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful";
           User = cfg.user;
           Group = cfg.group;
           Type = "forking";
@@ -767,5 +787,32 @@ in
         };
       };
 
+    # postRun hooks on cert renew can't be used to restart Apache since renewal
+    # runs as the unprivileged acme user. sslTargets are added to wantedBy + before
+    # which allows the acme-finished-$cert.target to signify the successful updating
+    # of certs end-to-end.
+    systemd.services.httpd-config-reload = let
+      sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
+      sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
+    in mkIf (sslServices != []) {
+      wantedBy = sslServices ++ [ "multi-user.target" ];
+      # Before the finished targets, after the renew services.
+      # This service might be needed for HTTP-01 challenges, but we only want to confirm
+      # certs are updated _after_ config has been reloaded.
+      before = sslTargets;
+      after = sslServices;
+      restartTriggers = [ cfg.configFile ];
+      # Block reloading if not all certs exist yet.
+      # Happens when config changes add new vhosts/certs.
+      unitConfig.ConditionPathExists = map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames;
+      serviceConfig = {
+        Type = "oneshot";
+        TimeoutSec = 60;
+        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active httpd.service";
+        ExecStartPre = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -t";
+        ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service";
+      };
+    };
+
   };
 }
diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
index 173c0f8561c..394f9a30554 100644
--- a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -112,7 +112,7 @@ in
 
     acmeRoot = mkOption {
       type = types.str;
-      default = "/var/lib/acme/acme-challenges";
+      default = "/var/lib/acme/acme-challenge";
       description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here";
     };
 
diff --git a/nixos/modules/services/web-servers/caddy.nix b/nixos/modules/services/web-servers/caddy.nix
index 0e6e10a5f47..955b9756406 100644
--- a/nixos/modules/services/web-servers/caddy.nix
+++ b/nixos/modules/services/web-servers/caddy.nix
@@ -5,7 +5,45 @@ with lib;
 let
   cfg = config.services.caddy;
   configFile = pkgs.writeText "Caddyfile" cfg.config;
+
+  tlsConfig = {
+    apps.tls.automation.policies = [{
+      issuer = {
+        inherit (cfg) ca email;
+        module = "acme";
+      };
+    }];
+  };
+
+  adaptedConfig = pkgs.runCommand "caddy-config-adapted.json" { } ''
+    ${cfg.package}/bin/caddy adapt \
+      --config ${configFile} --adapter ${cfg.adapter} > $out
+  '';
+  tlsJSON = pkgs.writeText "tls.json" (builtins.toJSON tlsConfig);
+
+  # merge the TLS config options we expose with the ones originating in the Caddyfile
+  configJSON =
+    let tlsConfigMerge = ''
+      {"apps":
+        {"tls":
+          {"automation":
+            {"policies":
+              (if .[0].apps.tls.automation.policies == .[1]?.apps.tls.automation.policies
+               then .[0].apps.tls.automation.policies
+               else (.[0].apps.tls.automation.policies + .[1]?.apps.tls.automation.policies)
+               end)
+            }
+          }
+        }
+      }'';
+    in pkgs.runCommand "caddy-config.json" { } ''
+    ${pkgs.jq}/bin/jq -s '.[0] * ${tlsConfigMerge}' ${adaptedConfig} ${tlsJSON} > $out
+  '';
 in {
+  imports = [
+    (mkRemovedOptionModule [ "services" "caddy" "agree" ] "this option is no longer necessary for Caddy 2")
+  ];
+
   options.services.caddy = {
     enable = mkEnableOption "Caddy web server";
 
@@ -13,15 +51,38 @@ in {
       default = "";
       example = ''
         example.com {
-        gzip
-        minify
-        log syslog
-
-        root /srv/http
+          encode gzip
+          log
+          root /srv/http
         }
       '';
       type = types.lines;
-      description = "Verbatim Caddyfile to use";
+      description = ''
+        Verbatim Caddyfile to use.
+        Caddy v2 supports multiple config formats via adapters (see <option>services.caddy.adapter</option>).
+      '';
+    };
+
+    user = mkOption {
+      default = "caddy";
+      type = types.str;
+      description = "User account under which caddy runs.";
+    };
+
+    group = mkOption {
+      default = "caddy";
+      type = types.str;
+      description = "Group account under which caddy runs.";
+    };
+
+    adapter = mkOption {
+      default = "caddyfile";
+      example = "nginx";
+      type = types.str;
+      description = ''
+        Name of the config adapter to use.
+        See https://caddyserver.com/docs/config-adapters for the full list.
+      '';
     };
 
     ca = mkOption {
@@ -37,12 +98,6 @@ in {
       description = "Email address (for Let's Encrypt certificate)";
     };
 
-    agree = mkOption {
-      default = false;
-      type = types.bool;
-      description = "Agree to Let's Encrypt Subscriber Agreement";
-    };
-
     dataDir = mkOption {
       default = "/var/lib/caddy";
       type = types.path;
@@ -50,39 +105,39 @@ in {
         The data directory, for storing certificates. Before 17.09, this
         would create a .caddy directory. With 17.09 the contents of the
         .caddy directory are in the specified data directory instead.
+
+        Caddy v2 replaced CADDYPATH with XDG directories.
+        See https://caddyserver.com/docs/conventions#file-locations.
       '';
     };
 
     package = mkOption {
       default = pkgs.caddy;
       defaultText = "pkgs.caddy";
+      example = "pkgs.caddy";
       type = types.package;
-      description = "Caddy package to use.";
+      description = ''
+        Caddy package to use.
+      '';
     };
   };
 
   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
+      # upstream unit: https://github.com/caddyserver/dist/blob/master/init/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; };
+      startLimitIntervalSec = 14400;
+      startLimitBurst = 10;
       serviceConfig = {
-        ExecStart = ''
-          ${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 -USR1 $MAINPID";
+        ExecStart = "${cfg.package}/bin/caddy run --config ${configJSON}";
+        ExecReload = "${cfg.package}/bin/caddy reload --config ${configJSON}";
         Type = "simple";
-        User = "caddy";
-        Group = "caddy";
+        User = cfg.user;
+        Group = cfg.group;
         Restart = "on-abnormal";
-        StartLimitIntervalSec = 14400;
-        StartLimitBurst = 10;
         AmbientCapabilities = "cap_net_bind_service";
         CapabilityBoundingSet = "cap_net_bind_service";
         NoNewPrivileges = true;
@@ -99,13 +154,18 @@ in {
       };
     };
 
-    users.users.caddy = {
-      group = "caddy";
-      uid = config.ids.uids.caddy;
-      home = cfg.dataDir;
-      createHome = true;
+    users.users = optionalAttrs (cfg.user == "caddy") {
+      caddy = {
+        group = cfg.group;
+        uid = config.ids.uids.caddy;
+        home = cfg.dataDir;
+        createHome = true;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "caddy") {
+      caddy.gid = config.ids.gids.caddy;
     };
 
-    users.groups.caddy.gid = config.ids.uids.caddy;
   };
 }
diff --git a/nixos/modules/services/web-servers/darkhttpd.nix b/nixos/modules/services/web-servers/darkhttpd.nix
index d6649fd472d..f6b693139a1 100644
--- a/nixos/modules/services/web-servers/darkhttpd.nix
+++ b/nixos/modules/services/web-servers/darkhttpd.nix
@@ -19,7 +19,7 @@ in {
 
     port = mkOption {
       default = 80;
-      type = ints.u16;
+      type = types.port;
       description = ''
         Port to listen on.
         Pass 0 to let the system choose any free port for you.
diff --git a/nixos/modules/services/web-servers/jboss/default.nix b/nixos/modules/services/web-servers/jboss/default.nix
index ca5b8635fc0..d243e0f3f1b 100644
--- a/nixos/modules/services/web-servers/jboss/default.nix
+++ b/nixos/modules/services/web-servers/jboss/default.nix
@@ -31,32 +31,38 @@ in
 
       tempDir = mkOption {
         default = "/tmp";
+        type = types.str;
         description = "Location where JBoss stores its temp files";
       };
 
       logDir = mkOption {
         default = "/var/log/jboss";
+        type = types.str;
         description = "Location of the logfile directory of JBoss";
       };
 
       serverDir = mkOption {
         description = "Location of the server instance files";
         default = "/var/jboss/server";
+        type = types.str;
       };
 
       deployDir = mkOption {
         description = "Location of the deployment files";
         default = "/nix/var/nix/profiles/default/server/default/deploy/";
+        type = types.str;
       };
 
       libUrl = mkOption {
         default = "file:///nix/var/nix/profiles/default/server/default/lib";
         description = "Location where the shared library JARs are stored";
+        type = types.str;
       };
 
       user = mkOption {
         default = "nobody";
         description = "User account under which jboss runs.";
+        type = types.str;
       };
 
       useJK = mkOption {
diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix
index 7a3df26e47a..7a691aa7891 100644
--- a/nixos/modules/services/web-servers/lighttpd/default.nix
+++ b/nixos/modules/services/web-servers/lighttpd/default.nix
@@ -134,7 +134,7 @@ in
 
       port = mkOption {
         default = 80;
-        type = types.int;
+        type = types.port;
         description = ''
           TCP port number for lighttpd to bind to.
         '';
@@ -193,7 +193,7 @@ in
       configText = mkOption {
         default = "";
         type = types.lines;
-        example = ''...verbatim config file contents...'';
+        example = "...verbatim config file contents...";
         description = ''
           Overridable config file contents to use for lighttpd. By default, use
           the contents automatically generated by NixOS.
diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix
index cd123000f00..d075449012f 100644
--- a/nixos/modules/services/web-servers/minio.nix
+++ b/nixos/modules/services/web-servers/minio.nix
@@ -4,6 +4,11 @@ with lib;
 
 let
   cfg = config.services.minio;
+
+  legacyCredentials = cfg: pkgs.writeText "minio-legacy-credentials" ''
+    MINIO_ROOT_USER=${cfg.accessKey}
+    MINIO_ROOT_PASSWORD=${cfg.secretKey}
+  '';
 in
 {
   meta.maintainers = [ maintainers.bachp ];
@@ -18,9 +23,9 @@ in
     };
 
     dataDir = mkOption {
-      default = "/var/lib/minio/data";
-      type = types.path;
-      description = "The data directory, for storing the objects.";
+      default = [ "/var/lib/minio/data" ];
+      type = types.listOf types.path;
+      description = "The list of data directories for storing the objects. Use one path for regular operation and the minimum of 4 endpoints for Erasure Code mode.";
     };
 
     configDir = mkOption {
@@ -49,6 +54,17 @@ in
       '';
     };
 
+    rootCredentialsFile = mkOption  {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        File containing the MINIO_ROOT_USER, default is "minioadmin", and
+        MINIO_ROOT_PASSWORD (length >= 8), default is "minioadmin"; in the format of
+        an EnvironmentFile=, as described by systemd.exec(5).
+      '';
+      example = "/etc/nixos/minio-root-credentials";
+    };
+
     region = mkOption {
       default = "us-east-1";
       type = types.str;
@@ -72,29 +88,29 @@ in
   };
 
   config = mkIf cfg.enable {
+    warnings = optional ((cfg.accessKey != "") || (cfg.secretKey != "")) "services.minio.`accessKey` and services.minio.`secretKey` are deprecated, please use services.minio.`rootCredentialsFile` instead.";
+
     systemd.tmpfiles.rules = [
       "d '${cfg.configDir}' - minio minio - -"
-      "d '${cfg.dataDir}' - minio minio - -"
-    ];
+    ] ++ (map (x:  "d '" + x + "' - minio minio - - ") cfg.dataDir);
 
     systemd.services.minio = {
       description = "Minio Object Storage";
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --config-dir=${cfg.configDir} ${cfg.dataDir}";
+        ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}";
         Type = "simple";
         User = "minio";
         Group = "minio";
         LimitNOFILE = 65536;
+        EnvironmentFile = if (cfg.rootCredentialsFile != null) then cfg.rootCredentialsFile
+                          else if ((cfg.accessKey != "") || (cfg.secretKey != "")) then (legacyCredentials cfg)
+                          else null;
       };
       environment = {
         MINIO_REGION = "${cfg.region}";
         MINIO_BROWSER = "${if cfg.browser then "on" else "off"}";
-      } // optionalAttrs (cfg.accessKey != "") {
-        MINIO_ACCESS_KEY = "${cfg.accessKey}";
-      } // optionalAttrs (cfg.secretKey != "") {
-        MINIO_SECRET_KEY = "${cfg.secretKey}";
       };
     };
 
diff --git a/nixos/modules/services/web-servers/molly-brown.nix b/nixos/modules/services/web-servers/molly-brown.nix
index e9052a184b2..58db9b9beda 100644
--- a/nixos/modules/services/web-servers/molly-brown.nix
+++ b/nixos/modules/services/web-servers/molly-brown.nix
@@ -4,23 +4,8 @@ with lib;
 
 let
   cfg = config.services.molly-brown;
-
-  settingsType = with types;
-    attrsOf (oneOf [
-      int
-      str
-      (listOf str)
-      (attrsOf (oneOf [ int str (listOf str) (attrsOf str) ]))
-    ]) // {
-      description = "primitive expression convertable to TOML";
-    };
-
-  configFile = pkgs.runCommand "molly-brown.toml" {
-    buildInputs = [ pkgs.remarshal ];
-    preferLocalBuild = true;
-    passAsFile = [ "settings" ];
-    settings = builtins.toJSON cfg.settings;
-  } "remarshal -if json -of toml < $settingsPath > $out";
+  settingsFormat = pkgs.formats.toml { };
+ configFile = settingsFormat.generate "molly-brown.toml" cfg.settings;
 in {
 
   options.services.molly-brown = {
@@ -56,7 +41,6 @@ in {
 
         As an example:
         <programlisting>
-        security.acme.certs."example.com".allowKeysForGroup = true;
         systemd.services.molly-brown.serviceConfig.SupplementaryGroups =
           [ config.security.acme.certs."example.com".group ];
         </programlisting>
@@ -76,7 +60,7 @@ in {
     };
 
     settings = mkOption {
-      type = settingsType;
+      inherit (settingsFormat) type;
       default = { };
       description = ''
         molly-brown configuration. Refer to
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 461888c4cc4..ebb3c38d6c2 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -6,27 +6,54 @@ let
   cfg = config.services.nginx;
   certs = config.security.acme.certs;
   vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
-  acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME && vhostConfig.useACMEHost == null) vhostsConfigs;
+  acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs;
+  dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
   virtualHosts = mapAttrs (vhostName: vhostConfig:
     let
       serverName = if vhostConfig.serverName != null
         then vhostConfig.serverName
         else vhostName;
+      certName = if vhostConfig.useACMEHost != null
+        then vhostConfig.useACMEHost
+        else serverName;
     in
     vhostConfig // {
-      inherit serverName;
-    } // (optionalAttrs vhostConfig.enableACME {
-      sslCertificate = "${certs.${serverName}.directory}/fullchain.pem";
-      sslCertificateKey = "${certs.${serverName}.directory}/key.pem";
-      sslTrustedCertificate = "${certs.${serverName}.directory}/full.pem";
-    }) // (optionalAttrs (vhostConfig.useACMEHost != null) {
-      sslCertificate = "${certs.${vhostConfig.useACMEHost}.directory}/fullchain.pem";
-      sslCertificateKey = "${certs.${vhostConfig.useACMEHost}.directory}/key.pem";
-      sslTrustedCertificate = "${certs.${vhostConfig.useACMEHost}.directory}/fullchain.pem";
+      inherit serverName certName;
+    } // (optionalAttrs (vhostConfig.enableACME || vhostConfig.useACMEHost != null) {
+      sslCertificate = "${certs.${certName}.directory}/fullchain.pem";
+      sslCertificateKey = "${certs.${certName}.directory}/key.pem";
+      sslTrustedCertificate = "${certs.${certName}.directory}/chain.pem";
     })
   ) cfg.virtualHosts;
   enableIPv6 = config.networking.enableIPv6;
 
+  defaultFastcgiParams = {
+    SCRIPT_FILENAME   = "$document_root$fastcgi_script_name";
+    QUERY_STRING      = "$query_string";
+    REQUEST_METHOD    = "$request_method";
+    CONTENT_TYPE      = "$content_type";
+    CONTENT_LENGTH    = "$content_length";
+
+    SCRIPT_NAME       = "$fastcgi_script_name";
+    REQUEST_URI       = "$request_uri";
+    DOCUMENT_URI      = "$document_uri";
+    DOCUMENT_ROOT     = "$document_root";
+    SERVER_PROTOCOL   = "$server_protocol";
+    REQUEST_SCHEME    = "$scheme";
+    HTTPS             = "$https if_not_empty";
+
+    GATEWAY_INTERFACE = "CGI/1.1";
+    SERVER_SOFTWARE   = "nginx/$nginx_version";
+
+    REMOTE_ADDR       = "$remote_addr";
+    REMOTE_PORT       = "$remote_port";
+    SERVER_ADDR       = "$server_addr";
+    SERVER_PORT       = "$server_port";
+    SERVER_NAME       = "$server_name";
+
+    REDIRECT_STATUS   = "200";
+  };
+
   recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" ''
     proxy_set_header        Host $host;
     proxy_set_header        X-Real-IP $remote_addr;
@@ -34,7 +61,6 @@ let
     proxy_set_header        X-Forwarded-Proto $scheme;
     proxy_set_header        X-Forwarded-Host $host;
     proxy_set_header        X-Forwarded-Server $host;
-    proxy_set_header        Accept-Encoding "";
   '';
 
   upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
@@ -53,6 +79,8 @@ let
       include ${pkgs.mailcap}/etc/nginx/mime.types;
       include ${cfg.package}/conf/fastcgi.conf;
       include ${cfg.package}/conf/uwsgi_params;
+
+      default_type application/octet-stream;
   '';
 
   configFile = pkgs.writers.writeNginxConfig "nginx.conf" ''
@@ -87,7 +115,7 @@ let
       ''}
 
       ssl_protocols ${cfg.sslProtocols};
-      ssl_ciphers ${cfg.sslCiphers};
+      ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
       ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
 
       ${optionalString (cfg.recommendedTlsSettings) ''
@@ -126,10 +154,10 @@ let
 
       ${optionalString (cfg.recommendedProxySettings) ''
         proxy_redirect          off;
-        proxy_connect_timeout   90;
-        proxy_send_timeout      90;
-        proxy_read_timeout      90;
-        proxy_http_version      1.0;
+        proxy_connect_timeout   ${cfg.proxyTimeout};
+        proxy_send_timeout      ${cfg.proxyTimeout};
+        proxy_read_timeout      ${cfg.proxyTimeout};
+        proxy_http_version      1.1;
         include ${recommendedProxyConfig};
       ''}
 
@@ -180,6 +208,12 @@ let
       ${cfg.httpConfig}
     }''}
 
+    ${optionalString (cfg.streamConfig != "") ''
+    stream {
+      ${cfg.streamConfig}
+    }
+    ''}
+
     ${cfg.appendConfig}
   '';
 
@@ -196,13 +230,13 @@ let
 
         defaultListen =
           if vhost.listen != [] then vhost.listen
-          else ((optionals hasSSL (
-            singleton                    { addr = "0.0.0.0"; port = 443; ssl = true; }
-            ++ optional enableIPv6 { addr = "[::]";    port = 443; ssl = true; }
-          )) ++ optionals (!onlySSL) (
-            singleton                    { addr = "0.0.0.0"; port = 80;  ssl = false; }
-            ++ optional enableIPv6 { addr = "[::]";    port = 80;  ssl = false; }
-          ));
+          else optionals (hasSSL || vhost.rejectSSL) (
+            singleton { addr = "0.0.0.0"; port = 443; ssl = true; }
+            ++ optional enableIPv6 { addr = "[::]"; port = 443; ssl = true; }
+          ) ++ optionals (!onlySSL) (
+            singleton { addr = "0.0.0.0"; port = 80; ssl = false; }
+            ++ optional enableIPv6 { addr = "[::]"; port = 80; ssl = false; }
+          );
 
         hostListen =
           if vhost.forceSSL
@@ -215,7 +249,15 @@ let
           + optionalString (ssl && vhost.http2) "http2 "
           + optionalString vhost.default "default_server "
           + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
-          + ";";
+          + ";"
+          + (if ssl && vhost.http3 then ''
+          # UDP listener for **QUIC+HTTP/3
+          listen ${addr}:${toString port} http3 reuseport;
+          # Advertise that HTTP/3 is available
+          add_header Alt-Svc 'h3=":443"';
+          # Sent when QUIC was used
+          add_header QUIC-Status $quic;
+          '' else "");
 
         redirectListen = filter (x: !x.ssl) defaultListen;
 
@@ -261,12 +303,12 @@ let
           ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) ''
             ssl_trusted_certificate ${vhost.sslTrustedCertificate};
           ''}
-
-          ${optionalString (vhost.basicAuthFile != null || vhost.basicAuth != {}) ''
-            auth_basic secured;
-            auth_basic_user_file ${if vhost.basicAuthFile != null then vhost.basicAuthFile else mkHtpasswd vhostName vhost.basicAuth};
+          ${optionalString vhost.rejectSSL ''
+            ssl_reject_handshake on;
           ''}
 
+          ${mkBasicAuth vhostName vhost}
+
           ${mkLocations vhost.locations}
 
           ${vhost.extraConfig}
@@ -287,6 +329,10 @@ let
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
       ''}
+      ${concatStringsSep "\n"
+        (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'')
+          (optionalAttrs (config.fastcgiParams != {})
+            (defaultFastcgiParams // config.fastcgiParams)))}
       ${optionalString (config.index != null) "index ${config.index};"}
       ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
       ${optionalString (config.root != null) "root ${config.root};"}
@@ -294,9 +340,19 @@ let
       ${optionalString (config.return != null) "return ${config.return};"}
       ${config.extraConfig}
       ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"}
+      ${mkBasicAuth "sublocation" config}
     }
   '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
-  mkHtpasswd = vhostName: authDef: pkgs.writeText "${vhostName}.htpasswd" (
+
+  mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let
+    auth_file = if zone.basicAuthFile != null
+      then zone.basicAuthFile
+      else mkHtpasswd name zone.basicAuth;
+  in ''
+    auth_basic secured;
+    auth_basic_user_file ${auth_file};
+  '');
+  mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" (
     concatStringsSep "\n" (mapAttrsToList (user: password: ''
       ${user}:{PLAIN}${password}
     '') authDef)
@@ -348,10 +404,22 @@ in
         ";
       };
 
+      proxyTimeout = mkOption {
+        type = types.str;
+        default = "60s";
+        example = "20s";
+        description = "
+          Change the proxy related timeouts in recommendedProxySettings.
+        ";
+      };
+
       package = mkOption {
         default = pkgs.nginxStable;
         defaultText = "pkgs.nginxStable";
         type = types.package;
+        apply = p: p.override {
+          modules = p.modules ++ cfg.additionalModules;
+        };
         description = "
           Nginx package to use. This defaults to the stable version. Note
           that the nginx team recommends to use the mainline version which
@@ -359,8 +427,20 @@ in
         ";
       };
 
+      additionalModules = mkOption {
+        default = [];
+        type = types.listOf (types.attrsOf types.anything);
+        example = literalExample "[ pkgs.nginxModules.brotli ]";
+        description = ''
+          Additional <link xlink:href="https://www.nginx.com/resources/wiki/modules/">third-party nginx modules</link>
+          to install. Packaged modules are available in
+          <literal>pkgs.nginxModules</literal>.
+        '';
+      };
+
       logError = mkOption {
         default = "stderr";
+        type = types.str;
         description = "
           Configures logging.
           The first parameter defines a file that will store the log. The
@@ -384,13 +464,24 @@ in
       };
 
       config = mkOption {
+        type = types.str;
         default = "";
-        description = "
-          Verbatim nginx.conf configuration.
-          This is mutually exclusive with the structured configuration
-          via virtualHosts and the recommendedXyzSettings configuration
-          options. See appendConfig for appending to the generated http block.
-        ";
+        description = ''
+          Verbatim <filename>nginx.conf</filename> configuration.
+          This is mutually exclusive to any other config option for
+          <filename>nginx.conf</filename> except for
+          <itemizedlist>
+          <listitem><para><xref linkend="opt-services.nginx.appendConfig" />
+          </para></listitem>
+          <listitem><para><xref linkend="opt-services.nginx.httpConfig" />
+          </para></listitem>
+          <listitem><para><xref linkend="opt-services.nginx.logError" />
+          </para></listitem>
+          </itemizedlist>
+
+          If additional verbatim config in addition to other options is needed,
+          <xref linkend="opt-services.nginx.appendConfig" /> should be used instead.
+        '';
       };
 
       appendConfig = mkOption {
@@ -435,6 +526,21 @@ in
         ";
       };
 
+      streamConfig = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          server {
+            listen 127.0.0.1:53 udp reuseport;
+            proxy_timeout 20s;
+            proxy_pass 192.168.0.1:53535;
+          }
+        '';
+        description = "
+          Configuration lines to be set inside the stream block.
+        ";
+      };
+
       eventsConfig = mkOption {
         type = types.lines;
         default = "";
@@ -463,14 +569,6 @@ in
         '';
       };
 
-      enableSandbox = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Starting Nginx web server with additional sandbox/hardening options.
-        '';
-      };
-
       user = mkOption {
         type = types.str;
         default = "nginx";
@@ -496,7 +594,7 @@ in
       };
 
       sslCiphers = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         # 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.";
@@ -598,6 +696,7 @@ in
                 Defines the address and other parameters of the upstream servers.
               '';
               default = {};
+              example = { "127.0.0.1:8000" = {}; };
             };
             extraConfig = mkOption {
               type = types.lines;
@@ -612,6 +711,14 @@ in
           Defines a group of servers to use as proxy target.
         '';
         default = {};
+        example = literalExample ''
+          "backend_server" = {
+            servers = { "127.0.0.1:8000" = {}; };
+            extraConfig = ''''
+              keepalive 16;
+            '''';
+          };
+        '';
       };
 
       virtualHosts = mkOption {
@@ -667,20 +774,27 @@ in
       }
 
       {
-        assertion = all (conf: with conf;
-          !(addSSL && (onlySSL || enableSSL)) &&
-          !(forceSSL && (onlySSL || enableSSL)) &&
-          !(addSSL && forceSSL)
+        assertion = all (host: with host;
+          count id [ addSSL (onlySSL || enableSSL) forceSSL rejectSSL ] <= 1
         ) (attrValues virtualHosts);
         message = ''
           Options services.nginx.service.virtualHosts.<name>.addSSL,
-          services.nginx.virtualHosts.<name>.onlySSL and services.nginx.virtualHosts.<name>.forceSSL
-          are mutually exclusive.
+          services.nginx.virtualHosts.<name>.onlySSL,
+          services.nginx.virtualHosts.<name>.forceSSL and
+          services.nginx.virtualHosts.<name>.rejectSSL are mutually exclusive.
+        '';
+      }
+
+      {
+        assertion = any (host: host.rejectSSL) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.19.4";
+        message = ''
+          services.nginx.virtualHosts.<name>.rejectSSL requires nginx version
+          1.19.4 or above; see the documentation for services.nginx.package.
         '';
       }
 
       {
-        assertion = all (conf: !(conf.enableACME && conf.useACMEHost != null)) (attrValues virtualHosts);
+        assertion = all (host: !(host.enableACME && host.useACMEHost != null)) (attrValues virtualHosts);
         message = ''
           Options services.nginx.service.virtualHosts.<name>.enableACME and
           services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
@@ -691,17 +805,19 @@ in
     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;
+      wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
+      after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
       # Nginx needs to be started in order to be able to request certificates
       # (it's hosting the acme challenge after all)
       # This fixes https://github.com/NixOS/nixpkgs/issues/81842
-      before = map (vhostConfig: "acme-${vhostConfig.serverName}.service") acmeEnabledVhosts;
+      before = map (certName: "acme-${certName}.service") dependentCertNames;
       stopIfChanged = false;
       preStart = ''
         ${cfg.preStart}
         ${execCommand} -t
       '';
+
+      startLimitIntervalSec = 60;
       serviceConfig = {
         ExecStart = execCommand;
         ExecReload = [
@@ -710,7 +826,6 @@ in
         ];
         Restart = "always";
         RestartSec = "10s";
-        StartLimitInterval = "1min";
         # User and group
         User = cfg.user;
         Group = cfg.group;
@@ -723,29 +838,38 @@ in
         # Logs directory and mode
         LogsDirectory = "nginx";
         LogsDirectoryMode = "0750";
+        # Proc filesystem
+        ProcSubset = "pid";
+        ProtectProc = "invisible";
+        # New file permissions
+        UMask = "0027"; # 0640 / 0750
         # Capabilities
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
         # Security
         NoNewPrivileges = true;
-      } // optionalAttrs cfg.enableSandbox {
-        # Sandboxing
+        # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
         ProtectSystem = "strict";
         ProtectHome = mkDefault true;
         PrivateTmp = true;
         PrivateDevices = true;
         ProtectHostname = true;
+        ProtectClock = true;
         ProtectKernelTunables = true;
         ProtectKernelModules = true;
+        ProtectKernelLogs = true;
         ProtectControlGroups = true;
         RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
         LockPersonality = true;
-        MemoryDenyWriteExecute = !(builtins.any (mod: (mod.allowMemoryWriteExecute or false)) pkgs.nginx.modules);
+        MemoryDenyWriteExecute = !(builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules);
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
+        RemoveIPC = true;
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
+        SystemCallFilter = "~@cpu-emulation @debug @keyring @ipc @mount @obsolete @privileged @setuid";
       };
     };
 
@@ -753,41 +877,47 @@ in
       source = configFile;
     };
 
-    systemd.services.nginx-config-reload = mkIf cfg.enableReload {
-      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 /run/current-system/systemd/bin/systemctl -q is-active nginx.service ; then
-          /run/current-system/systemd/bin/systemctl reload nginx.service
-        fi
-      '';
-      serviceConfig.RemainAfterExit = true;
+    # This service waits for all certificates to be available
+    # before reloading nginx configuration.
+    # sslTargets are added to wantedBy + before
+    # which allows the acme-finished-$cert.target to signify the successful updating
+    # of certs end-to-end.
+    systemd.services.nginx-config-reload = let
+      sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
+      sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
+    in mkIf (cfg.enableReload || sslServices != []) {
+      wants = optionals (cfg.enableReload) [ "nginx.service" ];
+      wantedBy = sslServices ++ [ "multi-user.target" ];
+      # Before the finished targets, after the renew services.
+      # This service might be needed for HTTP-01 challenges, but we only want to confirm
+      # certs are updated _after_ config has been reloaded.
+      before = sslTargets;
+      after = sslServices;
+      restartTriggers = optionals (cfg.enableReload) [ configFile ];
+      # Block reloading if not all certs exist yet.
+      # Happens when config changes add new vhosts/certs.
+      unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
+      serviceConfig = {
+        Type = "oneshot";
+        TimeoutSec = 60;
+        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service";
+        ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service";
+      };
     };
 
-    security.acme.certs = filterAttrs (n: v: v != {}) (
-      let
-        acmePairs = map (vhostConfig: { name = vhostConfig.serverName; value = {
-            user = cfg.user;
-            group = lib.mkDefault cfg.group;
-            webroot = vhostConfig.acmeRoot;
-            extraDomains = genAttrs vhostConfig.serverAliases (alias: null);
-            postRun = ''
-              /run/current-system/systemd/bin/systemctl reload nginx
-            '';
-          }; }) acmeEnabledVhosts;
-      in
-        listToAttrs acmePairs
-    );
+    security.acme.certs = let
+      acmePairs = map (vhostConfig: nameValuePair vhostConfig.serverName {
+        group = mkDefault cfg.group;
+        webroot = vhostConfig.acmeRoot;
+        extraDomainNames = vhostConfig.serverAliases;
+      # Filter for enableACME-only vhosts. Don't want to create dud certs
+      }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
+    in listToAttrs acmePairs;
 
     users.users = optionalAttrs (cfg.user == "nginx") {
       nginx = {
         group = cfg.group;
+        isSystemUser = true;
         uid = config.ids.uids.nginx;
       };
     };
diff --git a/nixos/modules/services/web-servers/nginx/gitweb.nix b/nixos/modules/services/web-servers/nginx/gitweb.nix
index f7fb07bb797..11bf2a309ea 100644
--- a/nixos/modules/services/web-servers/nginx/gitweb.nix
+++ b/nixos/modules/services/web-servers/nginx/gitweb.nix
@@ -89,6 +89,6 @@ in
 
   };
 
-  meta.maintainers = with maintainers; [ gnidorah ];
+  meta.maintainers = with maintainers; [ ];
 
 }
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
index 3d9e391ecf2..d8c976f202f 100644
--- a/nixos/modules/services/web-servers/nginx/location-options.nix
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -9,6 +9,34 @@ with lib;
 
 {
   options = {
+    basicAuth = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      example = literalExample ''
+        {
+          user = "password";
+        };
+      '';
+      description = ''
+        Basic Auth protection for a vhost.
+
+        WARNING: This is implemented to store the password in plain text in the
+        Nix store.
+      '';
+    };
+
+    basicAuthFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Basic Auth password file for a vhost.
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>.
+
+        WARNING: The generate file contains the users' passwords in a
+        non-cryptographically-securely hashed way.
+      '';
+    };
+
     proxyPass = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -24,7 +52,7 @@ with lib;
       default = false;
       example = true;
       description = ''
-        Whether to supporty proxying websocket connections with HTTP/1.1.
+        Whether to support proxying websocket connections with HTTP/1.1.
       '';
     };
 
@@ -73,6 +101,16 @@ with lib;
       '';
     };
 
+    fastcgiParams = mkOption {
+      type = types.attrsOf types.str;
+      default = {};
+      description = ''
+        FastCGI parameters to override.  Unlike in the Nginx
+        configuration file, overriding only some default parameters
+        won't unset the default values for other parameters.
+      '';
+    };
+
     extraConfig = mkOption {
       type = types.lines;
       default = "";
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 455854e2a96..bc18bcaa7b3 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -118,6 +118,18 @@ with lib;
       '';
     };
 
+    rejectSSL = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to listen for and reject all HTTPS connections to this vhost. Useful in
+        <link linkend="opt-services.nginx.virtualHosts._name_.default">default</link>
+        server blocks to avoid serving the certificate for another vhost. Uses the
+        <literal>ssl_reject_handshake</literal> directive available in nginx versions
+        1.19.4 and above.
+      '';
+    };
+
     sslCertificate = mkOption {
       type = types.path;
       example = "/var/host.cert";
@@ -151,6 +163,19 @@ with lib;
       '';
     };
 
+    http3 = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable HTTP 3.
+        This requires using <literal>pkgs.nginxQuic</literal> package
+        which can be achieved by setting <literal>services.nginx.package = pkgs.nginxQuic;</literal>.
+        Note that HTTP 3 support is experimental and
+        *not* yet recommended for production.
+        Read more at https://quic.nginx.org/
+      '';
+    };
+
     root = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -198,7 +223,7 @@ with lib;
         Basic Auth protection for a vhost.
 
         WARNING: This is implemented to store the password in plain text in the
-        nix store.
+        Nix store.
       '';
     };
 
@@ -207,7 +232,10 @@ 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>
+        Can be created via: <command>htpasswd -c &lt;filename&gt; &lt;username&gt;</command>.
+
+        WARNING: The generate file contains the users' passwords in a
+        non-cryptographically-securely hashed way.
       '';
     };
 
diff --git a/nixos/modules/services/web-servers/phpfpm/default.nix b/nixos/modules/services/web-servers/phpfpm/default.nix
index d090885a8ca..4d302299f5f 100644
--- a/nixos/modules/services/web-servers/phpfpm/default.nix
+++ b/nixos/modules/services/web-servers/phpfpm/default.nix
@@ -26,12 +26,9 @@ let
   phpIni = poolOpts: pkgs.runCommand "php.ini" {
     inherit (poolOpts) phpPackage phpOptions;
     preferLocalBuild = true;
-    nixDefaults = ''
-      sendmail_path = "/run/wrappers/bin/sendmail -t -i"
-    '';
-    passAsFile = [ "nixDefaults" "phpOptions" ];
+    passAsFile = [ "phpOptions" ];
   } ''
-    cat ${poolOpts.phpPackage}/etc/php.ini $nixDefaultsPath $phpOptionsPath > $out
+    cat ${poolOpts.phpPackage}/etc/php.ini $phpOptionsPath > $out
   '';
 
   poolOpts = { name, ... }:
@@ -277,6 +274,7 @@ in {
           ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
           RuntimeDirectory = "phpfpm";
           RuntimeDirectoryPreserve = true; # Relevant when multiple processes are running
+          Restart = "always";
         };
       }
     ) cfg.pools;
diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix
new file mode 100644
index 00000000000..2bc7d01c7c2
--- /dev/null
+++ b/nixos/modules/services/web-servers/pomerium.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  format = pkgs.formats.yaml {};
+in
+{
+  options.services.pomerium = {
+    enable = mkEnableOption "the Pomerium authenticating reverse proxy";
+
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = "Path to Pomerium config YAML. If set, overrides services.pomerium.settings.";
+    };
+
+    useACMEHost = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      description = ''
+        If set, use a NixOS-generated ACME certificate with the specified name.
+
+        Note that this will require you to use a non-HTTP-based challenge, or
+        disable Pomerium's in-built HTTP redirect server by setting
+        http_redirect_addr to null and use a different HTTP server for serving
+        the challenge response.
+
+        If you're using an HTTP-based challenge, you should use the
+        Pomerium-native autocert option instead.
+      '';
+    };
+
+    settings = mkOption {
+      description = ''
+        The contents of Pomerium's config.yaml, in Nix expressions.
+
+        Specifying configFile will override this in its entirety.
+
+        See <link xlink:href="https://pomerium.io/reference/">the Pomerium
+        configuration reference</link> for more information about what to put
+        here.
+      '';
+      default = {};
+      type = format.type;
+    };
+
+    secretsFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = ''
+        Path to file containing secrets for Pomerium, in systemd
+        EnvironmentFile format. See the systemd.exec(5) man page.
+      '';
+    };
+  };
+
+  config = let
+    cfg = config.services.pomerium;
+    cfgFile = if cfg.configFile != null then cfg.configFile else (format.generate "pomerium.yaml" cfg.settings);
+  in mkIf cfg.enable ({
+    systemd.services.pomerium = {
+      description = "Pomerium authenticating reverse proxy";
+      wants = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target");
+      after = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target");
+      wantedBy = [ "multi-user.target" ];
+      environment = optionalAttrs (cfg.useACMEHost != null) {
+        CERTIFICATE_FILE = "fullchain.pem";
+        CERTIFICATE_KEY_FILE = "key.pem";
+      };
+      startLimitIntervalSec = 60;
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = [ "pomerium" ];
+        ExecStart = "${pkgs.pomerium}/bin/pomerium -config ${cfgFile}";
+
+        PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
+        MemoryDenyWriteExecute = false;  # breaks LuaJIT
+
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        DevicePolicy = "closed";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectControlGroups = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectKernelLogs = true;
+        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+
+        EnvironmentFile = cfg.secretsFile;
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+
+        WorkingDirectory = mkIf (cfg.useACMEHost != null) "$CREDENTIALS_DIRECTORY";
+        LoadCredential = optionals (cfg.useACMEHost != null) [
+          "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem"
+          "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem"
+        ];
+      };
+    };
+
+    # postRun hooks on cert renew can't be used to restart Nginx since renewal
+    # runs as the unprivileged acme user. sslTargets are added to wantedBy + before
+    # which allows the acme-finished-$cert.target to signify the successful updating
+    # of certs end-to-end.
+    systemd.services.pomerium-config-reload = mkIf (cfg.useACMEHost != null) {
+      # TODO(lukegb): figure out how to make config reloading work with credentials.
+
+      wantedBy = [ "acme-finished-${cfg.useACMEHost}.target" "multi-user.target" ];
+      # Before the finished targets, after the renew services.
+      before = [ "acme-finished-${cfg.useACMEHost}.target" ];
+      after = [ "acme-${cfg.useACMEHost}.service" ];
+      # Block reloading if not all certs exist yet.
+      unitConfig.ConditionPathExists = [ "${config.security.acme.certs.${cfg.useACMEHost}.directory}/fullchain.pem" ];
+      serviceConfig = {
+        Type = "oneshot";
+        TimeoutSec = 60;
+        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active pomerium.service";
+        ExecStart = "/run/current-system/systemd/bin/systemctl restart pomerium.service";
+      };
+    };
+  });
+}
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index 6d12925829f..13fe98402c6 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -74,6 +74,7 @@ in
 
       extraGroups = mkOption {
         default = [];
+        type = types.listOf types.str;
         example = [ "users" ];
         description = "Defines extra groups to which the tomcat user belongs.";
       };
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
index 4ab7307c3b6..3d29199dd45 100644
--- a/nixos/modules/services/web-servers/traefik.nix
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -136,6 +136,8 @@ in {
       description = "Traefik web server";
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
+      startLimitIntervalSec = 86400;
+      startLimitBurst = 5;
       serviceConfig = {
         ExecStart =
           "${cfg.package}/bin/traefik --configfile=${staticConfigFile}";
@@ -143,8 +145,6 @@ in {
         User = "traefik";
         Group = cfg.group;
         Restart = "on-failure";
-        StartLimitInterval = 86400;
-        StartLimitBurst = 5;
         AmbientCapabilities = "cap_net_bind_service";
         CapabilityBoundingSet = "cap_net_bind_service";
         NoNewPrivileges = true;
diff --git a/nixos/modules/services/web-servers/trafficserver.nix b/nixos/modules/services/web-servers/trafficserver.nix
new file mode 100644
index 00000000000..db0e2ac0bd0
--- /dev/null
+++ b/nixos/modules/services/web-servers/trafficserver.nix
@@ -0,0 +1,318 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.trafficserver;
+  user = config.users.users.trafficserver.name;
+  group = config.users.groups.trafficserver.name;
+
+  getManualUrl = name: "https://docs.trafficserver.apache.org/en/latest/admin-guide/files/${name}.en.html";
+  getConfPath = name: "${pkgs.trafficserver}/etc/trafficserver/${name}";
+
+  yaml = pkgs.formats.yaml { };
+
+  fromYAML = f:
+    let
+      jsonFile = pkgs.runCommand "in.json"
+        {
+          nativeBuildInputs = [ pkgs.remarshal ];
+        } ''
+        yaml2json < "${f}" > "$out"
+      '';
+    in
+    builtins.fromJSON (builtins.readFile jsonFile);
+
+  mkYamlConf = name: cfg:
+    if cfg != null then {
+      "trafficserver/${name}.yaml".source = yaml.generate "${name}.yaml" cfg;
+    } else {
+      "trafficserver/${name}.yaml".text = "";
+    };
+
+  mkRecordLines = path: value:
+    if isAttrs value then
+      lib.mapAttrsToList (n: v: mkRecordLines (path ++ [ n ]) v) value
+    else if isInt value then
+      "CONFIG ${concatStringsSep "." path} INT ${toString value}"
+    else if isFloat value then
+      "CONFIG ${concatStringsSep "." path} FLOAT ${toString value}"
+    else
+      "CONFIG ${concatStringsSep "." path} STRING ${toString value}";
+
+  mkRecordsConfig = cfg: concatStringsSep "\n" (flatten (mkRecordLines [ ] cfg));
+  mkPluginConfig = cfg: concatStringsSep "\n" (map (p: "${p.path} ${p.arg}") cfg);
+in
+{
+  options.services.trafficserver = {
+    enable = mkEnableOption "Apache Traffic Server";
+
+    cache = mkOption {
+      type = types.lines;
+      default = "";
+      example = "dest_domain=example.com suffix=js action=never-cache";
+      description = ''
+        Caching rules that overrule the origin's caching policy.
+
+        Consult the <link xlink:href="${getManualUrl "cache.config"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    hosting = mkOption {
+      type = types.lines;
+      default = "";
+      example = "domain=example.com volume=1";
+      description = ''
+        Partition the cache according to origin server or domain
+
+        Consult the <link xlink:href="${getManualUrl "hosting.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    ipAllow = mkOption {
+      type = types.nullOr yaml.type;
+      default = fromYAML (getConfPath "ip_allow.yaml");
+      defaultText = "upstream defaults";
+      example = literalExample {
+        ip_allow = [{
+          apply = "in";
+          ip_addrs = "127.0.0.1";
+          action = "allow";
+          methods = "ALL";
+        }];
+      };
+      description = ''
+        Control client access to Traffic Server and Traffic Server connections
+        to upstream servers.
+
+        Consult the <link xlink:href="${getManualUrl "ip_allow.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    logging = mkOption {
+      type = types.nullOr yaml.type;
+      default = fromYAML (getConfPath "logging.yaml");
+      defaultText = "upstream defaults";
+      example = literalExample { };
+      description = ''
+        Configure logs.
+
+        Consult the <link xlink:href="${getManualUrl "logging.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    parent = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        dest_domain=. method=get parent="p1.example:8080; p2.example:8080" round_robin=true
+      '';
+      description = ''
+        Identify the parent proxies used in an cache hierarchy.
+
+        Consult the <link xlink:href="${getManualUrl "parent.config"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    plugins = mkOption {
+      default = [ ];
+
+      description = ''
+        Controls run-time loadable plugins available to Traffic Server, as
+        well as their configuration.
+
+        Consult the <link xlink:href="${getManualUrl "plugin.config"}">upstream
+        documentation</link> for more details.
+      '';
+
+      type = with types;
+        listOf (submodule {
+          options.path = mkOption {
+            type = str;
+            example = "xdebug.so";
+            description = ''
+              Path to plugin. The path can either be absolute, or relative to
+              the plugin directory.
+            '';
+          };
+          options.arg = mkOption {
+            type = str;
+            default = "";
+            example = "--header=ATS-My-Debug";
+            description = "arguments to pass to the plugin";
+          };
+        });
+    };
+
+    records = mkOption {
+      type = with types;
+        let valueType = (attrsOf (oneOf [ int float str valueType ])) // {
+          description = "Traffic Server records value";
+        };
+        in
+        valueType;
+      default = { };
+      example = literalExample { proxy.config.proxy_name = "my_server"; };
+      description = ''
+        List of configurable variables used by Traffic Server.
+
+        Consult the <link xlink:href="${getManualUrl "records.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    remap = mkOption {
+      type = types.lines;
+      default = "";
+      example = "map http://from.example http://origin.example";
+      description = ''
+        URL remapping rules used by Traffic Server.
+
+        Consult the <link xlink:href="${getManualUrl "remap.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    splitDns = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        dest_domain=internal.corp.example named="255.255.255.255:212 255.255.255.254" def_domain=corp.example search_list="corp.example corp1.example"
+        dest_domain=!internal.corp.example named=255.255.255.253
+      '';
+      description = ''
+        Specify the DNS server that Traffic Server should use under specific
+        conditions.
+
+        Consult the <link xlink:href="${getManualUrl "splitdns.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    sslMulticert = mkOption {
+      type = types.lines;
+      default = "";
+      example = "dest_ip=* ssl_cert_name=default.pem";
+      description = ''
+        Configure SSL server certificates to terminate the SSL sessions.
+
+        Consult the <link xlink:href="${getManualUrl "ssl_multicert.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    sni = mkOption {
+      type = types.nullOr yaml.type;
+      default = null;
+      example = literalExample {
+        sni = [{
+          fqdn = "no-http2.example.com";
+          https = "off";
+        }];
+      };
+      description = ''
+        Configure aspects of TLS connection handling for both inbound and
+        outbound connections.
+
+        Consult the <link xlink:href="${getManualUrl "sni.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    storage = mkOption {
+      type = types.lines;
+      default = "/var/cache/trafficserver 256M";
+      example = "/dev/disk/by-id/XXXXX volume=1";
+      description = ''
+        List all the storage that make up the Traffic Server cache.
+
+        Consult the <link xlink:href="${getManualUrl "storage.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    strategies = mkOption {
+      type = types.nullOr yaml.type;
+      default = null;
+      description = ''
+        Specify the next hop proxies used in an cache hierarchy and the
+        algorithms used to select the next proxy.
+
+        Consult the <link xlink:href="${getManualUrl "strategies.yaml"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    volume = mkOption {
+      type = types.nullOr yaml.type;
+      default = "";
+      example = "volume=1 scheme=http size=20%";
+      description = ''
+        Manage cache space more efficiently and restrict disk usage by
+        creating cache volumes of different sizes.
+
+        Consult the <link xlink:href="${getManualUrl "volume.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc = {
+      "trafficserver/cache.config".text = cfg.cache;
+      "trafficserver/hosting.config".text = cfg.hosting;
+      "trafficserver/parent.config".text = cfg.parent;
+      "trafficserver/plugin.config".text = mkPluginConfig cfg.plugins;
+      "trafficserver/records.config".text = mkRecordsConfig cfg.records;
+      "trafficserver/remap.config".text = cfg.remap;
+      "trafficserver/splitdns.config".text = cfg.splitDns;
+      "trafficserver/ssl_multicert.config".text = cfg.sslMulticert;
+      "trafficserver/storage.config".text = cfg.storage;
+      "trafficserver/volume.config".text = cfg.volume;
+    } // (mkYamlConf "ip_allow" cfg.ipAllow)
+    // (mkYamlConf "logging" cfg.logging)
+    // (mkYamlConf "sni" cfg.sni)
+    // (mkYamlConf "strategies" cfg.strategies);
+
+    environment.systemPackages = [ pkgs.trafficserver ];
+    systemd.packages = [ pkgs.trafficserver ];
+
+    # Traffic Server does privilege handling independently of systemd, and
+    # therefore should be started as root
+    systemd.services.trafficserver = {
+      enable = true;
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    # These directories can't be created by systemd because:
+    #
+    #   1. Traffic Servers starts as root and switches to an unprivileged user
+    #      afterwards. The runtime directories defined below are assumed to be
+    #      owned by that user.
+    #   2. The bin/trafficserver script assumes these directories exist.
+    systemd.tmpfiles.rules = [
+      "d '/run/trafficserver' - ${user} ${group} - -"
+      "d '/var/cache/trafficserver' - ${user} ${group} - -"
+      "d '/var/lib/trafficserver' - ${user} ${group} - -"
+      "d '/var/log/trafficserver' - ${user} ${group} - -"
+    ];
+
+    services.trafficserver = {
+      records.proxy.config.admin.user_id = user;
+      records.proxy.config.body_factory.template_sets_dir =
+        "${pkgs.trafficserver}/etc/trafficserver/body_factory";
+    };
+
+    users.users.trafficserver = {
+      description = "Apache Traffic Server";
+      isSystemUser = true;
+      inherit group;
+    };
+    users.groups.trafficserver = { };
+  };
+}
diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix
index 01a01d97a23..68d55ee6ffd 100644
--- a/nixos/modules/services/web-servers/ttyd.nix
+++ b/nixos/modules/services/web-servers/ttyd.nix
@@ -33,7 +33,7 @@ in
       enable = mkEnableOption "ttyd daemon";
 
       port = mkOption {
-        type = types.int;
+        type = types.port;
         default = 7681;
         description = "Port to listen on (use 0 for random port)";
       };
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 894271d1e55..2a264bf2e9a 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -28,10 +28,12 @@ in {
         description = "Group account under which unit runs.";
       };
       stateDir = mkOption {
+        type = types.path;
         default = "/var/spool/unit";
         description = "Unit data directory.";
       };
       logDir = mkOption {
+        type = types.path;
         default = "/var/log/unit";
         description = "Unit log directory.";
       };
diff --git a/nixos/modules/services/web-servers/uwsgi.nix b/nixos/modules/services/web-servers/uwsgi.nix
index 936e211ec71..2dfc39c847a 100644
--- a/nixos/modules/services/web-servers/uwsgi.nix
+++ b/nixos/modules/services/web-servers/uwsgi.nix
@@ -5,11 +5,24 @@ with lib;
 let
   cfg = config.services.uwsgi;
 
+  isEmperor = cfg.instance.type == "emperor";
+
+  imperialPowers =
+    [
+      # spawn other user processes
+      "CAP_SETUID" "CAP_SETGID"
+      "CAP_SYS_CHROOT"
+      # transfer capabilities
+      "CAP_SETPCAP"
+      # create other user sockets
+      "CAP_CHOWN"
+    ];
+
   buildCfg = name: c:
     let
       plugins =
         if any (n: !any (m: m == n) cfg.plugins) (c.plugins or [])
-        then throw "`plugins` attribute in UWSGI configuration contains plugins not in config.services.uwsgi.plugins"
+        then throw "`plugins` attribute in uWSGI configuration contains plugins not in config.services.uwsgi.plugins"
         else c.plugins or cfg.plugins;
 
       hasPython = v: filter (n: n == "python${v}") plugins != [];
@@ -18,7 +31,7 @@ let
 
       python =
         if hasPython2 && hasPython3 then
-          throw "`plugins` attribute in UWSGI configuration shouldn't contain both python2 and python3"
+          throw "`plugins` attribute in uWSGI configuration shouldn't contain both python2 and python3"
         else if hasPython2 then cfg.package.python2
         else if hasPython3 then cfg.package.python3
         else null;
@@ -43,7 +56,7 @@ let
                       oldPaths = filter (x: x != null) (map getPath env');
                   in env' ++ [ "PATH=${optionalString (oldPaths != []) "${last oldPaths}:"}${pythonEnv}/bin" ];
               }
-          else if c.type == "emperor"
+          else if isEmperor
             then {
               emperor = if builtins.typeOf c.vassals != "set" then c.vassals
                         else pkgs.buildEnv {
@@ -51,7 +64,7 @@ let
                           paths = mapAttrsToList buildCfg c.vassals;
                         };
             } // removeAttrs c [ "type" "vassals" ]
-          else throw "`type` attribute in UWSGI configuration should be either 'normal' or 'emperor'";
+          else throw "`type` attribute in uWSGI configuration should be either 'normal' or 'emperor'";
       };
 
     in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg);
@@ -79,7 +92,7 @@ in {
       };
 
       instance = mkOption {
-        type =  with lib.types; let
+        type =  with types; let
           valueType = nullOr (oneOf [
             bool
             int
@@ -137,31 +150,66 @@ in {
       user = mkOption {
         type = types.str;
         default = "uwsgi";
-        description = "User account under which uwsgi runs.";
+        description = "User account under which uWSGI runs.";
       };
 
       group = mkOption {
         type = types.str;
         default = "uwsgi";
-        description = "Group account under which uwsgi runs.";
+        description = "Group account under which uWSGI runs.";
+      };
+
+      capabilities = mkOption {
+        type = types.listOf types.str;
+        apply = caps: caps ++ optionals isEmperor imperialPowers;
+        default = [ ];
+        example = literalExample ''
+          [
+            "CAP_NET_BIND_SERVICE" # bind on ports <1024
+            "CAP_NET_RAW"          # open raw sockets
+          ]
+        '';
+        description = ''
+          Grant capabilities to the uWSGI instance. See the
+          <literal>capabilities(7)</literal> for available values.
+          <note>
+            <para>
+              uWSGI runs as an unprivileged user (even as Emperor) with the minimal
+              capabilities required. This option can be used to add fine-grained
+              permissions without running the service as root.
+            </para>
+            <para>
+              When in Emperor mode, any capability to be inherited by a vassal must
+              be specified again in the vassal configuration using <literal>cap</literal>.
+              See the uWSGI <link
+              xlink:href="https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html">docs</link>
+              for more information.
+            </para>
+          </note>
+        '';
       };
     };
   };
 
   config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") ''
+      d ${cfg.runDir} 775 ${cfg.user} ${cfg.group}
+    '';
+
     systemd.services.uwsgi = {
       wantedBy = [ "multi-user.target" ];
-      preStart = ''
-        mkdir -p ${cfg.runDir}
-        chown ${cfg.user}:${cfg.group} ${cfg.runDir}
-      '';
       serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
         Type = "notify";
-        ExecStart = "${cfg.package}/bin/uwsgi --uid ${cfg.user} --gid ${cfg.group} --json ${buildCfg "server" cfg.instance}/server.json";
+        ExecStart = "${cfg.package}/bin/uwsgi --json ${buildCfg "server" cfg.instance}/server.json";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
         NotifyAccess = "main";
         KillSignal = "SIGQUIT";
+        AmbientCapabilities = cfg.capabilities;
+        CapabilityBoundingSet = cfg.capabilities;
+        RuntimeDirectory = mkIf (cfg.runDir == "/run/uwsgi") "uwsgi";
       };
     };
 
diff --git a/nixos/modules/services/x11/clight.nix b/nixos/modules/services/x11/clight.nix
index 4daf6d8d9db..873f425fb8b 100644
--- a/nixos/modules/services/x11/clight.nix
+++ b/nixos/modules/services/x11/clight.nix
@@ -11,14 +11,21 @@ let
     else if isBool v      then boolToString v
     else if isString v    then ''"${escape [''"''] v}"''
     else if isList v      then "[ " + concatMapStringsSep ", " toConf v + " ]"
+    else if isAttrs v     then "\n{\n" + convertAttrs v + "\n}"
     else abort "clight.toConf: unexpected type (v = ${v})";
 
-  clightConf = pkgs.writeText "clight.conf"
-    (concatStringsSep "\n" (mapAttrsToList
-      (name: value: "${toString name} = ${toConf value};")
-      (filterAttrs
-        (_: value: value != null)
-        cfg.settings)));
+  getSep = v:
+    if isAttrs v then ":"
+    else "=";
+
+  convertAttrs = attrs: concatStringsSep "\n" (mapAttrsToList
+    (name: value: "${toString name} ${getSep value} ${toConf value};")
+    attrs);
+
+  clightConf = pkgs.writeText "clight.conf" (convertAttrs
+    (filterAttrs
+      (_: value: value != null)
+      cfg.settings));
 in {
   options.services.clight = {
     enable = mkOption {
@@ -49,9 +56,10 @@ in {
     };
 
     settings = let
-      validConfigTypes = with types; either int (either str (either bool float));
+      validConfigTypes = with types; oneOf [ int str bool float ];
+      collectionTypes = with types; oneOf [ validConfigTypes (listOf validConfigTypes) ];
     in mkOption {
-      type = with types; attrsOf (nullOr (either validConfigTypes (listOf validConfigTypes)));
+      type = with types; attrsOf (nullOr (either collectionTypes (attrsOf collectionTypes)));
       default = {};
       example = { captures = 20; gamma_long_transition = true; ac_capture_timeouts = [ 120 300 60 ]; };
       description = ''
@@ -69,10 +77,10 @@ in {
     services.upower.enable = true;
 
     services.clight.settings = {
-      gamma_temp = with cfg.temperature; mkDefault [ day night ];
+      gamma.temp = with cfg.temperature; mkDefault [ day night ];
     } // (optionalAttrs (config.location.provider == "manual") {
-      latitude = mkDefault config.location.latitude;
-      longitude = mkDefault config.location.longitude;
+      daytime.latitude = mkDefault config.location.latitude;
+      daytime.longitude = mkDefault config.location.longitude;
     });
 
     services.geoclue2.appConfig.clightc = {
diff --git a/nixos/modules/services/x11/desktop-managers/cde.nix b/nixos/modules/services/x11/desktop-managers/cde.nix
index 2d9504fb5f1..3f1575a0ca6 100644
--- a/nixos/modules/services/x11/desktop-managers/cde.nix
+++ b/nixos/modules/services/x11/desktop-managers/cde.nix
@@ -68,5 +68,5 @@ in {
     }];
   };
 
-  meta.maintainers = [ maintainers.gnidorah ];
+  meta.maintainers = [ ];
 }
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
new file mode 100644
index 00000000000..d201c1a5334
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -0,0 +1,211 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.cinnamon;
+  serviceCfg = config.services.cinnamon;
+
+  nixos-gsettings-overrides = pkgs.cinnamon.cinnamon-gsettings-overrides.override {
+    extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
+    extraGSettingsOverrides = cfg.extraGSettingsOverrides;
+  };
+
+in
+
+{
+  options = {
+    services.cinnamon = {
+      apps.enable = mkEnableOption "Cinnamon default applications";
+    };
+
+    services.xserver.desktopManager.cinnamon = {
+      enable = mkEnableOption "the cinnamon desktop manager";
+
+      sessionPath = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExample "[ pkgs.gnome.gpaste ]";
+        description = ''
+          Additional list of packages to be added to the session search path.
+          Useful for GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+      };
+
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "Additional gsettings overrides.";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "List of packages for which gsettings are overridden.";
+      };
+    };
+
+    environment.cinnamon.excludePackages = mkOption {
+      default = [];
+      example = literalExample "[ pkgs.cinnamon.blueberry ]";
+      type = types.listOf types.package;
+      description = "Which packages cinnamon should exclude from the default environment";
+    };
+
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable && config.services.xserver.displayManager.lightdm.enable && config.services.xserver.displayManager.lightdm.greeters.gtk.enable) {
+      services.xserver.displayManager.lightdm.greeters.gtk.extraConfig = mkDefault (builtins.readFile "${pkgs.cinnamon.mint-artwork}/etc/lightdm/lightdm-gtk-greeter.conf.d/99_linuxmint.conf");
+      })
+
+    (mkIf cfg.enable {
+      services.xserver.displayManager.sessionPackages = [ pkgs.cinnamon.cinnamon-common ];
+
+      services.xserver.displayManager.sessionCommands = ''
+        if test "$XDG_CURRENT_DESKTOP" = "Cinnamon"; then
+            true
+            ${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
+      '';
+
+      # Default services
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      security.polkit.enable = true;
+      services.accounts-daemon.enable = true;
+      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+      services.dbus.packages = with pkgs.cinnamon; [
+        cinnamon-common
+        cinnamon-screensaver
+        nemo
+        xapps
+      ];
+      services.cinnamon.apps.enable = mkDefault true;
+      services.gnome.glib-networking.enable = true;
+      services.gnome.gnome-keyring.enable = true;
+      services.gvfs.enable = true;
+      services.udisks2.enable = true;
+      services.upower.enable = mkDefault config.powerManagement.enable;
+      services.xserver.libinput.enable = mkDefault true;
+      services.xserver.updateDbusEnvironment = true;
+      networking.networkmanager.enable = mkDefault true;
+
+      # Enable colord server
+      services.colord.enable = true;
+
+      # Enable dconf
+      programs.dconf.enable = true;
+
+      # Enable org.a11y.Bus
+      services.gnome.at-spi2-core.enable = true;
+
+      # Fix lockscreen
+      security.pam.services = {
+        cinnamon-screensaver = {};
+      };
+
+      environment.systemPackages = with pkgs.cinnamon // pkgs; [
+        desktop-file-utils
+        nixos-artwork.wallpapers.simple-dark-gray
+        onboard
+        sound-theme-freedesktop
+
+        # common-files
+        cinnamon-common
+        cinnamon-session
+        cinnamon-desktop
+        cinnamon-menus
+        cinnamon-translations
+
+        # utils needed by some scripts
+        killall
+
+        # session requirements
+        cinnamon-screensaver
+        # cinnamon-killer-daemon: provided by cinnamon-common
+        gnome.networkmanagerapplet # session requirement - also nm-applet not needed
+
+        # For a polkit authentication agent
+        polkit_gnome
+
+        # packages
+        nemo
+        cinnamon-control-center
+        cinnamon-settings-daemon
+        gnome.libgnomekbd
+        orca
+
+        # theme
+        gnome.adwaita-icon-theme
+        hicolor-icon-theme
+        gnome.gnome-themes-extra
+        gtk3.out
+        mint-artwork
+        mint-themes
+        mint-x-icons
+        mint-y-icons
+        vanilla-dmz
+
+        # other
+        glib # for gsettings
+        shared-mime-info # for update-mime-database
+        xdg-user-dirs
+      ];
+
+      # Override GSettings schemas
+      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+      environment.pathsToLink = [
+        # FIXME: modules should link subdirs of `/share` rather than relying on this
+        "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
+      ];
+
+      # 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; [
+        source-code-pro # Default monospace font in 3.32
+        ubuntu_font_family # required for default theme
+      ];
+    })
+
+    (mkIf serviceCfg.apps.enable {
+      programs.geary.enable = mkDefault true;
+      programs.gnome-disks.enable = mkDefault true;
+      programs.gnome-terminal.enable = mkDefault true;
+      programs.evince.enable = mkDefault true;
+      programs.file-roller.enable = mkDefault true;
+
+      environment.systemPackages = (with pkgs // pkgs.gnome // pkgs.cinnamon; pkgs.gnome.removePackagesByName [
+        # cinnamon team apps
+        bulky
+        blueberry
+        warpinator
+
+        # external apps shipped with linux-mint
+        hexchat
+        gnome-calculator
+      ] config.environment.cinnamon.excludePackages);
+    })
+  ];
+}
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 5d3a84d7139..6ee5b0fc54f 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -19,8 +19,9 @@ in
   # E.g., if Plasma 5 is enabled, it supersedes xterm.
   imports = [
     ./none.nix ./xterm.nix ./xfce.nix ./plasma5.nix ./lumina.nix
-    ./lxqt.nix ./enlightenment.nix ./gnome3.nix ./kodi.nix
+    ./lxqt.nix ./enlightenment.nix ./gnome.nix ./kodi.nix
     ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix
+    ./cinnamon.nix
   ];
 
   options = {
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.nix b/nixos/modules/services/x11/desktop-managers/gnome.nix
new file mode 100644
index 00000000000..b0859321a52
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/gnome.nix
@@ -0,0 +1,590 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.xserver.desktopManager.gnome;
+  serviceCfg = config.services.gnome;
+
+  # Prioritize nautilus by default when opening directories
+  mimeAppsList = pkgs.writeTextFile {
+    name = "gnome-mimeapps";
+    destination = "/share/applications/mimeapps.list";
+    text = ''
+      [Default Applications]
+      inode/directory=nautilus.desktop;org.gnome.Nautilus.desktop
+    '';
+  };
+
+  defaultFavoriteAppsOverride = ''
+    [org.gnome.shell]
+    favorite-apps=[ 'org.gnome.Epiphany.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Calendar.desktop', 'org.gnome.Music.desktop', 'org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop' ]
+  '';
+
+  nixos-gsettings-desktop-schemas = let
+    defaultPackages = with pkgs; [ gsettings-desktop-schemas gnome.gnome-shell ];
+  in
+  pkgs.runCommand "nixos-gsettings-desktop-schemas" { preferLocalBuild = true; }
+    ''
+     mkdir -p $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
+
+     ${concatMapStrings
+        (pkg: "cp -rf ${pkg}/share/gsettings-schemas/*/glib-2.0/schemas/*.xml $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas\n")
+        (defaultPackages ++ cfg.extraGSettingsOverridePackages)}
+
+     cp -f ${pkgs.gnome.gnome-shell}/share/gsettings-schemas/*/glib-2.0/schemas/*.gschema.override $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
+
+     ${optionalString flashbackEnabled ''
+       cp -f ${pkgs.gnome.gnome-flashback}/share/gsettings-schemas/*/glib-2.0/schemas/*.gschema.override $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
+     ''}
+
+     chmod -R a+w $out/share/gsettings-schemas/nixos-gsettings-overrides
+     cat - > $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/nixos-defaults.gschema.override <<- EOF
+       [org.gnome.desktop.background]
+       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray.gnomeFilePath}'
+
+       [org.gnome.desktop.screensaver]
+       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath}'
+
+       ${cfg.favoriteAppsOverride}
+
+       ${cfg.extraGSettingsOverrides}
+     EOF
+
+     ${pkgs.glib.dev}/bin/glib-compile-schemas $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/
+    '';
+
+  flashbackEnabled = cfg.flashback.enableMetacity || length cfg.flashback.customSessions > 0;
+  flashbackWms = optional cfg.flashback.enableMetacity {
+    wmName = "metacity";
+    wmLabel = "Metacity";
+    wmCommand = "${pkgs.gnome.metacity}/bin/metacity";
+    enableGnomePanel = true;
+  } ++ cfg.flashback.customSessions;
+
+  notExcluded = pkg: mkDefault (!(lib.elem pkg config.environment.gnome.excludePackages));
+
+in
+
+{
+
+  meta = {
+    doc = ./gnome.xml;
+    maintainers = teams.gnome.members;
+  };
+
+  imports = [
+    # Added 2021-05-07
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-os-services" "enable" ]
+      [ "services" "gnome" "core-os-services" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-shell" "enable" ]
+      [ "services" "gnome" "core-shell" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-utilities" "enable" ]
+      [ "services" "gnome" "core-utilities" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "core-developer-tools" "enable" ]
+      [ "services" "gnome" "core-developer-tools" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "games" "enable" ]
+      [ "services" "gnome" "games" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "gnome3" "experimental-features" "realtime-scheduling" ]
+      [ "services" "gnome" "experimental-features" "realtime-scheduling" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "enable" ]
+      [ "services" "xserver" "desktopManager" "gnome" "enable" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "sessionPath" ]
+      [ "services" "xserver" "desktopManager" "gnome" "sessionPath" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "favoriteAppsOverride" ]
+      [ "services" "xserver" "desktopManager" "gnome" "favoriteAppsOverride" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "extraGSettingsOverrides" ]
+      [ "services" "xserver" "desktopManager" "gnome" "extraGSettingsOverrides" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "extraGSettingsOverridePackages" ]
+      [ "services" "xserver" "desktopManager" "gnome" "extraGSettingsOverridePackages" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "debug" ]
+      [ "services" "xserver" "desktopManager" "gnome" "debug" ]
+    )
+    (mkRenamedOptionModule
+      [ "services" "xserver" "desktopManager" "gnome3" "flashback" ]
+      [ "services" "xserver" "desktopManager" "gnome" "flashback" ]
+    )
+    (mkRenamedOptionModule
+      [ "environment" "gnome3" "excludePackages" ]
+      [ "environment" "gnome" "excludePackages" ]
+    )
+  ];
+
+  options = {
+
+    services.gnome = {
+      core-os-services.enable = mkEnableOption "essential services for GNOME3";
+      core-shell.enable = mkEnableOption "GNOME Shell services";
+      core-utilities.enable = mkEnableOption "GNOME core utilities";
+      core-developer-tools.enable = mkEnableOption "GNOME core developer tools";
+      games.enable = mkEnableOption "GNOME games";
+
+      experimental-features = {
+        realtime-scheduling = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Makes mutter (which propagates to gnome-shell) request a low priority real-time
+            scheduling which is only available on the wayland session.
+            To enable this experimental feature it requires a restart of the compositor.
+            Note that enabling this option only enables the <emphasis>capability</emphasis>
+            for realtime-scheduling to be used. It doesn't automatically set the gsetting
+            so that mutter actually uses realtime-scheduling. This would require adding <literal>
+            rt-scheduler</literal> to <literal>/org/gnome/mutter/experimental-features</literal>
+            with dconf-editor. You cannot use extraGSettingsOverrides because that will only
+            change the default value of the setting.
+
+            Please be aware of these known issues with the feature in nixos:
+            <itemizedlist>
+             <listitem>
+              <para>
+               <link xlink:href="https://github.com/NixOS/nixpkgs/issues/90201">NixOS/nixpkgs#90201</link>
+              </para>
+             </listitem>
+             <listitem>
+              <para>
+               <link xlink:href="https://github.com/NixOS/nixpkgs/issues/86730">NixOS/nixpkgs#86730</link>
+              </para>
+            </listitem>
+            </itemizedlist>
+          '';
+        };
+      };
+    };
+
+    services.xserver.desktopManager.gnome = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable GNOME desktop manager.";
+      };
+
+      sessionPath = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = literalExample "[ pkgs.gnome.gpaste ]";
+        description = ''
+          Additional list of packages to be added to the session search path.
+          Useful for GNOME Shell extensions or GSettings-conditional autostart.
+
+          Note that this should be a last resort; patching the package is preferred (see GPaste).
+        '';
+        apply = list: list ++ [ pkgs.gnome.gnome-shell pkgs.gnome.gnome-shell-extensions ];
+      };
+
+      favoriteAppsOverride = mkOption {
+        internal = true; # this is messy
+        default = defaultFavoriteAppsOverride;
+        type = types.lines;
+        example = literalExample ''
+          [org.gnome.shell]
+          favorite-apps=[ 'firefox.desktop', 'org.gnome.Calendar.desktop' ]
+        '';
+        description = "List of desktop files to put as favorite apps into gnome-shell. These need to be installed somehow globally.";
+      };
+
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = "Additional gsettings overrides.";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        default = [];
+        type = types.listOf types.path;
+        description = "List of packages for which gsettings are overridden.";
+      };
+
+      debug = mkEnableOption "gnome-session debug messages";
+
+      flashback = {
+        enableMetacity = mkEnableOption "the standard GNOME Flashback session with Metacity";
+
+        customSessions = mkOption {
+          type = types.listOf (types.submodule {
+            options = {
+              wmName = mkOption {
+                type = types.strMatching "[a-zA-Z0-9_-]+";
+                description = "A unique identifier for the window manager.";
+                example = "xmonad";
+              };
+
+              wmLabel = mkOption {
+                type = types.str;
+                description = "The name of the window manager to show in the session chooser.";
+                example = "XMonad";
+              };
+
+              wmCommand = mkOption {
+                type = types.str;
+                description = "The executable of the window manager to use.";
+                example = "\${pkgs.haskellPackages.xmonad}/bin/xmonad";
+              };
+
+              enableGnomePanel = mkOption {
+                type = types.bool;
+                default = true;
+                example = "false";
+                description = "Whether to enable the GNOME panel in this session.";
+              };
+            };
+          });
+          default = [];
+          description = "Other GNOME Flashback sessions to enable.";
+        };
+
+        panelModulePackages = mkOption {
+          default = [ pkgs.gnome.gnome-applets ];
+          type = types.listOf types.path;
+          description = ''
+            Packages containing modules that should be made available to <literal>gnome-panel</literal> (usually for applets).
+
+            If you're packaging something to use here, please install the modules in <literal>$out/lib/gnome-panel/modules</literal>.
+          '';
+          example = literalExample "[ pkgs.gnome.gnome-applets ]";
+        };
+      };
+    };
+
+    environment.gnome.excludePackages = mkOption {
+      default = [];
+      example = literalExample "[ pkgs.gnome.totem ]";
+      type = types.listOf types.package;
+      description = "Which packages gnome should exclude from the default environment";
+    };
+
+  };
+
+  config = mkMerge [
+    (mkIf (cfg.enable || flashbackEnabled) {
+      # Seed our configuration into nixos-generate-config
+      system.nixos-generate-config.desktopConfiguration = [''
+        # Enable the GNOME Desktop Environment.
+        services.xserver.displayManager.gdm.enable = true;
+        services.xserver.desktopManager.gnome.enable = true;
+      ''];
+
+      services.gnome.core-os-services.enable = true;
+      services.gnome.core-shell.enable = true;
+      services.gnome.core-utilities.enable = mkDefault true;
+
+      services.xserver.displayManager.sessionPackages = [ pkgs.gnome.gnome-session.sessions ];
+
+      environment.extraInit = ''
+        ${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}
+      '';
+
+      environment.systemPackages = cfg.sessionPath;
+
+      environment.sessionVariables.GNOME_SESSION_DEBUG = mkIf cfg.debug "1";
+
+      # Override GSettings schemas
+      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+       # If gnome is installed, build vim for gtk3 too.
+      nixpkgs.config.vim.gui = "gtk3";
+    })
+
+    (mkIf flashbackEnabled {
+      services.xserver.displayManager.sessionPackages =
+        let
+          wmNames = map (wm: wm.wmName) flashbackWms;
+          namesAreUnique = lib.unique wmNames == wmNames;
+        in
+          assert (assertMsg namesAreUnique "Flashback WM names must be unique.");
+          map
+            (wm:
+              pkgs.gnome.gnome-flashback.mkSessionForWm {
+                inherit (wm) wmName wmLabel wmCommand enableGnomePanel;
+                inherit (cfg.flashback) panelModulePackages;
+              }
+            ) flashbackWms;
+
+      security.pam.services.gnome-flashback = {
+        enableGnomeKeyring = true;
+      };
+
+      systemd.packages = with pkgs.gnome; [
+        gnome-flashback
+      ] ++ map gnome-flashback.mkSystemdTargetForWm flashbackWms;
+
+      # gnome-panel needs these for menu applet
+      environment.sessionVariables.XDG_DATA_DIRS = [ "${pkgs.gnome.gnome-flashback}/share" ];
+      # TODO: switch to sessionVariables (resolve conflict)
+      environment.variables.XDG_CONFIG_DIRS = [ "${pkgs.gnome.gnome-flashback}/etc/xdg" ];
+    })
+
+    (mkIf serviceCfg.core-os-services.enable {
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      programs.dconf.enable = true;
+      security.polkit.enable = true;
+      services.accounts-daemon.enable = true;
+      services.dleyna-renderer.enable = mkDefault true;
+      services.dleyna-server.enable = mkDefault true;
+      services.power-profiles-daemon.enable = mkDefault true;
+      services.gnome.at-spi2-core.enable = true;
+      services.gnome.evolution-data-server.enable = true;
+      services.gnome.gnome-keyring.enable = true;
+      services.gnome.gnome-online-accounts.enable = mkDefault true;
+      services.gnome.gnome-online-miners.enable = true;
+      services.gnome.tracker-miners.enable = mkDefault true;
+      services.gnome.tracker.enable = mkDefault true;
+      services.hardware.bolt.enable = mkDefault true;
+      services.packagekit.enable = mkDefault true;
+      services.udisks2.enable = true;
+      services.upower.enable = config.powerManagement.enable;
+      services.xserver.libinput.enable = mkDefault true; # for controlling touchpad settings via gnome control center
+
+      xdg.portal.enable = true;
+      xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
+
+      networking.networkmanager.enable = mkDefault true;
+
+      services.xserver.updateDbusEnvironment = true;
+
+      # gnome has a custom alert theme but it still
+      # inherits from the freedesktop theme.
+      environment.systemPackages = with pkgs; [
+        sound-theme-freedesktop
+      ];
+
+      # Needed for themes and backgrounds
+      environment.pathsToLink = [
+        "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
+      ];
+    })
+
+    (mkIf serviceCfg.core-shell.enable {
+      services.colord.enable = mkDefault true;
+      services.gnome.chrome-gnome-shell.enable = mkDefault true;
+      services.gnome.glib-networking.enable = true;
+      services.gnome.gnome-initial-setup.enable = mkDefault true;
+      services.gnome.gnome-remote-desktop.enable = mkDefault true;
+      services.gnome.gnome-settings-daemon.enable = true;
+      services.gnome.gnome-user-share.enable = mkDefault true;
+      services.gnome.rygel.enable = mkDefault true;
+      services.gvfs.enable = true;
+      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
+      services.telepathy.enable = mkDefault true;
+
+      systemd.packages = with pkgs.gnome; [
+        gnome-session
+        gnome-shell
+      ];
+
+      services.udev.packages = with pkgs.gnome; [
+        # Force enable KMS modifiers for devices that require them.
+        # https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1443
+        mutter
+      ];
+
+      services.avahi.enable = mkDefault true;
+
+      xdg.portal.extraPortals = [
+        pkgs.gnome.gnome-shell
+      ];
+
+      services.geoclue2.enable = mkDefault true;
+      services.geoclue2.enableDemoAgent = false; # GNOME has its own geoclue agent
+
+      services.geoclue2.appConfig.gnome-datetime-panel = {
+        isAllowed = true;
+        isSystem = true;
+      };
+      services.geoclue2.appConfig.gnome-color-panel = {
+        isAllowed = true;
+        isSystem = true;
+      };
+      services.geoclue2.appConfig."org.gnome.Shell" = {
+        isAllowed = true;
+        isSystem = true;
+      };
+
+      fonts.fonts = with pkgs; [
+        cantarell-fonts
+        dejavu_fonts
+        source-code-pro # Default monospace font in 3.32
+        source-sans-pro
+      ];
+
+      # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-38/elements/core/meta-gnome-core-shell.bst
+      environment.systemPackages = with pkgs.gnome; [
+        adwaita-icon-theme
+        gnome-backgrounds
+        gnome-bluetooth
+        gnome-color-manager
+        gnome-control-center
+        gnome-shell
+        gnome-shell-extensions
+        gnome-themes-extra
+        pkgs.gnome-tour # GNOME Shell detects the .desktop file on first log-in.
+        pkgs.nixos-artwork.wallpapers.simple-dark-gray
+        pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom
+        pkgs.gnome-user-docs
+        pkgs.orca
+        pkgs.glib # for gsettings
+        pkgs.gnome-menus
+        pkgs.gtk3.out # for gtk-launch
+        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/
+      ];
+    })
+
+    # Enable soft realtime scheduling, only supported on wayland
+    (mkIf serviceCfg.experimental-features.realtime-scheduling {
+      security.wrappers.".gnome-shell-wrapped" = {
+        source = "${pkgs.gnome.gnome-shell}/bin/.gnome-shell-wrapped";
+        capabilities = "cap_sys_nice=ep";
+      };
+
+      systemd.user.services.gnome-shell-wayland = let
+        gnomeShellRT = with pkgs.gnome; pkgs.runCommand "gnome-shell-rt" {} ''
+          mkdir -p $out/bin/
+          cp ${gnome-shell}/bin/gnome-shell $out/bin
+          sed -i "s@${gnome-shell}/bin/@${config.security.wrapperDir}/@" $out/bin/gnome-shell
+        '';
+      in {
+        # Note we need to clear ExecStart before overriding it
+        serviceConfig.ExecStart = ["" "${gnomeShellRT}/bin/gnome-shell"];
+        # Do not use the default environment, it provides a broken PATH
+        environment = mkForce {};
+      };
+    })
+
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-38/elements/core/meta-gnome-core-utilities.bst
+    (mkIf serviceCfg.core-utilities.enable {
+      environment.systemPackages =
+        with pkgs.gnome;
+        removePackagesByName
+          ([
+            baobab
+            cheese
+            eog
+            epiphany
+            gedit
+            gnome-calculator
+            gnome-calendar
+            gnome-characters
+            gnome-clocks
+            gnome-contacts
+            gnome-font-viewer
+            gnome-logs
+            gnome-maps
+            gnome-music
+            pkgs.gnome-photos
+            gnome-screenshot
+            gnome-system-monitor
+            gnome-weather
+            nautilus
+            pkgs.gnome-connections
+            simple-scan
+            totem
+            yelp
+          ] ++ lib.optionals config.services.flatpak.enable [
+            # Since PackageKit Nix support is not there yet,
+            # only install gnome-software if flatpak is enabled.
+            gnome-software
+          ])
+          config.environment.gnome.excludePackages;
+
+      # Enable default program modules
+      # Since some of these have a corresponding package, we only
+      # enable that program module if the package hasn't been excluded
+      # through `environment.gnome.excludePackages`
+      programs.evince.enable = notExcluded pkgs.gnome.evince;
+      programs.file-roller.enable = notExcluded pkgs.gnome.file-roller;
+      programs.geary.enable = notExcluded pkgs.gnome.geary;
+      programs.gnome-disks.enable = notExcluded pkgs.gnome.gnome-disk-utility;
+      programs.gnome-terminal.enable = notExcluded pkgs.gnome.gnome-terminal;
+      programs.seahorse.enable = notExcluded pkgs.gnome.seahorse;
+      services.gnome.sushi.enable = notExcluded pkgs.gnome.sushi;
+
+      # Let nautilus find extensions
+      # TODO: Create nautilus-with-extensions package
+      environment.sessionVariables.NAUTILUS_EXTENSION_DIR = "${config.system.path}/lib/nautilus/extensions-3.0";
+
+      # Override default mimeapps for nautilus
+      environment.sessionVariables.XDG_DATA_DIRS = [ "${mimeAppsList}/share" ];
+
+      environment.pathsToLink = [
+        "/share/nautilus-python/extensions"
+      ];
+    })
+
+    (mkIf serviceCfg.games.enable {
+      environment.systemPackages = (with pkgs.gnome; removePackagesByName [
+        aisleriot
+        atomix
+        five-or-more
+        four-in-a-row
+        gnome-chess
+        gnome-klotski
+        gnome-mahjongg
+        gnome-mines
+        gnome-nibbles
+        gnome-robots
+        gnome-sudoku
+        gnome-taquin
+        gnome-tetravex
+        hitori
+        iagno
+        lightsoff
+        quadrapassel
+        swell-foop
+        tali
+      ] config.environment.gnome.excludePackages);
+    })
+
+    # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/-/blob/3.38.0/elements/core/meta-gnome-core-developer-tools.bst
+    (mkIf serviceCfg.core-developer-tools.enable {
+      environment.systemPackages = (with pkgs.gnome; removePackagesByName [
+        dconf-editor
+        devhelp
+        pkgs.gnome-builder
+        # boxes would make sense in this option, however
+        # it doesn't function well enough to be included
+        # in default configurations.
+        # https://github.com/NixOS/nixpkgs/issues/60908
+        /* gnome-boxes */
+      ] config.environment.gnome.excludePackages);
+
+      services.sysprof.enable = notExcluded pkgs.sysprof;
+    })
+  ];
+
+}
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.xml b/nixos/modules/services/x11/desktop-managers/gnome.xml
new file mode 100644
index 00000000000..6c53bacacb3
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/gnome.xml
@@ -0,0 +1,277 @@
+<chapter xmlns="http://docbook.org/ns/docbook"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xml:id="chap-gnome">
+ <title>GNOME Desktop</title>
+ <para>
+  GNOME provides a simple, yet full-featured desktop environment with a focus on productivity. Its Mutter compositor supports both Wayland and X server, and the GNOME Shell user interface is fully customizable by extensions.
+ </para>
+
+ <section xml:id="sec-gnome-enable">
+  <title>Enabling GNOME</title>
+
+  <para>
+   All of the core apps, optional apps, games, and core developer tools from GNOME are available.
+  </para>
+
+  <para>
+   To enable the GNOME desktop use:
+  </para>
+
+<programlisting>
+<xref linkend="opt-services.xserver.desktopManager.gnome.enable"/> = true;
+<xref linkend="opt-services.xserver.displayManager.gdm.enable"/> = true;
+</programlisting>
+
+  <note>
+   <para>
+    While it is not strictly necessary to use GDM as the display manager with GNOME, it is recommended, as some features such as screen lock <link xlink:href="#sec-gnome-faq-can-i-use-lightdm-with-gnome">might not work</link> without it.
+   </para>
+  </note>
+
+  <para>
+   The default applications used in NixOS are very minimal, inspired by the defaults used in <link xlink:href="https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/40.0/elements/core/meta-gnome-core-utilities.bst">gnome-build-meta</link>.
+  </para>
+
+  <section xml:id="sec-gnome-without-the-apps">
+   <title>GNOME without the apps</title>
+
+   <para>
+    If you’d like to only use the GNOME desktop and not the apps, you can disable them with:
+   </para>
+
+<programlisting>
+<xref linkend="opt-services.gnome.core-utilities.enable"/> = false;
+</programlisting>
+
+   <para>
+    and none of them will be installed.
+   </para>
+
+   <para>
+    If you’d only like to omit a subset of the core utilities, you can use <xref linkend="opt-environment.gnome.excludePackages"/>.
+    Note that this mechanism can only exclude core utilities, games and core developer tools.
+   </para>
+  </section>
+
+  <section xml:id="sec-gnome-disabling-services">
+   <title>Disabling GNOME services</title>
+
+   <para>
+    It is also possible to disable many of the <link xlink:href="https://github.com/NixOS/nixpkgs/blob/b8ec4fd2a4edc4e30d02ba7b1a2cc1358f3db1d5/nixos/modules/services/x11/desktop-managers/gnome.nix#L329-L348">core services</link>. For example, if you do not need indexing files, you can disable Tracker with:
+   </para>
+
+<programlisting>
+<xref linkend="opt-services.gnome.tracker-miners.enable"/> = false;
+<xref linkend="opt-services.gnome.tracker.enable"/> = false;
+</programlisting>
+
+   <para>
+    Note, however, that doing so is not supported and might break some applications. Notably, GNOME Music cannot work without Tracker.
+   </para>
+  </section>
+
+  <section xml:id="sec-gnome-games">
+   <title>GNOME games</title>
+
+   <para>
+    You can install all of the GNOME games with:
+   </para>
+
+<programlisting>
+<xref linkend="opt-services.gnome.games.enable"/> = true;
+</programlisting>
+  </section>
+
+  <section xml:id="sec-gnome-core-developer-tools">
+   <title>GNOME core developer tools</title>
+
+   <para>
+    You can install GNOME core developer tools with:
+   </para>
+
+<programlisting>
+<xref linkend="opt-services.gnome.core-developer-tools.enable"/> = true;
+</programlisting>
+  </section>
+ </section>
+
+ <section xml:id="sec-gnome-enable-flashback">
+  <title>Enabling GNOME Flashback</title>
+
+  <para>
+   GNOME Flashback provides a desktop environment based on the classic GNOME 2 architecture. You can enable the default GNOME Flashback session, which uses the Metacity window manager, with:
+  </para>
+
+<programlisting>
+<xref linkend="opt-services.xserver.desktopManager.gnome.flashback.enableMetacity"/> = true;
+</programlisting>
+
+  <para>
+   It is also possible to create custom sessions that replace Metacity with a different window manager using <xref linkend="opt-services.xserver.desktopManager.gnome.flashback.customSessions"/>.
+  </para>
+
+  <para>
+   The following example uses <literal>xmonad</literal> window manager:
+  </para>
+
+<programlisting>
+<xref linkend="opt-services.xserver.desktopManager.gnome.flashback.customSessions"/> = [
+  {
+    wmName = "xmonad";
+    wmLabel = "XMonad";
+    wmCommand = "${pkgs.haskellPackages.xmonad}/bin/xmonad";
+    enableGnomePanel = false;
+  }
+];
+</programlisting>
+
+ </section>
+ <section xml:id="sec-gnome-gdm">
+  <title>GDM</title>
+
+  <para>
+   If you want to use GNOME Wayland session on Nvidia hardware, you need to enable:
+  </para>
+
+<programlisting>
+<xref linkend="opt-services.xserver.displayManager.gdm.nvidiaWayland"/> = true;
+</programlisting>
+
+  <para>
+   as the default configuration will forbid this.
+  </para>
+ </section>
+
+ <section xml:id="sec-gnome-icons-and-gtk-themes">
+  <title>Icons and GTK Themes</title>
+
+  <para>
+   Icon themes and GTK themes don’t require any special option to install in NixOS.
+  </para>
+
+  <para>
+   You can add them to <xref linkend="opt-environment.systemPackages"/> and switch to them with GNOME Tweaks.
+   If you’d like to do this manually in dconf, change the values of the following keys:
+  </para>
+
+<programlisting>
+/org/gnome/desktop/interface/gtk-theme
+/org/gnome/desktop/interface/icon-theme
+</programlisting>
+
+  <para>
+   in <literal>dconf-editor</literal>
+  </para>
+ </section>
+
+ <section xml:id="sec-gnome-shell-extensions">
+  <title>Shell Extensions</title>
+
+  <para>
+   Most Shell extensions are packaged under the <literal>gnomeExtensions</literal> attribute.
+   Some packages that include Shell extensions, like <literal>gnome.gpaste</literal>, don’t have their extension decoupled under this attribute.
+  </para>
+
+  <para>
+   You can install them like any other package:
+  </para>
+
+<programlisting>
+<xref linkend="opt-environment.systemPackages"/> = [
+  gnomeExtensions.dash-to-dock
+  gnomeExtensions.gsconnect
+  gnomeExtensions.mpris-indicator-button
+];
+</programlisting>
+
+  <para>
+   Unfortunately, we lack a way for these to be managed in a completely declarative way.
+   So you have to enable them manually with an Extensions application.
+   It is possible to use a <link xlink:href="#sec-gnome-gsettings-overrides">GSettings override</link> for this on <literal>org.gnome.shell.enabled-extensions</literal>, but that will only influence the default value.
+  </para>
+ </section>
+
+ <section xml:id="sec-gnome-gsettings-overrides">
+  <title>GSettings Overrides</title>
+
+  <para>
+   Majority of software building on the GNOME platform use GLib’s <link xlink:href="https://developer.gnome.org/gio/unstable/GSettings.html">GSettings</link> system to manage runtime configuration. For our purposes, the system consists of XML schemas describing the individual configuration options, stored in the package, and a settings backend, where the values of the settings are stored. On NixOS, like on most Linux distributions, dconf database is used as the backend.
+  </para>
+
+  <para>
+   <link xlink:href="https://developer.gnome.org/gio/unstable/GSettings.html#id-1.4.19.2.9.25">GSettings vendor overrides</link> can be used to adjust the default values for settings of the GNOME desktop and apps by replacing the default values specified in the XML schemas. Using overrides will allow you to pre-seed user settings before you even start the session.
+  </para>
+
+  <warning>
+   <para>
+    Overrides really only change the default values for GSettings keys so if you or an application changes the setting value, the value set by the override will be ignored. Until <link xlink:href="https://github.com/NixOS/nixpkgs/issues/54150">NixOS’s dconf module implements changing values</link>, you will either need to keep that in mind and clear the setting from the backend using <literal>dconf reset</literal> command when that happens, or use the <link xlink:href="https://nix-community.github.io/home-manager/options.html#opt-dconf.settings">module from home-manager</link>.
+   </para>
+  </warning>
+
+  <para>
+   You can override the default GSettings values using the <xref linkend="opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides"/> option.
+  </para>
+
+  <para>
+   Take note that whatever packages you want to override GSettings for, you need to add them to
+   <xref linkend="opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages"/>.
+  </para>
+
+  <para>
+   You can use <literal>dconf-editor</literal> tool to explore which GSettings you can set.
+  </para>
+
+  <section xml:id="sec-gnome-gsettings-overrides-example">
+   <title>Example</title>
+
+<programlisting>
+services.xserver.desktopManager.gnome = {
+  <link xlink:href="#opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides">extraGSettingsOverrides</link> = ''
+    # Change default background
+    [org.gnome.desktop.background]
+    picture-uri='file://${pkgs.nixos-artwork.wallpapers.mosaic-blue.gnomeFilePath}'
+
+    # Favorite apps in gnome-shell
+    [org.gnome.shell]
+    favorite-apps=['org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop']
+  '';
+
+  <link xlink:href="#opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages">extraGSettingsOverridePackages</link> = [
+    pkgs.gsettings-desktop-schemas # for org.gnome.desktop
+    pkgs.gnome.gnome-shell # for org.gnome.shell
+  ];
+};
+</programlisting>
+  </section>
+ </section>
+
+ <section xml:id="sec-gnome-faq">
+  <title>Frequently Asked Questions</title>
+
+  <section xml:id="sec-gnome-faq-can-i-use-lightdm-with-gnome">
+   <title>Can I use LightDM with GNOME?</title>
+
+   <para>
+    Yes you can, and any other display-manager in NixOS.
+   </para>
+
+   <para>
+    However, it doesn’t work correctly for the Wayland session of GNOME Shell yet, and
+    won’t be able to lock your screen.
+   </para>
+
+   <para>
+    See <link xlink:href="https://github.com/NixOS/nixpkgs/issues/56342">this issue.</link>
+   </para>
+  </section>
+
+  <section xml:id="sec-gnome-faq-nixos-rebuild-switch-kills-session">
+   <title>Why does <literal>nixos-rebuild switch</literal> sometimes kill my session?</title>
+
+   <para>
+    This is a known <link xlink:href="https://github.com/NixOS/nixpkgs/issues/44344">issue</link> without any workarounds.
+    If you are doing a fairly large upgrade, it is probably safer to use <literal>nixos-rebuild boot</literal>.
+   </para>
+  </section>
+ </section>
+</chapter>
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
deleted file mode 100644
index 69cf9832172..00000000000
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ /dev/null
@@ -1,397 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.xserver.desktopManager.gnome3;
-  serviceCfg = config.services.gnome3;
-
-  # Prioritize nautilus by default when opening directories
-  mimeAppsList = pkgs.writeTextFile {
-    name = "gnome-mimeapps";
-    destination = "/share/applications/mimeapps.list";
-    text = ''
-      [Default Applications]
-      inode/directory=nautilus.desktop;org.gnome.Nautilus.desktop
-    '';
-  };
-
-  nixos-gsettings-desktop-schemas = let
-    defaultPackages = with pkgs; [ gsettings-desktop-schemas gnome3.gnome-shell ];
-  in
-  pkgs.runCommand "nixos-gsettings-desktop-schemas" { preferLocalBuild = true; }
-    ''
-     mkdir -p $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
-
-     ${concatMapStrings
-        (pkg: "cp -rf ${pkg}/share/gsettings-schemas/*/glib-2.0/schemas/*.xml $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas\n")
-        (defaultPackages ++ cfg.extraGSettingsOverridePackages)}
-
-     cp -f ${pkgs.gnome3.gnome-shell}/share/gsettings-schemas/*/glib-2.0/schemas/*.gschema.override $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
-
-     ${optionalString flashbackEnabled ''
-       cp -f ${pkgs.gnome3.gnome-flashback}/share/gsettings-schemas/*/glib-2.0/schemas/*.gschema.override $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas
-     ''}
-
-     chmod -R a+w $out/share/gsettings-schemas/nixos-gsettings-overrides
-     cat - > $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/nixos-defaults.gschema.override <<- EOF
-       [org.gnome.desktop.background]
-       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray.gnomeFilePath}'
-
-       [org.gnome.desktop.screensaver]
-       picture-uri='file://${pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom.gnomeFilePath}'
-
-       [org.gnome.shell]
-       favorite-apps=[ 'org.gnome.Epiphany.desktop', 'org.gnome.Geary.desktop', 'org.gnome.Music.desktop', 'org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop', 'org.gnome.Software.desktop' ]
-
-       ${cfg.extraGSettingsOverrides}
-     EOF
-
-     ${pkgs.glib.dev}/bin/glib-compile-schemas $out/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas/
-    '';
-
-  flashbackEnabled = cfg.flashback.enableMetacity || length cfg.flashback.customSessions > 0;
-
-in
-
-{
-
-  meta = {
-    maintainers = teams.gnome.members;
-  };
-
-  options = {
-
-    services.gnome3 = {
-      core-os-services.enable = mkEnableOption "essential services for GNOME3";
-      core-shell.enable = mkEnableOption "GNOME Shell services";
-      core-utilities.enable = mkEnableOption "GNOME core utilities";
-      games.enable = mkEnableOption "GNOME games";
-    };
-
-    services.xserver.desktopManager.gnome3 = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Enable Gnome 3 desktop manager.";
-      };
-
-      sessionPath = mkOption {
-        default = [];
-        example = literalExample "[ pkgs.gnome3.gpaste ]";
-        description = ''
-          Additional list of packages to be added to the session search path.
-          Useful for GNOME Shell extensions or GSettings-conditional autostart.
-
-          Note that this should be a last resort; patching the package is preferred (see GPaste).
-        '';
-        apply = list: list ++ [ pkgs.gnome3.gnome-shell pkgs.gnome3.gnome-shell-extensions ];
-      };
-
-      extraGSettingsOverrides = mkOption {
-        default = "";
-        type = types.lines;
-        description = "Additional gsettings overrides.";
-      };
-
-      extraGSettingsOverridePackages = mkOption {
-        default = [];
-        type = types.listOf types.path;
-        description = "List of packages for which gsettings are overridden.";
-      };
-
-      debug = mkEnableOption "gnome-session debug messages";
-
-      flashback = {
-        enableMetacity = mkEnableOption "the standard GNOME Flashback session with Metacity";
-
-        customSessions = mkOption {
-          type = types.listOf (types.submodule {
-            options = {
-              wmName = mkOption {
-                type = types.str;
-                description = "The filename-compatible name of the window manager to use.";
-                example = "xmonad";
-              };
-
-              wmLabel = mkOption {
-                type = types.str;
-                description = "The pretty name of the window manager to use.";
-                example = "XMonad";
-              };
-
-              wmCommand = mkOption {
-                type = types.str;
-                description = "The executable of the window manager to use.";
-                example = "\${pkgs.haskellPackages.xmonad}/bin/xmonad";
-              };
-            };
-          });
-          default = [];
-          description = "Other GNOME Flashback sessions to enable.";
-        };
-      };
-    };
-
-    environment.gnome3.excludePackages = mkOption {
-      default = [];
-      example = literalExample "[ pkgs.gnome3.totem ]";
-      type = types.listOf types.package;
-      description = "Which packages gnome should exclude from the default environment";
-    };
-
-  };
-
-  config = mkMerge [
-    (mkIf (cfg.enable || flashbackEnabled) {
-      services.gnome3.core-os-services.enable = true;
-      services.gnome3.core-shell.enable = true;
-      services.gnome3.core-utilities.enable = mkDefault true;
-
-      services.xserver.displayManager.sessionPackages = [ pkgs.gnome3.gnome-session.sessions ];
-
-      environment.extraInit = ''
-        ${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}
-      '';
-
-      environment.systemPackages = cfg.sessionPath;
-
-      environment.sessionVariables.GNOME_SESSION_DEBUG = mkIf cfg.debug "1";
-
-      # Override GSettings schemas
-      environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-desktop-schemas}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
-
-       # If gnome3 is installed, build vim for gtk3 too.
-      nixpkgs.config.vim.gui = "gtk3";
-    })
-
-    (mkIf flashbackEnabled {
-      services.xserver.displayManager.sessionPackages =  map
-        (wm: pkgs.gnome3.gnome-flashback.mkSessionForWm {
-          inherit (wm) wmName wmLabel wmCommand;
-        }) (optional cfg.flashback.enableMetacity {
-              wmName = "metacity";
-              wmLabel = "Metacity";
-              wmCommand = "${pkgs.gnome3.metacity}/bin/metacity";
-            } ++ cfg.flashback.customSessions);
-
-      security.pam.services.gnome-flashback = {
-        enableGnomeKeyring = true;
-      };
-
-      systemd.packages = with pkgs.gnome3; [
-        gnome-flashback
-      ] ++ (map
-        (wm: gnome-flashback.mkSystemdTargetForWm {
-          inherit (wm) wmName;
-        }) cfg.flashback.customSessions);
-
-        # 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 {
-      hardware.bluetooth.enable = mkDefault true;
-      hardware.pulseaudio.enable = mkDefault true;
-      programs.dconf.enable = true;
-      security.polkit.enable = true;
-      services.accounts-daemon.enable = true;
-      services.dleyna-renderer.enable = mkDefault true;
-      services.dleyna-server.enable = mkDefault true;
-      services.gnome3.at-spi2-core.enable = true;
-      services.gnome3.evolution-data-server.enable = true;
-      services.gnome3.gnome-keyring.enable = true;
-      services.gnome3.gnome-online-accounts.enable = mkDefault true;
-      services.gnome3.gnome-online-miners.enable = true;
-      services.gnome3.tracker-miners.enable = mkDefault true;
-      services.gnome3.tracker.enable = mkDefault true;
-      services.hardware.bolt.enable = mkDefault true;
-      services.packagekit.enable = mkDefault true;
-      services.udisks2.enable = true;
-      services.upower.enable = config.powerManagement.enable;
-      services.xserver.libinput.enable = mkDefault true; # for controlling touchpad settings via gnome control center
-
-      xdg.portal.enable = true;
-      xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
-
-      networking.networkmanager.enable = mkDefault true;
-
-      services.xserver.updateDbusEnvironment = true;
-
-      # gnome has a custom alert theme but it still
-      # inherits from the freedesktop theme.
-      environment.systemPackages = with pkgs; [
-        sound-theme-freedesktop
-      ];
-
-      # Needed for themes and backgrounds
-      environment.pathsToLink = [
-        "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
-      ];
-    })
-
-    (mkIf serviceCfg.core-shell.enable {
-      services.colord.enable = mkDefault true;
-      services.gnome3.chrome-gnome-shell.enable = mkDefault true;
-      services.gnome3.glib-networking.enable = true;
-      services.gnome3.gnome-initial-setup.enable = mkDefault true;
-      services.gnome3.gnome-remote-desktop.enable = mkDefault true;
-      services.gnome3.gnome-settings-daemon.enable = true;
-      services.gnome3.gnome-user-share.enable = mkDefault true;
-      services.gnome3.rygel.enable = mkDefault true;
-      services.gvfs.enable = true;
-      services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
-      services.telepathy.enable = mkDefault true;
-
-      systemd.packages = with pkgs.gnome3; [
-        gnome-session
-        gnome-shell
-      ];
-
-      services.avahi.enable = mkDefault true;
-
-      xdg.portal.extraPortals = [
-        pkgs.gnome3.gnome-shell
-      ];
-
-      services.geoclue2.enable = mkDefault true;
-      services.geoclue2.enableDemoAgent = false; # GNOME has its own geoclue agent
-
-      services.geoclue2.appConfig.gnome-datetime-panel = {
-        isAllowed = true;
-        isSystem = true;
-      };
-      services.geoclue2.appConfig.gnome-color-panel = {
-        isAllowed = true;
-        isSystem = true;
-      };
-      services.geoclue2.appConfig."org.gnome.Shell" = {
-        isAllowed = true;
-        isSystem = true;
-      };
-
-      fonts.fonts = with pkgs; [
-        cantarell-fonts
-        dejavu_fonts
-        source-code-pro # Default monospace font in 3.32
-        source-sans-pro
-      ];
-
-      ## Enable soft realtime scheduling, only supported on wayland ##
-
-      security.wrappers.".gnome-shell-wrapped" = {
-        source = "${pkgs.gnome3.gnome-shell}/bin/.gnome-shell-wrapped";
-        capabilities = "cap_sys_nice=ep";
-      };
-
-      systemd.user.services.gnome-shell-wayland = let
-        gnomeShellRT = with pkgs.gnome3; pkgs.runCommand "gnome-shell-rt" {} ''
-          mkdir -p $out/bin/
-          cp ${gnome-shell}/bin/gnome-shell $out/bin
-          sed -i "s@${gnome-shell}/bin/@${config.security.wrapperDir}/@" $out/bin/gnome-shell
-        '';
-      in {
-        # Note we need to clear ExecStart before overriding it
-        serviceConfig.ExecStart = ["" "${gnomeShellRT}/bin/gnome-shell"];
-        # Do not use the default environment, it provides a broken PATH
-        environment = mkForce {};
-      };
-
-      # 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
-        gnome-bluetooth
-        gnome-color-manager
-        gnome-control-center
-        gnome-getting-started-docs
-        gnome-shell
-        gnome-shell-extensions
-        gnome-themes-extra
-        pkgs.nixos-artwork.wallpapers.simple-dark-gray
-        pkgs.nixos-artwork.wallpapers.simple-dark-gray-bottom
-        pkgs.gnome-user-docs
-        pkgs.orca
-        pkgs.glib # for gsettings
-        pkgs.gnome-menus
-        pkgs.gtk3.out # for gtk-launch
-        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/
-      ];
-    })
-
-    # 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
-        gedit
-        gnome-calculator
-        gnome-calendar
-        gnome-characters
-        gnome-clocks
-        gnome-contacts
-        gnome-font-viewer
-        gnome-logs
-        gnome-maps
-        gnome-music
-        gnome-photos
-        gnome-screenshot
-        gnome-software
-        gnome-system-monitor
-        gnome-weather
-        nautilus
-        simple-scan
-        totem
-        yelp
-        # Unsure if sensible for NixOS
-        /* gnome-boxes */
-      ] config.environment.gnome3.excludePackages);
-
-      # 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;
-      services.gnome3.sushi.enable = mkDefault true;
-
-      # Let nautilus find extensions
-      # TODO: Create nautilus-with-extensions package
-      environment.sessionVariables.NAUTILUS_EXTENSION_DIR = "${config.system.path}/lib/nautilus/extensions-3.0";
-
-      # Override default mimeapps for nautilus
-      environment.sessionVariables.XDG_DATA_DIRS = [ "${mimeAppsList}/share" ];
-
-      environment.pathsToLink = [
-        "/share/nautilus-python/extensions"
-      ];
-    })
-
-    (mkIf serviceCfg.games.enable {
-      environment.systemPackages = (with pkgs.gnome3; removePackagesByName [
-        aisleriot atomix five-or-more four-in-a-row gnome-chess gnome-klotski
-        gnome-mahjongg gnome-mines gnome-nibbles gnome-robots gnome-sudoku
-        gnome-taquin gnome-tetravex hitori iagno lightsoff quadrapassel
-        swell-foop tali
-      ] config.environment.gnome3.excludePackages);
-    })
-  ];
-
-}
diff --git a/nixos/modules/services/x11/desktop-managers/kodi.nix b/nixos/modules/services/x11/desktop-managers/kodi.nix
index bdae9c3afdb..af303d6fb27 100644
--- a/nixos/modules/services/x11/desktop-managers/kodi.nix
+++ b/nixos/modules/services/x11/desktop-managers/kodi.nix
@@ -14,6 +14,16 @@ in
         default = false;
         description = "Enable the kodi multimedia center.";
       };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.kodi;
+        defaultText = "pkgs.kodi";
+        example = "pkgs.kodi.withPackages (p: with p; [ jellyfin pvr-iptvsimple vfs-sftp ])";
+        description = ''
+          Package that should be used for Kodi.
+        '';
+      };
     };
   };
 
@@ -21,11 +31,11 @@ in
     services.xserver.desktopManager.session = [{
       name = "kodi";
       start = ''
-        LIRC_SOCKET_PATH=/run/lirc/lircd ${pkgs.kodi}/bin/kodi --standalone &
+        LIRC_SOCKET_PATH=/run/lirc/lircd ${cfg.package}/bin/kodi --standalone &
         waitPID=$!
       '';
     }];
 
-    environment.systemPackages = [ pkgs.kodi ];
+    environment.systemPackages = [ cfg.package ];
   };
 }
diff --git a/nixos/modules/services/x11/desktop-managers/lxqt.nix b/nixos/modules/services/x11/desktop-managers/lxqt.nix
index bf53082b267..71dfad5c7ca 100644
--- a/nixos/modules/services/x11/desktop-managers/lxqt.nix
+++ b/nixos/modules/services/x11/desktop-managers/lxqt.nix
@@ -51,15 +51,15 @@ in
     environment.systemPackages =
       pkgs.lxqt.preRequisitePackages ++
       pkgs.lxqt.corePackages ++
-      (pkgs.gnome3.removePackagesByName
+      (pkgs.gnome.removePackagesByName
         pkgs.lxqt.optionalPackages
         config.environment.lxqt.excludePackages);
 
     # Link some extra directories in /run/current-system/software/share
     environment.pathsToLink = [ "/share" ];
 
+    # virtual file systems support for PCManFM-QT
     services.gvfs.enable = true;
-    services.gvfs.package = pkgs.gvfs;
 
     services.upower.enable = config.powerManagement.enable;
   };
diff --git a/nixos/modules/services/x11/desktop-managers/mate.nix b/nixos/modules/services/x11/desktop-managers/mate.nix
index f236c14fcf3..19ab9edb732 100644
--- a/nixos/modules/services/x11/desktop-managers/mate.nix
+++ b/nixos/modules/services/x11/desktop-managers/mate.nix
@@ -76,7 +76,7 @@ in
 
     environment.systemPackages =
       pkgs.mate.basePackages ++
-      (pkgs.gnome3.removePackagesByName
+      (pkgs.gnome.removePackagesByName
         pkgs.mate.extraPackages
         config.environment.mate.excludePackages) ++
       [
@@ -97,8 +97,8 @@ in
     # Mate uses this for printing
     programs.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
 
-    services.gnome3.at-spi2-core.enable = true;
-    services.gnome3.gnome-keyring.enable = true;
+    services.gnome.at-spi2-core.enable = true;
+    services.gnome.gnome-keyring.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 6dabca6bf09..e492073b80f 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -42,7 +42,8 @@ in
 
       sessionPath = mkOption {
         default = [];
-        example = literalExample "[ pkgs.gnome3.gpaste ]";
+        type = types.listOf types.package;
+        example = literalExample "[ pkgs.gnome.gpaste ]";
         description = ''
           Additional list of packages to be added to the session search path.
           Useful for GSettings-conditional autostart.
@@ -141,12 +142,12 @@ in
       ];
       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.gnome.at-spi2-core.enable = true;
+      services.gnome.evolution-data-server.enable = true;
+      services.gnome.glib-networking.enable = true;
+      services.gnome.gnome-keyring.enable = true;
       services.gvfs.enable = true;
-      services.gnome3.rygel.enable = mkDefault true;
+      services.gnome.rygel.enable = mkDefault true;
       services.gsignond.enable = mkDefault true;
       services.gsignond.plugins = with pkgs.gsignondPlugins; [ lastfm mail oauth ];
       services.udisks2.enable = true;
@@ -176,11 +177,10 @@ in
         desktop-file-utils
         glib
         gnome-menus
-        gnome3.adwaita-icon-theme
+        gnome.adwaita-icon-theme
         gtk3.out
         hicolor-icon-theme
         lightlocker
-        nixos-artwork.wallpapers.simple-dark-gray
         onboard
         qgnomeplatform
         shared-mime-info
@@ -213,10 +213,10 @@ in
         elementary-settings-daemon
         pantheon-agent-geoclue2
         pantheon-agent-polkit
-      ]) ++ (gnome3.removePackagesByName [
-        gnome3.geary
-        gnome3.epiphany
-        gnome3.gnome-font-viewer
+      ]) ++ (gnome.removePackagesByName [
+        gnome.geary
+        gnome.epiphany
+        gnome.gnome-font-viewer
       ] config.environment.pantheon.excludePackages);
 
       programs.evince.enable = mkDefault true;
@@ -240,6 +240,8 @@ in
       # Otherwise you can't store NetworkManager Secrets with
       # "Store the password only for this user"
       programs.nm-applet.enable = true;
+      # Pantheon has its own network indicator
+      programs.nm-applet.indicator = false;
 
       # Shell integration for VTE terminals
       programs.bash.vteIntegration = mkDefault true;
@@ -263,7 +265,7 @@ in
     })
 
     (mkIf serviceCfg.apps.enable {
-      environment.systemPackages = (with pkgs.pantheon; pkgs.gnome3.removePackagesByName [
+      environment.systemPackages = (with pkgs.pantheon; pkgs.gnome.removePackagesByName [
         elementary-calculator
         elementary-calendar
         elementary-camera
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 75bf55a2639..b6be524aea6 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -7,7 +7,8 @@ let
   xcfg = config.services.xserver;
   cfg = xcfg.desktopManager.plasma5;
 
-  inherit (pkgs) kdeApplications plasma5 libsForQt5 qt5;
+  libsForQt5 = pkgs.plasma5Packages;
+  inherit (libsForQt5) kdeGear kdeFrameworks plasma5;
   inherit (pkgs) writeText;
 
   pulseaudio = config.hardware.pulseaudio;
@@ -83,7 +84,7 @@ let
     # recognize that software that has been removed.
     rm -fv $HOME/.cache/ksycoca*
 
-    ${pkgs.libsForQt5.kservice}/bin/kbuildsycoca5
+    ${libsForQt5.kservice}/bin/kbuildsycoca5
   '';
 
   set_XDG_CONFIG_HOME = ''
@@ -182,6 +183,13 @@ in
 
   config = mkMerge [
     (mkIf cfg.enable {
+      # Seed our configuration into nixos-generate-config
+      system.nixos-generate-config.desktopConfiguration = [''
+        # Enable the Plasma 5 Desktop Environment.
+        services.xserver.displayManager.sddm.enable = true;
+        services.xserver.desktopManager.plasma5.enable = true;
+      ''];
+
       services.xserver.desktopManager.session = singleton {
         name = "plasma5";
         bgSupport = true;
@@ -189,8 +197,8 @@ in
       };
 
       security.wrappers = {
-        kcheckpass.source = "${lib.getBin plasma5.kscreenlocker}/libexec/kcheckpass";
-        start_kdeinit.source = "${lib.getBin pkgs.kinit}/libexec/kf5/start_kdeinit";
+        kcheckpass.source = "${lib.getBin libsForQt5.kscreenlocker}/libexec/kcheckpass";
+        start_kdeinit.source = "${lib.getBin libsForQt5.kinit}/libexec/kf5/start_kdeinit";
         kwin_wayland = {
           source = "${lib.getBin plasma5.kwin}/bin/kwin_wayland";
           capabilities = "cap_sys_nice+ep";
@@ -203,7 +211,9 @@ in
         KERNEL=="i2c-[0-9]*", TAG+="uaccess"
       '';
 
-      environment.systemPackages = with pkgs; with qt5; with libsForQt5; with plasma5; with kdeApplications;
+      environment.systemPackages =
+        with libsForQt5;
+        with plasma5; with kdeGear; with kdeFrameworks;
         [
           frameworkintegration
           kactivities
@@ -226,6 +236,7 @@ in
           kidletime
           kimageformats
           kinit
+          kirigami2  # In system profile for SDDM theme. TODO: wrapper.
           kio
           kjobwidgets
           knewstuff
@@ -270,6 +281,7 @@ in
           plasma-browser-integration
           plasma-integration
           polkit-kde-agent
+          spectacle
           systemsettings
 
           plasma-desktop
@@ -293,7 +305,7 @@ in
 
           qtvirtualkeyboard
 
-          xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
+          pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
         ]
 
         # Phonon audio backend
@@ -301,13 +313,14 @@ in
         ++ lib.optional (cfg.phononBackend == "vlc") libsForQt5.phonon-backend-vlc
 
         # Optional hardware support features
-        ++ lib.optionals config.hardware.bluetooth.enable [ bluedevil bluez-qt openobex obexftp ]
+        ++ lib.optionals config.hardware.bluetooth.enable [ bluedevil bluez-qt pkgs.openobex pkgs.obexftp ]
         ++ lib.optional config.networking.networkmanager.enable plasma-nm
         ++ lib.optional config.hardware.pulseaudio.enable plasma-pa
+        ++ lib.optional config.services.pipewire.pulse.enable plasma-pa
         ++ lib.optional config.powerManagement.enable powerdevil
-        ++ lib.optional config.services.colord.enable colord-kde
+        ++ lib.optional config.services.colord.enable pkgs.colord-kde
         ++ lib.optionals config.services.samba.enable [ kdenetwork-filesharing pkgs.samba ]
-        ++ lib.optional config.services.xserver.wacom.enable wacomtablet;
+        ++ lib.optional config.services.xserver.wacom.enable pkgs.wacomtablet;
 
       environment.pathsToLink = [
         # FIXME: modules should link subdirs of `/share` rather than relying on this
@@ -354,10 +367,12 @@ in
       security.pam.services.sddm.enableKwallet = true;
 
       xdg.portal.enable = true;
-      xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-kde ];
+      xdg.portal.extraPortals = [ plasma5.xdg-desktop-portal-kde ];
 
       # Update the start menu for each user that is currently logged in
       system.userActivationScripts.plasmaSetup = activationScript;
+
+      nixpkgs.config.firefox.enablePlasmaBrowserIntegration = true;
     })
   ];
 
diff --git a/nixos/modules/services/x11/desktop-managers/xfce.nix b/nixos/modules/services/x11/desktop-managers/xfce.nix
index d39b4d64904..bbfdea2225b 100644
--- a/nixos/modules/services/x11/desktop-managers/xfce.nix
+++ b/nixos/modules/services/x11/desktop-managers/xfce.nix
@@ -9,7 +9,7 @@ in
 {
 
   meta = {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 
   imports = [
@@ -58,7 +58,7 @@ in
       noDesktop = mkOption {
         type = types.bool;
         default = false;
-        description = "Don't install XFCE desktop components (xfdesktop, panel and notification daemon).";
+        description = "Don't install XFCE desktop components (xfdesktop and panel).";
       };
 
       enableXfwm = mkOption {
@@ -74,8 +74,8 @@ in
       glib # for gsettings
       gtk3.out # gtk-update-icon-cache
 
-      gnome3.gnome-themes-extra
-      gnome3.adwaita-icon-theme
+      gnome.gnome-themes-extra
+      gnome.adwaita-icon-theme
       hicolor-icon-theme
       tango-icon-theme
       xfce4-icon-theme
@@ -98,6 +98,7 @@ in
       parole
       ristretto
       xfce4-appfinder
+      xfce4-notifyd
       xfce4-screenshooter
       xfce4-session
       xfce4-settings
@@ -119,7 +120,6 @@ in
         xfwm4
         xfwm4-themes
       ] ++ optionals (!cfg.noDesktop) [
-        xfce4-notifyd
         xfce4-panel
         xfdesktop
       ];
@@ -149,9 +149,8 @@ in
     security.polkit.enable = true;
     services.accounts-daemon.enable = true;
     services.upower.enable = config.powerManagement.enable;
-    services.gnome3.glib-networking.enable = true;
+    services.gnome.glib-networking.enable = true;
     services.gvfs.enable = true;
-    services.gvfs.package = pkgs.xfce.gvfs;
     services.tumbler.enable = true;
     services.system-config-printer.enable = (mkIf config.services.printing.enable (mkDefault true));
     services.xserver.libinput.enable = mkDefault true; # used in xfce4-settings-manager
@@ -166,7 +165,8 @@ in
     # Systemd services
     systemd.packages = with pkgs.xfce; [
       (thunar.override { thunarPlugins = cfg.thunarPlugins; })
-    ] ++ optional (!cfg.noDesktop) xfce4-notifyd;
+      xfce4-notifyd
+    ];
 
   };
 }
diff --git a/nixos/modules/services/x11/display-managers/account-service-util.nix b/nixos/modules/services/x11/display-managers/account-service-util.nix
index 2b08c62d0ad..dec5c06cb3c 100644
--- a/nixos/modules/services/x11/display-managers/account-service-util.nix
+++ b/nixos/modules/services/x11/display-managers/account-service-util.nix
@@ -39,6 +39,6 @@ python3.pkgs.buildPythonApplication {
   '';
 
   meta = with lib; {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 }
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index b8b36aa0532..e04fcdaf414 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -37,12 +37,10 @@ let
       . /etc/profile
       cd "$HOME"
 
-      ${optionalString cfg.startDbusSession ''
-        if test -z "$DBUS_SESSION_BUS_ADDRESS"; then
-          /run/current-system/systemd/bin/systemctl --user start dbus.socket
-          export `/run/current-system/systemd/bin/systemctl --user show-environment | grep '^DBUS_SESSION_BUS_ADDRESS'`
-        fi
-      ''}
+      # Allow the user to execute commands at the beginning of the X session.
+      if test -f ~/.xprofile; then
+          source ~/.xprofile
+      fi
 
       ${optionalString cfg.displayManager.job.logToJournal ''
         if [ -z "$_DID_SYSTEMD_CAT" ]; then
@@ -55,13 +53,6 @@ let
         exec &> >(tee ~/.xsession-errors)
       ''}
 
-      # Tell systemd about our $DISPLAY and $XAUTHORITY.
-      # This is needed by the ssh-agent unit.
-      #
-      # Also tell systemd about the dbus session bus address.
-      # This is required by user units using the session bus.
-      /run/current-system/systemd/bin/systemctl --user import-environment DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS
-
       # Load X defaults. This should probably be safe on wayland too.
       ${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
       if test -e ~/.Xresources; then
@@ -70,24 +61,31 @@ let
           ${xorg.xrdb}/bin/xrdb -merge ~/.Xdefaults
       fi
 
+      # Import environment variables into the systemd user environment.
+      ${optionalString (cfg.displayManager.importedVariables != []) (
+        "/run/current-system/systemd/bin/systemctl --user import-environment "
+          + toString (unique cfg.displayManager.importedVariables)
+      )}
+
       # Speed up application start by 50-150ms according to
       # http://kdemonkey.blogspot.nl/2008/04/magic-trick.html
-      rm -rf "$HOME/.compose-cache"
-      mkdir "$HOME/.compose-cache"
+      compose_cache="''${XCOMPOSECACHE:-$HOME/.compose-cache}"
+      mkdir -p "$compose_cache"
+      # To avoid accidentally deleting a wrongly set up XCOMPOSECACHE directory,
+      # defensively try to delete cache *files* only, following the file format specified in
+      # https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/modules/im/ximcp/imLcIm.c#L353-358
+      # sprintf (*res, "%s/%c%d_%03x_%08x_%08x", dir, _XimGetMyEndian(), XIM_CACHE_VERSION, (unsigned int)sizeof (DefTree), hash, hash2);
+      ${pkgs.findutils}/bin/find "$compose_cache" -maxdepth 1 -regextype posix-extended -regex '.*/[Bl][0-9]+_[0-9a-f]{3}_[0-9a-f]{8}_[0-9a-f]{8}' -delete
+      unset compose_cache
 
       # Work around KDE errors when a user first logs in and
       # .local/share doesn't exist yet.
-      mkdir -p "$HOME/.local/share"
+      mkdir -p "''${XDG_DATA_HOME:-$HOME/.local/share}"
 
       unset _DID_SYSTEMD_CAT
 
       ${cfg.displayManager.sessionCommands}
 
-      # Allow the user to execute commands at the beginning of the X session.
-      if test -f ~/.xprofile; then
-          source ~/.xprofile
-      fi
-
       # Start systemd user services for graphical sessions
       /run/current-system/systemd/bin/systemctl --user start graphical-session.target
 
@@ -289,6 +287,14 @@ in
         '';
       };
 
+      importedVariables = mkOption {
+        type = types.listOf (types.strMatching "[a-zA-Z_][a-zA-Z0-9_]*");
+        visible = false;
+        description = ''
+          Environment variables to import into the systemd user environment.
+        '';
+      };
+
       job = {
 
         preStart = mkOption {
@@ -393,6 +399,16 @@ in
 
     services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
 
+    services.xserver.displayManager.importedVariables = [
+      # This is required by user units using the session bus.
+      "DBUS_SESSION_BUS_ADDRESS"
+      # These are needed by the ssh-agent unit.
+      "DISPLAY"
+      "XAUTHORITY"
+      # This is required to specify session within user units (e.g. loginctl lock-session).
+      "XDG_SESSION_ID"
+    ];
+
     systemd.user.targets.graphical-session = {
       unitConfig = {
         RefuseManualStart = false;
@@ -434,8 +450,8 @@ in
       in
         # We will generate every possible pair of WM and DM.
         concatLists (
-          crossLists
-            (dm: wm: let
+            builtins.map
+            ({dm, wm}: let
               sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
               script = xsession dm wm;
               desktopNames = if dm ? desktopNames
@@ -462,8 +478,14 @@ in
                   providedSessions = [ sessionName ];
                 })
             )
-            [dms wms]
+            (cartesianProductOfSets { dm = dms; wm = wms; })
           );
+
+    # Make xsessions and wayland sessions available in XDG_DATA_DIRS
+    # as some programs have behavior that depends on them being present
+    environment.sessionVariables.XDG_DATA_DIRS = [
+      "${cfg.displayManager.sessionData.desktops}/share"
+    ];
   };
 
   imports = [
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 23ab7f2ae43..ef9ec438cc1 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -5,7 +5,7 @@ with lib;
 let
 
   cfg = config.services.xserver.displayManager;
-  gdm = pkgs.gnome3.gdm;
+  gdm = pkgs.gnome.gdm;
 
   xSessionWrapper = if (cfg.setupCommands == "") then null else
     pkgs.writeScript "gdm-x-session-wrapper" ''
@@ -64,13 +64,9 @@ in
 
     services.xserver.displayManager.gdm = {
 
-      enable = mkEnableOption ''
-        GDM, the GNOME Display Manager
-      '';
+      enable = mkEnableOption "GDM, the GNOME Display Manager";
 
-      debug = mkEnableOption ''
-        debugging messages in GDM
-      '';
+      debug = mkEnableOption "debugging messages in GDM";
 
       # Auto login options specific to GDM
       autoLogin.delay = mkOption {
@@ -103,7 +99,8 @@ in
       autoSuspend = mkOption {
         default = true;
         description = ''
-          Suspend the machine after inactivity.
+          On the GNOME Display Manager login screen, suspend the machine after inactivity.
+          (Does not affect automatic suspend while logged in, or at lock screen.)
         '';
         type = types.bool;
       };
@@ -158,14 +155,14 @@ in
     ] ++ 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 [
+    ] ++ optionals config.services.gnome.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.packages = with pkgs.gnome; [ gdm gnome-session gnome-shell ];
+    environment.systemPackages = [ pkgs.gnome.adwaita-icon-theme ];
 
     systemd.services.display-manager.wants = [
       # Because sd_login_monitor_new requires /run/systemd/machines
@@ -187,14 +184,20 @@ in
       "systemd-udev-settle.service"
     ];
     systemd.services.display-manager.conflicts = [
-       "getty@tty${gdm.initialVT}.service"
-       # TODO: Add "plymouth-quit.service" so GDM can control when plymouth quits.
-       # Currently this breaks switching configurations while using plymouth.
+      "getty@tty${gdm.initialVT}.service"
+      "plymouth-quit.service"
     ];
     systemd.services.display-manager.onFailure = [
       "plymouth-quit.service"
     ];
 
+    # Prevent nixos-rebuild switch from bringing down the graphical
+    # session. (If multi-user.target wants plymouth-quit.service which
+    # conflicts display-manager.service, then when nixos-rebuild
+    # switch starts multi-user.target, display-manager.service is
+    # stopped so plymouth-quit.service can be started.)
+    systemd.services.plymouth-quit.wantedBy = lib.mkForce [];
+
     systemd.services.display-manager.serviceConfig = {
       # Restart = "always"; - already defined in xserver.nix
       KillMode = "mixed";
@@ -206,7 +209,7 @@ in
       EnvironmentFile = "-/etc/locale.conf";
     };
 
-    systemd.services.display-manager.path = [ pkgs.gnome3.gnome-session ];
+    systemd.services.display-manager.path = [ pkgs.gnome.gnome-session ];
 
     # Allow choosing an user account
     services.accounts-daemon.enable = true;
@@ -216,14 +219,14 @@ in
     # We duplicate upstream's udev rules manually to make wayland with nvidia configurable
     services.udev.extraRules = ''
       # disable Wayland on Cirrus chipsets
-      ATTR{vendor}=="0x1013", ATTR{device}=="0x00b8", ATTR{subsystem_vendor}=="0x1af4", ATTR{subsystem_device}=="0x1100", RUN+="${gdm}/libexec/gdm-disable-wayland"
+      ATTR{vendor}=="0x1013", ATTR{device}=="0x00b8", ATTR{subsystem_vendor}=="0x1af4", ATTR{subsystem_device}=="0x1100", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
       # disable Wayland on Hi1710 chipsets
-      ATTR{vendor}=="0x19e5", ATTR{device}=="0x1711", RUN+="${gdm}/libexec/gdm-disable-wayland"
+      ATTR{vendor}=="0x19e5", ATTR{device}=="0x1711", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
       ${optionalString (!cfg.gdm.nvidiaWayland) ''
-        DRIVER=="nvidia", RUN+="${gdm}/libexec/gdm-disable-wayland"
+        DRIVER=="nvidia", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
       ''}
       # disable Wayland when modesetting is disabled
-      IMPORT{cmdline}="nomodeset", RUN+="${gdm}/libexec/gdm-disable-wayland"
+      IMPORT{cmdline}="nomodeset", RUN+="${gdm}/libexec/gdm-runtime-config set daemon WaylandEnable false"
     '';
 
     systemd.user.services.dbus.wantedBy = [ "default.target" ];
@@ -268,7 +271,7 @@ in
     # presented and there's a little delay.
     environment.etc."gdm/custom.conf".text = ''
       [daemon]
-      WaylandEnable=${if cfg.gdm.wayland then "true" else "false"}
+      WaylandEnable=${boolToString cfg.gdm.wayland}
       ${optionalString cfg.autoLogin.enable (
         if cfg.gdm.autoLogin.delay > 0 then ''
           TimedLoginEnable=true
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
index 129df139c61..ecd46a9ee6d 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/enso-os.nix
@@ -34,8 +34,8 @@ in {
       theme = {
         package = mkOption {
           type = types.package;
-          default = pkgs.gnome3.gnome-themes-extra;
-          defaultText = "pkgs.gnome3.gnome-themes-extra";
+          default = pkgs.gnome.gnome-themes-extra;
+          defaultText = "pkgs.gnome.gnome-themes-extra";
           description = ''
             The package path that contains the theme given in the name option.
           '';
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
index de932e6e840..fe5a16bc60f 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
@@ -47,8 +47,8 @@ in
 
         package = mkOption {
           type = types.package;
-          default = pkgs.gnome3.gnome-themes-extra;
-          defaultText = "pkgs.gnome3.gnome-themes-extra";
+          default = pkgs.gnome.gnome-themes-extra;
+          defaultText = "pkgs.gnome.gnome-themes-extra";
           description = ''
             The package path that contains the theme given in the name option.
           '';
@@ -68,8 +68,8 @@ in
 
         package = mkOption {
           type = types.package;
-          default = pkgs.gnome3.adwaita-icon-theme;
-          defaultText = "pkgs.gnome3.adwaita-icon-theme";
+          default = pkgs.gnome.adwaita-icon-theme;
+          defaultText = "pkgs.gnome.adwaita-icon-theme";
           description = ''
             The package path that contains the icon theme given in the name option.
           '';
@@ -88,8 +88,9 @@ in
       cursorTheme = {
 
         package = mkOption {
-          default = pkgs.gnome3.adwaita-icon-theme;
-          defaultText = "pkgs.gnome3.adwaita-icon-theme";
+          type = types.package;
+          default = pkgs.gnome.adwaita-icon-theme;
+          defaultText = "pkgs.gnome.adwaita-icon-theme";
           description = ''
             The package path that contains the cursor theme given in the name option.
           '';
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 9bc9e2bf616..76f16646cf5 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
@@ -11,7 +11,7 @@ let
 in
 {
   meta = {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 
   options = {
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 143785db0b4..3d497c9f25e 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -70,7 +70,7 @@ let
 in
 {
   meta = {
-    maintainers = with maintainers; [ worldofpeace ];
+    maintainers = with maintainers; [ ];
   };
 
   # Note: the order in which lightdm greeter modules are imported
@@ -308,6 +308,7 @@ in
       home = "/var/lib/lightdm";
       group = "lightdm";
       uid = config.ids.uids.lightdm;
+      shell = pkgs.bash;
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index e63bb2e4453..116994db1c1 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -1,97 +1,94 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   xcfg = config.services.xserver;
   dmcfg = xcfg.displayManager;
   cfg = dmcfg.sddm;
   xEnv = config.systemd.services.display-manager.environment;
 
-  inherit (pkgs) sddm;
+  sddm = pkgs.libsForQt5.sddm;
+
+  iniFmt = pkgs.formats.ini { };
 
-  xserverWrapper = pkgs.writeScript "xserver-wrapper" ''
-    #!/bin/sh
+  xserverWrapper = pkgs.writeShellScript "xserver-wrapper" ''
     ${concatMapStrings (n: "export ${n}=\"${getAttr n xEnv}\"\n") (attrNames xEnv)}
     exec systemd-cat -t xserver-wrapper ${dmcfg.xserverBin} ${toString dmcfg.xserverArgs} "$@"
   '';
 
-  Xsetup = pkgs.writeScript "Xsetup" ''
-    #!/bin/sh
+  Xsetup = pkgs.writeShellScript "Xsetup" ''
     ${cfg.setupScript}
     ${dmcfg.setupCommands}
   '';
 
-  Xstop = pkgs.writeScript "Xstop" ''
-    #!/bin/sh
+  Xstop = pkgs.writeShellScript "Xstop" ''
     ${cfg.stopScript}
   '';
 
-  cfgFile = pkgs.writeText "sddm.conf" ''
-    [General]
-    HaltCommand=/run/current-system/systemd/bin/systemctl poweroff
-    RebootCommand=/run/current-system/systemd/bin/systemctl reboot
-    ${optionalString cfg.autoNumlock ''
-    Numlock=on
-    ''}
-
-    [Theme]
-    Current=${cfg.theme}
-    ThemeDir=/run/current-system/sw/share/sddm/themes
-    FacesDir=/run/current-system/sw/share/sddm/faces
-
-    [Users]
-    MaximumUid=${toString config.ids.uids.nixbld}
-    HideUsers=${concatStringsSep "," dmcfg.hiddenUsers}
-    HideShells=/run/current-system/sw/bin/nologin
-
-    [X11]
-    MinimumVT=${toString (if xcfg.tty != null then xcfg.tty else 7)}
-    ServerPath=${xserverWrapper}
-    XephyrPath=${pkgs.xorg.xorgserver.out}/bin/Xephyr
-    SessionCommand=${dmcfg.sessionData.wrapper}
-    SessionDir=${dmcfg.sessionData.desktops}/share/xsessions
-    XauthPath=${pkgs.xorg.xauth}/bin/xauth
-    DisplayCommand=${Xsetup}
-    DisplayStopCommand=${Xstop}
-    EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
-
-    [Wayland]
-    EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
-    SessionDir=${dmcfg.sessionData.desktops}/share/wayland-sessions
-
-    ${optionalString dmcfg.autoLogin.enable ''
-    [Autologin]
-    User=${dmcfg.autoLogin.user}
-    Session=${autoLoginSessionName}.desktop
-    Relogin=${boolToString cfg.autoLogin.relogin}
-    ''}
-
-    ${cfg.extraConfig}
-  '';
+  defaultConfig = {
+    General = {
+      HaltCommand = "/run/current-system/systemd/bin/systemctl poweroff";
+      RebootCommand = "/run/current-system/systemd/bin/systemctl reboot";
+      Numlock = if cfg.autoNumlock then "on" else "none"; # on, off none
+    };
+
+    Theme = {
+      Current = cfg.theme;
+      ThemeDir = "/run/current-system/sw/share/sddm/themes";
+      FacesDir = "/run/current-system/sw/share/sddm/faces";
+    };
+
+    Users = {
+      MaximumUid = config.ids.uids.nixbld;
+      HideUsers = concatStringsSep "," dmcfg.hiddenUsers;
+      HideShells = "/run/current-system/sw/bin/nologin";
+    };
 
-  autoLoginSessionName = dmcfg.sessionData.autologinSession;
+    X11 = {
+      MinimumVT = if xcfg.tty != null then xcfg.tty else 7;
+      ServerPath = toString xserverWrapper;
+      XephyrPath = "${pkgs.xorg.xorgserver.out}/bin/Xephyr";
+      SessionCommand = toString dmcfg.sessionData.wrapper;
+      SessionDir = "${dmcfg.sessionData.desktops}/share/xsessions";
+      XauthPath = "${pkgs.xorg.xauth}/bin/xauth";
+      DisplayCommand = toString Xsetup;
+      DisplayStopCommand = toString Xstop;
+      EnableHiDPI = cfg.enableHidpi;
+    };
+
+    Wayland = {
+      EnableHiDPI = cfg.enableHidpi;
+      SessionDir = "${dmcfg.sessionData.desktops}/share/wayland-sessions";
+    };
+  } // lib.optionalAttrs dmcfg.autoLogin.enable {
+    Autologin = {
+      User = dmcfg.autoLogin.user;
+      Session = autoLoginSessionName;
+      Relogin = cfg.autoLogin.relogin;
+    };
+  };
+
+  cfgFile =
+    iniFmt.generate "sddm.conf" (lib.recursiveUpdate defaultConfig cfg.settings);
+
+  autoLoginSessionName =
+    "${dmcfg.sessionData.autologinSession}.desktop";
 
 in
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "xserver" "displayManager" "sddm" "themes" ]
+    (mkRemovedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "themes" ]
       "Set the option `services.xserver.displayManager.sddm.package' instead.")
-    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "sddm" "autoLogin" "enable" ] [
-      "services"
-      "xserver"
-      "displayManager"
-      "autoLogin"
-      "enable"
-    ])
-    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "sddm" "autoLogin" "user" ] [
-      "services"
-      "xserver"
-      "displayManager"
-      "autoLogin"
-      "user"
-    ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "autoLogin" "enable" ]
+      [ "services" "xserver" "displayManager" "autoLogin" "enable" ])
+    (mkRenamedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "autoLogin" "user" ]
+      [ "services" "xserver" "displayManager" "autoLogin" "user" ])
+    (mkRemovedOptionModule
+      [ "services" "xserver" "displayManager" "sddm" "extraConfig" ]
+      "Set the option `services.xserver.displayManager.sddm.settings' instead.")
   ];
 
   options = {
@@ -110,22 +107,22 @@ in
         default = true;
         description = ''
           Whether to enable automatic HiDPI mode.
-          </para>
-          <para>
-          Versions up to 0.17 are broken so this only works from 0.18 onwards.
         '';
       };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
+      settings = mkOption {
+        type = iniFmt.type;
+        default = { };
         example = ''
-          [Autologin]
-          User=john
-          Session=plasma.desktop
+          {
+            Autologin = {
+              User = "john";
+              Session = "plasma.desktop";
+            };
+          }
         '';
         description = ''
-          Extra lines appended to the configuration of SDDM.
+          Extra settings merged in and overwritting defaults in sddm.conf.
         '';
       };
 
@@ -168,28 +165,38 @@ in
       };
 
       # Configuration for automatic login specific to SDDM
-      autoLogin.relogin = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          If true automatic login will kick in again on session exit (logout), otherwise it
-          will only log in automatically when the display-manager is started.
-        '';
+      autoLogin = {
+        relogin = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            If true automatic login will kick in again on session exit (logout), otherwise it
+            will only log in automatically when the display-manager is started.
+          '';
+        };
+
+        minimumUid = mkOption {
+          type = types.ints.u16;
+          default = 1000;
+          description = ''
+            Minimum user ID for auto-login user.
+          '';
+        };
       };
-
     };
-
   };
 
   config = mkIf cfg.enable {
 
     assertions = [
-      { assertion = xcfg.enable;
+      {
+        assertion = xcfg.enable;
         message = ''
           SDDM requires services.xserver.enable to be true
         '';
       }
-      { assertion = dmcfg.autoLogin.enable -> autoLoginSessionName != null;
+      {
+        assertion = dmcfg.autoLogin.enable -> autoLoginSessionName != null;
         message = ''
           SDDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
@@ -230,7 +237,7 @@ in
 
       sddm-autologin.text = ''
         auth     requisite pam_nologin.so
-        auth     required  pam_succeed_if.so uid >= 1000 quiet
+        auth     required  pam_succeed_if.so uid >= ${toString cfg.autoLogin.minimumUid} quiet
         auth     required  pam_permit.so
 
         account  include   sddm
diff --git a/nixos/modules/services/x11/display-managers/startx.nix b/nixos/modules/services/x11/display-managers/startx.nix
index 3980203b945..6cd46cdf964 100644
--- a/nixos/modules/services/x11/display-managers/startx.nix
+++ b/nixos/modules/services/x11/display-managers/startx.nix
@@ -39,6 +39,18 @@ in
       displayManager.lightdm.enable = lib.mkForce false;
     };
     systemd.services.display-manager.enable = false;
+
+    # Other displayManagers log to /dev/null because they're services and put
+    # Xorg's stdout in the journal
+    #
+    # To send log to Xorg's default log location ($XDG_DATA_HOME/xorg/), we do
+    # not specify a log file when running X
+    services.xserver.logFile = mkDefault null;
+
+    # Implement xserverArgs via xinit's system-wide xserverrc
+    environment.etc."X11/xinit/xserverrc".source = pkgs.writeShellScript "xserverrc" ''
+      exec ${pkgs.xorg.xorgserver}/bin/X ${toString config.services.xserver.displayManager.xserverArgs} "$@"
+    '';
     environment.systemPackages =  with pkgs; [ xorg.xinit ];
   };
 
diff --git a/nixos/modules/services/x11/hardware/libinput.nix b/nixos/modules/services/x11/hardware/libinput.nix
index 9548ecb8ef6..439708bc47e 100644
--- a/nixos/modules/services/x11/hardware/libinput.nix
+++ b/nixos/modules/services/x11/hardware/libinput.nix
@@ -3,23 +3,18 @@
 with lib;
 
 let cfg = config.services.xserver.libinput;
-    xorgBool = v: if v then "on" else "off";
-in {
 
-  options = {
-
-    services.xserver.libinput = {
-
-      enable = mkEnableOption "libinput";
+    xorgBool = v: if v then "on" else "off";
 
+    mkConfigForDevice = deviceType: {
       dev = mkOption {
         type = types.nullOr types.str;
         default = null;
         example = "/dev/input/event0";
         description =
           ''
-            Path for touchpad device.  Set to null to apply to any
-            auto-detected touchpad.
+            Path for ${deviceType} device.  Set to null to apply to any
+            auto-detected ${deviceType}.
           '';
       };
 
@@ -185,14 +180,64 @@ in {
           Option "DragLockButtons" "L1 B1 L2 B2"
         '';
         description = ''
-          Additional options for libinput touchpad driver. See
+          Additional options for libinput ${deviceType} driver. See
           <citerefentry><refentrytitle>libinput</refentrytitle><manvolnum>4</manvolnum></citerefentry>
           for available options.";
         '';
       };
-
     };
 
+    mkX11ConfigForDevice = deviceType: matchIs: ''
+      Identifier "libinput ${deviceType} configuration"
+      MatchDriver "libinput"
+      MatchIs${matchIs} "${xorgBool true}"
+      ${optionalString (cfg.${deviceType}.dev != null) ''MatchDevicePath "${cfg.${deviceType}.dev}"''}
+      Option "AccelProfile" "${cfg.${deviceType}.accelProfile}"
+      ${optionalString (cfg.${deviceType}.accelSpeed != null) ''Option "AccelSpeed" "${cfg.${deviceType}.accelSpeed}"''}
+      ${optionalString (cfg.${deviceType}.buttonMapping != null) ''Option "ButtonMapping" "${cfg.${deviceType}.buttonMapping}"''}
+      ${optionalString (cfg.${deviceType}.calibrationMatrix != null) ''Option "CalibrationMatrix" "${cfg.${deviceType}.calibrationMatrix}"''}
+      ${optionalString (cfg.${deviceType}.clickMethod != null) ''Option "ClickMethod" "${cfg.${deviceType}.clickMethod}"''}
+      Option "LeftHanded" "${xorgBool cfg.${deviceType}.leftHanded}"
+      Option "MiddleEmulation" "${xorgBool cfg.${deviceType}.middleEmulation}"
+      Option "NaturalScrolling" "${xorgBool cfg.${deviceType}.naturalScrolling}"
+      ${optionalString (cfg.${deviceType}.scrollButton != null) ''Option "ScrollButton" "${toString cfg.${deviceType}.scrollButton}"''}
+      Option "ScrollMethod" "${cfg.${deviceType}.scrollMethod}"
+      Option "HorizontalScrolling" "${xorgBool cfg.${deviceType}.horizontalScrolling}"
+      Option "SendEventsMode" "${cfg.${deviceType}.sendEventsMode}"
+      Option "Tapping" "${xorgBool cfg.${deviceType}.tapping}"
+      Option "TappingDragLock" "${xorgBool cfg.${deviceType}.tappingDragLock}"
+      Option "DisableWhileTyping" "${xorgBool cfg.${deviceType}.disableWhileTyping}"
+      ${cfg.${deviceType}.additionalOptions}
+    '';
+in {
+
+  imports =
+    (map (option: mkRenamedOptionModule ([ "services" "xserver" "libinput" option ]) [ "services" "xserver" "libinput" "touchpad" option ]) [
+      "accelProfile"
+      "accelSpeed"
+      "buttonMapping"
+      "calibrationMatrix"
+      "clickMethod"
+      "leftHanded"
+      "middleEmulation"
+      "naturalScrolling"
+      "scrollButton"
+      "scrollMethod"
+      "horizontalScrolling"
+      "sendEventsMode"
+      "tapping"
+      "tappingDragLock"
+      "disableWhileTyping"
+      "additionalOptions"
+    ]);
+
+  options = {
+
+    services.xserver.libinput = {
+      enable = mkEnableOption "libinput";
+      mouse = mkConfigForDevice "mouse";
+      touchpad = mkConfigForDevice "touchpad";
+    };
   };
 
 
@@ -212,32 +257,10 @@ in {
 
     services.udev.packages = [ pkgs.libinput.out ];
 
-    services.xserver.config =
-      ''
-        # General libinput configuration.
-        # See CONFIGURATION DETAILS section of man:libinput(4).
-        Section "InputClass"
-          Identifier "libinputConfiguration"
-          MatchDriver "libinput"
-          ${optionalString (cfg.dev != null) ''MatchDevicePath "${cfg.dev}"''}
-          Option "AccelProfile" "${cfg.accelProfile}"
-          ${optionalString (cfg.accelSpeed != null) ''Option "AccelSpeed" "${cfg.accelSpeed}"''}
-          ${optionalString (cfg.buttonMapping != null) ''Option "ButtonMapping" "${cfg.buttonMapping}"''}
-          ${optionalString (cfg.calibrationMatrix != null) ''Option "CalibrationMatrix" "${cfg.calibrationMatrix}"''}
-          ${optionalString (cfg.clickMethod != null) ''Option "ClickMethod" "${cfg.clickMethod}"''}
-          Option "LeftHanded" "${xorgBool cfg.leftHanded}"
-          Option "MiddleEmulation" "${xorgBool cfg.middleEmulation}"
-          Option "NaturalScrolling" "${xorgBool cfg.naturalScrolling}"
-          ${optionalString (cfg.scrollButton != null) ''Option "ScrollButton" "${toString cfg.scrollButton}"''}
-          Option "ScrollMethod" "${cfg.scrollMethod}"
-          Option "HorizontalScrolling" "${xorgBool cfg.horizontalScrolling}"
-          Option "SendEventsMode" "${cfg.sendEventsMode}"
-          Option "Tapping" "${xorgBool cfg.tapping}"
-          Option "TappingDragLock" "${xorgBool cfg.tappingDragLock}"
-          Option "DisableWhileTyping" "${xorgBool cfg.disableWhileTyping}"
-          ${cfg.additionalOptions}
-        EndSection
-      '';
+    services.xserver.inputClassSections = [
+      (mkX11ConfigForDevice "mouse" "Pointer")
+      (mkX11ConfigForDevice "touchpad" "Touchpad")
+    ];
 
     assertions = [
       # already present in synaptics.nix
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index 1289edd2904..977d0fea219 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -57,7 +57,15 @@ in {
       type = types.bool;
       default = false;
       description = ''
-        Whether of not to enable Picom as the X.org composite manager.
+        Whether or not to enable Picom as the X.org composite manager.
+      '';
+    };
+
+    experimentalBackends = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to use the unstable new reimplementation of the backends.
       '';
     };
 
@@ -302,7 +310,8 @@ in {
       };
 
       serviceConfig = {
-        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}";
+        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}"
+          + (optionalString cfg.experimentalBackends " --experimental-backends");
         RestartSec = 3;
         Restart = "always";
       };
diff --git a/nixos/modules/services/x11/redshift.nix b/nixos/modules/services/x11/redshift.nix
index 21b0b33553a..60d80a28762 100644
--- a/nixos/modules/services/x11/redshift.nix
+++ b/nixos/modules/services/x11/redshift.nix
@@ -82,6 +82,15 @@ in {
       '';
     };
 
+    executable = mkOption {
+      type = types.str;
+      default = "/bin/redshift";
+      example = "/bin/redshift-gtk";
+      description = ''
+        Redshift executable to use within the package.
+      '';
+    };
+
     extraOptions = mkOption {
       type = types.listOf types.str;
       default = [];
@@ -114,7 +123,7 @@ in {
       partOf = [ "graphical-session.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${cfg.package}/bin/redshift \
+          ${cfg.package}${cfg.executable} \
             -l ${providerString} \
             -t ${toString cfg.temperature.day}:${toString cfg.temperature.night} \
             -b ${toString cfg.brightness.day}:${toString cfg.brightness.night} \
diff --git a/nixos/modules/services/x11/terminal-server.nix b/nixos/modules/services/x11/terminal-server.nix
index 503c14c9b62..e6b50c21a95 100644
--- a/nixos/modules/services/x11/terminal-server.nix
+++ b/nixos/modules/services/x11/terminal-server.nix
@@ -32,7 +32,7 @@ with lib;
 
         path =
           [ pkgs.xorg.xorgserver.out pkgs.gawk pkgs.which pkgs.openssl pkgs.xorg.xauth
-            pkgs.nettools pkgs.shadow pkgs.procps pkgs.utillinux pkgs.bash
+            pkgs.nettools pkgs.shadow pkgs.procps pkgs.util-linux pkgs.bash
           ];
 
         environment.FD_GEOM = "1024x786x24";
diff --git a/nixos/modules/services/x11/window-managers/clfswm.nix b/nixos/modules/services/x11/window-managers/clfswm.nix
index 176c1f46127..171660c53ac 100644
--- a/nixos/modules/services/x11/window-managers/clfswm.nix
+++ b/nixos/modules/services/x11/window-managers/clfswm.nix
@@ -15,10 +15,10 @@ in
     services.xserver.windowManager.session = singleton {
       name = "clfswm";
       start = ''
-        ${pkgs.clfswm}/bin/clfswm &
+        ${pkgs.lispPackages.clfswm}/bin/clfswm &
         waitPID=$!
       '';
     };
-    environment.systemPackages = [ pkgs.clfswm ];
+    environment.systemPackages = [ pkgs.lispPackages.clfswm ];
   };
 }
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index 87702c58727..53285fbce87 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -13,7 +13,9 @@ in
     ./berry.nix
     ./bspwm.nix
     ./cwm.nix
+    ./clfswm.nix
     ./dwm.nix
+    ./e16.nix
     ./evilwm.nix
     ./exwm.nix
     ./fluxbox.nix
@@ -36,6 +38,7 @@ in
     ./tinywm.nix
     ./twm.nix
     ./windowmaker.nix
+    ./wmderland.nix
     ./wmii.nix
     ./xmonad.nix
     ./yeahwm.nix
diff --git a/nixos/modules/services/x11/window-managers/e16.nix b/nixos/modules/services/x11/window-managers/e16.nix
new file mode 100644
index 00000000000..3e1a22c4dab
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/e16.nix
@@ -0,0 +1,26 @@
+{ config , lib , pkgs , ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.e16;
+in
+{
+  ###### interface
+  options = {
+    services.xserver.windowManager.e16.enable = mkEnableOption "e16";
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "E16";
+      start = ''
+        ${pkgs.e16}/bin/e16 &
+        waitPID=$!
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.e16 ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/evilwm.nix b/nixos/modules/services/x11/window-managers/evilwm.nix
index 6e19e3572c7..6f1db2110f8 100644
--- a/nixos/modules/services/x11/window-managers/evilwm.nix
+++ b/nixos/modules/services/x11/window-managers/evilwm.nix
@@ -16,8 +16,8 @@ in
     services.xserver.windowManager.session = singleton {
       name = "evilwm";
       start = ''
-	${pkgs.evilwm}/bin/evilwm &
-	waitPID=$!
+        ${pkgs.evilwm}/bin/evilwm &
+        waitPID=$!
       '';
     };
     environment.systemPackages = [ pkgs.evilwm ];
diff --git a/nixos/modules/services/x11/window-managers/exwm.nix b/nixos/modules/services/x11/window-managers/exwm.nix
index dc1d957c170..4b707d39849 100644
--- a/nixos/modules/services/x11/window-managers/exwm.nix
+++ b/nixos/modules/services/x11/window-managers/exwm.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.xserver.windowManager.exwm;
   loadScript = pkgs.writeText "emacs-exwm-load" ''
-    (require 'exwm)
+    ${cfg.loadScript}
     ${optionalString cfg.enableDefaultConfig ''
       (require 'exwm-config)
       (exwm-config-default)
@@ -19,12 +19,26 @@ in
   options = {
     services.xserver.windowManager.exwm = {
       enable = mkEnableOption "exwm";
+      loadScript = mkOption {
+        default = "(require 'exwm)";
+        type = types.lines;
+        example = literalExample ''
+          (require 'exwm)
+          (exwm-enable)
+        '';
+        description = ''
+          Emacs lisp code to be run after loading the user's init
+          file. If enableDefaultConfig is true, this will be run
+          before loading the default config.
+        '';
+      };
       enableDefaultConfig = mkOption {
         default = true;
         type = lib.types.bool;
         description = "Enable an uncustomised exwm configuration.";
       };
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         example = literalExample ''
           epkgs: [
@@ -36,7 +50,7 @@ in
         description = ''
           Extra packages available to Emacs. The value must be a
           function which receives the attrset defined in
-          <varname>emacsPackages</varname> as the sole argument.
+          <varname>emacs.pkgs</varname> as the sole argument.
         '';
       };
     };
diff --git a/nixos/modules/services/x11/window-managers/fvwm.nix b/nixos/modules/services/x11/window-managers/fvwm.nix
index 9a51b9cd660..e283886ecc4 100644
--- a/nixos/modules/services/x11/window-managers/fvwm.nix
+++ b/nixos/modules/services/x11/window-managers/fvwm.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.xserver.windowManager.fvwm;
-  fvwm = pkgs.fvwm.override { gestures = cfg.gestures; };
+  fvwm = pkgs.fvwm.override { enableGestures = cfg.gestures; };
 in
 
 {
diff --git a/nixos/modules/services/x11/window-managers/herbstluftwm.nix b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
index e3ea61cb9a6..548097a412d 100644
--- a/nixos/modules/services/x11/window-managers/herbstluftwm.nix
+++ b/nixos/modules/services/x11/window-managers/herbstluftwm.nix
@@ -11,6 +11,15 @@ in
     services.xserver.windowManager.herbstluftwm = {
       enable = mkEnableOption "herbstluftwm";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.herbstluftwm;
+        defaultText = "pkgs.herbstluftwm";
+        description = ''
+          Herbstluftwm package to use.
+        '';
+      };
+
       configFile = mkOption {
         default     = null;
         type        = with types; nullOr path;
@@ -31,8 +40,8 @@ in
             (cfg.configFile != null)
             ''-c "${cfg.configFile}"''
             ;
-        in "${pkgs.herbstluftwm}/bin/herbstluftwm ${configFileClause}";
+        in "${cfg.package}/bin/herbstluftwm ${configFileClause}";
     };
-    environment.systemPackages = [ pkgs.herbstluftwm ];
+    environment.systemPackages = [ cfg.package ];
   };
 }
diff --git a/nixos/modules/services/x11/window-managers/metacity.nix b/nixos/modules/services/x11/window-managers/metacity.nix
index 5175fd7f3b1..600afe759b2 100644
--- a/nixos/modules/services/x11/window-managers/metacity.nix
+++ b/nixos/modules/services/x11/window-managers/metacity.nix
@@ -5,7 +5,7 @@ with lib;
 let
 
   cfg = config.services.xserver.windowManager.metacity;
-  inherit (pkgs) gnome3;
+  inherit (pkgs) gnome;
 in
 
 {
@@ -18,12 +18,12 @@ in
     services.xserver.windowManager.session = singleton
       { name = "metacity";
         start = ''
-          ${gnome3.metacity}/bin/metacity &
+          ${gnome.metacity}/bin/metacity &
           waitPID=$!
         '';
       };
 
-    environment.systemPackages = [ gnome3.metacity ];
+    environment.systemPackages = [ gnome.metacity ];
 
   };
 
diff --git a/nixos/modules/services/x11/window-managers/wmderland.nix b/nixos/modules/services/x11/window-managers/wmderland.nix
new file mode 100644
index 00000000000..a6864a82771
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/wmderland.nix
@@ -0,0 +1,61 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.wmderland;
+in
+
+{
+  options.services.xserver.windowManager.wmderland = {
+    enable = mkEnableOption "wmderland";
+
+    extraSessionCommands = mkOption {
+      default = "";
+      type = types.lines;
+      description = ''
+        Shell commands executed just before wmderland is started.
+      '';
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs; [
+        rofi
+        dunst
+        light
+        hsetroot
+        feh
+        rxvt-unicode
+      ];
+      example = literalExample ''
+        with pkgs; [
+          rofi
+          dunst
+          light
+          hsetroot
+          feh
+          rxvt-unicode
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "wmderland";
+      start = ''
+        ${cfg.extraSessionCommands}
+
+        ${pkgs.wmderland}/bin/wmderland &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [
+      pkgs.wmderland pkgs.wmderlandc
+    ] ++ cfg.extraPackages;
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
index 070758720fe..fe8ed381251 100644
--- a/nixos/modules/services/x11/window-managers/xmonad.nix
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -4,22 +4,39 @@ with lib;
 let
   inherit (lib) mkOption mkIf optionals literalExample;
   cfg = config.services.xserver.windowManager.xmonad;
-  xmonad = pkgs.xmonad-with-packages.override {
-    ghcWithPackages = cfg.haskellPackages.ghcWithPackages;
-    packages = self: cfg.extraPackages self ++
-                     optionals cfg.enableContribAndExtras
-                     [ self.xmonad-contrib self.xmonad-extras ];
+
+  ghcWithPackages = cfg.haskellPackages.ghcWithPackages;
+  packages = self: cfg.extraPackages self ++
+                   optionals cfg.enableContribAndExtras
+                   [ self.xmonad-contrib self.xmonad-extras ];
+
+  xmonad-vanilla = pkgs.xmonad-with-packages.override {
+    inherit ghcWithPackages packages;
   };
-  xmonadBin = pkgs.writers.writeHaskell "xmonad" {
-    ghc = cfg.haskellPackages.ghc;
-    libraries = [ cfg.haskellPackages.xmonad ] ++
-                cfg.extraPackages cfg.haskellPackages ++
-                optionals cfg.enableContribAndExtras
-                (with cfg.haskellPackages; [ xmonad-contrib xmonad-extras ]);
-  } cfg.config;
-
-in
-{
+
+  xmonad-config =
+    let
+      xmonadAndPackages = self: [ self.xmonad ] ++ packages self;
+      xmonadEnv = ghcWithPackages xmonadAndPackages;
+      configured = pkgs.writers.writeHaskellBin "xmonad" {
+        ghc = cfg.haskellPackages.ghc;
+        libraries = xmonadAndPackages cfg.haskellPackages;
+        inherit (cfg) ghcArgs;
+      } cfg.config;
+    in
+      pkgs.runCommandLocal "xmonad" {
+        nativeBuildInputs = [ pkgs.makeWrapper ];
+      } ''
+        install -D ${xmonadEnv}/share/man/man1/xmonad.1.gz $out/share/man/man1/xmonad.1.gz
+        makeWrapper ${configured}/bin/xmonad $out/bin/xmonad \
+          --set NIX_GHC "${xmonadEnv}/bin/ghc" \
+          --set XMONAD_XMESSAGE "${pkgs.xorg.xmessage}/bin/xmessage"
+      '';
+
+  xmonad = if (cfg.config != null) then xmonad-config else xmonad-vanilla;
+in {
+  meta.maintainers = with maintainers; [ lassulus xaverdh ivanbrennan ];
+
   options = {
     services.xserver.windowManager.xmonad = {
       enable = mkEnableOption "xmonad";
@@ -36,6 +53,7 @@ in
       };
 
       extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
         default = self: [];
         defaultText = "self: []";
         example = literalExample ''
@@ -61,31 +79,84 @@ in
         default = null;
         type = with lib.types; nullOr (either path str);
         description = ''
-          Configuration from which XMonad gets compiled. If no value
-          is specified, the xmonad config from $HOME/.xmonad is taken.
-          If you use xmonad --recompile, $HOME/.xmonad will be taken as
-          the configuration, but on the next restart of display-manager
-          this config will be reapplied.
+          Configuration from which XMonad gets compiled. If no value is
+          specified, a vanilla xmonad binary is put in PATH, which will
+          attempt to recompile and exec your xmonad config from $HOME/.xmonad.
+          This setup is then analogous to other (non-NixOS) linux distributions.
+
+          If you do set this option, you likely want to use "launch" as your
+          entry point for xmonad (as in the example), to avoid xmonad's
+          recompilation logic on startup. Doing so will render the default
+          "mod+q" restart key binding dysfunctional though, because that attempts
+          to call your binary with the "--restart" command line option, unless
+          you implement that yourself. You way mant to bind "mod+q" to
+          <literal>(restart "xmonad" True)</literal> instead, which will just restart
+          xmonad from PATH. This allows e.g. switching to the new xmonad binary
+          after rebuilding your system with nixos-rebuild.
+
+          If you actually want to run xmonad with a config specified here, but
+          also be able to recompile and restart it from a copy of that source in
+          $HOME/.xmonad on the fly, you will have to implement that yourself
+          using something like "compileRestart" from the example.
+          This should allow you to switch at will between the local xmonad and
+          the one NixOS puts in your PATH.
         '';
         example = ''
           import XMonad
+          import XMonad.Util.EZConfig (additionalKeys)
+          import Control.Monad (when)
+          import Text.Printf (printf)
+          import System.Posix.Process (executeFile)
+          import System.Info (arch,os)
+          import System.Environment (getArgs)
+          import System.FilePath ((</>))
+
+          compiledConfig = printf "xmonad-%s-%s" arch os
+
+          compileRestart resume =
+            whenX (recompile True) $
+              when resume writeStateToFile
+                *> catchIO
+                  ( do
+                      dir <- getXMonadDataDir
+                      args <- getArgs
+                      executeFile (dir </> compiledConfig) False args Nothing
+                  )
 
           main = launch defaultConfig
-                 { modMask = mod4Mask -- Use Super instead of Alt
-                 , terminal = "urxvt"
-                 }
+              { modMask = mod4Mask -- Use Super instead of Alt
+              , terminal = "urxvt" }
+              `additionalKeys`
+              [ ( (mod4Mask,xK_r), compileRestart True)
+              , ( (mod4Mask,xK_q), restart "xmonad" True ) ]
         '';
       };
+
+      xmonadCliArgs = mkOption {
+        default = [];
+        type = with lib.types; listOf str;
+        description = ''
+          Command line arguments passed to the xmonad binary.
+        '';
+      };
+
+      ghcArgs = mkOption {
+        default = [];
+        type = with lib.types; listOf str;
+        description = ''
+          Command line arguments passed to the compiler (ghc)
+          invocation when xmonad.config is set.
+        '';
+      };
+
     };
   };
   config = mkIf cfg.enable {
     services.xserver.windowManager = {
       session = [{
         name = "xmonad";
-        start = let
-          xmonadCommand = if (cfg.config != null) then xmonadBin else "${xmonad}/bin/xmonad";
-        in ''
-           systemd-cat -t xmonad ${xmonadCommand} &
+        start = ''
+           systemd-cat -t xmonad -- ${xmonad}/bin/xmonad ${lib.escapeShellArgs cfg.xmonadCliArgs} &
            waitPID=$!
         '';
       }];
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 400173745d3..37e004ae80a 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -81,13 +81,7 @@ let
     monitors = forEach xrandrHeads (h: ''
       Option "monitor-${h.config.output}" "${h.name}"
     '');
-    # First option is indented through the space in the config but any
-    # subsequent options aren't so we need to apply indentation to
-    # them here
-    monitorsIndented = if length monitors > 1
-      then singleton (head monitors) ++ map (m: "  " + m) (tail monitors)
-      else monitors;
-  in concatStrings monitorsIndented;
+  in concatStrings monitors;
 
   # Here we chain every monitor from the left to right, so we have:
   # m4 right of m3 right of m2 right of m1   .----.----.----.----.
@@ -113,14 +107,14 @@ let
   in concatMapStrings (getAttr "value") monitors;
 
   configFile = pkgs.runCommand "xserver.conf"
-    { xfs = optionalString (cfg.useXFS != false)
-        ''FontPath "${toString cfg.useXFS}"'';
+    { fontpath = optionalString (cfg.fontPath != null)
+        ''FontPath "${cfg.fontPath}"'';
       inherit (cfg) config;
       preferLocalBuild = true;
     }
       ''
         echo 'Section "Files"' >> $out
-        echo $xfs >> $out
+        echo $fontpath >> $out
 
         for i in ${toString fontsForXServer}; do
           if test "''${i:0:''${#NIX_STORE}}" == "$NIX_STORE"; then
@@ -136,11 +130,17 @@ let
           fi
         done
 
+        echo '${cfg.filesSection}' >> $out
         echo 'EndSection' >> $out
+        echo >> $out
 
         echo "$config" >> $out
       ''; # */
 
+  prefixStringLines = prefix: str:
+    concatMapStringsSep "\n" (line: prefix + line) (splitString "\n" str);
+
+  indent = prefixStringLines "  ";
 in
 
 {
@@ -151,6 +151,11 @@ in
       ./desktop-managers/default.nix
       (mkRemovedOptionModule [ "services" "xserver" "startGnuPGAgent" ]
         "See the 16.09 release notes for more information.")
+      (mkRemovedOptionModule
+        [ "services" "xserver" "startDbusSession" ]
+        "The user D-Bus session is now always socket activated and this option can safely be removed.")
+      (mkRemovedOptionModule ["services" "xserver" "useXFS" ]
+        "Use services.xserver.fontPath instead of useXFS")
     ];
 
 
@@ -245,11 +250,10 @@ in
 
       videoDrivers = mkOption {
         type = types.listOf types.str;
-        # !!! We'd like "nv" here, but it segfaults the X server.
-        default = [ "radeon" "cirrus" "vesa" "modesetting" ];
+        default = [ "amdgpu" "radeon" "nouveau" "modesetting" "fbdev" ];
         example = [
-          "ati_unfree" "amdgpu" "amdgpu-pro"
-          "nv" "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
+          "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
+          "amdgpu-pro"
         ];
         # TODO(@oxij): think how to easily add the rest, like those nvidia things
         relatedPackages = concatLists
@@ -296,14 +300,6 @@ in
         description = "DPI resolution to use for X server.";
       };
 
-      startDbusSession = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether to start a new DBus session when you log in with dbus-launch.
-        '';
-      };
-
       updateDbusEnvironment = mkOption {
         type = types.bool;
         default = false;
@@ -361,9 +357,23 @@ in
         description = ''
           The contents of the configuration file of the X server
           (<filename>xorg.conf</filename>).
+
+          This option is set by multiple modules, and the configs are
+          concatenated together.
+
+          In Xorg configs the last config entries take precedence,
+          so you may want to use <literal>lib.mkAfter</literal> on this option
+          to override NixOS's defaults.
         '';
       };
 
+      filesSection = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''FontPath "/path/to/my/fonts"'';
+        description = "Contents of the first <literal>Files</literal> section of the X server configuration file.";
+      };
+
       deviceSection = mkOption {
         type = types.lines;
         default = "";
@@ -436,6 +446,7 @@ in
 
       serverFlagsSection = mkOption {
         default = "";
+        type = types.lines;
         example =
           ''
           Option "BlankTime" "0"
@@ -481,11 +492,15 @@ in
         description = "Default colour depth.";
       };
 
-      useXFS = mkOption {
-        # FIXME: what's the type of this option?
-        default = false;
+      fontPath = mkOption {
+        type = types.nullOr types.str;
+        default = null;
         example = "unix/:7100";
-        description = "Determines how to connect to the X Font Server.";
+        description = ''
+          Set the X server FontPath. Defaults to null, which
+          means the compiled in defaults will be used. See
+          man xorg.conf for details.
+        '';
       };
 
       tty = mkOption {
@@ -509,6 +524,19 @@ in
         '';
       };
 
+      logFile = mkOption {
+        type = types.nullOr types.str;
+        default = "/dev/null";
+        example = "/var/log/Xorg.0.log";
+        description = ''
+          Controls the file Xorg logs to.
+
+          The default of <literal>/dev/null</literal> is set so that systemd services (like <literal>displayManagers</literal>) only log to the journal and don't create their own log files.
+
+          Setting this to <literal>null</literal> will not pass the <literal>-logfile</literal> argument to Xorg which allows it to log to its default logfile locations instead (see <literal>man Xorg</literal>). You probably only want this behaviour when running Xorg manually (e.g. via <literal>startx</literal>).
+        '';
+      };
+
       verbose = mkOption {
         type = types.nullOr types.int;
         default = 3;
@@ -627,7 +655,7 @@ in
         xorg.xprop
         xorg.xauth
         pkgs.xterm
-        pkgs.xdg_utils
+        pkgs.xdg-utils
         xorg.xf86inputevdev.out # get evdev.4 man page
       ]
       ++ optional (elem "virtualbox" cfg.videoDrivers) xorg.xrefresh;
@@ -644,6 +672,7 @@ in
     # The default max inotify watches is 8192.
     # Nowadays most apps require a good number of inotify watches,
     # the value below is used by default on several other distros.
+    boot.kernel.sysctl."fs.inotify.max_user_instances" = mkDefault 524288;
     boot.kernel.sysctl."fs.inotify.max_user_watches" = mkDefault 524288;
 
     systemd.defaultUnit = mkIf cfg.autorun "graphical.target";
@@ -669,25 +698,24 @@ in
 
         script = "${cfg.displayManager.job.execCmd}";
 
+        # Stop restarting if the display manager stops (crashes) 2 times
+        # in one minute. Starting X typically takes 3-4s.
+        startLimitIntervalSec = 30;
+        startLimitBurst = 3;
         serviceConfig = {
           Restart = "always";
           RestartSec = "200ms";
           SyslogIdentifier = "display-manager";
-          # Stop restarting if the display manager stops (crashes) 2 times
-          # in one minute. Starting X typically takes 3-4s.
-          StartLimitInterval = "30s";
-          StartLimitBurst = "3";
         };
       };
 
     services.xserver.displayManager.xserverArgs =
       [ "-config ${configFile}"
         "-xkbdir" "${cfg.xkbDir}"
-        # Log at the default verbosity level to stderr rather than /var/log/X.*.log.
-         "-logfile" "/dev/null"
       ] ++ optional (cfg.display != null) ":${toString cfg.display}"
         ++ optional (cfg.tty     != null) "vt${toString cfg.tty}"
         ++ optional (cfg.dpi     != null) "-dpi ${toString cfg.dpi}"
+        ++ optional (cfg.logFile != null) "-logfile ${toString cfg.logFile}"
         ++ optional (cfg.verbose != null) "-verbose ${toString cfg.verbose}"
         ++ optional (!cfg.enableTCP) "-nolisten tcp"
         ++ optional (cfg.autoRepeatDelay != null) "-ardelay ${toString cfg.autoRepeatDelay}"
@@ -702,7 +730,7 @@ in
 
     system.extraDependencies = singleton (pkgs.runCommand "xkb-validated" {
       inherit (cfg) xkbModel layout xkbVariant xkbOptions;
-      nativeBuildInputs = [ pkgs.xkbvalidate ];
+      nativeBuildInputs = with pkgs.buildPackages; [ xkbvalidate ];
       preferLocalBuild = true;
     } ''
       xkbvalidate "$xkbModel" "$layout" "$xkbVariant" "$xkbOptions"
@@ -714,29 +742,29 @@ in
         Section "ServerFlags"
           Option "AllowMouseOpenFail" "on"
           Option "DontZap" "${if cfg.enableCtrlAltBackspace then "off" else "on"}"
-          ${cfg.serverFlagsSection}
+        ${indent cfg.serverFlagsSection}
         EndSection
 
         Section "Module"
-          ${cfg.moduleSection}
+        ${indent cfg.moduleSection}
         EndSection
 
         Section "Monitor"
           Identifier "Monitor[0]"
-          ${cfg.monitorSection}
+        ${indent cfg.monitorSection}
         EndSection
 
         # Additional "InputClass" sections
-        ${flip concatMapStrings cfg.inputClassSections (inputClassSection: ''
-        Section "InputClass"
-          ${inputClassSection}
-        EndSection
+        ${flip (concatMapStringsSep "\n") cfg.inputClassSections (inputClassSection: ''
+          Section "InputClass"
+          ${indent inputClassSection}
+          EndSection
         '')}
 
 
         Section "ServerLayout"
           Identifier "Layout[all]"
-          ${cfg.serverLayoutSection}
+        ${indent cfg.serverLayoutSection}
           # Reference the Screen sections for each driver.  This will
           # cause the X server to try each in turn.
           ${flip concatMapStrings (filter (d: d.display) cfg.drivers) (d: ''
@@ -759,9 +787,9 @@ in
             Identifier "Device-${driver.name}[0]"
             Driver "${driver.driverName or driver.name}"
             ${if cfg.useGlamor then ''Option "AccelMethod" "glamor"'' else ""}
-            ${cfg.deviceSection}
-            ${driver.deviceSection or ""}
-            ${xrandrDeviceSection}
+          ${indent cfg.deviceSection}
+          ${indent (driver.deviceSection or "")}
+          ${indent xrandrDeviceSection}
           EndSection
           ${optionalString driver.display ''
 
@@ -772,18 +800,22 @@ in
                 Monitor "Monitor[0]"
               ''}
 
-              ${cfg.screenSection}
-              ${driver.screenSection or ""}
+            ${indent cfg.screenSection}
+            ${indent (driver.screenSection or "")}
 
               ${optionalString (cfg.defaultDepth != 0) ''
                 DefaultDepth ${toString cfg.defaultDepth}
               ''}
 
               ${optionalString
-                  (driver.name != "virtualbox" &&
+                (
+                  driver.name != "virtualbox"
+                  &&
                   (cfg.resolutions != [] ||
                     cfg.extraDisplaySettings != "" ||
-                    cfg.virtualScreen != null))
+                    cfg.virtualScreen != null
+                  )
+                )
                 (let
                   f = depth:
                     ''
@@ -791,7 +823,7 @@ in
                         Depth ${toString depth}
                         ${optionalString (cfg.resolutions != [])
                           "Modes ${concatMapStrings (res: ''"${toString res.x}x${toString res.y}"'') cfg.resolutions}"}
-                        ${cfg.extraDisplaySettings}
+                      ${indent cfg.extraDisplaySettings}
                         ${optionalString (cfg.virtualScreen != null)
                           "Virtual ${toString cfg.virtualScreen.x} ${toString cfg.virtualScreen.y}"}
                       EndSubSection