My backup script using btrfs
Find a file
Philip Johansson 5578b342eb flake.lock update:
• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/49f0870db23e8c1ca0b5259734a02cd9e1e371a1?narHash=sha256-F82%2BgS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE%3D' (2025-06-01)
  → 'github:hercules-ci/flake-parts/af66ad14b28a127c5c0f3bbb298218fc63528a18?narHash=sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8%3D' (2025-08-06)
• Updated input 'flake-parts/nixpkgs-lib':
    'github:nix-community/nixpkgs.lib/656a64127e9d791a334452c6b6606d17539476e2?narHash=sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc%3D' (2025-06-01)
  → 'github:nix-community/nixpkgs.lib/0f36c44e01a6129be94e3ade315a5883f0228a6e?narHash=sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA%3D' (2025-07-27)
• Updated input 'git-hooks-nix':
    'github:cachix/git-hooks.nix/80479b6ec16fefd9c1db3ea13aeb038c60530f46?narHash=sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo%2BbnXU9D9k%3D' (2025-05-16)
  → 'github:cachix/git-hooks.nix/4b04db83821b819bbbe32ed0a025b31e7971f22e?narHash=sha256-I0Ok1OGDwc1jPd8cs2VvAYZsHriUVFGIUqW%2B7uSsOUM%3D' (2025-08-17)
• Updated input 'git-hooks-nix/flake-compat':
    'github:edolstra/flake-compat/0f9255e01c2351cc7d116c072cb317785dd33b33?narHash=sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U%3D' (2023-10-04)
  → 'github:edolstra/flake-compat/9100a0f413b0c601e0533d1d94ffd501ce2e7885?narHash=sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX%2BfjA8Xf8PUmqCY%3D' (2025-05-12)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/910796cabe436259a29a72e8d3f5e180fc6dfacc?narHash=sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8%3D' (2025-05-31)
  → 'github:nixos/nixpkgs/20075955deac2583bb12f07151c2df830ef346b4?narHash=sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs%2BStOp19xNsbqdOg%3D' (2025-08-19)
• Updated input 'pyproject-nix':
    'github:pyproject-nix/pyproject.nix/e09c10c24ebb955125fda449939bfba664c467fd?narHash=sha256-QxdHGNpbicIrw5t6U3x%2BZxeY/7IEJ6lYbvsjXmcxFIM%3D' (2025-05-06)
  → 'github:pyproject-nix/pyproject.nix/023cd4be230eacae52635be09eef100c37ef78da?narHash=sha256-QSKpYg%2BTs9HYF155ltlj40iBex39c05cpOF8gjoE2EM%3D' (2025-08-11)
.git/hooks/pre-commit: line 13: /nix/store/1grwmkj3qf897wgxycyymip87i9cvk29-pre-commit-4.0.1/bin/pre-commit: No such file or directory
2025-08-22 12:09:06 +02:00
modules flake improvements 2025-04-02 11:56:00 +02:00
src/restricted_backup updated url and email 2025-03-29 12:55:43 +01:00
.envrc changed to flake-parts 2025-03-26 10:05:19 +01:00
.gitignore changed to flake-parts 2025-03-26 10:05:19 +01:00
default.nix added flake 2024-11-06 21:15:22 +01:00
derivation.nix flake improvements 2025-04-02 11:56:00 +02:00
flake.lock flake.lock update: 2025-08-22 12:09:06 +02:00
flake.nix flake improvements 2025-04-02 11:56:00 +02:00
LICENSE Create LICENSE 2022-08-18 16:17:39 +02:00
pyproject.toml updated url and email 2025-03-29 12:55:43 +01:00
README.md updated documentation 2025-03-10 10:54:08 +01:00

Restricted Backup

This is a script to send either btrfs snapshots or rsync backups to a remote server It uses rrsync to restrict the rsync connection to a certain directory and does something similar with btrfs to restrict those to a certain connection as well.

Installation

git clone https://git.phlipphlop.me/phlipphlop/restricted_backup.git
cd restricted_backup
python -m build
pip install dist/restricted_backup-0.0.1.tar.gz

Nixos

Include this repository as a flake and follow the instructions below

Usage

Server

When run in server mode all commands are run the from the host machine and ssh is used to interact with the script.

  • Decrypt and mount cat backup.key | ssh $user@$ip cryptsetup open
  • Unmount and encrypt ssh $user@$ip cryptsetup close
  • rsync rsync ... ssh $user@$ip:/ use regular rsync command, the destination is restricted. Snapshots are automatically created
  • Receive a btrfs snapshot btrfs send snapshot | ssh $user@$ip btrfs receive $subvolume where subvolume is a subvolume inside the restricted directory
  • Get parent snapshot for sending incremental snapshot ssh $user@$ip btrfs parent

Client

Client mode is supposed to be used directly via the command line or automated to run periodically using your method of choice. Run restricted_backup.py client -h for more info.

Limitations

  • The backup drive must be btrfs formatted
  • The subvolumes that get sent cannot have the same basename

Setup

On backup drive

The backup drive must be formated with the btrfs filesystem. The layout of the subvolumes is unimportant however once mounted all the hostnames of the devices you wish to backup should be present on the root of the backup device. You must therefore create a subvolume for every host that will backup to this drive in the root of the drive as well as a snapshots subvolume to store snapshots created from rsync backups. The snapshots directory can be omitted if only btrfs snapshots are sent and rsync is never used.

The directory structure will be different for devices backed up with rsync or with btrfs. Both can be used in conjunction, however not for the same client.

btrfs

/path/to/mnt/client_hostname/root/ # contains snapshots of subvol named / root
/path/to/mnt/client_hostname/subvol/ # contains snapshots of subvol named subvol

rsync

/path/to/mnt/client_hostname/ # root of backup containing everything from rsync
/path/to/mnt/snapshots/client_hostname/root/ # contains snapshots of client_hostname/
/path/to/mnt/snapshots/client_hostname/home/ # contains snapshots of client_hostname/home

Btrfs subvolumes and their parent directories must be created, the rest are created automatically. It is up to you to decide what should be a directory and what should be a subvolume, snapshots are by definition subvolumes.

On server

The server is the machine that will store the backups on the external drive.

restricted_backup.py is the script that does most of the main work however there is some setup required.

  1. Create a user with a home directory optionally in /var.
  2. Give the user permission to run restricted-backup.py as sudo passwordless and enable editing the environment in sudo. Add to sudoers file: username ALL=(root) NOPASSWD:SETENV: /path/to/restricted_backup.py replace username with the user that you created in the first step.
  3. Configure sshd for the user to force a command. Add the following to the sshd_config file again replacing Username with the user you created in the first step as well as the path where the drive will be mounted.
Match User Username
    ForceCommand sudo -E /var/remote-backup/restricted_backup.py server --your --args --here
    PasswordAuthentication no

  1. Add the ssh-keys of the machines that you want to backup to $HOME/.ssh/authorized_keys.

On Client

No setup on the client is required. Run the script with the argument client and other wanted arguments. Optionally run the script periodically using either cron or a systemd. I leave this setup as an exercise to the user.

Note: This script must be able to read all of the files that are going to be backed up. Easiest is to run as root

Nixos

Include this repository in your flake.nix file

  inputs = {
    restricted_backup = {
      type = "git";
      url = "https://git.phlipphlop.me/phlipphlop/restricted_backup.git";
      inputs.nixpkgs.follows = "nixpkgs"; # optional
    };
  };

Then include it in your imports list

  imports = [
    inputs.restricted_backup.nixosModules.default
  ];

Options

There are a number of options defined for nixos. Client options can be found in modules/backup-client.nix Server options can be found in modules/backup-server.nix