Decrypting a Checkmk Backup File

I recently needed to restore a single file from a Checkmk backup without restoring the entire site. This is not officially supported by Checkmk, and the recommended way is to set up a second site and restore the backup via the GUI. However, for a custom graph I accidentally deleted, manual decryption and restoring the single file was the quickest solution.

I couldn’t find any clear documentation on how Checkmk encrypts its backup files, so I decided to dig into the source code. ( checkmk/cmk/utils/backup/stream.py )

I learned that the backup is first tarred and gzipped, then written into a bytestream with a specific header format that includes the version, the length of the RSA-encrypted AES key, and the encrypted key itself, followed by the actual backup data:

<version>\0<AES key length>\0<RSA-encrypted AES key>\0<site-mysite.tar.gz>

Below is a simple Python script that, reads the header, decrypts the AES key using your RSA private key and decrypts the backup payload using AES-CBC

#!/usr/bin/env python3

import argparse
import getpass
import os
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP


def read_field(f):
    buf = b""
    while True:
        c = f.read(1)
        if c == b"\0":
            break
        buf += c
    return buf


def decrypt_file(enc_file, rsa_key_file, output_file, passphrase=None):
    with open(enc_file, "rb") as f:
        # Read header
        file_version = read_field(f)
        # compare bytes correctly
        if file_version != b"2":
            raise ValueError(f"Invalid file version: {file_version}")

        key_len = int(read_field(f))
        encrypted_secret_key = f.read(key_len)
        if f.read(1) != b"\0":
            raise ValueError("Header broken")

        # Decrypt AES key using RSA
        with open(rsa_key_file, "rb") as kf:
            rsa_key_data = kf.read()

        # Try importing the RSA key. If it's encrypted and no passphrase was
        # provided, prompt the user for one.
        try:
            rsa_key = RSA.import_key(rsa_key_data, passphrase=passphrase)
        except (ValueError, TypeError) as e:
            # If no passphrase supplied, prompt interactively and retry.
            if passphrase is None:
                prompt = getpass.getpass("Enter passphrase for RSA key: ")
                rsa_key = RSA.import_key(rsa_key_data, passphrase=prompt)
            else:
                raise
        cipher_rsa = PKCS1_OAEP.new(rsa_key)
        secret_key = cipher_rsa.decrypt(encrypted_secret_key)

        # AES-CBC decryption (IV is 16 zero bytes)
        iv = b"\x00" * AES.block_size
        cipher_aes = AES.new(secret_key, AES.MODE_CBC, iv)

        # Decrypt payload
        encrypted_data = f.read()
        decrypted_data = cipher_aes.decrypt(encrypted_data)

        # Remove PKCS#7 padding
        pad_len = decrypted_data[-1]  # Get padding size
        decrypted_data = decrypted_data[:-pad_len]

        # Write output
        with open(output_file, "wb") as out:
            out.write(decrypted_data)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Decrypt a file encrypted with the Checkmk backup format.",
        epilog=("If the RSA private key is encrypted, the passphrase will be read "
                "from the MKBACKUP_PASSPHRASE environment variable. "
                "If that variable is not set the script will prompt "
                "interactively for the passphrase."))
    parser.add_argument(
        "encrypted_backup", help="Path to the encrypted backup file")
    parser.add_argument("rsa_private_key",
                        help="Path to RSA private key (PEM)")
    parser.add_argument("output_file", help="Path to write decrypted output")
    args = parser.parse_args()

    env_pass = os.getenv("MKBACKUP_PASSPHRASE")

    decrypt_file(args.encrypted_backup, args.rsa_private_key,
                 args.output_file, env_pass)

This method is unsupported and should only be used for special cases. The official recommendation is to restore via the Checkmk GUI to a second site.

Note:
i would have liked to add my post to Unable to decrypt WATO backup from command line or Decrypt checkmk Backup File but these are already closed.

6 Likes

cool - thanks for sharing this!

1 Like

Thank you so much! :slight_smile::slight_smile: