diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml index aaa85138dfa1..87066e6cdf26 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml @@ -124,6 +124,13 @@ virtualisation.appvm. + + + syncstorage-rs, + a self-hostable sync server for Firefox. Available as + services.firefox-syncserver. + + dragonflydb, diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md index e1253d46190f..be0d17f9054d 100644 --- a/nixos/doc/manual/release-notes/rl-2211.section.md +++ b/nixos/doc/manual/release-notes/rl-2211.section.md @@ -55,6 +55,7 @@ In addition to numerous new and upgraded packages, this release has the followin ## New Services {#sec-release-22.11-new-services} - [appvm](https://github.com/jollheef/appvm), Nix based app VMs. Available as [virtualisation.appvm](options.html#opt-virtualisation.appvm.enable). +- [syncstorage-rs](https://github.com/mozilla-services/syncstorage-rs), a self-hostable sync server for Firefox. Available as [services.firefox-syncserver](options.html#opt-services.firefox-syncserver.enable). - [dragonflydb](https://dragonflydb.io/), a modern replacement for Redis and Memcached. Available as [services.dragonflydb](#opt-services.dragonflydb.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 4c51210bc200..6e979561fa03 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -783,6 +783,7 @@ ./services/networking/expressvpn.nix ./services/networking/fakeroute.nix ./services/networking/ferm.nix + ./services/networking/firefox-syncserver.nix ./services/networking/fireqos.nix ./services/networking/firewall.nix ./services/networking/flannel.nix diff --git a/nixos/modules/services/networking/firefox-syncserver.md b/nixos/modules/services/networking/firefox-syncserver.md new file mode 100644 index 000000000000..3ee863343ece --- /dev/null +++ b/nixos/modules/services/networking/firefox-syncserver.md @@ -0,0 +1,55 @@ +# Firefox Sync server {#module-services-firefox-syncserver} + +A storage server for Firefox Sync that you can easily host yourself. + +## Quickstart {#module-services-firefox-syncserver-quickstart} + +The absolute minimal configuration for the sync server looks like this: + +```nix +services.mysql.package = pkgs.mariadb; + +services.firefox-syncserver = { + enable = true; + secrets = builtins.toFile "sync-secrets" '' + SYNC_MASTER_SECRET=this-secret-is-actually-leaked-to-/nix/store + ''; + singleNode = { + enable = true; + hostname = "localhost"; + url = "http://localhost:5000"; + }; +}; +``` + +This will start a sync server that is only accessible locally. Once the services is +running you can navigate to `about:config` in your Firefox profile and set +`identity.sync.tokenserver.uri` to `http://localhost:5000/1.0/sync/1.5`. Your browser +will now use your local sync server for data storage. + +::: {.warning} +This configuration should never be used in production. It is not encrypted and +stores its secrets in a world-readable location. +::: + +## More detailed setup {#module-services-firefox-syncserver-configuration} + +The `firefox-syncserver` service provides a number of options to make setting up +small deployment easier. These are grouped under the `singleNode` element of the +option tree and allow simple configuration of the most important parameters. + +Single node setup is split into two kinds of options: those that affect the sync +server itself, and those that affect its surroundings. Options that affect the +sync server are `capacity`, which configures how many accounts may be active on +this instance, and `url`, which holds the URL under which the sync server can be +accessed. The `url` can be configured automatically when using nginx. + +Options that affect the surroundings of the sync server are `enableNginx`, +`enableTLS` and `hostnam`. If `enableNginx` is set the sync server module will +automatically add an nginx virtual host to the system using `hostname` as the +domain and set `url` accordingly. If `enableTLS` is set the module will also +enable ACME certificates on the new virtual host and force all connections to +be made via TLS. + +For actual deployment it is also recommended to store the `secrets` file in a +secure location. diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix new file mode 100644 index 000000000000..254d5c1dc670 --- /dev/null +++ b/nixos/modules/services/networking/firefox-syncserver.nix @@ -0,0 +1,328 @@ +{ config, pkgs, lib, options, ... }: + +let + cfg = config.services.firefox-syncserver; + opt = options.services.firefox-syncserver; + defaultDatabase = "firefox_syncserver"; + defaultUser = "firefox-syncserver"; + + dbIsLocal = cfg.database.host == "localhost"; + dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}"; + + format = pkgs.formats.toml {}; + settings = { + database_url = dbURL; + human_logs = true; + tokenserver = { + node_type = "mysql"; + database_url = dbURL; + fxa_email_domain = "api.accounts.firefox.com"; + fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1"; + run_migrations = true; + } // lib.optionalAttrs cfg.singleNode.enable { + # Single-node mode is likely to be used on small instances with little + # capacity. The default value (0.1) can only ever release capacity when + # accounts are removed if the total capacity is 10 or larger to begin + # with. + # https://github.com/mozilla-services/syncstorage-rs/issues/1313#issuecomment-1145293375 + node_capacity_release_rate = 1; + }; + }; + configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings); +in + +{ + options = { + services.firefox-syncserver = { + enable = lib.mkEnableOption '' + the Firefox Sync storage service. + + Out of the box this will not be very useful unless you also configure at least + one service and one nodes by inserting them into the mysql database manually, e.g. + by running + + + INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}'); + INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`, + `capacity`, `downed`, `backoff`) + VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0'); + + + does this automatically when enabled + ''; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.syncstorage-rs; + defaultText = lib.literalExpression "pkgs.syncstorage-rs"; + description = '' + Package to use. + ''; + }; + + database.name = lib.mkOption { + # the mysql module does not allow `-quoting without resorting to shell + # escaping, so we restrict db names for forward compaitiblity should this + # behavior ever change. + type = lib.types.strMatching "[a-z_][a-z0-9_]*"; + default = defaultDatabase; + description = '' + Database to use for storage. Will be created automatically if it does not exist + and config.${opt.database.createLocally} is set. + ''; + }; + + database.user = lib.mkOption { + type = lib.types.str; + default = defaultUser; + description = '' + Username for database connections. + ''; + }; + + database.host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = '' + Database host name. localhost is treated specially and inserts + systemd dependencies, other hostnames or IP addresses of the local machine do not. + ''; + }; + + database.createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to create database and user on the local machine if they do not exist. + This includes enabling unix domain socket authentication for the configured user. + ''; + }; + + logLevel = lib.mkOption { + type = lib.types.str; + default = "error"; + description = '' + Log level to run with. This can be a simple log level like error + or trace, or a more complicated logging expression. + ''; + }; + + secrets = lib.mkOption { + type = lib.types.path; + description = '' + A file containing the various secrets. Should be in the format expected by systemd's + EnvironmentFile directory. Two secrets are currently available: + SYNC_MASTER_SECRET and + SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET. + ''; + }; + + singleNode = { + enable = lib.mkEnableOption "auto-configuration for a simple single-node setup"; + + enableTLS = lib.mkEnableOption "automatic TLS setup"; + + enableNginx = lib.mkEnableOption "nginx virtualhost definitions"; + + hostname = lib.mkOption { + type = lib.types.str; + description = '' + Host name to use for this service. + ''; + }; + + capacity = lib.mkOption { + type = lib.types.ints.unsigned; + default = 10; + description = '' + How many sync accounts are allowed on this server. Setting this value + equal to or less than the number of currently active accounts will + effectively deny service to accounts not yet registered here. + ''; + }; + + url = lib.mkOption { + type = lib.types.str; + default = "${if cfg.singleNode.enableTLS then "https" else "http"}://${cfg.singleNode.hostname}"; + defaultText = lib.literalExpression '' + ''${if cfg.singleNode.enableTLS then "https" else "http"}://''${config.${opt.singleNode.hostname}} + ''; + description = '' + URL of the host. If you are not using the automatic webserver proxy setup you will have + to change this setting or your sync server may not be functional. + ''; + }; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = format.type; + + options = { + port = lib.mkOption { + type = lib.types.port; + default = 5000; + description = '' + Port to bind to. + ''; + }; + + tokenserver.enabled = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to enable the token service as well. + ''; + }; + }; + }; + default = { }; + description = '' + Settings for the sync server. These take priority over values computed + from NixOS options. + + See the doc comments on the Settings structs in + + and + + for available options. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.mysql = lib.mkIf cfg.database.createLocally { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [{ + name = cfg.database.user; + ensurePermissions = { + "${cfg.database.name}.*" = "all privileges"; + }; + }]; + }; + + systemd.services.firefox-syncserver = { + wantedBy = [ "multi-user.target" ]; + requires = lib.mkIf dbIsLocal [ "mysql.service" ]; + after = lib.mkIf dbIsLocal [ "mysql.service" ]; + environment.RUST_LOG = cfg.logLevel; + serviceConfig = { + User = defaultUser; + Group = defaultUser; + ExecStart = "${cfg.package}/bin/syncstorage --config ${configFile}"; + Stderr = "journal"; + EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}"; + + # hardening + RemoveIPC = true; + CapabilityBoundingSet = [ "" ]; + DynamicUser = true; + NoNewPrivileges = true; + PrivateDevices = true; + ProtectClock = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + SystemCallArchitectures = "native"; + # syncstorage-rs uses python-cffi internally, and python-cffi does not + # work with MemoryDenyWriteExecute=true + MemoryDenyWriteExecute = false; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + ProtectHostname = true; + LockPersonality = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictRealtime = true; + ProtectSystem = "strict"; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectHome = true; + PrivateUsers = true; + PrivateTmp = true; + SystemCallFilter = [ "@system-service" "~ @privileged @resources" ]; + UMask = "0077"; + }; + }; + + systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable { + wantedBy = [ "firefox-syncserver.service" ]; + requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service"; + after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service"; + path = [ config.services.mysql.package ]; + script = '' + set -euo pipefail + shopt -s inherit_errexit + + schema_configured() { + mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services + } + + services_configured() { + [ 1 != $(mysql ${cfg.database.name} -Ne 'SELECT COUNT(*) < 1 FROM `services`') ] + } + + create_services() { + mysql ${cfg.database.name} <<"EOF" + BEGIN; + + INSERT INTO `services` (`id`, `service`, `pattern`) + VALUES (1, 'sync-1.5', '{node}/1.5/{uid}'); + INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`, + `capacity`, `downed`, `backoff`) + VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity}, + 0, ${toString cfg.singleNode.capacity}, 0, 0); + + COMMIT; + EOF + } + + update_nodes() { + mysql ${cfg.database.name} <<"EOF" + UPDATE `nodes` + SET `capacity` = ${toString cfg.singleNode.capacity} + WHERE `id` = 1; + EOF + } + + for (( try = 0; try < 60; try++ )); do + if ! schema_configured; then + sleep 2 + elif services_configured; then + update_nodes + exit 0 + else + create_services + exit 0 + fi + done + + echo "Single-node setup failed" + exit 1 + ''; + }; + + services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx { + ${cfg.singleNode.hostname} = { + enableACME = cfg.singleNode.enableTLS; + forceSSL = cfg.singleNode.enableTLS; + locations."/" = { + proxyPass = "http://localhost:${toString cfg.settings.port}"; + # source mentions that this header should be set + extraConfig = '' + add_header X-Content-Type-Options nosniff; + ''; + }; + }; + }; + }; + + meta = { + maintainers = with lib.maintainers; [ pennae ]; + # Don't edit the docbook xml directly, edit the md and generate it: + # `pandoc firefox-syncserver.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > firefox-syncserver.xml` + doc = ./firefox-syncserver.xml; + }; +} diff --git a/nixos/modules/services/networking/firefox-syncserver.xml b/nixos/modules/services/networking/firefox-syncserver.xml new file mode 100644 index 000000000000..66c812266951 --- /dev/null +++ b/nixos/modules/services/networking/firefox-syncserver.xml @@ -0,0 +1,77 @@ + + Firefox Sync server + + A storage server for Firefox Sync that you can easily host yourself. + +
+ Quickstart + + The absolute minimal configuration for the sync server looks like + this: + + +services.mysql.package = pkgs.mariadb; + +services.firefox-syncserver = { + enable = true; + secrets = builtins.toFile "sync-secrets" '' + SYNC_MASTER_SECRET=this-secret-is-actually-leaked-to-/nix/store + ''; + singleNode = { + enable = true; + hostname = "localhost"; + url = "http://localhost:5000"; + }; +}; + + + This will start a sync server that is only accessible locally. + Once the services is running you can navigate to + about:config in your Firefox profile and set + identity.sync.tokenserver.uri to + http://localhost:5000/1.0/sync/1.5. Your + browser will now use your local sync server for data storage. + + + + This configuration should never be used in production. It is not + encrypted and stores its secrets in a world-readable location. + + +
+
+ More detailed setup + + The firefox-syncserver service provides a + number of options to make setting up small deployment easier. + These are grouped under the singleNode element + of the option tree and allow simple configuration of the most + important parameters. + + + Single node setup is split into two kinds of options: those that + affect the sync server itself, and those that affect its + surroundings. Options that affect the sync server are + capacity, which configures how many accounts + may be active on this instance, and url, which + holds the URL under which the sync server can be accessed. The + url can be configured automatically when using + nginx. + + + Options that affect the surroundings of the sync server are + enableNginx, enableTLS and + hostnam. If enableNginx is + set the sync server module will automatically add an nginx virtual + host to the system using hostname as the domain + and set url accordingly. If + enableTLS is set the module will also enable + ACME certificates on the new virtual host and force all + connections to be made via TLS. + + + For actual deployment it is also recommended to store the + secrets file in a secure location. + +
+
diff --git a/pkgs/servers/syncstorage-rs/default.nix b/pkgs/servers/syncstorage-rs/default.nix new file mode 100644 index 000000000000..17a7bb799fdf --- /dev/null +++ b/pkgs/servers/syncstorage-rs/default.nix @@ -0,0 +1,63 @@ +{ fetchFromGitHub +, rustPlatform +, pkg-config +, python3 +, openssl +, cmake +, libmysqlclient +, makeBinaryWrapper +, lib +}: + +let + pyFxADeps = python3.withPackages (p: [ + p.setuptools # imports pkg_resources + # remainder taken from requirements.txt + p.pyfxa + p.tokenlib + p.cryptography + ]); +in + +rustPlatform.buildRustPackage rec { + pname = "syncstorage-rs"; + version = "0.12.0"; + + src = fetchFromGitHub { + owner = "mozilla-services"; + repo = pname; + rev = version; + hash = "sha256-VfIpjpBS7LXe32fxIFp7xmbm40VwxUdHIEm5PnMpd4s="; + }; + + nativeBuildInputs = [ + cmake + makeBinaryWrapper + pkg-config + python3 + ]; + + buildInputs = [ + libmysqlclient + openssl + ]; + + preFixup = '' + wrapProgram $out/bin/syncstorage \ + --prefix PATH : ${lib.makeBinPath [ pyFxADeps ]} + ''; + + cargoSha256 = "sha256-JXxArKA/2SIYJvjNA1yZHR9xDKt3N2U7HVMP/6M3BxE="; + + buildFeatures = [ "grpcio/openssl" ]; + + # almost all tests need a DB to test against + doCheck = false; + + meta = { + description = "Mozilla Sync Storage built with Rust"; + homepage = "https://github.com/mozilla-services/syncstorage-rs"; + license = lib.licenses.mpl20; + maintainers = with lib.maintainers; [ pennae ]; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 68cdd2a8958e..018e6dfc9084 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -22805,6 +22805,8 @@ with pkgs; sympa = callPackage ../servers/mail/sympa { }; + syncstorage-rs = callPackage ../servers/syncstorage-rs { }; + system-sendmail = lowPrio (callPackage ../servers/mail/system-sendmail { }); # PulseAudio daemons