Ansible Role: BorgBackup
An Ansible role that installs and configures a systemd service for BorgBackup on a client and (delegated) server. This allows you to very easily add backups to your hosts.
The documentation assumes basic knowledge about BorgBackup. You can get up to speed with Borg by reading their excellent documentation on https://borgbackup.readthedocs.io.
How it works
sequenceDiagram
participant Ansible
participant borg-client
participant borg-server
Ansible->>borg-server: Install dependencies
Ansible->>borg-client: Install dependencies
Ansible->>borg-server: Setup Repository
Ansible->>borg-client: Setup Repository
Ansible->>borg-client: Setup backup job
borg-client->>Ansible: Store decryption keys
borg-client-->borg-server: Do Backup
Installation
via ansible-galaxy
ansible-galaxy role install kliwniloc.borgbackup
ansible-galaxy collection install -r collections/requirements.yml
# requirements.yml
- src: kliwniloc.borgbackup
# collections/requirements.yml
collections:
- name: community.crypto
via git
ansible-galaxy role install git+https://github.com/kliwniloc/ansible-role-borgbackup.git,master
ansible-galaxy collection install -r collections/requirements.yml
# requirements.yml
- src: https://github.com/kliwniloc/ansible-role-borgbackup
version: master
name: kliwniloc.borgbackup
Role Variables
Some configuration options were left out of this high level overview. For the
full set of configuration options, see: defaults/main.yml
Backup User Configuration
By default, the role runs all client-side operations as root. To use a
non-root user, set borg_client_user:
borg_client_user: backup
The user must exist before running the role; it will not be created
automatically. Ensure the user can read all directories specified in
borg_included_dirs, or backups will fail at runtime.
Server Configuration
You need to specify your Borg hostname (as defined in your Ansible inventory)
as well as the public SSH key from that Borg SSH server.
You can get this key by scanning the Borg server host like this:
ssh-keyscan -t rsa borg.example.org and removing the hostname, so the output
looks like this:
ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA....
If the Ansible hostname is not reachable from the client host, you must set
borg_server_host_url to a host that is reachable from the Borg clients. It
will default to borg_server_host.
borg_server_hostis used by Ansible to delegate tasks to the Borg serverborg_server_host_urlis used by the client to connect to the Borg server
borg_server_host: "" # Required
borg_server_host_ssh_key: "" # Required
borg_server_host_url: { { borg_server_host } } # Optional
By default, the role uses the borg user on the backup server and creates it
automatically. To use a different user, set borg_server_user:
borg_server_user: backupserver
borg_server_user_home: /var/backups
The borg_server_user_home specifies the home directory of the borg user on
the server. All client repositories will be saved in this directory.
To disable automatic user creation (when the user is managed externally), set:
borg_server_user_create: false
When disabled, the user must exist before running the role.
For the backup itself, there are a few Borg parameters you can configure. Not all options that Borg offers are available in the Ansible role yet. For more configuration options, see https://borgbackup.readthedocs.io.
borg_repo_name configures the name of the repository as it is created on the
Borg server in the borg_server_user_home directory. By default, we use the
inventory hostname, which is fine if you don't use multiple Borg repositories on
the same client and server hosts.
The individual backup names can be customized in the borg_backup_name_format
variable. You can use placeholders such as {hostname} for the backups, see:
https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-placeholders.
borg_mode_append_only restricts the deletion of the backups from the client.
This can increase security but comes at the cost of not being able to clean up
old backups from the client.
borg_storage_quota limits the storage space used by the repository on the
Borg server. Format: N (bytes), NK (kilobytes), NM (megabytes), NG
(gigabytes), NT (terabytes). Empty string means no quota.
borg_repo_name: "{{ inventory_hostname }}"
borg_backup_name_format: "{hostname}-{now:%Y-%m-%dT%H:%M:%S}"
borg_mode_append_only: false
borg_storage_quota: "" # e.g., "100G" for 100 gigabytes
We use zstd compression by default, but you can change it to any of the supported modes see: https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-compression.
You can configure a passphrase for the encryption keys in borg_passphraseof
your Borg backups, but it is not used by default.
[!WARNING] Enable a passphrase for true end-to-end encryption. If you want true end-to-end encryption, you should configure a passphrase Read more: BorgBackup Security
borg_included_dirs and borg_excluded_dirs finally picks what directories
will be backed up by Borg. borg_included_dirs must not be empty when
state: present.
borg_compression: zstd # -C argument
borg_create_additional_arguments: "" # Additional borg create options
borg_passphrase: "" # Env variable
borg_included_dirs: [] # Positional arguments
borg_excluded_dirs: [] # --exclude arguments
borg_create_additional_arguments lets you pass additional options to borg
create. These arguments are appended after all role-defined options, so
user-specified options can override defaults. For example:
# Add verbose output
borg_create_additional_arguments: "--stats --list --filter=AME"
# Stay in one filesystem and exclude caches
borg_create_additional_arguments: "--one-file-system --exclude-caches"
Prune and Compact Configuration
The role can manage Borg retention with borg prune and optionally run borg
compact afterwards to actually free repository disk space.
Enable pruning with borg_prune_enabled. Pruning always runs in a dedicated
systemd service. By default, borg_prune_trigger is after_backup, so a
successful backup service starts the prune service. The backup script itself
only runs borg create; the prune script runs borg prune and optionally
borg compact.
If you want prune on its own schedule, set borg_prune_trigger: timer. That
creates a dedicated prune timer using borg_prune_timer_name,
borg_prune_systemd_oncalendar, and borg_prune_systemd_accuracysec to start
the same prune service.
You can set an archive filter via borg_prune_glob_archives so prune only
touches the archive series for this backup job.
borg_prune_enabled: true
borg_prune_trigger: after_backup
borg_prune_glob_archives: "{hostname}-*"
borg_prune_keep_daily: "7"
borg_prune_keep_weekly: "4"
borg_prune_keep_monthly: "6"
borg_prune_keep_yearly: "1"
borg_prune_compact_enabled: true
borg_compact_threshold: 10
Example with a separate weekly prune timer:
borg_prune_enabled: true
borg_prune_trigger: timer
borg_prune_systemd_oncalendar: "Sun *-*-* 04:00:00"
borg_prune_systemd_accuracysec: 60min
borg_prune_glob_archives: "{hostname}-*"
borg_prune_keep_daily: "7"
borg_prune_keep_weekly: "8"
borg_prune_keep_monthly: "12"
borg_prune_compact_enabled: true
borg_compact_threshold: 1
For the full list of supported variables, see:
defaults/main.yml as well as the relevant Borg
documentation:
- https://borgbackup.readthedocs.io/en/stable/usage/prune.html
- https://borgbackup.readthedocs.io/en/stable/usage/compact.html
[!INFO] Prune and compact are not compatible with append-only repositories from the client side. The role will fail early if you enable both
borg_prune_enabled: trueandborg_mode_append_only: true.
To decrypt your backups without the client, we store the decryption keys in a YAML file in your Ansible repository. You require the decryption keys as well as access to the repository files on the Borg server to access the backups. If you wish to add another layer of security for these decryption keys, consider using git encryption tools like https://github.com/AGWA/git-crypt or storing the keys in a secure location outside of git.
borg_decryption_keys_yaml_path: "{{ inventory_dir }}/decryption_keys.yml"
Set to an empty string to disable exporting decryption keys entirely:
borg_decryption_keys_yaml_path: ""
The scheduling of the backups is done via systemd. For this purpose a systemd service and a corresponding timer are created. Additionally, we create backup scripts for use in the systemd service and for the ability to easily trigger manual backups.
To support multiple backups (with different schedules), we have the
borg_backup_argument which defaults to {{ borg_server_host_url }} and should
be unique per backup target.
The names of the systemd service and timer are:
{{ borg_backup_timer_name }}{{ borg_backup_argument }}.service and
{{ borg_backup_timer_name }}{{ borg_backup_argument }}.timer.
Each target gets exactly one repository-specific backup script at
{{ borg_backup_script_location }}@{{ borg_backup_argument }}. Systemd executes
that script, and the same script can be called manually. The role does not
create an aggregate script that runs all configured targets. If
borg_backup_argument is empty, the script uses the unsuffixed
borg_backup_script_location path.
To configure the backup schedule, we offer borg_systemd_oncalendar and
borg_systemd_accuracysec, which map to the corresponding systemd options,
see man-page systemd.timer(5).
borg_backup_script_location: /usr/local/bin/run_borg_backup
borg_backup_timer_name: borg_backup
borg_backup_service_name: borg_backup
borg_backup_argument: "{{ borg_server_host_url }}"
borg_systemd_oncalendar: "*-*-* 02:00:00"
borg_systemd_accuracysec: 60min
Example Playbooks
Simple Playbook
- name: >-
Add Borg backup to the borg-client and save the
decryption keys alongside the playbook
hosts: borg-client
roles:
- role: kliwniloc.borgbackup
borg_server_host: borg-server
borg_server_host_ssh_key: ssh-rsa AAAAAAAA...
borg_decryption_keys_yaml_path: "{{ playbook_dir }}/decryption_keys.yml"
borg_included_dirs:
- /home
- /opt/project
Multiple Backup Servers
- name: >-
Add Borg backup with multiple borg servers to the borg-client and
save the decryption keys alongside the playbook
hosts: borg-client
vars:
borg_included_dirs:
- /home
- /opt/project
roles:
- role: kliwniloc.borgbackup
borg_server_host: borg-server1
borg_server_host_ssh_key: ssh-rsa AAAAAAAA...
- role: kliwniloc.borgbackup
borg_server_host: borg-server2
borg_server_host_ssh_key: ssh-rsa BBBBBBBB...
Multiple Repositories on Single Server, Single Client (Multi-Instance)
[!NOTE] This is considered a beta feature This feature is still being developed and has some quirks around it. We plan to release this properly on the next major version with some breaking changes to make this less of a footgun. Until then, read this section carefully.
When a single client host needs to back up different data sets to the same Borg
server (e.g., system configs and user data), you can run the role multiple times
with different borg_repo_name values.
Important: Each invocation must have a unique borg_backup_argument to create
separate systemd timer units.
- name: Configure multi-instance backup (same host, different repos)
hosts: borg-client
vars:
borg_server_host: borg-server
borg_server_host_ssh_key: ssh-rsa AAAAAAAA...
borg_decryption_keys_yaml_path: "{{ playbook_dir }}/decryption_keys.yml"
roles:
- role: kliwniloc.borgbackup
vars:
borg_repo_name: configs
borg_backup_argument: configs
borg_compression: zstd
borg_included_dirs:
- /etc
borg_systemd_oncalendar: "*-*-* 02:00:00"
- role: kliwniloc.borgbackup
vars:
borg_repo_name: home-data
borg_backup_argument: home-data
borg_compression: lz4
borg_included_dirs:
- /home
borg_excluded_dirs:
- /home/*/.cache
borg_systemd_oncalendar: "*-*-* 04:00:00"
This creates:
- Two repositories on the Borg server:
/opt/borg/configsand/opt/borg/home-data - Two systemd timers:
borg_backup@configs.timerandborg_backup@home-data.timer - A single SSH keypair shared between both repositories
- A single
authorized_keysentry with both repositories in--restrict-to-repository
Important Behaviors and Limitations
Consistent --append-only and --storage-quota Settings Required
All repositories for a given host must use the same borg_mode_append_only and
borg_storage_quota settings when sharing a single SSH key. The role will fail
with an error if you attempt to configure repositories with conflicting settings
for the same host.
This is because the SSH authorized_keys entry uses a single --append-only and
--storage-quota flag that applies to all repositories accessible via that key.
# This will FAIL - conflicting append-only settings
roles:
- role: kliwniloc.borgbackup
vars:
borg_repo_name: configs
borg_mode_append_only: false # ERROR: inconsistent
- role: kliwniloc.borgbackup
vars:
borg_repo_name: home-data
borg_mode_append_only: true # ERROR: inconsistent
# This will FAIL - conflicting storage quota settings
roles:
- role: kliwniloc.borgbackup
vars:
borg_repo_name: configs
borg_storage_quota: 10G # ERROR: inconsistent
- role: kliwniloc.borgbackup
vars:
borg_repo_name: home-data
borg_storage_quota: 50G # ERROR: inconsistent
Per-Repo SSH Keys for Independent Settings
To use different --append-only or --storage-quota settings per repository,
enable borg_ssh_key_per_repo: true. This generates a unique SSH keypair for
each (server, repo) combination, allowing each repository to have its own
authorized_keys entry with independent settings.
- name: Configure repos with independent settings using per-repo SSH keys
hosts: borg-client
vars:
borg_server_host: borg-server
borg_server_host_ssh_key: ssh-rsa AAAAAAAA...
borg_ssh_key_per_repo: true
roles:
- role: kliwniloc.borgbackup
vars:
borg_repo_name: configs
borg_backup_argument: configs
borg_mode_append_only: true
borg_storage_quota: 10G
borg_included_dirs:
- /etc
- role: kliwniloc.borgbackup
vars:
borg_repo_name: home-data
borg_backup_argument: home-data
borg_mode_append_only: false # OK: different key
borg_storage_quota: 50G # OK: different key
borg_included_dirs:
- /home
This creates:
- Two separate SSH keypairs:
id_ed25519_borgbackup_borg_server_configsandid_ed25519_borgbackup_borg_server_home_data - Two separate
authorized_keysentries with independent restrictions so that each repository can have its own--append-onlyand--storage-quotasettings
Decryption Keys Storage Format
Decryption keys are stored in the decryption_keys.yml file using the format:
hostname_repo_name: |
To restore key use borg key import --paper /path/to/repo
BORG PAPER KEY v1
...
For hosts with multiple repos, each repository has its own entry:
borg-client_configsborg-client_home-data
Deprovisioning Borg Repositories
Remove a repository's configuration without deleting the backup data:
- role: kliwniloc.borgbackup
vars:
borg_server_host: borg-server
borg_repo_name: my-backup
state: absent
This will not delete the repository data on the backup server.
Deleting Repository Data
To also delete the repository data on the backup server:
- role: kliwniloc.borgbackup
vars:
borg_server_host: borg-server
borg_repo_name: my-backup
state: absent
borg_dangerously_delete_backups: true # WARNING destructive!
[!WARNING]
borg_dangerously_delete_backups: truepermanently deletes all backup data from the server. Use with caution.
Multi-Instance Considerations
When removing one repository from a multi-instance setup:
- Shared SSH keys (
borg_ssh_key_per_repo: false) are kept authorized_keysis updated to remove only the deleted repo restriction- Other repositories remain unaffected
Example - Remove one repo from multi-instance:
- role: kliwniloc.borgbackup
vars:
borg_server_host: borg-server
borg_repo_name: configs
borg_backup_argument: configs
state: absent
# Other repos using same SSH key will continue to work
Limitations
The following components are NOT removed by state: absent:
| Component | Reason |
|---|---|
borgbackup package |
May be required by other backups or system |
Server user (borg_server_user) |
May be used by other backup clients |
| Server home directory | Contains other repositories |
| Shared SSH keys | Other repos still depend on them |
| SSH known_hosts entries | May be used for other purposes |
Per-Repo SSH Key Behavior
When borg_ssh_key_per_repo: true:
- The specific SSH key for the repo is deleted
- The corresponding
authorized_keysline is removed
When borg_ssh_key_per_repo: false:
- Shared SSH key is kept
authorized_keysline is updated to remove repo restriction- If the deleted repo is the last one, the entire line is removed
Manual Deprovisioning
To remove a specific repository (applies to both single and multi-instance setups):
- Stop and disable the systemd timer and service:
bash
systemctl stop borg_backup@ARGUMENT.timer
systemctl disable borg_backup@ARGUMENT.timer
systemctl stop borg_backup@ARGUMENT.service
systemctl disable borg_backup@ARGUMENT.service
Replace ARGUMENT with the borg_backup_argument value for that repository.
- Remove the backup script:
bash
rm /usr/local/bin/run_borg_backup@ARGUMENT
- (If other repositories remain) Update
authorized_keyson the Borg server to remove the repository restriction. Edit/opt/borg/.ssh/authorized_keysand remove the--restrict-to-repository /opt/borg/REPONAMEfrom the appropriate line.
If this is the last repository for the host, remove the entire line instead.
- Delete the repository on the Borg server:
bash
ssh borg@SERVER "borg delete /opt/borg/REPONAME"
# Or simply: ssh borg@SERVER "rm -rf /opt/borg/REPONAME"
- Remove the decryption key entry from your
decryption_keys.yml.
Dependencies
This role requires the community.crypto collection for ssh key generation.
License
MIT