at master 11 kB view raw
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}