most secure "rsync + ssh" setup , possibly for backups

This instructions are for a setup that pushes data (such as backups) from a localhost to a serverhost, using rsync in daemon mode, but tunneled inside an SSH connection, and making sure that if someone hacks into the localhost, they will not be able to use the backup machinery to hack into the serverhost.

A maintained implementation of this setup, including the current Python 3 wrapper and an optional post-transfer snapshot/prune hook, is in the Git repository

http://mennucc1.debian.net/simple_rsync_backup.git
This page explains the design; for a real installation you should clone that repository and use the scripts from there.

Server installation

Note, all following commands should be executed as "root" user, or prepended by the "sudo" command.
Create special system user
serverhost# adduser --system rsyncd
Install the current scripts from the repository:
serverhost# apt install git rsync openssh-server python3 python3-daemon
serverhost# apt install btrfs-progs        # optional, for Btrfs-backed snapshots
serverhost# git clone http://mennucc1.debian.net/simple_rsync_backup.git /usr/local/src/simple_rsync_backup
serverhost# git -C /usr/local/src/simple_rsync_backup submodule init
serverhost# git -C /usr/local/src/simple_rsync_backup submodule update
serverhost# install -o root -g root -m 755 /usr/local/src/simple_rsync_backup/rsyncd_wrapper /usr/local/sbin/rsyncd_wrapper
serverhost# install -d -o rsyncd -g rsyncd /home/rsyncd/log
serverhost# install -d -o rsyncd -g rsyncd -m 700 /home/rsyncd/.ssh
serverhost# install -o rsyncd -g rsyncd -m 600 /dev/null /home/rsyncd/.ssh/authorized_keys
serverhost# su rsyncd -s /bin/sh -c 'touch ~/.hushlogin'
If you use Btrfs, allow the rsyncd user to run only the required btrfs subvol commands, for example by adapting the provided examples_sudoers.txt and installing it as /etc/sudoers.d/rsyncd-btrfs. Always validate the result with visudo -cf /etc/sudoers.d/rsyncd-btrfs.
The command
serverhost# usermod rsyncd -s /usr/local/sbin/rsyncd_wrapper
is the key security step: the rsyncd account will not get an interactive shell, it will only be able to start the restricted wrapper. The post-transfer hook should be run from the cloned repository, so it can import the bundled plain_config.py helper.

Server configuration files

Create the config file /home/rsyncd/rsyncd.conf for rsyncd and add one or more backup modules. Here is an example /home/rsyncd/rsyncd.conf.
# sample rsyncd.conf configuration file
# GLOBAL OPTIONS
log file=/home/rsyncd/log/rsyncd.log
pid file=/home/rsyncd/rsyncd.pid
# MODULE OPTIONS
[host1]
        comment = backup space for one host
        path = /srv/backups/host1/current
        use chroot = no
# the default for read only is yes...
        read only = no
        ignore nonreadable = yes
        transfer logging = no
        refuse options = checksum
        dont compress = *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz
        auth users = host1
        secrets file = /home/rsyncd/rsyncd.secrets
        post-xfer exec = /usr/local/src/simple_rsync_backup/post_backup
        # for this , /srv/backups must be mounted with option user_xattr
        fake super = yes
The matching rsync-daemon passwords are stored in /home/rsyncd/rsyncd.secrets. The example above uses fake super = yes. For that to work correctly, the backup filesystem must support extended attributes; otherwise metadata such as owner, mode, and modification time may be lost in the backup. On Linux this usually means mounting the backup filesystem with user_xattr.
With this layout the server keeps:
The post-transfer hook may also read optional config overrides via the bundled plain_config.py helper. Global settings are read first from /home/rsyncd/.config/simple_rsync_backup/post_backup.conf, then a per-backup file may override them from /srv/backups/host1/post_backup.conf. The local file wins over the global one. For the full format and type modifiers, see sub/plain_config/README.md in the repository. Currently the relevant variables are PRUNE_MIN_LEN, PRUNE_KEEP_RECENT, and PRUNE_CADENCE, which control when pruning starts, how many recent snapshots are always kept, and how sparse the older retained history becomes. For example:
PRUNE_MIN_LEN/i=60
PRUNE_KEEP_RECENT/i=14
PRUNE_CADENCE/i=7

If /srv/backups is a Btrfs filesystem, the post-transfer script may create read-only snapshots in past/YYYY/MM/DD; otherwise it falls back to copying the tree.

Recommended workflow: control

