Deployment guide

Sync in 2.6.1

Overview

This guide covers launching and using the Sync-in 2.6.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud file sharing and sync listing on AWS Marketplace. You select it as the software version when you subscribe and launch.

Sync-in is an open-source (AGPL-3.0) server for storing, sharing and synchronizing files. It provides 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 hardening listed below.

What you get:

  • 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 account, admin, with a password generated on your instance. It 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 rate-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. 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 is fully patched at build time, with unattended security upgrades turned 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

Before you launch, make sure you have the following:

  • 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.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select Sync-in 2.6.1 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2. Choose m5.large (or larger), your key pair, a security group as described above, and a subnet. Under Configure storage, you see two volumes: the root volume and the 30 GiB data volume for your files. If you already know you need more space, increase the data volume size now.

Alternatively, launch 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 before you set up clients. Ideally, also set your own DNS name (Step 10).

2

Connect to your instance

Connect over SSH as the default user, ubuntu, on Sync-in 2.6.1 on Ubuntu 24.04:

ssh -i <your-key.pem> ubuntu@<public-ip>

The login message shows the web address and the location of the credentials.

3

Check the services

First boot creates the database, generates every password and key, creates the administrator and runs a self-test. This takes under a minute. Check that all three services are active:

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. In the failure case, the site stays down rather than open. Checksystemctl status sync-in-firstboot and journalctl -u sync-in-firstboot.

4

Your address and the administrator password

The web address, the administrator login, its password and the certificate fingerprint are in a root-only file. Read it:

sudo cat /root/sync-in-credentials.txt
# Sync-in 2.6.1 - per-instance credentials, generated on first boot (sac). 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, password and fingerprint. Compare the fingerprint with the one 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 = sac 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.

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. Use Upload (or drag files onto the page) to add files, and New to create folders and documents. The left bar opens recent files, favorites, 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 at any time:

sudo sync-in-verify.sh
SYNCIN_ROUNDTRIP_OK host=<public-dns-name> via=<private-ip>
6

Change the administrator password

Run the helper and type the new password twice (16 to 128 characters). The password never appears on a command line, and the helper writes it to the credentials file:

sudo sync-in-set-admin-password

