You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I was wondering if it would be useful if we were able to disable user+browser registration, in the case we expose the service publicly?
The idea would be that you can set SELFSYNC_ALLOW_NEW_REG to 1 or true to allow new registrations, and 0 or false to disable new device registrations. This would be useful to prevent randoms from consuming server resources.
Once I've registered my devices to selfsync after it's enabled (default), I could disable it to prevent future user registrations.
Update: After setting it up in Kubernetes, I can see how it's working now. Google OIDC in front of the /command endpoint causes it to fail, and I suspect basic auth will suffer the same issue.
Update 2 The same is true with mTLS, where Brave sync will error out with ERR_BAD_SSL_CLIENT_AUTH_CERT even with AutoSelectCertificateForUrls policy set. It seems the HTTP client for sync has no access to installed certificates and/or the Chromium/Brave enterprise policy to autoselect.
Having the option to disable registrations would in theory still be relatively secure with the Nigori key encryption.
Thanks for the detailed writeup — the OIDC/mTLS findings are useful context.
For your scenario I'd recommend not exposing selfsync to the public internet at all. The README's suggested deployment is LAN/homelab only, with remote access via a private network overlay (Tailscale, WireGuard, headscale, or Cloudflare Zero Trust). That sidesteps the whole problem: the sync client doesn't understand OIDC/basic-auth flows, as you found, but it doesn't need to — devices join your tailnet and talk to the server as if it were on the LAN. Your phones/laptops change networks all the time, so IP filtering at a reverse proxy won't reliably cover them either.
Once it's tailnet-only, new registrations can only come from devices you admitted to the network, which achieves what the registration toggle would — and it also protects the rest of the surface (selfsync has no login screen, so anything that can reach the port can read the data, not just register).
I'll leave the SELFSYNC_ALLOW_NEW_REG idea open as a possible hardening option, but I don't plan to prioritize it — the private-network path is the supported one.
@loyalpartner I understand your intent - you don't take responsibility for data hosted publicly, and you're right that it's the most secure way to host it. If this was only for home, I would definitely do this.
My main concern is that the Chrome Sync Protocol intentionally does not support OIDC, mTLS, or Basic Auth. I assume this is to maintain separation from user-space browser elements. It seems the protocol is designed as a public service, with Nigori encryption serving as both the authentication and encryption mechanism. A similar example is Syncthing, where device keys are generated, explicitly trusted, and the service can be operated publicly, as long as the admin UI is secure, everything is fine.
Edit: To clarify, using Brave it uses Sync v2 and a 24-word BIP39 passphrase as the mandatory, client-side encryption key for all synced data. I guess this is separate from Nigori key, which I guess must be only for passwords by default. I have inspected the sqlite DB to confirm I couldn't see anything plain text before exposing publicly.
I had also previously looked at SyncServer (now SyncServer-RS) for Firefox. The intent there was that the server would be public, but the user is attested via Firefox's auth. At some point they had a similar ability to limit the number of sync chains that could be established on a self-hosted server, and I've seen this same approach in at least one Chrome Sync Protocol implementation.
In practice, putting it behind Tailscale/Headscale doesn't work for my requirements between work and home. My current solution is to obfuscate the /command and /chrome-sync endpoints behind a complex prefix and use GeoIP region blocking at the reverse proxy to limit access. It's not ideal, but it's better than nothing for now. The actual selfsync UI I have behind Google OIDC and LAN access only, which clearly works fine. I also do not sync passwords, we're only talking bookmarks, tabs, history, extensions, themes, settings.
So in terms of risk, the main issue is if someone can overwrite my store somehow potentially with a nefarious bookmark URL. I can always restore my sqlite DB from backup. The data actually leaking isn't something I'm that concerned about, my main concern is unwanted users being able to register new sync chains and consume server resources.
If I get some time, I may open a PR for this feature if you don't have time to implement it. I think it should be a relatively straightforward addition. I've not written any rust, but have worked on some issues/PR review for Vaultwarden which is also in rust, it didn't seem too difficult to understand the language syntax. Since you have a handy rust build container it should be simple to test out.
@berglh Thanks for following up — I see your point now. The sync protocol leaves no room for OIDC / mTLS / basic auth, and a private network isn't always an option, so the server itself has to be able to turn strangers away.
I'm working on it, with one change from the original proposal: approval per device instead of a global "new registrations" switch. A switch that only blocks new users doesn't stop someone who sends requests with an email that's already registered; a per-device check does.
The plan:
Every sync request carries the browser's cache GUID in the URL (?client_id=...), so the server can tell devices apart.
A toggle "Require approval for new devices". When it's on, unknown devices are held as pending and get TRANSIENT_ERROR, so the browser keeps retrying and resumes on its own once approved, no restart needed.
While the toggle is off, devices are recorded and auto-approved, so turning it on later won't lock out the ones you already use.
The admin page moves to its own listener (SELFSYNC_ADMIN_ADDR, default 127.0.0.1:8081), so the approve buttons aren't reachable from the sync port. You can keep it behind your OIDC proxy as you do now.
@loyalpartner Thanks, this is a well-thought-out solution for the protocol and the flow. Using the client ID and approving devices manually in the UI is very useful for a restricted setup.
I have a few concerns about the behaviour:
Pending devices can still be created in bulk. With "Require approval for new devices" on, each unknown device request seems to add a row to the devices table. This stops the server from creating sync chains for unapproved devices, but it still lets someone create many pending devices. If approval is later switched off, those devices are approved on their next request, which creates sync chains that can only be removed with the sqlite CLI. Is there a limit on pending rows, or any pruning? It looks like client_id and the email also have no length limit.
Pending devices are auto-approved when approval is switched off. I'm not sure that's desirable. It might be safer to discard pending requests when approval is turned off, or at least to offer that choice. I don't know whether that fits your design.
A full lockdown mode would help. My workflow is to leave manual approval off while I register all my devices, then switch approval on so that nothing new can get in. From the code, I understand that known devices keep syncing and unknown ones are refused. But unknown requests are still recorded as pending, so they still write to the DB. Could there be a mode that rejects unknown devices without recording them? That would remove the pending-row abuse in point 1 for setups that don't need to approve devices later.
@berglh Thanks, all three points were valid. I've opened #16 to address them. I went with a slightly different approach than a separate lockdown mode:
Pending rows in bulk: there's now a device limit on the admin page (default 20). Once approved + pending devices reach it, unknown devices are refused and not recorded. Blocked devices don't count toward it, so blocking junk frees slots. client_id must be ≤ 64 base64 chars and the email ≤ 254 chars; anything else is refused without a write. There's also a "Clear all" button for pending devices, and an "Unblock" button that deletes a blocked row.
Pending auto-approved when approval is switched off: no longer happens. Pending devices are refused outright on every request (no DB write) and stay pending until you approve, block, or clear them, whatever the switch says.
Lockdown: set the limit to the number of devices you have. Unknown devices are then refused without being recorded, which is the lockdown behaviour you described. To add a device later, raise the limit by one, approve it, and lower it again.
"Rejected" is now called Blocklist. Blocked devices are always refused without a write, and you can move them back with Approve or Unblock.
Feedback on #16 is welcome if you get a chance to try it.
Activity
loyalpartner commentedon Oct 12, 2026
Thanks for the detailed writeup — the OIDC/mTLS findings are useful context.
For your scenario I'd recommend not exposing selfsync to the public internet at all. The README's suggested deployment is LAN/homelab only, with remote access via a private network overlay (Tailscale, WireGuard, headscale, or Cloudflare Zero Trust). That sidesteps the whole problem: the sync client doesn't understand OIDC/basic-auth flows, as you found, but it doesn't need to — devices join your tailnet and talk to the server as if it were on the LAN. Your phones/laptops change networks all the time, so IP filtering at a reverse proxy won't reliably cover them either.
Once it's tailnet-only, new registrations can only come from devices you admitted to the network, which achieves what the registration toggle would — and it also protects the rest of the surface (selfsync has no login screen, so anything that can reach the port can read the data, not just register).
I'll leave the
SELFSYNC_ALLOW_NEW_REGidea open as a possible hardening option, but I don't plan to prioritize it — the private-network path is the supported one.berglh commentedon Oct 13, 2026
@loyalpartner I understand your intent - you don't take responsibility for data hosted publicly, and you're right that it's the most secure way to host it. If this was only for home, I would definitely do this.
My main concern is that the Chrome Sync Protocol intentionally does not support OIDC, mTLS, or Basic Auth. I assume this is to maintain separation from user-space browser elements. It seems the protocol is designed as a public service, with Nigori encryption serving as both the authentication and encryption mechanism. A similar example is Syncthing, where device keys are generated, explicitly trusted, and the service can be operated publicly, as long as the admin UI is secure, everything is fine.
Edit: To clarify, using Brave it uses Sync v2 and a 24-word BIP39 passphrase as the mandatory, client-side encryption key for all synced data. I guess this is separate from Nigori key, which I guess must be only for passwords by default. I have inspected the sqlite DB to confirm I couldn't see anything plain text before exposing publicly.
I had also previously looked at SyncServer (now SyncServer-RS) for Firefox. The intent there was that the server would be public, but the user is attested via Firefox's auth. At some point they had a similar ability to limit the number of sync chains that could be established on a self-hosted server, and I've seen this same approach in at least one Chrome Sync Protocol implementation.
In practice, putting it behind Tailscale/Headscale doesn't work for my requirements between work and home. My current solution is to obfuscate the /command and /chrome-sync endpoints behind a complex prefix and use GeoIP region blocking at the reverse proxy to limit access. It's not ideal, but it's better than nothing for now. The actual
selfsyncUI I have behind Google OIDC and LAN access only, which clearly works fine. I also do not sync passwords, we're only talking bookmarks, tabs, history, extensions, themes, settings.So in terms of risk, the main issue is if someone can overwrite my store somehow potentially with a nefarious bookmark URL. I can always restore my sqlite DB from backup. The data actually leaking isn't something I'm that concerned about, my main concern is unwanted users being able to register new sync chains and consume server resources.
If I get some time, I may open a PR for this feature if you don't have time to implement it. I think it should be a relatively straightforward addition. I've not written any rust, but have worked on some issues/PR review for Vaultwarden which is also in rust, it didn't seem too difficult to understand the language syntax. Since you have a handy rust build container it should be simple to test out.
loyalpartner commentedon Oct 15, 2026
@berglh Thanks for following up — I see your point now. The sync protocol leaves no room for OIDC / mTLS / basic auth, and a private network isn't always an option, so the server itself has to be able to turn strangers away.
I'm working on it, with one change from the original proposal: approval per device instead of a global "new registrations" switch. A switch that only blocks new users doesn't stop someone who sends requests with an email that's already registered; a per-device check does.
The plan:
?client_id=...), so the server can tell devices apart.TRANSIENT_ERROR, so the browser keeps retrying and resumes on its own once approved, no restart needed.SELFSYNC_ADMIN_ADDR, default127.0.0.1:8081), so the approve buttons aren't reachable from the sync port. You can keep it behind your OIDC proxy as you do now.I'll post here when there's a PR to try.
berglh commentedon Oct 15, 2026
@loyalpartner Thanks, this is a well-thought-out solution for the protocol and the flow. Using the client ID and approving devices manually in the UI is very useful for a restricted setup.
I have a few concerns about the behaviour:
Pending devices can still be created in bulk. With "Require approval for new devices" on, each unknown device request seems to add a row to the devices table. This stops the server from creating sync chains for unapproved devices, but it still lets someone create many pending devices. If approval is later switched off, those devices are approved on their next request, which creates sync chains that can only be removed with the sqlite CLI. Is there a limit on pending rows, or any pruning? It looks like client_id and the email also have no length limit.
Pending devices are auto-approved when approval is switched off. I'm not sure that's desirable. It might be safer to discard pending requests when approval is turned off, or at least to offer that choice. I don't know whether that fits your design.
A full lockdown mode would help. My workflow is to leave manual approval off while I register all my devices, then switch approval on so that nothing new can get in. From the code, I understand that known devices keep syncing and unknown ones are refused. But unknown requests are still recorded as pending, so they still write to the DB. Could there be a mode that rejects unknown devices without recording them? That would remove the pending-row abuse in point 1 for setups that don't need to approve devices later.
loyalpartner commentedon Oct 16, 2026
@berglh Thanks, all three points were valid. I've opened #16 to address them. I went with a slightly different approach than a separate lockdown mode:
Pending rows in bulk: there's now a device limit on the admin page (default 20). Once approved + pending devices reach it, unknown devices are refused and not recorded. Blocked devices don't count toward it, so blocking junk frees slots.
client_idmust be ≤ 64 base64 chars and the email ≤ 254 chars; anything else is refused without a write. There's also a "Clear all" button for pending devices, and an "Unblock" button that deletes a blocked row.Pending auto-approved when approval is switched off: no longer happens. Pending devices are refused outright on every request (no DB write) and stay pending until you approve, block, or clear them, whatever the switch says.
Lockdown: set the limit to the number of devices you have. Unknown devices are then refused without being recorded, which is the lockdown behaviour you described. To add a device later, raise the limit by one, approve it, and lower it again.
"Rejected" is now called Blocklist. Blocked devices are always refused without a write, and you can move them back with Approve or Unblock.
Feedback on #16 is welcome if you get a chance to try it.