diff --git a/AGENTS.md b/AGENTS.md index 31661b3..9dbb460 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,6 +98,7 @@ Each service file in `services/` follows this structure: - **git-crypt**: `secrets/` directory and `usb-secrets/usb-secrets-key*` are encrypted (see `.gitattributes`) - **agenix**: secrets declared in `modules/age-secrets.nix`, decrypted at runtime to `/run/agenix/` - **Identity**: USB drive at `/mnt/usb-secrets/usb-secrets-key` +- **Encrypting new secrets**: The agenix encryption key is in `usb-secrets/usb-secrets-key` (SSH private key, git-crypt encrypted). To create a new secret: derive the age public key with `ssh-keygen -y -f usb-secrets/usb-secrets-key | ssh-to-age`, then encrypt with `age -r -o secrets/.age`. - Never read or commit plaintext secrets. Never log secret values. ### Important Patterns diff --git a/configuration.nix b/configuration.nix index 2c19e8b..066cf4f 100644 --- a/configuration.nix +++ b/configuration.nix @@ -47,6 +47,7 @@ ./services/ups.nix ./services/bitwarden.nix + ./services/firefox-syncserver.nix ./services/matrix.nix ./services/coturn.nix diff --git a/flake.nix b/flake.nix index 5fb257a..e62b269 100644 --- a/flake.nix +++ b/flake.nix @@ -105,7 +105,19 @@ service_configs = import ./service-configs.nix; - pkgs = import nixpkgs { + # Bootstrap pkgs used only to apply patches to nixpkgs source. + bootstrapPkgs = import nixpkgs { inherit system; }; + + # Patch nixpkgs to add PostgreSQL backend support for firefox-syncserver. + patchedNixpkgsSrc = bootstrapPkgs.applyPatches { + name = "nixpkgs-patched"; + src = nixpkgs; + patches = [ + ./patches/0001-firefox-syncserver-add-postgresql-backend-support.patch + ]; + }; + + pkgs = import patchedNixpkgsSrc { inherit system; targetPlatform = system; buildPlatform = builtins.currentSystem; @@ -157,10 +169,21 @@ ./disk-config.nix ./configuration.nix + # Replace upstream firefox-syncserver module + package with patched + # versions that add PostgreSQL backend support. { + disabledModules = [ "services/networking/firefox-syncserver.nix" ]; + imports = [ + "${patchedNixpkgsSrc}/nixos/modules/services/networking/firefox-syncserver.nix" + ]; nixpkgs.overlays = [ nix-minecraft.overlay (import ./modules/overlays.nix) + (_final: prev: { + syncstorage-rs = + prev.callPackage "${patchedNixpkgsSrc}/pkgs/by-name/sy/syncstorage-rs/package.nix" + { }; + }) ]; nixpkgs.config.allowUnfreePredicate = pkg: diff --git a/modules/age-secrets.nix b/modules/age-secrets.nix index c4af393..ab42655 100644 --- a/modules/age-secrets.nix +++ b/modules/age-secrets.nix @@ -82,5 +82,11 @@ owner = "root"; group = "root"; }; + + # Firefox Sync server secrets (SYNC_MASTER_SECRET) + firefox-syncserver-env = { + file = ../secrets/firefox-syncserver-env.age; + mode = "0400"; + }; }; } diff --git a/patches/0001-firefox-syncserver-add-postgresql-backend-support.patch b/patches/0001-firefox-syncserver-add-postgresql-backend-support.patch new file mode 100644 index 0000000..67c59db --- /dev/null +++ b/patches/0001-firefox-syncserver-add-postgresql-backend-support.patch @@ -0,0 +1,379 @@ +From ab57092a60123e361cf0de1c1a314a9888c45219 Mon Sep 17 00:00:00 2001 +From: Simon Gardling +Date: Sat, 21 Mar 2026 09:24:39 -0400 +Subject: [PATCH] temp + +--- + .../services/networking/firefox-syncserver.md | 23 +++ + .../networking/firefox-syncserver.nix | 140 ++++++++++++++---- + pkgs/by-name/sy/syncstorage-rs/package.nix | 49 ++++-- + 3 files changed, 174 insertions(+), 38 deletions(-) + +diff --git a/nixos/modules/services/networking/firefox-syncserver.md b/nixos/modules/services/networking/firefox-syncserver.md +index 991e97f799d6..3bc45cfa5640 100644 +--- a/nixos/modules/services/networking/firefox-syncserver.md ++++ b/nixos/modules/services/networking/firefox-syncserver.md +@@ -32,6 +32,29 @@ This configuration should never be used in production. It is not encrypted and + stores its secrets in a world-readable location. + ::: + ++## Database backends {#module-services-firefox-syncserver-database} ++ ++The sync server supports MySQL/MariaDB (the default) and PostgreSQL as database ++backends. Set `database.type` to choose the backend: ++ ++```nix ++{ ++ services.firefox-syncserver = { ++ enable = true; ++ database.type = "postgresql"; ++ secrets = "/run/secrets/firefox-syncserver"; ++ singleNode = { ++ enable = true; ++ hostname = "localhost"; ++ url = "http://localhost:5000"; ++ }; ++ }; ++} ++``` ++ ++When `database.createLocally` is `true` (the default), the module will ++automatically enable and configure the corresponding database service. ++ + ## More detailed setup {#module-services-firefox-syncserver-configuration} + + The `firefox-syncserver` service provides a number of options to make setting up +diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix +index 6a50e49fc096..70a56314e323 100644 +--- a/nixos/modules/services/networking/firefox-syncserver.nix ++++ b/nixos/modules/services/networking/firefox-syncserver.nix +@@ -13,7 +13,21 @@ let + defaultUser = "firefox-syncserver"; + + dbIsLocal = cfg.database.host == "localhost"; +- dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}"; ++ dbIsMySQL = cfg.database.type == "mysql"; ++ dbIsPostgreSQL = cfg.database.type == "postgresql"; ++ ++ dbURL = ++ if dbIsMySQL then ++ "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}" ++ else ++ "postgres://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?host=/run/postgresql"}"; ++ ++ # postgresql.target waits for postgresql-setup.service (which runs ++ # ensureDatabases / ensureUsers) to complete, avoiding race conditions ++ # where the syncserver starts before its database and role exist. ++ dbService = if dbIsMySQL then "mysql.service" else "postgresql.target"; ++ ++ syncserver = cfg.package.override { dbBackend = cfg.database.type; }; + + format = pkgs.formats.toml { }; + settings = { +@@ -22,7 +36,7 @@ let + database_url = dbURL; + }; + tokenserver = { +- node_type = "mysql"; ++ node_type = if dbIsMySQL then "mysql" else "postgres"; + database_url = dbURL; + fxa_email_domain = "api.accounts.firefox.com"; + fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1"; +@@ -41,7 +55,8 @@ let + }; + }; + configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings); +- setupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' ++ ++ mysqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' + set -euo pipefail + shopt -s inherit_errexit + +@@ -79,6 +94,47 @@ let + echo "Single-node setup failed" + exit 1 + ''; ++ ++ postgresqlSetupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' ++ set -euo pipefail ++ shopt -s inherit_errexit ++ ++ schema_configured() { ++ psql -d ${cfg.database.name} -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'services')" | grep -q t ++ } ++ ++ update_config() { ++ psql -d ${cfg.database.name} <<'EOF' ++ BEGIN; ++ ++ INSERT INTO services (id, service, pattern) ++ VALUES (1, 'sync-1.5', '{node}/1.5/{uid}') ++ ON CONFLICT (id) DO UPDATE SET service = 'sync-1.5', pattern = '{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) ++ ON CONFLICT (id) DO UPDATE SET node = '${cfg.singleNode.url}', capacity = ${toString cfg.singleNode.capacity}; ++ ++ COMMIT; ++ EOF ++ } ++ ++ ++ for (( try = 0; try < 60; try++ )); do ++ if ! schema_configured; then ++ sleep 2 ++ else ++ update_config ++ exit 0 ++ fi ++ done ++ ++ echo "Single-node setup failed" ++ exit 1 ++ ''; ++ ++ setupScript = if dbIsMySQL then mysqlSetupScript else postgresqlSetupScript; + in + + { +@@ -88,25 +144,26 @@ in + 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'); +- ``` ++ one service and one nodes by inserting them into the database manually, e.g. ++ by running the equivalent SQL for your database backend. + + {option}`${opt.singleNode.enable}` does this automatically when enabled + ''; + + package = lib.mkPackageOption pkgs "syncstorage-rs" { }; + ++ database.type = lib.mkOption { ++ type = lib.types.enum [ ++ "mysql" ++ "postgresql" ++ ]; ++ default = "mysql"; ++ description = '' ++ Which database backend to use for storage. ++ ''; ++ }; ++ + 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 = '' +@@ -117,9 +174,15 @@ in + + database.user = lib.mkOption { + type = lib.types.str; +- default = defaultUser; ++ default = if dbIsPostgreSQL then defaultDatabase else defaultUser; ++ defaultText = lib.literalExpression '' ++ if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}" ++ ''; + description = '' +- Username for database connections. ++ Username for database connections. When using PostgreSQL with ++ `createLocally`, this defaults to the database name so that ++ `ensureDBOwnership` works (it requires user and database names ++ to match). + ''; + }; + +@@ -137,7 +200,8 @@ in + 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. ++ This includes enabling the configured database service and setting up ++ authentication for the configured user. + ''; + }; + +@@ -237,7 +301,7 @@ in + }; + + config = lib.mkIf cfg.enable { +- services.mysql = lib.mkIf cfg.database.createLocally { ++ services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ +@@ -250,16 +314,27 @@ in + ]; + }; + ++ services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) { ++ enable = true; ++ ensureDatabases = [ cfg.database.name ]; ++ ensureUsers = [ ++ { ++ name = cfg.database.user; ++ ensureDBOwnership = true; ++ } ++ ]; ++ }; ++ + systemd.services.firefox-syncserver = { + wantedBy = [ "multi-user.target" ]; +- requires = lib.mkIf dbIsLocal [ "mysql.service" ]; +- after = lib.mkIf dbIsLocal [ "mysql.service" ]; ++ requires = lib.mkIf dbIsLocal [ dbService ]; ++ after = lib.mkIf dbIsLocal [ dbService ]; + restartTriggers = lib.optional cfg.singleNode.enable setupScript; + environment.RUST_LOG = cfg.logLevel; + serviceConfig = { +- User = defaultUser; +- Group = defaultUser; +- ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}"; ++ User = cfg.database.user; ++ Group = cfg.database.user; ++ ExecStart = "${syncserver}/bin/syncserver --config ${configFile}"; + EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}"; + + # hardening +@@ -303,10 +378,19 @@ in + + 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 ]; +- serviceConfig.ExecStart = [ "${setupScript}" ]; ++ requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService; ++ after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService; ++ path = ++ if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ]; ++ serviceConfig = { ++ ExecStart = [ "${setupScript}" ]; ++ } ++ // lib.optionalAttrs dbIsPostgreSQL { ++ # PostgreSQL peer authentication requires the system user to match the ++ # database user. Run as the superuser so we can access all databases. ++ User = "postgres"; ++ Group = "postgres"; ++ }; + }; + + services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx { +diff --git a/pkgs/by-name/sy/syncstorage-rs/package.nix b/pkgs/by-name/sy/syncstorage-rs/package.nix +index 39b2b53ab03c..944ed72525af 100644 +--- a/pkgs/by-name/sy/syncstorage-rs/package.nix ++++ b/pkgs/by-name/sy/syncstorage-rs/package.nix +@@ -1,14 +1,18 @@ + { + fetchFromGitHub, ++ fetchurl, + rustPlatform, + pkg-config, + python3, + cmake, + libmysqlclient, ++ libpq, ++ openssl, + makeBinaryWrapper, + lib, + nix-update-script, + nixosTests, ++ dbBackend ? "mysql", + }: + + let +@@ -19,17 +23,23 @@ let + p.tokenlib + p.cryptography + ]); ++ # utoipa-swagger-ui downloads Swagger UI assets at build time. ++ # Prefetch the archive for sandboxed builds. ++ swaggerUi = fetchurl { ++ url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip"; ++ hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw="; ++ }; + in + +-rustPlatform.buildRustPackage rec { ++rustPlatform.buildRustPackage (finalAttrs: { + pname = "syncstorage-rs"; +- version = "0.21.1-unstable-2026-01-26"; ++ version = "0.21.1-unstable-2026-02-24"; + + src = fetchFromGitHub { + owner = "mozilla-services"; + repo = "syncstorage-rs"; +- rev = "11659d98f9c69948a0aab353437ce2036c388711"; +- hash = "sha256-G37QvxTNh/C3gmKG0UYHI6QBr0F+KLGRNI/Sx33uOsc="; ++ rev = "50a739b58dc9ec81995f86e71d992aa14ccc450e"; ++ hash = "sha256-idq0RGdwoV6GVuq36IVVVCFbyMTe8i/EpVWE59D/dhM="; + }; + + nativeBuildInputs = [ +@@ -39,16 +49,35 @@ rustPlatform.buildRustPackage rec { + python3 + ]; + +- buildInputs = [ +- libmysqlclient +- ]; ++ buildInputs = ++ lib.optional (dbBackend == "mysql") libmysqlclient ++ ++ lib.optionals (dbBackend == "postgresql") [ ++ libpq ++ openssl ++ ]; ++ ++ buildNoDefaultFeatures = true; ++ # The syncserver "postgres" feature only enables syncstorage-db/postgres. ++ # tokenserver-db/postgres must be enabled separately so the tokenserver ++ # can also connect to PostgreSQL (it dispatches on the URL scheme at runtime). ++ buildFeatures = ++ let ++ cargoFeature = if dbBackend == "postgresql" then "postgres" else dbBackend; ++ in ++ [ ++ cargoFeature ++ "tokenserver-db/${cargoFeature}" ++ "py_verifier" ++ ]; ++ ++ SWAGGER_UI_DOWNLOAD_URL = "file://${swaggerUi}"; + + preFixup = '' + wrapProgram $out/bin/syncserver \ + --prefix PATH : ${lib.makeBinPath [ pyFxADeps ]} + ''; + +- cargoHash = "sha256-9Dcf5mDyK/XjsKTlCPXTHoBkIq+FFPDg1zfK24Y9nHQ="; ++ cargoHash = "sha256-80EztkSX+SnmqsRWIXbChUB8AeV1Tp9WXoWNbDY8rUE="; + + # almost all tests need a DB to test against + doCheck = false; +@@ -60,10 +89,10 @@ rustPlatform.buildRustPackage rec { + meta = { + description = "Mozilla Sync Storage built with Rust"; + homepage = "https://github.com/mozilla-services/syncstorage-rs"; +- changelog = "https://github.com/mozilla-services/syncstorage-rs/releases/tag/${version}"; ++ changelog = "https://github.com/mozilla-services/syncstorage-rs/releases/tag/${finalAttrs.version}"; + license = lib.licenses.mpl20; + maintainers = [ ]; + platforms = lib.platforms.linux; + mainProgram = "syncserver"; + }; +-} ++}) +-- +2.53.0 + diff --git a/secrets/firefox-syncserver-env.age b/secrets/firefox-syncserver-env.age new file mode 100644 index 0000000..bd2cfc2 Binary files /dev/null and b/secrets/firefox-syncserver-env.age differ diff --git a/service-configs.nix b/service-configs.nix index e1fb0c1..93d0f1a 100644 --- a/service-configs.nix +++ b/service-configs.nix @@ -44,6 +44,7 @@ rec { monero_rpc = 18081; # TCP monero_zmq = 18083; # TCP p2pool_stratum = 3334; # TCP + firefox_syncserver = 5000; # TCP }; https = { @@ -147,6 +148,10 @@ rec { dataDir = services_dir + "/recyclarr"; }; + firefox_syncserver = { + domain = "firefox-sync.${https.domain}"; + }; + media = { moviesDir = torrents_path + "/media/movies"; tvDir = torrents_path + "/media/tv"; diff --git a/services/firefox-syncserver.nix b/services/firefox-syncserver.nix new file mode 100644 index 0000000..8e783bc --- /dev/null +++ b/services/firefox-syncserver.nix @@ -0,0 +1,39 @@ +{ + config, + lib, + pkgs, + service_configs, + ... +}: +{ + services.firefox-syncserver = { + enable = true; + database = { + type = "postgresql"; + createLocally = false; + user = "firefox_syncserver"; + }; + secrets = config.age.secrets.firefox-syncserver-env.path; + settings.port = service_configs.ports.firefox_syncserver; + singleNode = { + enable = true; + hostname = service_configs.firefox_syncserver.domain; + url = "https://${service_configs.firefox_syncserver.domain}"; + capacity = 10; + }; + }; + + services.postgresql = { + ensureDatabases = [ "firefox_syncserver" ]; + ensureUsers = [ + { + name = "firefox_syncserver"; + ensureDBOwnership = true; + } + ]; + }; + + services.caddy.virtualHosts."${service_configs.firefox_syncserver.domain}".extraConfig = '' + reverse_proxy :${builtins.toString service_configs.ports.firefox_syncserver} + ''; +}