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.