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