Skip to content

Network backups (restic)

The darkone.service.restic module backs up fleet hosts with restic 🡕: each host (client) pushes its data to a repository, either local or remote via a zone REST server.

Two roles, one module:

  • REST server: a host that declares the restic service in config.yaml. It stores all hosts’ repositories and listens on the internal network.
  • Client: any host with enable = true that declares targets (backup destinations).
Diagram

Repository layout: one repository per category, named after it.

<root>/<host>/system # category "system" (/ minus exclusions)
<root>/<host>/nfs # category "nfs" (/srv/nfs/<...>)
<root>/<host>/medias # category "medias" (/srv/medias/<...>)
  1. In etc/config.yaml, add the restic service to the server host:

    services:
    restic:
  2. Plug in the backup disk and mount it on /mnt/backup in the host configuration (usr/machines/<host>/), with the nofail option:

    fileSystems."/mnt/backup" = {
    device = "/dev/disk/by-uuid/<uuid>";
    fsType = "ext4";
    options = [ "nofail" ];
    };
    # The REST server only starts once the disk is mounted.
    systemd.services.restic-rest-server = {
    requires = [ "mnt-backup.mount" ];
    after = [ "mnt-backup.mount" ];
    };
  3. The repository is stored under serverDataDir (default /mnt/backup/restic). The firewall opens the REST port on the internal interface automatically.

  4. If other zone clients need to access it via the tailnet, add listenAll: by default the server only listens on the host’s LAN address, invisible from the VPN.

    darkone.service.restic = {
    enable = true;
    enableServer = true;
    listenAll = true;
    };

Declare one or more targets in the host configuration:

darkone.service.restic = {
enable = true;
targets = [
{
# Remote repository via the zone's REST server.
root = "rest:http://restic.home.example.net:8888";
zone = "home";
categories = [ "system" "nfs" ];
}
];
};

target fields:

FieldRole
nameIdentifier (default main), suffix for jobs/units.
rootLocal path or rest:// URL.
zoneZone that selects the restic-password-<zone> passphrase.
categoriesData to back up: system, nfs, medias.

A second target pointing to another zone’s REST server provides the “1” in the 3-2-1 rule. Two precautions specific to off-site backups.

Address the server by its tailnet IP, never by its zone name:

{
name = "offsite";
root = "rest:http://100.64.0.20:8888";
zone = "home";
categories = [ "system" ];
}

A name like restic.<remote-zone>.example.net is only resolved by that zone’s gateway. If the remote site goes down, the resolver goes down with it and the failure appears as a DNS resolution error, which misleads diagnostics. The tailnet IP removes this indirection, along with the dependency on the remote site’s subnet route. It requires listenAll on the server side (see above).

The generation of internal secrets is idempotent: it creates what is missing, never overwrites.

Fenêtre de terminal
just configure-admin-host

It generates, in usr/secrets/secrets.yaml:

  • a restic-password-<zone> passphrase per zone;
  • a restic/<host>/rest-password credential per host in the fleet.

The module creates one systemd service per target × category pair, named restic-backups-<category>-<name> (the <name> comes from the target’s name field, main by default). Examples: restic-backups-system-main, restic-backups-nfs-main.

  1. List the backup services present on the host:

    Fenêtre de terminal
    systemctl list-units 'restic-backups-*'
  2. Trigger an immediate backup without waiting for the timer:

    Fenêtre de terminal
    sudo systemctl start restic-backups-system-main.service
  3. Follow its progress in real time:

    Fenêtre de terminal
    journalctl -fu restic-backups-system-main.service

The REST server runs with private repositories (privateRepos = true): each host only accesses the repository whose path starts with its own hostname. The module automatically aligns the REST account (<hôte>) with the repository prefix (<racine>/<hôte>/...), so isolation is permanent → no configuration needed. An attempt to access another host’s repository returns a 403 error.

On the host in question, the secrets are already in place. List and restore:

Fenêtre de terminal
# Adapter la zone du dépôt (et la catégorie : system, nfs, medias).
ZONE=home
# Variables d'accès REST (générées par sops) et passphrase de dépôt.
set -a; . /run/secrets/rendered/restic-rest-env; set +a
export RESTIC_PASSWORD_FILE=/run/secrets/restic-password-$ZONE
export RESTIC_REPOSITORY=rest:http://restic.$ZONE.example.net:8888/$(hostname)/system
restic snapshots
restic restore latest --target /restore
  1. On a server, one account per fleet host in the htpasswd file:

    Fenêtre de terminal
    sudo cut -d: -f1 /run/restic-rest/htpasswd
  2. On a client, run a manual backup (see above) and check that it completes without errors.

  3. Isolation: from a client, accessing another host’s repository must be denied (401/403 error).