All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 54m48s
Admin → Backup: Ziel (SFTP oder Nextcloud/WebDAV) konfigurieren, Verbindung testen, Jetzt sichern,
Zeitplan (aus/täglich/wöchentlich) + Aufbewahrung. Best Practice: verschlüsselt + off-site.
- Verschlüsselung: Backup als tar.gz (database.dump + MinIO-Dateien) → AES-256-GCM mit Passphrase.
Zugangsdaten + Passphrase verschlüsselt in DB (src/lib/crypto-secret.ts, Schlüssel aus Server-Secret)
- Engine (src/lib/backup): pg_dump + archiver-Stream aus MinIO + Stream-Verschlüsselung + Upload + Prune
- Ziel-Adapter: SFTP (ssh2-sftp-client) + WebDAV (webdav), je test/upload/list/delete
- API (SERVER_ADMIN): /api/admin/backup/{config,test,run}; öffentlich per CRON_SECRET: /api/cron/backup
- Scheduler in server-custom.js (stündliche Fälligkeitsprüfung)
- Dockerfile: postgresql16-client (pg_dump) + runtime-Libs; next.config serverExternalPackages
- scripts/decrypt-backup.js + docs/BACKUP.md (Restore-Anleitung), .env.example (CRON_SECRET, BACKUP_ENC_KEY)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
1.9 KiB
JavaScript
52 lines
1.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Entschlüsselt ein Lageplan-Backup (.tar.gz.enc) → .tar.gz
|
|
*
|
|
* Verwendung:
|
|
* node scripts/decrypt-backup.js <input.tar.gz.enc> <output.tar.gz> [passphrase]
|
|
* (ohne Passphrase-Argument wird sie interaktiv abgefragt bzw. aus BACKUP_PASSPHRASE gelesen)
|
|
*
|
|
* Danach:
|
|
* tar xzf output.tar.gz → enthält database.dump + files/<uploads>
|
|
* pg_restore --clean --if-exists -d <DB> database.dump
|
|
* files/ zurück in MinIO spielen (siehe docs/BACKUP.md)
|
|
*
|
|
* Format der .enc-Datei: [salt(16)][iv(12)][ciphertext][tag(16)], AES-256-GCM, Key = scrypt(passphrase, salt).
|
|
*/
|
|
const fs = require('fs')
|
|
const crypto = require('crypto')
|
|
|
|
const [inPath, outPath] = process.argv.slice(2)
|
|
let passphrase = process.argv[4] || process.env.BACKUP_PASSPHRASE
|
|
|
|
if (!inPath || !outPath) {
|
|
console.error('Verwendung: node decrypt-backup.js <input.tar.gz.enc> <output.tar.gz> [passphrase]')
|
|
process.exit(1)
|
|
}
|
|
if (!passphrase) {
|
|
console.error('Passphrase fehlt. Als 3. Argument oder via BACKUP_PASSPHRASE übergeben.')
|
|
process.exit(1)
|
|
}
|
|
|
|
const size = fs.statSync(inPath).size
|
|
if (size < 44) { console.error('Datei zu klein / kein gültiges Backup.'); process.exit(1) }
|
|
|
|
const fd = fs.openSync(inPath, 'r')
|
|
const head = Buffer.alloc(28)
|
|
fs.readSync(fd, head, 0, 28, 0)
|
|
const tag = Buffer.alloc(16)
|
|
fs.readSync(fd, tag, 0, 16, size - 16)
|
|
fs.closeSync(fd)
|
|
|
|
const salt = head.subarray(0, 16)
|
|
const iv = head.subarray(16, 28)
|
|
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
decipher.setAuthTag(tag)
|
|
|
|
const input = fs.createReadStream(inPath, { start: 28, end: size - 17 })
|
|
const output = fs.createWriteStream(outPath)
|
|
input.pipe(decipher).pipe(output)
|
|
output.on('finish', () => console.log('OK →', outPath))
|
|
decipher.on('error', (e) => { console.error('Entschlüsselung fehlgeschlagen (falsche Passphrase?):', e.message); process.exit(1) })
|