Add sops support, improved secret handling

This commit is contained in:
Stefan Reimer 2020-04-08 16:30:58 +01:00
parent 80e8ff9463
commit afe3e35d4c
5 changed files with 58 additions and 7 deletions

View File

@ -1,10 +1,16 @@
# Changelog # Changelog
## 0.8.0
- Added support for sops encrypted config files, see: https://github.com/mozilla/sops
- hide stack parameter output in terminal if `NoEcho` is set
- *CloudBender no longer writes stack parameter files to prevent leaking secret values !*
These files were never actually used anyways and there sole purpose was to track changes via git.
## 0.7.8 ## 0.7.8
- Add new function `outputs`, to query already deployed stack for their outputs - Add new function `outputs`, to query already deployed stack for their outputs
## 0.7.7 ## 0.7.7
- Add support for CLOUDBENDER_PROJECT_ROOT env variable to specify your root project - Add support for CLOUDBENDER_PROJECT_ROOT env variable to specify your root project
- Switch most os.path operations to pathlib to fix various corner cases caused by string matching - Switch most os.path operations to pathlib to fix various corner cases caused by string matching
## 0.7.6 ## 0.7.6

View File

@ -32,3 +32,11 @@ Commands:
sync Renders template and provisions it right away sync Renders template and provisions it right away
validate Validates already rendered templates using cfn-lint validate Validates already rendered templates using cfn-lint
``` ```
# Secrets
CloudBender supports Mozilla's [SOPS](https://github.com/mozilla/sops) to encrypt values in any config yaml file since version 0.8.
If a sops encrypted config file is detected CloudBender will automatically try to decrypt the file during execution.
All required information to decrypt has to be present in the embedded sops config or set ahead of time via sops supported ENVIRONMENT variables.

View File

@ -2,7 +2,7 @@ import logging
__author__ = "Stefan Reimer" __author__ = "Stefan Reimer"
__email__ = "stefan@zero-downtimet.net" __email__ = "stefan@zero-downtimet.net"
__version__ = "0.7.8" __version__ = "0.8.0"
# Set up logging to ``/dev/null`` like a library is supposed to. # Set up logging to ``/dev/null`` like a library is supposed to.

View File

@ -5,6 +5,8 @@ import re
import base64 import base64
import yaml import yaml
import copy import copy
import subprocess
import sys
import jinja2 import jinja2
from jinja2.utils import missing, object_type_repr from jinja2.utils import missing, object_type_repr
@ -208,12 +210,16 @@ def read_config_file(path, variables={}):
if path.exists(): if path.exists():
logger.debug("Reading config file: {}".format(path)) logger.debug("Reading config file: {}".format(path))
# First check for sops being present
try: try:
jenv = jinja2.Environment( jenv = jinja2.Environment(
loader=jinja2.FileSystemLoader(str(path.parent)), enable_async=True,
auto_reload=False,
loader=jinja2.FunctionLoader(_sops_loader),
undefined=jinja2.StrictUndefined, undefined=jinja2.StrictUndefined,
extensions=['jinja2.ext.loopcontrols']) extensions=['jinja2.ext.loopcontrols'])
template = jenv.get_template(path.name) template = jenv.get_template(str(path))
rendered_template = template.render(jinja_variables) rendered_template = template.render(jinja_variables)
data = yaml.safe_load(rendered_template) data = yaml.safe_load(rendered_template)
if data: if data:
@ -221,5 +227,31 @@ def read_config_file(path, variables={}):
except Exception as e: except Exception as e:
logger.exception("Error reading config file: {} ({})".format(path, e)) logger.exception("Error reading config file: {} ({})".format(path, e))
sys.exit(1)
return {} return {}
def _sops_loader(path):
""" Tries to loads yaml file
If "sops" key is detected the file is piped through sops before returned
"""
with open(path, 'r') as f:
config_raw = f.read()
data = yaml.safe_load(config_raw)
if 'sops' in data:
try:
result = subprocess.run([
'sops',
'--input-type', 'yaml',
'--output-type', 'yaml',
'--decrypt', '/dev/stdin'
], stdout=subprocess.PIPE, input=config_raw.encode('utf-8'))
except FileNotFoundError:
logger.exception("SOPS encrypted config {}, but unable to find sops binary! Try eg: https://github.com/mozilla/sops/releases/download/v3.5.0/sops-v3.5.0.linux".format(path))
sys.exit(1)
return result.stdout.decode('utf-8')
else:
return config_raw

View File

@ -349,6 +349,11 @@ class Stack(object):
if p in self.parameters: if p in self.parameters:
value = str(self.parameters[p]) value = str(self.parameters[p])
self.cfn_parameters.append({'ParameterKey': p, 'ParameterValue': value}) self.cfn_parameters.append({'ParameterKey': p, 'ParameterValue': value})
# Hide NoEcho parameters in shell output
if 'NoEcho' in self.cfn_data['Parameters'][p] and self.cfn_data['Parameters'][p]['NoEcho']:
value = '****'
logger.info('{} {} Parameter {}={}'.format(self.region, self.stackname, p, value)) logger.info('{} {} Parameter {}={}'.format(self.region, self.stackname, p, value))
else: else:
# If we have a Default defined in the CFN skip, as AWS will use it # If we have a Default defined in the CFN skip, as AWS will use it
@ -385,7 +390,7 @@ class Stack(object):
# Prepare parameters # Prepare parameters
self.resolve_parameters() self.resolve_parameters()
self.write_parameter_file() # self.write_parameter_file()
self.read_template_file() self.read_template_file()
logger.info('Creating {0} {1}'.format(self.region, self.stackname)) logger.info('Creating {0} {1}'.format(self.region, self.stackname))
@ -407,7 +412,7 @@ class Stack(object):
# Prepare parameters # Prepare parameters
self.resolve_parameters() self.resolve_parameters()
self.write_parameter_file() # self.write_parameter_file()
self.read_template_file() self.read_template_file()
logger.info('Updating {0} {1}'.format(self.region, self.stackname)) logger.info('Updating {0} {1}'.format(self.region, self.stackname))
@ -446,7 +451,7 @@ class Stack(object):
# Prepare parameters # Prepare parameters
self.resolve_parameters() self.resolve_parameters()
self.write_parameter_file() # self.write_parameter_file()
self.read_template_file() self.read_template_file()
logger.info('Creating change set {0} for stack {1}'.format(change_set_name, self.stackname)) logger.info('Creating change set {0} for stack {1}'.format(change_set_name, self.stackname))