{ config, lib, pkgs, ... }: let cfg = config.services.geoipupdate; inherit (builtins) isAttrs isString isInt isList typeOf hashString; in { imports = [ (lib.mkRemovedOptionModule [ "services" "geoip-updater" ] "services.geoip-updater has been removed, use services.geoipupdate instead.") ]; options = { services.geoipupdate = { enable = lib.mkEnableOption '' periodic downloading of GeoIP databases using geoipupdate. ''; interval = lib.mkOption { type = lib.types.str; default = "weekly"; description = '' Update the GeoIP databases at this time / interval. The format is described in systemd.time 7. ''; }; settings = lib.mkOption { example = lib.literalExpression '' { AccountID = 200001; DatabaseDirectory = "/var/lib/GeoIP"; LicenseKey = { _secret = "/run/keys/maxmind_license_key"; }; Proxy = "10.0.0.10:8888"; ProxyUserPassword = { _secret = "/run/keys/proxy_pass"; }; } ''; description = '' geoipupdate configuration options. See for a full list of available options. Settings containing secret data should be set to an attribute set containing the attribute _secret - a string pointing to a file containing the value the option should be set to. See the example to get a better picture of this: in the resulting GeoIP.conf file, the ProxyUserPassword key will be set to the contents of the /run/keys/proxy_pass file. ''; type = lib.types.submodule { freeformType = with lib.types; let type = oneOf [str int bool]; in attrsOf (either type (listOf type)); options = { AccountID = lib.mkOption { type = lib.types.int; description = '' Your MaxMind account ID. ''; }; EditionIDs = lib.mkOption { type = with lib.types; listOf (either str int); example = [ "GeoLite2-ASN" "GeoLite2-City" "GeoLite2-Country" ]; description = '' List of database edition IDs. This includes new string IDs like GeoIP2-City and old numeric IDs like 106. ''; }; LicenseKey = lib.mkOption { type = with lib.types; either path (attrsOf path); description = '' A file containing the MaxMind license key. Always handled as a secret whether the value is wrapped in a { _secret = ...; } attrset or not (refer to for details). ''; apply = x: if isAttrs x then x else { _secret = x; }; }; DatabaseDirectory = lib.mkOption { type = lib.types.path; default = "/var/lib/GeoIP"; example = "/run/GeoIP"; description = '' The directory to store the database files in. The directory will be automatically created, the owner changed to geoip and permissions set to world readable. This applies if the directory already exists as well, so don't use a directory with sensitive contents. ''; }; }; }; }; }; }; config = lib.mkIf cfg.enable { services.geoipupdate.settings = { LockFile = "/run/geoipupdate/.lock"; }; systemd.services.geoipupdate-create-db-dir = { serviceConfig.Type = "oneshot"; script = '' set -o errexit -o pipefail -o nounset -o errtrace shopt -s inherit_errexit mkdir -p ${cfg.settings.DatabaseDirectory} chmod 0755 ${cfg.settings.DatabaseDirectory} ''; }; systemd.services.geoipupdate = { description = "GeoIP Updater"; requires = [ "geoipupdate-create-db-dir.service" ]; after = [ "geoipupdate-create-db-dir.service" "network-online.target" "nss-lookup.target" ]; path = [ pkgs.replace-secret ]; wants = [ "network-online.target" ]; startAt = cfg.interval; serviceConfig = { ExecStartPre = let isSecret = v: isAttrs v && v ? _secret && isString v._secret; geoipupdateKeyValue = lib.generators.toKeyValue { mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec { mkValueString = v: if isInt v then toString v else if isString v then v else if true == v then "1" else if false == v then "0" else if isList v then lib.concatMapStringsSep " " mkValueString v else if isSecret v then hashString "sha256" v._secret else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}"; }; }; secretPaths = lib.catAttrs "_secret" (lib.collect isSecret cfg.settings); mkSecretReplacement = file: '' replace-secret ${lib.escapeShellArgs [ (hashString "sha256" file) file "/run/geoipupdate/GeoIP.conf" ]} ''; secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings); script = '' set -o errexit -o pipefail -o nounset -o errtrace shopt -s inherit_errexit chown geoip "${cfg.settings.DatabaseDirectory}" cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf ${secretReplacements} ''; in "+${pkgs.writeShellScript "start-pre-full-privileges" script}"; ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf"; User = "geoip"; DynamicUser = true; ReadWritePaths = cfg.settings.DatabaseDirectory; RuntimeDirectory = "geoipupdate"; RuntimeDirectoryMode = 0700; CapabilityBoundingSet = ""; PrivateDevices = true; PrivateMounts = true; PrivateUsers = true; ProtectClock = true; ProtectControlGroups = true; ProtectHome = true; ProtectHostname = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProcSubset = "pid"; SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; RestrictRealtime = true; RestrictNamespaces = true; MemoryDenyWriteExecute = true; LockPersonality = true; SystemCallArchitectures = "native"; }; }; systemd.timers.geoipupdate-initial-run = { wantedBy = [ "timers.target" ]; unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}"; timerConfig = { Unit = "geoipupdate.service"; OnActiveSec = 0; }; }; }; meta.maintainers = [ lib.maintainers.talyz ]; }