Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Category:

Systems Programming

Published:

Tags:
  • restic
  • backups
  • sftp
  • encryption
  • linux
  • systemd

How to Set Up Automated Encrypted Backups with restic and a Remote SFTP Target

If you already have a box somewhere with SSH access, whether that is a VPS you rent for a few pounds a month or a Raspberry Pi at a relative's house, you have everything you need for an offsite backup target. No object storage account, no API keys, no third-party bucket policy to get wrong. Just SFTP and restic, which encrypts everything client-side before it leaves your machine.

This is a practical walkthrough: set up the remote target, initialise an encrypted restic repository over SFTP, run a first backup, then wire it up as a systemd timer so it happens on its own and you find out about failures instead of discovering them the day you need a restore.

Why SFTP as a backend

restic supports several backends: local paths, SFTP, S3-compatible object storage, Backblaze B2, Azure, Google Cloud Storage, and REST servers. SFTP is the least glamorous of these, and that is exactly its appeal for a personal setup. It needs nothing more than an SSH-accessible account on the far end: no bucket permissions model to reason about, no lifecycle rules, no vendor SDK. The repository is just a directory tree that restic manages entirely through the SFTP protocol, and the remote side never sees plaintext or the encryption key.

The tradeoff is throughput and concurrency. SFTP is single-connection-per-operation and generally slower than a well-tuned S3 backend for very large repositories. For a home server, a handful of laptops, or a small fleet of VPS instances, that ceiling is rarely the limiting factor.

Prerequisites

  • A remote host reachable over SSH, with enough disk space for your backup set plus growth headroom.
  • A dedicated, unprivileged user on that remote host for backups (do not use your normal login account or root).
  • restic installed on the machine being backed up. It ships as a single static binary; install via your distribution's package manager or download the release binary directly from the restic GitHub releases page.
  • SSH key authentication already working from the source machine to the remote host, with the private key unprotected by a passphrase (or unlocked via an agent) so that unattended backups can run without a prompt.

Step 1: prepare the remote user and directory

On the remote host, create a user with no shell and a home directory dedicated to the repository:

sudo useradd --system --create-home --shell /usr/sbin/nologin backup
sudo mkdir -p /home/backup/repo
sudo chown backup:backup /home/backup/repo

Add the source machine's public key to that user's authorized_keys:

sudo -u backup mkdir -p /home/backup/.ssh
sudo -u backup tee -a /home/backup/.ssh/authorized_keys < /path/to/id_ed25519.pub
sudo chmod 700 /home/backup/.ssh
sudo chmod 600 /home/backup/.ssh/authorized_keys

If you want to restrict this key to nothing but SFTP (no interactive shell, no port forwarding), prefix the key's line in authorized_keys with a restriction, and give the user internal-sftp as its forced command in sshd_config:

Match User backup
    ForceCommand internal-sftp
    ChrootDirectory /home/backup
    AllowTcpForwarding no
    X11Forwarding no

Restart sshd after this change. The ChrootDirectory requires the chroot root (/home/backup) to be owned by root and not writable by the backup user, so if you use this, adjust ownership: chown root:root /home/backup and put the actual writable repo in a subdirectory the backup user owns.

Step 2: initialise the repository

On the source machine, decide how you will store the repository password. The cleanest option for unattended backups is a password file, not an environment variable baked into a script, because a file can be locked down with normal filesystem permissions:

sudo mkdir -p /etc/restic
sudo sh -c 'tr -dc A-Za-z0-9 </dev/urandom | head -c 48 > /etc/restic/password'
sudo chmod 600 /etc/restic/password

Keep a copy of this password somewhere outside the machine you are backing up. restic encrypts the repository with a key derived from this password, and it is not recoverable from the repository itself. Lose the password and the backups are cryptographically unrecoverable, which is the point of an encrypted backup but also the way people lose entire archives.

Now initialise the repository over SFTP:

