1{ config, pkgs, lib, options, ... }:
2
3let
4 cfg = config.services.firefox-syncserver;
5 opt = options.services.firefox-syncserver;
6 defaultDatabase = "firefox_syncserver";
7 defaultUser = "firefox-syncserver";
8
9 dbIsLocal = cfg.database.host == "localhost";
10 dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}";
11
12 format = pkgs.formats.toml {};
13 settings = {
14 database_url = dbURL;
15 human_logs = true;
16 tokenserver = {
17 node_type = "mysql";
18 database_url = dbURL;
19 fxa_email_domain = "api.accounts.firefox.com";
20 fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1";
21 run_migrations = true;
22 # if JWK caching is not enabled the token server must verify tokens
23 # using the fxa api, on a thread pool with a static size.
24 additional_blocking_threads_for_fxa_requests = 10;
25 } // lib.optionalAttrs cfg.singleNode.enable {
26 # Single-node mode is likely to be used on small instances with little
27 # capacity. The default value (0.1) can only ever release capacity when
28 # accounts are removed if the total capacity is 10 or larger to begin
29 # with.
30 # https://github.com/mozilla-services/syncstorage-rs/issues/1313#issuecomment-1145293375
31 node_capacity_release_rate = 1;
32 };
33 };
34 configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings);
35 setupScript = pkgs.writeShellScript "firefox-syncserver-setup" ''
36 set -euo pipefail
37 shopt -s inherit_errexit
38
39 schema_configured() {
40 mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services
41 }
42
43 update_config() {
44 mysql ${cfg.database.name} <<"EOF"
45 BEGIN;
46
47 INSERT INTO `services` (`id`, `service`, `pattern`)
48 VALUES (1, 'sync-1.5', '{node}/1.5/{uid}')
49 ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}';
50 INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
51 `capacity`, `downed`, `backoff`)
52 VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity},
53 0, ${toString cfg.singleNode.capacity}, 0, 0)
54 ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity};
55
56 COMMIT;
57 EOF
58 }
59
60
61 for (( try = 0; try < 60; try++ )); do
62 if ! schema_configured; then
63 sleep 2
64 else
65 update_config
66 exit 0
67 fi
68 done
69
70 echo "Single-node setup failed"
71 exit 1
72 '';
73in
74
75{
76 options = {
77 services.firefox-syncserver = {
78 enable = lib.mkEnableOption (lib.mdDoc ''
79 the Firefox Sync storage service.
80
81 Out of the box this will not be very useful unless you also configure at least
82 one service and one nodes by inserting them into the mysql database manually, e.g.
83 by running
84
85 ```
86 INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}');
87 INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`,
88 `capacity`, `downed`, `backoff`)
89 VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0');
90 ```
91
92 {option}`${opt.singleNode.enable}` does this automatically when enabled
93 '');
94
95 package = lib.mkOption {
96 type = lib.types.package;
97 default = pkgs.syncstorage-rs;
98 defaultText = lib.literalExpression "pkgs.syncstorage-rs";
99 description = lib.mdDoc ''
100 Package to use.
101 '';
102 };
103
104 database.name = lib.mkOption {
105 # the mysql module does not allow `-quoting without resorting to shell
106 # escaping, so we restrict db names for forward compaitiblity should this
107 # behavior ever change.
108 type = lib.types.strMatching "[a-z_][a-z0-9_]*";
109 default = defaultDatabase;
110 description = lib.mdDoc ''
111 Database to use for storage. Will be created automatically if it does not exist
112 and `config.${opt.database.createLocally}` is set.
113 '';
114 };
115
116 database.user = lib.mkOption {
117 type = lib.types.str;
118 default = defaultUser;
119 description = lib.mdDoc ''
120 Username for database connections.
121 '';
122 };
123
124 database.host = lib.mkOption {
125 type = lib.types.str;
126 default = "localhost";
127 description = lib.mdDoc ''
128 Database host name. `localhost` is treated specially and inserts
129 systemd dependencies, other hostnames or IP addresses of the local machine do not.
130 '';
131 };
132
133 database.createLocally = lib.mkOption {
134 type = lib.types.bool;
135 default = true;
136 description = lib.mdDoc ''
137 Whether to create database and user on the local machine if they do not exist.
138 This includes enabling unix domain socket authentication for the configured user.
139 '';
140 };
141
142 logLevel = lib.mkOption {
143 type = lib.types.str;
144 default = "error";
145 description = lib.mdDoc ''
146 Log level to run with. This can be a simple log level like `error`
147 or `trace`, or a more complicated logging expression.
148 '';
149 };
150
151 secrets = lib.mkOption {
152 type = lib.types.path;
153 description = lib.mdDoc ''
154 A file containing the various secrets. Should be in the format expected by systemd's
155 `EnvironmentFile` directory. Two secrets are currently available:
156 `SYNC_MASTER_SECRET` and
157 `SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET`.
158 '';
159 };
160
161 singleNode = {
162 enable = lib.mkEnableOption (lib.mdDoc "auto-configuration for a simple single-node setup");
163
164 enableTLS = lib.mkEnableOption (lib.mdDoc "automatic TLS setup");
165
166 enableNginx = lib.mkEnableOption (lib.mdDoc "nginx virtualhost definitions");
167
168 hostname = lib.mkOption {
169 type = lib.types.str;
170 description = lib.mdDoc ''
171 Host name to use for this service.
172 '';
173 };
174
175 capacity = lib.mkOption {
176 type = lib.types.ints.unsigned;
177 default = 10;
178 description = lib.mdDoc ''
179 How many sync accounts are allowed on this server. Setting this value
180 equal to or less than the number of currently active accounts will
181 effectively deny service to accounts not yet registered here.
182 '';
183 };
184
185 url = lib.mkOption {
186 type = lib.types.str;
187 default = "${if cfg.singleNode.enableTLS then "https" else "http"}://${cfg.singleNode.hostname}";
188 defaultText = lib.literalExpression ''
189 ''${if cfg.singleNode.enableTLS then "https" else "http"}://''${config.${opt.singleNode.hostname}}
190 '';
191 description = lib.mdDoc ''
192 URL of the host. If you are not using the automatic webserver proxy setup you will have
193 to change this setting or your sync server may not be functional.
194 '';
195 };
196 };
197
198 settings = lib.mkOption {
199 type = lib.types.submodule {
200 freeformType = format.type;
201
202 options = {
203 port = lib.mkOption {
204 type = lib.types.port;
205 default = 5000;
206 description = lib.mdDoc ''
207 Port to bind to.
208 '';
209 };
210
211 tokenserver.enabled = lib.mkOption {
212 type = lib.types.bool;
213 default = true;
214 description = lib.mdDoc ''
215 Whether to enable the token service as well.
216 '';
217 };
218 };
219 };
220 default = { };
221 description = lib.mdDoc ''
222 Settings for the sync server. These take priority over values computed
223 from NixOS options.
224
225 See the doc comments on the `Settings` structs in
226 <https://github.com/mozilla-services/syncstorage-rs/blob/master/syncstorage/src/settings.rs>
227 and
228 <https://github.com/mozilla-services/syncstorage-rs/blob/master/syncstorage/src/tokenserver/settings.rs>
229 for available options.
230 '';
231 };
232 };
233 };
234
235 config = lib.mkIf cfg.enable {
236 services.mysql = lib.mkIf cfg.database.createLocally {
237 enable = true;
238 ensureDatabases = [ cfg.database.name ];
239 ensureUsers = [{
240 name = cfg.database.user;
241 ensurePermissions = {
242 "${cfg.database.name}.*" = "all privileges";
243 };
244 }];
245 };
246
247 systemd.services.firefox-syncserver = {
248 wantedBy = [ "multi-user.target" ];
249 requires = lib.mkIf dbIsLocal [ "mysql.service" ];
250 after = lib.mkIf dbIsLocal [ "mysql.service" ];
251 restartTriggers = lib.optional cfg.singleNode.enable setupScript;
252 environment.RUST_LOG = cfg.logLevel;
253 serviceConfig = {
254 User = defaultUser;
255 Group = defaultUser;
256 ExecStart = "${cfg.package}/bin/syncstorage --config ${configFile}";
257 Stderr = "journal";
258 EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}";
259
260 # hardening
261 RemoveIPC = true;
262 CapabilityBoundingSet = [ "" ];
263 DynamicUser = true;
264 NoNewPrivileges = true;
265 PrivateDevices = true;
266 ProtectClock = true;
267 ProtectKernelLogs = true;
268 ProtectControlGroups = true;
269 ProtectKernelModules = true;
270 SystemCallArchitectures = "native";
271 # syncstorage-rs uses python-cffi internally, and python-cffi does not
272 # work with MemoryDenyWriteExecute=true
273 MemoryDenyWriteExecute = false;
274 RestrictNamespaces = true;
275 RestrictSUIDSGID = true;
276 ProtectHostname = true;
277 LockPersonality = true;
278 ProtectKernelTunables = true;
279 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
280 RestrictRealtime = true;
281 ProtectSystem = "strict";
282 ProtectProc = "invisible";
283 ProcSubset = "pid";
284 ProtectHome = true;
285 PrivateUsers = true;
286 PrivateTmp = true;
287 SystemCallFilter = [ "@system-service" "~ @privileged @resources" ];
288 UMask = "0077";
289 };
290 };
291
292 systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable {
293 wantedBy = [ "firefox-syncserver.service" ];
294 requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
295 after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service";
296 path = [ config.services.mysql.package ];
297 serviceConfig.ExecStart = [ "${setupScript}" ];
298 };
299
300 services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx {
301 ${cfg.singleNode.hostname} = {
302 enableACME = cfg.singleNode.enableTLS;
303 forceSSL = cfg.singleNode.enableTLS;
304 locations."/" = {
305 proxyPass = "http://127.0.0.1:${toString cfg.settings.port}";
306 };
307 };
308 };
309 };
310
311 meta = {
312 maintainers = with lib.maintainers; [ pennae ];
313 # Don't edit the docbook xml directly, edit the md and generate it:
314 # `pandoc firefox-syncserver.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > firefox-syncserver.xml`
315 doc = ./firefox-syncserver.xml;
316 };
317}