tangled
alpha
login
or
join now
karitham.dev
/
dotfiles
0
fork
atom
nix all the things
0
fork
atom
overview
issues
pulls
pipelines
new pds module
karitham.dev
2 months ago
865501d7
01c25e63
+677
-338
9 changed files
expand all
collapse all
unified
split
modules
default.nix
pds
nixos.nix
pds-recovery-full.nix
pds-recovery-simple.nix
services
pds.nix
secrets
pds-backup-s3.env
pds.env
systems
default.nix
reg
pds.nix
+5
-1
modules/default.nix
···
25
25
26
26
wakuna-image = self.lib.sdImageFromSystem self.nixosConfigurations.wakuna;
27
27
};
28
28
+
checks = {
29
29
+
pds-simple = pkgs.callPackage ./pds/pds-recovery-simple.nix { inherit (inputs) nixpkgs; };
30
30
+
pds-full = pkgs.callPackage ./pds/pds-recovery-full.nix { inherit (inputs) nixpkgs; };
31
31
+
};
28
32
formatter = pkgs.nixfmt-rfc-style;
29
33
devShells.default = pkgs.mkShell { packages = with pkgs; [ sops ]; };
30
34
};
···
73
77
dev = import ./dev/nixos.nix;
74
78
desktop = import ./desktop/nixos.nix;
75
79
multi-scrobbler = import ./services/multi-scrobbler.nix;
76
76
-
pds-backup = import ./services/pds.nix;
80
80
+
pds = import ./pds/nixos.nix;
77
81
};
78
82
};
79
83
}
+365
modules/pds/nixos.nix
···
1
1
+
{
2
2
+
config,
3
3
+
lib,
4
4
+
pkgs,
5
5
+
...
6
6
+
}:
7
7
+
let
8
8
+
inherit (lib)
9
9
+
mkOption
10
10
+
mkIf
11
11
+
mkMerge
12
12
+
mkEnableOption
13
13
+
mkDefault
14
14
+
types
15
15
+
;
16
16
+
17
17
+
cfg = config.services.pds-with-backups;
18
18
+
19
19
+
pdsUser = "pds";
20
20
+
pdsGroup = "pds";
21
21
+
22
22
+
secretsFiles = cfg.secretsFiles;
23
23
+
24
24
+
litestreamConfig = pkgs.writeText "litestream-pds-config.yml" ''
25
25
+
dbs:
26
26
+
- dir: ${cfg.pdsDataDir}
27
27
+
pattern: "*.sqlite"
28
28
+
recursive: true
29
29
+
watch: true
30
30
+
replica:
31
31
+
type: s3
32
32
+
path: ${cfg.s3Prefix}
33
33
+
bucket: ''${S3_BUCKET}
34
34
+
'';
35
35
+
36
36
+
restoreScript = pkgs.writeShellApplication {
37
37
+
name = "pds-litestream-restore";
38
38
+
runtimeInputs = with pkgs; [
39
39
+
awscli2
40
40
+
litestream
41
41
+
gnugrep
42
42
+
gnused
43
43
+
coreutils
44
44
+
findutils
45
45
+
gawk
46
46
+
];
47
47
+
excludeShellChecks = [
48
48
+
"SC1091"
49
49
+
"SC2046"
50
50
+
"SC2168"
51
51
+
"SC1090"
52
52
+
"SC2043"
53
53
+
];
54
54
+
text = ''
55
55
+
main() {
56
56
+
set -euo pipefail
57
57
+
58
58
+
echo "[PDS Restore] Starting automatic restore from S3..."
59
59
+
60
60
+
for f in ${toString secretsFiles}; do
61
61
+
if [ -f "$f" ]; then
62
62
+
set -a
63
63
+
source "$f"
64
64
+
set +a
65
65
+
else
66
66
+
echo "[PDS Restore] Error: Secrets file not found: $f"
67
67
+
exit 1
68
68
+
fi
69
69
+
done
70
70
+
71
71
+
if [ -z "''${S3_BUCKET:-}" ]; then
72
72
+
echo "[PDS Restore] Error: S3_BUCKET not set in secrets file"
73
73
+
exit 1
74
74
+
fi
75
75
+
76
76
+
s3Bucket="''${S3_BUCKET}"
77
77
+
s3Prefix="${cfg.s3Prefix}"
78
78
+
79
79
+
run_aws() {
80
80
+
local envArgs=()
81
81
+
if [ -n "''${AWS_ENDPOINT_URL:-}" ]; then
82
82
+
envArgs+=("AWS_ENDPOINT_URL=''${AWS_ENDPOINT_URL}")
83
83
+
fi
84
84
+
env "''${envArgs[@]}" aws "$@"
85
85
+
}
86
86
+
87
87
+
run_litestream() {
88
88
+
local envArgs=()
89
89
+
if [ -n "''${AWS_ENDPOINT_URL:-}" ]; then
90
90
+
envArgs+=("AWS_ENDPOINT_URL=''${AWS_ENDPOINT_URL}")
91
91
+
fi
92
92
+
env "''${envArgs[@]}" litestream "$@"
93
93
+
}
94
94
+
95
95
+
echo "[PDS Restore] Verifying S3 connectivity..."
96
96
+
local retries=5
97
97
+
local connected=false
98
98
+
for i in $(seq 1 $retries); do
99
99
+
if run_aws s3 ls "s3://$s3Bucket/" &>/dev/null; then
100
100
+
connected=true
101
101
+
break
102
102
+
fi
103
103
+
echo "[PDS Restore] Waiting for S3 bucket (attempt $i/$retries)..."
104
104
+
sleep 2
105
105
+
done
106
106
+
107
107
+
if [ "$connected" = false ]; then
108
108
+
echo "[PDS Restore] Error: Cannot connect to S3 bucket: $s3Bucket"
109
109
+
exit 1
110
110
+
fi
111
111
+
112
112
+
echo "[PDS Restore] S3 connection verified"
113
113
+
114
114
+
local objects
115
115
+
objects=$(run_aws s3 ls "s3://$s3Bucket/$s3Prefix" --recursive 2>/dev/null | awk '{print $4}' || true)
116
116
+
117
117
+
local databases
118
118
+
databases=$(echo "$objects" | grep '\.sqlite/' | sed 's|.*/\([^/]*\.sqlite\).*|\1|' | sort -u || true)
119
119
+
120
120
+
if [ -z "$databases" ]; then
121
121
+
echo "[PDS Restore] No databases found in S3 at s3://$s3Bucket/$s3Prefix"
122
122
+
echo "[PDS Restore] New deployment - skipping restore."
123
123
+
exit 0
124
124
+
fi
125
125
+
126
126
+
echo "[PDS Restore] Found databases to restore:"
127
127
+
echo "$databases"
128
128
+
echo ""
129
129
+
130
130
+
mkdir -p "${cfg.pdsDataDir}"
131
131
+
132
132
+
local restoredCount=0
133
133
+
for db in $databases; do
134
134
+
local localPath="${cfg.pdsDataDir}/$db"
135
135
+
local s3DbPath="$s3Prefix/$db"
136
136
+
local s3DbUrl="s3://$s3Bucket/$s3DbPath"
137
137
+
138
138
+
if [ -f "$localPath" ]; then
139
139
+
echo "[PDS Restore] Database already exists locally: $db (skipping)"
140
140
+
continue
141
141
+
fi
142
142
+
143
143
+
echo "[PDS Restore] Restoring database: $db"
144
144
+
mkdir -p "$(dirname "$localPath")"
145
145
+
146
146
+
if run_litestream restore -if-db-not-exists -if-replica-exists -o "$localPath" "$s3DbUrl"; then
147
147
+
echo "[PDS Restore] Successfully restored: $db"
148
148
+
restoredCount=$((restoredCount + 1))
149
149
+
150
150
+
if [ -f "$localPath" ]; then
151
151
+
chown ${pdsUser}:${pdsGroup} "$localPath"
152
152
+
chmod 644 "$localPath"
153
153
+
fi
154
154
+
else
155
155
+
echo "[PDS Restore] Warning: Failed to restore $db"
156
156
+
fi
157
157
+
done
158
158
+
159
159
+
echo ""
160
160
+
echo "[PDS Restore] Restore completed. Restored $restoredCount database(s)."
161
161
+
162
162
+
if [ $restoredCount -eq 0 ]; then
163
163
+
echo "[PDS Restore] No new databases restored. PDS will start with fresh state."
164
164
+
fi
165
165
+
}
166
166
+
167
167
+
main
168
168
+
'';
169
169
+
};
170
170
+
171
171
+
healthCheckScript = pkgs.writeShellScript "pds-healthcheck" ''
172
172
+
set -euo pipefail
173
173
+
174
174
+
if ! systemctl is-active --quiet bluesky-pds; then
175
175
+
echo "[PDS HealthCheck] PDS service is not running"
176
176
+
exit 1
177
177
+
fi
178
178
+
179
179
+
if [ -f "${cfg.pdsDataDir}/primary.sqlite" ]; then
180
180
+
if ! systemctl is-active --quiet litestream-pds; then
181
181
+
echo "[PDS HealthCheck] Litestream service is not running"
182
182
+
exit 1
183
183
+
fi
184
184
+
fi
185
185
+
186
186
+
echo "[PDS HealthCheck] All services healthy"
187
187
+
exit 0
188
188
+
'';
189
189
+
in
190
190
+
{
191
191
+
options.services.pds-with-backups = {
192
192
+
enable = mkEnableOption "Zero-Touch Recovery PDS with Litestream and S3 blob storage";
193
193
+
194
194
+
domain = mkOption {
195
195
+
type = types.str;
196
196
+
description = "PDS domain name (e.g., bsky.example.com).";
197
197
+
example = "bsky.example.com";
198
198
+
};
199
199
+
200
200
+
pdsDataDir = mkOption {
201
201
+
type = types.str;
202
202
+
default = "/var/lib/pds";
203
203
+
description = "PDS data directory for SQLite databases.";
204
204
+
};
205
205
+
206
206
+
secretsFiles = mkOption {
207
207
+
type = types.listOf types.path;
208
208
+
description = ''
209
209
+
List of paths to secrets files in dotenv format.
210
210
+
All files will be sourced to load credentials.
211
211
+
Required variables: PDS_JWT_SECRET, PDS_ADMIN_PASSWORD, PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX,
212
212
+
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET.
213
213
+
Optional: AWS_ENDPOINT_URL.
214
214
+
'';
215
215
+
example = [ "/run/secrets/pds.env" ];
216
216
+
};
217
217
+
218
218
+
s3Prefix = mkOption {
219
219
+
type = types.strMatching "[^/].*[^/]";
220
220
+
default = "pds";
221
221
+
description = "S3 directory prefix for Litestream replicas.";
222
222
+
example = "pds-backups";
223
223
+
};
224
224
+
225
225
+
pdsSettings = mkOption {
226
226
+
type = types.attrs;
227
227
+
default = { };
228
228
+
description = "Additional settings to pass to bluesky-pds.";
229
229
+
example = {
230
230
+
PDS_PORT = 3000;
231
231
+
PDS_DISABLE_PHONE_VERIFICATION = "true";
232
232
+
};
233
233
+
};
234
234
+
235
235
+
backupLogDir = mkOption {
236
236
+
type = types.path;
237
237
+
default = "/var/log/pds-backup";
238
238
+
description = "Directory for backup and restore logs.";
239
239
+
};
240
240
+
};
241
241
+
242
242
+
config = mkIf cfg.enable {
243
243
+
services.bluesky-pds = {
244
244
+
enable = mkDefault true;
245
245
+
settings = mkMerge [
246
246
+
{
247
247
+
PDS_HOSTNAME = cfg.domain;
248
248
+
PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT = "true";
249
249
+
PDS_DATA_DIRECTORY = cfg.pdsDataDir;
250
250
+
}
251
251
+
cfg.pdsSettings
252
252
+
];
253
253
+
environmentFiles = secretsFiles;
254
254
+
};
255
255
+
256
256
+
users.users.${pdsUser} = {
257
257
+
isSystemUser = true;
258
258
+
group = pdsGroup;
259
259
+
description = "Bluesky PDS service user";
260
260
+
};
261
261
+
users.groups.${pdsGroup} = { };
262
262
+
263
263
+
systemd.tmpfiles.rules = [
264
264
+
"d ${cfg.pdsDataDir} 0755 ${pdsUser} ${pdsGroup} -"
265
265
+
"d ${cfg.backupLogDir} 0755 ${pdsUser} ${pdsGroup} -"
266
266
+
];
267
267
+
268
268
+
systemd.services.bluesky-pds = {
269
269
+
after = [
270
270
+
"network.target"
271
271
+
"pds-restore.service"
272
272
+
];
273
273
+
wants = [ "pds-restore.service" ];
274
274
+
serviceConfig.Restart = lib.mkDefault "on-failure";
275
275
+
serviceConfig.RestartSec = "10s";
276
276
+
};
277
277
+
278
278
+
systemd.services.pds-restore = {
279
279
+
description = "PDS Automatic Restore from S3";
280
280
+
wantedBy = [ "multi-user.target" ];
281
281
+
before = [ "bluesky-pds.service" ];
282
282
+
283
283
+
serviceConfig = {
284
284
+
Type = "oneshot";
285
285
+
ExecStart = "${restoreScript}/bin/pds-litestream-restore";
286
286
+
EnvironmentFile = secretsFiles;
287
287
+
User = "root";
288
288
+
Group = "root";
289
289
+
RemainAfterExit = true;
290
290
+
291
291
+
NoNewPrivileges = true;
292
292
+
ProtectSystem = "strict";
293
293
+
ProtectHome = true;
294
294
+
PrivateTmp = true;
295
295
+
RestrictRealtime = true;
296
296
+
};
297
297
+
};
298
298
+
299
299
+
systemd.services.litestream-pds = {
300
300
+
description = "Litestream real-time replication for PDS databases";
301
301
+
after = [
302
302
+
"network.target"
303
303
+
"pds-restore.service"
304
304
+
"bluesky-pds.service"
305
305
+
];
306
306
+
requires = [ "bluesky-pds.service" ];
307
307
+
wantedBy = [ "multi-user.target" ];
308
308
+
309
309
+
serviceConfig = {
310
310
+
ExecStart = "${pkgs.litestream}/bin/litestream replicate -config ${litestreamConfig}";
311
311
+
EnvironmentFile = secretsFiles;
312
312
+
User = pdsUser;
313
313
+
Group = pdsGroup;
314
314
+
Restart = "on-failure";
315
315
+
RestartSec = "5s";
316
316
+
317
317
+
NoNewPrivileges = true;
318
318
+
ProtectSystem = "strict";
319
319
+
ProtectHome = true;
320
320
+
ReadWritePaths = [
321
321
+
cfg.pdsDataDir
322
322
+
cfg.backupLogDir
323
323
+
];
324
324
+
RestrictRealtime = true;
325
325
+
MemoryDenyWriteExecute = true;
326
326
+
};
327
327
+
};
328
328
+
329
329
+
systemd.services.pds-healthcheck = {
330
330
+
description = "PDS Health Check";
331
331
+
after = [
332
332
+
"bluesky-pds.service"
333
333
+
"litestream-pds.service"
334
334
+
];
335
335
+
requires = [ "bluesky-pds.service" ];
336
336
+
337
337
+
serviceConfig = {
338
338
+
Type = "oneshot";
339
339
+
ExecStart = healthCheckScript;
340
340
+
User = "root";
341
341
+
Group = "root";
342
342
+
343
343
+
NoNewPrivileges = true;
344
344
+
ProtectSystem = "strict";
345
345
+
ProtectHome = true;
346
346
+
};
347
347
+
348
348
+
startAt = "hourly";
349
349
+
};
350
350
+
351
351
+
systemd.timers.pds-healthcheck = {
352
352
+
wantedBy = [ "timers.target" ];
353
353
+
timerConfig = {
354
354
+
OnCalendar = "hourly";
355
355
+
Persistent = true;
356
356
+
};
357
357
+
};
358
358
+
359
359
+
environment.systemPackages = with pkgs; [
360
360
+
litestream
361
361
+
restoreScript
362
362
+
awscli2
363
363
+
];
364
364
+
};
365
365
+
}
+163
modules/pds/pds-recovery-full.nix
···
1
1
+
{ nixpkgs, pkgs }:
2
2
+
let
3
3
+
_pkgs = import nixpkgs {
4
4
+
config = { };
5
5
+
system = pkgs.stdenv.hostPlatform.system;
6
6
+
overlays = [ (import ../overlays) ];
7
7
+
};
8
8
+
in
9
9
+
_pkgs.testers.runNixOSTest {
10
10
+
name = "pds-full";
11
11
+
meta.maintainers = [ ];
12
12
+
13
13
+
nodes.machine =
14
14
+
{ pkgs, ... }:
15
15
+
{
16
16
+
imports = [ ./default.nix ];
17
17
+
18
18
+
services.minio = {
19
19
+
enable = true;
20
20
+
rootCredentialsFile = "/tmp/minio-credentials";
21
21
+
};
22
22
+
23
23
+
systemd.tmpfiles.rules = [
24
24
+
"f /tmp/minio-credentials 0600 root root - MINIO_ROOT_USER=minioadmin\\nMINIO_ROOT_PASSWORD=minioadmin123"
25
25
+
"f /run/secrets/s3.env 0600 root root - AWS_ACCESS_KEY_ID=minioadmin\\nAWS_SECRET_ACCESS_KEY=minioadmin123\\nAWS_ENDPOINT_URL=http://127.0.0.1:9000\\nS3_BUCKET=pds-test-bucket"
26
26
+
"f /run/secrets/pds.env 0600 root root - PDS_JWT_SECRET=test-jwt-secret-for-full-testing\\nPDS_ADMIN_PASSWORD=test-admin-password\\nPDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=1111111111111111111111111111111111111111111111111111111111111111"
27
27
+
];
28
28
+
29
29
+
services.pds-with-backups = {
30
30
+
enable = true;
31
31
+
domain = "test.example.com";
32
32
+
pdsDataDir = "/var/lib/pds";
33
33
+
secretsFiles = [
34
34
+
"/run/secrets/pds.env"
35
35
+
"/run/secrets/s3.env"
36
36
+
];
37
37
+
s3Bucket = "pds-test-bucket";
38
38
+
s3Prefix = "pds-replica";
39
39
+
enableStatelessBlobs = false;
40
40
+
pdsSettings = {
41
41
+
PDS_PORT = 3000;
42
42
+
PDS_DISABLE_PHONE_VERIFICATION = "true";
43
43
+
};
44
44
+
};
45
45
+
46
46
+
environment.systemPackages = with pkgs; [
47
47
+
sqlite
48
48
+
curl
49
49
+
jq
50
50
+
minio-client
51
51
+
awscli2
52
52
+
];
53
53
+
};
54
54
+
55
55
+
testScript = ''
56
56
+
import json
57
57
+
58
58
+
machine.start()
59
59
+
60
60
+
print("=== PDS Recovery Full Integration Test ===")
61
61
+
62
62
+
print("\n--- Setting up MinIO ---")
63
63
+
machine.wait_for_unit("minio.service")
64
64
+
machine.wait_for_open_port(9000)
65
65
+
machine.succeed("sleep 5")
66
66
+
67
67
+
machine.succeed("test -f /tmp/minio-credentials")
68
68
+
machine.succeed("test -f /run/secrets/s3.env")
69
69
+
machine.succeed("test -f /run/secrets/pds.env")
70
70
+
71
71
+
machine.succeed("mc alias set local http://127.0.0.1:9000 minioadmin minioadmin123")
72
72
+
machine.succeed("mc mb local/pds-test-bucket --ignore-existing")
73
73
+
print(" [PASS] MinIO bucket created")
74
74
+
75
75
+
print("\n--- Test 1: PDS Initialization ---")
76
76
+
machine.succeed("systemctl start pds-restore")
77
77
+
machine.wait_for_unit("pds-restore.service")
78
78
+
79
79
+
machine.succeed("systemctl start bluesky-pds")
80
80
+
machine.wait_for_unit("bluesky-pds.service")
81
81
+
machine.wait_for_open_port(3000)
82
82
+
83
83
+
machine.succeed("sleep 30")
84
84
+
85
85
+
health_response = machine.succeed("curl -s http://127.0.0.1:3000/xrpc/_health")
86
86
+
try:
87
87
+
health_data = json.loads(health_response)
88
88
+
assert "version" in health_data or "status" in health_data, f"Unexpected health response: {health_response}"
89
89
+
print(f" [PASS] PDS is running and healthy: {health_response}")
90
90
+
except json.JSONDecodeError:
91
91
+
assert health_response.strip() in ["", "OK"], f"Unexpected health response: {health_response}"
92
92
+
print(" [PASS] PDS is running and healthy (empty response)")
93
93
+
94
94
+
print("\n--- Test 2: Litestream replication ---")
95
95
+
machine.wait_for_unit("litestream-pds.service")
96
96
+
machine.succeed("systemctl status litestream-pds.service")
97
97
+
print(" [PASS] Litestream service is running")
98
98
+
99
99
+
print("\n--- Test 3: Database creation ---")
100
100
+
machine.succeed("sleep 30")
101
101
+
pds_files = machine.succeed("ls -la /var/lib/pds/")
102
102
+
print(f" Files in /var/lib/pds: {pds_files}")
103
103
+
104
104
+
sqlite_files = machine.succeed("find /var/lib/pds -name '*.sqlite' 2>/dev/null || true")
105
105
+
assert sqlite_files.strip() != "", f"No sqlite files found in /var/lib/pds. Output: {sqlite_files}"
106
106
+
print(f" Found sqlite files: {sqlite_files.strip()}")
107
107
+
print(" [PASS] Database files created")
108
108
+
109
109
+
print("\n--- Test 4: Simulating disaster (data loss) ---")
110
110
+
machine.succeed("systemctl stop bluesky-pds litestream-pds")
111
111
+
112
112
+
machine.succeed("rm -rf /var/lib/pds/*")
113
113
+
remaining = machine.succeed("find /var/lib/pds -name '*.sqlite' 2>/dev/null || true")
114
114
+
assert remaining.strip() == "", "Should have no sqlite files after deletion"
115
115
+
print(" [PASS] All data deleted - simulating complete server failure")
116
116
+
117
117
+
print("\n--- Test 5: Automatic restore from S3 ---")
118
118
+
restore_result = machine.succeed("pds-litestream-restore 2>&1")
119
119
+
print(f" Restore output: {restore_result}")
120
120
+
121
121
+
restored_sqlite = machine.succeed("find /var/lib/pds -name '*.sqlite' 2>/dev/null || true")
122
122
+
assert restored_sqlite.strip() != "", "Expected databases to be restored from S3"
123
123
+
print(f" Restored sqlite files: {restored_sqlite.strip()}")
124
124
+
print(" [PASS] Databases restored from S3")
125
125
+
126
126
+
print("\n--- Test 6: Data integrity verification ---")
127
127
+
machine.succeed("systemctl start bluesky-pds")
128
128
+
machine.wait_for_unit("bluesky-pds.service")
129
129
+
machine.wait_for_open_port(3000)
130
130
+
machine.succeed("sleep 30")
131
131
+
132
132
+
health_response2 = machine.succeed("curl -s http://127.0.0.1:3000/xrpc/_health")
133
133
+
try:
134
134
+
health_data2 = json.loads(health_response2)
135
135
+
assert "version" in health_data2, f"Unexpected health response: {health_response2}"
136
136
+
print(" [PASS] PDS is healthy after recovery")
137
137
+
except json.JSONDecodeError:
138
138
+
pass
139
139
+
140
140
+
print("\n--- Test 7: Litestream resumption ---")
141
141
+
machine.succeed("systemctl start litestream-pds")
142
142
+
machine.wait_for_unit("litestream-pds.service")
143
143
+
machine.succeed("sleep 10")
144
144
+
145
145
+
final_minio = machine.succeed("mc ls local/pds-test-bucket/pds-replica/ --recursive 2>/dev/null")
146
146
+
assert ".sqlite" in final_minio, "Expected ongoing replication"
147
147
+
print(" [PASS] Litestream resumed replication after recovery")
148
148
+
149
149
+
print("\n--- Test 8: Health check service ---")
150
150
+
machine.succeed("systemctl start pds-healthcheck")
151
151
+
machine.succeed("sleep 2")
152
152
+
health_log = machine.succeed("journalctl -u pds-healthcheck.service -o cat 2>/dev/null || true")
153
153
+
assert "All services healthy" in health_log, f"Expected health check message, got: {health_log}"
154
154
+
print(" [PASS] Health check service working")
155
155
+
156
156
+
print("\n" + "="*60)
157
157
+
print("ALL FULL INTEGRATION TESTS PASSED")
158
158
+
print("Zero-touch disaster recovery verified successfully")
159
159
+
print("Data integrity maintained through backup/restore cycle")
160
160
+
print("Multiple secrets files work correctly")
161
161
+
print("="*60)
162
162
+
'';
163
163
+
}
+118
modules/pds/pds-recovery-simple.nix
···
1
1
+
{ nixpkgs, pkgs }:
2
2
+
let
3
3
+
_pkgs = import nixpkgs {
4
4
+
config = { };
5
5
+
system = pkgs.stdenv.hostPlatform.system;
6
6
+
overlays = [ (import ../overlays) ];
7
7
+
};
8
8
+
in
9
9
+
_pkgs.testers.runNixOSTest {
10
10
+
name = "pds-simple";
11
11
+
meta.maintainers = [ ];
12
12
+
13
13
+
nodes.machine =
14
14
+
{ pkgs, lib, ... }:
15
15
+
{
16
16
+
imports = [ ./default.nix ];
17
17
+
18
18
+
services.pds-with-backups = {
19
19
+
enable = true;
20
20
+
domain = "test.example.com";
21
21
+
pdsDataDir = "/var/lib/pds";
22
22
+
secretsFiles = [
23
23
+
"/run/secrets/pds.env"
24
24
+
"/run/secrets/s3.env"
25
25
+
];
26
26
+
s3Bucket = "test-bucket";
27
27
+
s3Prefix = "test-pds";
28
28
+
enableStatelessBlobs = false;
29
29
+
pdsSettings = {
30
30
+
PDS_PORT = 3000;
31
31
+
PDS_DISABLE_PHONE_VERIFICATION = "true";
32
32
+
};
33
33
+
};
34
34
+
35
35
+
systemd.tmpfiles.rules = [
36
36
+
"f /run/secrets/pds.env 0644 root root - PDS_JWT_SECRET=test-jwt-secret\nPDS_ADMIN_PASSWORD=test-password\nPDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000000"
37
37
+
"f /run/secrets/s3.env 0644 root root - AWS_ACCESS_KEY_ID=test-key\nAWS_SECRET_ACCESS_KEY=test-secret\nAWS_ENDPOINT_URL=https://s3.test.example.com\nS3_BUCKET=test-bucket"
38
38
+
];
39
39
+
40
40
+
services.bluesky-pds.enable = true;
41
41
+
systemd.services.bluesky-pds.serviceConfig.ExecStart =
42
42
+
lib.mkForce "${pkgs.coreutils}/bin/echo 'PDS mocked for testing'";
43
43
+
44
44
+
environment.systemPackages = with pkgs; [
45
45
+
sqlite
46
46
+
coreutils
47
47
+
];
48
48
+
};
49
49
+
50
50
+
testScript = ''
51
51
+
machine.start()
52
52
+
machine.wait_for_unit("multi-user.target")
53
53
+
54
54
+
print("=== PDS Recovery Simple Configuration Tests ===")
55
55
+
56
56
+
print("\n--- Test 1: Systemd service creation ---")
57
57
+
services_to_check = [
58
58
+
"/etc/systemd/system/pds-restore.service",
59
59
+
"/etc/systemd/system/litestream-pds.service",
60
60
+
"/etc/systemd/system/pds-healthcheck.timer"
61
61
+
]
62
62
+
63
63
+
for service in services_to_check:
64
64
+
machine.succeed(f"test -f {service}")
65
65
+
print(f" [PASS] {service} exists")
66
66
+
67
67
+
print("\n--- Test 2: User and group creation ---")
68
68
+
machine.succeed("getent passwd pds")
69
69
+
machine.succeed("getent group pds")
70
70
+
print(" [PASS] PDS user and group exist")
71
71
+
72
72
+
print("\n--- Test 3: Directory creation ---")
73
73
+
machine.succeed("test -d /var/lib/pds")
74
74
+
machine.succeed("test -d /var/log/pds-backup")
75
75
+
pds_dir_info = machine.succeed("stat -c '%U:%G %a' /var/lib/pds")
76
76
+
assert "pds:pds" in pds_dir_info, f"Unexpected ownership: {pds_dir_info}"
77
77
+
print(" [PASS] Directories created with correct permissions")
78
78
+
79
79
+
print("\n--- Test 4: Secrets files ---")
80
80
+
machine.succeed("test -f /run/secrets/pds.env")
81
81
+
machine.succeed("test -f /run/secrets/s3.env")
82
82
+
print(" [PASS] Secrets files exist")
83
83
+
84
84
+
print("\n--- Test 5: Restore script availability ---")
85
85
+
machine.succeed("which pds-litestream-restore")
86
86
+
print(" [PASS] Restore script is available")
87
87
+
88
88
+
print("\n--- Test 6: pds-restore service configuration ---")
89
89
+
restore_config = machine.succeed("systemctl cat pds-restore.service")
90
90
+
assert "Type=oneshot" in restore_config, "pds-restore should be oneshot"
91
91
+
assert "RemainAfterExit=true" in restore_config, "pds-restore should remain after exit"
92
92
+
assert "/run/secrets/pds.env" in restore_config, "Restore should use pds.env"
93
93
+
assert "/run/secrets/s3.env" in restore_config, "Restore should use s3.env"
94
94
+
print(" [PASS] pds-restore configured correctly")
95
95
+
96
96
+
print("\n--- Test 7: litestream-pds service configuration ---")
97
97
+
litestream_config = machine.succeed("systemctl cat litestream-pds.service")
98
98
+
assert "User=pds" in litestream_config, "litestream should run as pds user"
99
99
+
assert "Restart=on-failure" in litestream_config, "litestream should restart on failure"
100
100
+
assert "/run/secrets/pds.env" in litestream_config, "Litestream should use pds.env"
101
101
+
assert "/run/secrets/s3.env" in litestream_config, "Litestream should use s3.env"
102
102
+
print(" [PASS] litestream-pds configured correctly")
103
103
+
104
104
+
print("\n--- Test 8: PDS service dependencies ---")
105
105
+
pds_config = machine.succeed("systemctl cat bluesky-pds.service")
106
106
+
assert "pds-restore.service" in pds_config, "PDS should depend on restore service"
107
107
+
print(" [PASS] PDS service correctly depends on restore")
108
108
+
109
109
+
print("\n--- Test 9: Package dependencies ---")
110
110
+
machine.succeed("which litestream")
111
111
+
machine.succeed("which aws")
112
112
+
print(" [PASS] Required packages are available")
113
113
+
114
114
+
print("\n" + "="*60)
115
115
+
print("ALL SIMPLE CONFIGURATION TESTS PASSED")
116
116
+
print("="*60)
117
117
+
'';
118
118
+
}
-319
modules/services/pds.nix
···
1
1
-
{
2
2
-
config,
3
3
-
lib,
4
4
-
pkgs,
5
5
-
...
6
6
-
}:
7
7
-
let
8
8
-
cfg = config.services.pds-backup;
9
9
-
10
10
-
pdsUser = config.systemd.services.bluesky-pds.serviceConfig.User or "pds";
11
11
-
pdsGroup = config.systemd.services.bluesky-pds.serviceConfig.Group or "pds";
12
12
-
13
13
-
restoreScript = pkgs.writeShellApplication {
14
14
-
name = "pds-restore";
15
15
-
runtimeInputs = with pkgs; [
16
16
-
awscli2
17
17
-
gnutar
18
18
-
coreutils
19
19
-
];
20
20
-
excludeShellChecks = [ "SC1091" ];
21
21
-
text = ''
22
22
-
echo "Starting PDS restore..."
23
23
-
24
24
-
if [ -f "${cfg.s3CredentialsFile}" ]; then
25
25
-
set -a; source "${cfg.s3CredentialsFile}"; set +a
26
26
-
else
27
27
-
echo "Error: Credentials file not found at ${cfg.s3CredentialsFile}"
28
28
-
exit 1
29
29
-
fi
30
30
-
31
31
-
backups=$(aws s3 ls "s3://$S3_BUCKET/backups/")
32
32
-
if [ -z "$backups" ]; then
33
33
-
echo "Error: No backups found in S3"
34
34
-
exit 1
35
35
-
fi
36
36
-
37
37
-
LATEST=$(echo "$backups" | sort | tail -1 | awk '{print $4}')
38
38
-
echo "Latest backup: $LATEST"
39
39
-
40
40
-
local_file="/tmp/$LATEST"
41
41
-
echo "Downloading backup..."
42
42
-
if ! aws s3 cp "s3://$S3_BUCKET/backups/$LATEST" "$local_file"; then
43
43
-
echo "Error: Failed to download backup"
44
44
-
exit 1
45
45
-
fi
46
46
-
47
47
-
echo "Stopping PDS service..."
48
48
-
systemctl stop bluesky-pds
49
49
-
50
50
-
echo "Clearing existing data..."
51
51
-
rm -rf ${cfg.pdsDataDir}/*
52
52
-
53
53
-
echo "Extracting backup..."
54
54
-
tar -xzf "$local_file" -C ${cfg.pdsDataDir}
55
55
-
rm -f "$local_file"
56
56
-
57
57
-
echo "Setting ownership..."
58
58
-
chown -R ${pdsUser}:${pdsGroup} ${cfg.pdsDataDir}
59
59
-
60
60
-
echo "Starting PDS service..."
61
61
-
systemctl start bluesky-pds
62
62
-
63
63
-
echo "Restore completed successfully."
64
64
-
'';
65
65
-
};
66
66
-
67
67
-
backupScript = pkgs.writeShellApplication {
68
68
-
name = "pds-backup-script";
69
69
-
runtimeInputs = with pkgs; [
70
70
-
awscli2
71
71
-
gnutar
72
72
-
gzip
73
73
-
coreutils
74
74
-
];
75
75
-
bashOptions = [ "errexit" ];
76
76
-
text = ''
77
77
-
log() {
78
78
-
echo "$(date): $1" | tee -a "$LOG_FILE"
79
79
-
}
80
80
-
81
81
-
fail() {
82
82
-
log "ERROR: $1"
83
83
-
systemctl restart bluesky-pds 2>/dev/null || log "WARNING: Failed to restart PDS service"
84
84
-
exit 1
85
85
-
}
86
86
-
87
87
-
cleanup_old_logs() {
88
88
-
find "$LOG_DIR" -name "*.log" -mtime +90 -delete
89
89
-
if [ "$(find "$LOG_FILE" -mtime +30 2>/dev/null)" ]; then
90
90
-
mv "$LOG_FILE" "$LOG_FILE.old" && touch "$LOG_FILE"
91
91
-
fi
92
92
-
if [ "$(wc -l < "$LOG_FILE" 2>/dev/null)" -gt 1000 ]; then
93
93
-
mv "$LOG_FILE" "$LOG_FILE.old" && touch "$LOG_FILE"
94
94
-
fi
95
95
-
}
96
96
-
97
97
-
mkdir -p "$LOG_DIR"
98
98
-
DATE_LABEL=$(date +"%Y%m%d-%H%M")
99
99
-
LOG_FILE="$LOG_DIR/$DATE_LABEL.log"
100
100
-
ARCHIVE_FILE="/tmp/pds-backup-$DATE_LABEL.tar.gz"
101
101
-
ARCHIVE_NAME="$DATE_LABEL.tar.gz"
102
102
-
103
103
-
log "Starting backup..."
104
104
-
105
105
-
if ! systemctl list-units --full -all | grep -Fq "bluesky-pds.service"; then
106
106
-
fail "PDS service not found"
107
107
-
fi
108
108
-
109
109
-
log "Stopping PDS service..."
110
110
-
if ! systemctl stop bluesky-pds 2>/dev/null; then
111
111
-
log "Failed to stop PDS service"
112
112
-
fi
113
113
-
114
114
-
if [ ! -d "$PDS_DATA_DIR" ]; then
115
115
-
fail "Source directory $PDS_DATA_DIR does not exist"
116
116
-
fi
117
117
-
118
118
-
log "Creating archive..."
119
119
-
if ! tar -czf "$ARCHIVE_FILE" -C "$PDS_DATA_DIR" . 2>> "$LOG_FILE"; then
120
120
-
fail "Failed to create archive"
121
121
-
fi
122
122
-
123
123
-
log "Uploading to S3..."
124
124
-
attempt=1
125
125
-
while [ "$attempt" -le "$MAX_RETRIES" ]; do
126
126
-
if aws s3 cp "$ARCHIVE_FILE" "s3://$S3_BUCKET/backups/$ARCHIVE_NAME" 2>> "$LOG_FILE"; then
127
127
-
log "Upload successful"
128
128
-
break
129
129
-
else
130
130
-
if [ "$attempt" -lt "$MAX_RETRIES" ]; then
131
131
-
log "Upload failed, retrying in $RETRY_INTERVAL seconds..."
132
132
-
sleep "$RETRY_INTERVAL"
133
133
-
else
134
134
-
fail "Upload failed after $MAX_RETRIES attempts"
135
135
-
fi
136
136
-
fi
137
137
-
((attempt++))
138
138
-
done
139
139
-
140
140
-
rm -f "$ARCHIVE_FILE"
141
141
-
142
142
-
log "Starting PDS service..."
143
143
-
if ! systemctl start bluesky-pds 2>/dev/null; then
144
144
-
fail "Failed to start PDS service"
145
145
-
fi
146
146
-
147
147
-
log "Cleaning up old logs..."
148
148
-
cleanup_old_logs
149
149
-
150
150
-
log "Backup completed successfully"
151
151
-
'';
152
152
-
};
153
153
-
154
154
-
litestreamConfigFile = pkgs.writeText "litestream-pds-config.yml" ''
155
155
-
dbs:
156
156
-
- dir: ${cfg.pdsDataDir}
157
157
-
pattern: "*.sqlite"
158
158
-
recursive: true
159
159
-
watch: true
160
160
-
replica:
161
161
-
type: s3
162
162
-
path: ${cfg.s3Prefix}
163
163
-
endpoint: ''${AWS_ENDPOINT_URL}
164
164
-
bucket: ''${S3_BUCKET}
165
165
-
access-key-id: ''${AWS_ACCESS_KEY_ID}
166
166
-
secret-access-key: ''${AWS_SECRET_ACCESS_KEY}
167
167
-
'';
168
168
-
169
169
-
litestreamRestore = pkgs.writeShellApplication {
170
170
-
name = "pds-litestream-restore";
171
171
-
runtimeInputs = with pkgs; [
172
172
-
awscli2
173
173
-
litestream
174
174
-
gnugrep
175
175
-
coreutils
176
176
-
];
177
177
-
excludeShellChecks = [ "SC1091" ];
178
178
-
text = ''
179
179
-
set -e
180
180
-
181
181
-
if [ -f "${cfg.s3CredentialsFile}" ]; then
182
182
-
set -a; source "${cfg.s3CredentialsFile}"; set +a
183
183
-
else
184
184
-
echo "Error: Credentials file not found at ${cfg.s3CredentialsFile}"
185
185
-
exit 1
186
186
-
fi
187
187
-
188
188
-
systemctl stop bluesky-pds
189
189
-
190
190
-
S3_PREFIX="${cfg.s3Prefix}/"
191
191
-
S3_URI="s3://$S3_BUCKET/$S3_PREFIX"
192
192
-
MAP=$(aws s3 ls "$S3_URI" --recursive --endpoint-url "$AWS_ENDPOINT_URL" | grep -oE "$S3_PREFIX.+\.sqlite/" | sort -u)
193
193
-
194
194
-
if [ -z "$MAP" ]; then
195
195
-
echo "No databases found in S3."
196
196
-
exit 2
197
197
-
fi
198
198
-
199
199
-
for S3_DB_PATH in $MAP; do
200
200
-
REL_PATH=''${S3_DB_PATH#"$S3_PREFIX"}
201
201
-
REL_PATH=''${REL_PATH%/}
202
202
-
S3_DB_REPLICA_URL="s3://$S3_BUCKET/$S3_DB_PATH?endpoint=$AWS_ENDPOINT_URL"
203
203
-
S3_DB_REPLICA_URL=''${S3_DB_REPLICA_URL%/}
204
204
-
205
205
-
litestream restore -if-db-not-exists -if-replica-exists -o "${cfg.pdsDataDir}/$REL_PATH" "$S3_DB_REPLICA_URL"
206
206
-
207
207
-
chown ${pdsUser}:${pdsGroup} "${cfg.pdsDataDir}/$REL_PATH"
208
208
-
done
209
209
-
210
210
-
systemctl start bluesky-pds
211
211
-
'';
212
212
-
};
213
213
-
in
214
214
-
{
215
215
-
options.services.pds-backup = {
216
216
-
enable = lib.mkEnableOption "PDS backup with Litestream and S3 archive";
217
217
-
pdsDataDir = lib.mkOption {
218
218
-
type = lib.types.path;
219
219
-
default = "/var/lib/pds";
220
220
-
description = "PDS data directory.";
221
221
-
};
222
222
-
pdsSecretsFile = lib.mkOption {
223
223
-
type = lib.types.path;
224
224
-
description = "Path to PDS secrets file (dotenv format).";
225
225
-
};
226
226
-
s3Prefix = lib.mkOption {
227
227
-
type = lib.types.strMatching "[^/].*[^/]";
228
228
-
default = "pds";
229
229
-
description = "S3 directory subpath.";
230
230
-
};
231
231
-
s3CredentialsFile = lib.mkOption {
232
232
-
type = lib.types.path;
233
233
-
description = "Path to S3 credentials file (containing AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET, etc).";
234
234
-
};
235
235
-
pdsSettings = lib.mkOption {
236
236
-
type = lib.types.attrs;
237
237
-
default = { };
238
238
-
description = "Additional settings to pass to bluesky-pds.";
239
239
-
};
240
240
-
backupLogDir = lib.mkOption {
241
241
-
type = lib.types.path;
242
242
-
default = "/var/log/pds-backup";
243
243
-
description = "Directory for backup logs.";
244
244
-
};
245
245
-
maxRetries = lib.mkOption {
246
246
-
type = lib.types.int;
247
247
-
default = 3;
248
248
-
description = "Maximum number of retry attempts for S3 upload.";
249
249
-
};
250
250
-
retryInterval = lib.mkOption {
251
251
-
type = lib.types.int;
252
252
-
default = 60;
253
253
-
description = "Seconds to wait between retry attempts.";
254
254
-
};
255
255
-
};
256
256
-
257
257
-
config = lib.mkIf cfg.enable {
258
258
-
services.bluesky-pds = {
259
259
-
enable = true;
260
260
-
settings = lib.mkMerge [
261
261
-
{ PDS_SQLITE_DISABLE_WAL_AUTO_CHECKPOINT = "true"; }
262
262
-
cfg.pdsSettings
263
263
-
];
264
264
-
environmentFiles = [ cfg.pdsSecretsFile ];
265
265
-
};
266
266
-
267
267
-
systemd.services.litestream-pds = {
268
268
-
description = "Litestream backup for PDS databases";
269
269
-
after = [
270
270
-
"network.target"
271
271
-
"bluesky-pds.service"
272
272
-
];
273
273
-
requires = [ "bluesky-pds.service" ];
274
274
-
wantedBy = [ "multi-user.target" ];
275
275
-
276
276
-
serviceConfig = {
277
277
-
ExecStart = "${pkgs.litestream}/bin/litestream replicate -config ${litestreamConfigFile}";
278
278
-
EnvironmentFile = cfg.s3CredentialsFile;
279
279
-
User = pdsUser;
280
280
-
Group = pdsGroup;
281
281
-
Restart = "on-failure";
282
282
-
RestartSec = "5s";
283
283
-
NoNewPrivileges = true;
284
284
-
ProtectSystem = "full";
285
285
-
RestrictRealtime = true;
286
286
-
};
287
287
-
};
288
288
-
289
289
-
systemd.services.pds-backup = {
290
290
-
description = "Backup PDS data to S3";
291
291
-
serviceConfig = {
292
292
-
ExecStart = "${backupScript}/bin/pds-backup-script";
293
293
-
Environment = [
294
294
-
"PDS_DATA_DIR=${cfg.pdsDataDir}"
295
295
-
"LOG_DIR=${cfg.backupLogDir}"
296
296
-
"MAX_RETRIES=${toString cfg.maxRetries}"
297
297
-
"RETRY_INTERVAL=${toString cfg.retryInterval}"
298
298
-
];
299
299
-
EnvironmentFile = [ cfg.s3CredentialsFile ];
300
300
-
User = "root";
301
301
-
Type = "oneshot";
302
302
-
};
303
303
-
};
304
304
-
305
305
-
systemd.timers.pds-backup = {
306
306
-
wantedBy = [ "timers.target" ];
307
307
-
timerConfig = {
308
308
-
OnCalendar = "daily";
309
309
-
Persistent = true;
310
310
-
};
311
311
-
};
312
312
-
313
313
-
environment.systemPackages = [
314
314
-
restoreScript
315
315
-
litestreamRestore
316
316
-
pkgs.litestream
317
317
-
];
318
318
-
};
319
319
-
}
+2
-6
secrets/pds-backup-s3.env
···
1
1
-
AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:5oKU0U+FeR666BTNL7gdb8Wnts/nvhCYDlGtetcq+Dc=,iv:i3i4ykyAR3tpfpDoke6renys9tZp/a6/JpRe7ajAnsg=,tag:S3yDGCIl+wG9tedXbcPbmw==,type:str]
2
2
-
AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:3UEACvF1q20QRdSxVcTrMJNsjZmSsGHmX0fq98KMvnJJffjYO6h+ClNT+38aDhgh8Dd2VRujYMVTI6RXPn28DQ==,iv:bBEs9lMhDMoeH3HymUQEOT8Y13457xcrevKI89nXBeg=,tag:eSekCTOKUu2u1S7/Mcy9sg==,type:str]
3
3
-
AWS_ENDPOINT_URL=ENC[AES256_GCM,data:bqp2c1pPg1ZWG2w8XF9dm+3wKMYFucxzNLmvlhoKYuIxJX5CZwUSrmDXsx4FXZtq0l2fIWUIMCRurE49Jq8QtL8M6GQ=,iv:j4uL5y6E3qd6KS7DALc/GdL2/HgpheQj2J/uuLZ7XZU=,tag:dWVbavob4IC02bY+EqfYGA==,type:str]
4
4
-
S3_BUCKET=ENC[AES256_GCM,data:1q3k8mjG6kM=,iv:3nktB4AvrGmH6MfIijV3mPajO9088UWfuBOeM99hwCQ=,tag:cfLCokyYHWqvtCeokIGrtg==,type:str]
5
1
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2SkJUdWdYclR0U1pPUHpn\nc3pWSml5b3FOR3cvaVhNczJrbHA4NG83U1JZClVibTgyc3JBWC9LSXVJVmhZckxT\nSk1sN040MmRKd3BodUdFOVRxMTdGcUEKLS0tIHhjZUs5TXhhMm1nK3RQc3BJMTJG\nZmQ5cGlRbXNiczBwUnd1aTFaWk94bUkKjQEr03lQRuWxzQ6uTCRgpTj3C/FwBFwz\nQoYYAyqN5RBAJvN+7TFewgGgSBu+bE2RFazAxOizdXQXAmgceZnT5A==\n-----END AGE ENCRYPTED FILE-----\n
6
2
sops_age__list_0__map_recipient=age17cxj5zwkkxjkjvmpskpkyh6yt4xj4l8h6jyjxez3nmq6y9tvhqjsdp0m5j
7
3
sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpdEo3Nlp6RmgvZER2ZWtS\nU2dFazU0Q2FndnZBL1g2Ky9lcjNKRDBoaGlBCmlIVG9iUXVmazkzb2NTeDhXKzhT\nc3JQZjhhb0h3VjI2b2tpcGxiVG9NQmMKLS0tIG9tTjlPLzVQSjRLSkw2djM5QzQ2\nenJ6RFJUazlmQStJRStudlhuV1Rsc1UKMaAFJszBkgONafeLGMYO1zzS+dHzX3Uh\n8wwni17QDZTTE5Q2P8KPBquVWyzh7UUI3GhbxuZhANfs/RA5AQlg2w==\n-----END AGE ENCRYPTED FILE-----\n
8
4
sops_age__list_1__map_recipient=age1j6j2ldpsj7jmchstwl3nktvatut9hzxnemmy6py84rrga5eaf93q5w8s39
9
9
-
sops_lastmodified=2025-10-24T22:20:59Z
10
10
-
sops_mac=ENC[AES256_GCM,data:h7telhUinjAmbijnepEv3NXjvtYuCvQzCfQ6PA5vqr9QY8kAUlqICLofOP9+SVocUR0LlXsoKfmLcdIMMpZG1Jb6D0PQ8RuG4sTm5BSrWvvPj+U/Cm8JuPAqtOie0vWPBsL8vHg9BQGrUWEVtiK9fBLVmEvafVj9mmKzbwBZNOg=,iv:hzi5UOVUAdQW2wY8fAxLysfaLUertVLYo1D4H9Ubz+4=,tag:fRB9Lerke0aDkmcu+OzIfw==,type:str]
5
5
+
sops_lastmodified=2026-01-04T15:43:03Z
6
6
+
sops_mac=ENC[AES256_GCM,data:Icr7CvMbO79L0m+nzWVE3anuzOP8cO86apYB5Ax9OEtS6kZG/V94YR+hP8Zles+65brIux9cVcQuwiJUBddxt3ifMxqatjvj3ARAsKisBCTTEHgAaLos6Np7jHt/Av37ejIb90jU7bE3pFRjkBqimYPhqkVtlM00XmNzuZZFAvI=,iv:yduWTYvLtTG6Qc/rzbzyRNmchQ1OdnxziNd0DHwyYDM=,tag:6f0uFuLzj9yIovVrtomD9g==,type:str]
11
7
sops_unencrypted_suffix=_unencrypted
12
8
sops_version=3.11.0
+13
-2
secrets/pds.env
···
3
3
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=ENC[AES256_GCM,data:OjAyEohVNTmWKapzJX1e/ed6ZbWL3I5+SgM93Bm89qymJWGX90ssca+fAcny38g2sZJkiJWjfUulE5sTMAbGog==,iv:vquXtXGhDMEFNJ6tqE76HTv1PZtGjtIL3+K05eY7KxE=,tag:sVDuGtkxfYL6csJ4d80+dQ==,type:str]
4
4
PDS_EMAIL_SMTP_URL=ENC[AES256_GCM,data:u2ylh4YK4aR5WhOFTazwLk3i2CU71Yakcb3pyoc2D+i94KMX0xEWKLb0xKpBnKyVBEI9CLMpiLUOPR1CuJRVv/aOR4yBIA==,iv:/NuzdQxYq0r4z9iTLGRi4aa/qBxFp0NeyNIOxIoEjbY=,tag:3dhfx9BbMdTahW02mYwKjA==,type:str]
5
5
PDS_EMAIL_FROM_ADDRESS=ENC[AES256_GCM,data:Xqly+3nqg8VpOQ==,iv:Dx0YD81azbMt2iT6EaqmJRXCXxhycM/pyW9eFH1cHGE=,tag:zi6bE3BNu443IFaTZDe3Wg==,type:str]
6
6
+
PDS_BLOBSTORE_S3_BUCKET=ENC[AES256_GCM,data:JPb/0f2Q/6U=,iv:LBjZhWB1gF6G+z+clxqmP9BPvD8zixH9wnT2Wt7ezFo=,tag:EM5oM36aIPW5Iyv+6P3L0w==,type:str]
7
7
+
PDS_BLOBSTORE_S3_REGION=ENC[AES256_GCM,data:ze3/cw==,iv:lQH76YOv/cA2NADm0h9h2m60Q9KPIayVU2Vo9IQV3Ho=,tag:GulJcJ2eJ5Dn3RwaRnpR8g==,type:str]
8
8
+
PDS_BLOBSTORE_S3_ENDPOINT=ENC[AES256_GCM,data:eYdOT/bHeURLI6gvxfVmsb68MrZ/m0hwfotxbekpLDu7nM6zVykTrUoF0gyr0tnh0JZFNdqMjfjfijiM7HNBoQSCYqs=,iv:sq987mZrdj1d6ELyoKuVPl+ZB82nRAfGwtDEhw+pUqU=,tag:/nilWfeOiy2Gjgep1RkUuQ==,type:str]
9
9
+
PDS_BLOBSTORE_S3_FORCE_PATH_STYLE=ENC[AES256_GCM,data:UKfn+o4=,iv:cWjc04TNNaiOap7NH4gVgmV6/EzNZUmXQuk4xjyJcxA=,tag:hRXEc7wbGC7kMN4KQKAmzQ==,type:str]
10
10
+
PDS_BLOBSTORE_S3_ACCESS_KEY_ID=ENC[AES256_GCM,data:1lCFrKcGFWIwka4fsxw4N2HrYG1EEBvNTf3NbgFMa5k=,iv:+CjSrZKsiHt2mdfaXLD/fPlckuidUhMfvIMOT1y18Ds=,tag:vxFX1CEPXP51vo/ANC74VQ==,type:str]
11
11
+
PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:6WdWC+7H5p8+tQ10G27oksww/y0Qa0iQN/UsgFmac/FOEf5+jnFGYMXRKgKzLpEzsCyqqZ906zHsyEiTV+2iCA==,iv:Dz7Mcr5qr9iRkykSQUmnmk7rjLmQgiKZxOLJq5Z0sJs=,tag:xRZ1N2snHB0NrERBFlUb9w==,type:str]
12
12
+
PDS_BLOBSTORE_S3_UPLOAD_TIMEOUT_MS=ENC[AES256_GCM,data:H9XeBJJ0,iv:c5q14JkgHA0hmFRskinfVZyfIeB7hiT7lA0LD5TDTZQ=,tag:neUysh5VmF0GFYb9L0ujGA==,type:str]
13
13
+
AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:TkeTnnIwu/kzaE0DZzzAM6zTSWVd9Qlrf0PrtKFIb+s=,iv:vDpULwiHD4zkZJdLZkNYUNttnV4w89/1JREt808BHUE=,tag:jGT9sAozIIGOwq3D0Xycvg==,type:str]
14
14
+
AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:e1dHdjB23FbUObOrmqP3+CsRVHTKleQDLktdiIyLUmsM53uL6RGtX/+1eVUXj9kszjkZCnKG2AOGKrsWWUgnqw==,iv:I/a5Ayj5RYZC63s92pX84c6sMXVgLL7oL5xwG6o6x/0=,tag:vDD9WeN5+w+0TAiu4Ssz/Q==,type:str]
15
15
+
AWS_ENDPOINT_URL=ENC[AES256_GCM,data:3t0VPGK36rV9V37kdMZYLamVDN3vh9zCgl7oJ6EnsPJwlYrH1GQ9zfOATk5w3RqICuqkRLAW+xmPOVUfdOedArfSY74=,iv:p2AHzQhmHA+hmVQohS5Go+f4W4skO1kjXBRsXkgNgyE=,tag:AEgdxUt9J0SWdL8MZc6h9A==,type:str]
16
16
+
S3_BUCKET=ENC[AES256_GCM,data:tJJxdFgiuXA=,iv:yEOuQZWSsN3zAdXSHJPhKTBIIEN4y8E2X+Q/XwstZ7E=,tag:HEHaY+I2XlzdOEfXYBtTWw==,type:str]
6
17
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwS0dHSU9pd2tWVzZPZDlQ\nYVd6NjF0SkFrSGNUUUprUGN0c1Yzdk5BYXpZCnBiajU0cGVDQ0UwUVl6dHg5SjFO\nU3RScFhiTlZuYUJ6aC84K1F6YnEza2cKLS0tIDNFZ21BdHRTQTFGMkR3MDBxOHhG\nOVByYktJdjlsbTlNSDMvOTkvK0FyeU0KYBMndJPeIMpnqSrAUs0Em5Pbm7GBo/0e\nsaBKyYhn/pIPtJCyOiISqfXwMFHsiWCtd3dEejunG0x9eEkjWzDKqw==\n-----END AGE ENCRYPTED FILE-----\n
7
18
sops_age__list_0__map_recipient=age17cxj5zwkkxjkjvmpskpkyh6yt4xj4l8h6jyjxez3nmq6y9tvhqjsdp0m5j
8
19
sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2WW9pWHVZSWl4eWdCUGUv\nT3VSdVVPdVh0bENRanFjNnJVdlhQS2JvbzNRCmY2YnZmVlNhSDFYZ2VDWnN1MHp5\nS3lyMEpOa0hmTVdFeUtIQ1lrMWZVODgKLS0tIC93Q2hzb0pxdUNySEYwa1VyUjFN\nNk9tekkvUC9JT3NpcENqTHF5TUIyWXMKJ082QrvQ6QRfJ1RZqL9sSyKCmTLi+I9R\nxuo8E1SfP/204OQaihP6+9cCbLh3yYtESaAFw2Alisnoe4PvSR2I9g==\n-----END AGE ENCRYPTED FILE-----\n
9
20
sops_age__list_1__map_recipient=age1j6j2ldpsj7jmchstwl3nktvatut9hzxnemmy6py84rrga5eaf93q5w8s39
10
10
-
sops_lastmodified=2025-10-24T21:30:51Z
11
11
-
sops_mac=ENC[AES256_GCM,data:T7pttRlFl7HQwRY+AAXIX00wsABEyiHJsKPxXatl50Auaa+S3JJss56tVhphp2iMch7Z0nxnVT9kS03eAU1RUV0wHgCyu0QWNAib7PbUQIQvVq7pqF8SXORNuvwHJj2S9niASIDQqIenLYzUCRyq6tBnJ/XkW9JbiaOxs6w75G4=,iv:nvo7iAVjYkN45RZsqV3nGWzoaPcg74NRdhzy44lIi+Y=,tag:nIPrJy1srFn/nFTRjtdLbg==,type:str]
21
21
+
sops_lastmodified=2026-01-04T15:13:30Z
22
22
+
sops_mac=ENC[AES256_GCM,data:C+FIoCt7i/dMOwGs2BA0/t4XS41MkIO87D5lVXfwRPXihewG73574Xp5VeyQ5/+SH1OHrsXDTIE+TBrbzlnOr6LCbfqHaoRnYnhT0E8Xk84+z3Ihky2ziSViArYJjWOHMhKHyu+6XiaOD7UV/Cuimp/79PVNwk8VGD9dyUuvXVg=,iv:d11RcB3KVt9Me6I8gVVjs80jHQE5ss1j3iRwjvSdhmw=,tag:VhmTUP+M5cGeYaE0AAZUfA==,type:str]
12
23
sops_unencrypted_suffix=_unencrypted
13
24
sops_version=3.11.0
+4
-1
systems/default.nix
···
4
4
5
5
config.easy-hosts = {
6
6
shared = {
7
7
-
modules = [ ../modules/core.nix ];
7
7
+
modules = [
8
8
+
../modules/core.nix
9
9
+
{ nixpkgs.overlays = [ self.overlays.default ]; }
10
10
+
];
8
11
9
12
specialArgs = { inherit inputs self; };
10
13
};
+7
-9
systems/reg/pds.nix
···
1
1
{ config, self, ... }:
2
2
{
3
3
imports = [
4
4
-
self.nixosModules.pds-backup
4
4
+
self.nixosModules.pds
5
5
../../modules/services/acme-nginx.nix
6
6
];
7
7
···
15
15
format = "dotenv";
16
16
sopsFile = ../../secrets/cloudflare-api.env;
17
17
};
18
18
-
secrets.pds-s3 = {
19
19
-
format = "dotenv";
20
20
-
sopsFile = ../../secrets/pds-backup-s3.env;
21
21
-
};
22
18
};
23
19
24
24
-
services.pds-backup = {
20
20
+
services.pds-with-backups = {
25
21
enable = true;
26
26
-
pdsSecretsFile = config.sops.secrets.pds.path;
27
27
-
s3CredentialsFile = config.sops.secrets.pds-s3.path;
22
22
+
domain = "0xf.fr";
23
23
+
secretsFiles = [ config.sops.secrets.pds.path ];
24
24
+
s3Prefix = "backups";
28
25
29
26
pdsSettings = {
30
30
-
PDS_HOSTNAME = "0xf.fr";
27
27
+
PDS_PORT = 3000;
28
28
+
PDS_BLOBSTORE_DISK_LOCATION = null;
31
29
};
32
30
};
33
31