👍 What we like
- ✓Genuinely follows the 3-2-1 rule, with an off-site copy.
- ✓Client-side encryption: neither the NAS nor the cloud provider can read your data.
- ✓Deduplication: after the first backup, only changes are transferred.
- ✓Fine-grained restore of a single file or folder with restore --include or dump.
- ✓Free and open-source software, no mandatory proprietary service.
👎 What to watch
- ✕Command-line setup: no official graphical interface.
- ✕Losing a repository password makes that repository unrecoverable.
- ✕The server holds the credentials for both repositories: plan versioning or Object Lock against ransomware.
- ✕Cloud storage and data re-reads (check) may be billed by the provider.
🏆 Our picks
Affiliate links · same price for youSynology DiskStation DS1821+
Amazon · price and availability on the listing
See all our recommended gear in the shop.
📑 Contents ▾
- 01 Prerequisites
- 02 Step-by-step installation
- · Step 1: Install restic
- · Step 2: Prepare SFTP access to the NAS
- · Step 3: Create the secret files
- · Step 4: Initialize the NAS repository (copy 2)
- · Step 5: Initialize the cloud repository (copy 3)
- · Step 6: Write the backup script
- · Step 7: Schedule the daily backup
- · Step 8: Test a restore
- 11 Verification checks
- 12 Security and best practices
- 13 Troubleshooting common errors
- 14 FAQ
- · Are the backups encrypted?
- · Why restic copy instead of two independent backups?
- · How do I restore a single file?
- · How do I check that the cloud backups are intact?
- 19 Read also
The 3-2-1 rule calls for three copies of your data, on two different media, with one off-site. This tutorial sets it up with restic, a free and open-source backup tool that encrypts everything client-side. The architecture looks like this:
- Copy 1: the original data on your Linux server (for example
/etc,/home,/srv). - Copy 2: a restic repository on your NAS, reached over SFTP on the local network.
- Copy 3: a second restic repository in an S3-compatible bucket (Amazon S3, Backblaze B2’s S3 API, Wasabi, an off-site MinIO…), filled by
restic copyfrom the NAS repository.
restic encrypts both repositories before anything is sent, so neither the NAS nor the cloud provider stores anything readable without the password. restic reads no YAML configuration file. Everything goes through command-line options and environment variables (RESTIC_PASSWORD_FILE, AWS_ACCESS_KEY_ID…), which is why you’ll write a small shell script below.
Prerequisites
- A recent Linux server with
rootorsudoaccess: Debian 12/13 or Ubuntu 22.04/24.04. Commands are run as root, because the scheduled job will run as root to read every file. - A NAS on the local network, with the SFTP service enabled and a dedicated shared folder (for example
backup). A QNAP TS-453D or a Synology DiskStation DS1821+ works as the SFTP target for copy 2. But the NAS alone does not satisfy the 3-2-1 rule: it stays in your home and is not the off-site copy. - An account with an S3-compatible storage provider, with an empty bucket and an access key restricted to that bucket. For Backblaze B2, the restic documentation recommends the S3-compatible API rather than the native
b2:backend. - No inbound ports need to be opened: the server connects to the NAS over the LAN and reaches the cloud outbound over HTTPS.
Step-by-step installation
Step 1: Install restic
The distribution package is the easiest to install, but it often lags behind the current version (0.19.1 at the time of writing). Install it, then fetch the current official binary into /usr/local/bin, without overwriting the file managed by dpkg:
sudo apt update
sudo apt install -y restic
sudo restic self-update --output /usr/local/bin/restic
hash -r
restic version
If self-update isn’t available in your distribution’s package, install the official binary by hand (set VER to the latest release on GitHub, and change amd64 to arm64 if needed):
mkdir -p /tmp/restic-dl && cd /tmp/restic-dl
VER=0.19.1
curl -LO "https://github.com/restic/restic/releases/download/v${VER}/restic_${VER}_linux_amd64.bz2"
curl -LO "https://github.com/restic/restic/releases/download/v${VER}/SHA256SUMS"
sha256sum --ignore-missing -c SHA256SUMS
bunzip2 "restic_${VER}_linux_amd64.bz2"
sudo install -m 755 "restic_${VER}_linux_amd64" /usr/local/bin/restic
Check: command -v restic must return /usr/local/bin/restic, and restic version the expected version. sha256sum must print OK for the downloaded file.
Step 2: Prepare SFTP access to the NAS
Enable SFTP on the NAS:
- Synology DSM 7: Control Panel > File Services > FTP, tick Enable SFTP service, then Apply.
- QNAP QTS 5.1.8+ / QuTS hero: Control Panel > Network & File Services > Telnet / SSH, tick Enable SFTP, then Apply.
Create a dedicated user on the NAS (for example backup) with write access to the backup shared folder only. Then, on the server, generate an SSH key without a passphrase (cron must be able to use it) and declare an alias:
sudo ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519_restic
sudo tee -a /root/.ssh/config >/dev/null <<'EOF'
Host nas
HostName 192.168.1.20
User backup
Port 22
IdentityFile /root/.ssh/id_ed25519_restic
IdentitiesOnly yes
EOF
sudo chmod 600 /root/.ssh/config
sudo ssh-copy-id -i /root/.ssh/id_ed25519_restic.pub nas
Replace 192.168.1.20 with your NAS’s address. If ssh-copy-id fails, see your NAS documentation for how to enable key-based authentication. On DSM, you usually need to enable the user home service.
Check: sudo sftp nas must open a session without asking for a password. Type ls: the shared folder should appear (on DSM, shares show up as /backup). Note the exact path, then exit with bye.
Step 3: Create the secret files
Each repository gets its own password, stored in a file only root can read. The S3 credentials go into an environment file:
sudo install -d -m 700 /etc/restic
sudo sh -c 'openssl rand -base64 32 > /etc/restic/nas.pass'
sudo sh -c 'openssl rand -base64 32 > /etc/restic/cloud.pass'
sudo tee /etc/restic/cloud.env >/dev/null <<'EOF'
AWS_ACCESS_KEY_ID=YOUR_KEY_ID
AWS_SECRET_ACCESS_KEY=YOUR_SECRET
AWS_DEFAULT_REGION=eu-central-1
CLOUD_REPO=s3:https://YOUR_PROVIDER_S3_ENDPOINT/BUCKET_NAME/restic
EOF
sudo sh -c 'chmod 600 /etc/restic/*'
Take the endpoint and region from your provider’s console. On Backblaze B2, the S3 endpoint is listed in the bucket details. Copy both passwords into a password manager right away, off the server: without them, the repositories cannot be recovered.
Check: sudo ls -l /etc/restic must show three files (cloud.env, cloud.pass, nas.pass) as -rw------- root root.
Step 4: Initialize the NAS repository (copy 2)
sudo restic -r sftp:nas:/backup/restic --password-file /etc/restic/nas.pass init
Adjust /backup/restic to the path you noted in step 2. restic creates the restic folder itself.
Check:
sudo restic -r sftp:nas:/backup/restic --password-file /etc/restic/nas.pass cat config
The command should print JSON containing "version" and "chunker_polynomial".
Step 5: Initialize the cloud repository (copy 3)
The cloud repository is created with the same chunker parameters as the NAS repository, so deduplication works during restic copy. These parameters can’t be changed after creation.
sudo bash -c 'set -a; . /etc/restic/cloud.env; set +a
restic -r "$CLOUD_REPO" --password-file /etc/restic/cloud.pass init \
--from-repo sftp:nas:/backup/restic --from-password-file /etc/restic/nas.pass \
--copy-chunker-params'
Check: your provider’s console should show a restic/ prefix in the bucket containing config, keys/, and so on. The same command with snapshots in place of init ... should run without errors (no snapshots yet).
Step 6: Write the backup script
First, create the exclusion list:
sudo tee /etc/restic/excludes.txt >/dev/null <<'EOF'
/home/*/.cache
/var/tmp
*.tmp
EOF
Then create /usr/local/sbin/restic-321.sh. It backs up to the NAS, applies retention, copies the snapshots to the cloud, then applies the same retention on the cloud side:
sudo tee /usr/local/sbin/restic-321.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
export PATH=/usr/local/bin:/usr/bin:/bin
exec >>/var/log/restic-321.log 2>&1
trap 'echo "$(date -Is) FAILED (line $LINENO)"' ERR
echo "=== $(date -Is) start ==="
NAS_REPO="sftp:nas:/backup/restic"
NAS_PASS=/etc/restic/nas.pass
set -a; . /etc/restic/cloud.env; set +a
KEEP=(--keep-daily 7 --keep-weekly 4 --keep-monthly 12)
restic -r "$NAS_REPO" --password-file "$NAS_PASS" backup \
/etc /home /srv --exclude-file /etc/restic/excludes.txt
restic -r "$NAS_REPO" --password-file "$NAS_PASS" forget "${KEEP[@]}" --prune
restic -r "$CLOUD_REPO" --password-file /etc/restic/cloud.pass copy \
--from-repo "$NAS_REPO" --from-password-file "$NAS_PASS"
restic -r "$CLOUD_REPO" --password-file /etc/restic/cloud.pass forget "${KEEP[@]}" --prune
echo "=== $(date -Is) end OK ==="
EOF
sudo chmod 700 /usr/local/sbin/restic-321.sh
sudo /usr/local/sbin/restic-321.sh
Change /etc /home /srv to your own data. A path that doesn’t exist makes backup exit with an error code, and the script then stops. The first run can take a while: everything is uploaded once, and after that only changes are sent.
Check: sudo tail -n 20 /var/log/restic-321.log must end with end OK. Then:
sudo restic -r sftp:nas:/backup/restic --password-file /etc/restic/nas.pass snapshots
sudo bash -c 'set -a; . /etc/restic/cloud.env; set +a; restic -r "$CLOUD_REPO" --password-file /etc/restic/cloud.pass snapshots'
Both lists must contain the same snapshot: same date, same paths.
Step 7: Schedule the daily backup
sudo tee /etc/cron.d/restic-321 >/dev/null <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 2 * * * root flock -n /run/restic-321.lock /usr/local/sbin/restic-321.sh
EOF
sudo chmod 644 /etc/cron.d/restic-321
flock -n prevents two runs at the same time, for example if a prune takes longer than expected. Output is not sent to /dev/null: the script writes to /var/log/restic-321.log itself.
Check: the next day, sudo journalctl -u cron --since yesterday | grep restic-321 should show the run, and the log should contain a new end OK line. To test without waiting, temporarily set the time a few minutes ahead.
Step 8: Test a restore
A backup that has never been restored is not a backup. Restore a folder from the cloud (the worst case) into a temporary directory, then compare:
sudo bash -c 'set -a; . /etc/restic/cloud.env; set +a
restic -r "$CLOUD_REPO" --password-file /etc/restic/cloud.pass \
restore latest --target /tmp/restore-test --include /etc/ssh'
sudo diff -r /etc/ssh /tmp/restore-test/etc/ssh && echo "Restore identical"
sudo rm -rf /tmp/restore-test
Check: Restore identical should be printed. A difference can be normal if the file changed since the backup.
Verification checks
Every so often, for example once a month, run an integrity check on both repositories. --read-data-subset actually re-reads part of the data, not just the metadata:
sudo restic -r sftp:nas:/backup/restic --password-file /etc/restic/nas.pass check --read-data-subset=10%
sudo bash -c 'set -a; . /etc/restic/cloud.env; set +a; restic -r "$CLOUD_REPO" --password-file /etc/restic/cloud.pass check --read-data-subset=5%'
Expected result: no errors were found. On the cloud side, re-reading data generates egress traffic, which some providers charge for, so keep the percentage modest.
Security and best practices
- Never expose to the Internet the NAS admin interface (DSM, QTS), its SSH or its SFTP, and set up no port forwarding to them. For remote administration, go through a mesh VPN: see our Tailscale tutorial.
- Lost password = lost data: restic offers no way to recover it. Keep both passwords off the server.
- Least privilege: the NAS
backupuser can only write to its share, and the S3 key only has access to its bucket. - Ransomware: the server holds the credentials for both repositories, so an attacker who compromises it could delete them. To protect yourself, enable bucket versioning or Object Lock if your provider offers it, as well as NAS snapshots. Check how they work with
prunein your provider’s documentation and in the restic documentation (“append-only” section). - Updates:
sudo apt update && sudo apt upgradefor the system,sudo restic self-update --output /usr/local/bin/resticfor restic.
Troubleshooting common errors
- Symptom:
Fatal: unable to open config file/Is there a repository at the following location?Cause: wrong SFTP path or S3 URL, or the repository was never initialized. Fix: check the path withsudo sftp nasthenls, and checkCLOUD_REPO(formats3:https://endpoint/bucket/prefix, path-style). - Symptom:
subsystem request failed on channel 0or connection refused. Cause: SFTP is disabled on the NAS, or the port is wrong. Fix: re-enable SFTP (menu paths in step 2) and checkPortin/root/.ssh/config. - Symptom:
wrong password or no key found. Cause: you are using one repository’s password file on the other. Fix:nas.passgoes with the NAS repository,cloud.passwith the cloud repository (and--from-password-filewith the source ofcopy). - Symptom:
Access Deniedor a region error on the S3 side. Cause: the key has no rights on the bucket, or the region is wrong. Fix: generate a new key restricted to the bucket and setAWS_DEFAULT_REGIONto the value your provider gives. - Symptom:
repository is already locked. Cause: another operation is running, or a run was interrupted. Fix: check withpgrep -a resticthat no restic process is running, then runrestic -r ... unlockon the repository concerned.
FAQ
Are the backups encrypted?
Yes. restic encrypts and authenticates all data on the server before sending it (AES-256 in counter mode, authenticated with Poly1305-AES, according to its design documentation). The NAS and the cloud provider only see encrypted blobs.
Why restic copy instead of two independent backups?
Files are read only once, and both repositories hold exactly the same snapshots. Thanks to --copy-chunker-params, only data missing from the cloud is transferred.
How do I restore a single file?
Find its path with restic -r ... ls latest, then run restic -r ... restore latest --target /tmp/r --include /path/to/file. To print the file to standard output, use restic -r ... dump latest /path/to/file.
How do I check that the cloud backups are intact?
restic check verifies the repository structure. Add --read-data-subset to re-read and verify part of the data actually stored.