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