summary refs log tree commit diff
path: root/nixos/modules/services/security
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2022-05-31 09:59:33 +0000
committerAlyssa Ross <hi@alyssa.is>2022-05-31 09:59:57 +0000
commit9ff36293d1e428cd7bf03e8d4b03611b6d361c28 (patch)
tree1ab51a42b868c55b83f6ccdb80371b9888739dd9 /nixos/modules/services/security
parent1c4fcd0d4b0541e674ee56ace1053e23e562cc80 (diff)
parentddc3c396a51918043bb0faa6f676abd9562be62c (diff)
downloadnixpkgs-archive.tar
nixpkgs-archive.tar.gz
nixpkgs-archive.tar.bz2
nixpkgs-archive.tar.lz
nixpkgs-archive.tar.xz
nixpkgs-archive.tar.zst
nixpkgs-archive.zip
Last good Nixpkgs for Weston+nouveau? archive
I came this commit hash to terwiz[m] on IRC, who is trying to figure out
what the last version of Spectrum that worked on their NUC with Nvidia
graphics is.
Diffstat (limited to 'nixos/modules/services/security')
-rw-r--r--nixos/modules/services/security/aesmd.nix236
-rw-r--r--nixos/modules/services/security/certmgr.nix201
-rw-r--r--nixos/modules/services/security/cfssl.nix222
-rw-r--r--nixos/modules/services/security/clamav.nix151
-rw-r--r--nixos/modules/services/security/fail2ban.nix340
-rw-r--r--nixos/modules/services/security/fprintd.nix64
-rw-r--r--nixos/modules/services/security/haka.nix156
-rw-r--r--nixos/modules/services/security/haveged.nix77
-rw-r--r--nixos/modules/services/security/hockeypuck.nix106
-rw-r--r--nixos/modules/services/security/hologram-agent.nix58
-rw-r--r--nixos/modules/services/security/hologram-server.nix130
-rw-r--r--nixos/modules/services/security/munge.nix68
-rw-r--r--nixos/modules/services/security/nginx-sso.nix67
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix591
-rw-r--r--nixos/modules/services/security/oauth2_proxy_nginx.nix66
-rw-r--r--nixos/modules/services/security/opensnitch.nix125
-rw-r--r--nixos/modules/services/security/physlock.nix139
-rw-r--r--nixos/modules/services/security/privacyidea.nix309
-rw-r--r--nixos/modules/services/security/shibboleth-sp.nix75
-rw-r--r--nixos/modules/services/security/sks.nix146
-rw-r--r--nixos/modules/services/security/sshguard.nix161
-rw-r--r--nixos/modules/services/security/step-ca.nix146
-rw-r--r--nixos/modules/services/security/tor.nix1067
-rw-r--r--nixos/modules/services/security/torify.nix80
-rw-r--r--nixos/modules/services/security/torsocks.nix121
-rw-r--r--nixos/modules/services/security/usbguard.nix214
-rw-r--r--nixos/modules/services/security/vault.nix204
-rw-r--r--nixos/modules/services/security/vaultwarden/backup.sh17
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix185
-rw-r--r--nixos/modules/services/security/yubikey-agent.nix66
30 files changed, 5588 insertions, 0 deletions
diff --git a/nixos/modules/services/security/aesmd.nix b/nixos/modules/services/security/aesmd.nix
new file mode 100644
index 00000000000..8268b034a15
--- /dev/null
+++ b/nixos/modules/services/security/aesmd.nix
@@ -0,0 +1,236 @@
+{ config, options, pkgs, lib, ... }:
+with lib;
+let
+  cfg = config.services.aesmd;
+  opt = options.services.aesmd;
+
+  sgx-psw = pkgs.sgx-psw.override { inherit (cfg) debug; };
+
+  configFile = with cfg.settings; pkgs.writeText "aesmd.conf" (
+    concatStringsSep "\n" (
+      optional (whitelistUrl != null) "whitelist url = ${whitelistUrl}" ++
+      optional (proxy != null) "aesm proxy = ${proxy}" ++
+      optional (proxyType != null) "proxy type = ${proxyType}" ++
+      optional (defaultQuotingType != null) "default quoting type = ${defaultQuotingType}" ++
+      # Newline at end of file
+      [ "" ]
+    )
+  );
+in
+{
+  options.services.aesmd = {
+    enable = mkEnableOption "Intel's Architectural Enclave Service Manager (AESM) for Intel SGX";
+    debug = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether to build the PSW package in debug mode.";
+    };
+    settings = mkOption {
+      description = "AESM configuration";
+      default = { };
+      type = types.submodule {
+        options.whitelistUrl = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "http://whitelist.trustedservices.intel.com/SGX/LCWL/Linux/sgx_white_list_cert.bin";
+          description = "URL to retrieve authorized Intel SGX enclave signers.";
+        };
+        options.proxy = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          example = "http://proxy_url:1234";
+          description = "HTTP network proxy.";
+        };
+        options.proxyType = mkOption {
+          type = with types; nullOr (enum [ "default" "direct" "manual" ]);
+          default = if (cfg.settings.proxy != null) then "manual" else null;
+          defaultText = literalExpression ''
+            if (config.${opt.settings}.proxy != null) then "manual" else null
+          '';
+          example = "default";
+          description = ''
+            Type of proxy to use. The <literal>default</literal> uses the system's default proxy.
+            If <literal>direct</literal> is given, uses no proxy.
+            A value of <literal>manual</literal> uses the proxy from
+            <option>services.aesmd.settings.proxy</option>.
+          '';
+        };
+        options.defaultQuotingType = mkOption {
+          type = with types; nullOr (enum [ "ecdsa_256" "epid_linkable" "epid_unlinkable" ]);
+          default = null;
+          example = "ecdsa_256";
+          description = "Attestation quote type.";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [{
+      assertion = !(config.boot.specialFileSystems."/dev".options ? "noexec");
+      message = "SGX requires exec permission for /dev";
+    }];
+
+    hardware.cpu.intel.sgx.provision.enable = true;
+
+    # Make sure the AESM service can find the SGX devices until
+    # https://github.com/intel/linux-sgx/issues/772 is resolved
+    # and updated in nixpkgs.
+    hardware.cpu.intel.sgx.enableDcapCompat = mkForce true;
+
+    systemd.services.aesmd =
+      let
+        storeAesmFolder = "${sgx-psw}/aesm";
+        # Hardcoded path AESM_DATA_FOLDER in psw/ae/aesm_service/source/oal/linux/aesm_util.cpp
+        aesmDataFolder = "/var/opt/aesmd/data";
+        aesmStateDirSystemd = "%S/aesmd";
+      in
+      {
+        description = "Intel Architectural Enclave Service Manager";
+        wantedBy = [ "multi-user.target" ];
+
+        after = [
+          "auditd.service"
+          "network.target"
+          "syslog.target"
+        ];
+
+        environment = {
+          NAME = "aesm_service";
+          AESM_PATH = storeAesmFolder;
+          LD_LIBRARY_PATH = storeAesmFolder;
+        };
+
+        # Make sure any of the SGX application enclave devices is available
+        unitConfig.AssertPathExists = [
+          # legacy out-of-tree driver
+          "|/dev/isgx"
+          # DCAP driver
+          "|/dev/sgx/enclave"
+          # in-tree driver
+          "|/dev/sgx_enclave"
+        ];
+
+        serviceConfig = rec {
+          ExecStartPre = pkgs.writeShellScript "copy-aesmd-data-files.sh" ''
+            set -euo pipefail
+            whiteListFile="${aesmDataFolder}/white_list_cert_to_be_verify.bin"
+            if [[ ! -f "$whiteListFile" ]]; then
+              ${pkgs.coreutils}/bin/install -m 644 -D \
+                "${storeAesmFolder}/data/white_list_cert_to_be_verify.bin" \
+                "$whiteListFile"
+            fi
+          '';
+          ExecStart = "${sgx-psw}/bin/aesm_service --no-daemon";
+          ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"'';
+
+          Restart = "on-failure";
+          RestartSec = "15s";
+
+          DynamicUser = true;
+          Group = "sgx";
+          SupplementaryGroups = [
+            config.hardware.cpu.intel.sgx.provision.group
+          ];
+
+          Type = "simple";
+
+          WorkingDirectory = storeAesmFolder;
+          StateDirectory = "aesmd";
+          StateDirectoryMode = "0700";
+          RuntimeDirectory = "aesmd";
+          RuntimeDirectoryMode = "0750";
+
+          # Hardening
+
+          # chroot into the runtime directory
+          RootDirectory = "%t/aesmd";
+          BindReadOnlyPaths = [
+            builtins.storeDir
+            # Hardcoded path AESM_CONFIG_FILE in psw/ae/aesm_service/source/utils/aesm_config.cpp
+            "${configFile}:/etc/aesmd.conf"
+          ];
+          BindPaths = [
+            # Hardcoded path CONFIG_SOCKET_PATH in psw/ae/aesm_service/source/core/ipc/SocketConfig.h
+            "%t/aesmd:/var/run/aesmd"
+            "%S/aesmd:/var/opt/aesmd"
+          ];
+
+          # PrivateDevices=true will mount /dev noexec which breaks AESM
+          PrivateDevices = false;
+          DevicePolicy = "closed";
+          DeviceAllow = [
+            # legacy out-of-tree driver
+            "/dev/isgx rw"
+            # DCAP driver
+            "/dev/sgx rw"
+            # in-tree driver
+            "/dev/sgx_enclave rw"
+            "/dev/sgx_provision rw"
+          ];
+
+          # Requires Internet access for attestation
+          PrivateNetwork = false;
+
+          RestrictAddressFamilies = [
+            # Allocates the socket /var/run/aesmd/aesm.socket
+            "AF_UNIX"
+            # Uses the HTTP protocol to initialize some services
+            "AF_INET"
+            "AF_INET6"
+          ];
+
+          # True breaks stuff
+          MemoryDenyWriteExecute = false;
+
+          # needs the ipc syscall in order to run
+          SystemCallFilter = [
+            "@system-service"
+            "~@aio"
+            "~@chown"
+            "~@clock"
+            "~@cpu-emulation"
+            "~@debug"
+            "~@keyring"
+            "~@memlock"
+            "~@module"
+            "~@mount"
+            "~@privileged"
+            "~@raw-io"
+            "~@reboot"
+            "~@resources"
+            "~@setuid"
+            "~@swap"
+            "~@sync"
+            "~@timer"
+          ];
+          SystemCallArchitectures = "native";
+          SystemCallErrorNumber = "EPERM";
+
+          CapabilityBoundingSet = "";
+          KeyringMode = "private";
+          LockPersonality = true;
+          NoNewPrivileges = true;
+          NotifyAccess = "none";
+          PrivateMounts = 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";
+          RemoveIPC = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          UMask = "0066";
+        };
+      };
+  };
+}
diff --git a/nixos/modules/services/security/certmgr.nix b/nixos/modules/services/security/certmgr.nix
new file mode 100644
index 00000000000..d302a4e0002
--- /dev/null
+++ b/nixos/modules/services/security/certmgr.nix
@@ -0,0 +1,201 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.certmgr;
+
+  specs = mapAttrsToList (n: v: rec {
+    name = n + ".json";
+    path = if isAttrs v then pkgs.writeText name (builtins.toJSON v) else v;
+  }) cfg.specs;
+
+  allSpecs = pkgs.linkFarm "certmgr.d" specs;
+
+  certmgrYaml = pkgs.writeText "certmgr.yaml" (builtins.toJSON {
+    dir = allSpecs;
+    default_remote = cfg.defaultRemote;
+    svcmgr = cfg.svcManager;
+    before = cfg.validMin;
+    interval = cfg.renewInterval;
+    inherit (cfg) metricsPort metricsAddress;
+  });
+
+  specPaths = map dirOf (concatMap (spec:
+    if isAttrs spec then
+      collect isString (filterAttrsRecursive (n: v: isAttrs v || n == "path") spec)
+    else
+      [ spec ]
+  ) (attrValues cfg.specs));
+
+  preStart = ''
+    ${concatStringsSep " \\\n" (["mkdir -p"] ++ map escapeShellArg specPaths)}
+    ${cfg.package}/bin/certmgr -f ${certmgrYaml} check
+  '';
+in
+{
+  options.services.certmgr = {
+    enable = mkEnableOption "certmgr";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.certmgr;
+      defaultText = literalExpression "pkgs.certmgr";
+      description = "Which certmgr package to use in the service.";
+    };
+
+    defaultRemote = mkOption {
+      type = types.str;
+      default = "127.0.0.1:8888";
+      description = "The default CA host:port to use.";
+    };
+
+    validMin = mkOption {
+      default = "72h";
+      type = types.str;
+      description = "The interval before a certificate expires to start attempting to renew it.";
+    };
+
+    renewInterval = mkOption {
+      default = "30m";
+      type = types.str;
+      description = "How often to check certificate expirations and how often to update the cert_next_expires metric.";
+    };
+
+    metricsAddress = mkOption {
+      default = "127.0.0.1";
+      type = types.str;
+      description = "The address for the Prometheus HTTP endpoint.";
+    };
+
+    metricsPort = mkOption {
+      default = 9488;
+      type = types.ints.u16;
+      description = "The port for the Prometheus HTTP endpoint.";
+    };
+
+    specs = mkOption {
+      default = {};
+      example = literalExpression ''
+      {
+        exampleCert =
+        let
+          domain = "example.com";
+          secret = name: "/var/lib/secrets/''${name}.pem";
+        in {
+          service = "nginx";
+          action = "reload";
+          authority = {
+            file.path = secret "ca";
+          };
+          certificate = {
+            path = secret domain;
+          };
+          private_key = {
+            owner = "root";
+            group = "root";
+            mode = "0600";
+            path = secret "''${domain}-key";
+          };
+          request = {
+            CN = domain;
+            hosts = [ "mail.''${domain}" "www.''${domain}" ];
+            key = {
+              algo = "rsa";
+              size = 2048;
+            };
+            names = {
+              O = "Example Organization";
+              C = "USA";
+            };
+          };
+        };
+        otherCert = "/var/certmgr/specs/other-cert.json";
+      }
+      '';
+      type = with types; attrsOf (either path (submodule {
+        options = {
+          service = mkOption {
+            type = nullOr str;
+            default = null;
+            description = "The service on which to perform &lt;action&gt; after fetching.";
+          };
+
+          action = mkOption {
+            type = addCheck str (x: cfg.svcManager == "command" || elem x ["restart" "reload" "nop"]);
+            default = "nop";
+            description = "The action to take after fetching.";
+          };
+
+          # These ought all to be specified according to certmgr spec def.
+          authority = mkOption {
+            type = attrs;
+            description = "certmgr spec authority object.";
+          };
+
+          certificate = mkOption {
+            type = nullOr attrs;
+            description = "certmgr spec certificate object.";
+          };
+
+          private_key = mkOption {
+            type = nullOr attrs;
+            description = "certmgr spec private_key object.";
+          };
+
+          request = mkOption {
+            type = nullOr attrs;
+            description = "certmgr spec request object.";
+          };
+        };
+    }));
+      description = ''
+        Certificate specs as described by:
+        <link xlink:href="https://github.com/cloudflare/certmgr#certificate-specs" />
+        These will be added to the Nix store, so they will be world readable.
+      '';
+    };
+
+    svcManager = mkOption {
+      default = "systemd";
+      type = types.enum [ "circus" "command" "dummy" "openrc" "systemd" "sysv" ];
+      description = ''
+        This specifies the service manager to use for restarting or reloading services.
+        See: <link xlink:href="https://github.com/cloudflare/certmgr#certmgryaml" />.
+        For how to use the "command" service manager in particular,
+        see: <link xlink:href="https://github.com/cloudflare/certmgr#command-svcmgr-and-how-to-use-it" />.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.specs != {};
+        message = "Certmgr specs cannot be empty.";
+      }
+      {
+        assertion = !any (hasAttrByPath [ "authority" "auth_key" ]) (attrValues cfg.specs);
+        message = ''
+          Inline services.certmgr.specs are added to the Nix store rendering them world readable.
+          Specify paths as specs, if you want to use include auth_key - or use the auth_key_file option."
+        '';
+      }
+    ];
+
+    systemd.services.certmgr = {
+      description = "certmgr";
+      path = mkIf (cfg.svcManager == "command") [ pkgs.bash ];
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      inherit preStart;
+
+      serviceConfig = {
+        Restart = "always";
+        RestartSec = "10s";
+        ExecStart = "${cfg.package}/bin/certmgr -f ${certmgrYaml}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/cfssl.nix b/nixos/modules/services/security/cfssl.nix
new file mode 100644
index 00000000000..6df2343b84d
--- /dev/null
+++ b/nixos/modules/services/security/cfssl.nix
@@ -0,0 +1,222 @@
+{ config, options, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cfssl;
+in {
+  options.services.cfssl = {
+    enable = mkEnableOption "the CFSSL CA api-server";
+
+    dataDir = mkOption {
+      default = "/var/lib/cfssl";
+      type = types.path;
+      description = ''
+        The work directory for CFSSL.
+
+        <note><para>
+          If left as the default value this directory will automatically be
+          created before the CFSSL server starts, otherwise you are
+          responsible for ensuring the directory exists with appropriate
+          ownership and permissions.
+        </para></note>
+      '';
+    };
+
+    address = mkOption {
+      default = "127.0.0.1";
+      type = types.str;
+      description = "Address to bind.";
+    };
+
+    port = mkOption {
+      default = 8888;
+      type = types.port;
+      description = "Port to bind.";
+    };
+
+    ca = mkOption {
+      defaultText = literalExpression ''"''${cfg.dataDir}/ca.pem"'';
+      type = types.str;
+      description = "CA used to sign the new certificate -- accepts '[file:]fname' or 'env:varname'.";
+    };
+
+    caKey = mkOption {
+      defaultText = literalExpression ''"file:''${cfg.dataDir}/ca-key.pem"'';
+      type = types.str;
+      description = "CA private key -- accepts '[file:]fname' or 'env:varname'.";
+    };
+
+    caBundle = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Path to root certificate store.";
+    };
+
+    intBundle = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Path to intermediate certificate store.";
+    };
+
+    intDir = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Intermediates directory.";
+    };
+
+    metadata = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = ''
+        Metadata file for root certificate presence.
+        The content of the file is a json dictionary (k,v): each key k is
+        a SHA-1 digest of a root certificate while value v is a list of key
+        store filenames.
+      '';
+    };
+
+    remote = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Remote CFSSL server.";
+    };
+
+    configFile = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Path to configuration file. Do not put this in nix-store as it might contain secrets.";
+    };
+
+    responder = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Certificate for OCSP responder.";
+    };
+
+    responderKey = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Private key for OCSP responder certificate. Do not put this in nix-store.";
+    };
+
+    tlsKey = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Other endpoint's CA private key. Do not put this in nix-store.";
+    };
+
+    tlsCert = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Other endpoint's CA to set up TLS protocol.";
+    };
+
+    mutualTlsCa = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Mutual TLS - require clients be signed by this CA.";
+    };
+
+    mutualTlsCn = mkOption {
+      default = null;
+      type = types.nullOr types.str;
+      description = "Mutual TLS - regex for whitelist of allowed client CNs.";
+    };
+
+    tlsRemoteCa = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "CAs to trust for remote TLS requests.";
+    };
+
+    mutualTlsClientCert = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Mutual TLS - client certificate to call remote instance requiring client certs.";
+    };
+
+    mutualTlsClientKey = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Mutual TLS - client key to call remote instance requiring client certs. Do not put this in nix-store.";
+    };
+
+    dbConfig = mkOption {
+      default = null;
+      type = types.nullOr types.path;
+      description = "Certificate db configuration file. Path must be writeable.";
+    };
+
+    logLevel = mkOption {
+      default = 1;
+      type = types.enum [ 0 1 2 3 4 5 ];
+      description = "Log level (0 = DEBUG, 5 = FATAL).";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups.cfssl = {
+      gid = config.ids.gids.cfssl;
+    };
+
+    users.users.cfssl = {
+      description = "cfssl user";
+      home = cfg.dataDir;
+      group = "cfssl";
+      uid = config.ids.uids.cfssl;
+    };
+
+    systemd.services.cfssl = {
+      description = "CFSSL CA API server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = lib.mkMerge [
+        {
+          WorkingDirectory = cfg.dataDir;
+          Restart = "always";
+          User = "cfssl";
+          Group = "cfssl";
+
+          ExecStart = with cfg; let
+            opt = n: v: optionalString (v != null) ''-${n}="${v}"'';
+          in
+            lib.concatStringsSep " \\\n" [
+              "${pkgs.cfssl}/bin/cfssl serve"
+              (opt "address" address)
+              (opt "port" (toString port))
+              (opt "ca" ca)
+              (opt "ca-key" caKey)
+              (opt "ca-bundle" caBundle)
+              (opt "int-bundle" intBundle)
+              (opt "int-dir" intDir)
+              (opt "metadata" metadata)
+              (opt "remote" remote)
+              (opt "config" configFile)
+              (opt "responder" responder)
+              (opt "responder-key" responderKey)
+              (opt "tls-key" tlsKey)
+              (opt "tls-cert" tlsCert)
+              (opt "mutual-tls-ca" mutualTlsCa)
+              (opt "mutual-tls-cn" mutualTlsCn)
+              (opt "mutual-tls-client-key" mutualTlsClientKey)
+              (opt "mutual-tls-client-cert" mutualTlsClientCert)
+              (opt "tls-remote-ca" tlsRemoteCa)
+              (opt "db-config" dbConfig)
+              (opt "loglevel" (toString logLevel))
+            ];
+        }
+        (mkIf (cfg.dataDir == options.services.cfssl.dataDir.default) {
+          StateDirectory = baseNameOf cfg.dataDir;
+          StateDirectoryMode = 700;
+        })
+      ];
+    };
+
+    services.cfssl = {
+      ca = mkDefault "${cfg.dataDir}/ca.pem";
+      caKey = mkDefault "${cfg.dataDir}/ca-key.pem";
+    };
+  };
+}
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
new file mode 100644
index 00000000000..95a0ad8770e
--- /dev/null
+++ b/nixos/modules/services/security/clamav.nix
@@ -0,0 +1,151 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  clamavUser = "clamav";
+  stateDir = "/var/lib/clamav";
+  runDir = "/run/clamav";
+  clamavGroup = clamavUser;
+  cfg = config.services.clamav;
+  pkg = pkgs.clamav;
+
+  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 = [
+    (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 = {
+    services.clamav = {
+      daemon = {
+        enable = mkEnableOption "ClamAV clamd daemon";
+
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+          default = { };
+          description = ''
+            ClamAV configuration. Refer to <link xlink:href="https://linux.die.net/man/5/clamd.conf"/>,
+            for details on supported values.
+          '';
+        };
+      };
+      updater = {
+        enable = mkEnableOption "ClamAV freshclam updater";
+
+        frequency = mkOption {
+          type = types.int;
+          default = 12;
+          description = ''
+            Number of database checks per day.
+          '';
+        };
+
+        interval = mkOption {
+          type = types.str;
+          default = "hourly";
+          description = ''
+            How often freshclam is invoked. See systemd.time(7) for more
+            information about the format.
+          '';
+        };
+
+        settings = mkOption {
+          type = with types; attrsOf (oneOf [ bool int str (listOf str) ]);
+          default = { };
+          description = ''
+            freshclam configuration. Refer to <link xlink:href="https://linux.die.net/man/5/freshclam.conf"/>,
+            for details on supported values.
+          '';
+        };
+      };
+    };
+  };
+
+  config = mkIf (cfg.updater.enable || cfg.daemon.enable) {
+    environment.systemPackages = [ pkg ];
+
+    users.users.${clamavUser} = {
+      uid = config.ids.uids.clamav;
+      group = clamavGroup;
+      description = "ClamAV daemon user";
+      home = stateDir;
+    };
+
+    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;
+
+    systemd.services.clamav-daemon = mkIf cfg.daemon.enable {
+      description = "ClamAV daemon (clamd)";
+      after = optional cfg.updater.enable "clamav-freshclam.service";
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ clamdConfigFile ];
+
+      preStart = ''
+        mkdir -m 0755 -p ${runDir}
+        chown ${clamavUser}:${clamavGroup} ${runDir}
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkg}/bin/clamd";
+        ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        PrivateNetwork = "yes";
+      };
+    };
+
+    systemd.timers.clamav-freshclam = mkIf cfg.updater.enable {
+      description = "Timer for ClamAV virus database updater (freshclam)";
+      wantedBy = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = cfg.updater.interval;
+        Unit = "clamav-freshclam.service";
+      };
+    };
+
+    systemd.services.clamav-freshclam = mkIf cfg.updater.enable {
+      description = "ClamAV virus database updater (freshclam)";
+      restartTriggers = [ freshclamConfigFile ];
+      after = [ "network-online.target" ];
+      preStart = ''
+        mkdir -m 0755 -p ${stateDir}
+        chown ${clamavUser}:${clamavGroup} ${stateDir}
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkg}/bin/freshclam";
+        SuccessExitStatus = "1"; # if databases are up to date
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
new file mode 100644
index 00000000000..67e1026dcef
--- /dev/null
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -0,0 +1,340 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fail2ban;
+
+  fail2banConf = pkgs.writeText "fail2ban.local" cfg.daemonConfig;
+
+  jailConf = pkgs.writeText "jail.local" ''
+    [INCLUDES]
+
+    before = paths-nixos.conf
+
+    ${concatStringsSep "\n" (attrValues (flip mapAttrs cfg.jails (name: def:
+      optionalString (def != "")
+        ''
+          [${name}]
+          ${def}
+        '')))}
+  '';
+
+  pathsConf = pkgs.writeText "paths-nixos.conf" ''
+    # NixOS
+
+    [INCLUDES]
+
+    before = paths-common.conf
+
+    after  = paths-overrides.local
+
+    [DEFAULT]
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.fail2ban = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        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 {
+        default = pkgs.fail2ban;
+        defaultText = literalExpression "pkgs.fail2ban";
+        type = types.package;
+        example = literalExpression "pkgs.fail2ban_0_11";
+        description = "The fail2ban package to use for running the fail2ban service.";
+      };
+
+      packageFirewall = mkOption {
+        default = pkgs.iptables;
+        defaultText = literalExpression "pkgs.iptables";
+        type = types.package;
+        example = literalExpression "pkgs.nftables";
+        description = "The firewall package used by fail2ban service.";
+      };
+
+      extraPackages = mkOption {
+        default = [];
+        type = types.listOf types.package;
+        example = lib.literalExpression "[ 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;
+        example = "nftables-multiport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      banaction-allports = mkOption {
+        default = "iptables-allport";
+        type = types.str;
+        example = "nftables-allport";
+        description = ''
+          Default banning action (e.g. iptables, iptables-new, iptables-multiport,
+          shorewall, etc) It is used to define action_* variables. Can be overridden
+          globally or per section within jail.local file
+        '';
+      };
+
+      bantime-increment.enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          Allows to use database for searching of previously banned ip's to increase
+          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
+        '';
+      };
+
+      bantime-increment.rndtime = mkOption {
+        default = "4m";
+        type = types.str;
+        example = "8m";
+        description = ''
+          "bantime-increment.rndtime" is the max number of seconds using for mixing with random time
+          to prevent "clever" botnets calculate exact time IP can be unbanned again
+        '';
+      };
+
+      bantime-increment.maxtime = mkOption {
+        default = "10h";
+        type = types.str;
+        example = "48h";
+        description = ''
+          "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
+        '';
+      };
+
+      bantime-increment.factor = mkOption {
+        default = "1";
+        type = types.str;
+        example = "4";
+        description = ''
+          "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
+          default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
+        '';
+      };
+
+      bantime-increment.formula = mkOption {
+        default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor";
+        type = types.str;
+        example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
+        description = ''
+          "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow,
+          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
+        '';
+      };
+
+      bantime-increment.multipliers = mkOption {
+        default = "1 2 4 8 16 32 64";
+        type = types.str;
+        example = "2 4 16 128";
+        description = ''
+          "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, coresponding
+          previously ban count and given "bantime.factor" (for multipliers default is 1);
+          following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
+          always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
+        '';
+      };
+
+      bantime-increment.overalljails = mkOption {
+        default = false;
+        type = types.bool;
+        example = true;
+        description = ''
+          "bantime-increment.overalljails"  (if true) specifies the search of IP in the database will be executed
+          cross over all jails, if false (dafault), only current jail of the ban IP will be searched
+        '';
+      };
+
+      ignoreIP = mkOption {
+        default = [ ];
+        type = types.listOf types.str;
+        example = [ "192.168.0.0/16" "2001:DB8::42" ];
+        description = ''
+          "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
+          matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
+        '';
+      };
+
+      daemonConfig = mkOption {
+        default = ''
+          [Definition]
+          logtarget = SYSLOG
+          socket    = /run/fail2ban/fail2ban.sock
+          pidfile   = /run/fail2ban/fail2ban.pid
+          dbfile    = /var/lib/fail2ban/fail2ban.sqlite3
+        '';
+        type = types.lines;
+        description = ''
+          The contents of Fail2ban's main configuration file.  It's
+          generally not necessary to change it.
+       '';
+      };
+
+      jails = mkOption {
+        default = { };
+        example = literalExpression ''
+          { apache-nohome-iptables = '''
+              # Block an IP address if it accesses a non-existent
+              # home directory more than 5 times in 10 minutes,
+              # since that indicates that it's scanning.
+              filter   = apache-nohome
+              action   = iptables-multiport[name=HTTP, port="http,https"]
+              logpath  = /var/log/httpd/error_log*
+              findtime = 600
+              bantime  = 600
+              maxretry = 5
+            ''';
+          }
+        '';
+        type = types.attrsOf types.lines;
+        description = ''
+          The configuration of each Fail2ban “jail”.  A jail
+          consists of an action (such as blocking a port using
+          <command>iptables</command>) that is triggered when a
+          filter applied to a log file triggers more than a certain
+          number of times in a certain time period.  Actions are
+          defined in <filename>/etc/fail2ban/action.d</filename>,
+          while filters are defined in
+          <filename>/etc/fail2ban/filter.d</filename>.
+
+          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.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    warnings = mkIf (config.networking.firewall.enable == false && config.networking.nftables.enable == false) [
+      "fail2ban can not be used without a firewall"
+    ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    environment.etc = {
+      "fail2ban/fail2ban.local".source = fail2banConf;
+      "fail2ban/jail.local".source = jailConf;
+      "fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
+      "fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
+      "fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
+      "fail2ban/paths-nixos.conf".source = pathsConf;
+      "fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
+      "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
+    };
+
+    systemd.services.fail2ban = {
+      description = "Fail2ban Intrusion Prevention System";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      partOf = optional config.networking.firewall.enable "firewall.service";
+
+      restartTriggers = [ fail2banConf jailConf pathsConf ];
+
+      path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
+
+      unitConfig.Documentation = "man:fail2ban(1)";
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/fail2ban-server -xf start";
+        ExecStop = "${cfg.package}/bin/fail2ban-server stop";
+        ExecReload = "${cfg.package}/bin/fail2ban-server reload";
+        Type = "simple";
+        Restart = "on-failure";
+        PIDFile = "/run/fail2ban/fail2ban.pid";
+        # Capabilities
+        CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
+        # Security
+        NoNewPrivileges = true;
+        # Directory
+        RuntimeDirectory = "fail2ban";
+        RuntimeDirectoryMode = "0750";
+        StateDirectory = "fail2ban";
+        StateDirectoryMode = "0750";
+        LogsDirectory = "fail2ban";
+        LogsDirectoryMode = "0750";
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+      };
+    };
+
+    # Add some reasonable default jails.  The special "DEFAULT" jail
+    # sets default values for all other jails.
+    services.fail2ban.jails.DEFAULT = ''
+      ${optionalString cfg.bantime-increment.enable ''
+        # Bantime incremental
+        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 = ${boolToString cfg.bantime-increment.overalljails}
+      ''}
+      # Miscellaneous options
+      ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
+      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
new file mode 100644
index 00000000000..87c3f1f6f9e
--- /dev/null
+++ b/nixos/modules/services/security/fprintd.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.fprintd;
+  fprintdPkg = if cfg.tod.enable then pkgs.fprintd-tod else pkgs.fprintd;
+
+in
+
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.fprintd = {
+
+      enable = mkEnableOption "fprintd daemon and PAM module for fingerprint readers handling";
+
+      package = mkOption {
+        type = types.package;
+        default = fprintdPkg;
+        defaultText = literalExpression "if config.services.fprintd.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 = literalExpression "pkgs.libfprint-2-tod1-goodix";
+          description = ''
+            Touch OEM Drivers (TOD) package to use.
+          '';
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.dbus.packages = [ cfg.package ];
+
+    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/haka.nix b/nixos/modules/services/security/haka.nix
new file mode 100644
index 00000000000..2cfc05f3033
--- /dev/null
+++ b/nixos/modules/services/security/haka.nix
@@ -0,0 +1,156 @@
+# This module defines global configuration for Haka.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.haka;
+
+  haka = cfg.package;
+
+  hakaConf = pkgs.writeText "haka.conf"
+  ''
+    [general]
+    configuration = ${if lib.strings.hasPrefix "/" cfg.configFile
+      then "${cfg.configFile}"
+      else "${haka}/share/haka/sample/${cfg.configFile}"}
+    ${optionalString (builtins.lessThan 0 cfg.threads) "thread = ${cfg.threads}"}
+
+    [packet]
+    ${optionalString cfg.pcap ''module = "packet/pcap"''}
+    ${optionalString cfg.nfqueue ''module = "packet/nqueue"''}
+    ${optionalString cfg.dump.enable ''dump = "yes"''}
+    ${optionalString cfg.dump.enable ''dump_input = "${cfg.dump.input}"''}
+    ${optionalString cfg.dump.enable ''dump_output = "${cfg.dump.output}"''}
+
+    interfaces = "${lib.strings.concatStringsSep "," cfg.interfaces}"
+
+    [log]
+    # Select the log module
+    module = "log/syslog"
+
+    # Set the default logging level
+    #level = "info,packet=debug"
+
+    [alert]
+    # Select the alert module
+    module = "alert/syslog"
+
+    # Disable alert on standard output
+    #alert_on_stdout = no
+
+    # alert/file module option
+    #file = "/dev/null"
+  '';
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.haka = {
+
+      enable = mkEnableOption "Haka";
+
+      package = mkOption {
+        default = pkgs.haka;
+        defaultText = literalExpression "pkgs.haka";
+        type = types.package;
+        description = "
+          Which Haka derivation to use.
+        ";
+      };
+
+      configFile = mkOption {
+        default = "empty.lua";
+        example = "/srv/haka/myfilter.lua";
+        type = types.str;
+        description = ''
+          Specify which configuration file Haka uses.
+          It can be absolute path or a path relative to the sample directory of
+          the haka git repo.
+        '';
+      };
+
+      interfaces = mkOption {
+        default = [ "eth0" ];
+        example = [ "any" ];
+        type = with types; listOf str;
+        description = ''
+          Specify which interface(s) Haka listens to.
+          Use 'any' to listen to all interfaces.
+        '';
+      };
+
+      threads = mkOption {
+        default = 0;
+        example = 4;
+        type = types.int;
+        description = ''
+          The number of threads that will be used.
+          All system threads are used by default.
+        '';
+      };
+
+      pcap = mkOption {
+        default = true;
+        type = types.bool;
+        description = "Whether to enable pcap";
+      };
+
+      nfqueue = mkEnableOption "nfqueue";
+
+      dump.enable = mkEnableOption "dump";
+      dump.input  = mkOption {
+        default = "/tmp/input.pcap";
+        example = "/path/to/file.pcap";
+        type = types.path;
+        description = "Path to file where incoming packets are dumped";
+      };
+
+      dump.output  = mkOption {
+        default = "/tmp/output.pcap";
+        example = "/path/to/file.pcap";
+        type = types.path;
+        description = "Path to file where outgoing packets are dumped";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.pcap != cfg.nfqueue;
+        message = "either pcap or nfqueue can be enabled, not both.";
+      }
+      { assertion = cfg.nfqueue -> !dump.enable;
+        message = "dump can only be used with nfqueue.";
+      }
+      { assertion = cfg.interfaces != [];
+        message = "at least one interface must be specified.";
+      }];
+
+
+    environment.systemPackages = [ haka ];
+
+    systemd.services.haka = {
+      description = "Haka";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        ExecStart = "${haka}/bin/haka -c ${hakaConf}";
+        ExecStop = "${haka}/bin/hakactl stop";
+        User = "root";
+        Type = "forking";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/haveged.nix b/nixos/modules/services/security/haveged.nix
new file mode 100644
index 00000000000..57cef7e44d5
--- /dev/null
+++ b/nixos/modules/services/security/haveged.nix
@@ -0,0 +1,77 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.haveged;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.haveged = {
+
+      enable = mkEnableOption ''
+        haveged entropy daemon, which refills /dev/random when low.
+        NOTE: does nothing on kernels newer than 5.6.
+      '';
+      # source for the note https://github.com/jirka-h/haveged/issues/57
+
+      refill_threshold = mkOption {
+        type = types.int;
+        default = 1024;
+        description = ''
+          The number of bits of available entropy beneath which
+          haveged should refill the entropy pool.
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    # https://github.com/jirka-h/haveged/blob/a4b69d65a8dfc5a9f52ff8505c7f58dcf8b9234f/contrib/Fedora/haveged.service
+    systemd.services.haveged = {
+      description = "Entropy Daemon based on the HAVEGE algorithm";
+      unitConfig = {
+        Documentation = "man:haveged(8)";
+        DefaultDependencies = false;
+        ConditionKernelVersion = "<5.6";
+      };
+      wantedBy = [ "sysinit.target" ];
+      after = [ "systemd-tmpfiles-setup-dev.service" ];
+      before = [ "sysinit.target" "shutdown.target" "systemd-journald.service" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.haveged}/bin/haveged -w ${toString cfg.refill_threshold} --Foreground -v 1";
+        Restart = "always";
+        SuccessExitStatus = "137 143";
+        SecureBits = "noroot-locked";
+        CapabilityBoundingSet = [ "CAP_SYS_ADMIN" "CAP_SYS_CHROOT" ];
+        # We can *not* set PrivateTmp=true as it can cause an ordering cycle.
+        PrivateTmp = false;
+        PrivateDevices = true;
+        ProtectSystem = "full";
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "newuname" "~@mount" ];
+        SystemCallErrorNumber = "EPERM";
+      };
+
+    };
+  };
+
+}
diff --git a/nixos/modules/services/security/hockeypuck.nix b/nixos/modules/services/security/hockeypuck.nix
new file mode 100644
index 00000000000..d0e152934f5
--- /dev/null
+++ b/nixos/modules/services/security/hockeypuck.nix
@@ -0,0 +1,106 @@
+{ 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.literalExpression ''
+        {
+          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;
+      group = "hockeypuck";
+      description = "Hockeypuck user";
+    };
+    users.groups.hockeypuck = {};
+
+    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
new file mode 100644
index 00000000000..e29267e5000
--- /dev/null
+++ b/nixos/modules/services/security/hologram-agent.nix
@@ -0,0 +1,58 @@
+{pkgs, config, lib, ...}:
+
+with lib;
+
+let
+  cfg = config.services.hologram-agent;
+
+  cfgFile = pkgs.writeText "hologram-agent.json" (builtins.toJSON {
+    host = cfg.dialAddress;
+  });
+in {
+  options = {
+    services.hologram-agent = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Hologram agent for AWS instance credentials";
+      };
+
+      dialAddress = mkOption {
+        type        = types.str;
+        default     = "localhost:3100";
+        description = "Hologram server and port.";
+      };
+
+      httpPort = mkOption {
+        type        = types.str;
+        default     = "80";
+        description = "Port for metadata service to listen on.";
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "dummy" ];
+
+    networking.interfaces.dummy0.ipv4.addresses = [
+      { address = "169.254.169.254"; prefixLength = 32; }
+    ];
+
+    systemd.services.hologram-agent = {
+      description = "Provide EC2 instance credentials to machines outside of EC2";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+      requires    = [ "network-link-dummy0.service" "network-addresses-dummy0.service" ];
+      preStart = ''
+        /run/current-system/sw/bin/rm -fv /run/hologram.sock
+      '';
+      serviceConfig = {
+        ExecStart = "${pkgs.hologram}/bin/hologram-agent -debug -conf ${cfgFile} -port ${cfg.httpPort}";
+      };
+    };
+
+  };
+
+  meta.maintainers = with lib.maintainers; [ ];
+}
diff --git a/nixos/modules/services/security/hologram-server.nix b/nixos/modules/services/security/hologram-server.nix
new file mode 100644
index 00000000000..4acf6ae0e21
--- /dev/null
+++ b/nixos/modules/services/security/hologram-server.nix
@@ -0,0 +1,130 @@
+{pkgs, config, lib, ...}:
+
+with lib;
+
+let
+  cfg = config.services.hologram-server;
+
+  cfgFile = pkgs.writeText "hologram-server.json" (builtins.toJSON {
+    ldap = {
+      host = cfg.ldapHost;
+      bind = {
+        dn       = cfg.ldapBindDN;
+        password = cfg.ldapBindPassword;
+      };
+      insecureldap    = cfg.ldapInsecure;
+      userattr        = cfg.ldapUserAttr;
+      baseDN          = cfg.ldapBaseDN;
+      enableldapRoles = cfg.enableLdapRoles;
+      roleAttr        = cfg.roleAttr;
+      groupClassAttr  = cfg.groupClassAttr;
+    };
+    aws = {
+      account     = cfg.awsAccount;
+      defaultrole = cfg.awsDefaultRole;
+    };
+    stats        = cfg.statsAddress;
+    listen       = cfg.listenAddress;
+    cachetimeout = cfg.cacheTimeoutSeconds;
+  });
+in {
+  options = {
+    services.hologram-server = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Hologram server for AWS instance credentials";
+      };
+
+      listenAddress = mkOption {
+        type        = types.str;
+        default     = "0.0.0.0:3100";
+        description = "Address and port to listen on";
+      };
+
+      ldapHost = mkOption {
+        type        = types.str;
+        description = "Address of the LDAP server to use";
+      };
+
+      ldapInsecure = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Whether to connect to LDAP over SSL or not";
+      };
+
+      ldapUserAttr = mkOption {
+        type        = types.str;
+        default     = "cn";
+        description = "The LDAP attribute for usernames";
+      };
+
+      ldapBaseDN = mkOption {
+        type        = types.str;
+        description = "The base DN for your Hologram users";
+      };
+
+      ldapBindDN = mkOption {
+        type        = types.str;
+        description = "DN of account to use to query the LDAP server";
+      };
+
+      ldapBindPassword = mkOption {
+        type        = types.str;
+        description = "Password of account to use to query the LDAP server";
+      };
+
+      enableLdapRoles = mkOption {
+        type        = types.bool;
+        default     = false;
+        description = "Whether to assign user roles based on the user's LDAP group memberships";
+      };
+
+      groupClassAttr = mkOption {
+        type = types.str;
+        default = "groupOfNames";
+        description = "The objectclass attribute to search for groups when enableLdapRoles is true";
+      };
+
+      roleAttr = mkOption {
+        type        = types.str;
+        default     = "businessCategory";
+        description = "Which LDAP group attribute to search for authorized role ARNs";
+      };
+
+      awsAccount = mkOption {
+        type        = types.str;
+        description = "AWS account number";
+      };
+
+      awsDefaultRole = mkOption {
+        type        = types.str;
+        description = "AWS default role";
+      };
+
+      statsAddress = mkOption {
+        type        = types.str;
+        default     = "";
+        description = "Address of statsd server";
+      };
+
+      cacheTimeoutSeconds = mkOption {
+        type        = types.int;
+        default     = 3600;
+        description = "How often (in seconds) to refresh the LDAP cache";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.hologram-server = {
+      description = "Provide EC2 instance credentials to machines outside of EC2";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.hologram}/bin/hologram-server --debug --conf ${cfgFile}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/munge.nix b/nixos/modules/services/security/munge.nix
new file mode 100644
index 00000000000..89178886471
--- /dev/null
+++ b/nixos/modules/services/security/munge.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.munge;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.munge = {
+      enable = mkEnableOption "munge service";
+
+      password = mkOption {
+        default = "/etc/munge/munge.key";
+        type = types.path;
+        description = ''
+          The path to a daemon's secret key.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.munge ];
+
+    users.users.munge = {
+      description   = "Munge daemon user";
+      isSystemUser  = true;
+      group         = "munge";
+    };
+
+    users.groups.munge = {};
+
+    systemd.services.munged = {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = [ pkgs.munge pkgs.coreutils ];
+
+      serviceConfig = {
+        ExecStartPre = "+${pkgs.coreutils}/bin/chmod 0400 ${cfg.password}";
+        ExecStart = "${pkgs.munge}/bin/munged --syslog --key-file ${cfg.password}";
+        PIDFile = "/run/munge/munged.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+        User = "munge";
+        Group = "munge";
+        StateDirectory = "munge";
+        StateDirectoryMode = "0711";
+        RuntimeDirectory = "munge";
+      };
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/security/nginx-sso.nix b/nixos/modules/services/security/nginx-sso.nix
new file mode 100644
index 00000000000..b4de1d36edd
--- /dev/null
+++ b/nixos/modules/services/security/nginx-sso.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.nginx.sso;
+  pkg = getBin cfg.package;
+  configYml = pkgs.writeText "nginx-sso.yml" (builtins.toJSON cfg.configuration);
+in {
+  options.services.nginx.sso = {
+    enable = mkEnableOption "nginx-sso service";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.nginx-sso;
+      defaultText = literalExpression "pkgs.nginx-sso";
+      description = ''
+        The nginx-sso package that should be used.
+      '';
+    };
+
+    configuration = mkOption {
+      type = types.attrsOf types.unspecified;
+      default = {};
+      example = literalExpression ''
+        {
+          listen = { addr = "127.0.0.1"; port = 8080; };
+
+          providers.token.tokens = {
+            myuser = "MyToken";
+          };
+
+          acl = {
+            rule_sets = [
+              {
+                rules = [ { field = "x-application"; equals = "MyApp"; } ];
+                allow = [ "myuser" ];
+              }
+            ];
+          };
+        }
+      '';
+      description = ''
+        nginx-sso configuration
+        (<link xlink:href="https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration">documentation</link>)
+        as a Nix attribute set.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.nginx-sso = {
+      description = "Nginx SSO Backend";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = ''
+          ${pkg}/bin/nginx-sso \
+            --config ${configYml} \
+            --frontend-dir ${pkg}/share/frontend
+        '';
+        Restart = "always";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
new file mode 100644
index 00000000000..ce295bd4ba3
--- /dev/null
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -0,0 +1,591 @@
+# NixOS module for oauth2_proxy.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.oauth2_proxy;
+
+  # oauth2_proxy provides many options that are only relevant if you are using
+  # a certain provider. This set maps from provider name to a function that
+  # takes the configuration and returns a string that can be inserted into the
+  # command-line to launch oauth2_proxy.
+  providerSpecificOptions = {
+    azure = cfg: {
+      azure-tenant = cfg.azure.tenant;
+      resource = cfg.azure.resource;
+    };
+
+    github = cfg: { github = {
+      inherit (cfg.github) org team;
+    }; };
+
+    google = cfg: { google = with cfg.google; optionalAttrs (groups != []) {
+      admin-email = adminEmail;
+      service-account = serviceAccountJSON;
+      group = groups;
+    }; };
+  };
+
+  authenticatedEmailsFile = pkgs.writeText "authenticated-emails" cfg.email.addresses;
+
+  getProviderOptions = cfg: provider: providerSpecificOptions.${provider} or (_: {}) cfg;
+
+  allConfig = with cfg; {
+    inherit (cfg) provider scope upstream;
+    approval-prompt = approvalPrompt;
+    basic-auth-password = basicAuthPassword;
+    client-id = clientID;
+    client-secret = clientSecret;
+    custom-templates-dir = customTemplatesDir;
+    email-domain = email.domains;
+    http-address = httpAddress;
+    login-url = loginURL;
+    pass-access-token = passAccessToken;
+    pass-basic-auth = passBasicAuth;
+    pass-host-header = passHostHeader;
+    reverse-proxy = reverseProxy;
+    proxy-prefix = proxyPrefix;
+    profile-url = profileURL;
+    redeem-url = redeemURL;
+    redirect-url = redirectURL;
+    request-logging = requestLogging;
+    skip-auth-regex = skipAuthRegexes;
+    signature-key = signatureKey;
+    validate-url = validateURL;
+    htpasswd-file = htpasswd.file;
+    cookie = {
+      inherit (cookie) domain secure expire name secret refresh;
+      httponly = cookie.httpOnly;
+    };
+    set-xauthrequest = setXauthrequest;
+  } // lib.optionalAttrs (cfg.email.addresses != null) {
+    authenticated-emails-file = authenticatedEmailsFile;
+  } // lib.optionalAttrs (cfg.passBasicAuth) {
+    basic-auth-password = cfg.basicAuthPassword;
+  } // lib.optionalAttrs (cfg.htpasswd.file != null) {
+    display-htpasswd-file = cfg.htpasswd.displayForm;
+  } // lib.optionalAttrs tls.enable {
+    tls-cert-file = tls.certificate;
+    tls-key-file = tls.key;
+    https-address = tls.httpsAddress;
+  } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
+
+  mapConfig = key: attr:
+  if attr != null && attr != [] then (
+    if isDerivation attr then mapConfig key (toString attr) else
+    if (builtins.typeOf attr) == "set" then concatStringsSep " "
+      (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else
+    if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else
+    if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else
+    if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else
+    "--${key}=${toString attr}")
+    else "";
+
+  configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig);
+in
+{
+  options.services.oauth2_proxy = {
+    enable = mkEnableOption "oauth2_proxy";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.oauth2-proxy;
+      defaultText = literalExpression "pkgs.oauth2-proxy";
+      description = ''
+        The package that provides oauth2-proxy.
+      '';
+    };
+
+    ##############################################
+    # PROVIDER configuration
+    # Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
+    provider = mkOption {
+      type = types.enum [
+        "adfs"
+        "azure"
+        "bitbucket"
+        "digitalocean"
+        "facebook"
+        "github"
+        "gitlab"
+        "google"
+        "keycloak"
+        "keycloak-oidc"
+        "linkedin"
+        "login.gov"
+        "nextcloud"
+        "oidc"
+      ];
+      default = "google";
+      description = ''
+        OAuth provider.
+      '';
+    };
+
+    approvalPrompt = mkOption {
+      type = types.enum ["force" "auto"];
+      default = "force";
+      description = ''
+        OAuth approval_prompt.
+      '';
+    };
+
+    clientID = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        The OAuth Client ID.
+      '';
+      example = "123456.apps.googleusercontent.com";
+    };
+
+    clientSecret = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        The OAuth Client Secret.
+      '';
+    };
+
+    skipAuthRegexes = mkOption {
+     type = types.listOf types.str;
+     default = [];
+     description = ''
+       Skip authentication for requests matching any of these regular
+       expressions.
+     '';
+    };
+
+    # XXX: Not clear whether these two options are mutually exclusive or not.
+    email = {
+      domains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Authenticate emails with the specified domains. Use
+          <literal>*</literal> to authenticate any email.
+        '';
+      };
+
+      addresses = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        description = ''
+          Line-separated email addresses that are allowed to authenticate.
+        '';
+      };
+    };
+
+    loginURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Authentication endpoint.
+
+        You only need to set this if you are using a self-hosted provider (e.g.
+        Github Enterprise). If you're using a publicly hosted provider
+        (e.g github.com), then the default works.
+      '';
+      example = "https://provider.example.com/oauth/authorize";
+    };
+
+    redeemURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Token redemption endpoint.
+
+        You only need to set this if you are using a self-hosted provider (e.g.
+        Github Enterprise). If you're using a publicly hosted provider
+        (e.g github.com), then the default works.
+      '';
+      example = "https://provider.example.com/oauth/token";
+    };
+
+    validateURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Access token validation endpoint.
+
+        You only need to set this if you are using a self-hosted provider (e.g.
+        Github Enterprise). If you're using a publicly hosted provider
+        (e.g github.com), then the default works.
+      '';
+      example = "https://provider.example.com/user/emails";
+    };
+
+    redirectURL = mkOption {
+      # XXX: jml suspects this is always necessary, but the command-line
+      # doesn't require it so making it optional.
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The OAuth2 redirect URL.
+      '';
+      example = "https://internalapp.yourcompany.com/oauth2/callback";
+    };
+
+    azure = {
+      tenant = mkOption {
+        type = types.str;
+        default = "common";
+        description = ''
+          Go to a tenant-specific or common (tenant-independent) endpoint.
+        '';
+      };
+
+      resource = mkOption {
+        type = types.str;
+        description = ''
+          The resource that is protected.
+        '';
+      };
+    };
+
+    google = {
+      adminEmail = mkOption {
+        type = types.str;
+        description = ''
+          The Google Admin to impersonate for API calls.
+
+          Only users with access to the Admin APIs can access the Admin SDK
+          Directory API, thus the service account needs to impersonate one of
+          those users to access the Admin SDK Directory API.
+
+          See <link xlink:href="https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account" />.
+        '';
+      };
+
+      groups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Restrict logins to members of these Google groups.
+        '';
+      };
+
+      serviceAccountJSON = mkOption {
+        type = types.path;
+        description = ''
+          The path to the service account JSON credentials.
+        '';
+      };
+    };
+
+    github = {
+      org = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Restrict logins to members of this organisation.
+        '';
+      };
+
+      team = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Restrict logins to members of this team.
+        '';
+      };
+    };
+
+
+    ####################################################
+    # UPSTREAM Configuration
+    upstream = mkOption {
+      type = with types; coercedTo str (x: [x]) (listOf str);
+      default = [];
+      description = ''
+        The http url(s) of the upstream endpoint or <literal>file://</literal>
+        paths for static files. Routing is based on the path.
+      '';
+    };
+
+    passAccessToken = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Pass OAuth access_token to upstream via X-Forwarded-Access-Token header.
+      '';
+    };
+
+    passBasicAuth = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream.
+      '';
+    };
+
+    basicAuthPassword = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        The password to set when passing the HTTP Basic Auth header.
+      '';
+    };
+
+    passHostHeader = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Pass the request Host Header to upstream.
+      '';
+    };
+
+    signatureKey = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        GAP-Signature request signature key.
+      '';
+      example = "sha1:secret0";
+    };
+
+    cookie = {
+      domain = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Optional cookie domains to force cookies to (ie: `.yourcompany.com`).
+          The longest domain matching the request's host will be used (or the shortest
+          cookie domain if there is no match).
+        '';
+        example = ".yourcompany.com";
+      };
+
+      expire = mkOption {
+        type = types.str;
+        default = "168h0m0s";
+        description = ''
+          Expire timeframe for cookie.
+        '';
+      };
+
+      httpOnly = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Set HttpOnly cookie flag.
+        '';
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "_oauth2_proxy";
+        description = ''
+          The name of the cookie that the oauth_proxy creates.
+        '';
+      };
+
+      refresh = mkOption {
+        # XXX: Unclear what the behavior is when this is not specified.
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Refresh the cookie after this duration; 0 to disable.
+        '';
+        example = "168h0m0s";
+      };
+
+      secret = mkOption {
+        type = types.nullOr types.str;
+        description = ''
+          The seed string for secure cookies.
+        '';
+      };
+
+      secure = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Set secure (HTTPS) cookie flag.
+        '';
+      };
+    };
+
+    ####################################################
+    # OAUTH2 PROXY configuration
+
+    httpAddress = mkOption {
+      type = types.str;
+      default = "http://127.0.0.1:4180";
+      description = ''
+        HTTPS listening address.  This module does not expose the port by
+        default. If you want this URL to be accessible to other machines, please
+        add the port to <literal>networking.firewall.allowedTCPPorts</literal>.
+      '';
+    };
+
+    htpasswd = {
+      file = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = ''
+          Additionally authenticate against a htpasswd file. Entries must be
+          created with <literal>htpasswd -s</literal> for SHA encryption.
+        '';
+      };
+
+      displayForm = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Display username / password login form if an htpasswd file is provided.
+        '';
+      };
+    };
+
+    customTemplatesDir = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        Path to custom HTML templates.
+      '';
+    };
+
+    reverseProxy = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        In case when running behind a reverse proxy, controls whether headers
+        like <literal>X-Real-Ip</literal> are accepted. Usage behind a reverse
+        proxy will require this flag to be set to avoid logging the reverse
+        proxy IP address.
+      '';
+    };
+
+    proxyPrefix = mkOption {
+      type = types.str;
+      default = "/oauth2";
+      description = ''
+        The url root path that this proxy should be nested under.
+      '';
+    };
+
+    tls = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to serve over TLS.
+        '';
+      };
+
+      certificate = mkOption {
+        type = types.path;
+        description = ''
+          Path to certificate file.
+        '';
+      };
+
+      key = mkOption {
+        type = types.path;
+        description = ''
+          Path to private key file.
+        '';
+      };
+
+      httpsAddress = mkOption {
+        type = types.str;
+        default = ":443";
+        description = ''
+          <literal>addr:port</literal> to listen on for HTTPS clients.
+
+          Remember to add <literal>port</literal> to
+          <literal>allowedTCPPorts</literal> if you want other machines to be
+          able to connect to it.
+        '';
+      };
+    };
+
+    requestLogging = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Log requests to stdout.
+      '';
+    };
+
+    ####################################################
+    # UNKNOWN
+
+    # XXX: Is this mandatory? Is it part of another group? Is it part of the provider specification?
+    scope = mkOption {
+      # XXX: jml suspects this is always necessary, but the command-line
+      # doesn't require it so making it optional.
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        OAuth scope specification.
+      '';
+    };
+
+    profileURL = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Profile access endpoint.
+      '';
+    };
+
+    setXauthrequest = mkOption {
+      type = types.nullOr types.bool;
+      default = false;
+      description = ''
+        Set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode). Setting this to 'null' means using the upstream default (false).
+      '';
+    };
+
+    extraConfig = mkOption {
+      default = {};
+      type = types.attrsOf types.anything;
+      description = ''
+        Extra config to pass to oauth2-proxy.
+      '';
+    };
+
+    keyFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = ''
+        oauth2-proxy allows passing sensitive configuration via environment variables.
+        Make a file that contains lines like
+        OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com
+        and specify the path here.
+      '';
+      example = "/run/keys/oauth2_proxy";
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    services.oauth2_proxy = mkIf (cfg.keyFile != null) {
+      clientID = mkDefault null;
+      clientSecret = mkDefault null;
+      cookie.secret = mkDefault null;
+    };
+
+    users.users.oauth2_proxy = {
+      description = "OAuth2 Proxy";
+      isSystemUser = true;
+    };
+
+    systemd.services.oauth2_proxy = {
+      description = "OAuth2 Proxy";
+      path = [ cfg.package ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = "oauth2_proxy";
+        Restart = "always";
+        ExecStart = "${cfg.package}/bin/oauth2-proxy ${configString}";
+        EnvironmentFile = mkIf (cfg.keyFile != null) cfg.keyFile;
+      };
+    };
+
+  };
+}
diff --git a/nixos/modules/services/security/oauth2_proxy_nginx.nix b/nixos/modules/services/security/oauth2_proxy_nginx.nix
new file mode 100644
index 00000000000..5853c5a123c
--- /dev/null
+++ b/nixos/modules/services/security/oauth2_proxy_nginx.nix
@@ -0,0 +1,66 @@
+{ config, lib, ... }:
+with lib;
+let
+  cfg = config.services.oauth2_proxy.nginx;
+in
+{
+  options.services.oauth2_proxy.nginx = {
+    proxy = mkOption {
+      type = types.str;
+      default = config.services.oauth2_proxy.httpAddress;
+      defaultText = literalExpression "config.services.oauth2_proxy.httpAddress";
+      description = ''
+        The address of the reverse proxy endpoint for oauth2_proxy
+      '';
+    };
+    virtualHosts = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        A list of nginx virtual hosts to put behind the oauth2 proxy
+      '';
+    };
+  };
+  config.services.oauth2_proxy = mkIf (cfg.virtualHosts != [] && (hasPrefix "127.0.0.1:" cfg.proxy)) {
+    enable = true;
+  };
+  config.services.nginx = mkIf config.services.oauth2_proxy.enable (mkMerge
+  ((optional (cfg.virtualHosts != []) {
+    recommendedProxySettings = true; # needed because duplicate headers
+  }) ++ (map (vhost: {
+    virtualHosts.${vhost} = {
+      locations."/oauth2/" = {
+        proxyPass = cfg.proxy;
+        extraConfig = ''
+          proxy_set_header X-Scheme                $scheme;
+          proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
+        '';
+      };
+      locations."/oauth2/auth" = {
+        proxyPass = cfg.proxy;
+        extraConfig = ''
+          proxy_set_header X-Scheme         $scheme;
+          # nginx auth_request includes headers but not body
+          proxy_set_header Content-Length   "";
+          proxy_pass_request_body           off;
+        '';
+      };
+      locations."/".extraConfig = ''
+        auth_request /oauth2/auth;
+        error_page 401 = /oauth2/sign_in;
+
+        # pass information via X-User and X-Email headers to backend,
+        # requires running with --set-xauthrequest flag
+        auth_request_set $user   $upstream_http_x_auth_request_user;
+        auth_request_set $email  $upstream_http_x_auth_request_email;
+        proxy_set_header X-User  $user;
+        proxy_set_header X-Email $email;
+
+        # if you enabled --cookie-refresh, this is needed for it to work with auth_request
+        auth_request_set $auth_cookie $upstream_http_set_cookie;
+        add_header Set-Cookie $auth_cookie;
+      '';
+
+    };
+  }) cfg.virtualHosts)));
+}
diff --git a/nixos/modules/services/security/opensnitch.nix b/nixos/modules/services/security/opensnitch.nix
new file mode 100644
index 00000000000..f9b4985e199
--- /dev/null
+++ b/nixos/modules/services/security/opensnitch.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.opensnitch;
+  format = pkgs.formats.json {};
+in {
+  options = {
+    services.opensnitch = {
+      enable = mkEnableOption "Opensnitch application firewall";
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = format.type;
+
+          options = {
+            Server = {
+
+              Address = mkOption {
+                type = types.str;
+                description = ''
+                  Unix socket path (unix:///tmp/osui.sock, the "unix:///" part is
+                  mandatory) or TCP socket (192.168.1.100:50051).
+                '';
+              };
+
+              LogFile = mkOption {
+                type = types.path;
+                description = ''
+                  File to write logs to (use /dev/stdout to write logs to standard
+                  output).
+                '';
+              };
+
+            };
+
+            DefaultAction = mkOption {
+              type = types.enum [ "allow" "deny" ];
+              description = ''
+                Default action whether to block or allow application internet
+                access.
+              '';
+            };
+
+            DefaultDuration = mkOption {
+              type = types.enum [
+                "once" "always" "until restart" "30s" "5m" "15m" "30m" "1h"
+              ];
+              description = ''
+                Default duration of firewall rule.
+              '';
+            };
+
+            InterceptUnknown = mkOption {
+              type = types.bool;
+              description = ''
+                Wheter to intercept spare connections.
+              '';
+            };
+
+            ProcMonitorMethod = mkOption {
+              type = types.enum [ "ebpf" "proc" "ftrace" "audit" ];
+              description = ''
+                Which process monitoring method to use.
+              '';
+            };
+
+            LogLevel = mkOption {
+              type = types.enum [ 0 1 2 3 4 ];
+              description = ''
+                Default log level from 0 to 4 (debug, info, important, warning,
+                error).
+              '';
+            };
+
+            Firewall = mkOption {
+              type = types.enum [ "iptables" "nftables" ];
+              description = ''
+                Which firewall backend to use.
+              '';
+            };
+
+            Stats = {
+
+              MaxEvents = mkOption {
+                type = types.int;
+                description = ''
+                  Max events to send to the GUI.
+                '';
+              };
+
+              MaxStats = mkOption {
+                type = types.int;
+                description = ''
+                  Max stats per item to keep in backlog.
+                '';
+              };
+
+            };
+          };
+        };
+        description = ''
+          opensnitchd configuration. Refer to
+          <link xlink:href="https://github.com/evilsocket/opensnitch/wiki/Configurations"/>
+          for details on supported values.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    # pkg.opensnitch is referred to elsewhere in the module so we don't need to worry about it being garbage collected
+    services.opensnitch.settings = mapAttrs (_: v: mkDefault v) (builtins.fromJSON (builtins.unsafeDiscardStringContext (builtins.readFile "${pkgs.opensnitch}/etc/default-config.json")));
+
+    systemd = {
+      packages = [ pkgs.opensnitch ];
+      services.opensnitchd.wantedBy = [ "multi-user.target" ];
+    };
+
+    environment.etc."opensnitchd/default-config.json".source = format.generate "default-config.json" cfg.settings;
+
+  };
+}
+
diff --git a/nixos/modules/services/security/physlock.nix b/nixos/modules/services/security/physlock.nix
new file mode 100644
index 00000000000..760e80f147f
--- /dev/null
+++ b/nixos/modules/services/security/physlock.nix
@@ -0,0 +1,139 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.physlock;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.physlock = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the <command>physlock</command> screen locking mechanism.
+
+          Enable this and then run <command>systemctl start physlock</command>
+          to securely lock the screen.
+
+          This will switch to a new virtual terminal, turn off console
+          switching and disable SysRq mechanism (when
+          <option>services.physlock.disableSysRq</option> is set)
+          until the root or user password is given.
+        '';
+      };
+
+      allowAnyUser = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to allow any user to lock the screen. This will install a
+          setuid wrapper to allow any user to start physlock as root, which
+          is a minor security risk. Call the physlock binary to use this instead
+          of using the systemd service.
+        '';
+      };
+
+      disableSysRq = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to disable SysRq when locked with physlock.
+        '';
+      };
+
+      lockMessage = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Message to show on physlock login terminal.
+        '';
+      };
+
+      lockOn = {
+
+        suspend = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to lock screen with physlock just before suspend.
+          '';
+        };
+
+        hibernate = mkOption {
+          type = types.bool;
+          default = true;
+          description = ''
+            Whether to lock screen with physlock just before hibernate.
+          '';
+        };
+
+        extraTargets = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          example = [ "display-manager.service" ];
+          description = ''
+            Other targets to lock the screen just before.
+
+            Useful if you want to e.g. both autologin to X11 so that
+            your <filename>~/.xsession</filename> gets executed and
+            still to have the screen locked so that the system can be
+            booted relatively unattended.
+          '';
+        };
+
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+
+      # for physlock -l and physlock -L
+      environment.systemPackages = [ pkgs.physlock ];
+
+      systemd.services.physlock = {
+        enable = true;
+        description = "Physlock";
+        wantedBy = optional cfg.lockOn.suspend   "suspend.target"
+                ++ optional cfg.lockOn.hibernate "hibernate.target"
+                ++ cfg.lockOn.extraTargets;
+        before   = optional cfg.lockOn.suspend   "systemd-suspend.service"
+                ++ optional cfg.lockOn.hibernate "systemd-hibernate.service"
+                ++ optional (cfg.lockOn.hibernate || cfg.lockOn.suspend) "systemd-suspend-then-hibernate.service"
+                ++ cfg.lockOn.extraTargets;
+        serviceConfig = {
+          Type = "forking";
+          ExecStart = "${pkgs.physlock}/bin/physlock -d${optionalString cfg.disableSysRq "s"}${optionalString (cfg.lockMessage != "") " -p \"${cfg.lockMessage}\""}";
+        };
+      };
+
+      security.pam.services.physlock = {};
+
+    }
+
+    (mkIf cfg.allowAnyUser {
+
+      security.wrappers.physlock =
+        { setuid = true;
+          owner = "root";
+          group = "root";
+          source = "${pkgs.physlock}/bin/physlock";
+        };
+
+    })
+  ]);
+
+}
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
new file mode 100644
index 00000000000..b8e2d9a8b0d
--- /dev/null
+++ b/nixos/modules/services/security/privacyidea.nix
@@ -0,0 +1,309 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.privacyidea;
+  opt = options.services.privacyidea;
+
+  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; };
+  python = uwsgi.python3;
+  penv = python.withPackages (const [ pkgs.privacyidea ]);
+  logCfg = pkgs.writeText "privacyidea-log.cfg" ''
+    [formatters]
+    keys=detail
+
+    [handlers]
+    keys=stream
+
+    [formatter_detail]
+    class=privacyidea.lib.log.SecureFormatter
+    format=[%(asctime)s][%(process)d][%(thread)d][%(levelname)s][%(name)s:%(lineno)d] %(message)s
+
+    [handler_stream]
+    class=StreamHandler
+    level=NOTSET
+    formatter=detail
+    args=(sys.stdout,)
+
+    [loggers]
+    keys=root,privacyidea
+
+    [logger_privacyidea]
+    handlers=stream
+    qualname=privacyidea
+    level=INFO
+
+    [logger_root]
+    handlers=stream
+    level=ERROR
+  '';
+
+  piCfgFile = pkgs.writeText "privacyidea.cfg" ''
+    SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
+    SQLALCHEMY_DATABASE_URI = 'postgresql:///privacyidea'
+    SECRET_KEY = '${cfg.secretKey}'
+    PI_PEPPER = '${cfg.pepper}'
+    PI_ENCFILE = '${cfg.encFile}'
+    PI_AUDIT_KEY_PRIVATE = '${cfg.auditKeyPrivate}'
+    PI_AUDIT_KEY_PUBLIC = '${cfg.auditKeyPublic}'
+    PI_LOGCONFIG = '${logCfg}'
+    ${cfg.extraConfig}
+  '';
+
+in
+
+{
+  options = {
+    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";
+        description = ''
+          Directory where all PrivacyIDEA files will be placed by default.
+        '';
+      };
+
+      superuserRealm = mkOption {
+        type = types.listOf types.str;
+        default = [ "super" "administrators" ];
+        description = ''
+          The realm where users are allowed to login as administrators.
+        '';
+      };
+
+      secretKey = mkOption {
+        type = types.str;
+        example = "t0p s3cr3t";
+        description = ''
+          This is used to encrypt the auth_token.
+        '';
+      };
+
+      pepper = mkOption {
+        type = types.str;
+        example = "Never know...";
+        description = ''
+          This is used to encrypt the admin passwords.
+        '';
+      };
+
+      encFile = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/enckey";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/enckey"'';
+        description = ''
+          This is used to encrypt the token data and token passwords
+        '';
+      };
+
+      auditKeyPrivate = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/private.pem";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/private.pem"'';
+        description = ''
+          Private Key for signing the audit log.
+        '';
+      };
+
+      auditKeyPublic = mkOption {
+        type = types.str;
+        default = "${cfg.stateDir}/public.pem";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/public.pem"'';
+        description = ''
+          Public key for checking signatures of the audit log.
+        '';
+      };
+
+      adminPasswordFile = mkOption {
+        type = types.path;
+        description = "File containing password for the admin user";
+      };
+
+      adminEmail = mkOption {
+        type = types.str;
+        example = "admin@example.com";
+        description = "Mail address for the admin user";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration options for pi.cfg.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "privacyidea";
+        description = "User account under which PrivacyIDEA runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "privacyidea";
+        description = "Group account under which PrivacyIDEA runs.";
+      };
+
+      ldap-proxy = {
+        enable = mkEnableOption "PrivacyIDEA LDAP Proxy";
+
+        configFile = mkOption {
+          type = types.path;
+          description = ''
+            Path to PrivacyIDEA LDAP Proxy configuration (proxy.ini).
+          '';
+        };
+
+        user = mkOption {
+          type = types.str;
+          default = "pi-ldap-proxy";
+          description = "User account under which PrivacyIDEA LDAP proxy runs.";
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "pi-ldap-proxy";
+          description = "Group account under which PrivacyIDEA LDAP proxy runs.";
+        };
+      };
+    };
+  };
+
+  config = mkMerge [
+
+    (mkIf cfg.enable {
+
+      environment.systemPackages = [ pkgs.privacyidea ];
+
+      services.postgresql.enable = mkDefault true;
+
+      systemd.services.privacyidea = let
+        piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON {
+          uwsgi = {
+            buffer-size = 8192;
+            plugins = [ "python3" ];
+            pythonpath = "${penv}/${uwsgi.python3.sitePackages}";
+            socket = "/run/privacyidea/socket";
+            uid = cfg.user;
+            gid = cfg.group;
+            chmod-socket = 770;
+            chown-socket = "${cfg.user}:nginx";
+            chdir = cfg.stateDir;
+            wsgi-file = "${penv}/etc/privacyidea/privacyideaapp.wsgi";
+            processes = 4;
+            harakiri = 60;
+            reload-mercy = 8;
+            stats = "/run/privacyidea/stats.socket";
+            max-requests = 2000;
+            limit-as = 1024;
+            reload-on-as = 512;
+            reload-on-rss = 256;
+            no-orphans = true;
+            vacuum = true;
+          };
+        });
+      in {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "postgresql.service" ];
+        path = with pkgs; [ openssl ];
+        environment.PRIVACYIDEA_CONFIGFILE = "${cfg.stateDir}/privacyidea.cfg";
+        preStart = let
+          pi-manage = "${config.security.sudo.package}/bin/sudo -u privacyidea -HE ${penv}/bin/pi-manage";
+          pgsu = config.services.postgresql.superUser;
+          psql = config.services.postgresql.package;
+        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
+            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user}
+            ${config.security.sudo.package}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea
+            ${pi-manage} create_enckey
+            ${pi-manage} create_audit_keys
+            ${pi-manage} createdb
+            ${pi-manage} admin add admin -e ${cfg.adminEmail} -p "$(cat ${cfg.adminPasswordFile})"
+            ${pi-manage} db stamp head -d ${penv}/lib/privacyidea/migrations
+            touch "${cfg.stateDir}/db-created"
+            chmod g+r "${cfg.stateDir}/enckey" "${cfg.stateDir}/private.pem"
+          fi
+          ${pi-manage} db upgrade -d ${penv}/lib/privacyidea/migrations
+        '';
+        serviceConfig = {
+          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";
+        };
+      };
+
+      users.users.privacyidea = mkIf (cfg.user == "privacyidea") {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+
+      users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {};
+    })
+
+    (mkIf cfg.ldap-proxy.enable {
+
+      systemd.services.privacyidea-ldap-proxy = let
+        ldap-proxy-env = pkgs.python3.withPackages (ps: [ ps.privacyidea-ldap-proxy ]);
+      in {
+        description = "privacyIDEA LDAP proxy";
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          User = cfg.ldap-proxy.user;
+          Group = cfg.ldap-proxy.group;
+          ExecStart = ''
+            ${ldap-proxy-env}/bin/twistd \
+              --nodaemon \
+              --pidfile= \
+              -u ${cfg.ldap-proxy.user} \
+              -g ${cfg.ldap-proxy.group} \
+              ldap-proxy \
+              -c ${cfg.ldap-proxy.configFile}
+          '';
+          Restart = "always";
+        };
+      };
+
+      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/shibboleth-sp.nix b/nixos/modules/services/security/shibboleth-sp.nix
new file mode 100644
index 00000000000..fea2a855e20
--- /dev/null
+++ b/nixos/modules/services/security/shibboleth-sp.nix
@@ -0,0 +1,75 @@
+{pkgs, config, lib, ...}:
+
+with lib;
+let
+  cfg = config.services.shibboleth-sp;
+in {
+  options = {
+    services.shibboleth-sp = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the shibboleth service";
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        example = literalExpression ''"''${pkgs.shibboleth-sp}/etc/shibboleth/shibboleth2.xml"'';
+        description = "Path to shibboleth config file";
+      };
+
+      fastcgi.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to include the shibauthorizer and shibresponder FastCGI processes";
+      };
+
+      fastcgi.shibAuthorizerPort = mkOption {
+        type = types.int;
+        default = 9100;
+        description = "Port for shibauthorizer FastCGI proccess to bind to";
+      };
+
+      fastcgi.shibResponderPort = mkOption {
+        type = types.int;
+        default = 9101;
+        description = "Port for shibauthorizer FastCGI proccess to bind to";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.shibboleth-sp = {
+      description = "Provides SSO and federation for web applications";
+      after       = lib.optionals cfg.fastcgi.enable [ "shibresponder.service" "shibauthorizer.service" ];
+      wantedBy    = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.shibboleth-sp}/bin/shibd -F -d ${pkgs.shibboleth-sp} -c ${cfg.configFile}";
+      };
+    };
+
+    systemd.services.shibresponder = mkIf cfg.fastcgi.enable {
+      description = "Provides SSO through Shibboleth via FastCGI";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+      path    	  = [ "${pkgs.spawn_fcgi}" ];
+      environment.SHIBSP_CONFIG = "${cfg.configFile}";
+      serviceConfig = {
+        ExecStart = "${pkgs.spawn_fcgi}/bin/spawn-fcgi -n -p ${toString cfg.fastcgi.shibResponderPort} ${pkgs.shibboleth-sp}/lib/shibboleth/shibresponder";
+      };
+    };
+
+    systemd.services.shibauthorizer = mkIf cfg.fastcgi.enable {
+      description = "Provides SSO through Shibboleth via FastCGI";
+      after       = [ "network.target" ];
+      wantedBy    = [ "multi-user.target" ];
+      path    	  = [ "${pkgs.spawn_fcgi}" ];
+      environment.SHIBSP_CONFIG = "${cfg.configFile}";
+      serviceConfig = {
+        ExecStart = "${pkgs.spawn_fcgi}/bin/spawn-fcgi -n -p ${toString cfg.fastcgi.shibAuthorizerPort} ${pkgs.shibboleth-sp}/lib/shibboleth/shibauthorizer";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ jammerful ];
+}
diff --git a/nixos/modules/services/security/sks.nix b/nixos/modules/services/security/sks.nix
new file mode 100644
index 00000000000..f4911597564
--- /dev/null
+++ b/nixos/modules/services/security/sks.nix
@@ -0,0 +1,146 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sks;
+  sksPkg = cfg.package;
+  dbConfig = pkgs.writeText "DB_CONFIG" ''
+    ${cfg.extraDbConfig}
+  '';
+
+in {
+  meta.maintainers = with maintainers; [ primeos calbrecht jcumming ];
+
+  options = {
+
+    services.sks = {
+
+      enable = mkEnableOption ''
+        SKS (synchronizing key server for OpenPGP) and start the database
+        server. You need to create "''${dataDir}/dump/*.gpg" for the initial
+        import'';
+
+      package = mkOption {
+        default = pkgs.sks;
+        defaultText = literalExpression "pkgs.sks";
+        type = types.package;
+        description = "Which SKS derivation to use.";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/db/sks";
+        example = "/var/lib/sks";
+        # TODO: The default might change to "/var/lib/sks" as this is more
+        # common. There's also https://github.com/NixOS/nixpkgs/issues/26256
+        # and "/var/db" is not FHS compliant (seems to come from BSD).
+        description = ''
+          Data directory (-basedir) for SKS, where the database and all
+          configuration files are located (e.g. KDB, PTree, membership and
+          sksconf).
+        '';
+      };
+
+      extraDbConfig = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          Set contents of the files "KDB/DB_CONFIG" and "PTree/DB_CONFIG" within
+          the ''${dataDir} directory. This is used to configure options for the
+          database for the sks key server.
+
+          Documentation of available options are available in the file named
+          "sampleConfig/DB_CONFIG" in the following repository:
+          https://bitbucket.org/skskeyserver/sks-keyserver/src
+        '';
+      };
+
+      hkpAddress = mkOption {
+        default = [ "127.0.0.1" "::1" ];
+        type = types.listOf types.str;
+        description = ''
+          Domain names, IPv4 and/or IPv6 addresses to listen on for HKP
+          requests.
+        '';
+      };
+
+      hkpPort = mkOption {
+        default = 11371;
+        type = types.ints.u16;
+        description = "HKP port to listen on.";
+      };
+
+      webroot = mkOption {
+        type = types.nullOr types.path;
+        default = "${sksPkg.webSamples}/OpenPKG";
+        defaultText = literalExpression ''"''${package.webSamples}/OpenPKG"'';
+        description = ''
+          Source directory (will be symlinked, if not null) for the files the
+          built-in webserver should serve. SKS (''${pkgs.sks.webSamples})
+          provides the following examples: "HTML5", "OpenPKG", and "XHTML+ES".
+          The index file can be named index.html, index.htm, index.xhtm, or
+          index.xhtml. Files with the extensions .css, .es, .js, .jpg, .jpeg,
+          .png, or .gif are supported. Subdirectories and filenames with
+          anything other than alphanumeric characters and the '.' character
+          will be ignored.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users = {
+      users.sks = {
+        isSystemUser = true;
+        description = "SKS user";
+        home = cfg.dataDir;
+        createHome = true;
+        group = "sks";
+        useDefaultShell = true;
+        packages = [ sksPkg pkgs.db ];
+      };
+      groups.sks = { };
+    };
+
+    systemd.services = let
+      hkpAddress = "'" + (builtins.concatStringsSep " " cfg.hkpAddress) + "'" ;
+      hkpPort = builtins.toString cfg.hkpPort;
+    in {
+      sks-db = {
+        description = "SKS database server";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        preStart = ''
+          ${lib.optionalString (cfg.webroot != null)
+            "ln -sfT \"${cfg.webroot}\" web"}
+          mkdir -p dump
+          ${sksPkg}/bin/sks build dump/*.gpg -n 10 -cache 100 || true #*/
+          ${sksPkg}/bin/sks cleandb || true
+          ${sksPkg}/bin/sks pbuild -cache 20 -ptree_cache 70 || true
+          # Check that both database configs are symlinks before overwriting them
+          # TODO: The initial build will be without DB_CONFIG, but this will
+          # hopefully not cause any significant problems. It might be better to
+          # create both directories manually but we have to check that this does
+          # not affect the initial build of the DB.
+          for CONFIG_FILE in KDB/DB_CONFIG PTree/DB_CONFIG; do
+            if [ -e $CONFIG_FILE ] && [ ! -L $CONFIG_FILE ]; then
+              echo "$CONFIG_FILE exists but is not a symlink." >&2
+              echo "Please remove $PWD/$CONFIG_FILE manually to continue." >&2
+              exit 1
+            fi
+            ln -sf ${dbConfig} $CONFIG_FILE
+          done
+        '';
+        serviceConfig = {
+          WorkingDirectory = "~";
+          User = "sks";
+          Group = "sks";
+          Restart = "always";
+          ExecStart = "${sksPkg}/bin/sks db -hkp_address ${hkpAddress} -hkp_port ${hkpPort}";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/sshguard.nix b/nixos/modules/services/security/sshguard.nix
new file mode 100644
index 00000000000..53bd9efa5ac
--- /dev/null
+++ b/nixos/modules/services/security/sshguard.nix
@@ -0,0 +1,161 @@
+{ config, lib, pkgs, ... }:
+
+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
+
+  options = {
+
+    services.sshguard = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to enable the sshguard service.";
+      };
+
+      attack_threshold = mkOption {
+        default = 30;
+        type = types.int;
+        description = ''
+            Block attackers when their cumulative attack score exceeds threshold. Most attacks have a score of 10.
+          '';
+      };
+
+      blacklist_threshold = mkOption {
+        default = null;
+        example = 120;
+        type = types.nullOr types.int;
+        description = ''
+            Blacklist an attacker when its score exceeds threshold. Blacklisted addresses are loaded from and added to blacklist-file.
+          '';
+      };
+
+      blacklist_file = mkOption {
+        default = "/var/lib/sshguard/blacklist.db";
+        type = types.path;
+        description = ''
+            Blacklist an attacker when its score exceeds threshold. Blacklisted addresses are loaded from and added to blacklist-file.
+          '';
+      };
+
+      blocktime = mkOption {
+        default = 120;
+        type = types.int;
+        description = ''
+            Block attackers for initially blocktime seconds after exceeding threshold. Subsequent blocks increase by a factor of 1.5.
+
+            sshguard unblocks attacks at random intervals, so actual block times will be longer.
+          '';
+      };
+
+      detection_time = mkOption {
+        default = 1800;
+        type = types.int;
+        description = ''
+            Remember potential attackers for up to detection_time seconds before resetting their score.
+          '';
+      };
+
+      whitelist = mkOption {
+        default = [ ];
+        example = [ "198.51.100.56" "198.51.100.2" ];
+        type = types.listOf types.str;
+        description = ''
+            Whitelist a list of addresses, hostnames, or address blocks.
+          '';
+      };
+
+      services = mkOption {
+        default = [ "sshd" ];
+        example = [ "sshd" "exim" ];
+        type = types.listOf types.str;
+        description = ''
+            Systemd services sshguard should receive logs of.
+          '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.etc."sshguard.conf".source = configFile;
+
+    systemd.services.sshguard = {
+      description = "SSHGuard brute-force attacks protection system";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      partOf = optional config.networking.firewall.enable "firewall.service";
+
+      restartTriggers = [ configFile ];
+
+      path = with pkgs; if config.networking.nftables.enable
+        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
+      # necessary, but if we let sshguard do it, we can't reliably add
+      # the iptables rules because postStart races with the creation
+      # of the ipsets. So instead, we create both the ipsets and
+      # firewall rules before sshguard starts.
+      preStart = optionalString config.networking.firewall.enable ''
+        ${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:net family inet
+        ${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.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
+      '';
+
+      unitConfig.Documentation = "man:sshguard(8)";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = let
+          args = lib.concatStringsSep " " ([
+            "-a ${toString cfg.attack_threshold}"
+            "-p ${toString cfg.blocktime}"
+            "-s ${toString cfg.detection_time}"
+            (optionalString (cfg.blacklist_threshold != null) "-b ${toString cfg.blacklist_threshold}:${cfg.blacklist_file}")
+          ] ++ (map (name: "-w ${escapeShellArg name}") cfg.whitelist));
+        in "${pkgs.sshguard}/bin/sshguard ${args}";
+        Restart = "always";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+        RuntimeDirectory = "sshguard";
+        StateDirectory = "sshguard";
+        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/security/step-ca.nix b/nixos/modules/services/security/step-ca.nix
new file mode 100644
index 00000000000..95183078d7b
--- /dev/null
+++ b/nixos/modules/services/security/step-ca.nix
@@ -0,0 +1,146 @@
+{ 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;
+        defaultText = lib.literalExpression "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 = {
+          User = "step-ca";
+          Group = "step-ca";
+          UMask = "0077";
+          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 yet
+          # PrivateUsers = true; # doesn't work with privileged ports therefore not supported by upstream
+
+          DynamicUser = true;
+          StateDirectory = "step-ca";
+        };
+      };
+
+      users.users.step-ca = {
+        home = "/var/lib/step-ca";
+        group = "step-ca";
+        isSystemUser = true;
+      };
+
+      users.groups.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
new file mode 100644
index 00000000000..a5822c02794
--- /dev/null
+++ b/nixos/modules/services/security/tor.nix
@@ -0,0 +1,1067 @@
+{ config, lib, options, pkgs, ... }:
+
+with builtins;
+with lib;
+
+let
+  cfg = config.services.tor;
+  opt = options.services.tor;
+  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 = [];
+    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}";
+        };
+      }))
+    ];
+  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;
+  };
+
+  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" "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 = 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 = literalExpression "pkgs.tor";
+        description = "Tor package to use.";
+      };
+
+      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 = mkEnableOption ''control socket,
+        created in <literal>${runDir}/control</literal>'';
+
+      client = {
+        enable = mkEnableOption ''the routing of application connections.
+          You might want to disable this if you plan running a dedicated Tor relay'';
+
+        transparentProxy.enable = mkEnableOption "transparent proxy";
+        dns.enable = mkEnableOption "DNS resolver";
+
+        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.
+          '';
+        };
+
+        onionServices = mkOption {
+          description = descriptionGeneric "HiddenServiceDir";
+          default = {};
+          example = {
+            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" = {
+              clientAuthorizations = ["/run/keys/tor/alice.prv.x25519"];
+            };
+          };
+          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 = mkEnableOption ''relaying of Tor traffic for others.
+
+          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.settings.ORPort</option>
+          options'';
+
+        role = mkOption {
+          type = types.enum [ "exit" "relay" "bridge" "private-bridge" ];
+          description = ''
+            Your role in Tor network. There're several options:
+
+            <variablelist>
+            <varlistentry>
+              <term><literal>exit</literal></term>
+              <listitem>
+                <para>
+                  An exit relay. This allows Tor users to access regular
+                  Internet services through your public IP.
+                </para>
+
+                <important><para>
+                  Running an exit relay may expose you to abuse
+                  complaints. See
+                  <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>settings.ExitPolicy</option> option.
+                </para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><literal>relay</literal></term>
+              <listitem>
+                <para>
+                  Regular relay. This allows Tor users to relay onion
+                  traffic to other Tor nodes, but not to public
+                  Internet.
+                </para>
+
+                <important><para>
+                  Note that some misconfigured and/or disrespectful
+                  towards privacy sites will block you even if your
+                  relay is not an exit relay. That is, just being listed
+                  in a public relay directory can have unwanted
+                  consequences.
+
+                  Which means you might not want to use
+                  this role if you browse public Internet from the same
+                  network as your relay, unless you want to write
+                  e-mails to those sites (you should!).
+                </para></important>
+
+                <para>
+                  See
+                  <link xlink:href="https://www.torproject.org/docs/tor-doc-relay.html.en" />
+                  for more info.
+                </para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><literal>bridge</literal></term>
+              <listitem>
+                <para>
+                  Regular bridge. Works like a regular relay, but
+                  doesn't list you in the public relay directory and
+                  hides your Tor node behind obfs4proxy.
+                </para>
+
+                <para>
+                  Using this option will make Tor advertise your bridge
+                  to users through various mechanisms like
+                  <link xlink:href="https://bridges.torproject.org/" />, though.
+                </para>
+
+                <important>
+                  <para>
+                    WARNING: THE FOLLOWING PARAGRAPH IS NOT LEGAL ADVICE.
+                    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).
+                  </para>
+                </important>
+
+                <para>
+                  See <link xlink:href="https://www.torproject.org/docs/bridges.html.en" />
+                  for more info.
+                </para>
+              </listitem>
+            </varlistentry>
+
+            <varlistentry>
+              <term><literal>private-bridge</literal></term>
+              <listitem>
+                <para>
+                  Private bridge. Works like regular bridge, but does
+                  not advertise your node in any way.
+                </para>
+
+                <para>
+                  Using this role means that you won't contribute to Tor
+                  network in any way unless you advertise your node
+                  yourself in some way.
+                </para>
+
+                <para>
+                  Use this if you want to run a private bridge, for
+                  example because you'll give out your bridge addr
+                  manually to your friends.
+                </para>
+
+                <para>
+                  Switching to this role after measurable time in
+                  "bridge" role is pretty useless as some Tor users
+                  would have learned about your node already. In the
+                  latter case you can still change
+                  <option>port</option> option.
+                </para>
+
+                <para>
+                  See <link xlink:href="https://www.torproject.org/docs/bridges.html.en" />
+                  for more info.
+                </para>
+              </listitem>
+            </varlistentry>
+            </variablelist>
+          '';
+        };
+
+        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;
+            };
+          }));
+        };
+      };
+
+      settings = mkOption {
+        description = ''
+          See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en">torrc manual</link>
+          for documentation.
+        '';
+        default = {};
+        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";
+              }
+            ];
+          };
+          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.ShutdownWaitLength = mkOption {
+            type = types.int;
+            default = 30;
+            description = descriptionGeneric "ShutdownWaitLength";
+          };
+          options.SocksPolicy = optionStrings "SocksPolicy" // {
+            example = ["accept *:*"];
+          };
+          options.SOCKSPort = mkOption {
+            description = descriptionGeneric "SOCKSPort";
+            default = if cfg.settings.HiddenServiceNonAnonymousMode == true then [{port = 0;}] else [];
+            defaultText = literalExpression ''
+              if config.${opt.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";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # Not sure if `cfg.relay.role == "private-bridge"` helps as tor
+    # sends a lot of stats
+    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        = stateDir;
+        group       = "tor";
+        uid         = config.ids.uids.tor;
+      };
+
+    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) {
+        # Make sure application connections via SOCKS are disabled
+        # when services.tor.client.enable is false
+        SOCKSPort = mkForce [ 0 ];
+      })
+      (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";
+        }
+      ))
+    ];
+
+    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
+        ]);
+    };
+
+    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} | head -1)"
+                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 = cfg.settings.ShutdownWaitLength + 30; # Wait a bit longer than ShutdownWaitLength before actually timing out
+        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" ] ++
+          optionals config.services.resolved.enable [
+            "/run/systemd/resolve/stub-resolv.conf"
+            "/run/systemd/resolve/resolv.conf"
+          ];
+        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;
+        ProcSubset = "pid";
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
+        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/torify.nix b/nixos/modules/services/security/torify.nix
new file mode 100644
index 00000000000..39551190dd3
--- /dev/null
+++ b/nixos/modules/services/security/torify.nix
@@ -0,0 +1,80 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+
+  cfg = config.services.tor;
+
+  torify = pkgs.writeTextFile {
+    name = "tsocks";
+    text = ''
+        #!${pkgs.runtimeShell}
+        TSOCKS_CONF_FILE=${pkgs.writeText "tsocks.conf" cfg.tsocks.config} LD_PRELOAD="${pkgs.tsocks}/lib/libtsocks.so $LD_PRELOAD" "$@"
+    '';
+    executable = true;
+    destination = "/bin/tsocks";
+  };
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.tor.tsocks = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to build tsocks wrapper script to relay application traffic via Tor.
+
+          <important>
+            <para>You shouldn't use this unless you know what you're
+            doing because your installation of Tor already comes with
+            its own superior (doesn't leak DNS queries)
+            <literal>torsocks</literal> wrapper which does pretty much
+            exactly the same thing as this.</para>
+          </important>
+        '';
+      };
+
+      server = mkOption {
+        type = types.str;
+        default = "localhost:9050";
+        example = "192.168.0.20";
+        description = ''
+          IP address of TOR client to use.
+        '';
+      };
+
+      config = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration. Contents will be added verbatim to TSocks
+          configuration file.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.tsocks.enable {
+
+    environment.systemPackages = [ torify ];  # expose it to the users
+
+    services.tor.tsocks.config = ''
+      server = ${toString(head (splitString ":" cfg.tsocks.server))}
+      server_port = ${toString(tail (splitString ":" cfg.tsocks.server))}
+
+      local = 127.0.0.0/255.128.0.0
+      local = 127.128.0.0/255.192.0.0
+    '';
+  };
+
+}
diff --git a/nixos/modules/services/security/torsocks.nix b/nixos/modules/services/security/torsocks.nix
new file mode 100644
index 00000000000..fdd6ac32cc6
--- /dev/null
+++ b/nixos/modules/services/security/torsocks.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.tor.torsocks;
+  optionalNullStr = b: v: optionalString (b != null) v;
+
+  configFile = server: ''
+    TorAddress ${toString (head (splitString ":" server))}
+    TorPort    ${toString (tail (splitString ":" server))}
+
+    OnionAddrRange ${cfg.onionAddrRange}
+
+    ${optionalNullStr cfg.socks5Username
+        "SOCKS5Username ${cfg.socks5Username}"}
+    ${optionalNullStr cfg.socks5Password
+        "SOCKS5Password ${cfg.socks5Password}"}
+
+    AllowInbound ${if cfg.allowInbound then "1" else "0"}
+  '';
+
+  wrapTorsocks = name: server: pkgs.writeTextFile {
+    name = name;
+    text = ''
+        #!${pkgs.runtimeShell}
+        TORSOCKS_CONF_FILE=${pkgs.writeText "torsocks.conf" (configFile server)} ${pkgs.torsocks}/bin/torsocks "$@"
+    '';
+    executable = true;
+    destination = "/bin/${name}";
+  };
+
+in
+{
+  options = {
+    services.tor.torsocks = {
+      enable = mkOption {
+        type        = types.bool;
+        default     = config.services.tor.enable && config.services.tor.client.enable;
+        defaultText = literalExpression "config.services.tor.enable && config.services.tor.client.enable";
+        description = ''
+          Whether to build <literal>/etc/tor/torsocks.conf</literal>
+          containing the specified global torsocks configuration.
+        '';
+      };
+
+      server = mkOption {
+        type    = types.str;
+        default = "127.0.0.1:9050";
+        example = "192.168.0.20:1234";
+        description = ''
+          IP/Port of the Tor SOCKS server. Currently, hostnames are
+          NOT supported by torsocks.
+        '';
+      };
+
+      fasterServer = mkOption {
+        type    = types.str;
+        default = "127.0.0.1:9063";
+        example = "192.168.0.20:1234";
+        description = ''
+          IP/Port of the Tor SOCKS server for torsocks-faster wrapper suitable for HTTP.
+          Currently, hostnames are NOT supported by torsocks.
+        '';
+      };
+
+      onionAddrRange = mkOption {
+        type    = types.str;
+        default = "127.42.42.0/24";
+        description = ''
+          Tor hidden sites do not have real IP addresses. This
+          specifies what range of IP addresses will be handed to the
+          application as "cookies" for .onion names.  Of course, you
+          should pick a block of addresses which you aren't going to
+          ever need to actually connect to. This is similar to the
+          MapAddress feature of the main tor daemon.
+        '';
+      };
+
+      socks5Username = mkOption {
+        type    = types.nullOr types.str;
+        default = null;
+        example = "bob";
+        description = ''
+          SOCKS5 username. The <literal>TORSOCKS_USERNAME</literal>
+          environment variable overrides this option if it is set.
+        '';
+      };
+
+      socks5Password = mkOption {
+        type    = types.nullOr types.str;
+        default = null;
+        example = "sekret";
+        description = ''
+          SOCKS5 password. The <literal>TORSOCKS_PASSWORD</literal>
+          environment variable overrides this option if it is set.
+        '';
+      };
+
+      allowInbound = mkOption {
+        type    = types.bool;
+        default = false;
+        description = ''
+          Set Torsocks to accept inbound connections. If set to
+          <literal>true</literal>, listen() and accept() will be
+          allowed to be used with non localhost address.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.torsocks (wrapTorsocks "torsocks-faster" cfg.fasterServer) ];
+
+    environment.etc."tor/torsocks.conf" =
+      {
+        source = pkgs.writeText "torsocks.conf" (configFile cfg.server);
+      };
+  };
+}
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
new file mode 100644
index 00000000000..201b37f17ba
--- /dev/null
+++ b/nixos/modules/services/security/usbguard.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.usbguard;
+
+  # valid policy options
+  policy = (types.enum [ "allow" "block" "reject" "keep" "apply-policy" ]);
+
+  defaultRuleFile = "/var/lib/usbguard/rules.conf";
+
+  # decide what file to use for rules
+  ruleFile = if cfg.rules != null then pkgs.writeText "usbguard-rules" cfg.rules else defaultRuleFile;
+
+  daemonConf = ''
+    # generated by nixos/modules/services/security/usbguard.nix
+    RuleFile=${ruleFile}
+    ImplicitPolicyTarget=${cfg.implictPolicyTarget}
+    PresentDevicePolicy=${cfg.presentDevicePolicy}
+    PresentControllerPolicy=${cfg.presentControllerPolicy}
+    InsertedDevicePolicy=${cfg.insertedDevicePolicy}
+    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=${boolToString cfg.deviceRulesWithPort}
+    # HACK: that way audit logs still land in the journal
+    AuditFilePath=/dev/null
+  '';
+
+  daemonConfFile = pkgs.writeText "usbguard-daemon-conf" daemonConf;
+
+in
+{
+
+  ###### interface
+
+  options = {
+    services.usbguard = {
+      enable = mkEnableOption "USBGuard daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.usbguard;
+        defaultText = literalExpression "pkgs.usbguard";
+        description = ''
+          The usbguard package to use. If you do not need the Qt GUI, use
+          <literal>pkgs.usbguard-nox</literal> to save disk space.
+        '';
+      };
+
+      rules = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        example = ''
+          allow with-interface equals { 08:*:* }
+        '';
+        description = ''
+          The USBGuard daemon will load this as the policy rule set.
+          As these rules are NixOS managed they are immutable and can't
+          be changed by the IPC interface.
+
+          If you do not set this option, the USBGuard daemon will load
+          it's policy rule set from <literal>${defaultRuleFile}</literal>.
+          This file can be changed manually or via the IPC interface.
+
+          Running <literal>usbguard generate-policy</literal> as root will
+          generate a config for your currently plugged in devices.
+
+          For more details see <citerefentry>
+          <refentrytitle>usbguard-rules.conf</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry>.
+        '';
+      };
+
+      implictPolicyTarget = mkOption {
+        type = policy;
+        default = "block";
+        description = ''
+          How to treat USB devices that don't match any rule in the policy.
+          Target should be one of allow, block or reject (logically remove the
+          device node from the system).
+        '';
+      };
+
+      presentDevicePolicy = mkOption {
+        type = policy;
+        default = "apply-policy";
+        description = ''
+          How to treat USB devices that are already connected when the daemon
+          starts. Policy should be one of allow, block, reject, keep (keep
+          whatever state the device is currently in) or apply-policy (evaluate
+          the rule set for every present device).
+        '';
+      };
+
+      presentControllerPolicy = mkOption {
+        type = policy;
+        default = "keep";
+        description = ''
+          How to treat USB controller devices that are already connected when
+          the daemon starts. One of allow, block, reject, keep or apply-policy.
+        '';
+      };
+
+      insertedDevicePolicy = mkOption {
+        type = policy;
+        default = "apply-policy";
+        description = ''
+          How to treat USB devices that are already connected after the daemon
+          starts. One of block, reject, apply-policy.
+        '';
+      };
+
+      restoreControllerDeviceState = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          The  USBGuard  daemon  modifies  some attributes of controller
+          devices like the default authorization state of new child device
+          instances. Using this setting, you can controll whether the daemon
+          will try to restore the attribute values to the state before
+          modificaton on shutdown.
+        '';
+      };
+
+      IPCAllowedUsers = mkOption {
+        type = types.listOf types.str;
+        default = [ "root" ];
+        example = [ "root" "yourusername" ];
+        description = ''
+          A list of usernames that the daemon will accept IPC connections from.
+        '';
+      };
+
+      IPCAllowedGroups = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "wheel" ];
+        description = ''
+          A list of groupnames that the daemon will accept IPC connections
+          from.
+        '';
+      };
+
+      deviceRulesWithPort = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Generate device specific rules including the "via-port" attribute.
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.usbguard = {
+      description = "USBGuard daemon";
+
+      wantedBy = [ "basic.target" ];
+      wants = [ "systemd-udevd.service" ];
+
+      # make sure an empty rule file exists
+      preStart = ''[ -f "${ruleFile}" ] || touch ${ruleFile}'';
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
+        Restart = "on-failure";
+
+        StateDirectory = [
+          "usbguard"
+          "usbguard/IPCAccessControl.d"
+        ];
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
+        DeviceAllow = "/dev/null rw";
+        DevicePolicy = "strict";
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelModules = true;
+        ProtectSystem = true;
+        ReadOnlyPaths = "-/";
+        ReadWritePaths = "-/dev/shm -/tmp";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_NETLINK" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
+      };
+    };
+  };
+  imports = [
+    (mkRemovedOptionModule [ "services" "usbguard" "ruleFile" ] "The usbguard module now uses ${defaultRuleFile} as ruleFile. Alternatively, use services.usbguard.rules to configure rules.")
+    (mkRemovedOptionModule [ "services" "usbguard" "IPCAccessControlFiles" ] "The usbguard module now hardcodes IPCAccessControlFiles to /var/lib/usbguard/IPCAccessControl.d.")
+    (mkRemovedOptionModule [ "services" "usbguard" "auditFilePath" ] "Removed usbguard module audit log files. Audit logs can be found in the systemd journal.")
+  ];
+}
diff --git a/nixos/modules/services/security/vault.nix b/nixos/modules/services/security/vault.nix
new file mode 100644
index 00000000000..d48bc472cb8
--- /dev/null
+++ b/nixos/modules/services/security/vault.nix
@@ -0,0 +1,204 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.vault;
+  opt = options.services.vault;
+
+  configFile = pkgs.writeText "vault.hcl" ''
+    listener "tcp" {
+      address = "${cfg.address}"
+      ${if (cfg.tlsCertFile == null || cfg.tlsKeyFile == null) then ''
+          tls_disable = "true"
+        '' else ''
+          tls_cert_file = "${cfg.tlsCertFile}"
+          tls_key_file = "${cfg.tlsKeyFile}"
+        ''}
+      ${cfg.listenerExtraConfig}
+    }
+    storage "${cfg.storageBackend}" {
+      ${optionalString (cfg.storagePath   != null) ''path = "${cfg.storagePath}"''}
+      ${optionalString (cfg.storageConfig != null) cfg.storageConfig}
+    }
+    ${optionalString (cfg.telemetryConfig != "") ''
+        telemetry {
+          ${cfg.telemetryConfig}
+        }
+      ''}
+    ${cfg.extraConfig}
+  '';
+
+  allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
+
+  configOptions = escapeShellArgs (concatMap (p: ["-config" p]) allConfigPaths);
+
+in
+
+{
+  options = {
+    services.vault = {
+      enable = mkEnableOption "Vault daemon";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.vault;
+        defaultText = literalExpression "pkgs.vault";
+        description = "This option specifies the vault package to use.";
+      };
+
+      address = mkOption {
+        type = types.str;
+        default = "127.0.0.1:8200";
+        description = "The name of the ip interface to listen to";
+      };
+
+      tlsCertFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/cert.pem";
+        description = "TLS certificate file. TLS will be disabled unless this option is set";
+      };
+
+      tlsKeyFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        example = "/path/to/your/key.pem";
+        description = "TLS private key file. TLS will be disabled unless this option is set";
+      };
+
+      listenerExtraConfig = mkOption {
+        type = types.lines;
+        default = ''
+          tls_min_version = "tls12"
+        '';
+        description = "Extra text appended to the listener section.";
+      };
+
+      storageBackend = mkOption {
+        type = types.enum [ "inmem" "file" "consul" "zookeeper" "s3" "azure" "dynamodb" "etcd" "mssql" "mysql" "postgresql" "swift" "gcs" "raft" ];
+        default = "inmem";
+        description = "The name of the type of storage backend";
+      };
+
+      storagePath = mkOption {
+        type = types.nullOr types.path;
+        default = if cfg.storageBackend == "file" then "/var/lib/vault" else null;
+        defaultText = literalExpression ''
+          if config.${opt.storageBackend} == "file"
+          then "/var/lib/vault"
+          else null
+        '';
+        description = "Data directory for file backend";
+      };
+
+      storageConfig = mkOption {
+        type = types.nullOr types.lines;
+        default = null;
+        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 {
+        type = types.lines;
+        default = "";
+        description = "Telemetry configuration";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        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>
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.storageBackend == "inmem" -> (cfg.storagePath == null && cfg.storageConfig == null);
+        message = ''The "inmem" storage expects no services.vault.storagePath nor services.vault.storageConfig'';
+      }
+      { assertion = (cfg.storageBackend == "file" -> (cfg.storagePath != null && cfg.storageConfig == null)) && (cfg.storagePath != null -> cfg.storageBackend == "file");
+        message = ''You must set services.vault.storagePath only when using the "file" backend'';
+      }
+    ];
+
+    users.users.vault = {
+      name = "vault";
+      group = "vault";
+      uid = config.ids.uids.vault;
+      description = "Vault daemon user";
+    };
+    users.groups.vault.gid = config.ids.gids.vault;
+
+    systemd.tmpfiles.rules = optional (cfg.storagePath != null)
+      "d '${cfg.storagePath}' 0700 vault vault - -";
+
+    systemd.services.vault = {
+      description = "Vault server daemon";
+
+      wantedBy = ["multi-user.target"];
+      after = [ "network.target" ]
+           ++ optional (config.services.consul.enable && cfg.storageBackend == "consul") "consul.service";
+
+      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 ${configOptions}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectSystem = "full";
+        ProtectHome = "read-only";
+        AmbientCapabilities = "cap_ipc_lock";
+        NoNewPrivileges = true;
+        KillSignal = "SIGINT";
+        TimeoutStopSec = "30s";
+        Restart = "on-failure";
+      };
+
+      unitConfig.RequiresMountsFor = optional (cfg.storagePath != null) cfg.storagePath;
+    };
+  };
+
+}
diff --git a/nixos/modules/services/security/vaultwarden/backup.sh b/nixos/modules/services/security/vaultwarden/backup.sh
new file mode 100644
index 00000000000..2a3de0ab1de
--- /dev/null
+++ b/nixos/modules/services/security/vaultwarden/backup.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+
+# 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
+fi
+
+if [[ ! -f "$DATA_FOLDER"/db.sqlite3 ]]; then
+  echo "Could not find SQLite database file '$DATA_FOLDER/db.sqlite3'" >&2
+  exit 1
+fi
+
+sqlite3 "$DATA_FOLDER"/db.sqlite3 ".backup '$BACKUP_FOLDER/db.sqlite3'"
+cp "$DATA_FOLDER"/rsa_key.{der,pem,pub.der} "$BACKUP_FOLDER"
+cp -r "$DATA_FOLDER"/attachments "$BACKUP_FOLDER"
+cp -r "$DATA_FOLDER"/icon_cache "$BACKUP_FOLDER"
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
new file mode 100644
index 00000000000..8277f493639
--- /dev/null
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -0,0 +1,185 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  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:
+    let
+      parts = builtins.split "([A-Z0-9]+)" name;
+      partsToEnvVar = parts: foldl' (key: x: let last = stringLength key - 1; in
+        if isList x then key + optionalString (key != "" && substring last 1 key != "_") "_" + head x
+        else if key != "" && elem (substring 0 1 x) lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
+          substring 0 last key + optionalString (substring (last - 1) 1 key != "_") "_" + substring last 1 key + toUpper x
+        else key + toUpper x) "" parts;
+    in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
+
+  # Due to the different naming schemes allowed for config keys,
+  # we can only check for values consistently after converting them to their corresponding environment variable name.
+  configEnv =
+    let
+      configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
+        if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
+      ) cfg.config));
+    in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
+      WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
+    } // configEnv;
+
+  configFile = pkgs.writeText "vaultwarden.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
+
+  vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
+
+in {
+  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 vaultwarden will be using.
+      '';
+    };
+
+    backupDir = mkOption {
+      type = nullOr str;
+      default = null;
+      description = ''
+        The directory under which vaultwarden will backup its persistent data.
+      '';
+    };
+
+    config = mkOption {
+      type = attrsOf (nullOr (oneOf [ bool int str ]));
+      default = {};
+      example = literalExpression ''
+        {
+          domain = "https://bw.domain.tld:8443";
+          signupsAllowed = true;
+          rocketPort = 8222;
+          rocketLog = "critical";
+        }
+      '';
+      description = ''
+        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,
+        so foo2 would be converted to FOO_2.
+        Names already in this format remain unchanged, so FOO2 remains FOO2 if passed as such,
+        even though foo2 would have been converted to FOO_2.
+        This allows working around any potential future conflicting naming conventions.
+
+        Based on the attributes passed to this config option an environment file will be generated
+        that is passed to vaultwarden's systemd service.
+
+        The available configuration options can be found in
+        <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 = literalExpression "pkgs.vaultwarden";
+      description = "Vaultwarden package to use.";
+    };
+
+    webVaultPackage = mkOption {
+      type = package;
+      default = pkgs.vaultwarden-vault;
+      defaultText = literalExpression "pkgs.vaultwarden-vault";
+      description = "Web vault package to use.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
+      message = "Backups for database backends other than sqlite will need customization";
+    } ];
+
+    users.users.vaultwarden = {
+      inherit group;
+      isSystemUser = true;
+    };
+    users.groups.vaultwarden = { };
+
+    systemd.services.vaultwarden = {
+      aliases = [ "bitwarden_rs.service" ];
+      after = [ "network.target" ];
+      path = with pkgs; [ openssl ];
+      serviceConfig = {
+        User = user;
+        Group = group;
+        EnvironmentFile = [ configFile ] ++ optional (cfg.environmentFile != null) cfg.environmentFile;
+        ExecStart = "${vaultwarden}/bin/vaultwarden";
+        LimitNOFILE = "1048576";
+        PrivateTmp = "true";
+        PrivateDevices = "true";
+        ProtectHome = "true";
+        ProtectSystem = "strict";
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+        StateDirectory = "bitwarden_rs";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.services.backup-vaultwarden = mkIf (cfg.backupDir != null) {
+      aliases = [ "backup-bitwarden_rs.service" ];
+      description = "Backup vaultwarden";
+      environment = {
+        DATA_FOLDER = "/var/lib/bitwarden_rs";
+        BACKUP_FOLDER = cfg.backupDir;
+      };
+      path = with pkgs; [ sqlite ];
+      serviceConfig = {
+        SyslogIdentifier = "backup-vaultwarden";
+        Type = "oneshot";
+        User = mkDefault user;
+        Group = mkDefault group;
+        ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    systemd.timers.backup-vaultwarden = mkIf (cfg.backupDir != null) {
+      aliases = [ "backup-bitwarden_rs.service" ];
+      description = "Backup vaultwarden on time";
+      timerConfig = {
+        OnCalendar = mkDefault "23:00";
+        Persistent = "true";
+        Unit = "backup-vaultwarden.service";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+  };
+
+  # uses attributes of the linked package
+  meta.buildDocsInSandbox = false;
+}
diff --git a/nixos/modules/services/security/yubikey-agent.nix b/nixos/modules/services/security/yubikey-agent.nix
new file mode 100644
index 00000000000..8be2457e1e2
--- /dev/null
+++ b/nixos/modules/services/security/yubikey-agent.nix
@@ -0,0 +1,66 @@
+# Global configuration for yubikey-agent.
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.yubikey-agent;
+
+  # reuse the pinentryFlavor option from the gnupg module
+  pinentryFlavor = config.programs.gnupg.agent.pinentryFlavor;
+in
+{
+  ###### interface
+
+  meta.maintainers = with maintainers; [ philandstuff rawkode jwoudenberg ];
+
+  options = {
+
+    services.yubikey-agent = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to start yubikey-agent when you log in.  Also sets
+          SSH_AUTH_SOCK to point at yubikey-agent.
+
+          Note that yubikey-agent will use whatever pinentry is
+          specified in programs.gnupg.agent.pinentryFlavor.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.yubikey-agent;
+        defaultText = literalExpression "pkgs.yubikey-agent";
+        description = ''
+          The package used for the yubikey-agent daemon.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    systemd.packages = [ cfg.package ];
+
+    # This overrides the systemd user unit shipped with the
+    # yubikey-agent package
+    systemd.user.services.yubikey-agent = mkIf (pinentryFlavor != null) {
+      path = [ pkgs.pinentry.${pinentryFlavor} ];
+      wantedBy = [
+        (if pinentryFlavor == "tty" || pinentryFlavor == "curses" then
+          "default.target"
+        else
+          "graphical-session.target")
+      ];
+    };
+
+    environment.extraInit = ''
+      if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
+        export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/yubikey-agent/yubikey-agent.sock"
+      fi
+    '';
+  };
+}