Восстановление 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Как пользоваться скриптом:
- Сохраните скрипт выше в файл (например,
decrypt_backup.py) - Поменяйте параметры в примере запуска в конце файла
- Запустите скрипт:
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.dumpMySQL
Бекапы 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.sqlMariaDB
Бекапы 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.sqlMongoDB
Бекапы 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 отлично помогают с архиваторами и восстановлением баз данных. Просто опишите проблему — и вам подскажут каждый шаг.
- Вступите в наше сообщество. Наши разработчики и участники сообщества помогут разобраться с вашим конкретным случаем.