Backup files with special permissions

2020 June 4

This article is the third of a four part series describing my current backup system:

  1. In the first article I explain how I use restic to perform my backups.
  2. In the second article I describe how I use Healthchecks and Gotify to verify that the backups run periodically.
  3. In this article I show how I automate the backups, in particular when I need to read files with special permissions.
  4. Finally in the last article I detail the process I had to follow to backup a remote server to a machine on my home local network.

Script setup

Before diving into the use cases, let's discuss how the scripts are automated. To run my backups periodically I use the cron job scheduler. I schedule a job that runs a python script which implements the logic discussed in the second article.

Since I try my best to apply the DRY principle, my python script backup.py is pretty generic and configurable through a YAML file config.yaml. Moreover, I don't like having my passwords in my config files, so whenever I need to fill in a password, I fill in the path to a file containing the password instead.

Directory structure

The files involved are listed below:

-rwxr-x--- 1 braincoke braincoke backup.py
-rw-r----- 1 braincoke braincoke config.yaml
-rw-r----- 1 braincoke braincoke healthchecks_token
-rw-r----- 1 braincoke braincoke rest_server_password

YAML configuration file

The configuration file contains the information necessary to know:

  • how to ping the healtchecks server
  • which data to backup
  • where the data should be backed up
restic:
  repository: rest:http://192.168.5.5:12000/NUC
  password_file: /absolute/path/to/rest_server_password

healthchecks:
  url: https://my-healthchecks-server.com
  token_file: /absolute/path/to/healthchecks_token

backup:
  folders:
    - /home/user/folder1
    - /home/braincoke/folder2
  files:
    - /var/log/file1
    - /home/braincoke/file2

Python script

You can find the python script I use below:

#!/usr/bin/env python3

import yaml
from string import Template
import sys
import os
import requests

# Read config
with open('config.yaml', 'r') as configfile:
  config = yaml.load(configfile)

# Basic config checks
error_message = Template('YAML configuration missing $section info')

for section in ['healthchecks', 'restic', 'backup']:
  if section not in config:
    sys.exit(error_message.substitute(section=section))

# Load config
healthchecks = config['healthchecks']
with open(healthchecks['token'], 'r') as tokenfile:
  check_token = tokenfile.readline().rstrip('\n').rstrip('\r')

check_url = healthchecks['url'] + "/ping/" + check_token
restic = config['restic']
restic_repo = restic['repository']
restic_password_file = restic['password_file']
folders = config['backup']['folders']
files = config['backup']['files']
backup_list = ' '.join(folders) + ' ' + ' '.join(files)

# Ping start
try:
  requests.get(check_url + "/start")
except requests.exceptions.RequestException:
  # If the network request fails for any reason, we don't want
  # it to prevent the main job from running
  pass

# Run the backup
print(f"Backing up {backup_list}")
exit_code = os.system(f'restic -r {restic_repo} -p {restic_password_file} backup {backup_list}')

# Ping fail
if exit_code != 0:
  requests.get(check_url + "/fail")
else:
  # Ping end
  requests.get(check_url)

I also put it on Github.

Backup with special permissions

At some point I had to backup files that required special privileges. To avoid using the user root to perform the backups I followed the documentation for this use case.

New user: restic

First I created a user restic (the -m creates the home directory):

sudo useradd -m restic

Then I added the restic binary to the user's home:

sudo mkdir /home/restic/bin
sudo curl -L https://github.com/restic/restic/releases/download/v0.9.6/restic_0.9.6_linux_amd64.bz2 -o /home/restic/bin/restic.bz2
sudo bzip2 -d /home/restic/bin/restic.bz2 -f

I restricted the binary privileges to give all permissions to root and read and execute permissions to restic:

sudo chown root:restic /home/restic/bin/restic
sudo chmod 750 /home/restic/bin/restic

Note that this means you have to modify the backup.py script to use /home/restic/bin/restic in the os.system() command

Special capabilities

I finally used setcap to set the linux capability CAP_DAC_READ_SEARCH to the binary.

CAP_DAC_READ_SEARCH : bypass file read permission checks and directory read and execute permission checks.

sudo setcap cap_dac_read_search=+ep /home/restic/bin/restic

I now have a binary capable of reading every file on the system and a user restic capable of using this binary. The only thing left to do is setting up a cronjob as this user.

sudo su - restic
crontab -e

Use sudo passwd restic to set the password for the user restic

Using it in practice

Since the user restic will be running the backup script I have to modify the permissions on the files listed earlier.

The backup script, the config file and the password file should belong to the group restic:

cd ~/scripts
sudo chown :restic backup.py config.yaml healtchecks_token rest_server_password

The group restic only needs read rights over the files, and execute rights over the python script.

chmod 750 backup.py
chmod 640 config.yaml healthchecks_token rest_server_password

The files attributes should now look like this:

-rwxr-x--- 1 braincoke restic backup.py
-rw-r----- 1 braincoke restic config.yaml
-rw-r----- 1 braincoke restic healthchecks_token
-rw-r----- 1 braincoke restic rest_server_password

Finally I can set up the cron job as the restic user:

sudo su - restic
crontab -e

If you want to know how to do a "pull" backup to retrieve files from a server to a local machine behind your NAT, check out the last article.

Resources

I used and studied the following resources while writing this article: