Восстановление PostgreSQL из бекапа без Databasus

Резервное копирование — это не только защита данных, но и их восстановление. Databasus устроен так, чтобы бекапы оставались восстановимыми, даже если VPS с Databasus удален, вы потеряли доступ или по какой-то причине не можете открыть интерфейс. Для восстановления бекапов Databasus не нужен: они хранятся в стандартном формате, и никакого vendor lock-in нет.

Что понадобится

Чтобы восстановить бекап вручную, вам нужны:

  • Файл бекапа из вашего хранилища (локальное хранилище, S3, Google Drive и т.д.)
  • Файл метаданных из того же хранилища. Он называется так же, как файл бекапа, но с расширением .metadata.
  • Секретный ключ из ./databasus-data/secret.key (лежит в том же каталоге, что и файлы бекапов, обычно /opt/databasus/)

Структура файлов

Каждый бекап состоит из двух файлов в вашем хранилище (локальном или облачном):

  • {database-name}-{timestamp}-{backup-id} — зашифрованные и сжатые данные бекапа
  • {database-name}-{timestamp}-{backup-id}.metadata — JSON-файл с параметрами шифрования

Файл метаданных содержит соль шифрования и IV (nonce) в формате Base64:

{
  "backupId": "550e8400-e29b-41d4-a716-446655440000",
  "encryptionSalt": "base64-encoded-salt",
  "encryptionIV": "base64-encoded-nonce",
  "encryption": "encrypted"
}

Расшифровка

Databasus использует шифрование AES-256-GCM с выведением ключа через PBKDF2. У каждого бекапа свой уникальный ключ шифрования, который выводится из:

  • мастер-ключа (из файла secret.key)
  • ID бекапа
  • случайной соли (хранится в метаданных)

Расшифруйте бекап этим Python-скриптом:

import json
import base64
import struct
import os
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256

# Constants from Databasus encryption
MAGIC_BYTES = b"PGRSUS01"
HEADER_LENGTH = 64
CHUNK_SIZE = 1024 * 1024
PBKDF2_ITERATIONS = 100000


def decrypt_backup(backup_file, metadata_file, master_key):
    """
    Decrypt a Databasus backup file using metadata and master key.

    Args:
        backup_file: Path to encrypted backup file
        metadata_file: Path to metadata JSON file
        master_key: Master key from ./databasus-data/secret.key
    """
    # Validate files exist
    if not os.path.exists(backup_file):
        print(f"Error: Backup file not found: {backup_file}")
        return

    if not os.path.exists(metadata_file):
        print(f"Error: Metadata file not found: {metadata_file}")
        return

    # Read metadata
    with open(metadata_file, "r") as f:
        metadata = json.load(f)

    # Check if file is encrypted (case-insensitive check)
    encryption_status = metadata.get("encryption", "").upper()
    if encryption_status != "ENCRYPTED":
        print(
            f"Error: Backup is not encrypted (encryption status: {metadata.get('encryption')})"
        )
        print("No decryption needed. You can decompress/restore the file directly.")
        return

    backup_id = metadata["backupId"]
    salt = base64.b64decode(metadata["encryptionSalt"])
    iv = base64.b64decode(metadata["encryptionIV"])

    # Generate output filename with decrypted_ prefix
    backup_dir = os.path.dirname(backup_file) or "."
    backup_name = os.path.basename(backup_file)
    output_file = os.path.join(backup_dir, f"decrypted_{backup_name}")

    # Derive encryption key using PBKDF2
    key_material = (master_key + backup_id).encode("utf-8")
    derived_key = PBKDF2(
        key_material, salt, dkLen=32, count=PBKDF2_ITERATIONS, hmac_hash_module=SHA256
    )

    try:
        with open(backup_file, "rb") as f_in, open(output_file, "wb") as f_out:
            # Read and validate header
            header = f_in.read(HEADER_LENGTH)

            # Validate magic bytes
            magic = header[:8]
            if magic != MAGIC_BYTES:
                raise ValueError(
                    f"Invalid magic bytes: expected {MAGIC_BYTES}, got {magic}"
                )

            # Decrypt chunks
            chunk_index = 0
            while True:
                # Read chunk length (4 bytes)
                length_bytes = f_in.read(4)
                if not length_bytes:
                    break

                chunk_length = struct.unpack(">I", length_bytes)[0]

                # Read encrypted chunk
                encrypted_chunk = f_in.read(chunk_length)
                if not encrypted_chunk:
                    break

                # Generate chunk nonce (base IV + chunk index)
                chunk_nonce = bytearray(iv)
                chunk_nonce[4:12] = struct.pack(">Q", chunk_index)

                # Create cipher for this chunk
                chunk_cipher = AES.new(derived_key, AES.MODE_GCM, nonce=bytes(chunk_nonce))

                # Decrypt chunk
                try:
                    decrypted_chunk = chunk_cipher.decrypt_and_verify(
                        encrypted_chunk[:-16],  # ciphertext
                        encrypted_chunk[-16:],  # auth tag
                    )
                except ValueError as e:
                    if "MAC check failed" in str(e):
                        print("\nError: Failed to decrypt backup (MAC check failed)")
                        print("This usually means:")
                        print("  - The master key is incorrect")
                        print("  - The backup file is corrupted")
                        print("  - The metadata doesn't match this backup file")
                        print(f"\nFailed at chunk {chunk_index}")
                        raise
                    raise

                # Write decrypted data
                f_out.write(decrypted_chunk)
                chunk_index += 1

        print(f"Successfully decrypted {chunk_index} chunks to {output_file}")
        
    except ValueError as e:
        # Clean up partial output file after files are closed
        if "MAC check failed" in str(e) and os.path.exists(output_file):
            os.remove(output_file)
        return


