aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 5281a015d82a8c670805dc7c5dd849e3097a7a3b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# 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

```mermaid
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

```sh
ansible-galaxy role install kliwniloc.borgbackup
```

```yaml
# requirements.yml
- src: kliwniloc.borgbackup
```

via git

```sh
ansible-galaxy role install git+https://github.com/kliwniloc/ansible-role-borgbackup.git,master
```

```yaml
# requirements.yml
- src: https://github.com/kliwniloc/ansible-role-borgbackup
  version: master
  name: kliwniloc.borgbackup
```

## Role Variables

For more details, see: [`defaults/main.yml`](defaults/main.yml)

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_host` is used by Ansible to delegate tasks to the Borg server
> - `borg_server_host_url` is used by the client to connect to the Borg server

```yaml
borg_server_host: "" # Required
borg_server_host_ssh_key: "" # Required
borg_server_host_url: { { borg_server_host } } # Optional
```

The role creates a user for the Borg repositories on the Borg server. You can
specify the location of the home directory with `borg_server_user_home`, which
will also be the location for the backups.

```yaml
borg_server_user_home: /opt/borg
```

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.

```yaml
borg_repo_name: "{{ inventory_hostname }}"
borg_backup_name_format: "{hostname}-{now:%Y-%m-%dT%H:%M:%S}"
borg_mode_append_only: false # Server side append only config
```

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_passphrase`of
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](https://borgbackup.readthedocs.io/en/stable/internals/security.html#offline-key-security)

<!-- TODO: Include keyfile backup mode -->

`borg_included_dirs` and `borg_excluded_dirs` finally picks what directories
will be backed up by Borg.

```yaml
borg_compression: zstd # -C argument
borg_passphrase: "" # Env variable
borg_included_dirs: [] # Positional arguments
borg_excluded_dirs: [] # --exclude arguments
```

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.

```yaml
borg_decryption_keys_yaml_path: "{{ inventory_dir }}/decryption_keys.yml"
```

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`.

For the backup scripts we add `{{ borg_backup_script_location }}` for creating a
backup on all specified targets and
`{{ borg_backup_script_location }}{{ borg_backup_argument }}` for backing up to
each target.

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).

```yaml
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

```yaml
- 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

```yaml
- 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.

<!-- TODO: (next major version): Change default borg_backup_argument to include
     borg_repo_name for uniqueness:
     "{{ borg_server_host_url }}-{{ borg_repo_name }}".
     This will be a breaking change requiring migration of existing systemd
     units. -->

```yaml
- 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/configs` and `/opt/borg/home-data`
- Two systemd timers: `borg_backup@configs.timer` and `borg_backup@home-data.timer`
- A single SSH keypair shared between both repositories
- A single `authorized_keys` entry with both repositories in `--restrict-to-repository`

## Important Behaviors and Limitations

### Consistent `--append-only` Setting Required

All repositories for a given host must use the same `borg_mode_append_only`
setting. The role will fail with an error if you attempt to configure
repositories with conflicting `--append-only` settings for the same host.

This is because the SSH `authorized_keys` entry uses a single `--append-only`
flag that applies to all repositories accessible via that key.

```yaml
# 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
```

### Single SSH Key per Host\*\*

The role generates one SSH keypair per client host. All repositories for that
host share the same SSH key for authentication to the Borg server.

### Decryption Keys Storage Format

Decryption keys are stored in the `decryption_keys.yml` file using the format:

```yaml
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_configs`
- `borg-client_home-data`

### Deprovisioning Borg Repositories

This role does not currently handle deprovisioning or removing repositories.
Deprovisioning must be done manually or via separate playbooks.

### Removing a Repository

To remove a specific repository (applies to both single and multi-instance setups):

1. 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.

2. Remove the backup script:

   ```bash
   rm /usr/local/bin/run_borg_backup@ARGUMENT
   ```

3. _(Multi-instance only)_ Edit the base backup script to remove the repository block:

   ```bash
   # Edit /usr/local/bin/run_borg_backup
   # Remove the ANSIBLE MANAGED BLOCK for the deleted repository
   ```

4. _(If other repositories remain)_ Update `authorized_keys` on the Borg server
   to remove the repository restriction. Edit `/opt/borg/.ssh/authorized_keys`
   and remove the `--restrict-to-repository /opt/borg/REPONAME` from the
   appropriate line.

   If this is the last repository for the host, remove the entire line instead.

5. 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"
   ```

6. Remove the decryption key entry from your `decryption_keys.yml`.

## Deprovisioning a Complete Host

To remove all backup configuration for a host:

1. Stop and disable all systemd timers and services:

   ```bash
   systemctl list-units --type=timer --all | grep borg_backup
   # For each relevant timer:
   systemctl stop borg_backup@*.timer
   systemctl disable borg_backup@*.timer
   ```

2. Remove all backup scripts:

   ```bash
   rm -f /usr/local/bin/run_borg_backup*
   ```

3. Remove the SSH key from the Borg server's `authorized_keys`:

   ```bash
   # On the Borg server, edit /opt/borg/.ssh/authorized_keys
   # Remove the line containing root@HOSTNAME
   ```

4. Delete all repositories for the host on the Borg server:

   ```bash
   ssh borg@SERVER "rm -rf /opt/borg/HOSTNAME"
   # For multi-instance:
   ssh borg@SERVER "rm -rf /opt/borg/HOSTNAME_repo1"
   ssh borg@SERVER "rm -rf /opt/borg/HOSTNAME_repo2"
   ```

5. Remove all decryption key entries for the host from your
   `decryption_keys.yml`.

## Dependencies

None.

## License

MIT