Add passphrase-protected export/import for integrations, DNS providers, and settings

Nothing let you back up or migrate the app's own configuration short
of copying the raw SQLite file. Adds Settings -> Backup: export
decrypts every integration/DNS provider credential (normally
encrypted at rest with this server's CREDENTIALS_ENCRYPTION_KEY) and
re-encrypts the whole payload with a passphrase you choose (scrypt-
derived key, AES-256-GCM), so the file is portable to a different
instance with a different encryption key rather than being tied to
this one. Import decrypts with that passphrase and merges settings
onto the current ones; integrations/DNS providers are only added when
no existing row shares their type+name, so re-running an import never
duplicates or overwrites a working credential.

Scope is configuration only — no DNS records, secrets, IPAM, servers,
or audit/diagnostic log data.

Verified end-to-end against two isolated scratch databases with
different encryption keys (proving actual cross-instance portability,
not just round-tripping through the same key): export -> encrypt ->
write file -> decrypt on the other DB -> import -> re-decrypt the
newly created integration/provider using the target's own key,
confirming the plaintext credentials survived correctly; a wrong
passphrase failed loudly (GCM auth failure) as expected; and
re-running the same import a second time skipped both rows instead of
duplicating them. Confirmed the real dev database's mtime was
untouched throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-19 21:18:41 +02:00
1 parent b5a4c6e2d9
commit 23eb7f0d70
6 files changed
+460

No files matched your search

+20
View File
@@ -175,6 +175,22 @@ export interface AppSettings {
export type AppSettingsPatch = { [K in keyof AppSettings]?: Partial<AppSettings[K]> };
export interface EncryptedExportFile {
app: "homelab-manager-backup";
version: 1;
salt: string;
iv: string;
authTag: string;
ciphertext: string;
}
export interface ImportResult {
integrationsCreated: number;
integrationsSkipped: string[];
dnsProvidersCreated: number;
dnsProvidersSkipped: string[];
}
export interface DnsProviderField {
key: string;
label: string;
@@ -803,5 +819,9 @@ export const api = {
request<{ ok: true }>("/api/settings/test-webhook", { method: "POST", body: JSON.stringify(data) }),
purgeLogs: () =>
request<{ diagDeleted: number; auditDeleted: number }>("/api/settings/purge-logs", { method: "POST" }),
exportConfig: (passphrase: string) =>
request<EncryptedExportFile>("/api/settings/export", { method: "POST", body: JSON.stringify({ passphrase }) }),
importConfig: (passphrase: string, file: EncryptedExportFile) =>
request<ImportResult>("/api/settings/import", { method: "POST", body: JSON.stringify({ passphrase, file }) }),
},
};