diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..4c128be --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +eval "$(nix print-dev-env)" diff --git a/README.md b/README.md index a95751b..5e43968 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ │ ├── tacyon # rpi 5 │ └── terebithia # oracle cloud aarch64 server ├── modules +│ ├── lib # shared nix utilities +│ │ └── mkService.nix # base service factory │ ├── home # home-manager modules │ │ ├── aesthetics # theming and wallpapers │ │ ├── apps # any app specific config @@ -33,6 +35,8 @@ │ │ └── hyprland │ └── nixos # nixos modules │ ├── apps # also app specific configs +│ ├── services # self-hosted services with automatic backup +│ │ └── restic # backup system (see modules/nixos/services/restic/README.md) │ └── system # pam and my fancy wifi module for now └── secrets # keep your grubby hands (or paws) off my data @@ -242,6 +246,19 @@ atuin login atuin sync ``` +## Backups + +Services are automatically backed up nightly using restic to Backblaze B2. The `atelier-backup` CLI provides an interactive TUI for managing backups: + +```bash +atelier-backup # Interactive menu +atelier-backup status # Show backup status +atelier-backup restore # Restore wizard +atelier-backup dr # Disaster recovery +``` + +See [modules/nixos/services/restic/README.md](modules/nixos/services/restic/README.md) for setup and usage. + ## some odd things for helix if you want the grammar to work you must run the following as per [this helix discussion](https://github.com/helix-editor/helix/discussions/10035#discussioncomment-13852637) diff --git a/machines/terebithia/default.nix b/machines/terebithia/default.nix index 56a6108..8316f5e 100644 --- a/machines/terebithia/default.nix +++ b/machines/terebithia/default.nix @@ -137,6 +137,9 @@ file = ../../secrets/l4.age; owner = "l4"; }; + "restic/env".file = ../../secrets/restic/env.age; + "restic/repo".file = ../../secrets/restic/repo.age; + "restic/password".file = ../../secrets/restic/password.age; }; environment.sessionVariables = { @@ -151,6 +154,7 @@ atelier = { authentication.enable = true; + backup.enable = true; }; networking = { @@ -373,6 +377,23 @@ }; }; + # Backup configuration for tangled services + atelier.backup.services.knot = { + paths = [ "/home/git" ]; # Git repositories managed by knot + exclude = [ "*.log" ]; + # Uses SQLite, stop before backup + preBackup = "systemctl stop knot"; + postBackup = "systemctl start knot"; + }; + + atelier.backup.services.spindle = { + paths = [ "/var/lib/spindle" ]; + exclude = [ "*.log" "cache/*" ]; + # Uses SQLite, stop before backup + preBackup = "systemctl stop spindle"; + postBackup = "systemctl start spindle"; + }; + atelier.services.knot-sync = { enable = true; secretsFile = config.age.secrets.github-knot-sync.path; @@ -421,6 +442,15 @@ ''; }; + # Backup configuration for n8n + atelier.backup.services.n8n = { + paths = [ "/var/lib/n8n" ]; + exclude = [ "*.log" "cache/*" ]; + # n8n uses SQLite, stop before backup + preBackup = "systemctl stop n8n"; + postBackup = "systemctl start n8n"; + }; + boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; boot.kernelParams = [ "console=ttyS0" ]; diff --git a/modules/lib/mkService.nix b/modules/lib/mkService.nix new file mode 100644 index 0000000..5f1702e --- /dev/null +++ b/modules/lib/mkService.nix @@ -0,0 +1,284 @@ +# mkService - Base service factory for atelier services +# +# Creates a standardized NixOS service module with: +# - Common options (domain, port, dataDir, secrets, etc.) +# - Systemd service with git-based deployment +# - Caddy reverse proxy configuration +# - Automatic backup integration via data declarations +# +# Usage in a service module: +# let +# mkService = import ../../lib/mkService.nix; +# in +# mkService { +# name = "myapp"; +# defaultPort = 3000; +# extraOptions = { ... }; +# extraConfig = cfg: { ... }; +# } + +# This file is a function that takes service parameters and returns a NixOS module +{ + # Service identity + name, + description ? "${name} service", + defaultPort ? 3000, + + # Runtime configuration + runtime ? "bun", # "bun" | "node" | "custom" + entryPoint ? "src/index.ts", + startCommand ? null, # Override the start command entirely + + # Additional options specific to this service + extraOptions ? {}, + + # Additional config when service is enabled + # Receives cfg (the service config) as argument + extraConfig ? cfg: {}, +}: + +# Return a proper NixOS module +{ config, lib, pkgs, ... }: + +let + cfg = config.atelier.services.${name}; + + # Generate start command based on runtime + defaultStartCommand = { + bun = "${pkgs.unstable.bun}/bin/bun run ${entryPoint}"; + node = "${pkgs.nodejs_20}/bin/node ${entryPoint}"; + }.${runtime} or ""; + + finalStartCommand = if startCommand != null then startCommand else defaultStartCommand; + +in { + options.atelier.services.${name} = { + enable = lib.mkEnableOption description; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain to serve ${name} on"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = defaultPort; + description = "Port to run ${name} on"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/${name}"; + description = "Directory to store ${name} data"; + }; + + secretsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to agenix secrets file"; + }; + + # Git-based deployment + deploy = { + enable = lib.mkEnableOption "Git-based deployment" // { default = true; }; + + repository = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Git repository URL for auto-deployment"; + }; + + autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; + + branch = lib.mkOption { + type = lib.types.str; + default = "main"; + description = "Git branch to deploy"; + }; + }; + + # Data declarations for automatic backup + data = { + sqlite = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Path to SQLite database (will checkpoint WAL and stop service for backup)"; + example = "/var/lib/myapp/data/app.db"; + }; + + postgres = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "PostgreSQL database name (will use pg_dump for backup)"; + }; + + files = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Additional file paths to backup (no service interruption)"; + example = [ "/var/lib/myapp/uploads" ]; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.log" "node_modules" ".git" "cache" "tmp" ]; + description = "Glob patterns to exclude from backup"; + }; + }; + + # Caddy configuration + caddy = { + enable = lib.mkEnableOption "Caddy reverse proxy" // { default = true; }; + + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + description = "Additional Caddy configuration"; + }; + + rateLimit = { + enable = lib.mkEnableOption "Rate limiting"; + + events = lib.mkOption { + type = lib.types.int; + default = 60; + description = "Number of requests allowed per window"; + }; + + window = lib.mkOption { + type = lib.types.str; + default = "1m"; + description = "Time window for rate limiting"; + }; + }; + }; + + # Environment variables (in addition to secretsFile) + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = {}; + description = "Additional environment variables"; + }; + } // extraOptions; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + # Base service configuration + { + # Create user and group + users.groups.services = {}; + + users.users.${name} = { + isSystemUser = true; + group = name; + extraGroups = [ "services" ]; + home = cfg.dataDir; + createHome = true; + shell = pkgs.bash; + }; + + users.groups.${name} = {}; + + # Allow service user to restart their own service + security.sudo.extraRules = [ + { + users = [ name ]; + commands = [ + { + command = "/run/current-system/sw/bin/systemctl restart ${name}.service"; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; + + # Systemd service + systemd.services.${name} = { + inherit description; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkgs.git ]; + + preStart = lib.optionalString (cfg.deploy.enable && cfg.deploy.repository != null) '' + # Clone repository if not present + if [ ! -d ${cfg.dataDir}/app/.git ]; then + ${pkgs.git}/bin/git clone -b ${cfg.deploy.branch} ${cfg.deploy.repository} ${cfg.dataDir}/app + fi + + cd ${cfg.dataDir}/app + '' + lib.optionalString (cfg.deploy.enable && cfg.deploy.autoUpdate) '' + ${pkgs.git}/bin/git fetch origin + ${pkgs.git}/bin/git reset --hard origin/${cfg.deploy.branch} + '' + lib.optionalString (runtime == "bun") '' + + if [ -f package.json ]; then + echo "Installing dependencies..." + ${pkgs.unstable.bun}/bin/bun install + fi + '' + lib.optionalString (runtime == "node") '' + + if [ -f package.json ]; then + echo "Installing dependencies..." + ${pkgs.nodejs_20}/bin/npm ci --production + fi + ''; + + serviceConfig = { + Type = "simple"; + User = name; + Group = name; + WorkingDirectory = "${cfg.dataDir}/app"; + EnvironmentFile = lib.mkIf (cfg.secretsFile != null) cfg.secretsFile; + Environment = [ + "NODE_ENV=production" + "PORT=${toString cfg.port}" + ] ++ (lib.mapAttrsToList (k: v: "${k}=${v}") cfg.environment); + ExecStart = "${pkgs.bash}/bin/bash -c '${finalStartCommand}'"; + Restart = "always"; + RestartSec = "10s"; + + # Security hardening + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ cfg.dataDir ]; + PrivateTmp = true; + }; + + serviceConfig.ExecStartPre = [ + "+${pkgs.writeShellScript "${name}-setup" '' + mkdir -p ${cfg.dataDir}/app + mkdir -p ${cfg.dataDir}/data + chown -R ${name}:services ${cfg.dataDir} + chmod -R g+rwX ${cfg.dataDir} + ''}" + ]; + }; + + # Caddy reverse proxy + services.caddy.virtualHosts.${cfg.domain} = lib.mkIf cfg.caddy.enable { + extraConfig = '' + tls { + dns cloudflare {env.CLOUDFLARE_API_TOKEN} + } + + ${lib.optionalString cfg.caddy.rateLimit.enable '' + rate_limit { + zone ${name}_limit { + key {http.request.remote_ip} + events ${toString cfg.caddy.rateLimit.events} + window ${cfg.caddy.rateLimit.window} + } + } + ''} + + ${cfg.caddy.extraConfig} + + reverse_proxy localhost:${toString cfg.port} + ''; + }; + } + + # Extra config from the service module + (extraConfig cfg) + ]); +} diff --git a/modules/nixos/services/battleship-arena.nix b/modules/nixos/services/battleship-arena.nix index bc30803..8169702 100644 --- a/modules/nixos/services/battleship-arena.nix +++ b/modules/nixos/services/battleship-arena.nix @@ -55,6 +55,22 @@ in type = types.package; description = "The battleship-arena package to use"; }; + + backup = { + enable = mkEnableOption "Enable backups for battleship-arena" // { default = true; }; + + paths = mkOption { + type = types.listOf types.str; + default = [ "/var/lib/battleship-arena" ]; + description = "Paths to back up"; + }; + + exclude = mkOption { + type = types.listOf types.str; + default = [ "*.log" ]; + description = "Patterns to exclude from backup"; + }; + }; }; config = mkIf cfg.enable { @@ -158,5 +174,13 @@ in ''; networking.firewall.allowedTCPPorts = [ cfg.sshPort ]; + + # Register backup configuration + atelier.backup.services.battleship-arena = mkIf cfg.backup.enable { + inherit (cfg.backup) paths exclude; + # Has SQLite database, stop before backup + preBackup = "systemctl stop battleship-arena"; + postBackup = "systemctl start battleship-arena"; + }; }; } diff --git a/modules/nixos/services/cachet.nix b/modules/nixos/services/cachet.nix index b05df96..8b42b61 100644 --- a/modules/nixos/services/cachet.nix +++ b/modules/nixos/services/cachet.nix @@ -1,131 +1,30 @@ -{ - config, - lib, - pkgs, - ... -}: +# Cachet - Slack emoji/profile cache service +# +# Uses the mkService base to provide standardized: +# - Systemd service with git deployment +# - Caddy reverse proxy +# - Automatic SQLite backup with WAL checkpoint + let - cfg = config.atelier.services.cachet; + mkService = import ../../lib/mkService.nix; in -{ - options.atelier.services.cachet = { - enable = lib.mkEnableOption "Cachet Slack emoji/profile cache"; - - domain = lib.mkOption { - type = lib.types.str; - description = "Domain to serve cachet on"; - }; - - port = lib.mkOption { - type = lib.types.port; - default = 3000; - description = "Port to run cachet on"; - }; - - dataDir = lib.mkOption { - type = lib.types.path; - default = "/var/lib/cachet"; - description = "Directory to store cachet data"; - }; - - secretsFile = lib.mkOption { - type = lib.types.path; - description = "Path to secrets file containing SLACK_TOKEN, SLACK_SIGNING_SECRET, BEARER_TOKEN"; - }; - repository = lib.mkOption { - type = lib.types.str; - default = "https://github.com/taciturnaxolotl/cachet.git"; - description = "Git repository URL (optional, for auto-deployment)"; - }; - - autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; - }; - - config = lib.mkIf cfg.enable { - users.groups.services = { }; - - users.users.cachet = { - isSystemUser = true; - group = "cachet"; - extraGroups = [ "services" ]; - home = cfg.dataDir; - createHome = true; - shell = pkgs.bash; - }; - - users.groups.cachet = { }; - - security.sudo.extraRules = [ - { - users = [ "cachet" ]; - commands = [ - { - command = "/run/current-system/sw/bin/systemctl restart cachet.service"; - options = [ "NOPASSWD" ]; - } - ]; - } +mkService { + name = "cachet"; + description = "Cachet Slack emoji/profile cache"; + defaultPort = 3000; + runtime = "bun"; + entryPoint = "src/index.ts"; + + extraConfig = cfg: { + # Set DATABASE_PATH environment variable + systemd.services.cachet.serviceConfig.Environment = [ + "DATABASE_PATH=${cfg.dataDir}/data/cachet.db" ]; - systemd.services.cachet = { - description = "Cachet Slack emoji/profile cache"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - path = [ pkgs.git ]; - - preStart = '' - if [ ! -d ${cfg.dataDir}/app/.git ]; then - ${pkgs.git}/bin/git clone ${cfg.repository} ${cfg.dataDir}/app - fi - - cd ${cfg.dataDir}/app - '' + lib.optionalString cfg.autoUpdate '' - ${pkgs.git}/bin/git pull - '' + '' - - if [ ! -f src/index.ts ]; then - echo "No code found at ${cfg.dataDir}/app/src/index.ts" - exit 1 - fi - - echo "Installing dependencies..." - ${pkgs.unstable.bun}/bin/bun install - ''; - - serviceConfig = { - Type = "simple"; - User = "cachet"; - Group = "cachet"; - EnvironmentFile = cfg.secretsFile; - Environment = [ - "NODE_ENV=production" - "PORT=${toString cfg.port}" - "DATABASE_PATH=${cfg.dataDir}/data/cachet.db" - ]; - ExecStart = "${pkgs.bash}/bin/bash -c 'cd ${cfg.dataDir}/app && ${pkgs.unstable.bun}/bin/bun run src/index.ts'"; - Restart = "always"; - RestartSec = "10s"; - }; - - serviceConfig.ExecStartPre = [ - "+${pkgs.writeShellScript "cachet-setup" '' - mkdir -p ${cfg.dataDir}/data - mkdir -p ${cfg.dataDir}/app - chown -R cachet:services ${cfg.dataDir} - chmod -R g+rwX ${cfg.dataDir} - ''}" - ]; - }; - - services.caddy.virtualHosts.${cfg.domain} = { - extraConfig = '' - tls { - dns cloudflare {env.CLOUDFLARE_API_TOKEN} - } - - reverse_proxy localhost:${toString cfg.port} - ''; + # Data declarations for automatic backup + atelier.services.cachet.data = { + sqlite = "${cfg.dataDir}/data/cachet.db"; }; }; } diff --git a/modules/nixos/services/emojibot.nix b/modules/nixos/services/emojibot.nix index 60ff4be..6d35bfc 100644 --- a/modules/nixos/services/emojibot.nix +++ b/modules/nixos/services/emojibot.nix @@ -50,6 +50,22 @@ in }; autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; + + backup = { + enable = lib.mkEnableOption "Enable backups for emojibot" // { default = true; }; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ cfg.dataDir ]; + description = "Paths to back up"; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.log" "app/.git" "app/node_modules" ]; + description = "Patterns to exclude from backup"; + }; + }; }; config = lib.mkIf cfg.enable { @@ -135,5 +151,11 @@ in reverse_proxy localhost:${toString cfg.port} ''; }; + + # Register backup configuration + atelier.backup.services.emojibot = lib.mkIf cfg.backup.enable { + inherit (cfg.backup) paths exclude; + # Stateless service, no pre/post hooks needed + }; }; } diff --git a/modules/nixos/services/hn-alerts.nix b/modules/nixos/services/hn-alerts.nix index 025a830..62667d6 100644 --- a/modules/nixos/services/hn-alerts.nix +++ b/modules/nixos/services/hn-alerts.nix @@ -40,6 +40,22 @@ in }; autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; + + backup = { + enable = lib.mkEnableOption "Enable backups for hn-alerts" // { default = true; }; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ cfg.dataDir ]; + description = "Paths to back up"; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.log" "app/.git" "app/node_modules" ]; + description = "Patterns to exclude from backup"; + }; + }; }; config = lib.mkIf cfg.enable { @@ -120,5 +136,13 @@ in reverse_proxy localhost:${toString cfg.port} ''; }; + + # Register backup configuration + atelier.backup.services.hn-alerts = lib.mkIf cfg.backup.enable { + inherit (cfg.backup) paths exclude; + # Has database, stop before backup + preBackup = "systemctl stop hn-alerts"; + postBackup = "systemctl start hn-alerts"; + }; }; } diff --git a/modules/nixos/services/indiko.nix b/modules/nixos/services/indiko.nix index 0c71636..a31f0c3 100644 --- a/modules/nixos/services/indiko.nix +++ b/modules/nixos/services/indiko.nix @@ -44,6 +44,22 @@ in }; autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; + + backup = { + enable = lib.mkEnableOption "Enable backups for indiko" // { default = true; }; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ cfg.dataDir ]; + description = "Paths to back up"; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.log" "app/.git" "app/node_modules" ]; + description = "Patterns to exclude from backup"; + }; + }; }; config = lib.mkIf cfg.enable { @@ -165,5 +181,13 @@ in } ''; }; + + # Register backup configuration + atelier.backup.services.indiko = lib.mkIf cfg.backup.enable { + inherit (cfg.backup) paths exclude; + # Has SQLite database for sessions/tokens + preBackup = "systemctl stop indiko"; + postBackup = "systemctl start indiko"; + }; }; } diff --git a/modules/nixos/services/l4.nix b/modules/nixos/services/l4.nix index 734f977..451303e 100644 --- a/modules/nixos/services/l4.nix +++ b/modules/nixos/services/l4.nix @@ -50,6 +50,22 @@ in }; autoUpdate = lib.mkEnableOption "Automatically git pull on service restart"; + + backup = { + enable = lib.mkEnableOption "Enable backups for l4" // { default = true; }; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ cfg.dataDir ]; + description = "Paths to back up"; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.log" "app/.git" "app/node_modules" ]; + description = "Patterns to exclude from backup"; + }; + }; }; config = lib.mkIf cfg.enable { @@ -137,5 +153,11 @@ in reverse_proxy localhost:${toString cfg.port} ''; }; + + # Register backup configuration + atelier.backup.services.l4 = lib.mkIf cfg.backup.enable { + inherit (cfg.backup) paths exclude; + # Stateless service (images in R2), no pre/post hooks needed + }; }; } diff --git a/modules/nixos/services/restic/README.md b/modules/nixos/services/restic/README.md new file mode 100644 index 0000000..730167c --- /dev/null +++ b/modules/nixos/services/restic/README.md @@ -0,0 +1,162 @@ +# Restic Backup System + +Per-service backup system using Restic and Backblaze B2, with automatic backup discovery from `mkService` data declarations. + +## Quick Start + +### 1. Create B2 Bucket + +1. Go to [Backblaze B2 console](https://secure.backblaze.com/b2_buckets.htm) +2. Create a new bucket (e.g., `terebithia-backup`) +3. Create an application key with read/write access to this bucket +4. Note the Account ID, Application Key, and Bucket name + +### 2. Create Agenix Secrets + +```bash +cd ~/dots/secrets +mkdir -p restic + +# Repository encryption password +echo "choose-a-strong-encryption-password" | agenix -e restic/password.age + +# B2 credentials +cat > /tmp/restic-env << 'EOF' +B2_ACCOUNT_ID="your-account-id" +B2_ACCOUNT_KEY="your-application-key" +EOF +agenix -e restic/env.age < /tmp/restic-env +rm /tmp/restic-env + +# Repository URL +echo "b2:your-bucket-name:/" | agenix -e restic/repo.age +``` + +### 3. Add Secrets to Machine Config + +```nix +age.secrets = { + "restic/env".file = ../../secrets/restic/env.age; + "restic/repo".file = ../../secrets/restic/repo.age; + "restic/password".file = ../../secrets/restic/password.age; +}; +``` + +### 4. Enable Backup System + +```nix +atelier.backup.enable = true; +``` + +### 5. Deploy and Verify + +```bash +deploy .#terebithia + +# Check timers are active +ssh terebithia 'systemctl list-timers | grep restic' +``` + +## Service Integration + +### Automatic (mkService) + +Services using `mkService` with `data.*` declarations get automatic backup: + +```nix +# In your service module +mkService { + name = "myapp"; + # ... + extraConfig = cfg: { + atelier.services.myapp.data = { + sqlite = "${cfg.dataDir}/data/app.db"; # Auto WAL checkpoint + stop/start + files = [ "${cfg.dataDir}/uploads" ]; # Just backed up, no hooks + }; + }; +} +``` + +The backup system automatically: +- Checkpoints SQLite WAL before backup +- Stops the service during backup +- Restarts after completion +- Tags snapshots with `service:myapp` and `type:sqlite` + +### Manual Registration + +For services not using `mkService`: + +```nix +atelier.backup.services.myservice = { + paths = [ "/var/lib/myservice" ]; + exclude = [ "*.log" "cache/*" ]; + preBackup = "systemctl stop myservice"; + postBackup = "systemctl start myservice"; +}; +``` + +## CLI Usage + +The `atelier-backup` command provides an interactive TUI: + +```bash +atelier-backup # Interactive menu +atelier-backup status # Show backup status for all services +atelier-backup list # Browse snapshots +atelier-backup backup # Trigger manual backup +atelier-backup restore # Interactive restore wizard +atelier-backup dr # Disaster recovery mode +``` + +See `man atelier-backup` for full documentation. + +## Backup Schedule + +- **Time**: 02:00 AM daily +- **Random delay**: 0-2 hours (spreads load across services) +- **Retention**: + - Last 3 snapshots + - 7 daily backups + - 5 weekly backups + - 12 monthly backups + +## Disaster Recovery + +On a fresh NixOS install: + +1. Rebuild from flake: `nixos-rebuild switch --flake .#hostname` +2. Run: `atelier-backup dr` +3. All services restored from latest snapshots + +A manifest at `/etc/atelier/backup-manifest.json` tracks all configured backups. + +## Systemd Units + +Each service gets: +- Timer: `restic-backups-.timer` +- Service: `restic-backups-.service` + +```bash +# Check timer status +systemctl list-timers | grep restic + +# View backup logs +journalctl -u restic-backups-.service + +# Manual backup trigger +systemctl start restic-backups-.service +``` + +## Testing Backups + +Always verify backups work before relying on them: + +```bash +# Restore to /tmp for inspection +atelier-backup restore +# → Select service → Select snapshot → "Inspect (restore to /tmp)" + +# Check restored files +ls -la /tmp/restore-myservice-*/ +``` diff --git a/modules/nixos/services/restic/atelier-backup.1.md b/modules/nixos/services/restic/atelier-backup.1.md new file mode 100644 index 0000000..a0c706d --- /dev/null +++ b/modules/nixos/services/restic/atelier-backup.1.md @@ -0,0 +1,140 @@ +% ATELIER-BACKUP(1) atelier-backup 1.0 +% Kieran Klukas +% December 2024 + +# NAME + +atelier-backup - interactive backup management for atelier services + +# SYNOPSIS + +**atelier-backup** [*COMMAND*] + +**atelier-backup** **status** + +**atelier-backup** **list** + +**atelier-backup** **backup** + +**atelier-backup** **restore** + +**atelier-backup** **dr** + +# DESCRIPTION + +**atelier-backup** is an interactive CLI for managing restic backups of atelier services. It provides a gum-powered TUI for browsing snapshots, triggering backups, restoring data, and performing disaster recovery. + +When run without arguments, an interactive menu is displayed. + +# COMMANDS + +**status** +: Show the backup status for all configured services, including the date of the most recent snapshot. + +**list** +: Interactively select a service and browse its available snapshots. + +**backup** +: Trigger a manual backup for a selected service or all services. + +**restore** +: Interactive restore wizard. Select a service, choose a snapshot, and restore either to /tmp for inspection or in-place (with service stop/start). + +**dr**, **disaster-recovery** +: Full disaster recovery mode. Restores the latest snapshot for ALL services. Only use on a fresh NixOS install after rebuilding from the flake. + +# OPTIONS + +**-h**, **--help** +: Display usage information and exit. + +# RESTORE MODES + +When restoring, you can choose between two modes: + +**Inspect (restore to /tmp)** +: Restores the snapshot to /tmp/restore-SERVICE-SNAPSHOT for inspection. Safe and non-destructive. + +**In-place (DANGEROUS)** +: Stops the service, restores directly to the original paths, and restarts the service. Use with caution. + +# DISASTER RECOVERY + +The **dr** command is designed for full server recovery: + +1. Rebuild NixOS from the flake: `nixos-rebuild switch --flake .#hostname` +2. Run: `atelier-backup dr` +3. The CLI restores the latest snapshot for each service +4. Services are started automatically after restore + +A backup manifest is stored at **/etc/atelier/backup-manifest.json** containing metadata about all configured backups. + +# EXAMPLES + +Interactive menu: +``` +$ atelier-backup +``` + +Check backup status for all services: +``` +$ atelier-backup status +``` + +Browse snapshots for a service: +``` +$ atelier-backup list +``` + +Trigger manual backup: +``` +$ atelier-backup backup +``` + +Restore a service from backup: +``` +$ atelier-backup restore +``` + +Full disaster recovery: +``` +$ atelier-backup dr +``` + +# FILES + +**/etc/atelier/backup-manifest.json** +: Generated manifest containing backup configuration for all services. + +**/run/agenix/restic/*** +: Agenix-managed secrets for restic (env, repo, password). + +# BACKUP SCHEDULE + +Services are backed up nightly at 02:00 with a randomized delay of up to 2 hours to spread load. Backups are triggered via systemd timers: + +- `restic-backups-SERVICE.timer` +- `restic-backups-SERVICE.service` + +# RETENTION POLICY + +Snapshots are retained according to: + +- Last 3 snapshots +- 7 daily backups +- 5 weekly backups +- 12 monthly backups + +# SEE ALSO + +**restic**(1), **systemctl**(1) + +Restic documentation: https://restic.readthedocs.io/ + +# BUGS + +Report bugs at: https://github.com/taciturnaxolotl/dots/issues + +# AUTHORS + +Kieran Klukas diff --git a/modules/nixos/services/restic/cli.nix b/modules/nixos/services/restic/cli.nix new file mode 100644 index 0000000..8855479 --- /dev/null +++ b/modules/nixos/services/restic/cli.nix @@ -0,0 +1,341 @@ +# atelier-backup CLI - Interactive backup management with gum +# +# Commands: +# atelier-backup - Interactive menu +# atelier-backup status - Show backup status for all services +# atelier-backup list - List snapshots (interactive service selection) +# atelier-backup restore - Interactive restore wizard +# atelier-backup backup - Trigger manual backup +# atelier-backup dr - Disaster recovery mode + +{ config, lib, pkgs, ... }: + +let + cfg = config.atelier.backup; + + # Collect all services with backup data for the manifest + atelierServices = lib.filterAttrs (name: svc: + (svc.enable or false) && (svc.data or null) != null + ) (config.atelier.services or {}); + + hasData = svc: + (svc.data.sqlite or null) != null || + (svc.data.postgres or null) != null || + (svc.data.files or []) != []; + + servicesWithData = lib.filterAttrs (name: svc: hasData svc) atelierServices; + + # Also include manually registered backup services + allBackupServices = (lib.attrNames cfg.services) ++ (lib.attrNames servicesWithData); + + # Generate manifest for disaster recovery + backupManifest = pkgs.writeText "backup-manifest.json" (builtins.toJSON { + version = 1; + generated = "nixos-rebuild"; + services = lib.mapAttrs (name: svc: { + dataDir = svc.dataDir or "/var/lib/${name}"; + data = { + sqlite = svc.data.sqlite or null; + postgres = svc.data.postgres or null; + files = svc.data.files or []; + exclude = svc.data.exclude or []; + }; + }) servicesWithData // lib.mapAttrs (name: backupCfg: { + paths = backupCfg.paths; + exclude = backupCfg.exclude or []; + manual = true; + }) cfg.services; + }); + + backupCliScript = pkgs.writeShellScript "atelier-backup" '' + set -e + + # Colors via gum + style() { ${pkgs.gum}/bin/gum style "$@"; } + confirm() { ${pkgs.gum}/bin/gum confirm "$@"; } + choose() { ${pkgs.gum}/bin/gum choose "$@"; } + input() { ${pkgs.gum}/bin/gum input "$@"; } + spin() { ${pkgs.gum}/bin/gum spin "$@"; } + + # Restic wrapper with secrets + restic_cmd() { + ${pkgs.restic}/bin/restic \ + --repository-file ${config.age.secrets."restic/repo".path} \ + --password-file ${config.age.secrets."restic/password".path} \ + "$@" + } + export -f restic_cmd + export B2_ACCOUNT_ID=$(cat ${config.age.secrets."restic/env".path} | grep B2_ACCOUNT_ID | cut -d= -f2) + export B2_ACCOUNT_KEY=$(cat ${config.age.secrets."restic/env".path} | grep B2_ACCOUNT_KEY | cut -d= -f2) + + # Available services + SERVICES="${lib.concatStringsSep " " allBackupServices}" + MANIFEST="${backupManifest}" + + cmd_status() { + style --bold --foreground 212 "Backup Status" + echo + + for svc in $SERVICES; do + # Get latest snapshot for this service + latest=$(restic_cmd snapshots --tag "service:$svc" --json --latest 1 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[0] // empty') + + if [ -n "$latest" ]; then + time=$(echo "$latest" | ${pkgs.jq}/bin/jq -r '.time' | cut -d'T' -f1) + hostname=$(echo "$latest" | ${pkgs.jq}/bin/jq -r '.hostname') + style --foreground 35 "✓ $svc" + style --foreground 117 " Last backup: $time on $hostname" + else + style --foreground 214 "! $svc" + style --foreground 117 " No backups found" + fi + done + } + + cmd_list() { + style --bold --foreground 212 "List Snapshots" + echo + + # Let user pick a service + svc=$(echo "$SERVICES" | tr ' ' '\n' | choose --header "Select service:") + + if [ -z "$svc" ]; then + style --foreground 196 "No service selected" + exit 1 + fi + + style --foreground 117 "Snapshots for $svc:" + echo + + restic_cmd snapshots --tag "service:$svc" --compact + } + + cmd_backup() { + style --bold --foreground 212 "Manual Backup" + echo + + # Let user pick a service or all + svc=$(echo "all $SERVICES" | tr ' ' '\n' | choose --header "Select service to backup:") + + if [ -z "$svc" ]; then + style --foreground 196 "No service selected" + exit 1 + fi + + if [ "$svc" = "all" ]; then + for s in $SERVICES; do + style --foreground 117 "Backing up $s..." + systemctl start "restic-backups-$s.service" || style --foreground 214 "! Failed to backup $s" + done + else + style --foreground 117 "Backing up $svc..." + systemctl start "restic-backups-$svc.service" + fi + + style --foreground 35 "✓ Backup triggered" + } + + cmd_restore() { + style --bold --foreground 212 "Restore Wizard" + echo + + # Pick service + svc=$(echo "$SERVICES" | tr ' ' '\n' | choose --header "Select service to restore:") + + if [ -z "$svc" ]; then + style --foreground 196 "No service selected" + exit 1 + fi + + # List snapshots for selection + style --foreground 117 "Fetching snapshots for $svc..." + snapshots=$(restic_cmd snapshots --tag "service:$svc" --json 2>/dev/null) + + if [ "$(echo "$snapshots" | ${pkgs.jq}/bin/jq 'length')" = "0" ]; then + style --foreground 196 "No snapshots found for $svc" + exit 1 + fi + + # Format snapshots for selection + snapshot_list=$(echo "$snapshots" | ${pkgs.jq}/bin/jq -r '.[] | "\(.short_id) - \(.time | split("T")[0]) - \(.paths | join(", "))"') + + selected=$(echo "$snapshot_list" | choose --header "Select snapshot:") + snapshot_id=$(echo "$selected" | cut -d' ' -f1) + + if [ -z "$snapshot_id" ]; then + style --foreground 196 "No snapshot selected" + exit 1 + fi + + # Restore options + restore_mode=$(choose --header "Restore mode:" "Inspect (restore to /tmp)" "In-place (DANGEROUS)") + + case "$restore_mode" in + "Inspect"*) + target="/tmp/restore-$svc-$snapshot_id" + mkdir -p "$target" + + style --foreground 117 "Restoring to $target..." + restic_cmd restore "$snapshot_id" --target "$target" + + style --foreground 35 "✓ Restored to $target" + style --foreground 117 " Inspect files, then copy what you need" + ;; + + "In-place"*) + style --foreground 196 --bold "⚠ WARNING: This will overwrite existing data!" + echo + + if ! confirm "Stop $svc and restore data?"; then + style --foreground 214 "Restore cancelled" + exit 0 + fi + + style --foreground 117 "Stopping $svc..." + systemctl stop "$svc" 2>/dev/null || true + + style --foreground 117 "Restoring snapshot $snapshot_id..." + restic_cmd restore "$snapshot_id" --target / + + style --foreground 117 "Starting $svc..." + systemctl start "$svc" + + style --foreground 35 "✓ Restore complete" + ;; + esac + } + + cmd_dr() { + style --bold --foreground 196 "⚠ DISASTER RECOVERY MODE" + echo + style --foreground 214 "This will restore ALL services from backup." + style --foreground 214 "Only use this on a fresh NixOS install." + echo + + if ! confirm "Continue with full disaster recovery?"; then + style --foreground 117 "Cancelled" + exit 0 + fi + + style --foreground 117 "Reading backup manifest..." + + for svc in $SERVICES; do + style --foreground 212 "Restoring $svc..." + + # Get latest snapshot + snapshot_id=$(restic_cmd snapshots --tag "service:$svc" --json --latest 1 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[0].short_id // empty') + + if [ -z "$snapshot_id" ]; then + style --foreground 214 " ! No snapshots found, skipping" + continue + fi + + # Stop service if running + systemctl stop "$svc" 2>/dev/null || true + + # Restore + restic_cmd restore "$snapshot_id" --target / + + # Start service + systemctl start "$svc" 2>/dev/null || true + + style --foreground 35 " ✓ Restored from $snapshot_id" + done + + echo + style --foreground 35 --bold "✓ Disaster recovery complete" + } + + cmd_menu() { + style --bold --foreground 212 "Atelier Backup" + echo + + action=$(choose \ + "Status - Show backup status" \ + "List - Browse snapshots" \ + "Backup - Trigger manual backup" \ + "Restore - Restore from backup" \ + "DR - Disaster recovery mode") + + case "$action" in + Status*) cmd_status ;; + List*) cmd_list ;; + Backup*) cmd_backup ;; + Restore*) cmd_restore ;; + DR*) cmd_dr ;; + *) exit 0 ;; + esac + } + + # Main + case "''${1:-}" in + status) cmd_status ;; + list) cmd_list ;; + backup) cmd_backup ;; + restore) cmd_restore ;; + dr|disaster-recovery) cmd_dr ;; + --help|-h) + echo "Usage: atelier-backup [command]" + echo + echo "Commands:" + echo " status Show backup status for all services" + echo " list List snapshots" + echo " backup Trigger manual backup" + echo " restore Interactive restore wizard" + echo " dr Disaster recovery mode" + echo + echo "Run without arguments for interactive menu." + ;; + "") cmd_menu ;; + *) + style --foreground 196 "Unknown command: $1" + exit 1 + ;; + esac + ''; + + backupCli = pkgs.stdenv.mkDerivation { + pname = "atelier-backup"; + version = "1.0.0"; + + dontUnpack = true; + + nativeBuildInputs = [ pkgs.installShellFiles pkgs.pandoc ]; + + manPageSrc = ./atelier-backup.1.md; + bashCompletionSrc = ./completions/atelier-backup.bash; + zshCompletionSrc = ./completions/atelier-backup.zsh; + fishCompletionSrc = ./completions/atelier-backup.fish; + + buildPhase = '' + ${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o atelier-backup.1 + ''; + + installPhase = '' + mkdir -p $out/bin + cp ${backupCliScript} $out/bin/atelier-backup + chmod +x $out/bin/atelier-backup + + # Install man page + installManPage atelier-backup.1 + + # Install completions + installShellCompletion --bash --name atelier-backup $bashCompletionSrc + installShellCompletion --zsh --name _atelier-backup $zshCompletionSrc + installShellCompletion --fish --name atelier-backup.fish $fishCompletionSrc + ''; + + meta = with lib; { + description = "Interactive backup management CLI for atelier services"; + license = licenses.mit; + }; + }; + +in { + config = lib.mkIf cfg.enable { + environment.systemPackages = [ backupCli ]; + + # Store manifest for reference + environment.etc."atelier/backup-manifest.json".source = backupManifest; + }; +} diff --git a/modules/nixos/services/restic/completions/atelier-backup.bash b/modules/nixos/services/restic/completions/atelier-backup.bash new file mode 100644 index 0000000..42d3a44 --- /dev/null +++ b/modules/nixos/services/restic/completions/atelier-backup.bash @@ -0,0 +1,27 @@ +# bash completion for atelier-backup + +_atelier_backup_completion() { + local cur prev + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Main commands + local commands="status list backup restore dr --help" + + # Complete flags + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "--help -h" -- ${cur}) ) + return 0 + fi + + # Complete commands as first argument + if [[ ${COMP_CWORD} -eq 1 ]]; then + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + fi + + return 0 +} + +complete -F _atelier_backup_completion atelier-backup diff --git a/modules/nixos/services/restic/completions/atelier-backup.fish b/modules/nixos/services/restic/completions/atelier-backup.fish new file mode 100644 index 0000000..3fb3c57 --- /dev/null +++ b/modules/nixos/services/restic/completions/atelier-backup.fish @@ -0,0 +1,14 @@ +# fish completion for atelier-backup + +# Disable file completion +complete -c atelier-backup -f + +# Commands (first argument only) +complete -c atelier-backup -n '__fish_is_first_token' -a 'status' -d 'Show backup status for all services' +complete -c atelier-backup -n '__fish_is_first_token' -a 'list' -d 'List snapshots for a service' +complete -c atelier-backup -n '__fish_is_first_token' -a 'backup' -d 'Trigger manual backup' +complete -c atelier-backup -n '__fish_is_first_token' -a 'restore' -d 'Interactive restore wizard' +complete -c atelier-backup -n '__fish_is_first_token' -a 'dr' -d 'Disaster recovery mode' + +# Flags +complete -c atelier-backup -s h -l help -d 'Show help' diff --git a/modules/nixos/services/restic/completions/atelier-backup.zsh b/modules/nixos/services/restic/completions/atelier-backup.zsh new file mode 100644 index 0000000..ce23bce --- /dev/null +++ b/modules/nixos/services/restic/completions/atelier-backup.zsh @@ -0,0 +1,30 @@ +#compdef atelier-backup + +_atelier-backup() { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + '1: :->command' \ + '--help[Show help]' \ + '-h[Show help]' \ + && return 0 + + case $state in + command) + local -a commands + commands=( + 'status:Show backup status for all services' + 'list:List snapshots for a service' + 'backup:Trigger manual backup' + 'restore:Interactive restore wizard' + 'dr:Disaster recovery mode' + ) + _describe 'command' commands + ;; + esac + + return 0 +} + +_atelier-backup "$@" diff --git a/modules/nixos/services/restic/default.nix b/modules/nixos/services/restic/default.nix new file mode 100644 index 0000000..51b4c07 --- /dev/null +++ b/modules/nixos/services/restic/default.nix @@ -0,0 +1,194 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.atelier.backup; + + # Collect all atelier services that have data declarations + atelierServices = lib.filterAttrs (name: svc: + svc.enable or false && (svc.data or null) != null + ) (config.atelier.services or {}); + + # Check if a service has any data to backup + hasData = svc: + (svc.data.sqlite or null) != null || + (svc.data.postgres or null) != null || + (svc.data.files or []) != []; + + # Collect services with data declarations + servicesWithData = lib.filterAttrs (name: svc: hasData svc) atelierServices; + + # Also include manually registered services + allBackups = cfg.services // (lib.mapAttrs mkAutoBackup servicesWithData); + + # Auto-generate backup config from service data declarations + mkAutoBackup = name: svc: let + data = svc.data; + hasSqlite = data.sqlite or null != null; + hasPostgres = data.postgres or null != null; + + # Collect all paths to backup + paths = + (lib.optional hasSqlite (builtins.dirOf data.sqlite)) ++ + (data.files or []); + + # Pre-backup: handle database consistency + preBackup = lib.concatStringsSep "\n" ( + # SQLite: checkpoint WAL then stop service + (lib.optional hasSqlite '' + echo "Checkpointing SQLite WAL for ${name}..." + ${pkgs.sqlite}/bin/sqlite3 "${data.sqlite}" "PRAGMA wal_checkpoint(TRUNCATE);" || true + echo "Stopping ${name} for backup..." + systemctl stop ${name} + '') ++ + # PostgreSQL: dump to file + (lib.optional hasPostgres '' + echo "Dumping PostgreSQL database ${data.postgres}..." + ${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/pg_dump ${data.postgres} > /tmp/${name}-pg-dump.sql + '') ++ + # If no database but service needs to be stopped (manual override possible) + [] + ); + + # Post-backup: restart service + postBackup = lib.concatStringsSep "\n" ( + (lib.optional hasSqlite '' + echo "Restarting ${name} after backup..." + systemctl start ${name} + '') ++ + (lib.optional hasPostgres '' + rm -f /tmp/${name}-pg-dump.sql + '') + ); + + in { + enable = true; + inherit paths; + exclude = data.exclude or [ "*.log" "node_modules" ".git" ]; + tags = [ "service:${name}" ] ++ + (lib.optional hasSqlite "type:sqlite") ++ + (lib.optional hasPostgres "type:postgres"); + preBackup = if preBackup != "" then preBackup else null; + postBackup = if postBackup != "" then postBackup else null; + }; + + # Create a restic backup job for a service + mkBackupJob = name: serviceCfg: { + inherit (serviceCfg) paths exclude; + + initialize = true; + + # Use secrets from agenix + environmentFile = config.age.secrets."restic/env".path; + repositoryFile = config.age.secrets."restic/repo".path; + passwordFile = config.age.secrets."restic/password".path; + + # Tags for easier filtering during restore + extraBackupArgs = + (map (t: "--tag ${t}") (serviceCfg.tags or [ "service:${name}" ])) ++ + [ "--verbose" ]; + + # Retention policy + pruneOpts = [ + "--keep-last 3" + "--keep-daily 7" + "--keep-weekly 5" + "--keep-monthly 12" + "--tag service:${name}" # Only prune this service's snapshots + ]; + + # Backup schedule (nightly at 2 AM + random delay) + timerConfig = { + OnCalendar = "02:00"; + RandomizedDelaySec = "2h"; + Persistent = true; + }; + + # Pre/post backup hooks for database consistency + backupPrepareCommand = lib.optionalString (serviceCfg.preBackup or null != null) serviceCfg.preBackup; + backupCleanupCommand = lib.optionalString (serviceCfg.postBackup or null != null) serviceCfg.postBackup; + }; + +in +{ + imports = [ ./cli.nix ]; + + options.atelier.backup = { + enable = lib.mkEnableOption "Restic backup system"; + + # Manual service registration (for services not using mkService) + services = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable backups for this service"; + }; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "Paths to back up"; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "*.log" "node_modules" ".git" ]; + description = "Glob patterns to exclude from backup"; + }; + + tags = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Tags to apply to snapshots"; + }; + + preBackup = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Command to run before backup"; + }; + + postBackup = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Command to run after backup"; + }; + }; + } + ); + default = { }; + description = "Per-service backup configurations (manual registration)"; + }; + }; + + config = lib.mkIf cfg.enable { + # Ensure secrets are defined + assertions = [ + { + assertion = config.age.secrets ? "restic/env"; + message = "atelier.backup requires age.secrets.\"restic/env\" to be defined"; + } + { + assertion = config.age.secrets ? "restic/repo"; + message = "atelier.backup requires age.secrets.\"restic/repo\" to be defined"; + } + { + assertion = config.age.secrets ? "restic/password"; + message = "atelier.backup requires age.secrets.\"restic/password\" to be defined"; + } + ]; + + # Create restic backup jobs for each service (auto + manual) + services.restic.backups = lib.mapAttrs mkBackupJob ( + lib.filterAttrs (n: v: v.enable) allBackups + ); + + # Add restic and sqlite to system packages for manual operations + environment.systemPackages = [ pkgs.restic pkgs.sqlite ]; + }; +} diff --git a/secrets/restic/env.age b/secrets/restic/env.age new file mode 100644 index 0000000..bf07b45 --- /dev/null +++ b/secrets/restic/env.age @@ -0,0 +1,13 @@ +age-encryption.org/v1 +-> ssh-rsa DqcG0Q +lT4PA3DsRI4Br6Dvx0walY/MPOUWzuhGdiGPRtzjFIqGYnlaJNX/SjT+gn+THM/7 +wKWWQCQ4bXw6EqoguRsW4fcONCUvxQj/by7+JnMsLNM+eu1xlZHVlyi9rEkbkE9K +xtgxuKuXVhPEPeyaZjCMmaWu7QMXeepFnUrUGcqmhTdF5cYHr8z6qMCqyfm7nC27 +rkBqqs4z3hAXRjS3gDxBLJCbWMMKG7RGvO1E5wgMlQeLLdmbDOYnFBajJdt4KLMN +EzjnWp/BcqCAbJc8OVuawV4EDEdoN74IG7CQteZ5DF77vExzESuMVpEhpKOq92ho +JF7qwFHeQtKoHAFmg1xl4X0e+V2pEDhW1VXKAwLXgljUYS5QQKYhzciKT3yXN6o7 +AaLk407EUSMvXmVQ7isfUQrFMtFD2AXP1tE445vMu3u9SmoyBAiMg0AM1orPtAaW +wYxPt3zOezMBFGjnLYfJ2uZzdPJXGk5NfhkKojw++ZhCJ0F6D7Nawpln6McYYEuX + +--- IgHl41EQ9rKfWh8SbOjEYhgK5NeHr9njPcLOYEx2OtI +wNX����}�O�X)�Ч�N��e+I����p ]g����$�‹�C��}����Y���K��-p�V��z{�51�w��ﯹŽ��X��XFMXV�^K�yOf��7����[}�0�tt0$a}��h@/ \ No newline at end of file diff --git a/secrets/restic/password.age b/secrets/restic/password.age new file mode 100644 index 0000000000000000000000000000000000000000..c78b60b61e1ab4a385bd8db1e764bfbf94f04c82 GIT binary patch literal 684 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!*`Do#{zDNJ@Z2;_1q2+Omm@Gj0T zH%$#s^(#!btV&DG@e4{b)^^X#^>WFJ^b06SPD?foHz~>V$<8g03M)4bPE0d2N-B&f zH%RA-2(3)Y&5N)IGL6irs*Ln;w9rn`Hz>*}3=H-P$_z7f4>!*8%=9P>3y;bV$W1LW z%gx9ROYsY+Dk{&htn_x{DsW59^Nn&1ve5Q7DKqn_NGx+TDl62_tgJB6cQQ1pswxRj zHTO<(P03A*EOO6I4hziJ_SV;S_b_lWDl7I0;PMNv^iNOoEJ{mG_0l$S%}#VHFs%%T zFmiXxOvyFy2uSg8P4*9QHVY5UEiKD8Gm3IfG&W2N$Z!lc&NPk)4&lnt&P#Iib}TF` z(>G7kcMmDeEid=7$n|ng3@`90_fHS7)GqU_GRX)raJMiwHg*dvt~4o&ib~9K$|wyf zb+Y90ORowou1E~2%&^G!FmcRu4ll?w%PDkENh|OyE-rU0$_OxZD)CNA3r|b*C@A(b z&i5#BDsr`q%GWl{Df15Es zEq8bH^h>L#Dk+cj4mR{psW2}zO3U|h_b9VS=PI)BFDlKk3^30qH!F*B@rta}t_)4d z4)*aW(=Q6}^(l5XHi^tKE>E=#^Yw`;%hWc_H8U@CcL^$U3v&xGD&^wR)zwvSa*s-N z_xCGFF7YWy3(RoO4fm)FE;My@GzvD&3obD8D$C6(O-XYpa`NPQXLm~C%=XP1cRw&3 zV|dDXz(D25tQ8JS=}wnTn>Ot^ed_1yHKKW2cb@&eJM(+1Kj*VV7yZs#x{}{{Cnc*g TPgQPQ!{l{M@L2HeO}bwI_Pp@e literal 0 HcmV?d00001 diff --git a/secrets/restic/repo.age b/secrets/restic/repo.age new file mode 100644 index 0000000..bdcb646 --- /dev/null +++ b/secrets/restic/repo.age @@ -0,0 +1,13 @@ +age-encryption.org/v1 +-> ssh-rsa DqcG0Q +KQE6ZRRHyd/oWWkkl80O502xQwJQcmbn40ihhTsjzQm//fxKCjhOkRf0O7ZL3ZPn +sdM+Qr+LphvyoqDvgJwKE4L0MIBVw46swql+wTBGSBLa/fZ676Bx1hKbBs6PvcPO +z2Rr3KFh1vMWm3W8CdPVYnFPdW7PCQaXUTZSext+AvAcZ6f+rGnnoEq1RW05FMoD +nYec+PJk9A7bQJ9OrLj4yDKj8GZbd0k5gox/HiWTNGy42fvdXkpjZ29BEPeMlITD +2msyzEg4ITNIzkyFr0h0WFMhL515CnHxXvTeO3sGKDYJPZ9iqpJlV5fvtz76v2h9 +ZvJYzY9EgRyhk6t6rDk98ZrtRcp2BPwfNHocdYdUHRO0wKoFubZaw2Ifr22aJHhb ++9MiSzJ6qVc91Rtbc20Ib/dKh3OYdJvJ08pQrW8gDD4dd1hOx9kPREBKbaoqoQ7C +CldNO49lGYO1U7bnohHF11LiwnFtnESdZJUtJBrldawDmY4ikntP2oTrPFsDMAz8 + +--- fdcl8C/41l9nxbxvz7bwkSsfvFgmtXXQjyj72ijyK5c +�)��+C�#m����I� RJ7R���W�h�x"Rx-^��q�`z������/�� \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index de2fa56..f00333a 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -47,4 +47,13 @@ in "l4.age".publicKeys = [ kierank ]; + "restic/env.age".publicKeys = [ + kierank + ]; + "restic/repo.age".publicKeys = [ + kierank + ]; + "restic/password.age".publicKeys = [ + kierank + ]; }