To set the password 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. Example output from 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 expires (4 hours). To sign out every session 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.

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. A new instance has only the admin account.

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, and 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. To see this, run the following 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.

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 carry JavaScript, which the viewer would otherwise run in a scripting sandbox. That script 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. If you trust the file and need them, download it and open it in a desktop PDF reader. To check this from the instance, run:

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 paths are the scripting engine (the list is also in /usr/share/doc/sac/sync-in-pdfjs-sandbox-404.txt). The last two paths 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 that the dialog gives you (Copy link, https://<host>/#/link/<id>). The download addresses that the page uses behind the scenes (/api/app/link/...) answer only requests made by Sync-in's own pages. If someone pastes one of those raw addresses into the browser's address bar, the server refuses it with 403. This stops another website from triggering a link download or access in a recipient's browser. The following commands, run from the instance, send the same request as a pasted address and as one from Sync-in's own page. The second request 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. Remove a link there when you no longer need it.

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 instead. 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. For example, with rclone on your computer:

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

The following is a real run against the instance used for this guide. It used rclone 1.75.2 and an application password created for that instance. Because no public certificate was installed, rclone was pointed at the instance's certificate with --ca-cert after the fingerprint was compared:

$ 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 reporting this. With a wrong password, or after you delete the application password, 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, a file whose name contains a backslash cannot be uploaded or reached over WebDAV. Rename it first (Windows does not allow the character anyway). A client must also 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
10

Host name and public address

Sync-in is served under one name, and every link it creates uses that name. On first boot, that name is the instance's public DNS name from EC2 instance metadata (IMDSv2), or the public IP address if the instance has no DNS name. At each boot,sync-in-set-host.service runs sync-in-set-hostname --boot, which reads the name you saved (or instance metadata again if you saved none) 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)

To use your own name, point a DNS record at the instance (with an Elastic IP), then run the helper with your name. The helper updates nginx, the cross-site guard and Sync-in's public address, and restarts Sync-in. It issues a new self-signed certificate for the name, but only if the certificate is still one that SecureAnyCloud's helper made (see Step 11). It also keeps the name for every later boot:

sudo sync-in-set-hostname <your-domain>

The following output is from the instance used for this guide. It first applies a name with --no-persist, which does not save the name, and then returns 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. If you have not set 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, because the boot unit re-reads instance metadata. Attach an Elastic IP to avoid this. To return to automatic naming after you save a name, delete /etc/sac/sync-in-hostname and run sudo sync-in-set-hostname --boot.

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. The image replaces only SecureAnyCloud's first-boot certificate. When the helper issues a certificate, it records that certificate's SHA-256 fingerprint in /var/lib/sac/data/sync-in-cert.sha256 (root only). 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/sac/data/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/sac/data/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, and the image keeps it byte for byte on every boot and every name change. To install your certificate, copy 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

On the instance used for this guide, a customer-style certificate was installed as 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

Before each version is released, the same rule is checked with a real reboot on a fresh instance launched from the published image. 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
12

Storage, uploads and backups

Every file, the database and nginx's upload spool are on the data volume. Inspect the 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. 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;

These settings allow 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 (in 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 run sudo systemctl restart sync-in and sudo systemctl reload nginx. Without a session, the server refuses an upload 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. The system root user reaches MariaDB's root account 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 (readable by root and the sync-in service only) with the backups. It holds the database password and the encryption key that Sync-in needs to read its own data.

Growing the volume. Increase the data volume's size in the EC2 console (or with aws ec2 modify-volume), then grow the file system online:

lsblk
sudo resize2fs "$(findmnt -no SOURCE /var/lib/sync-in)"
13

Updates

Sync-in is updated through new versions of this image, which SecureAnyCloud publishes after the Sync-in project releases. To move to a new version, launch it alongside your instance and bring your files and database across from a backup (Step 12). SecureAnyCloud 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 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 the repository publishes is pinned to -1, so apt never installs or upgrades it from there:

grep -E '^(Package|Pin|Pin-Priority):' /etc/apt/preferences.d/sac-mariadb.pref
grep -h 'Origins-Pattern' /etc/apt/apt.conf.d/52sac-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 the apt-cache policy output, 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/52sac-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 any X-Forwarded-For or X-Real-IP header that a 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/24 on Sync-in (the first line) is the address that its update check is sent to (see "Kept off the network" 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 a sandbox policy and nosniff, so an uploaded HTML or SVG file opens with no access to your session. HTML and SVG files are downloaded rather than shown. Check the header:
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 website's Origin is refused by nginx with 403. The links that Sync-in follows on a plain GET (share link access and downloads, task downloads, two-factor enrollment) 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 rate-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.org for 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 the releases.sync-in.org line from /etc/sync-in/hosts and run sudo systemctl restart sync-in. To add OCR languages, put their <lang>.traineddata.gz files in /opt/sync-in/ocr-languages and list them under ocr: languages: in the settings file. To review both locations, run:
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-in user under systemd with a read-only system (it can write only to /var/lib/sync-in/data), and reads its settings from a file owned by root.
  • Restrict network access. Limit 22/tcp to your management network, and limit 80/443 to the networks your users need.

What SecureAnyCloud changed, and the advisories that remain

Sync-in's own files are installed exactly as the project published them. The image records what SecureAnyCloud changed, which published advisories remain in the third-party libraries, and why none of them can be reached. The record is in /usr/share/doc/sync-in/SOURCE.txt (the same text is in /usr/share/doc/sac/sync-in-BUILD-INFO.txt):

sed -n '/^What sac changed:/,/^Written offer:/p' /usr/share/doc/sync-in/SOURCE.txt | sed '$d'
What sac 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/sac/sync-in-release.sha256). - As served (nginx, /etc/nginx/sites-available/sac-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; sac 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/sac/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/sac/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.
  • 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, and deepmerge only merges the root-owned settings file at start-up.

To check the release files yourself, run:

cd /opt/sync-in/app && sha256sum --quiet -c /usr/share/doc/sac/sync-in-release.sha256 && echo "all $(wc -l < /usr/share/doc/sac/sync-in-release.sha256) release files match"
all 1347 release files match

Architecture summary

The image is made up of the following components:

  • 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). Port 80 redirects to HTTPS, and unknown host names are 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. The host name and certificate check on every later boot is 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

SecureAnyCloud provides 24/7 support for this image, the Ubuntu operating system and its deployment. Email sales@secureanycloud.com with your AWS account ID and the instance ID. For questions about 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. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the Sync-in project.

Deploy on AWS

Launch Sync-in 2.6.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS