This article is the third of a four part series describing my current backup system:
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.
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
The configuration file contains the information necessary to know:
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
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.
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.
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
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
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.