diff options
Diffstat (limited to 'infra/libkookie/nixpkgs/nixos/modules/services/security')
27 files changed, 4593 insertions, 0 deletions
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/bitwarden_rs/backup.sh b/infra/libkookie/nixpkgs/nixos/modules/services/security/bitwarden_rs/backup.sh new file mode 100644 index 000000000000..264a7da9cbb6 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/bitwarden_rs/backup.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Based on: https://github.com/dani-garcia/bitwarden_rs/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/infra/libkookie/nixpkgs/nixos/modules/services/security/bitwarden_rs/default.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/bitwarden_rs/default.nix new file mode 100644 index 000000000000..a04bc883bf0f --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/bitwarden_rs/default.nix @@ -0,0 +1,162 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.bitwarden_rs; + user = config.users.users.bitwarden_rs.name; + group = config.users.groups.bitwarden_rs.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 = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault"; + } // configEnv; + + configFile = pkgs.writeText "bitwarden_rs.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv)); + + bitwarden_rs = pkgs.bitwarden_rs.override { inherit (cfg) dbBackend; }; + +in { + options.services.bitwarden_rs = with types; { + enable = mkEnableOption "bitwarden_rs"; + + dbBackend = mkOption { + type = enum [ "sqlite" "mysql" "postgresql" ]; + default = "sqlite"; + description = '' + Which database backend bitwarden_rs will be using. + ''; + }; + + backupDir = mkOption { + type = nullOr str; + default = null; + description = '' + The directory under which bitwarden_rs will backup its persistent data. + ''; + }; + + config = mkOption { + type = attrsOf (nullOr (oneOf [ bool int str ])); + default = {}; + example = literalExample '' + { + domain = "https://bw.domain.tld:8443"; + signupsAllowed = true; + rocketPort = 8222; + rocketLog = "critical"; + } + ''; + description = '' + The configuration of bitwarden_rs 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 bitwarden_rs's systemd service. + + The available configuration options can be found in + <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/${bitwarden_rs.version}/.env.template">the environment template file</link>. + ''; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/root/bitwarden_rs.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>bitwarden_rs</literal> is running. + ''; + }; + }; + + 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.bitwarden_rs = { + inherit group; + isSystemUser = true; + }; + users.groups.bitwarden_rs = { }; + + systemd.services.bitwarden_rs = { + after = [ "network.target" ]; + path = with pkgs; [ openssl ]; + serviceConfig = { + User = user; + Group = group; + EnvironmentFile = [ configFile ] ++ optional (cfg.environmentFile != null) cfg.environmentFile; + ExecStart = "${bitwarden_rs}/bin/bitwarden_rs"; + LimitNOFILE = "1048576"; + LimitNPROC = "64"; + PrivateTmp = "true"; + PrivateDevices = "true"; + ProtectHome = "true"; + ProtectSystem = "strict"; + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + StateDirectory = "bitwarden_rs"; + }; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services.backup-bitwarden_rs = mkIf (cfg.backupDir != null) { + description = "Backup bitwarden_rs"; + environment = { + DATA_FOLDER = "/var/lib/bitwarden_rs"; + BACKUP_FOLDER = cfg.backupDir; + }; + path = with pkgs; [ sqlite ]; + serviceConfig = { + SyslogIdentifier = "backup-bitwarden_rs"; + Type = "oneshot"; + User = mkDefault user; + Group = mkDefault group; + ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}"; + }; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.timers.backup-bitwarden_rs = mkIf (cfg.backupDir != null) { + description = "Backup bitwarden_rs on time"; + timerConfig = { + OnCalendar = mkDefault "23:00"; + Persistent = "true"; + Unit = "backup-bitwarden_rs.service"; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/certmgr.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/certmgr.nix new file mode 100644 index 000000000000..94c0ba141179 --- /dev/null +++ b/infra/libkookie/nixpkgs/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 = "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 = literalExample '' + { + 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/infra/libkookie/nixpkgs/nixos/modules/services/security/cfssl.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/cfssl.nix new file mode 100644 index 000000000000..ee6d5d91fe15 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/cfssl.nix @@ -0,0 +1,209 @@ +{ config, 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 = "Cfssl work directory."; + }; + + address = mkOption { + default = "127.0.0.1"; + type = types.str; + description = "Address to bind."; + }; + + port = mkOption { + default = 8888; + type = types.ints.u16; + description = "Port to bind."; + }; + + ca = mkOption { + defaultText = "\${cfg.dataDir}/ca.pem"; + type = types.str; + description = "CA used to sign the new certificate -- accepts '[file:]fname' or 'env:varname'."; + }; + + caKey = mkOption { + defaultText = "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.extraGroups.cfssl = { + gid = config.ids.gids.cfssl; + }; + + users.extraUsers.cfssl = { + description = "cfssl user"; + createHome = true; + 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 = { + WorkingDirectory = cfg.dataDir; + StateDirectory = cfg.dataDir; + StateDirectoryMode = 700; + Restart = "always"; + User = "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)) + ]; + }; + }; + + services.cfssl = { + ca = mkDefault "${cfg.dataDir}/ca.pem"; + caKey = mkDefault "${cfg.dataDir}/ca-key.pem"; + }; + }; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/clamav.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/clamav.nix new file mode 100644 index 000000000000..aaf6fb0479ba --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/clamav.nix @@ -0,0 +1,147 @@ +{ config, lib, pkgs, ... }: +with lib; +let + clamavUser = "clamav"; + stateDir = "/var/lib/clamav"; + runDir = "/run/clamav"; + clamavGroup = clamavUser; + cfg = config.services.clamav; + pkg = pkgs.clamav; + + clamdConfigFile = pkgs.writeText "clamd.conf" '' + DatabaseDirectory ${stateDir} + LocalSocket ${runDir}/clamd.ctl + PidFile ${runDir}/clamd.pid + TemporaryDirectory /tmp + User clamav + Foreground yes + + ${cfg.daemon.extraConfig} + ''; + + freshclamConfigFile = pkgs.writeText "freshclam.conf" '' + DatabaseDirectory ${stateDir} + Foreground yes + Checks ${toString cfg.updater.frequency} + + ${cfg.updater.extraConfig} + + DatabaseMirror database.clamav.net + ''; +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "clamav" "updater" "config" ] [ "services" "clamav" "updater" "extraConfig" ]) + ]; + + options = { + services.clamav = { + daemon = { + enable = mkEnableOption "ClamAV clamd daemon"; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration for clamd. Contents will be added verbatim to the + configuration file. + ''; + }; + }; + 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. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration for freshclam. Contents will be added verbatim to the + configuration file. + ''; + }; + }; + }; + }; + + 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; }; + + 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"; + requires = 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 ]; + + 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/infra/libkookie/nixpkgs/nixos/modules/services/security/fail2ban.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/fail2ban.nix new file mode 100644 index 000000000000..cf0d72d5c531 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/fail2ban.nix @@ -0,0 +1,306 @@ +{ 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."; + }; + + package = mkOption { + default = pkgs.fail2ban; + type = types.package; + example = "pkgs.fail2ban_0_11"; + description = "The fail2ban package to use for running the fail2ban service."; + }; + + packageFirewall = mkOption { + default = pkgs.iptables; + type = types.package; + example = "pkgs.nftables"; + description = "The firewall package used by fail2ban service."; + }; + + banaction = mkOption { + default = "iptables-multiport"; + type = types.str; + example = "nftables-multiport"; + description = '' + Default banning action (e.g. iptables, iptables-new, iptables-multiport, + shorewall, etc) It is used to define action_* variables. Can be overridden + globally or per section within jail.local file + ''; + }; + + banaction-allports = mkOption { + default = "iptables-allport"; + type = types.str; + example = "nftables-allport"; + description = '' + Default banning action (e.g. iptables, iptables-new, iptables-multiport, + shorewall, etc) It is used to define action_* variables. Can be overridden + globally or per section within jail.local file + ''; + }; + + bantime-increment.enable = mkOption { + default = false; + type = types.bool; + description = '' + Allows to use database for searching of previously banned ip's to increase + a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32... + ''; + }; + + bantime-increment.rndtime = mkOption { + default = "4m"; + type = types.str; + example = "8m"; + description = '' + "bantime-increment.rndtime" is the max number of seconds using for mixing with random time + to prevent "clever" botnets calculate exact time IP can be unbanned again + ''; + }; + + bantime-increment.maxtime = mkOption { + default = "10h"; + type = types.str; + example = "48h"; + description = '' + "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further) + ''; + }; + + bantime-increment.factor = mkOption { + default = "1"; + type = types.str; + example = "4"; + description = '' + "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier, + default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ... + ''; + }; + + bantime-increment.formula = mkOption { + default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor"; + type = types.str; + example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)"; + description = '' + "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow, + the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32... + ''; + }; + + bantime-increment.multipliers = mkOption { + default = "1 2 4 8 16 32 64"; + type = types.str; + example = "2 4 16 128"; + description = '' + "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, coresponding + previously ban count and given "bantime.factor" (for multipliers default is 1); + following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count, + always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours + ''; + }; + + bantime-increment.overalljails = mkOption { + default = false; + type = types.bool; + example = true; + description = '' + "bantime-increment.overalljails" (if true) specifies the search of IP in the database will be executed + cross over all jails, if false (dafault), only current jail of the ban IP will be searched + ''; + }; + + ignoreIP = mkOption { + default = [ ]; + type = types.listOf types.str; + example = [ "192.168.0.0/16" "2001:DB8::42" ]; + description = '' + "ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which + matches an address in this list. Several addresses can be defined using space (and/or comma) separator. + ''; + }; + + daemonConfig = mkOption { + default = '' + [Definition] + 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 = literalExample '' + { 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>. + ''; + }; + + }; + + }; + + ###### 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 ]; + reloadIfChanged = true; + + path = [ cfg.package cfg.packageFirewall pkgs.iproute ]; + + 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 = 3 + backend = systemd + # Actions + banaction = ${cfg.banaction} + banaction_allports = ${cfg.banaction-allports} + ''; + # Block SSH if there are too many failing connection attempts. + services.fail2ban.jails.sshd = mkDefault '' + enabled = true + port = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports} + ''; + }; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/fprintd.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/fprintd.nix new file mode 100644 index 000000000000..cbac4ef05b8d --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/fprintd.nix @@ -0,0 +1,54 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.fprintd; + +in + + +{ + + ###### interface + + options = { + + services.fprintd = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable fprintd daemon and PAM module for fingerprint readers handling. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.fprintd; + defaultText = "pkgs.fprintd"; + description = '' + fprintd package to use. + ''; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + services.dbus.packages = [ pkgs.fprintd ]; + + environment.systemPackages = [ pkgs.fprintd ]; + + systemd.packages = [ cfg.package ]; + + }; + +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/fprot.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/fprot.nix new file mode 100644 index 000000000000..3a0b08b3c6d8 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/fprot.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: +with lib; +let + fprotUser = "fprot"; + stateDir = "/var/lib/fprot"; + fprotGroup = fprotUser; + cfg = config.services.fprot; +in { + options = { + + services.fprot = { + updater = { + enable = mkEnableOption "automatic F-Prot virus definitions database updates"; + + productData = mkOption { + description = '' + product.data file. Defaults to the one supplied with installation package. + ''; + }; + + frequency = mkOption { + default = 30; + description = '' + Update virus definitions every X minutes. + ''; + }; + + licenseKeyfile = mkOption { + description = '' + License keyfile. Defaults to the one supplied with installation package. + ''; + }; + + }; + }; + }; + + ###### implementation + + config = mkIf cfg.updater.enable { + + services.fprot.updater.productData = mkDefault "${pkgs.fprot}/opt/f-prot/product.data"; + services.fprot.updater.licenseKeyfile = mkDefault "${pkgs.fprot}/opt/f-prot/license.key"; + + environment.systemPackages = [ pkgs.fprot ]; + environment.etc."f-prot.conf" = { + source = "${pkgs.fprot}/opt/f-prot/f-prot.conf"; + }; + + users.users.${fprotUser} = + { uid = config.ids.uids.fprot; + description = "F-Prot daemon user"; + home = stateDir; + }; + + users.groups.${fprotGroup} = + { gid = config.ids.gids.fprot; }; + + services.cron.systemCronJobs = [ "*/${toString cfg.updater.frequency} * * * * root start fprot-updater" ]; + + systemd.services.fprot-updater = { + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = false; + }; + wantedBy = [ "multi-user.target" ]; + + # have to copy fpupdate executable because it insists on storing the virus database in the same dir + preStart = '' + mkdir -m 0755 -p ${stateDir} + chown ${fprotUser}:${fprotGroup} ${stateDir} + cp ${pkgs.fprot}/opt/f-prot/fpupdate ${stateDir} + ln -sf ${cfg.updater.productData} ${stateDir}/product.data + ''; + + script = "/var/lib/fprot/fpupdate --keyfile ${cfg.updater.licenseKeyfile}"; + }; + }; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/haka.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/haka.nix new file mode 100644 index 000000000000..618e689924fd --- /dev/null +++ b/infra/libkookie/nixpkgs/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 = "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/infra/libkookie/nixpkgs/nixos/modules/services/security/haveged.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/haveged.nix new file mode 100644 index 000000000000..22ece1883446 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/haveged.nix @@ -0,0 +1,67 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.haveged; + +in + + +{ + + ###### interface + + options = { + + services.haveged = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable to haveged entropy daemon, which refills + /dev/random when low. + ''; + }; + + refill_threshold = mkOption { + type = types.int; + default = 1024; + description = '' + The number of bits of available entropy beneath which + haveged should refill the entropy pool. + ''; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + systemd.services.haveged = + { description = "Entropy Harvesting Daemon"; + unitConfig.Documentation = "man:haveged(8)"; + wantedBy = [ "multi-user.target" ]; + + path = [ pkgs.haveged ]; + + serviceConfig = { + ExecStart = "${pkgs.haveged}/bin/haveged -F -w ${toString cfg.refill_threshold} -v 1"; + SuccessExitStatus = 143; + PrivateTmp = true; + PrivateDevices = true; + PrivateNetwork = true; + ProtectSystem = "full"; + ProtectHome = true; + }; + }; + + }; + +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/hologram-agent.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/hologram-agent.nix new file mode 100644 index 000000000000..e37334b3cf5e --- /dev/null +++ b/infra/libkookie/nixpkgs/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; [ nand0p ]; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/hologram-server.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/hologram-server.nix new file mode 100644 index 000000000000..4acf6ae0e218 --- /dev/null +++ b/infra/libkookie/nixpkgs/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/infra/libkookie/nixpkgs/nixos/modules/services/security/munge.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/munge.nix new file mode 100644 index 000000000000..891788864710 --- /dev/null +++ b/infra/libkookie/nixpkgs/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/infra/libkookie/nixpkgs/nixos/modules/services/security/nginx-sso.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/nginx-sso.nix new file mode 100644 index 000000000000..50d250fc4d76 --- /dev/null +++ b/infra/libkookie/nixpkgs/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 = "pkgs.nginx-sso"; + description = '' + The nginx-sso package that should be used. + ''; + }; + + configuration = mkOption { + type = types.attrsOf types.unspecified; + default = {}; + example = literalExample '' + { + listen = { addr = "127.0.0.1"; port = 8080; }; + + providers.token.tokens = { + myuser = "MyToken"; + }; + + acl = { + rule_sets = [ + { + rules = [ { field = "x-application"; equals = "MyApp"; } ]; + allow = [ "myuser" ]; + } + ]; + }; + } + ''; + description = '' + nginx-sso configuration + (<link xlink:href="https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration">documentation</link>) + as a Nix attribute set. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.nginx-sso = { + description = "Nginx SSO Backend"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${pkg}/bin/nginx-sso \ + --config ${configYml} \ + --frontend-dir ${pkg}/share/frontend + ''; + Restart = "always"; + DynamicUser = true; + }; + }; + }; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/oauth2_proxy.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/oauth2_proxy.nix new file mode 100644 index 000000000000..486f3ab05386 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/oauth2_proxy.nix @@ -0,0 +1,588 @@ +# 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 = "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 [ + "google" + "azure" + "facebook" + "github" + "keycloak" + "gitlab" + "linkedin" + "login.gov" + "bitbucket" + "nextcloud" + "digitalocean" + "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 = {}; + 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/infra/libkookie/nixpkgs/nixos/modules/services/security/oauth2_proxy_nginx.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/oauth2_proxy_nginx.nix new file mode 100644 index 000000000000..be6734f439f3 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/oauth2_proxy_nginx.nix @@ -0,0 +1,64 @@ +{ 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; + 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 = 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 $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/infra/libkookie/nixpkgs/nixos/modules/services/security/physlock.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/physlock.nix new file mode 100644 index 000000000000..da5c22a90a09 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/physlock.nix @@ -0,0 +1,137 @@ +{ 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. + + Note that you might need to relog to have the correct binary in your + PATH upon changing this option. + ''; + }; + + 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 = { source = "${pkgs.physlock}/bin/physlock"; user = "root"; }; + + }) + ]); + +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/privacyidea.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/privacyidea.nix new file mode 100644 index 000000000000..c2988858e56c --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/privacyidea.nix @@ -0,0 +1,278 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.privacyidea; + + uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; }; + python = uwsgi.python3; + penv = python.withPackages (ps: [ ps.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"; + + 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"; + description = '' + This is used to encrypt the token data and token passwords + ''; + }; + + auditKeyPrivate = mkOption { + type = types.str; + default = "${cfg.stateDir}/private.pem"; + description = '' + Private Key for signing the audit log. + ''; + }; + + auditKeyPublic = mkOption { + type = types.str; + default = "${cfg.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; + default = ""; + 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 = [ python.pkgs.privacyidea ]; + + services.postgresql.enable = mkDefault true; + + systemd.services.privacyidea = let + piuwsgi = pkgs.writeText "uwsgi.json" (builtins.toJSON { + uwsgi = { + 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 = piCfgFile; + preStart = let + pi-manage = "${pkgs.sudo}/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 + if ! test -e "${cfg.stateDir}/db-created"; then + ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createuser --no-superuser --no-createdb --no-createrole ${cfg.user} + ${pkgs.sudo}/bin/sudo -u ${pgsu} ${psql}/bin/createdb --owner ${cfg.user} privacyidea + ${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"; + ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; + NotifyAccess = "main"; + KillSignal = "SIGQUIT"; + }; + }; + + users.users.privacyidea = mkIf (cfg.user == "privacyidea") { + group = cfg.group; + }; + + users.groups.privacyidea = mkIf (cfg.group == "privacyidea") {}; + }) + + (mkIf cfg.ldap-proxy.enable { + + systemd.services.privacyidea-ldap-proxy = let + ldap-proxy-env = pkgs.python2.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; + }; + + users.groups.pi-ldap-proxy = mkIf (cfg.ldap-proxy.group == "pi-ldap-proxy") {}; + }) + ]; + +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/shibboleth-sp.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/shibboleth-sp.nix new file mode 100644 index 000000000000..5908f727d535 --- /dev/null +++ b/infra/libkookie/nixpkgs/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 = "${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/infra/libkookie/nixpkgs/nixos/modules/services/security/sks.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/sks.nix new file mode 100644 index 000000000000..a91060dc659a --- /dev/null +++ b/infra/libkookie/nixpkgs/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 = "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 = "\${pkgs.sks.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/infra/libkookie/nixpkgs/nixos/modules/services/security/sshguard.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/sshguard.nix new file mode 100644 index 000000000000..e7a9cefdef30 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/sshguard.nix @@ -0,0 +1,155 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.sshguard; + +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".text = let + args = lib.concatStringsSep " " ([ + "-afb" + "-p info" + "-o cat" + "-n1" + ] ++ (map (name: "-t ${escapeShellArg name}") cfg.services)); + backend = if config.networking.nftables.enable + then "sshg-fw-nft-sets" + else "sshg-fw-ipset"; + in '' + BACKEND="${pkgs.sshguard}/libexec/${backend}" + LOGREADER="LANG=C ${pkgs.systemd}/bin/journalctl ${args}" + ''; + + systemd.services.sshguard = { + description = "SSHGuard brute-force attacks protection system"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + partOf = optional config.networking.firewall.enable "firewall.service"; + + path = with pkgs; if config.networking.nftables.enable + then [ nftables iproute systemd ] + else [ iptables ipset iproute systemd ]; + + # The sshguard ipsets must exist before we invoke + # iptables. sshguard creates the ipsets after startup if + # 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.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6 + ${pkgs.iptables}/bin/iptables -I INPUT -m set --match-set sshguard4 src -j DROP + ${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP + ''; + + postStop = optionalString config.networking.firewall.enable '' + ${pkgs.iptables}/bin/iptables -D INPUT -m set --match-set sshguard4 src -j DROP + ${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP + ${pkgs.ipset}/bin/ipset -quiet destroy sshguard4 + ${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/infra/libkookie/nixpkgs/nixos/modules/services/security/tor.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/tor.nix new file mode 100644 index 000000000000..38dc378887a8 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/tor.nix @@ -0,0 +1,799 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.tor; + torDirectory = "/var/lib/tor"; + torRunDirectory = "/run/tor"; + + opt = name: value: optionalString (value != null) "${name} ${value}"; + optint = name: value: optionalString (value != null && value != 0) "${name} ${toString value}"; + + isolationOptions = { + type = types.listOf (types.enum [ + "IsolateClientAddr" + "IsolateSOCKSAuth" + "IsolateClientProtocol" + "IsolateDestPort" + "IsolateDestAddr" + ]); + default = []; + example = [ + "IsolateClientAddr" + "IsolateSOCKSAuth" + "IsolateClientProtocol" + "IsolateDestPort" + "IsolateDestAddr" + ]; + description = "Tor isolation options"; + }; + + + torRc = '' + User tor + DataDirectory ${torDirectory} + ${optionalString cfg.enableGeoIP '' + GeoIPFile ${cfg.package.geoip}/share/tor/geoip + GeoIPv6File ${cfg.package.geoip}/share/tor/geoip6 + ''} + + ${optint "ControlPort" cfg.controlPort} + ${optionalString cfg.controlSocket.enable "ControlPort unix:${torRunDirectory}/control GroupWritable RelaxDirModeCheck"} + '' + # Client connection config + + optionalString cfg.client.enable '' + SOCKSPort ${cfg.client.socksListenAddress} ${toString cfg.client.socksIsolationOptions} + SOCKSPort ${cfg.client.socksListenAddressFaster} + ${opt "SocksPolicy" cfg.client.socksPolicy} + + ${optionalString cfg.client.transparentProxy.enable '' + TransPort ${cfg.client.transparentProxy.listenAddress} ${toString cfg.client.transparentProxy.isolationOptions} + ''} + + ${optionalString cfg.client.dns.enable '' + DNSPort ${cfg.client.dns.listenAddress} ${toString cfg.client.dns.isolationOptions} + AutomapHostsOnResolve 1 + AutomapHostsSuffixes ${concatStringsSep "," cfg.client.dns.automapHostsSuffixes} + ''} + '' + # Explicitly disable the SOCKS server if the client is disabled. In + # particular, this makes non-anonymous hidden services possible. + + optionalString (! cfg.client.enable) '' + SOCKSPort 0 + '' + # Relay config + + optionalString cfg.relay.enable '' + ORPort ${toString cfg.relay.port} + ${opt "Address" cfg.relay.address} + ${opt "Nickname" cfg.relay.nickname} + ${opt "ContactInfo" cfg.relay.contactInfo} + + ${optint "RelayBandwidthRate" cfg.relay.bandwidthRate} + ${optint "RelayBandwidthBurst" cfg.relay.bandwidthBurst} + ${opt "AccountingMax" cfg.relay.accountingMax} + ${opt "AccountingStart" cfg.relay.accountingStart} + + ${if (cfg.relay.role == "exit") then + opt "ExitPolicy" cfg.relay.exitPolicy + else + "ExitPolicy reject *:*"} + + ${optionalString (elem cfg.relay.role ["bridge" "private-bridge"]) '' + BridgeRelay 1 + ServerTransportPlugin ${concatStringsSep "," cfg.relay.bridgeTransports} exec ${pkgs.obfs4}/bin/obfs4proxy managed + ExtORPort auto + ${optionalString (cfg.relay.role == "private-bridge") '' + ExtraInfoStatistics 0 + PublishServerDescriptor 0 + ''} + ''} + '' + # Hidden services + + concatStrings (flip mapAttrsToList cfg.hiddenServices (n: v: '' + HiddenServiceDir ${torDirectory}/onion/${v.name} + ${optionalString (v.version != null) "HiddenServiceVersion ${toString v.version}"} + ${flip concatMapStrings v.map (p: '' + HiddenServicePort ${toString p.port} ${p.destination} + '')} + ${optionalString (v.authorizeClient != null) '' + HiddenServiceAuthorizeClient ${v.authorizeClient.authType} ${concatStringsSep "," v.authorizeClient.clientNames} + ''} + '')) + + cfg.extraConfig; + + torRcFile = pkgs.writeText "torrc" torRc; + +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "tor" "relay" "portSpec" ] [ "services" "tor" "relay" "port" ]) + (mkRemovedOptionModule [ "services" "tor" "relay" "isBridge" ] "Use services.tor.relay.role instead.") + (mkRemovedOptionModule [ "services" "tor" "relay" "isExit" ] "Use services.tor.relay.role instead.") + ]; + + options = { + services.tor = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Enable the Tor daemon. By default, the daemon is run without + relay, exit, bridge or client connectivity. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.tor; + defaultText = "pkgs.tor"; + example = literalExample "pkgs.tor"; + description = '' + Tor package to use + ''; + }; + + enableGeoIP = mkOption { + type = types.bool; + default = true; + description = '' + Whenever to configure Tor daemon to use GeoIP databases. + + Disabling this will disable by-country statistics for + bridges and relays and some client and third-party software + functionality. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration. Contents will be added verbatim to the + configuration file at the end. + ''; + }; + + controlPort = mkOption { + type = types.nullOr (types.either types.int types.str); + default = null; + example = 9051; + description = '' + If set, Tor will accept connections on the specified port + and allow them to control the tor process. + ''; + }; + + controlSocket = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Tor control socket. Control socket is created + in <literal>${torRunDirectory}/control</literal> + ''; + }; + }; + + client = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Tor daemon to route application + connections. You might want to disable this if you plan + running a dedicated Tor relay. + ''; + }; + + socksListenAddress = mkOption { + type = types.str; + default = "127.0.0.1:9050"; + example = "192.168.0.1:9100"; + description = '' + Bind to this address to listen for connections from + Socks-speaking applications. Provides strong circuit + isolation, separate circuit per IP address. + ''; + }; + + socksListenAddressFaster = mkOption { + type = types.str; + default = "127.0.0.1:9063"; + example = "192.168.0.1:9101"; + description = '' + Bind to this address to listen for connections from + Socks-speaking applications. Same as + <option>socksListenAddress</option> but uses weaker + circuit isolation to provide performance suitable for a + web browser. + ''; + }; + + socksPolicy = mkOption { + type = types.nullOr types.str; + default = null; + example = "accept 192.168.0.0/16, reject *"; + description = '' + Entry policies to allow/deny SOCKS requests based on IP + address. First entry that matches wins. If no SocksPolicy + is set, we accept all (and only) requests from + <option>socksListenAddress</option>. + ''; + }; + + socksIsolationOptions = mkOption (isolationOptions // { + default = ["IsolateDestAddr"]; + }); + + transparentProxy = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable tor transparent proxy"; + }; + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1:9040"; + example = "192.168.0.1:9040"; + description = '' + Bind transparent proxy to this address. + ''; + }; + + isolationOptions = mkOption isolationOptions; + }; + + dns = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable tor dns resolver"; + }; + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1:9053"; + example = "192.168.0.1:9053"; + description = '' + Bind tor dns to this address. + ''; + }; + + isolationOptions = mkOption isolationOptions; + + automapHostsSuffixes = mkOption { + type = types.listOf types.str; + default = [".onion" ".exit"]; + example = [".onion"]; + description = "List of suffixes to use with automapHostsOnResolve"; + }; + }; + + privoxy.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable and configure the system Privoxy to use Tor's + faster port, suitable for HTTP. + + To have anonymity, protocols need to be scrubbed of identifying + information, and this can be accomplished for HTTP by Privoxy. + + Privoxy can also be useful for KDE torification. A good setup would be: + setting SOCKS proxy to the default Tor port, providing maximum + circuit isolation where possible; and setting HTTP proxy to Privoxy + to route HTTP traffic over faster, but less isolated port. + ''; + }; + }; + + relay = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable relaying 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.relay.port</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>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 lawer 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 address + 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> + ''; + }; + + bridgeTransports = mkOption { + type = types.listOf types.str; + default = ["obfs4"]; + example = ["obfs2" "obfs3" "obfs4" "scramblesuit"]; + description = "List of pluggable transports"; + }; + + nickname = mkOption { + type = types.str; + default = "anonymous"; + description = '' + A unique handle for your TOR relay. + ''; + }; + + contactInfo = mkOption { + type = types.nullOr types.str; + default = null; + example = "admin@relay.com"; + description = '' + Contact information for the relay owner (e.g. a mail + address and GPG key ID). + ''; + }; + + accountingMax = mkOption { + type = types.nullOr types.str; + default = null; + example = "450 GBytes"; + description = '' + Specify maximum bandwidth allowed during an accounting period. This + allows you to limit overall tor bandwidth over some time period. + See the <literal>AccountingMax</literal> option by looking at the + tor manual <citerefentry><refentrytitle>tor</refentrytitle> + <manvolnum>1</manvolnum></citerefentry> for more. + + Note this limit applies individually to upload and + download; if you specify <literal>"500 GBytes"</literal> + here, then you may transfer up to 1 TBytes of overall + bandwidth (500 GB upload, 500 GB download). + ''; + }; + + accountingStart = mkOption { + type = types.nullOr types.str; + default = null; + example = "month 1 1:00"; + description = '' + Specify length of an accounting period. This allows you to limit + overall tor bandwidth over some time period. See the + <literal>AccountingStart</literal> option by looking at the tor + manual <citerefentry><refentrytitle>tor</refentrytitle> + <manvolnum>1</manvolnum></citerefentry> for more. + ''; + }; + + bandwidthRate = mkOption { + type = types.nullOr types.int; + default = null; + example = 100; + description = '' + Specify this to limit the bandwidth usage of relayed (server) + traffic. Your own traffic is still unthrottled. Units: bytes/second. + ''; + }; + + bandwidthBurst = mkOption { + type = types.nullOr types.int; + default = cfg.relay.bandwidthRate; + example = 200; + description = '' + Specify this to allow bursts of the bandwidth usage of relayed (server) + traffic. The average usage will still be as specified in relayBandwidthRate. + Your own traffic is still unthrottled. Units: bytes/second. + ''; + }; + + address = mkOption { + type = types.nullOr types.str; + default = null; + example = "noname.example.com"; + description = '' + The IP address or full DNS name for advertised address of your relay. + Leave unset and Tor will guess. + ''; + }; + + port = mkOption { + type = types.either types.int types.str; + example = 143; + description = '' + What port to advertise for Tor connections. This corresponds to the + <literal>ORPort</literal> section in the Tor manual; see + <citerefentry><refentrytitle>tor</refentrytitle> + <manvolnum>1</manvolnum></citerefentry> for more details. + + At a minimum, you should just specify the port for the + relay to listen on; a common one like 143, 22, 80, or 443 + to help Tor users who may have very restrictive port-based + firewalls. + ''; + }; + + exitPolicy = mkOption { + type = types.nullOr types.str; + default = null; + example = "accept *:6660-6667,reject *:*"; + description = '' + A comma-separated list of exit policies. They're + considered first to last, and the first match wins. If you + want to _replace_ the default exit policy, end this with + either a reject *:* or an accept *:*. Otherwise, you're + _augmenting_ (prepending to) the default exit policy. + Leave commented to just use the default, which is + available in the man page or at + <link xlink:href="https://www.torproject.org/documentation.html" />. + + Look at + <link xlink:href="https://www.torproject.org/faq-abuse.html#TypicalAbuses" /> + for issues you might encounter if you use the default + exit policy. + + If certain IPs and ports are blocked externally, e.g. by + your firewall, you should update your exit policy to + reflect this -- otherwise Tor users will be told that + those destinations are down. + ''; + }; + }; + + hiddenServices = mkOption { + description = '' + A set of static hidden services that terminate their Tor + circuits at this node. + + Every element in this set declares a virtual onion host. + + You can specify your onion address by putting corresponding + private key to an appropriate place in ${torDirectory}. + + For services without private keys in ${torDirectory} Tor + daemon will generate random key pairs (which implies random + onion addresses) on restart. The latter could take a while, + please be patient. + + <note><para> + Hidden services can be useful even if you don't intend to + actually <emphasis>hide</emphasis> them, since they can + also be seen as a kind of NAT traversal mechanism. + + E.g. the example will make your sshd, whatever runs on + "8080" and your mail server available from anywhere where + the Tor network is available (which, with the help from + bridges, is pretty much everywhere), even if both client + and server machines are behind NAT you have no control + over. + </para></note> + ''; + default = {}; + example = literalExample '' + { "my-hidden-service-example".map = [ + { port = 22; } # map ssh port to this machine's ssh + { port = 80; toPort = 8080; } # map http port to whatever runs on 8080 + { port = "sip"; toHost = "mail.example.com"; toPort = "imap"; } # because we can + ]; + } + ''; + type = types.attrsOf (types.submodule ({name, ...}: { + options = { + + name = mkOption { + type = types.str; + description = '' + Name of this tor hidden service. + + This is purely descriptive. + + After restarting Tor daemon you should be able to + find your .onion address in + <literal>${torDirectory}/onion/$name/hostname</literal>. + ''; + }; + + map = mkOption { + default = []; + description = "Port mapping for this hidden service."; + type = types.listOf (types.submodule ({config, ...}: { + options = { + + port = mkOption { + type = types.either types.int types.str; + example = 80; + description = '' + Hidden service port to "bind to". + ''; + }; + + destination = mkOption { + internal = true; + type = types.str; + description = "Forward these connections where?"; + }; + + toHost = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Mapping destination host."; + }; + + toPort = mkOption { + type = types.either types.int types.str; + example = 8080; + description = "Mapping destination port."; + }; + + }; + + config = { + toPort = mkDefault config.port; + destination = mkDefault "${config.toHost}:${toString config.toPort}"; + }; + })); + }; + + authorizeClient = mkOption { + default = null; + description = "If configured, the hidden service is accessible for authorized clients only."; + type = types.nullOr (types.submodule ({...}: { + + options = { + + authType = mkOption { + type = types.enum [ "basic" "stealth" ]; + description = '' + Either <literal>"basic"</literal> for a general-purpose authorization protocol + or <literal>"stealth"</literal> for a less scalable protocol + that also hides service activity from unauthorized clients. + ''; + }; + + clientNames = mkOption { + type = types.nonEmptyListOf (types.strMatching "[A-Za-z0-9+-_]+"); + description = '' + Only clients that are listed here are authorized to access the hidden service. + Generated authorization data can be found in <filename>${torDirectory}/onion/$name/hostname</filename>. + Clients need to put this authorization data in their configuration file using <literal>HidServAuth</literal>. + ''; + }; + }; + })); + }; + + version = mkOption { + default = null; + description = "Rendezvous service descriptor version to publish for the hidden service. Currently, versions 2 and 3 are supported. (Default: 2)"; + type = types.nullOr (types.enum [ 2 3 ]); + }; + }; + + config = { + name = mkDefault name; + }; + })); + }; + }; + }; + + config = mkIf cfg.enable { + # Not sure if `cfg.relay.role == "private-bridge"` helps as tor + # sends a lot of stats + warnings = optional (cfg.relay.enable && cfg.hiddenServices != {}) + '' + Running Tor hidden services on a public relay makes the + presence of hidden services visible through simple statistical + analysis of publicly available data. + + 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. + ''; + + users.groups.tor.gid = config.ids.gids.tor; + users.users.tor = + { description = "Tor Daemon User"; + createHome = true; + home = torDirectory; + group = "tor"; + uid = config.ids.uids.tor; + }; + + # We have to do this instead of using RuntimeDirectory option in + # the service below because systemd has no way to set owners of + # RuntimeDirectory and putting this into the service below + # requires that service to relax it's sandbox since this needs + # writable /run + systemd.services.tor-init = + { description = "Tor Daemon Init"; + wantedBy = [ "tor.service" ]; + script = '' + install -m 0700 -o tor -g tor -d ${torDirectory} ${torDirectory}/onion + install -m 0750 -o tor -g tor -d ${torRunDirectory} + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + }; + + systemd.services.tor = + { description = "Tor Daemon"; + path = [ pkgs.tor ]; + + wantedBy = [ "multi-user.target" ]; + after = [ "tor-init.service" "network.target" ]; + restartTriggers = [ torRcFile ]; + + serviceConfig = + { Type = "simple"; + # Translated from the upstream contrib/dist/tor.service.in + ExecStartPre = "${cfg.package}/bin/tor -f ${torRcFile} --verify-config"; + ExecStart = "${cfg.package}/bin/tor -f ${torRcFile}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + KillSignal = "SIGINT"; + TimeoutSec = 30; + Restart = "on-failure"; + LimitNOFILE = 32768; + + # Hardening + # this seems to unshare /run despite what systemd.exec(5) says + PrivateTmp = mkIf (!cfg.controlSocket.enable) "yes"; + PrivateDevices = "yes"; + ProtectHome = "yes"; + ProtectSystem = "strict"; + InaccessiblePaths = "/home"; + ReadOnlyPaths = "/"; + ReadWritePaths = [ torDirectory torRunDirectory ]; + NoNewPrivileges = "yes"; + + # tor.service.in has this in, but this line it fails to spawn a namespace when using hidden services + #CapabilityBoundingSet = "CAP_SETUID CAP_SETGID CAP_NET_BIND_SERVICE"; + }; + }; + + environment.systemPackages = [ cfg.package ]; + + services.privoxy = mkIf (cfg.client.enable && cfg.client.privoxy.enable) { + enable = true; + extraConfig = '' + forward-socks4a / ${cfg.client.socksListenAddressFaster} . + toggle 1 + enable-remote-toggle 0 + enable-edit-actions 0 + enable-remote-http-toggle 0 + ''; + }; + }; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/security/torify.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/torify.nix new file mode 100644 index 000000000000..39551190dd33 --- /dev/null +++ b/infra/libkookie/nixpkgs/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/infra/libkookie/nixpkgs/nixos/modules/services/security/torsocks.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/torsocks.nix new file mode 100644 index 000000000000..47ac95c4626e --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/torsocks.nix @@ -0,0 +1,120 @@ +{ 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; + 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/infra/libkookie/nixpkgs/nixos/modules/services/security/usbguard.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/usbguard.nix new file mode 100644 index 000000000000..71fd71a2cab2 --- /dev/null +++ b/infra/libkookie/nixpkgs/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 = "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/infra/libkookie/nixpkgs/nixos/modules/services/security/vault.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/vault.nix new file mode 100644 index 000000000000..64622454b9de --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/vault.nix @@ -0,0 +1,156 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.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} + ''; +in + +{ + options = { + services.vault = { + enable = mkEnableOption "Vault daemon"; + + package = mkOption { + type = types.package; + default = pkgs.vault; + defaultText = "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; + description = "Data directory for file backend"; + }; + + storageConfig = mkOption { + type = types.nullOr types.lines; + default = null; + description = "Storage configuration"; + }; + + telemetryConfig = mkOption { + type = types.lines; + default = ""; + description = "Telemetry configuration"; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Extra text appended to <filename>vault.hcl</filename>."; + }; + }; + }; + + 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 -config ${configFile}"; + 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/infra/libkookie/nixpkgs/nixos/modules/services/security/yubikey-agent.nix b/infra/libkookie/nixpkgs/nixos/modules/services/security/yubikey-agent.nix new file mode 100644 index 000000000000..2972c64a3641 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/security/yubikey-agent.nix @@ -0,0 +1,60 @@ +# 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 ]; + + 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 = "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} ]; + }; + + 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 + ''; + }; +} |