def cryptic(c, quadrant, encrypt="", decrypt="", cipher="PKCS1_v1_5", output="text"): """ Handle message crypting. If you need to encrypt or decrypt a message specify the quadrant and action. For messages that need to be larger than the modulus of the key use RSA_AES, This cipher wont work natively with Terraform so use a data external to have this tool decrypt your big strings at runtime. """ p_log("Task: cryptic") if encrypt: response = SecretsManager.SecretsManager({ "key": quadrant, "cipher": cipher }).rsa_encrypt(encrypt) elif decrypt: response = SecretsManager.SecretsManager({ "key": quadrant, "cipher": cipher }).rsa_decrypt(decrypt) if output == "json": print(json.dumps(response)) else: print(response)
def read(self): c = False configs = self.get_merge_configs() # print(json.dumps(configs, indent=4, sort_keys=True)) self.logger.debug("get_merge_configs: %s=%s" % (self.config_file, json.dumps(configs))) try: c = ConfigManager.get(configs, self.args["attribute"]) if c is not None: self.logger.info("Current Value: '%s'" % (c)) else: self.logger.info("Value '%s' doesn't exists." % (self.args["attribute"])) except: self.logger.warn("Value '%s' doesn't exists." % (self.args["attribute"])) # Emit the event to hook onto it ee.emit("ConfigManager.read", self.args["attribute"], c) if self.args["cipher"] is not None: c = SecretsManager.SecretsManager({ "key": self.args["env"], "cipher": self.args["cipher"] }).secretDecoderRing(c) return c
def pass_gen(c, length=10): """ Creates a strong random password. """ p_log("Task: pass_gen") print(SecretsManager.SecretsManager({}).passwordGenerate(length))
def secrets(c, quadrant, cipher="PKCS1_v1_5"): """ An interactive secret manager. Terraform doesn't handle secrets well, this fixes that for us. Using the keys we generated with the key-gen task we open an interactive editor with our secrets decrypted. When we save and exit the editor our secrets are encrypted and stored in a our config. Terraform has a poorly documented function called rsadecrypt https://www.terraform.io/docs/configuration/interpolation.html#rsadecrypt-string-key- If you need to manage secrets that are larger than 245 characters (For a 2048bit key modulus - 11 = 245) then you should use the RSA_AES cipher This allows larger secrets but you can't use the built in terraform rsadecrypt Instead you will have to use data external like this Note: this will be a little slower than terraforms native rsadecrypt # config/${quadrant}/secrets.json { "foo": { "bar": { "API_KEY": "I got a secret, a super, super secret" } } } # projects/infrastructure/main.tf data "external" "secret_decrypt" { #Use dot notation for path to key program = [ "reform", "config-get", "--quadrant", "${var.vpc_name}", "--attribute", "foo.bar.API_KEY", "--cipher", "RSA_AES", "--output", "json" ] } locals { API_KEY = "${data.external.secret_decrypt.result.usage}" } Now you can use local.API_KEY anywhere you need the decrypted secret and at run time terraform will call reform to decrypt your secret. """ p_log("Task: Secrets") # TODO make this work with revised config format print( SecretsManager.SecretsManager( {"key": quadrant, "cipher": cipher} ).InteractiveEdit() )
def rotate_key(c, quadrant, cipher="PKCS1_v1_5"): """ Rotate our RSA Keys. This will move our old keys to *\*.old* and generate a new key pair. It then walks through our configs and re-encrypts the secrets with the new keys """ p_log("Task: rotate_key") print(SecretsManager.SecretsManager({"key": quadrant, "cipher": cipher}).rekey())
def get_config(c): """ Fetches part of the config for use in a terraform map. Terraform can't handle multidimensional maps, this tool fetches a section of map and returns it as json. Unlike other tasks, this tasks gets it's args from a json string sent to stdin. """ p_log("Task: get_config") reform_root = settings.GetReformRoot() params = {} lines = [x.strip() for x in sys.stdin.readlines()] lines = list(filter(None, lines)) if len(lines) != 0: params = json.loads(",".join(lines)) c = {} if "cipher" in params and params["cipher"]: file = "%s/configs/%s/%s" % ( reform_root, params["env"], ReformSettings.ReformSettings.reform_quadrant_secret_file, ) if os.path.exists(file): with open(file, "r") as f: config = json.loads(f.read()) else: p_log("Nested map not found: %s" % (file)) exit(5) else: config = ConfigManager.ConfigManager({ "env": params["env"] }).get_merge_configs() p_log("args: %s" % (params)) if "swimlanes" in config: members = config["swimlanes"] # debug("Nested map found: %s"%(json.dumps(members))) if (params["client"] in members and params["service"] in members[params["client"]]["services"]): c = members[params["client"]]["services"][ params["service"]]["configstore"] else: if params["client"] in config and params["service"] in config[ params["client"]]: c = config[params["client"]][params["service"]] if "cipher" in params and params["cipher"]: c = SecretsManager.SecretsManager({ "key": params["env"], "cipher": params["cipher"] }).secretDecoderRing(c) print(json.dumps(c))
def key_gen(c, bucket, quadrant, region): """ Create RSA keys for secret management. We will use these keys to encrypt and decrypt our secrets in terraform. """ p_log("Task: key_gen") print( SecretsManager.SecretsManager( {"key": quadrant, "bucket": bucket, "region_name": region} ).generateKeyPair() )
def key_exists(c, bucket, quadrant): """ Check to see if a given key already exists """ p_log("Task: key_exists") r = SecretsManager.SecretsManager({"bucket": bucket}).keyExists(quadrant) if r: print("Found") return True else: print("Not Found") return False
def test_rotate_key(self): """ Test that we can rotate the keys in our bucket """ c = MockContext() tasks.key_gen(c, bucket_name, quadrant, region) s3 = boto3.resource("s3", region) # Test if old key exists before pri_path = "%s/SecretsMaster.old" % (quadrant) found = False try: _obj = s3.Object(bucket_name, pri_path).load() found = True except botocore.exceptions.ClientError as e: found = False self.assertFalse(found) # Verify old Public key exists # TODO check size pub_path = "%s/SecretsMaster.pub.old" % (quadrant) found = False try: _obj = s3.Object(bucket_name, pub_path).load() found = True except botocore.exceptions.ClientError as e: found = False self.assertFalse(found) # Make secret before rotate sm = SecretsManager.SecretsManager({"key": quadrant}) secret_file = sm.getSecretPath(quadrant) _secret_dict = {"test1": "hello world"} import copy copy_secret_dict = copy.deepcopy(_secret_dict) sm.encryptSecretFile(_secret_dict, secret_file) _secret_file_decrypt = sm.decryptSecretFile(secret_file) logging.getLogger("TestSecretsManager").debug( "Old: %s, New: %s" % (copy_secret_dict, _secret_file_decrypt)) self.assertEqual(copy_secret_dict, _secret_file_decrypt) # Lets get a file sum pre_checksum = hashlib.md5(open(secret_file, "rb").read()).hexdigest() # Rotate the keys now tasks.rotate_key(c, quadrant) # Test if old key exists after pri_path = "%s/SecretsMaster.old" % (quadrant) found = False try: _obj = s3.Object(bucket_name, pri_path).load() found = True except botocore.exceptions.ClientError as e: found = False self.assertTrue(found) # Verify old Public key exists # TODO check size pub_path = "%s/SecretsMaster.pub.old" % (quadrant) found = False try: _obj = s3.Object(bucket_name, pub_path).load() found = True except botocore.exceptions.ClientError as e: found = False self.assertTrue(found) # Test if crypted strings have been updated post_checksum = hashlib.md5(open(secret_file, "rb").read()).hexdigest() self.assertNotEqual(pre_checksum, post_checksum)
def preform(c, quadrant): """ A simple preprocessor for terraform that processes *\*.tf.tpl* files. This is how we work around terraforms lack of loops and conditionals. This is also how we seed our dynamic reform configs for state backend and and configs we've defined. """ p_log("Start: Preform") projects_base_path = settings.GetReformRoot() # TODO Open this more to include modules work_dir = settings.GetReformRoot() modules_dir = "%s/modules" % (settings.GetReformRoot()) projects_dir = "%s/projects" % (settings.GetReformRoot()) template_suffix = ".tpl" env = Environment(loader=FileSystemLoader(work_dir), trim_blocks=True) # Custom Jinja Filters def is_list(value): return isinstance(value, list) def is_dict(value): return isinstance(value, dict) env.filters["is_list"] = is_list env.filters["is_dict"] = is_dict env.filters["jsonify"] = json.dumps config = ConfigManager.ConfigManager({"env": quadrant}).get_merge_configs() secret_manager = SecretsManager.SecretsManager( {"key": quadrant, "cipher": "RSA_AES"} ) env_secret = secret_manager.getSecretPath(quadrant) secrets = secret_manager.decryptSecretFile(env_secret) # Handle modules dir for directory, subdirectories, files in os.walk(modules_dir): for file in files: if file.endswith(template_suffix): debug("Found template file: %s" % (file)) full_file_path = os.path.join(directory, file) template = env.get_template(full_file_path.replace(work_dir, "")) new_full_file_path = re.sub( template_suffix, "", os.path.join(directory, "preform_" + file) ) debug("Generating file: %s" % (new_full_file_path)) try: with open(new_full_file_path, "w+") as outfile: redered_template = template.render( config=config, project=os.path.basename(directory), quadrant=quadrant, secrets=secrets, ) debug(redered_template) outfile.write( "##################################################\n" ) outfile.write( "# This file auto generated by preform, do not edit!\n" ) outfile.write("# Instead edit \n") outfile.write("# %s\n" % (full_file_path)) outfile.write( "##################################################\n" ) outfile.write("\n\n") outfile.write(redered_template) outfile.write("\n\n") outfile.close() except: pass # Handle projects dir for directory, subdirectories, files in os.walk(projects_dir): for file in files: if file.endswith(template_suffix): debug("Found template file: %s" % (file)) full_file_path = os.path.join(directory, file) template = env.get_template(full_file_path.replace(work_dir, "")) new_full_file_path = re.sub( template_suffix, "", os.path.join(directory, "preform_" + file) ) debug("Generating file: %s" % (new_full_file_path)) with open(new_full_file_path, "w+") as outfile: redered_template = template.render( config=config, project=os.path.basename(directory), quadrant=quadrant, secrets=secrets, ) debug(redered_template) outfile.write( "##################################################\n" ) outfile.write( "# This file auto generated by preform, do not edit!\n" ) outfile.write("# Instead edit \n") outfile.write("# %s\n" % (full_file_path)) outfile.write( "##################################################\n" ) outfile.write("\n\n") outfile.write(redered_template) outfile.write("\n\n") outfile.close() p_log("Complete: Preform")