The repository also ships a helper script named control. This is the recommended way to create a new backup pair. Server-side usage is intended to run as user rsyncd and operate on /home/rsyncd/rsyncd.conf. Since rsyncd does not have a login shell, use sudo su - rsyncd -s /bin/bash to enter an interactive shell as rsyncd. For example:
serverhost$ python3 /usr/local/src/simple_rsync_backup/control check --module-name host1
serverhost$ python3 /usr/local/src/simple_rsync_backup/control server-add-module --module-name host1 --server-host backup-server.example.net --write-json /tmp/host1.json
The second command creates /srv/backups/host1/current and /srv/backups/host1/past, appends a module stanza to /home/rsyncd/rsyncd.conf, adds mandatory rsync-daemon auth in /home/rsyncd/rsyncd.secrets, and writes a JSON handoff file. By default the rsync auth user is the same as the module name, and if no password is passed a random 10-character password is generated.
On the client side, the same script can consume that file to create a dedicated SSH keypair and render a backup script:
localhost$ python3 /usr/local/src/simple_rsync_backup/control client-add --from /tmp/host1.json
By default that writes ~/bin/backup_<module_name>_<hostname>, ~/.config/simple_rsync_backup/<module_name>_<hostname>.rsync_password, and ~/.config/simple_rsync_backup/<module_name>_<hostname>.exclude_from. The generated script also expects helper commands such as flock, nocache, and ionice; on Debian systems you may need to install the packages that provide them, for example apt install nocache util-linux. The generated script uses a fixed receiver-side partial directory .rsync-partial together with --delete-after, so interrupted transfers can resume across runs while stale partial files are cleaned up by rsync after a successful run. The JSON handoff file contains the rsync password, so treat it as secret material.

Manual setup

This section is only needed if you do not want to use control.

Server

Add a stanza in /home/rsyncd/rsyncd.conf, for example:
[host1]
        comment = backup space for one host
        path = /srv/backups/host1/current
        use chroot = no
        read only = no
        ignore nonreadable = yes
        transfer logging = no
        refuse options = checksum
        dont compress = *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz
        auth users = host1
        secrets file = /home/rsyncd/rsyncd.secrets
        post-xfer exec = /usr/local/src/simple_rsync_backup/post_backup
        fake super = yes
Then add the matching rsync-daemon credential to /home/rsyncd/rsyncd.secrets:
host1:some_random_password
That file should have mode 0600.
Create the directory tree:
serverhost# install -d -o rsyncd -g rsyncd /srv/backups/host1/current /srv/backups/host1/past

Client

On the localhost
localhost# ssh-keygen -f ~/.ssh/rsyncd
Copy the key in the serverhost, e.g. copy/paste between terminals using
localhost# cat ~/.ssh/rsyncd.pub
serverhost# su rsyncd -s /bin/sh -c 'nano -w ~/.ssh/authorized_keys'
Test it
localhost# rsync  -e "ssh -l rsyncd -i /root/.ssh/rsyncd" serverhost::
it should return the list of available modules. Store the generated rsync password in a local password file with mode 0600, for example in /root/.config/simple_rsync_backup/host1.rsync_password:
localhost# install -d -m 700 /root/.config/simple_rsync_backup
localhost# install -m 600 /dev/null /root/.config/simple_rsync_backup/host1.rsync_password

Backup what you want to backup with
localhost# rsync -aHAX --delete -e "ssh -l rsyncd -i /root/.ssh/rsyncd" --password-file /root/.config/simple_rsync_backup/host1.rsync_password /home/ host1@serverhost::host1/
If it works fine, automate it with cron, systemd timers, or another scheduler. For example, if the backup runs as root, put that command in an executable script such as /root/bin/backup_host1_clienthost and link it into /etc/cron.daily/:
localhost# ln -s /root/bin/backup_host1_clienthost /etc/cron.daily/backup_host1

Why is this secure

IMHO this is quite secure, since the server part of the backups is the rsync daemon; the rsync daemon is a service that is worldwide offered on unprotected TCP/IP ports, and I would consider this to be quite secure; in my implementation moreover the rsync daemon is not listening on any public TCP/IP port, so it is quite protected. The other tool used is ssh with pub/private keys, enough said.
If someone hacks in the localhost, s/he may use the ssh key to open the connection to the serverhost, but all they would gain would be access to a rsync daemon; so the most damage they may do would be to the backupped files.

multiple local host backups

There are two layers of authentication, first layer by the ssh key, second possible layer by a rsync username/password. This method may be used to give backup access to multiple hosts, and/or to different users in the hosts.
By setting multiple modules with username/passwords in /home/rsyncd/rsyncd.conf, the different backups may be isolated one from the others.

further developments

By adding the setuid/setgid permissions to the script /usr/local/sbin/rsyncd_wrapper, and changing the script a bit, then the rsync daemon may run as root, and so it may chroot; I have no idea if this would make it all more secure, or less secure.

Other sources

I read some people ideas before developing my own. Some random comments.
Valid HTML 4.01 Transitional