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