1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.geoipupdate;
10 inherit (builtins)
11 isAttrs
12 isString
13 isInt
14 isList
15 typeOf
16 hashString
17 ;
18in
19{
20 imports = [
21 (lib.mkRemovedOptionModule [
22 "services"
23 "geoip-updater"
24 ] "services.geoip-updater has been removed, use services.geoipupdate instead.")
25 ];
26
27 options = {
28 services.geoipupdate = {
29 enable = lib.mkEnableOption ''
30 periodic downloading of GeoIP databases using geoipupdate
31 '';
32
33 interval = lib.mkOption {
34 type = lib.types.str;
35 default = "weekly";
36 description = ''
37 Update the GeoIP databases at this time / interval.
38 The format is described in
39 {manpage}`systemd.time(7)`.
40 '';
41 };
42
43 settings = lib.mkOption {
44 example = lib.literalExpression ''
45 {
46 AccountID = 200001;
47 DatabaseDirectory = "/var/lib/GeoIP";
48 LicenseKey = { _secret = "/run/keys/maxmind_license_key"; };
49 Proxy = "10.0.0.10:8888";
50 ProxyUserPassword = { _secret = "/run/keys/proxy_pass"; };
51 }
52 '';
53 description = ''
54 geoipupdate configuration options. See
55 <https://github.com/maxmind/geoipupdate/blob/main/doc/GeoIP.conf.md>
56 for a full list of available options.
57
58 Settings containing secret data should be set to an
59 attribute set containing the attribute
60 `_secret` - a string pointing to a file
61 containing the value the option should be set to. See the
62 example to get a better picture of this: in the resulting
63 {file}`GeoIP.conf` file, the
64 `ProxyUserPassword` key will be set to the
65 contents of the
66 {file}`/run/keys/proxy_pass` file.
67 '';
68 type = lib.types.submodule {
69 freeformType =
70 with lib.types;
71 let
72 type = oneOf [
73 str
74 int
75 bool
76 ];
77 in
78 attrsOf (either type (listOf type));
79
80 options = {
81
82 AccountID = lib.mkOption {
83 type = lib.types.int;
84 description = ''
85 Your MaxMind account ID.
86 '';
87 };
88
89 EditionIDs = lib.mkOption {
90 type = with lib.types; listOf (either str int);
91 example = [
92 "GeoLite2-ASN"
93 "GeoLite2-City"
94 "GeoLite2-Country"
95 ];
96 description = ''
97 List of database edition IDs. This includes new string
98 IDs like `GeoIP2-City` and old
99 numeric IDs like `106`.
100 '';
101 };
102
103 LicenseKey = lib.mkOption {
104 type = with lib.types; either path (attrsOf path);
105 description = ''
106 A file containing the MaxMind license key.
107
108 Always handled as a secret whether the value is
109 wrapped in a `{ _secret = ...; }`
110 attrset or not (refer to [](#opt-services.geoipupdate.settings) for
111 details).
112 '';
113 apply = x: if isAttrs x then x else { _secret = x; };
114 };
115
116 DatabaseDirectory = lib.mkOption {
117 type = lib.types.path;
118 default = "/var/lib/GeoIP";
119 example = "/run/GeoIP";
120 description = ''
121 The directory to store the database files in. The
122 directory will be automatically created, the owner
123 changed to `geoip` and permissions
124 set to world readable. This applies if the directory
125 already exists as well, so don't use a directory with
126 sensitive contents.
127 '';
128 };
129
130 };
131 };
132 };
133 };
134
135 };
136
137 config = lib.mkIf cfg.enable {
138
139 services.geoipupdate.settings = {
140 LockFile = "/run/geoipupdate/.lock";
141 };
142
143 systemd.services.geoipupdate-create-db-dir = {
144 serviceConfig.Type = "oneshot";
145 script = ''
146 set -o errexit -o pipefail -o nounset -o errtrace
147 shopt -s inherit_errexit
148
149 mkdir -p ${cfg.settings.DatabaseDirectory}
150 chmod 0755 ${cfg.settings.DatabaseDirectory}
151 '';
152 };
153
154 systemd.services.geoipupdate = {
155 description = "GeoIP Updater";
156 requires = [ "geoipupdate-create-db-dir.service" ];
157 after = [
158 "geoipupdate-create-db-dir.service"
159 "network-online.target"
160 "nss-lookup.target"
161 ];
162 path = [ pkgs.replace-secret ];
163 wants = [ "network-online.target" ];
164 startAt = cfg.interval;
165 serviceConfig = {
166 ExecStartPre =
167 let
168 isSecret = v: isAttrs v && v ? _secret && isString v._secret;
169 geoipupdateKeyValue = lib.generators.toKeyValue {
170 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault " " rec {
171 mkValueString =
172 v:
173 if isInt v then
174 toString v
175 else if isString v then
176 v
177 else if true == v then
178 "1"
179 else if false == v then
180 "0"
181 else if isList v then
182 lib.concatMapStringsSep " " mkValueString v
183 else if isSecret v then
184 hashString "sha256" v._secret
185 else
186 throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}";
187 };
188 };
189 secretPaths = lib.catAttrs "_secret" (lib.collect isSecret cfg.settings);
190 mkSecretReplacement = file: ''
191 replace-secret ${
192 lib.escapeShellArgs [
193 (hashString "sha256" file)
194 file
195 "/run/geoipupdate/GeoIP.conf"
196 ]
197 }
198 '';
199 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
200
201 geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
202
203 script = ''
204 set -o errexit -o pipefail -o nounset -o errtrace
205 shopt -s inherit_errexit
206
207 chown geoip "${cfg.settings.DatabaseDirectory}"
208
209 cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
210 ${secretReplacements}
211 '';
212 in
213 "+${pkgs.writeShellScript "start-pre-full-privileges" script}";
214 ExecStart = "${pkgs.geoipupdate}/bin/geoipupdate -f /run/geoipupdate/GeoIP.conf";
215 User = "geoip";
216 DynamicUser = true;
217 ReadWritePaths = cfg.settings.DatabaseDirectory;
218 RuntimeDirectory = "geoipupdate";
219 RuntimeDirectoryMode = "0700";
220 CapabilityBoundingSet = "";
221 PrivateDevices = true;
222 PrivateMounts = true;
223 PrivateUsers = true;
224 ProtectClock = true;
225 ProtectControlGroups = true;
226 ProtectHome = true;
227 ProtectHostname = true;
228 ProtectKernelLogs = true;
229 ProtectKernelModules = true;
230 ProtectKernelTunables = true;
231 ProtectProc = "invisible";
232 ProcSubset = "pid";
233 SystemCallFilter = [
234 "@system-service"
235 "~@privileged"
236 ];
237 RestrictAddressFamilies = [
238 "AF_INET"
239 "AF_INET6"
240 ];
241 RestrictRealtime = true;
242 RestrictNamespaces = true;
243 MemoryDenyWriteExecute = true;
244 LockPersonality = true;
245 SystemCallArchitectures = "native";
246 };
247 };
248
249 systemd.timers.geoipupdate-initial-run = {
250 wantedBy = [ "timers.target" ];
251 unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
252 timerConfig = {
253 Unit = "geoipupdate.service";
254 OnActiveSec = 0;
255 };
256 };
257 };
258
259 meta.maintainers = [ lib.maintainers.talyz ];
260}