Restaurar PostgreSQL desde una copia de seguridad sin Databasus

Hacer copias de seguridad no es solo proteger los datos, sino también poder recuperarlos. Databasus se asegura de que sus respaldos sigan siendo recuperables incluso si el VPS con Databasus se elimina, pierde el acceso o no puede entrar en la interfaz por cualquier motivo. No necesita Databasus para recuperar las copias: se guardan en formato estándar y no hay dependencia del proveedor.

Qué necesita

Para recuperar una copia de seguridad manualmente necesita:

  • El archivo de la copia desde su almacenamiento (almacenamiento local, S3, Google Drive, etc.)
  • El archivo de metadatos del mismo almacenamiento. Se llama igual que el archivo de la copia, pero con la extensión .metadata.
  • La clave secreta de ./databasus-data/secret.key (ubicada en el mismo directorio que los archivos de copia, normalmente /opt/databasus/)

Estructura de archivos

Cada copia de seguridad consta de dos archivos guardados en su almacenamiento (local o en la nube):

  • {database-name}-{timestamp}-{backup-id} - Datos de la copia cifrados y comprimidos
  • {database-name}-{timestamp}-{backup-id}.metadata - Archivo JSON con los detalles del cifrado

El archivo de metadatos contiene la sal de cifrado y el IV (nonce) en formato Base64:

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

Descifrado

Databasus usa cifrado AES-256-GCM con derivación de clave PBKDF2. Cada copia de seguridad tiene una clave de cifrado única derivada de:

  • La clave maestra (del archivo secret.key)
  • El ID de la copia
  • Una sal aleatoria (guardada en los metadatos)

Use este script de Python para descifrar su copia:

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
    )

Instale las dependencias necesarias:

pip install pycryptodome

Cómo usar el script:

  1. Guarde el script anterior en un archivo (por ejemplo, decrypt_backup.py)
  2. Actualice los parámetros en la sección de ejemplo de uso al final
  3. Ejecute el script:
python decrypt_backup.py

El script creará automáticamente el archivo de salida con el prefijo decrypted_. Por ejemplo, si su archivo de copia es backup-id.dump, el archivo descifrado será decrypted_backup-id.dump.

Restauración en la base de datos

Tras el descifrado, restaure con las herramientas propias de cada base de datos:

PostgreSQL

Las copias de PostgreSQL usan compresión integrada y se pueden restaurar directamente:

Base de datos local:

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

Base de datos remota:

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

MySQL

Las copias de MySQL se comprimen con zstd nivel 5 y deben descomprimirse antes de restaurar.

Paso 1: descomprima la copia

Use la herramienta de línea de comandos zstd o cualquier herramienta de descompresión compatible (7-Zip, PeaZip, WinRAR, etc.):

# 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

Paso 2: restaure en la base de datos

Base de datos local:

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

Base de datos remota:

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

MariaDB

Las copias de MariaDB se comprimen con zstd nivel 5 y deben descomprimirse antes de restaurar.

Paso 1: descomprima la copia

Use la herramienta de línea de comandos zstd o cualquier herramienta de descompresión compatible (7-Zip, PeaZip, WinRAR, etc.):

# 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

Paso 2: restaure en la base de datos

Base de datos local:

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

Base de datos remota:

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

MongoDB

Las copias de MongoDB usan compresión gzip integrada y se pueden restaurar directamente:

Base de datos local:

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

Base de datos remota:

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

¿Qué hago si tengo problemas?

Si encuentra algún problema durante el proceso de recuperación:

  • Pida ayuda a una IA. Los asistentes de IA como ChatGPT, Claude o Gemini ayudan muy bien con herramientas de compresión y procedimientos de restauración de bases de datos. Describa su problema y le guiarán durante el proceso.
  • Únase a nuestra comunidad. Nuestros desarrolladores y miembros de la comunidad pueden ayudarle con su caso concreto.