SynapSynapDocs

Security hardening

What a self-hosted pod exposes, the defaults that protect it, and the settings to check before you put data in it.

For Self-hostersOpen-source podReviewed 2026-10-20

A pod installed with install.sh starts from safe defaults. This page lists them, so you can check them on your own server, and the few things only you can do. Run the commands from the deploy directory:

cd /opt/synap/deploy

Network exposure

Only Caddy publishes ports on the server's public interfaces: 80 and 443 over TCP, and 443 over UDP for HTTP/3. Everything else that publishes a port binds it to 127.0.0.1:

ServicePort on 127.0.0.1
realtime4001
postgres5432
minio9000, 9001
typesense8108
kratos4433, 4434
rsshub profile: RSSHub, browserless1200, 3000
monitoring profile: Dozzle, Prometheus, Grafana, Alertmanager8888, 9090, 3333, 9093

The backend publishes no port: it is reached only through the edge proxy (Caddy, unless you installed with --edge traefik). To reach a loopback-only tool from your laptop, use an SSH tunnel (ssh -L 3333:127.0.0.1:3333 root@<server> for Grafana) instead of opening its port.

Use a host firewall that allows only what the pod needs, plus your SSH port. With ufw:

ufw allow 22/tcp      # or your SSH port
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 443/udp
ufw enable

A pod exposed through a tunnel does not need 80 and 443 open in the firewall: the tunnel connects from the inside. See Domains and TLS.

Secrets

  • The installer generates every secret with openssl rand and writes deploy/.env with mode 600.
  • synap config list and synap config get mask secrets. get --reveal shows one value; avoid it on a shared screen.
  • Never paste .env, or the output of docker compose config or docker inspect, into an issue or a chat: they contain the secrets in clear text.
  • Secrets that existing data depends on (POSTGRES_PASSWORD, JWT_SECRET, the Kratos, MinIO and Typesense keys, SYNAP_SERVICE_ENCRYPTION_KEY, VAULT_SERVER_KEY, PROVISIONING_TOKEN) are marked immutable: synap config set refuses to change them without --force. Do not force a change unless you know what reads the old value.
  • Rotation procedures for the Ory secrets and for the field-encryption keyring (SYNAP_FIELD_KEYS) are in deploy/runbooks/ of the backend repository. Try them on a test pod first.

The first admin and the bootstrap token

PROVISIONING_TOKEN exists to bootstrap the pod. Once the pod has an owner, remove it:

sudo synap setup admin --status          # ready = the pod has an admin
sudo synap setup admin --retire-token

Prefer sudo synap setup admin --email you@example.com --magic-link over --password on the command line, so the password never lands in your shell history. See Install.

Releases you install

Install cosign and refuse unsigned or tampered releases, for install.sh and for every synap update. sudo drops variables exported in your shell, so set the variable on the sudo command itself:

curl -fsSL https://github.com/synap-core/backend/releases/download/channel-stable/install.sh \
  | sudo SYNAP_RELEASE_VERIFY=enforce bash -s -- --domain pod.example.com --email you@example.com
sudo SYNAP_RELEASE_VERIFY=enforce synap update

A manual synap update also reads SYNAP_RELEASE_VERIFY from deploy/.env. Automatic updates always run with enforce. See Verifying releases.

Backups

Give the pod append-only credentials for its off-host repository, so a compromised server cannot delete its own snapshots, and keep the recovery kit off the server. See Backups and restore.

Outbound connections you can turn off

ConnectionHow to turn it off
Synap's marketplace catalog, read every 10 minutessudo synap config set SYNAP_CATALOG_SOURCE=off
A Control Plane (sign-in with a Synap account, registration, brokered connectors)Leave CONTROL_PLANE_URL empty. A pod without it trusts no Control Plane.

What a Control Plane may do on your pod

A pod trusts no outside service by default. When you connect it to a Control Plane (see Connect to the Control Plane), two separate things decide what that service may do:

  • The trusted-issuer registry, in the pod's database. The Control Plane is approved with a fixed set of four scopes: signing in people whose accounts are already linked, linking accounts, granting memberships, and configuring data sources (for example, delivering the credential for brokered connectors) for linked users. If it asks for more, the request is filed as a proposal in the owner's review queue and nothing more is granted until a pod admin approves it.
  • The command policy, deploy/state/pod-agent-policy.json, for commands sent to the optional pod agent. Only the owner writes it, with synap policy.
sudo synap policy show
sudo synap policy set-mode self-hosted    # or managed
ModeThe pod agent refuses
self-hostedarchive, restore-archive and configure
managednothing

With no policy file the pod behaves as self-hosted, and a file it cannot read also falls back to the self-hosted refusals. Registration writes the file once, from the Control Plane's answer, and never overwrites an existing file. synap policy add-recipient <age1…> and remove-recipient set who can decrypt an archive of the pod.

The pod agent

The pod agent is the service that carries out signed Control Plane commands (for example an update or an archive). It runs only when you turn it on (SYNAP_WITH_POD_AGENT=1, or --with-pod-agent at install) or set its trust settings, and it then needs both settings below, each a canonical https:// URL with no trailing slash:

SettingMeaning
POD_AGENT_ISSUER_URLThe only issuer whose signed commands it accepts. Its keys are fetched from that issuer, never from the request.
POD_AGENT_AUDIENCEThis pod's public URL; a command must be addressed to it.

The agent publishes no port on the host. Caddy routes /api/pod-agent/* to it, and it records each command's ID so a command cannot be replayed, even after a restart. A self-hosted pod that does not use the Control Plane's update or archive features does not need it.

On this page