Self-hosting & security
Syncle is built to run on your own machine or a trusted network. This page covers the protections it ships with, what stays your job when you expose it further, which volumes hold your data, and what the common failure messages mean.
The security posture
Every API route sits behind a single admin account — created once on first run, guarded by a one-time setup token, with no signup (the quickstart walks through it). The password is hashed with scrypt, and the session is a signed httpOnly cookie named db_session that expires after one week by default; the length is configurable in-app under Settings › Security. Login and setup attempts are rate-limited. Changing the password bumps the account's session version, which instantly invalidates every outstanding cookie on every device.
What Syncle does not ship: TLS. It serves plain HTTP, and the security policy is explicit that TLS termination and network-level control over who can reach the port are the operator's job the moment anything beyond localhost can connect.
What the compose stack exposes
The Docker install publishes exactly one host port. The API, Postgres and Redis containers are reachable only on the compose network:
| Container | Host port | Why |
|---|---|---|
syncle-web | 3002 (SYNCLE_PORT changes it) | The GUI and its /api proxy — the only published port. |
syncle-api | none | The browser reaches it through the web container's /api proxy. A commented ports mapping in docker-compose.app.yml exposes 4002 directly if you need to call the HTTP API without the proxy. |
syncle-postgres | none | Syncle's own metadata store. |
syncle-redis | none | The job queue. |
Encryption and the master key
Connection passwords, SSH secrets and bridge auth secrets are encrypted at rest with AES-256-GCM under SYNCLE_MASTER_KEY, and only ever returned to the browser redacted. Session cookies are signed with an HKDF-derived sub-key of the same master key, so encryption and signing stay independent. The key must be base64 encoding exactly 32 bytes — anything else fails with SYNCLE_MASTER_KEY must be a base64-encoded 32-byte value the first time the key is needed: a login, the first-run setup, or saving a connection. The API itself still boots and answers /api/health.
The installer generates a key into ~/.syncle/.env (mode 600) and preserves it across every syncle update. If you run docker-compose.app.yml by hand without exporting one, the API generates a random key on first use and writes it to master.key inside the syncle-api-data volume, logging a warning — the key then lives right next to the data it protects, and is lost with the volume. Set it explicitly in anything you care about.
Exposing Syncle beyond localhost
Before opening the port to a wider network:
- Complete first-run setup first. Create the admin account while the port is still private, so the setup screen is never reachable from the outside.
- Terminate TLS in front. The API trusts
X-Forwarded-Prototo decide whether session cookies get the Secure attribute. If your reverse proxy does not forward that header, setSYNCLE_SECURE_COOKIES=trueto force it — the decision is not keyed offNODE_ENV. - Restrict destinations. Set
SYNCLE_BLOCK_PRIVATE_DESTINATIONS=trueso bridge deliveries refuse loopback, private and link-local addresses — see the destination guard below. - Jail SQLite paths. Set
SYNCLE_SQLITE_DIRso SQLite connections may only open files under that directory on the server. - Control network access. A firewall or VPN deciding who can reach the port at all is still the outermost layer; the single admin login is the only thing behind it.
On a Docker install, note that the stock compose file passes only a fixed set of variables to the api container — adding the hardening variables to ~/.syncle/.env does nothing on its own. Add them to the api service's environment block instead:
environment:
# ...existing entries...
SYNCLE_BLOCK_PRIVATE_DESTINATIONS: 'true'
SYNCLE_SECURE_COOKIES: 'true'The full list of environment variables, with defaults, is on the configuration page.
The outbound destination guard
Bridge deliveries to HTTP destinations never follow redirects — a public host that 302s to an internal address would otherwise bypass any pre-flight check. Cloud metadata endpoints (169.254.169.254 and its equivalents) are always refused, and hostnames are resolved before delivery, so a public DNS name pointing at an internal IP is caught the same as a literal address.
Blocking loopback, private and link-local destinations is opt-in via SYNCLE_BLOCK_PRIVATE_DESTINATIONS=true, because posting to a service on localhost is a primary local use case. The value is compared to the literal string true — 1 or TRUE leaves the guard off.
What to back up
The stack keeps its state in three named Docker volumes:
| Volume | What it holds |
|---|---|
syncle-postgres-data | The metadata store: workspaces, connections (credentials encrypted), bridges, job history and per-row delivery logs. |
syncle-api-data | The API's local state: the first-run setup token while it exists, and the auto-generated master key if SYNCLE_MASTER_KEY was never set. |
syncle-redis-data | The job queue that lets running bridge jobs survive a restart and auto-resume. |
A backup of the Postgres volume is only useful together with the master key — without it the stored credentials cannot be decrypted. Back up ~/.syncle/.env alongside the volumes, and store it separately from them if you can.
syncle down stops the containers and keeps all three volumes. syncle uninstall is the destructive one: it asks This deletes all Syncle containers, images and DATA. Continue? [y/N] and then runs compose down -v --rmi all, deleting the volumes with everything in them.
Reporting a vulnerability
Do not open a public issue for security problems. Email osmanahmadxai@gmail.com with a description of the issue and its impact, steps to reproduce, and any suggested fix. You get an acknowledgement within a few days, disclosure timing is coordinated with you, and you are credited in the release notes unless you prefer to stay anonymous.
Security fixes land on main and the latest release only, so stay on the newest release. Always in scope: credential handling, SQL or command injection through the adapters, payload-template injection, auth bypasses, and SSRF that dodges the destination guard.
Troubleshooting
Port <n> is already in use. Stop the other process or set PORT.— the API found its port taken and exited with code 1. Stop whatever holds the port, or changePORT— and keep the web app's proxy target in step with it.- Every screen shows
Cannot reach the Syncle API— the web app's proxy could not reach the API (it returns a 503 with codeNETWORK). The api container is down or still starting; checksyncle statusandsyncle logs api. Could not refresh images — using what is already downloaded.—syncle upcould not pull newer images (offline, or the registry is unreachable) and started the cached ones instead. Not an error; the next successfulsyncle uporsyncle updaterefreshes them.Still starting — check `syncle logs`.—syncle uppolls the GUI every 2 seconds for up to 60 attempts and gave up waiting. The containers usually keep starting in the background;syncle logsshows what they are doing.- Lost the setup token, or the process died mid-setup — a fresh token is minted on the next boot as long as no account exists yet, and
syncle logs apiprints it. - Logged out everywhere after a password change — deliberate. A password change invalidates all outstanding sessions and re-issues only the one that made the change.
- Bridges can be built and previewed, but jobs will not run — Redis is down. Only running a job needs Redis; connecting databases, browsing and building or previewing bridges all work without it. Check
syncle logs redis. SYNCLE_MASTER_KEY must be a base64-encoded 32-byte value— the key is not valid base64 of exactly 32 bytes. On a fresh install, generate one withopenssl rand -base64 32. If data already exists, recover the original key rather than making a new one — see above.
