Syncle

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:

ContainerHost portWhy
syncle-web3002 (SYNCLE_PORT changes it)The GUI and its /api proxy — the only published port.
syncle-apinoneThe 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-postgresnoneSyncle's own metadata store.
syncle-redisnoneThe 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-Proto to decide whether session cookies get the Secure attribute. If your reverse proxy does not forward that header, set SYNCLE_SECURE_COOKIES=true to force it — the decision is not keyed off NODE_ENV.
  • Restrict destinations. Set SYNCLE_BLOCK_PRIVATE_DESTINATIONS=true so bridge deliveries refuse loopback, private and link-local addresses — see the destination guard below.
  • Jail SQLite paths. Set SYNCLE_SQLITE_DIR so 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:

~/.syncle/docker-compose.app.yml (api service)
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 true1 or TRUE leaves the guard off.

What to back up

The stack keeps its state in three named Docker volumes:

VolumeWhat it holds
syncle-postgres-dataThe metadata store: workspaces, connections (credentials encrypted), bridges, job history and per-row delivery logs.
syncle-api-dataThe 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-dataThe 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 change PORT — 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 code NETWORK). The api container is down or still starting; check syncle status and syncle logs api.
  • Could not refresh images — using what is already downloaded. syncle up could not pull newer images (offline, or the registry is unreachable) and started the cached ones instead. Not an error; the next successful syncle up or syncle update refreshes them.
  • Still starting — check `syncle logs`. syncle up polls the GUI every 2 seconds for up to 60 attempts and gave up waiting. The containers usually keep starting in the background; syncle logs shows 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 api prints 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 with openssl rand -base64 32. If data already exists, recover the original key rather than making a new one — see above.