Running syncstorage-rs in 2025
Firefox Sync is a great feature of Firefox. It lets you share your bookmarks, history and passwords among all the devices you use Firefox on. You can see the tabs you have open on other devices and you can send tabs from one device to another.
Best of all, you can host your own sync server so none of this data goes to Mozilla's or Google's servers.
For years, hosting your own SyncServer-1.5 was easy. But SyncServer-1.5 is dead. Mozilla abandoned it to rewrite it in Rust, which still hasn't reached feature parity. It remains a Python 2 codebase, and most Linux distros have dropped support for Python 2.
So, it's time to bite the bullet and move to the new implementation. It's not easy. Self-hosting is now a second-class citizen. But it can be done. This article will show you how.
This article covers syncserver as of January 2025. The instructions have been tested on Ubuntu 24.04.1, but should be readily adaptable to other distros. For consistency, the term "syncserver" refers to the new server written in Rust and the term "SyncServer-1.5" refers to the old server written in Python.
What you need to get started
You will need:
- A Firefox account
- A proxying web server such as Apache, Caddy or nginx
- MySQL or MariaDB
- Cargo, the Rust toolchain
You do not need Python since February 2024
A Firefox account
For now, you need a Firefox account to use Firefox Sync
You shouldn't, but you do. Mozilla offer you their full auth server for free, but it's even more difficult to self-host than syncserver, and nobody has written an alternative. So for now, signing up with Mozilla is your only choice.
What does that mean for your privacy?
Mozilla have documented how Firefox Authentication works; The Firefox browser never sends Mozilla your actual passphrase, they don't learn your syncserver's URL, synced data is is sent to your server rather than Mozilla, and your data is encrypted with a separate key derived from the browser passphrase, so Mozilla should not have access to your synced bookmark/password/history data.
Nonetheless, Mozilla will get your email address. It needs to be a real email address that only you control, because they send login codes to it. While you only need to login once per browser, the browser does regularly fetch auth tokens from Mozilla. This means Mozilla learns on a regular basis all the IP addresses you browse from, and can tie them to your email address.
A proxying web server
It is possible to set up syncserver so that it's directly on the internet, but I strongly advise you don't do that. Instead, host your self-hosted services behind a proxying web server such as Caddy, Apache httpd or nginx. This allows you to do things like:
- Have more than one service hosted at the same address
- Have a secure HTTPS endpoint using auto-renewing Let's Encrypt certificates
- Perform intrusion detection with fail2ban
- Standardise access logs for all services you host
I'm going to assume you have set up a web server to your own liking. Installation, setup and maintenance of your web server is outside the scope of this article, as are topics like setting up Let's Encrypt, setting up DDNS, port forwarding to your home server, and so on. I will only cover how to configure it to proxy to the syncserver service.
MySQL or MariaDB
syncserver supports either MySQL/MariaDB or Google's cloud database Spanner. The whole point of this article is you don't want your data in The Cloud, you want it self-hosted. So we must use MySQL or MariaDB.
There is work on supporting SQLite, but as of January 2025, it's still incomplete
Choose a base URL
Decide on a URL for your syncserver service. It should ideally use HTTPS and a fully qualified domain name, so whether you're at home or out-and-about, your devices can securely connect to your server. The example I'll be using throughout this article is:
https://myserver.example.com/firefox-sync
The real URL of the syncserver service will be http://localhost:8000/ but this will not be accessible to the outside world. Everything will be proxied by your web server to the syncserver service.
Configure the web server proxy
Here are configuration snippets showing how to configure Caddy, Apache or nginx to proxy to the syncserver service that will run on localhost port 8000.
They pass X-Forwarded-Host and X-Forwarded-Proto headers for reasons given below. If your base URL uses a non-standard port (not http port 80 or https port 443), include it in X-Forwarded-Host, e.g. myserver.example.com:8443.
- Caddy proxying configuration (Caddyfile)
handle_path /firefox-sync/* { reverse_proxy localhost:8000 # implicitly sends X-Forwarded-Host and X-Forwarded-Proto }
- Apache proxying configuration (httpd.conf)
<Location "/firefox-sync"> ProxyPass http://localhost:8000 # implicitly sends X-Fowarded-Host RequestHeader set X-Forwarded-Proto "https" </Location>
- nginx proxying configuration (nginx.conf)
location /firefox-sync { proxy_pass http://localhost:8000/; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto "https"; }
Why X-Forwarded-Host and X-Forwarded-Proto headers are needed
The example proxy configurations pass X-Forwarded-Host and X-Forwarded-Proto headers. Why?
The Firefox Sync protocol signs requests with a Hawk token, which includes a hash of the absolute URL. To check the signature is valid, syncserver has to determine the absolute URL of the current request, hash it, and check it matches the hash in the request signature. If they don't match, you get the error calculated mac doesn't match header.
Therefore syncserver must know its own public URL.
SyncServer-1.5 let you configure public_url = https://myserver.example.com/firefox-sync. syncserver doesn't support that and instead guesses what the URL might be. By default, it guesses wrong and thinks the URL begins http://localhost.config:8000/.
syncserver will look for a port number on the end of the hostname (which comes from either the Forwarded, X-Forwarded-Host or Host header), otherwise it will look at the scheme (which comes from either the Forwarded or X-Forwarded-Proto header and assume port 80 for "http" and port 443 for "https". If no scheme is defined, it assumes port 80.
syncserver also has no support for the /firefox-sync prefix in the URL. We will address this later by directly patching the code.
Set up syncserver on your own host
Install prerequisites
This will install Cargo, git and the MySQL server and client development libraries:
sudo apt-get install cargo git libmysqlclient-dev mysql-server
If you prefer MariaDB, run this instead:
sudo apt-get install cargo git libmariadb-dev-compat mariadb-server
This uses libmariadb-dev-compat because the Rust crate used for MySQL is hardcoded to use MySQL library names.
Download and compile syncserver
This will clone the syncstorage-rs repository and build the syncserver executable. It will enable only the MySQL/MariaDB storage feature. It will write crate versions to Cargo.lock and stick to them. Note that cargo install takes about 15-20 minutes to run!
git clone https://github.com/mozilla-services/syncstorage-rs cd syncstorage-rs cargo install --debug --path ./syncserver --no-default-features --features=syncstorage-db/mysql --locked
Create database accounts
This will generate a long random password, create a new syncstorage user in MySQL/MariaDB with the password, create two databases, and grant the syncstorage user full access to both of them.
DBPASSWORD=$(cat /dev/random | base32 | head -c64) echo The database password is ${DBPASSWORD} sudo mysql -u root <<EOF CREATE USER "syncstorage"@"localhost" IDENTIFIED BY "${DBPASSWORD}"; CREATE DATABASE syncstorage_rs; CREATE DATABASE tokenserver_rs; GRANT ALL PRIVILEGES on syncstorage_rs.* to syncstorage@localhost; GRANT ALL PRIVILEGES on tokenserver_rs.* to syncstorage@localhost; EOF
Create a config file
This will generate a master secret and create a the config file. The environment variable DBPASSWORD should still be set from the previous step.
MASTER_SECRET=$(cat /dev/random | base32 | head -c64) cat >config.toml <<EOF master_secret = "${MASTER_SECRET}" human_logs = 1 host = "localhost" # default port = 8000 # default syncstorage.enabled = true syncstorage.database_url = "mysql://syncstorage:${DBPASSWORD}@localhost/syncstorage_rs?unix_socket=/run/mysqld/mysqld.sock" syncstorage.enable_quota = 0 syncstorage.limits.max_total_records = 1666 # See issues #298/#333 tokenserver.enabled = true tokenserver.database_url = "mysql://syncstorage:${DBPASSWORD}@localhost/tokenserver_rs?unix_socket=/run/mysqld/mysqld.sock" tokenserver.run_migrations = true tokenserver.fxa_email_domain = "api.accounts.firefox.com" tokenserver.fxa_oauth_server_url = "https://oauth.accounts.firefox.com" tokenserver.fxa_browserid_audience = "https://token.services.mozilla.com" tokenserver.fxa_browserid_issuer = "https://api.accounts.firefox.com" tokenserver.fxa_browserid_server_url = "https://verifier.accounts.firefox.com/v2" EOF
Compared to the example config, this config file:
- Uses the /run/mysqld/mysqld.sock socket rather than make network connections to MySQL/MariaDB
- Does not configure a metrics hash (more on that later)
- Ensures all database tables get created
- Uses production, rather than staging, Firefox Account identifiers for verifying tokens
Initial run to set up database tables
Now you can run syncserver for the first time. It will create the tables it needs in the MySQL/MariaDB databases
Once the logs printed to screen indicate syncserver has created the database tables, press Ctrl+C to stop it.
RUST_LOG=trace ./target/debug/syncserver --config ./config.toml
Add more config in database tables
We are now going to add config directly to the database. It cannot be set in the config file.
This tells the tokenserver component of syncserver how to direct users to their storage, and that up to 42 distinct users are allowed:
sudo mysql -u root <<EOF USE tokenserver_rs 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://myserver.example.com/firefox-sync', 1, 0, 42, 0, 0); EOF
Code patch: remove the callbacks to Sentry API
We're about ready to run syncserver again. But before we do, wed need to fix some issues in the codebase.
Firstly, Mozilla sends metrics to a third party analytics product called Sentry. Their "metrics hash" is meant obscure the user identities when they send data to third party servers. But we don't want any data sent to third party servers! That's why we're self-hosting!
Sentry should be disabled by default in the config, but rather than leave it to chance, let's explicitly remove the code that sends data to Sentry
Apply this patch by running patch -p1, paste the diff below, and then press Enter followed by Ctrl+D
diff --git a/syncserver/src/server/mod.rs b/syncserver/src/server/mod.rs index f1e0a18..4c5caf5 100644 --- a/syncserver/src/server/mod.rs +++ b/syncserver/src/server/mod.rs @@ -14,7 +14,7 @@ use actix_web::{ use cadence::{Gauged, StatsdClient}; use futures::future::{self, Ready}; use syncserver_common::{ - middleware::sentry::SentryWrapper, BlockingThreadpool, BlockingThreadpoolMetrics, Metrics, + BlockingThreadpool, BlockingThreadpoolMetrics, Metrics, Taggable, }; use syncserver_db_common::{GetPoolState, PoolState}; @@ -95,7 +95,6 @@ macro_rules! build_app { // These will wrap all outbound responses with matching status codes. .wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, ApiError::render_404)) // These are our wrappers - .wrap(SentryWrapper::<ApiError>::new($metrics.clone())) .wrap_fn(middleware::weave::set_weave_timestamp) .wrap_fn(tokenserver::logging::handle_request_log_line) .wrap_fn(middleware::rejectua::reject_user_agent) @@ -201,9 +200,6 @@ macro_rules! build_app_without_syncstorage { // Middleware is applied LIFO // These will wrap all outbound responses with matching status codes. .wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, ApiError::render_404)) - .wrap(SentryWrapper::<tokenserver_common::TokenserverError>::new( - $metrics.clone(), - )) // These are our wrappers .wrap_fn(tokenserver::logging::handle_request_log_line) .wrap_fn(middleware::rejectua::reject_user_agent)
Code patch: Fix up the relative path
As mentioned under #Why X-Forwarded-Host and X-Forwarded-Proto headers are needed, syncserver guesses its public URL rather than support the public_url config entry that SyncServer-1.5 did, and when it guesses wrong it gets authentication errors saying calculated mac doesn't match header.
The chosen base URL in this article is https://myserver.example.com/firefox-sync. Setting up request headers X-Forwarded-Host and X-Forwarded-Proto lets syncserver get the https://myserver.example.com part right, but there's no support for configuring the base URL prefix /firefox-sync. The quickest way to do this is to write it directly into the code.
Apply this patch by running patch -p1, paste the diff below, and then press Enter followed by Ctrl+D
diff --git a/syncserver/src/web/auth.rs b/syncserver/src/web/auth.rs index 9153d38..0a7af57 100644 --- a/syncserver/src/web/auth.rs +++ b/syncserver/src/web/auth.rs @@ -197,6 +197,7 @@ impl HawkPayload { Utc::now().timestamp() as u64 }; + let path = "/firefox-sync".to_owned() + path.as_str(); // prepend prefix to path HawkPayload::new(header, method, path.as_str(), host, port, secrets, expiry) } }
Compile syncserver again in release mode
This compiles syncserver again now the above patches have been applied. Also it compiles in release mode rather than debug mode. We permanently lose trace-level and debug-level log messages, but we get a much smaller and faster binary.
cargo install --path ./syncserver --no-default-features --features=syncstorage-db/mysql --locked && strip target/release/syncserver
Install syncserver as a service
We can now install syncserver. This will install it in /usr/local, create a systemd service called syncserver and start it running.
If you are running MariaDB rather than MySQL, change mysql.service to mariadb.service.
If you are using Caddy as your webserver, change www-data to caddy.
sudo install target/release/syncserver /usr/local/sbin/syncserver sudo install -m 600 -o www-data config.toml /usr/local/etc/syncserver-config.toml cat >syncserver.service <<EOF [Unit] Description=Mozilla Firefox Sync Wants=mysql.service After=network.target mysql.service [Service] Type=simple User=www-data Environment="RUST_LOG=info" ExecStart=/usr/local/sbin/syncserver --config /usr/local/etc/syncserver-config.toml [Install] WantedBy=multi-user.target EOF sudo install syncserver.service /etc/systemd/system/syncserver.service sudo systemctl daemon-reload sudo systemctl enable syncserver sudo systemctl start syncserver
Configure Firefox to use your syncserver
Having installed your own syncserver service, you can now configure Firefox on your device(s) to use it.
If you were already using SyncServer-1.5 and want to migrate from your old server to the new one, the answer is don't bother, just switch to the new server. Each browser retains all the data it would sync, and will sync everything to the new server once connected.
Firefox for Desktop
- Enter about:config into the URL bar
- Set the preference identity.sync.tokenserver.uri to the value https://myserver.example.com/firefox-sync/1.0/sync/1.5
- You can now click on the hamburger menu and click the Sign In button at the top-right of it
Firefox for Mobile
- Go to Settings → About Firefox
- Tap on the Firefox Browser logo 5 times
- Go back and scroll up to Sync Debug, click it
- Set Custom Sync Server to https://myserver.example.com/firefox-sync/1.0/sync/1.5
- Stop and restart the Firefox app
- On a desktop computer running Firefox (signed in), go to https://firefox.com/pair and follow the steps until you're shown a QR code
- On the mobile, go to Settings → Synchronize → Ready to scan and scan the QR code on the desktop computer
- Alternatively, if you don't have a desktop computer, go to Settings → Synchronize → Use email instead
Troubleshooting
To see all the log messages produced by the server, run journalctl -u syncserver.
To follow current log messages produced by the server, run journalctl -f -u syncserver.
To see all the logs generated by your (desktop) browser when syncing, go to the URL about:sync-log in your browser.
How Mozilla could improve syncserver
I don't expect them to listen. For the £0 I paid for it, I can't necessarily expect anything. But to make syncserver less of a pain in the arse to self-host, I would suggest:
- Offer simple reverse-proxy support by adding a public_url config item that explicitly sets the public base URL (including scheme, host, port and path prefix), rather than guess from the request. This is what SyncServer-1.5 supported, and it was great. Issue #1217 has been gone three years without a fix, and is threatening to morph into "add HTTPS support"
- Support SQLite, like SyncServer-1.5 did. MySQL is overkill for a single user. Finish PR #1520
- Support configuring syncserver entirely from the config file. Currently you have to write a config file, start syncserver, stop syncserver, add more config directly to the DB, then start syncserver again. This is cumbersome. Instead, allow tokenserver to be configured directly in the config file, and not require manual DB manipulation. Issue #1648
- Document how to ensure syncserver is not reporting any metrics to third parties
- Offer an official Docker image of syncserver that runs standalone with just a config file and supports either MySQL or SQLite. Give sample invocations for both in the README
Thanks
I must thank Artemis's article on self-hosting syncserver for giving me hope that it can be achieved, and most of the details on how to achieve it. Their article is a little out of date, as syncserver no longer requires Python to work. I also found Arch Linux users sharing tips on getting the damn thing working, which were invaluable.