mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-11 08:55:22 +02:00
feat: optionally include encryption key in web server backups
This commit is contained in:
@@ -50,6 +50,7 @@ export const backups = pgTable("backup", {
|
||||
.notNull()
|
||||
.references(() => destinations.destinationId, { onDelete: "cascade" }),
|
||||
keepLatestCount: integer("keepLatestCount"),
|
||||
includeEncryptionKey: boolean("includeEncryptionKey").notNull().default(true),
|
||||
backupType: backupType("backupType").notNull().default("database"),
|
||||
databaseType: databaseType("databaseType").notNull(),
|
||||
composeId: text("composeId").references(
|
||||
@@ -160,6 +161,7 @@ const createSchema = createInsertSchema(backups, {
|
||||
mongoId: z.string().optional(),
|
||||
libsqlId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
includeEncryptionKey: z.boolean().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
|
||||
@@ -180,6 +182,7 @@ export const apiCreateBackup = createSchema.pick({
|
||||
backupType: true,
|
||||
composeId: true,
|
||||
serviceName: true,
|
||||
includeEncryptionKey: true,
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
@@ -206,7 +209,10 @@ export const apiUpdateBackup = createSchema
|
||||
metadata: true,
|
||||
databaseType: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
includeEncryptionKey: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
|
||||
@@ -4,9 +4,14 @@ import {
|
||||
createHmac,
|
||||
randomBytes,
|
||||
} from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { betterAuthSecret } from "./auth-secret";
|
||||
import { encryptionSecret } from "./encryption-secret";
|
||||
|
||||
export const ENCRYPTION_KEY_BACKUP_FILE = "encryption.key";
|
||||
|
||||
const ENCRYPTION_PREFIX = "enc:v1:";
|
||||
const IV_LENGTH = 12;
|
||||
const AUTH_TAG_LENGTH = 16;
|
||||
@@ -23,6 +28,33 @@ const decryptionKeys = encryptionSecret
|
||||
? [primaryKey, deriveKey(betterAuthSecret)]
|
||||
: [primaryKey];
|
||||
|
||||
export const exportEncryptionKey = () => primaryKey.toString("hex");
|
||||
|
||||
let restoredKey: Buffer | undefined;
|
||||
|
||||
// A backup created with "include encryption key" places the original
|
||||
// server's key at BASE_PATH when restored; use it as a last-resort
|
||||
// decryption fallback so restored values keep working on the new server.
|
||||
const loadRestoredKey = (): Buffer | undefined => {
|
||||
if (restoredKey) {
|
||||
return restoredKey;
|
||||
}
|
||||
try {
|
||||
const { BASE_PATH } = paths();
|
||||
const hex = readFileSync(
|
||||
join(BASE_PATH, ENCRYPTION_KEY_BACKUP_FILE),
|
||||
"utf8",
|
||||
).trim();
|
||||
const key = Buffer.from(hex, "hex");
|
||||
if (key.length === 32 && !key.equals(primaryKey)) {
|
||||
restoredKey = key;
|
||||
}
|
||||
} catch {
|
||||
// No restored key file present.
|
||||
}
|
||||
return restoredKey;
|
||||
};
|
||||
|
||||
export const isEncrypted = (value: string) =>
|
||||
value.startsWith(ENCRYPTION_PREFIX);
|
||||
|
||||
@@ -47,7 +79,9 @@ export const decryptValue = (value: string): string => {
|
||||
const iv = payload.subarray(0, IV_LENGTH);
|
||||
const authTag = payload.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
||||
const encrypted = payload.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
||||
for (const key of decryptionKeys) {
|
||||
const restored = loadRestoredKey();
|
||||
const keys = restored ? [...decryptionKeys, restored] : decryptionKeys;
|
||||
for (const key of keys) {
|
||||
try {
|
||||
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdtemp, rm, stat } from "node:fs/promises";
|
||||
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import {
|
||||
ENCRYPTION_KEY_BACKUP_FILE,
|
||||
exportEncryptionKey,
|
||||
} from "@dokploy/server/lib/encryption";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
@@ -78,11 +82,22 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
await execAsync(cleanupCommand);
|
||||
|
||||
await execAsync(
|
||||
`rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
`rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' --exclude='${ENCRYPTION_KEY_BACKUP_FILE}' ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
);
|
||||
|
||||
writeStream.write("Copied filesystem to temp directory\n");
|
||||
|
||||
if (backup.includeEncryptionKey) {
|
||||
// Restoring the filesystem places this file at BASE_PATH, where
|
||||
// the encryption keyring picks it up as a decryption fallback.
|
||||
await writeFile(
|
||||
join(tempDir, "filesystem", ENCRYPTION_KEY_BACKUP_FILE),
|
||||
exportEncryptionKey(),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
writeStream.write("Included encryption key in backup\n");
|
||||
}
|
||||
|
||||
await execAsync(
|
||||
// Zip all .sql files since we created more than one
|
||||
`cd ${tempDir} && zip -r ${backupFileName} *.sql filesystem/ > /dev/null 2>&1`,
|
||||
|
||||
Reference in New Issue
Block a user