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