Sync-in 2.6.1 on AWS User Guide
Overview
This guide covers launching and using the Sync-in 2.6.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg file sharing and sync listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Sync-in is an open source (AGPL-3.0) server for storing, sharing and synchronising files: personal and shared spaces, share links for people without an account, groups and guests, full-text search (with OCR for scanned documents), WebDAV access and the Sync-in desktop sync clients. This AMI runs the Sync-in 2.6.1 release natively under systemd (no containers) with MariaDB 11.8 and nginx, and adds the security work below.
- HTTPS from the first boot. nginx serves Sync-in on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS, the instance's other addresses redirect to its configured name, and a request for a host name the instance does not serve is dropped.
- No default account. First boot creates one administrator,
admin, with a password generated on your instance, and also generates the database password, the encryption key and the token secrets. Nothing secret is baked into the image. - The public address comes from configuration. The address Sync-in puts in its links is set from EC2 instance metadata at first boot (or by you, with one command), never taken from a request.
- Uploads are checked before they are accepted. nginx asks Sync-in whether the request is signed in before it reads a single byte of an upload (web app, desktop clients and WebDAV), caps each upload at 2 GiB and rate limits uploads per client. Sign-in attempts are limited too.
- Your files cannot run code in your browser. The web app runs under a strict Content-Security-Policy, and every file Sync-in serves gets a sandbox policy, so an uploaded HTML or SVG file cannot act as your account.
- PDF previews never run a PDF's JavaScript. PDFs open and render in Sync-in's built-in viewer, but the viewer's scripting engine is not served, so JavaScript embedded in a PDF (dialogs, form scripts, actions) never runs. This is a deliberate security feature of this image.
- Nothing calls home. Sync-in's daily update check, the desktop-client download list and the OCR model download are kept off the network: English OCR ships on the image. Online editors, mail, LDAP and single sign-on are off until you configure them.
- No service can reach the instance metadata service, and the AMI requires IMDSv2.
- Your data on its own volume. Every file and the MariaDB database live on a dedicated 30 GiB EBS volume mounted at
/var/lib/sync-in, which you can resize and snapshot on its own. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on, including MariaDB's own 11.8 security releases.
What is on the image: Sync-in 2.6.1 (the @sync-in/server release published by the Sync-in project from tag v2.6.1, with its complete source in /usr/share/doc/sync-in), Node.js 24.22.2, MariaDB 11.8.9 from MariaDB's own repository, nginx 1.24 from Ubuntu, the English OCR model, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large (larger for many users or heavy search indexing)
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH), TCP 443 (Sync-in, WebDAV and the desktop clients) and TCP 80 (redirects to HTTPS) from the networks your users and devices use. Nothing else needs to be open.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Sync-in 2.6.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger), your key pair, a security group as above, and a subnet. Under Configure storage you will see two volumes: the root volume and the 30 GiB data volume for your files; raise the data volume's size now if you already know you need more.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=sync-in}]'
Desktop clients and WebDAV connect to the instance by its name. For an address that never changes, attach an Elastic IP (and ideally your own DNS name, Step 10) before you set up clients.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Sync-in 2.6.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
The login message shows the web address and where the credentials are.
Step 3: Check the services
First boot creates the database, generates every password and key, creates the administrator and runs a self-test. It takes under a minute:
systemctl is-active mariadb sync-in nginx
active
active
active
Only SSH (22) and nginx (80, 443) listen on the network. Sync-in (8080) and MariaDB (3306) listen on the instance itself only; the two port 53 entries are Ubuntu's local DNS resolver:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/users:\(\("([^"]+)".*/\1/' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.1:3306 mariadbd
127.0.0.1:8080 MainThread
127.0.0.53%lo:53 systemd-resolve
127.0.0.54:53 systemd-resolve
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
If nginx is not active, first boot has not finished, or it failed and left the site down rather than open. Check systemctl status sync-in-firstboot and journalctl -u sync-in-firstboot.
Step 4: Your address and the administrator password
The web address, the administrator login, its password and the certificate fingerprint are in a file only root can read:
sudo cat /root/sync-in-credentials.txt
# Sync-in 2.6.1 - per-instance credentials, generated on first boot (cloudimg). Keep this file secret (0600 root).
# Sign in at SYNCIN_URL as SYNCIN_ADMIN_LOGIN. Change the password: sudo sync-in-set-admin-password
# Create the other accounts under Administration > Users. WebDAV: https://<host>/webdav/ with the same login.
# Certificate: self-signed for this instance; compare SYNCIN_TLS_SHA256 with what your browser shows, or replace it with
# a CA-issued certificate (see the guide).
SYNCIN_URL=https://<public-dns-name>/
SYNCIN_ADMIN_LOGIN=admin
SYNCIN_ADMIN_PASSWORD=<SYNCIN_ADMIN_PASSWORD>
SYNCIN_TLS_SHA256=A8:33:AD:67:92:5C:E7:DE:20:C3:42:D8:39:E3:72:DC:51:70:53:8F:3C:DA:3A:76:3C:AB:0D:D6:7A:7F:37:61
Your instance shows its own public DNS name, its own password and its own fingerprint. Compare the fingerprint with what your browser shows for the site, or with the served certificate:
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -fingerprint -sha256
sudo openssl x509 -in /etc/ssl/sync-in/sync-in.crt -noout -subject -ext basicConstraints,extendedKeyUsage
sha256 Fingerprint=A8:33:AD:67:92:5C:E7:DE:20:C3:42:D8:39:E3:72:DC:51:70:53:8F:3C:DA:3A:76:3C:AB:0D:D6:7A:7F:37:61
subject=CN = <public-dns-name>, O = cloudimg
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Server Authentication
The certificate is self-signed for this instance only (a server certificate, not a certificate authority). Your browser warns about it until you install a certificate from a public authority (Step 11); before you continue past the warning, check that the fingerprint matches.
Step 5: Sign in
Open https://<public-dns-name>/ (the SYNCIN_URL from Step 4) and sign in as admin with the password from the file.

