
Automate Daily Encrypted Backups With rsync and cron
Before you run this
This guide builds a small shell script that runs once a night from cron. Each run it makes a compressed tar archive of one source directory, encrypts that archive to a GPG public key, deletes the plaintext copy, then pushes the encrypted file to a remote server over SSH with rsync. The result is a backup that is encrypted at rest (GPG) and in transit (SSH). Only someone holding the matching GPG private key can read it — so keep that private key off the backup box.
Privileges: the script needs read access to the source directory and to the SSH key it uses. If you are backing up a normal data directory, run it as an ordinary user who owns the files. If the source contains root-owned system files, run it from root's crontab. It does not need to be more privileged than that. Installing packages (rsync, gnupg) needs sudo.
Test first. Read the script before you use it. Run it by hand once, against a small throwaway directory and a scratch path on the remote, before you ever put it in cron. Confirm you can actually decrypt a produced archive with your private key — an encrypted backup you cannot open is not a backup.
What it changes / what is irreversible: on the remote it creates files; on the local machine it writes archives into a staging directory and then deletes staged archives older than your retention window (find … -delete). That deletion is permanent, so point the staging directory at dedicated scratch space, not at anything you care about. The script never touches or deletes your source data.
Assumed environment: Debian 12 or Ubuntu 22.04, GNU bash, cron from the cron package, rsync 3.2.x, and gnupg. On RHEL/AlmaLinux the package for cron is cronie and rsync/gnupg are named the same; the script itself is identical.
Install what you need
sudo apt update
sudo apt install rsync gnupg openssh-client cron
Set up the SSH key for unattended login
cron runs with no terminal, so the SSH connection must be key-based and passphrase-free (or unlocked by an agent). Generate a dedicated key and install it on the remote:
# Run as the same user that will run the backup (e.g. root or a backup user)
ssh-keygen -t ed25519 -f ~/.ssh/backup_ed25519 -C "nightly-backup"
ssh-copy-id -i ~/.ssh/backup_ed25519.pub backup@backup.example.com
Replace backup@backup.example.com with your real remote user and host. Test it: ssh -i ~/.ssh/backup_ed25519 backup@backup.example.com should log in without a prompt.
Set up the GPG encryption key
Generate the key on a trusted machine that is NOT the backup server (your workstation), then export only the public half to the backup box. The private key stays with you, so a stolen backup server yields nothing but ciphertext.
On your workstation:
gpg --full-generate-key # create the keypair; note the email you use
gpg --armor --export backup@example.com > backup-pub.asc
Copy backup-pub.asc to the backup machine and import it there, as the user that runs the script:
gpg --import backup-pub.asc
gpg --list-keys backup@example.com # confirm it imported
Because you will not personally sign this key on the server, GPG treats it as untrusted and would normally stop in batch mode; the script passes --trust-model always to encrypt to it anyway. That is safe here — you generated the key yourself.
The backup script
Save this as /usr/local/bin/backup-encrypt.sh and make it executable (chmod 750). Every value in the top block is a placeholder — replace it with yours.
#!/usr/bin/env bash
set -euo pipefail
# ---- configuration: replace these ----
SOURCE_DIR="/path/to/source" # directory to back up (read-only to us)
STAGING_DIR="/var/backups/staging" # dedicated scratch space for archives
GPG_RECIPIENT="backup@example.com" # public key to encrypt to
REMOTE_USER="backup"
REMOTE_HOST="backup.example.com"
REMOTE_DIR="/srv/backups/$(hostname -s)" # must already exist on the remote
SSH_KEY="$HOME/.ssh/backup_ed25519"
RETENTION_DAYS=14 # prune local .gpg archives older than this
# --------------------------------------
TIMESTAMP="$(date +%Y%m%d)"
ARCHIVE="${STAGING_DIR}/backup-${TIMESTAMP}.tar.gz"
ENCRYPTED="${ARCHIVE}.gpg"
mkdir -p "$STAGING_DIR"
# 1. Compressed archive of the source directory (paths stored relative to SOURCE_DIR)
tar -czf "$ARCHIVE" -C "$SOURCE_DIR" .
# 2. Encrypt to the recipient's public key
gpg --batch --yes --trust-model always \
--recipient "$GPG_RECIPIENT" \
--output "$ENCRYPTED" \
--encrypt "$ARCHIVE"
# 3. Shred the plaintext archive so only ciphertext remains locally
rm -f "$ARCHIVE"
# 4. Push the encrypted archive over SSH. No -z: the .gpg is already compressed.
rsync -a --partial \
-e "ssh -i ${SSH_KEY} -o BatchMode=yes" \
"$ENCRYPTED" \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
# 5. Prune old local encrypted archives (this deletion is permanent)
find "$STAGING_DIR" -name 'backup-*.tar.gz.gpg' -type f -mtime +"$RETENTION_DAYS" -delete
Two things to know about rsync here. It will not create the remote directory for you, so create REMOTE_DIR on the remote once by hand (ssh … mkdir -p /srv/backups/$(hostname -s)). Recent rsync (3.2.3+, which Debian 12 has) offers --mkpath to create the destination path, but making the directory manually is the version-independent approach. This design keeps daily archives as separate dated files; it does not de-duplicate between days. If you want true deduplicated, encrypted, incremental backups, that is the job of borg or restic — different tools, out of scope here.
Run it once by hand
Before cron ever touches it, run it yourself and watch for errors:
/usr/local/bin/backup-encrypt.sh
Schedule it with cron
Edit the crontab of the user that owns the SSH and GPG setup (crontab -e, or sudo crontab -e for root):
# Run at 02:30 every day; capture output to a log
30 2 * * * /usr/local/bin/backup-encrypt.sh >> /var/log/backup-encrypt.log 2>&1
cron's environment is minimal, which is why the script uses absolute paths and $HOME for the key. Sending output to a log file gives you something to read when a run fails silently.
Verify it worked
Check the encrypted file actually landed on the remote:
ssh -i ~/.ssh/backup_ed25519 backup@backup.example.com \
"ls -lh /srv/backups/$(hostname -s)/"
Now the test that matters — prove you can restore. Pull one archive back and decrypt it on the machine that holds the private key:
scp backup@backup.example.com:/srv/backups/HOST/backup-YYYYMMDD.tar.gz.gpg .
gpg --output restored.tar.gz --decrypt backup-YYYYMMDD.tar.gz.gpg
tar -tzf restored.tar.gz | head # list contents without extracting
If gpg --decrypt succeeds and tar -tzf lists your files, the whole chain works. Do this once now and again whenever you rotate the GPG key.
After the first scheduled night, confirm cron ran: grep CRON /var/log/syslog (Debian/Ubuntu) shows the invocation, and /var/log/backup-encrypt.log shows the script's own output.
Undo / roll back
- Stop the schedule: remove the line from
crontab -e. The script and keys stay in place but nothing runs. - Remove the tooling: delete
/usr/local/bin/backup-encrypt.shand, if you want, the SSH key pair (~/.ssh/backup_ed25519*) and the imported public key (gpg --delete-keys backup@example.com). - Remove stored backups: delete files under
STAGING_DIRlocally and underREMOTE_DIRon the remote. There is nothing to undo on the source directory — the script only ever read from it.
Keep your GPG private key backed up somewhere safe and offline. Every archive this script produces is worthless without it — which is the point, and also the risk.