# Example usage:
if __name__ == "__main__":
    decrypt_backup(
        backup_file="./your-backup-file",             # <--- change this to your backup file
        metadata_file="./your-backup-file.metadata",  # <--- change this to your metadata file
        master_key="your-master-key-here",            # <--- change this to your master key
    )

Установите необходимые зависимости:

pip install pycryptodome

Как пользоваться скриптом:

  1. Сохраните скрипт выше в файл (например, decrypt_backup.py)
  2. Поменяйте параметры в примере запуска в конце файла
  3. Запустите скрипт:
python decrypt_backup.py

Скрипт сам создаст выходной файл с префиксом decrypted_. Например, если файл бекапа называется backup-id.dump, расшифрованный файл будет decrypted_backup-id.dump.

Восстановление базы данных

После расшифровки восстановите бекап штатными инструментами вашей СУБД:

PostgreSQL

Дампы PostgreSQL используют встроенное сжатие, поэтому их можно восстанавливать сразу:

Локальная база:

# Restore to local database
pg_restore -d your_database decrypted-backup.dump

Удаленная база:

# Restore to remote database
pg_restore -h hostname -p 5432 -U username -d database_name decrypted-backup.dump

MySQL

Бекапы MySQL сжаты zstd (уровень 5), перед восстановлением их нужно распаковать.

Шаг 1: распакуйте бекап

Используйте консольную утилиту zstd или любой совместимый архиватор (7-Zip, PeaZip, WinRAR и т.д.):

# Decompress with zstd command-line tool
zstd -d decrypted-backup.sql.zst -o decrypted-backup.sql

# Or use graphical tools like 7-Zip, PeaZip, or WinRAR

Шаг 2: восстановите базу данных

Локальная база:

# Restore to local database
mysql your_database < decrypted-backup.sql

Удаленная база:

# Restore to remote database
mysql -h hostname -P 3306 -u username -p database_name < decrypted-backup.sql

MariaDB

Бекапы MariaDB сжаты zstd (уровень 5), перед восстановлением их нужно распаковать.

Шаг 1: распакуйте бекап

Используйте консольную утилиту zstd или любой совместимый архиватор (7-Zip, PeaZip, WinRAR и т.д.):

# Decompress with zstd command-line tool
zstd -d decrypted-backup.sql.zst -o decrypted-backup.sql

# Or use graphical tools like 7-Zip, PeaZip, or WinRAR

Шаг 2: восстановите базу данных

Локальная база:

# Restore to local database
mariadb your_database < decrypted-backup.sql

Удаленная база:

# Restore to remote database
mariadb -h hostname -P 3306 -u username -p database_name < decrypted-backup.sql

MongoDB

Бекапы MongoDB используют встроенное сжатие gzip, поэтому их можно восстанавливать сразу:

Локальная база:

# Restore to local database
mongorestore --archive=decrypted-backup.archive --gzip --db your_database

Удаленная база:

# Restore to remote database
mongorestore --host hostname:27017 --username username --password password \
  --archive=decrypted-backup.archive --gzip --db database_name

Что делать, если возникли проблемы?

Если в процессе восстановления что-то пошло не так:

  • Спросите ИИ. ИИ-ассистенты вроде ChatGPT, Claude или Gemini отлично помогают с архиваторами и восстановлением баз данных. Просто опишите проблему — и вам подскажут каждый шаг.
  • Вступите в наше сообщество. Наши разработчики и участники сообщества помогут разобраться с вашим конкретным случаем.