export RESTIC_REPOSITORY="sftp:backup@remote-host:/home/backup/repo"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
restic init

restic shells out to your local ssh binary for the SFTP connection, so it picks up your existing ~/.ssh/config, including any IdentityFile, Port, or ProxyJump directives you have set for that host. If the remote host is not yet a known host, add it to known_hosts before running this unattended, or restic (via ssh) will hang on the interactive fingerprint prompt.

Step 3: run a backup and check it

restic backup /home/andy /etc --exclude-caches \
  --exclude '/home/andy/.cache' \
  --exclude '*.tmp'

--exclude-caches skips any directory containing a CACHEDIR.TAG file, which covers most browser and build tool caches without you having to enumerate them by hand. List the repository's snapshots to confirm it worked:

restic snapshots

restic deduplicates at the chunk level across all snapshots in a repository, so repeated backups of a mostly-unchanged filesystem are fast and the incremental storage cost is small, even though every snapshot is logically a full backup you can restore from directly (no chain of incrementals to replay).

Step 4: automate it with systemd

A cron entry works, but a systemd service plus timer gives you structured logging in the journal and a straightforward way to see the last run's exit status. Create the service unit:

[Unit]
Description=restic backup to remote SFTP repository
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
Environment=RESTIC_REPOSITORY=sftp:backup@remote-host:/home/backup/repo
Environment=RESTIC_PASSWORD_FILE=/etc/restic/password
ExecStart=/usr/bin/restic backup /home/andy /etc \
  --exclude-caches --exclude '/home/andy/.cache'
ExecStartPost=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

Save this as /etc/systemd/system/restic-backup.service. The forget step is what actually enforces a retention policy: backup on its own keeps every snapshot forever, and a repository with no pruning grows without bound. --keep-daily 7 --keep-weekly 4 --keep-monthly 12 is a reasonable starting policy; adjust it to how much history you actually want to be able to restore from.

Then the timer:

[Unit]
Description=Daily restic backup

[Timer]
OnCalendar=daily
RandomizedDelaySec=30min
Persistent=true

[Install]
WantedBy=timers.target

Save as /etc/systemd/system/restic-backup.timer, then enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
sudo systemctl list-timers restic-backup.timer

Persistent=true means a missed run (machine was off overnight) fires as soon as the machine is back up, rather than silently waiting for the next scheduled slot. RandomizedDelaySec is worth having if you manage more than one machine backing up to the same remote host, so they do not all hit the SFTP server at exactly the same second.

Verifying the backup is actually restorable

A backup you have never restored from is a hypothesis, not a backup. Periodically check repository integrity:

restic check

And actually restore something, into a scratch directory, to confirm the whole chain works end to end:

restic restore latest --target /tmp/restore-test --include /etc/hostname
diff /tmp/restore-test/etc/hostname /etc/hostname

Do this after the initial setup, and again after any change to the exclude list, retention policy, or SSH configuration. It is the cheapest insurance available against discovering, at the worst possible moment, that the password file was wrong or the remote host's disk quietly filled up months ago.

A few things that catch people out

The password file permissions matter more than they look. If /etc/restic/password is world-readable, anyone with local access to the source machine can decrypt every snapshot in the repository, which defeats the point of encrypting it in the first place.

Watch the remote disk. SFTP gives you no built-in lifecycle management or storage quota enforcement; if the remote filesystem fills up, restic backup fails loudly (check the journal), but nothing stops the underlying disk getting there in the first place except your own forget --prune schedule and occasional manual checks of df on the remote host.

Finally, forget without --prune only removes the snapshot metadata; the underlying data chunks stay on disk until a prune actually runs, because other retained snapshots might still reference them. Running --prune as part of every scheduled job (as in the unit above) keeps the repository from accumulating orphaned chunks, at the cost of a slower rewrite pass each time it removes something. For a large repository pruned daily, that cost is usually small; if it becomes noticeable, prune on a separate, less frequent timer instead of every run.