tangled
alpha
login
or
join now
jaspermayone.com
/
dots
0
fork
atom
@jaspermayone.com's dotfiles
0
fork
atom
overview
issues
pulls
pipelines
disabled: backups
jaspermayone.com
2 months ago
38bfe54e
69636fca
+636
8 changed files
expand all
collapse all
unified
split
hosts
alastor
configuration.nix
remus
default.nix
modules
restic
cli.nix
completions
castle.bash
castle.fish
castle.zsh
default.nix
secrets
secrets.nix
+62
hosts/alastor/configuration.nix
···
9
9
../../modules/knot/sync.nix
10
10
../../modules/bluesky-pds/default.nix
11
11
../../modules/atuin-server
12
12
+
../../modules/restic
12
13
inputs.tangled.nixosModules.knot
13
14
];
14
15
···
130
131
owner = "jsp";
131
132
mode = "400";
132
133
};
134
134
+
# Restic backup secrets (uncomment when ready)
135
135
+
# "restic/env" = {
136
136
+
# file = ../../secrets/restic/env.age;
137
137
+
# mode = "400";
138
138
+
# };
139
139
+
# "restic/repo" = {
140
140
+
# file = ../../secrets/restic/repo.age;
141
141
+
# mode = "400";
142
142
+
# };
143
143
+
# "restic/password" = {
144
144
+
# file = ../../secrets/restic/password.age;
145
145
+
# mode = "400";
146
146
+
# };
133
147
};
134
148
135
149
# FRP tunnel server
···
212
226
systemd.services.caddy.serviceConfig.EnvironmentFile = config.age.secrets.cloudflare-credentials.path;
213
227
214
228
networking.firewall.allowedTCPPorts = [ 80 443 2222 ]; # 2222 for knot SSH
229
229
+
230
230
+
# Castle backup system (disabled for now - enable when secrets are ready)
231
231
+
# To enable:
232
232
+
# 1. Create secrets: agenix -e secrets/restic/env.age (B2_ACCOUNT_ID=..., B2_ACCOUNT_KEY=...)
233
233
+
# 2. Create secrets: agenix -e secrets/restic/repo.age (b2:bucket-name:/backup-path)
234
234
+
# 3. Create secrets: agenix -e secrets/restic/password.age (repository encryption password)
235
235
+
# 4. Uncomment the age.secrets above
236
236
+
# 5. Uncomment castle.backup below
237
237
+
#
238
238
+
# castle.backup = {
239
239
+
# enable = true;
240
240
+
# services = {
241
241
+
# knot = {
242
242
+
# paths = [ "/var/lib/knot" "/home/git" ];
243
243
+
# exclude = [ "*.log" ".git" ];
244
244
+
# tags = [ "service:knot" "type:git" ];
245
245
+
# preBackup = ''
246
246
+
# systemctl stop tangled-knot || true
247
247
+
# '';
248
248
+
# postBackup = ''
249
249
+
# systemctl start tangled-knot || true
250
250
+
# '';
251
251
+
# };
252
252
+
# pds = {
253
253
+
# paths = [ "/var/lib/pds" ];
254
254
+
# exclude = [ "*.log" "node_modules" ];
255
255
+
# tags = [ "service:pds" "type:atproto" ];
256
256
+
# preBackup = ''
257
257
+
# systemctl stop bluesky-pds || true
258
258
+
# '';
259
259
+
# postBackup = ''
260
260
+
# systemctl start bluesky-pds || true
261
261
+
# '';
262
262
+
# };
263
263
+
# atuin = {
264
264
+
# paths = [ "/var/lib/atuin-server" ];
265
265
+
# exclude = [ "*.log" ];
266
266
+
# tags = [ "service:atuin" "type:sqlite" ];
267
267
+
# preBackup = ''
268
268
+
# sqlite3 /var/lib/atuin-server/atuin.db "PRAGMA wal_checkpoint(TRUNCATE);" || true
269
269
+
# systemctl stop atuin-server || true
270
270
+
# '';
271
271
+
# postBackup = ''
272
272
+
# systemctl start atuin-server || true
273
273
+
# '';
274
274
+
# };
275
275
+
# };
276
276
+
# };
215
277
216
278
# Automatic garbage collection
217
279
nix.gc = {
+2
hosts/remus/default.nix
···
26
26
27
27
# Laptop-specific nix packages
28
28
environment.systemPackages = with pkgs; [
29
29
+
coreutils
30
30
+
libyaml
29
31
yubikey-manager
30
32
];
31
33
+361
modules/restic/cli.nix
···
1
1
+
# castle CLI - Hogwarts infrastructure management
2
2
+
# Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots)
3
3
+
#
4
4
+
# Commands:
5
5
+
# sudo castle - Interactive menu
6
6
+
# sudo castle backup - Backup management submenu
7
7
+
# sudo castle backup status - Show backup status for all services
8
8
+
# sudo castle backup list - List snapshots
9
9
+
# sudo castle backup run - Trigger manual backup
10
10
+
# sudo castle backup restore - Interactive restore wizard
11
11
+
# sudo castle backup dr - Disaster recovery mode
12
12
+
#
13
13
+
# Future modules:
14
14
+
# castle status - Service health dashboard
15
15
+
# castle secrets - Manage agenix secrets
16
16
+
# castle deploy - Remote deployment tools
17
17
+
# castle logs - Service log viewer
18
18
+
19
19
+
{ config, lib, pkgs, ... }:
20
20
+
21
21
+
let
22
22
+
cfg = config.castle.backup;
23
23
+
24
24
+
# Get all configured backup services
25
25
+
allBackupServices = lib.attrNames cfg.services;
26
26
+
27
27
+
# Generate manifest for disaster recovery
28
28
+
backupManifest = pkgs.writeText "backup-manifest.json" (builtins.toJSON {
29
29
+
version = 1;
30
30
+
generated = "nixos-rebuild";
31
31
+
services = lib.mapAttrs (name: backupCfg: {
32
32
+
paths = backupCfg.paths;
33
33
+
exclude = backupCfg.exclude or [];
34
34
+
tags = backupCfg.tags or [];
35
35
+
}) cfg.services;
36
36
+
});
37
37
+
38
38
+
castleCliScript = pkgs.writeShellScript "castle" ''
39
39
+
set -e
40
40
+
41
41
+
# Must be run as root
42
42
+
if [ "$(id -u)" -ne 0 ]; then
43
43
+
echo "Error: castle must be run as root (use sudo)"
44
44
+
exit 1
45
45
+
fi
46
46
+
47
47
+
# Colors via gum
48
48
+
style() { ${pkgs.gum}/bin/gum style "$@"; }
49
49
+
confirm() { ${pkgs.gum}/bin/gum confirm "$@"; }
50
50
+
choose() { ${pkgs.gum}/bin/gum choose "$@"; }
51
51
+
input() { ${pkgs.gum}/bin/gum input "$@"; }
52
52
+
spin() { ${pkgs.gum}/bin/gum spin "$@"; }
53
53
+
54
54
+
# Load B2 credentials for backup commands
55
55
+
load_backup_env() {
56
56
+
set -a
57
57
+
source ${config.age.secrets."restic/env".path}
58
58
+
set +a
59
59
+
}
60
60
+
61
61
+
# Restic wrapper with secrets
62
62
+
restic_cmd() {
63
63
+
${pkgs.restic}/bin/restic \
64
64
+
--repository-file ${config.age.secrets."restic/repo".path} \
65
65
+
--password-file ${config.age.secrets."restic/password".path} \
66
66
+
"$@"
67
67
+
}
68
68
+
69
69
+
# Available backup services
70
70
+
BACKUP_SERVICES="${lib.concatStringsSep " " allBackupServices}"
71
71
+
MANIFEST="${backupManifest}"
72
72
+
73
73
+
# ========== BACKUP COMMANDS ==========
74
74
+
75
75
+
backup_status() {
76
76
+
load_backup_env
77
77
+
style --bold --foreground 212 "Backup Status"
78
78
+
echo
79
79
+
80
80
+
for svc in $BACKUP_SERVICES; do
81
81
+
latest=$(restic_cmd snapshots --tag "service:$svc" --json --latest 1 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[0] // empty')
82
82
+
83
83
+
if [ -n "$latest" ]; then
84
84
+
time=$(echo "$latest" | ${pkgs.jq}/bin/jq -r '.time' | cut -d'T' -f1)
85
85
+
hostname=$(echo "$latest" | ${pkgs.jq}/bin/jq -r '.hostname')
86
86
+
style --foreground 35 "✓ $svc"
87
87
+
style --foreground 117 " Last backup: $time on $hostname"
88
88
+
else
89
89
+
style --foreground 214 "! $svc"
90
90
+
style --foreground 117 " No backups found"
91
91
+
fi
92
92
+
done
93
93
+
}
94
94
+
95
95
+
backup_list() {
96
96
+
load_backup_env
97
97
+
style --bold --foreground 212 "List Snapshots"
98
98
+
echo
99
99
+
100
100
+
svc=$(echo "$BACKUP_SERVICES" | tr ' ' '\n' | choose --header "Select service:")
101
101
+
102
102
+
if [ -z "$svc" ]; then
103
103
+
style --foreground 196 "No service selected"
104
104
+
exit 1
105
105
+
fi
106
106
+
107
107
+
style --foreground 117 "Snapshots for $svc:"
108
108
+
echo
109
109
+
110
110
+
restic_cmd snapshots --tag "service:$svc" --compact
111
111
+
}
112
112
+
113
113
+
backup_run() {
114
114
+
style --bold --foreground 212 "Manual Backup"
115
115
+
echo
116
116
+
117
117
+
svc=$(echo "all $BACKUP_SERVICES" | tr ' ' '\n' | choose --header "Select service to backup:")
118
118
+
119
119
+
if [ -z "$svc" ]; then
120
120
+
style --foreground 196 "No service selected"
121
121
+
exit 1
122
122
+
fi
123
123
+
124
124
+
if [ "$svc" = "all" ]; then
125
125
+
for s in $BACKUP_SERVICES; do
126
126
+
style --foreground 117 "Backing up $s..."
127
127
+
systemctl start "restic-backups-$s.service" || style --foreground 214 "! Failed to backup $s"
128
128
+
done
129
129
+
else
130
130
+
style --foreground 117 "Backing up $svc..."
131
131
+
systemctl start "restic-backups-$svc.service"
132
132
+
fi
133
133
+
134
134
+
style --foreground 35 "✓ Backup triggered"
135
135
+
}
136
136
+
137
137
+
backup_restore() {
138
138
+
load_backup_env
139
139
+
style --bold --foreground 212 "Restore Wizard"
140
140
+
echo
141
141
+
142
142
+
svc=$(echo "$BACKUP_SERVICES" | tr ' ' '\n' | choose --header "Select service to restore:")
143
143
+
144
144
+
if [ -z "$svc" ]; then
145
145
+
style --foreground 196 "No service selected"
146
146
+
exit 1
147
147
+
fi
148
148
+
149
149
+
style --foreground 117 "Fetching snapshots for $svc..."
150
150
+
snapshots=$(restic_cmd snapshots --tag "service:$svc" --json 2>/dev/null)
151
151
+
152
152
+
if [ "$(echo "$snapshots" | ${pkgs.jq}/bin/jq 'length')" = "0" ]; then
153
153
+
style --foreground 196 "No snapshots found for $svc"
154
154
+
exit 1
155
155
+
fi
156
156
+
157
157
+
snapshot_list=$(echo "$snapshots" | ${pkgs.jq}/bin/jq -r '.[] | "\(.short_id) - \(.time | split("T")[0]) - \(.paths | join(", "))"')
158
158
+
159
159
+
selected=$(echo "$snapshot_list" | choose --header "Select snapshot:")
160
160
+
snapshot_id=$(echo "$selected" | cut -d' ' -f1)
161
161
+
162
162
+
if [ -z "$snapshot_id" ]; then
163
163
+
style --foreground 196 "No snapshot selected"
164
164
+
exit 1
165
165
+
fi
166
166
+
167
167
+
restore_mode=$(choose --header "Restore mode:" "Inspect (restore to /tmp)" "In-place (DANGEROUS)")
168
168
+
169
169
+
case "$restore_mode" in
170
170
+
"Inspect"*)
171
171
+
target="/tmp/restore-$svc-$snapshot_id"
172
172
+
mkdir -p "$target"
173
173
+
174
174
+
style --foreground 117 "Restoring to $target..."
175
175
+
restic_cmd restore "$snapshot_id" --target "$target"
176
176
+
177
177
+
style --foreground 35 "✓ Restored to $target"
178
178
+
style --foreground 117 " Inspect files, then copy what you need"
179
179
+
;;
180
180
+
181
181
+
"In-place"*)
182
182
+
style --foreground 196 --bold "⚠ WARNING: This will overwrite existing data!"
183
183
+
echo
184
184
+
185
185
+
if ! confirm "Stop $svc and restore data?"; then
186
186
+
style --foreground 214 "Restore cancelled"
187
187
+
exit 0
188
188
+
fi
189
189
+
190
190
+
style --foreground 117 "Stopping $svc..."
191
191
+
systemctl stop "$svc" 2>/dev/null || true
192
192
+
193
193
+
style --foreground 117 "Restoring snapshot $snapshot_id..."
194
194
+
restic_cmd restore "$snapshot_id" --target /
195
195
+
196
196
+
style --foreground 117 "Starting $svc..."
197
197
+
systemctl start "$svc"
198
198
+
199
199
+
style --foreground 35 "✓ Restore complete"
200
200
+
;;
201
201
+
esac
202
202
+
}
203
203
+
204
204
+
backup_dr() {
205
205
+
load_backup_env
206
206
+
style --bold --foreground 196 "⚠ DISASTER RECOVERY MODE"
207
207
+
echo
208
208
+
style --foreground 214 "This will restore ALL services from backup."
209
209
+
style --foreground 214 "Only use this on a fresh NixOS install."
210
210
+
echo
211
211
+
212
212
+
if ! confirm "Continue with full disaster recovery?"; then
213
213
+
style --foreground 117 "Cancelled"
214
214
+
exit 0
215
215
+
fi
216
216
+
217
217
+
style --foreground 117 "Reading backup manifest..."
218
218
+
219
219
+
for svc in $BACKUP_SERVICES; do
220
220
+
style --foreground 212 "Restoring $svc..."
221
221
+
222
222
+
snapshot_id=$(restic_cmd snapshots --tag "service:$svc" --json --latest 1 2>/dev/null | ${pkgs.jq}/bin/jq -r '.[0].short_id // empty')
223
223
+
224
224
+
if [ -z "$snapshot_id" ]; then
225
225
+
style --foreground 214 " ! No snapshots found, skipping"
226
226
+
continue
227
227
+
fi
228
228
+
229
229
+
systemctl stop "$svc" 2>/dev/null || true
230
230
+
restic_cmd restore "$snapshot_id" --target /
231
231
+
systemctl start "$svc" 2>/dev/null || true
232
232
+
233
233
+
style --foreground 35 " ✓ Restored from $snapshot_id"
234
234
+
done
235
235
+
236
236
+
echo
237
237
+
style --foreground 35 --bold "✓ Disaster recovery complete"
238
238
+
}
239
239
+
240
240
+
backup_menu() {
241
241
+
style --bold --foreground 212 "Backup Management"
242
242
+
echo
243
243
+
244
244
+
action=$(choose \
245
245
+
"Status - Show backup status" \
246
246
+
"List - Browse snapshots" \
247
247
+
"Run - Trigger manual backup" \
248
248
+
"Restore - Restore from backup" \
249
249
+
"DR - Disaster recovery mode" \
250
250
+
"← Back")
251
251
+
252
252
+
case "$action" in
253
253
+
Status*) backup_status ;;
254
254
+
List*) backup_list ;;
255
255
+
Run*) backup_run ;;
256
256
+
Restore*) backup_restore ;;
257
257
+
DR*) backup_dr ;;
258
258
+
*) main_menu ;;
259
259
+
esac
260
260
+
}
261
261
+
262
262
+
# ========== MAIN MENU ==========
263
263
+
264
264
+
main_menu() {
265
265
+
style --bold --foreground 212 "🏰 Castle - Hogwarts Infrastructure"
266
266
+
echo
267
267
+
268
268
+
action=$(choose \
269
269
+
"Backup - Manage backups and restores" \
270
270
+
"Exit")
271
271
+
272
272
+
case "$action" in
273
273
+
Backup*) backup_menu ;;
274
274
+
*) exit 0 ;;
275
275
+
esac
276
276
+
}
277
277
+
278
278
+
show_help() {
279
279
+
echo "Usage: castle [command] [subcommand]"
280
280
+
echo
281
281
+
echo "Commands:"
282
282
+
echo " backup Backup management menu"
283
283
+
echo " backup status Show backup status for all services"
284
284
+
echo " backup list List snapshots"
285
285
+
echo " backup run Trigger manual backup"
286
286
+
echo " backup restore Interactive restore wizard"
287
287
+
echo " backup dr Disaster recovery mode"
288
288
+
echo
289
289
+
echo "Run without arguments for interactive menu."
290
290
+
echo
291
291
+
echo "Note: Must be run as root (use sudo)"
292
292
+
}
293
293
+
294
294
+
# ========== MAIN ==========
295
295
+
296
296
+
case "''${1:-}" in
297
297
+
backup)
298
298
+
case "''${2:-}" in
299
299
+
status) backup_status ;;
300
300
+
list) backup_list ;;
301
301
+
run) backup_run ;;
302
302
+
restore) backup_restore ;;
303
303
+
dr|disaster-recovery) backup_dr ;;
304
304
+
"") backup_menu ;;
305
305
+
*)
306
306
+
style --foreground 196 "Unknown backup command: $2"
307
307
+
exit 1
308
308
+
;;
309
309
+
esac
310
310
+
;;
311
311
+
--help|-h)
312
312
+
show_help
313
313
+
;;
314
314
+
"")
315
315
+
main_menu
316
316
+
;;
317
317
+
*)
318
318
+
style --foreground 196 "Unknown command: $1"
319
319
+
echo "Run 'castle --help' for usage."
320
320
+
exit 1
321
321
+
;;
322
322
+
esac
323
323
+
'';
324
324
+
325
325
+
castleCli = pkgs.stdenv.mkDerivation {
326
326
+
pname = "castle";
327
327
+
version = "1.0.0";
328
328
+
329
329
+
dontUnpack = true;
330
330
+
331
331
+
nativeBuildInputs = [ pkgs.installShellFiles ];
332
332
+
333
333
+
bashCompletionSrc = ./completions/castle.bash;
334
334
+
zshCompletionSrc = ./completions/castle.zsh;
335
335
+
fishCompletionSrc = ./completions/castle.fish;
336
336
+
337
337
+
installPhase = ''
338
338
+
mkdir -p $out/bin
339
339
+
cp ${castleCliScript} $out/bin/castle
340
340
+
chmod +x $out/bin/castle
341
341
+
342
342
+
# Install completions
343
343
+
installShellCompletion --bash --name castle $bashCompletionSrc
344
344
+
installShellCompletion --zsh --name _castle $zshCompletionSrc
345
345
+
installShellCompletion --fish --name castle.fish $fishCompletionSrc
346
346
+
'';
347
347
+
348
348
+
meta = with lib; {
349
349
+
description = "Hogwarts castle infrastructure management CLI";
350
350
+
license = licenses.mit;
351
351
+
};
352
352
+
};
353
353
+
354
354
+
in {
355
355
+
config = lib.mkIf cfg.enable {
356
356
+
environment.systemPackages = [ castleCli pkgs.gum pkgs.jq ];
357
357
+
358
358
+
# Store manifest for reference
359
359
+
environment.etc."castle/backup-manifest.json".source = backupManifest;
360
360
+
};
361
361
+
}
+25
modules/restic/completions/castle.bash
···
1
1
+
# Bash completion for castle CLI
2
2
+
# Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots)
3
3
+
4
4
+
_castle() {
5
5
+
local cur prev words cword
6
6
+
_init_completion || return
7
7
+
8
8
+
local commands="backup"
9
9
+
local backup_commands="status list run restore dr"
10
10
+
11
11
+
case "${prev}" in
12
12
+
castle)
13
13
+
COMPREPLY=($(compgen -W "${commands} --help" -- "${cur}"))
14
14
+
return
15
15
+
;;
16
16
+
backup)
17
17
+
COMPREPLY=($(compgen -W "${backup_commands}" -- "${cur}"))
18
18
+
return
19
19
+
;;
20
20
+
esac
21
21
+
22
22
+
COMPREPLY=()
23
23
+
}
24
24
+
25
25
+
complete -F _castle castle
+16
modules/restic/completions/castle.fish
···
1
1
+
# Fish completion for castle CLI
2
2
+
# Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots)
3
3
+
4
4
+
# Disable file completions
5
5
+
complete -c castle -f
6
6
+
7
7
+
# Top-level commands
8
8
+
complete -c castle -n "__fish_use_subcommand" -a "backup" -d "Manage backups and restores"
9
9
+
complete -c castle -n "__fish_use_subcommand" -a "--help" -d "Show help message"
10
10
+
11
11
+
# Backup subcommands
12
12
+
complete -c castle -n "__fish_seen_subcommand_from backup" -a "status" -d "Show backup status for all services"
13
13
+
complete -c castle -n "__fish_seen_subcommand_from backup" -a "list" -d "List snapshots"
14
14
+
complete -c castle -n "__fish_seen_subcommand_from backup" -a "run" -d "Trigger manual backup"
15
15
+
complete -c castle -n "__fish_seen_subcommand_from backup" -a "restore" -d "Interactive restore wizard"
16
16
+
complete -c castle -n "__fish_seen_subcommand_from backup" -a "dr" -d "Disaster recovery mode"
+31
modules/restic/completions/castle.zsh
···
1
1
+
#compdef castle
2
2
+
# Zsh completion for castle CLI
3
3
+
# Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots)
4
4
+
5
5
+
_castle() {
6
6
+
local -a commands backup_commands
7
7
+
8
8
+
commands=(
9
9
+
'backup:Manage backups and restores'
10
10
+
'--help:Show help message'
11
11
+
)
12
12
+
13
13
+
backup_commands=(
14
14
+
'status:Show backup status for all services'
15
15
+
'list:List snapshots'
16
16
+
'run:Trigger manual backup'
17
17
+
'restore:Interactive restore wizard'
18
18
+
'dr:Disaster recovery mode'
19
19
+
)
20
20
+
21
21
+
case "${words[2]}" in
22
22
+
backup)
23
23
+
_describe -t backup_commands 'backup command' backup_commands
24
24
+
;;
25
25
+
*)
26
26
+
_describe -t commands 'castle command' commands
27
27
+
;;
28
28
+
esac
29
29
+
}
30
30
+
31
31
+
_castle "$@"
+131
modules/restic/default.nix
···
1
1
+
# Restic backup module for NixOS
2
2
+
# Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots)
3
3
+
#
4
4
+
# Provides automated backups to Backblaze B2 (or any restic-compatible backend)
5
5
+
# with per-service configuration, database handling, and an interactive CLI.
6
6
+
{
7
7
+
config,
8
8
+
lib,
9
9
+
pkgs,
10
10
+
...
11
11
+
}:
12
12
+
let
13
13
+
cfg = config.castle.backup;
14
14
+
15
15
+
# Create a restic backup job for a service
16
16
+
mkBackupJob = name: serviceCfg: {
17
17
+
inherit (serviceCfg) paths;
18
18
+
exclude = serviceCfg.exclude;
19
19
+
20
20
+
initialize = true;
21
21
+
22
22
+
# Use secrets from agenix
23
23
+
environmentFile = config.age.secrets."restic/env".path;
24
24
+
repositoryFile = config.age.secrets."restic/repo".path;
25
25
+
passwordFile = config.age.secrets."restic/password".path;
26
26
+
27
27
+
# Tags for easier filtering during restore
28
28
+
extraBackupArgs =
29
29
+
(map (t: "--tag ${t}") (serviceCfg.tags or [ "service:${name}" ]))
30
30
+
++ [ "--verbose" ];
31
31
+
32
32
+
# Retention policy
33
33
+
pruneOpts = [
34
34
+
"--keep-last 3"
35
35
+
"--keep-daily 7"
36
36
+
"--keep-weekly 5"
37
37
+
"--keep-monthly 12"
38
38
+
"--tag service:${name}"
39
39
+
];
40
40
+
41
41
+
# Backup schedule (nightly at 2 AM + random delay)
42
42
+
timerConfig = {
43
43
+
OnCalendar = "02:00";
44
44
+
RandomizedDelaySec = "2h";
45
45
+
Persistent = true;
46
46
+
};
47
47
+
48
48
+
# Pre/post backup hooks for database consistency
49
49
+
backupPrepareCommand = lib.optionalString (serviceCfg.preBackup or null != null) serviceCfg.preBackup;
50
50
+
backupCleanupCommand = lib.optionalString (serviceCfg.postBackup or null != null) serviceCfg.postBackup;
51
51
+
};
52
52
+
53
53
+
in
54
54
+
{
55
55
+
imports = [ ./cli.nix ];
56
56
+
57
57
+
options.castle.backup = {
58
58
+
enable = lib.mkEnableOption "Restic backup system";
59
59
+
60
60
+
services = lib.mkOption {
61
61
+
type = lib.types.attrsOf (
62
62
+
lib.types.submodule {
63
63
+
options = {
64
64
+
enable = lib.mkOption {
65
65
+
type = lib.types.bool;
66
66
+
default = true;
67
67
+
description = "Enable backups for this service";
68
68
+
};
69
69
+
70
70
+
paths = lib.mkOption {
71
71
+
type = lib.types.listOf lib.types.str;
72
72
+
description = "Paths to back up";
73
73
+
};
74
74
+
75
75
+
exclude = lib.mkOption {
76
76
+
type = lib.types.listOf lib.types.str;
77
77
+
default = [ "*.log" "node_modules" ".git" ];
78
78
+
description = "Glob patterns to exclude from backup";
79
79
+
};
80
80
+
81
81
+
tags = lib.mkOption {
82
82
+
type = lib.types.listOf lib.types.str;
83
83
+
default = [ ];
84
84
+
description = "Tags to apply to snapshots";
85
85
+
};
86
86
+
87
87
+
preBackup = lib.mkOption {
88
88
+
type = lib.types.nullOr lib.types.str;
89
89
+
default = null;
90
90
+
description = "Command to run before backup (e.g., stop service, checkpoint DB)";
91
91
+
};
92
92
+
93
93
+
postBackup = lib.mkOption {
94
94
+
type = lib.types.nullOr lib.types.str;
95
95
+
default = null;
96
96
+
description = "Command to run after backup (e.g., restart service)";
97
97
+
};
98
98
+
};
99
99
+
}
100
100
+
);
101
101
+
default = { };
102
102
+
description = "Per-service backup configurations";
103
103
+
};
104
104
+
};
105
105
+
106
106
+
config = lib.mkIf cfg.enable {
107
107
+
# Ensure secrets are defined
108
108
+
assertions = [
109
109
+
{
110
110
+
assertion = config.age.secrets ? "restic/env";
111
111
+
message = "castle.backup requires age.secrets.\"restic/env\" to be defined";
112
112
+
}
113
113
+
{
114
114
+
assertion = config.age.secrets ? "restic/repo";
115
115
+
message = "castle.backup requires age.secrets.\"restic/repo\" to be defined";
116
116
+
}
117
117
+
{
118
118
+
assertion = config.age.secrets ? "restic/password";
119
119
+
message = "castle.backup requires age.secrets.\"restic/password\" to be defined";
120
120
+
}
121
121
+
];
122
122
+
123
123
+
# Create restic backup jobs for each enabled service
124
124
+
services.restic.backups = lib.mapAttrs mkBackupJob (
125
125
+
lib.filterAttrs (n: v: v.enable) cfg.services
126
126
+
);
127
127
+
128
128
+
# Add restic and sqlite to system packages for manual operations
129
129
+
environment.systemPackages = [ pkgs.restic pkgs.sqlite ];
130
130
+
};
131
131
+
}
+8
secrets/secrets.nix
···
62
62
# NPM registry tokens
63
63
# Contains: npmjs.org and GitHub packages auth tokens
64
64
"npmrc.age".publicKeys = all;
65
65
+
66
66
+
# Restic backup secrets (for B2 or any S3-compatible storage)
67
67
+
# restic/env.age: B2_ACCOUNT_ID and B2_ACCOUNT_KEY (or AWS_ACCESS_KEY_ID, etc.)
68
68
+
# restic/repo.age: Repository URL (e.g., b2:bucket-name:/path)
69
69
+
# restic/password.age: Repository encryption password
70
70
+
"restic/env.age".publicKeys = [ jsp alastor ];
71
71
+
"restic/repo.age".publicKeys = [ jsp alastor ];
72
72
+
"restic/password.age".publicKeys = [ jsp alastor ];
65
73
}