Your personal space opens. Upload (or drag files onto the page) adds files, New creates folders and documents, and the left bar opens recent files, favourites, collaborative spaces, shares and the trash:

The built-in self-test signs in as admin, uploads a file through the web API and through WebDAV, reads both back, deletes them (trash included) and prints one line. First boot runs it, and you can run it any time:
sudo sync-in-verify.sh
SYNCIN_ROUNDTRIP_OK host=<public-dns-name> via=<private-ip>
Step 6: Change the administrator password
Run the helper and type the new password twice (16 to 128 characters). It never appears on a command line and is written to the credentials file:
sudo sync-in-set-admin-password
To set it from a script or a password manager, pipe it on standard input instead, for example openssl rand -base64 18 | sudo sync-in-set-admin-password. On the instance used for this guide:
sync-in-set-admin-password: the admin password was changed (and saved in /root/sync-in-credentials.txt)
Sessions that are already signed in keep working until their access token expires (15 minutes) and their refresh token (4 hours). To sign every session out at once, run sudo systemctl restart sync-in. Every user, admin included, can also change their own password in the web app under Account > Password.
Step 7: Users, two-factor authentication and sign-in limits
Create an account for each person under Administration > Users > New (the gear icon). Guest accounts are on the page's Guests tab; groups and collaborative spaces are under Administration > Groups and Administration > Spaces. Only admin exists on a new instance:

Two-factor authentication (TOTP, for any authenticator app) is available to every user and is opt-in: under Account > Security > Two-Factor Authentication choose Enable and scan the QR code. The QR code comes from your own instance as an inline image; nothing is fetched from another site. Once two-factor authentication is on, WebDAV and other password-only clients need an Application password from the same card instead of the account password:

Sync-in limits sign-in attempts from one address: after six failed attempts within a minute it answers 429 Too Many Requests until the minute has passed. From the instance itself, with an account that does not exist:
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
for i in 1 2 3 4 5 6 7; do
curl -s -o /dev/null -w '%{http_code} ' --resolve "$H:443:127.0.0.1" --cacert /etc/ssl/sync-in/sync-in.crt \
-H 'Content-Type: application/json' -d '{"login":"no-such-user","password":"not-a-real-password"}' "https://$H/api/auth/login"
done; echo
401 401 401 401 401 401 429
The sign-in page offers no self-registration: accounts are created by an administrator.
Step 8: Previews and share links
Click a file to preview it. Images, PDFs, text, audio and video open in the preview; HTML and SVG files are always downloaded, never shown.
PDF previews never run a PDF's JavaScript. PDFs open in Sync-in's built-in PDF viewer and render normally, with search, zoom and printing:

A PDF can also carry JavaScript, which the viewer would otherwise run in a scripting sandbox: it can open dialogs (for example a fake password prompt) for whoever previews the file. On this image nginx answers the viewer's scripting engine with 404 Not Found, so a PDF's JavaScript never runs, while the viewer itself loads normally. This is a deliberate security feature of the image: forms still display, but their scripts (calculations, validation) and PDF actions do nothing in the preview. Download the file and open it in a desktop PDF reader if you trust it and need them. Checked from the instance:
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
for p in /assets/pdfjs/build/pdf.sandbox.mjs /assets/pdfjs/web/wasm/quickjs-eval.js /assets/pdfjs/web/wasm/quickjs-eval.wasm \
/assets/pdfjs/build/pdf.mjs /assets/pdfjs/web/viewer.mjs; do
echo "$(curl -s -o /dev/null -w '%{http_code}' --resolve "$H:443:127.0.0.1" --cacert /etc/ssl/sync-in/sync-in.crt "https://$H$p") $p"
done
404 /assets/pdfjs/build/pdf.sandbox.mjs
404 /assets/pdfjs/web/wasm/quickjs-eval.js
404 /assets/pdfjs/web/wasm/quickjs-eval.wasm
200 /assets/pdfjs/build/pdf.mjs
200 /assets/pdfjs/web/viewer.mjs
The first three are the scripting engine (the list is also in /usr/share/doc/cloudimg/sync-in-pdfjs-sandbox-404.txt); the last two are the viewer, which loads.

Share links. Select a file or folder and choose Share > New share link to get a link for someone without an account; you can add a password, an expiry date and an access limit. The recipient opens the link page, previews the file and downloads it:

Always send the link the dialog gives you (Copy link, https://<host>/#/link/<id>). The download addresses that page uses behind the scenes (/api/app/link/...) only answer requests made by Sync-in's own pages, so if someone pastes one of those raw addresses into the browser's address bar it is refused with 403. That stops another web site from triggering a link download or access in a recipient's browser. From the instance, the same request as a pasted address and as one from Sync-in's own page (which then reaches Sync-in and is rejected only because the link does not exist):
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
for site in none same-origin; do
echo "Sec-Fetch-Site: $site -> $(curl -s -o /dev/null -w '%{http_code}' --resolve "$H:443:127.0.0.1" --cacert /etc/ssl/sync-in/sync-in.crt \
-H "Sec-Fetch-Site: $site" "https://$H/api/app/link/download/00000000-0000-0000-0000-000000000000")"
done
Sec-Fetch-Site: none -> 403
Sec-Fetch-Site: same-origin -> 400
Manage your links under Shared > Via links, and remove a link there when it is no longer needed.
Step 9: Desktop sync clients and WebDAV
Desktop sync client. Install the Sync-in desktop app from the Sync-in project (https://sync-in.com) on your computer, connect it to your address https://<host>/ and sign in with your Sync-in account. Each connected client appears under Account > Clients, where you can remove it. This image does not fetch the desktop-client download list from the Sync-in project, so Account > Applications stays empty; download the app directly. Desktop clients check the server's certificate, so install a certificate from a public authority (Step 11) before you connect them.
WebDAV. Every user's files are available at https://<host>/webdav/ with the same login (or an application password, which is required once two-factor authentication is on). The folders are personal, shares, spaces and trash. With rclone on your computer, for example:
rclone config create syncin webdav url=https://<host>/webdav vendor=other user=admin pass=<application-password> --obscure
rclone lsd syncin:
rclone mkdir syncin:personal/Reports
rclone copy ./Reports syncin:personal/Reports
rclone ls syncin:personal/Reports
rclone check ./Reports syncin:personal/Reports
A real run against the instance used for this guide (rclone 1.75.2, an application password created for it, and rclone pointed at that instance's certificate with --ca-cert after its fingerprint was compared, because no public certificate was installed):
$ rclone lsd syncin:
-1 2026-10-31 10:53:38 -1 personal
-1 2026-10-31 10:53:38 -1 shares
-1 2026-10-31 10:53:38 -1 spaces
-1 2026-10-31 10:53:38 -1 trash
$ rclone mkdir syncin:personal/Reports
$ rclone copy ./Reports syncin:personal/Reports
$ rclone ls syncin:personal/Reports
25 budget-2026.csv
43 q3-summary.txt
$ rclone check ./Reports syncin:personal/Reports
2026/10/31 11:11:13 ERROR : No common hash found - not using a hash for checks
2026/10/31 11:11:13 NOTICE: webdav root 'personal/Reports': 0 differences found
2026/10/31 11:11:13 NOTICE: webdav root 'personal/Reports': 2 hashes could not be checked
2026/10/31 11:11:13 NOTICE: webdav root 'personal/Reports': 2 matching files
$ rclone purge syncin:personal/Reports
Sync-in's WebDAV does not publish file hashes, so rclone check compares sizes (the ERROR line is rclone saying so). With a wrong password, or after the application password was deleted, the server answers 401 Unauthorized. Files removed over WebDAV go to the user's trash.
File names. nginx refuses a WebDAV address that contains an encoded backslash (%5C) or an encoded dot (%2E), with 400 Bad Request, so a request cannot step outside the folder it names. In practice this means a file whose name contains a backslash cannot be uploaded or reached over WebDAV (rename it first; Windows does not allow the character anyway), and a client must send dots in names unencoded, which every common WebDAV client does. Without credentials, the same request with an ordinary name gets 401:
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
for n in 'report%5Cq3.txt' 'report%2Etxt' 'report.txt'; do
echo "$(curl -s -o /dev/null -w '%{http_code}' -X PROPFIND --resolve "$H:443:127.0.0.1" --cacert /etc/ssl/sync-in/sync-in.crt "https://$H/webdav/personal/$n") $n"
done
400 report%5Cq3.txt
400 report%2Etxt
401 report.txt
Step 10: Host name and public address
Sync-in is served under one name, and every link it creates uses that name. On first boot it is the instance's public DNS name from EC2 instance metadata (IMDSv2); without one, the public IP address. Each boot, sync-in-set-host.service runs sync-in-set-hostname --boot, which reads the name you saved or else instance metadata again, and changes nothing when the names are the same:
sudo sync-in-set-hostname --boot
sed -n 's/^server_name //p' /etc/nginx/sync-in-host.conf
sudo grep '^ publicUrl:' /etc/sync-in/environment/environment.yaml
sync-in-set-hostname: unchanged (<public-dns-name>)
<public-dns-name>;
publicUrl: https://<public-dns-name>
The instance's IP addresses redirect to that name (301); any other host name is dropped without an answer (curl reports 000, exit code 92):
P=$(hostname -I | awk '{print $1}')
echo "private IP, https -> $(curl -sk -o /dev/null -w '%{http_code}' "https://$P/")"
echo "private IP, http -> $(curl -s -o /dev/null -w '%{http_code}' "http://$P/")"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}' -H 'Host: unknown.example.com' https://127.0.0.1/; echo " (curl exit code $?)"
private IP, https -> 301
private IP, http -> 301
unknown host -> 000 (curl exit code 92)
Use your own name. Point a DNS record at the instance (with an Elastic IP), then run the helper with your name. It updates nginx, the cross-site guard and Sync-in's public address, restarts Sync-in, issues a new self-signed certificate for the name (only if the certificate is still one cloudimg's helper made, see Step 11) and keeps the name for every later boot:
sudo sync-in-set-hostname <your-domain>
On the instance used for this guide (with --no-persist, which applies the name without saving it, and then back to the instance's own name with --boot):
$ sudo sync-in-set-hostname files.example.com --no-persist
sync-in-set-hostname: Sync-in is served at https://files.example.com/ (certificate SHA-256 7A:F5:34:74:EC:C9:52:41:E9:57:07:78:19:7A:16:7F:26:22:8C:85:8E:61:D2:30:ED:9D:F2:56:A2:F7:CA:0D)
# HTTPS status code for each name (curl):
files.example.com -> 200; public DNS name -> 000; private ip -> 301
$ sudo sync-in-set-hostname --boot
sync-in-set-hostname: Sync-in is served at https://<public-dns-name>/ (certificate SHA-256 A8:33:AD:67:92:5C:E7:DE:20:C3:42:D8:39:E3:72:DC:51:70:53:8F:3C:DA:3A:76:3C:AB:0D:D6:7A:7F:37:61)
After a name change the old public DNS name is no longer served (only the instance's IP addresses redirect), so desktop and WebDAV clients need the new address. Without your own name, a stop and start that gives the instance a new public address also gives Sync-in a new name on the next boot (the boot unit re-reads instance metadata); attach an Elastic IP to avoid that. To return to automatic naming after saving a name, delete /etc/cloudimg/sync-in-hostname and run sudo sync-in-set-hostname --boot.
Step 11: The TLS certificate: install your own, and go back
nginx serves /etc/ssl/sync-in/sync-in.crt (the full chain) and /etc/ssl/sync-in/sync-in.key. cloudimg's first-boot certificate is the only one the image ever replaces: when the helper issues a certificate it records that certificate's SHA-256 fingerprint in /var/lib/cloudimg/sync-in-cert.sha256 (root only), and only a certificate whose fingerprint matches that record is re-issued, on a boot or a name change, when it no longer names the instance:
echo "record: $(cat /var/lib/cloudimg/sync-in-cert.sha256)"
echo "file: $(sudo openssl x509 -in /etc/ssl/sync-in/sync-in.crt -noout -fingerprint -sha256 | cut -d= -f2)"
systemctl cat sync-in-set-host.service | grep -E '^(ExecStart|ConditionPathExists)='
record: A8:33:AD:67:92:5C:E7:DE:20:C3:42:D8:39:E3:72:DC:51:70:53:8F:3C:DA:3A:76:3C:AB:0D:D6:7A:7F:37:61
file: A8:33:AD:67:92:5C:E7:DE:20:C3:42:D8:39:E3:72:DC:51:70:53:8F:3C:DA:3A:76:3C:AB:0D:D6:7A:7F:37:61
ConditionPathExists=/var/lib/cloudimg/sync-in-firstboot.done
ExecStart=/usr/local/sbin/sync-in-set-hostname --boot
Your own certificate is never replaced. Any other certificate in that place, whether files you copied there or symbolic links to a chain kept elsewhere (for example certbot's live directory), is yours: it is kept byte for byte on every boot and every name change. Install it as root and reload nginx:
sudo install -m 0644 -o root -g root fullchain.pem /etc/ssl/sync-in/sync-in.crt
sudo install -m 0600 -o root -g root privkey.pem /etc/ssl/sync-in/sync-in.key
sudo nginx -t && sudo systemctl reload nginx
With a buyer-style certificate installed on the instance used for this guide (symbolic links to a chain kept elsewhere), the boot path and two name changes kept it, and nginx kept serving it:
$ sudo sync-in-set-hostname --boot
sync-in-set-hostname: unchanged (<public-dns-name>)
$ sudo sync-in-set-hostname files.example.com --no-persist
sync-in-set-hostname: the certificate in /etc/ssl/sync-in is not one this helper minted: it is kept unchanged
sync-in-set-hostname: Sync-in is served at https://files.example.com/ (certificate SHA-256 04:C2:AA:E2:97:C9:CF:6F:74:CB:3E:69:F1:02:03:02:1F:C9:4A:AB:BF:05:A1:BE:A7:22:B5:4B:C9:3F:6D:FA)
$ sudo sync-in-set-hostname --boot
sync-in-set-hostname: the certificate in /etc/ssl/sync-in is not one this helper minted: it is kept unchanged
sync-in-set-hostname: Sync-in is served at https://<public-dns-name>/ (certificate SHA-256 04:C2:AA:E2:97:C9:CF:6F:74:CB:3E:69:F1:02:03:02:1F:C9:4A:AB:BF:05:A1:BE:A7:22:B5:4B:C9:3F:6D:FA)
buyer files byte-identical after boot path + 2 name changes
buyer cert still served: yes
The same rule is checked with a real reboot on a fresh instance launched from the published image before each version is released. Renew your certificate the same way (certbot's deploy hook can run systemctl reload nginx). The SYNCIN_TLS_SHA256 line in the credentials file is refreshed on the next name change; until then, read the served fingerprint with the openssl s_client command in Step 4.
Go back to a self-signed certificate. Remove both files (or links) and run the helper; it issues a new self-signed certificate for the instance, records it and reloads nginx:
sudo rm /etc/ssl/sync-in/sync-in.crt /etc/ssl/sync-in/sync-in.key
sudo sync-in-set-hostname --boot
Step 12: Storage, uploads and backups
Every file, the database and nginx's upload spool are on the data volume:
findmnt -no SOURCE,FSTYPE,TARGET /var/lib/sync-in
sudo ls /var/lib/sync-in
/dev/nvme1n1 ext4 /var/lib/sync-in
data
lost+found
mysql
nginx-body
data holds each user's files (data/users/<login>, with its trash) and the collaborative spaces, mysql is the MariaDB database and nginx-body is where uploads are spooled while they arrive, so a large upload never fills the 8 GiB root disk.
Upload size. One file can be up to 2 GiB: Sync-in's own limit and nginx's limit on the three upload routes (web app, desktop clients, WebDAV) are set to match. Other requests (JSON) are limited to 25 MiB:
sudo grep -h 'maxUploadSize' /etc/sync-in/environment/environment.yaml
grep -h 'client_max_body_size\|limit_req \|limit_conn ' /etc/nginx/snippets/sync-in-upload.conf
maxUploadSize: 2147483648
client_max_body_size 2050m;
limit_req zone=syncin_upload_req burst=60 nodelay;
limit_conn syncin_upload_conn 5;
That is 2 GiB per file plus room for the request around it, 30 upload requests a second per address (bursts of 60) and five at once. To allow bigger files, raise maxUploadSize (bytes) and both client_max_body_size values in /etc/nginx/snippets/sync-in-upload.conf and /etc/nginx/snippets/sync-in-auth-proxy.conf, then sudo systemctl restart sync-in and sudo systemctl reload nginx. Without a session, an upload is refused before nginx reads its body:
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
curl -s -o /dev/null -w 'upload without a session -> %{http_code}\n' --resolve "$H:443:127.0.0.1" --cacert /etc/ssl/sync-in/sync-in.crt \
-X POST -H 'Content-Type: application/octet-stream' --data-binary 'hello' "https://$H/api/app/spaces/operation/upload/files/personal/hello.txt"
upload without a session -> 401
Backups. The simplest complete backup is an EBS snapshot of the data volume (or AWS Backup on the instance); for a fully consistent snapshot, stop Sync-in and MariaDB first (sudo systemctl stop sync-in mariadb) and start them again afterwards. For a file-level backup, dump the database and copy the files, for example to /root/backup (MariaDB's root account is reachable by the system root user through its socket, so no password is needed):
sudo install -d -m 0700 /root/backup
sudo mariadb-dump --single-transaction --routines --triggers sync_in | gzip | sudo tee /root/backup/sync_in-$(date +%F).sql.gz > /dev/null
sudo tar -C /var/lib/sync-in -czf /root/backup/sync-in-data-$(date +%F).tar.gz data
Keep /etc/sync-in/environment/environment.yaml (root and the sync-in service only) with the backups: it holds the database password and the encryption key Sync-in needs to read its own data.
Growing the volume. Increase the data volume's size in the EC2 console (or aws ec2 modify-volume), then grow the file system online:
lsblk
sudo resize2fs "$(findmnt -no SOURCE /var/lib/sync-in)"
Step 13: Updates
Sync-in is updated by new versions of this image, which cloudimg publishes after the Sync-in project releases. To move to one, launch it alongside your instance and bring your files and database across from a backup (Step 12); cloudimg support can help with the move. Every service is enabled, so Sync-in returns by itself after a reboot or a stop and start.
Ubuntu and MariaDB update themselves. Unattended upgrades install Ubuntu's security updates, and also MariaDB's own 11.8 releases from MariaDB's repository, because MariaDB 11.8 is not part of Ubuntu's main archive. An apt pin lets exactly eight packages come from MariaDB's repository (pin priority 1001); every other package that repository publishes is pinned to -1, so it can never be installed or upgraded from there:
grep -E '^(Package|Pin|Pin-Priority):' /etc/apt/preferences.d/cloudimg-mariadb.pref
grep -h 'Origins-Pattern' /etc/apt/apt.conf.d/52cloudimg-mariadb-unattended
apt-cache policy mariadb-server | sed -n 1,8p
Package: mariadb-server mariadb-server-core mariadb-client mariadb-client-core mariadb-common libmariadb3 mysql-common galera-4
Pin: origin dlm.mariadb.com
Pin-Priority: 1001
Package: *
Pin: origin dlm.mariadb.com
Pin-Priority: -1
Unattended-Upgrade::Origins-Pattern { "origin=MariaDB,label=MariaDB,codename=noble,site=dlm.mariadb.com"; };
mariadb-server:
Installed: 1:11.8.9+maria~ubu2404
Candidate: 1:11.8.9+maria~ubu2404
Version table:
*** 1:11.8.9+maria~ubu2404 1001
-1 https://dlm.mariadb.com/repo/mariadb-server/11.8/repo/ubuntu noble/main amd64 Packages
100 /var/lib/dpkg/status
1:10.11.14-0ubuntu0.24.04.1 500
In apt-cache policy, the 1001 on the installed version is the pin for the MariaDB packages; the -1 on the repository line is the general rule for the rest of that repository (apt prints the source's default priority there). Ubuntu's own 10.11 packages, at 500, are never chosen over 1001. To stop automatic MariaDB updates, delete /etc/apt/apt.conf.d/52cloudimg-mariadb-unattended.
Security notes
- Listeners. Only SSH and nginx are reachable from the network (Step 3). Sync-in trusts a forwarded client address only from nginx on the instance (
trustProxy: 127.0.0.1), and nginx replaces anyX-Forwarded-FororX-Real-IPa client sends with the real connection address. - Instance metadata. Sync-in, nginx and MariaDB are blocked from the EC2 instance metadata service by systemd, and the AMI requires IMDSv2. The extra
192.0.2.0/24on Sync-in (the first line) is the address its update check is sent to (below):
systemctl show -p IPAddressDeny sync-in nginx mariadb
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 192.0.2.0/24
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
- Content-Security-Policy. The web app runs under a strict policy with no inline scripts (
script-src 'self' 'wasm-unsafe-eval'), and every user file Sync-in serves is sent with asandboxpolicy andnosniff, so an uploaded HTML or SVG file opens with no access to your session. HTML and SVG files are downloaded rather than shown:
H=$(sed -n 's/^server_name \(.*\);$/\1/p' /etc/nginx/sync-in-host.conf)
curl -s -o /dev/null -D - --resolve "$H:443:127.0.0.1" --cacert /etc/ssl/sync-in/sync-in.crt "https://$H/" | grep -i '^content-security-policy'
content-security-policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-src 'self' blob:; media-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
- Cross-site requests. A request that changes data, or opens a live-update connection, with another web site's
Originis refused by nginx with 403, and the links Sync-in follows on a plain GET (share link access and downloads, task downloads, two-factor enrolment) answer only Sync-in's own pages (Step 8). - Uploads and sign-in. Uploads are authenticated before their body is read, capped at 2 GiB and rate limited (Step 12); sign-in attempts are limited (Step 7).
- PDF previews. JavaScript inside a PDF never runs in the built-in viewer (Step 8).
- Kept off the network. Sync-in checks
releases.sync-in.orgfor a new version every day and has no setting to turn that off; on this image that name points the service at a documentation address it is not allowed to reach, so no request leaves the instance. The desktop-client download list is local (empty), and OCR uses the English model on the image instead of downloading one. To let the update check run, delete thereleases.sync-in.orgline from/etc/sync-in/hostsand runsudo systemctl restart sync-in. To add OCR languages, put their<lang>.traineddata.gzfiles in/opt/sync-in/ocr-languagesand list them underocr: languages:in the settings file:
sudo grep -v '^#' /etc/sync-in/hosts
ls /opt/sync-in/ocr-languages
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
192.0.2.1 releases.sync-in.org
eng.traineddata.gz
package.json
- The service is sandboxed. Sync-in runs as its own
sync-inuser under systemd with a read-only system (it can write only to/var/lib/sync-in/data), from a settings file owned by root. - Restrict 22/tcp to your management network and 80/443 to the networks your users need.
What cloudimg changed, and the advisories that remain
Sync-in's own files are installed exactly as the project published them. The image records what cloudimg changed, and which published advisories remain in the third-party libraries and why none of them can be reached, in /usr/share/doc/sync-in/SOURCE.txt (the same text is in /usr/share/doc/cloudimg/sync-in-BUILD-INFO.txt):
sed -n '/^What cloudimg changed:/,/^Written offer:/p' /usr/share/doc/sync-in/SOURCE.txt | sed '$d'
What cloudimg changed:
- Sync-in files on disk: none. Every file of the @sync-in/server 2.6.1 release in /opt/sync-in/app is
byte-identical to the published package (sha256 manifest /usr/share/doc/cloudimg/sync-in-release.sha256).
- As served (nginx, /etc/nginx/sites-available/cloudimg-sync-in), change 1: the response for static/index.html has one
inline attribute pair removed, media="print" onload="this.media='all'" (nginx sub_filter; cloudimg commit 4473b5fafb), so
the app's Content-Security-Policy without 'unsafe-inline' can apply and the stylesheet still loads.
- As served, change 2: the pdf.js scripting sandbox (/assets/pdfjs/build/pdf.sandbox.mjs, /assets/pdfjs/web/wasm/quickjs-eval.js,
/assets/pdfjs/web/wasm/quickjs-eval.wasm; list in /usr/share/doc/cloudimg/sync-in-pdfjs-sandbox-404.txt) is answered with
HTTP 404, so JavaScript inside a PDF never runs in the built-in PDF viewer. PDFs still open and render.
- Installed natively (Node.js 24.22.2 under systemd, sync-in.service), no Docker.
- Third-party libraries in /opt/sync-in/app/node_modules: installed with "npm ci --workspace=backend --omit=dev" from the
project's own package-lock.json at the tag, with ONE library raised within the version range the project itself declares:
follow-redirects 1.16.1 -> 1.16.2 (security advisory GHSA-mrwp-f3q8-3g9p). The exact lockfile used is
/usr/share/doc/cloudimg/sync-in-package-lock.json.
- Residual advisories in the shipped libraries (no fix inside the ranges the project declares), and why each is unreachable:
glob GHSA-3g3m-2fjr-6pw4 (moderate, AbortSignal cancellation bypass): @fastify/static runs glob once at start-up over its
own static root with a fixed pattern and passes no AbortSignal; no request reaches it.
minimatch GHSA-g3hv-75wg-m249 (moderate, pattern amplification): only that fixed start-up pattern is ever compiled; no
request supplies a pattern.
esbuild GHSA-67mh-4wv8-2f99 (moderate, development server): shipped (esbuild 0.18.20 under @esbuild-kit/core-utils,
pulled in by drizzle-kit, which the project lists as a runtime dependency; the other esbuild copies are fixed
releases) but never run: nothing the server loads imports drizzle-kit (its only importer is the drizzle-kit CLI
config, which is never loaded; migrations use drizzle-orm's migrator), so esbuild's development server never starts.
deepmerge GHSA-gcqx-w48m-w2h4 (low, prototype poisoning; no fixed release): used only to merge the root-owned settings
file with the service's own environment at start-up; no request data reaches it.
In short: no Sync-in file is modified (every file matches the release's checksum list); as served, nginx removes one inline attribute pair from the web app's start page so the strict policy can apply, and answers the PDF viewer's scripting engine with 404; one library is raised for a security advisory; and four advisories remain with no fix in the versions the project allows, each in code that no request can reach (glob and minimatch run once at start-up with a fixed pattern; esbuild's development server ships but is never started; deepmerge only merges the root-owned settings file at start-up). You can check the release files yourself:
cd /opt/sync-in/app && sha256sum --quiet -c /usr/share/doc/cloudimg/sync-in-release.sha256 && echo "all $(wc -l < /usr/share/doc/cloudimg/sync-in-release.sha256) release files match"
all 1347 release files match
Architecture summary
| Component | Detail |
|---|---|
| Application | Sync-in 2.6.1 (AGPL-3.0-or-later), Node.js 24.22.2, sync-in.service, 127.0.0.1:8080, run as sync-in from /opt/sync-in/app |
| Database | MariaDB 11.8.9 (MariaDB's repository, automatic security updates), 127.0.0.1:3306, database sync_in, data in /var/lib/sync-in/mysql |
| Web server | nginx on 443 (HTTPS, per-instance certificate in /etc/ssl/sync-in), 80 redirects to HTTPS, unknown host names dropped |
| Data | /var/lib/sync-in on its own 30 GiB EBS volume (data files, mysql database, nginx-body upload spool) |
| Settings | /etc/sync-in/environment/environment.yaml (0640 root:sync-in; secrets generated at first boot) |
| Credentials | /root/sync-in-credentials.txt, mode 0600, root only |
| Helpers | sync-in-set-admin-password, sync-in-set-hostname, sync-in-verify.sh (in /usr/local/sbin) |
| First boot | sync-in-firstboot.service; host name and certificate check on every later boot sync-in-set-host.service |
| Ports | 22 SSH, 80 redirect, 443 Sync-in (web app, API, WebDAV, desktop clients) |
| Base image | Ubuntu 24.04 LTS, fully patched, unattended security upgrades enabled, IMDSv2 required |
Support
cloudimg provides 24/7 support for this image, the Ubuntu operating system and its deployment. Email support@cloudimg.co.uk with your AWS account ID and the instance ID. For Sync-in itself, see the Sync-in website and the Sync-in/server repository on GitHub. The complete corresponding source of Sync-in 2.6.1 is on the instance in /usr/share/doc/sync-in (see SOURCE.txt). Sync-in is a trademark of its owner; cloudimg is not affiliated with, endorsed by or sponsored by the Sync-in project.