From d221337beaaafd7ce71717da64e4c9d91babd712 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 15 May 2012 13:24:58 -0400 Subject: Added ability to store Cfg files with AES encryption --- doc/server/plugins/generators/cfg.txt | 75 ++++++++++++++++++++++ src/lib/Bcfg2/Encryption.py | 75 ++++++++++++++++++++++ .../Server/Plugins/Cfg/CfgEncryptedGenerator.py | 54 ++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100755 src/lib/Bcfg2/Encryption.py create mode 100644 src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt index 031c9e3fc..54dbe3a39 100644 --- a/doc/server/plugins/generators/cfg.txt +++ b/doc/server/plugins/generators/cfg.txt @@ -139,6 +139,81 @@ using different host-specific or group-specific files. For example: Cfg/etc/fstab/fstab.H_host.example.com.genshi Cfg/etc/fstab/fstab.G50_server.cheetah +Encrypted Files +=============== + +.. versionadded:: 1.3.0 + +Bcfg2 allows you to encrypt files stored in ``Cfg/`` to protect the +data in them from other people who need access to the repository. + +.. note:: + + This feature is *not* intended to secure the files against a + malicious attacker who has gained access to your Bcfg2 server, as + the encryption passphrases are held in plaintext in + ``bcfg2.conf``. This is only intended to make it easier to use a + single Bcfg2 repository with multiple admins who should not + necessarily have access to each other's sensitive data. + +Encrypting Files +---------------- + +An encrypted file should end with ``.crypt``, e.g.:: + + Cfg/etc/foo.conf + Cfg/etc/foo.conf/foo.conf.crypt + +To encrypt a file, you can run:: + + openssl enc -aes-256-cbc -k -in foo.conf -out foo.conf.crypt -a + +Once you are satisfied that the file has been encrypted as you wish, +you can remove the plaintext version. + +To decrypt a file, you can run:: + + + +.. note:: + + It is not currently possible to encrypt Genshi or Cheetah + templates. + +Configuring Encryption +---------------------- + +To configure encryption, add a ``[cfg:encryption]`` section to +``bcfg2.conf`` with any number of name-passphrase pairs. When +decrypting a file, _all_ passphrases will be tried; the passphrase +name is currently purely cosmetic, but at some point in the future the +ability to give Bcfg2 a "hint" about which passphrase to use will be +added. + +For instance:: + + [cfg:encryption] + foo_team=P4ssphr4se + bar_team=Pa55phra5e + +This would define two separate encryption passphrases, presumably for +use by two separate teams. The passphrase names are completely +arbitrary. + +Note that this does entail a chicken-and-egg problem. In order for +the Bcfg2 server to be able to decrypt encrypted files, the +passphrases must exist in ``bcfg2.conf`` in plaintext; but, if you're +encrypting data, presumably you don't want to include those plaintext +passphrases in your Bcfg2 repository, so you'll want to encrypt +``bcfg2.conf``. The best way to solve this is: + +#. On your Bcfg2 server, manually add the ``[cfg:encryption]`` section + to ``bcfg2.conf`` and restart the Bcfg2 server. +#. Update ``bcfg2.conf`` in your Bcfg2 repository with the + passphrases, and encrypt it. + +The first (manual) step breaks the mutual dependency. + Deltas ====== diff --git a/src/lib/Bcfg2/Encryption.py b/src/lib/Bcfg2/Encryption.py new file mode 100755 index 000000000..62b22d7de --- /dev/null +++ b/src/lib/Bcfg2/Encryption.py @@ -0,0 +1,75 @@ +#!/usr/bin/python -Ott + +import os +import base64 +from M2Crypto import Rand +from M2Crypto.EVP import Cipher, EVPError +from Bcfg2.Bcfg2Py3k import StringIO + +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +ENCRYPT = 1 +DECRYPT = 0 +ALGORITHM = "aes_256_cbc" +IV = '\0' * 16 + +Rand.rand_seed(os.urandom(1024)) + +def _cipher_filter(cipher, instr): + inbuf = StringIO(instr) + outbuf = StringIO() + while 1: + buf = inbuf.read() + if not buf: + break + outbuf.write(cipher.update(buf)) + outbuf.write(cipher.final()) + rv = outbuf.getvalue() + inbuf.close() + outbuf.close() + return rv + +def str_encrypt(plaintext, key, iv=IV, algorithm=ALGORITHM, salt=None): + """ encrypt a string """ + cipher = Cipher(alg=algorithm, key=key, iv=iv, op=ENCRYPT, salt=salt) + return _cipher_filter(cipher, plaintext) + +def str_decrypt(crypted, key, iv=IV, algorithm=ALGORITHM): + """ decrypt a string """ + cipher = Cipher(alg=algorithm, key=key, iv=iv, op=DECRYPT) + return _cipher_filter(cipher, crypted) + +def ssl_decrypt(data, passwd, algorithm=ALGORITHM): + """ decrypt openssl-encrypted data """ + # base64-decode the data if necessary + try: + data = base64.b64decode(data) + except TypeError: + # already decoded + pass + + salt = data[8:16] + hashes = [md5(passwd + salt).digest()] + for i in range(1,3): + hashes.append(md5(hashes[i-1] + passwd + salt).digest()) + key = hashes[0] + hashes[1] + iv = hashes[2] + + return str_decrypt(data[16:], key=key, iv=iv) + +def ssl_encrypt(plaintext, passwd, algorithm=ALGORITHM, salt=None): + """ encrypt data in a format that is openssl compatible """ + if salt is None: + salt = Rand.rand_bytes(8) + + hashes = [md5(passwd + salt).digest()] + for i in range(1,3): + hashes.append(md5(hashes[i-1] + passwd + salt).digest()) + key = hashes[0] + hashes[1] + iv = hashes[2] + + crypted = str_encrypt(plaintext, key=key, salt=salt, iv=iv) + return base64.b64encode("Salted__" + salt + crypted) + "\n" diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py new file mode 100644 index 000000000..6ba470fd5 --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py @@ -0,0 +1,54 @@ +import logging +import Bcfg2.Server.Plugin +from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP +try: + from Bcfg2.Encryption import ssl_decrypt, EVPError + have_crypto = True +except ImportError: + have_crypto = False + +logger = logging.getLogger(__name__) + +class CfgEncryptedGenerator(CfgGenerator): + __extensions__ = ["crypt"] + + def __init__(self, fname, spec, encoding): + CfgGenerator.__init__(self, fname, spec, encoding) + if not have_crypto: + msg = "Cfg: M2Crypto is not available: %s" % entry.get("name") + logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + + @property + def passphrases(self): + section = "cfg:encryption" + if SETUP.cfp.has_section(section): + return dict([(o, SETUP.cfp.get(section, o)) + for o in SETUP.cfp.options(section)]) + else: + return dict() + + def handle_event(self, event): + if event.code2str() == 'deleted': + return + try: + crypted = open(self.name).read() + except UnicodeDecodeError: + crypted = open(self.name, mode='rb').read() + except: + logger.error("Failed to read %s" % self.name) + return + # todo: let the user specify a passphrase by name + self.data = None + for passwd in self.passphrases.values(): + try: + self.data = ssl_decrypt(crypted, passwd) + return + except EVPError: + pass + logger.error("Failed to decrypt %s" % self.name) + + def get_data(self, entry, metadata): + if self.data is None: + raise Bcfg2.Server.Plugin.PluginExecutionError("Failed to decrypt %s" % self.name) + return CfgGenerator.get_data(self, entry, metadata) -- cgit v1.2.3-1-g7c22