diff --git a/docs/ops/backup-recovery.rst b/docs/ops/backup-recovery.rst index a39c96dfa859203d6b54c1812e70414715b920e9..2201fcb7ab7463a73cd30730b01c045b5c85d72e 100644 --- a/docs/ops/backup-recovery.rst +++ b/docs/ops/backup-recovery.rst @@ -113,3 +113,65 @@ Recovery #. Clean up the remote copies of the backup file :: [REMOTE]$ rm -iv recovery.tar.bz2 + +Storage Directories +~~~~~~~~~~~~~~~~~~~ + +The user ciphertext is backed up using `Borg backup <https://borgbackup.readthedocs.io/>`_ to a separate location - currently a SaaS backup storage service (`borgbase.com <https://borgbase.com>`_). + +Borg backup uses a *RepoKey* secured by a *passphrase* to encrypt the backup data and an *SSH key* to authenticate against the backup storage service. +Each Borg backup job requires one *backup repository*. + +The backups are automatically checked periodically. + +SSH keys +```````` + +Borgbase `recommends creating ed25519 ssh keys with one hundred KDF rounds <https://www.borgbase.com/ssh>`_. +We create one key pair per grid (not per host):: + + $ ssh-keygen -f borgbackup-appendonly-staging -t ed25519 -a 100 + $ ssh-keygen -f borgbackup-appendonly-production -t ed25519 -a 100 + +Save the key without a passphrase and upload the public part to `Borgbase SSH keys <https://www.borgbase.com/ssh>`_. + +Passphrase +`````````` + +Make up a passphrase to encrypt our repository key with. Use computer help if you like:: + + nix-shell --packages pwgen --command 'pwgen --secure 83 1' # 83 is the year I was born. Very random. + +Create & initialize the backup repository +````````````````````````````````````````` + +Borgbase.com offers a `borgbase.com GraphQL API <https://docs.borgbase.com/api/>`_. +Since our current number of repositories is small we save time by creating the repositories by clicking a few buttons in the `borgbase.com Web Interface <https://www.borgbase.com/repositories>`_: + +* Set up one repository per backup job. +* Set the *Repository Name* to the FQDN of the host to be backed up. +* Add the SSH key created earlier as *Append-Only Access* key. +* Leave the other settings at their defaults. + +Then initialize those repositories with our chosen parameters:: + + export BORG_PASSCOMMAND="cat borgbackup-passphrase-staging" + export BORG_RSH="ssh -i borgbackup-appendonly-staging" + borg init -e repokey-blake2 xyxyx123@xyxyx123.repo.borgbase.com:repo + +Reliability checks +`````````````````` + +Borg handles large amounts of data. +Given enough bits rare, spurious bit flips become a problem. +That is why regular runs of ``borg check`` are recommended +(see the `borgbase FAQ <https://docs.borgbase.com/faq/#how-often-should-i-run-borg-check>`_). + + + +Recovery +```````` + +Borg offers various methods to restore backups. +A very convenient method is to mount a backup set using FUSE. +Please consult the restore documentation at `Borgbase <https://docs.borgbase.com/restore/>`_ and `Borg <https://borgbackup.readthedocs.io/en/stable/usage/mount.html>`_. diff --git a/morph/grid/local/private-keys/borgbackup.passphrase b/morph/grid/local/private-keys/borgbackup.passphrase new file mode 100644 index 0000000000000000000000000000000000000000..c66b798a01fd511ae71532452a6933a2e5c709d1 --- /dev/null +++ b/morph/grid/local/private-keys/borgbackup.passphrase @@ -0,0 +1 @@ +The most interesting passphrase in the world. diff --git a/morph/grid/local/private-keys/borgbackup.ssh-key b/morph/grid/local/private-keys/borgbackup.ssh-key new file mode 100644 index 0000000000000000000000000000000000000000..d63f7803d35ef226f5c20a2a7944051627b56d86 --- /dev/null +++ b/morph/grid/local/private-keys/borgbackup.ssh-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +ratatatratatatratatatratatatratatatratatatratatatratatatratatatratatat +ratatatratatatratatatratatatratatatratatatratatatratatatratatatratatat +ratatatratatatratatatratatatratatatratatatratatatratatatratatatratatat +ratatatratatatratatatratatatratatatratatatratatatratatatratatatratatat +ratatatratatatratatatratatatratatatratatatc= +-----END OPENSSH PRIVATE KEY----- diff --git a/morph/grid/local/public-keys/borgbackup/storage1.repopath b/morph/grid/local/public-keys/borgbackup/storage1.repopath new file mode 100644 index 0000000000000000000000000000000000000000..5f5df8d1954eeefee7e733783d4d9f92571e199d --- /dev/null +++ b/morph/grid/local/public-keys/borgbackup/storage1.repopath @@ -0,0 +1 @@ +abc123de@abc123de.repo.borgbase.com:repo diff --git a/morph/grid/local/public-keys/borgbackup/storage2.repopath b/morph/grid/local/public-keys/borgbackup/storage2.repopath new file mode 100644 index 0000000000000000000000000000000000000000..e73b10d01f5502f943a0d9bce220cbdf758d113f --- /dev/null +++ b/morph/grid/local/public-keys/borgbackup/storage2.repopath @@ -0,0 +1 @@ +vwx789yz@vwx789yz.repo.borgbase.com:repo diff --git a/morph/grid/production/public-keys/borgbackup/storage001.repopath b/morph/grid/production/public-keys/borgbackup/storage001.repopath new file mode 100644 index 0000000000000000000000000000000000000000..e3342643a66dfaaedee7071c511e6d5ff4fc97f2 --- /dev/null +++ b/morph/grid/production/public-keys/borgbackup/storage001.repopath @@ -0,0 +1 @@ +gye1flhy@gye1flhy.repo.borgbase.com:repo diff --git a/morph/grid/production/public-keys/borgbackup/storage002.repopath b/morph/grid/production/public-keys/borgbackup/storage002.repopath new file mode 100644 index 0000000000000000000000000000000000000000..e2a6ec180927dc73eb5c6c6e790f45ccededac67 --- /dev/null +++ b/morph/grid/production/public-keys/borgbackup/storage002.repopath @@ -0,0 +1 @@ +l4642x1g@l4642x1g.repo.borgbase.com:repo diff --git a/morph/grid/production/public-keys/borgbackup/storage003.repopath b/morph/grid/production/public-keys/borgbackup/storage003.repopath new file mode 100644 index 0000000000000000000000000000000000000000..26bd44969458dbc933ec0617f0236df176c3834b --- /dev/null +++ b/morph/grid/production/public-keys/borgbackup/storage003.repopath @@ -0,0 +1 @@ +c7400xl6@c7400xl6.repo.borgbase.com:repo diff --git a/morph/grid/production/public-keys/borgbackup/storage004.repopath b/morph/grid/production/public-keys/borgbackup/storage004.repopath new file mode 100644 index 0000000000000000000000000000000000000000..cc1e8017b31ceaf80a7d6938e11c4d8c12db5592 --- /dev/null +++ b/morph/grid/production/public-keys/borgbackup/storage004.repopath @@ -0,0 +1 @@ +sbn13vf8@sbn13vf8.repo.borgbase.com:repo diff --git a/morph/grid/production/public-keys/borgbackup/storage005.repopath b/morph/grid/production/public-keys/borgbackup/storage005.repopath new file mode 100644 index 0000000000000000000000000000000000000000..74b928d15fd781fd2e7ed61e0d0e02a8cee7697a --- /dev/null +++ b/morph/grid/production/public-keys/borgbackup/storage005.repopath @@ -0,0 +1 @@ +wg8x4po7@wg8x4po7.repo.borgbase.com:repo diff --git a/morph/grid/testing/public-keys/borgbackup/storage001.repopath b/morph/grid/testing/public-keys/borgbackup/storage001.repopath new file mode 100644 index 0000000000000000000000000000000000000000..27c74225521a74fa6a8e60e19f44d0968f7c816b --- /dev/null +++ b/morph/grid/testing/public-keys/borgbackup/storage001.repopath @@ -0,0 +1 @@ +p2kt6691@p2kt6691.repo.borgbase.com:repo diff --git a/morph/lib/borgbackup.nix b/morph/lib/borgbackup.nix new file mode 100644 index 0000000000000000000000000000000000000000..e71ca55ef90bb3aa50efb3d48b4cd6268a18daf7 --- /dev/null +++ b/morph/lib/borgbackup.nix @@ -0,0 +1,81 @@ +# Importing this adds a daily borg backup job to a node. +# It has all the common config and keys, and can be configured +# to back up more (or entirely different) folders. + + +{ lib, config, pkgs, ...}: +let + cfg = config.services.private-storage.borgbackup; + inherit (config.grid) publicKeyPath privateKeyPath; + + # Get a per-host number so backup jobs don't all run at the + # same time. + ip-util = import ../../nixos/lib/ip-util.nix; + backupDelay = with builtins; bitAnd (ip-util.fromHexString + (hashString "md5" config.networking.hostName)) 15; + +in { + options.services.private-storage.borgbackup = { + enable = lib.mkEnableOption "Borgbackup daily backup job"; + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + A list of directories to back up using Borg. + ''; + default = [ "/storage" ]; + }; + }; + + config = lib.mkIf cfg.enable { + deployment = { + secrets = { + "borgbackup-passphrase" = { + # The passphrase is used to encrypt the repo key + # https://borgbackup.readthedocs.io/en/stable/usage/init.html + destination = "/run/keys/borgbackup/passphrase"; + source = "${privateKeyPath}/borgbackup.passphrase"; + }; + "borgbackup-appendonly-ssh-key" = { + # The ssh key is used to authenticate to the remote repo server + destination = "/run/keys/borgbackup/ssh-key"; + source = "${privateKeyPath}/borgbackup.ssh-key"; + }; + }; + }; + + services.borgbackup.jobs = { + daily = { + paths = cfg.paths; + repo = lib.fileContents "${publicKeyPath}/borgbackup/${config.networking.hostName}.repopath"; + doInit = false; + encryption = { + mode = "repokey-blake2"; + passCommand = "cat /run/keys/borgbackup/passphrase"; + }; + environment = { + BORG_RSH = "ssh -i /run/keys/borgbackup/ssh-key"; + }; + + # Ciphertext doesn't compress well + compression = "none"; + + # Start the backup at a different time per machine, + # and not at the full hour, but somewhat later + startAt = "*-*-* " + toString backupDelay + ":22:11 UTC"; + }; + }; + + # Check repo once a month + systemd.services.borgbackup-check-repo = { + # Once a month, 3h after last backup started + startAt = "*-*-" + toString backupDelay + " 18:33:22 UTC"; + path = [ pkgs.borgbackup ]; + environment = { + BORG_PASSCOMMAND = "cat /run/keys/borgbackup/passphrase"; + BORG_RSH = "ssh -i /run/keys/borgbackup/ssh-key"; + BORG_REPO = lib.fileContents "${publicKeyPath}/borgbackup/${config.networking.hostName}.repopath"; + }; + script = ''${pkgs.borgbackup}/bin/borg check''; + }; + }; +} diff --git a/morph/lib/default.nix b/morph/lib/default.nix index f236b8cada99b71cd1c5ab851f3c081421c4b717..c99c19a57e45a27e585830a8dfff95fa3d9d2efb 100644 --- a/morph/lib/default.nix +++ b/morph/lib/default.nix @@ -10,6 +10,7 @@ issuer = import ./issuer.nix; storage = import ./storage.nix; monitoring = import ./monitoring.nix; + borgbackup = import ./borgbackup.nix; modules = builtins.toString ../../nixos/modules; diff --git a/morph/lib/storage.nix b/morph/lib/storage.nix index 83c12f55cc077abb683482b8435dbcbd5025be10..1fd8c26ce3db81ae405025d97abe6068ce2a6903 100644 --- a/morph/lib/storage.nix +++ b/morph/lib/storage.nix @@ -7,6 +7,7 @@ in { # Any extra NixOS modules to load on this server. imports = [ ./monitoringvpn-client.nix + ./borgbackup.nix ]; options.grid.storage = { @@ -46,6 +47,8 @@ in { services.private-storage.monitoring.exporters.node.enable = true; services.private-storage.monitoring.exporters.tahoe.enable = true; + services.private-storage.borgbackup.enable = true; + # Turn on the Private Storage (Tahoe-LAFS) service. services.private-storage = { # Yep. Turn it on. diff --git a/nixos/lib/ip-util.nix b/nixos/lib/ip-util.nix new file mode 100644 index 0000000000000000000000000000000000000000..9d9c47890ae9a0f7b49a946422248a243ce8692e --- /dev/null +++ b/nixos/lib/ip-util.nix @@ -0,0 +1,219 @@ +# Thank you: https://gist.github.com/petabyteboy/558ffddb9aeb24e1eab2d5d6d021b5d7 + +with import <nixpkgs/lib>; + +rec { + # FIXME: add case for negative numbers + pow = base: exponent: if exponent == 0 then 1 else fold ( + x: y: y * base + ) base ( + range 2 exponent + ); + + fromHexString = hex: foldl ( + x: y: 16 * x + ( + ( + listToAttrs ( + map ( + x: nameValuePair ( + toString x + ) x + ) ( + range 0 9 + ) + ) // { + "a" = 10; + "b" = 11; + "c" = 12; + "d" = 13; + "e" = 14; + "f" = 15; + } + ).${y} + ) + ) 0 ( + stringToCharacters ( + removePrefix "0x" ( + hex + ) + ) + ); + + ipv4 = rec { + + decode = address: foldl ( + x: y: 256 * x + y + ) 0 ( + map toInt ( + splitString "." address + ) + ); + + encode = num: concatStringsSep "." ( + map ( + x: toString (mod (num / x) 256) + ) ( + reverseList ( + genList ( + x: pow 2 (x * 8) + ) 4 + ) + ) + ); + + netmask = prefixLength: ( + foldl ( + x: y: 2 * x + 1 + ) 0 ( + range 1 prefixLength + ) + ) * ( + pow 2 ( + 32 - prefixLength + ) + ); + + reverseZone = net: ( + concatStringsSep "." ( + reverseList ( + splitString "." net + ) + ) + ) + ".in-addr.arpa"; + + eachAddress = net: prefixLength: genList ( + x: decode ( + x + ( + decode net + ) + ) + ) ( + pow 2 ( + 32 - prefixLength + ) + ); + + networkOf = address: prefixLength: encode ( + bitAnd ( + decode address + ) ( + netmask prefixLength + ) + ); + + isInNetwork = net: address: networkOf address == net; + + /* nixos-specific stuff */ + + findOwnAddress = config: net: head ( + filter ( + isInNetwork net + ) ( + configuredAddresses config + ) + ); + + configuredAddresses = config: concatLists ( + mapAttrsToList ( + name: iface: iface.ipv4.addresses + ) config.networking.interfaces + ); + + }; + + ipv6 = rec { + + expand = address: ( + replaceStrings ["::"] [( + concatStringsSep "0" ( + genList (x: ":") ( + 9 - (count (x: x == ":") (stringToCharacters address)) + ) + ) + )] address + ) + ( + if hasSuffix "::" address then + "0" + else + "" + ); + + decode = address: map fromHexString ( + splitString ":" ( + expand address + ) + ); + + encode = address: toLower ( + concatStringsSep ":" ( + map toHexString address + ) + ); + + netmask = prefixLength: map ( + x: if prefixLength > x + 16 then + (pow 2 16) - 1 + else if prefixLength < x then + 0 + else + ( + foldl ( + x: y: 2 * x + 1 + ) 0 ( + range 1 (prefixLength - x) + ) + ) * ( + pow 2 ( + 16 - (prefixLength - x) + ) + ) + ) ( + genList ( + x: x * 16 + ) 8 + ); + + reverseZone = net: ( + concatStringsSep "." ( + concatLists ( + reverseList ( + map ( + x: stringToCharacters (fixedWidthString 4 "0" x) + ) ( + splitString ":" ( + expand net + ) + ) + ) + ) + ) + ) + ".ip6.arpa"; + + networkOf = address: prefixLength: encode ( + zipListsWith bitAnd ( + decode address + ) ( + netmask prefixLength + ) + ); + + isInNetwork = net: address: networkOf address == (expand net); + + /* nixos-specific stuff */ + + findOwnAddress = config: net: head ( + filter ( + isInNetwork net + ) ( + configuredAddresses config + ) + ); + + configuredAddresses = config: concatLists ( + mapAttrsToList ( + name: iface: iface.ipv6.addresses + ) config.networking.interfaces + ); + + }; +}