diff options
author | Alyssa Ross <hi@alyssa.is> | 2022-05-31 09:59:33 +0000 |
---|---|---|
committer | Alyssa Ross <hi@alyssa.is> | 2022-05-31 09:59:57 +0000 |
commit | 9ff36293d1e428cd7bf03e8d4b03611b6d361c28 (patch) | |
tree | 1ab51a42b868c55b83f6ccdb80371b9888739dd9 /nixos/modules/services/security | |
parent | 1c4fcd0d4b0541e674ee56ace1053e23e562cc80 (diff) | |
parent | ddc3c396a51918043bb0faa6f676abd9562be62c (diff) | |
download | nixpkgs-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')
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 <action> 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:<base32-private-key></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:<base32-public-key></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 + ''; + }; +} |