Security hardening
What a self-hosted pod exposes, the defaults that protect it, and the settings to check before you put data in it.
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/deployNetwork 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:
| Service | Port on 127.0.0.1 |
|---|---|
realtime | 4001 |
postgres | 5432 |
minio | 9000, 9001 |
typesense | 8108 |
kratos | 4433, 4434 |
rsshub profile: RSSHub, browserless | 1200, 3000 |
monitoring profile: Dozzle, Prometheus, Grafana, Alertmanager | 8888, 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 enableA 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 randand writesdeploy/.envwith mode600. synap config listandsynap config getmask secrets.get --revealshows one value; avoid it on a shared screen.- Never paste
.env, or the output ofdocker compose configordocker 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 setrefuses 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 indeploy/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-tokenPrefer 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 updateA 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
| Connection | How to turn it off |
|---|---|
| Synap's marketplace catalog, read every 10 minutes | sudo 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, withsynap policy.
sudo synap policy show
sudo synap policy set-mode self-hosted # or managed| Mode | The pod agent refuses |
|---|---|
self-hosted | archive, restore-archive and configure |
managed | nothing |
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:
| Setting | Meaning |
|---|---|
POD_AGENT_ISSUER_URL | The only issuer whose signed commands it accepts. Its keys are fetched from that issuer, never from the request. |
POD_AGENT_AUDIENCE | This 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.
