This repository provisions a new Hetzner server and configures it end-to-end for a secure Synapse deployment.
make bootstrap performs:
- Terraform provisioning (server + network firewall)
- Ansible hardening (SSH policy, UFW, fail2ban, unattended upgrades)
- Docker Compose deployment (Caddy + Synapse + Postgres + Redis)
- One-time Matrix admin creation
It also prepares compatibility with restic-kit by cloning it on the server and wiring its services discovery path.
infra/terraform: Hetzner infrastructureansible/playbooks/site.yml: host configuration pipelineansible/roles/*: hardening + runtime + Matrix deploy rolesscripts/bootstrap.sh: orchestration behindmake bootstrap
- Matrix stack root:
/home/<SSH_ADMIN_USER>/services/matrix - restic-kit clone:
/home/<SSH_ADMIN_USER>/restic-kit - restic-kit services link:
/home/<SSH_ADMIN_USER>/restic-kit/services -> /home/<SSH_ADMIN_USER>/services
Install locally:
maketerraform(>= 1.5)ansiblejqcurl
You also need:
- A Hetzner API token
- A domain pointed to the created server IP (for ACME TLS)
- SSH key pair available locally
Important DNS timing:
- For first deployment, get the server IP from Terraform first, then point
MATRIX_DOMAINto that IP, wait for DNS propagation, and only then run Ansible deploy. - If DNS is not ready when Caddy starts, ACME certificate issuance can fail.
- Copy and configure env file:
cp .env.example .env-
Edit
.envwith your token, domain, SSH key paths, andSSH_ADMIN_USER.- Optional restic source controls:
RESTIC_KIT_REPO_URL,RESTIC_KIT_REPO_VERSION.
- Optional restic source controls:
-
Provision infrastructure first:
make tf-apply- Point
MATRIX_DOMAINDNS (Arecord) to the created server IP and wait until:
dig +short A <your-matrix-domain>returns that IP.
- If your SSH key has a passphrase, load it into
ssh-agentfirst:
eval "$(ssh-agent -s)"
ssh-add <path-to-private-key>- Deploy hardening + Matrix stack:
make ansible-deployDuring deployment SSH login user is auto-detected:
- First run usually connects as
root. - After hardening creates
SSH_ADMIN_USERand disables root SSH, reruns useSSH_ADMIN_USER.
Optional one-shot flow:
make bootstrapis fine ifMATRIX_DOMAINalready resolves to the target server IP when Caddy starts.
make tf-plan # Terraform plan only
make tf-import-ssh-key # Import existing matching SSH key into Terraform state
make tf-apply # Terraform apply only
make ansible-deploy # Re-run hardening/deploy on existing host
make tf-destroy # Destroy Hetzner resources (keeps protected SSH key)- Inbound firewall:
22(restricted bySSH_ALLOWED_CIDR),80,443, optional8448 - SSH password auth disabled
- Root SSH login disabled (
PermitRootLogin no) - UFW + fail2ban enabled
- Unattended security upgrades enabled
- Synapse backend components are private to Docker network
- Synapse API exposed to host-only on
127.0.0.1:8008and proxied by Caddy
scripts/bootstrap.sh now refreshes the local ~/.ssh/known_hosts entry for the Terraform server IP by default (REFRESH_LOCAL_KNOWN_HOSTS=true).
This avoids stale-host-key errors after server recreation.
If you need to fix it manually:
ssh-keygen -R <server-ip>After deployment:
ssh root@<server-ip>should fail, and:
ssh <SSH_ADMIN_USER>@<server-ip>should succeed with the configured private key.
Admin user is created automatically once.
- Username: value of
MATRIX_ADMIN_USER - Password file on server:
/home/<SSH_ADMIN_USER>/services/matrix/secrets/admin_password
If MATRIX_ADMIN_PASSWORD is empty, a strong random password is generated.