From 0c3b23c3a3f48f1fbc2e59e76245a847de53ab92 Mon Sep 17 00:00:00 2001 From: "Sridhar K. N. Rao" Date: Sun, 18 Apr 2021 13:39:40 +0530 Subject: [WIP]: Openstack Security Check This patch adds openstack security checking. This is based on https://docs.openstack.org/security-guide/checklist.html Support reading configuration from default file and environment Added reference security.conf Update the Documentation. Update index to include security Fix bug reported by Parth, and another. JIRA: CIRV-49 Signed-off-by: Sridhar K. N. Rao Change-Id: I72579a861409c3aaf464f44f0cdc24dc33cd4345 --- sdv/docker/sdvsecurity/nfvsec/conf/00_common.conf | 20 ++ sdv/docker/sdvsecurity/nfvsec/conf/01_horizon.conf | 16 ++ .../sdvsecurity/nfvsec/conf/02_keystone.conf | 11 + sdv/docker/sdvsecurity/nfvsec/conf/03_nova.conf | 7 + sdv/docker/sdvsecurity/nfvsec/conf/04_cinder.conf | 7 + sdv/docker/sdvsecurity/nfvsec/conf/05_neutron.conf | 7 + sdv/docker/sdvsecurity/nfvsec/conf/10_access.conf | 7 + sdv/docker/sdvsecurity/nfvsec/conf/__init__.py | 223 +++++++++++++++++++++ 8 files changed, 298 insertions(+) create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/00_common.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/01_horizon.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/02_keystone.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/03_nova.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/04_cinder.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/05_neutron.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/10_access.conf create mode 100644 sdv/docker/sdvsecurity/nfvsec/conf/__init__.py (limited to 'sdv/docker/sdvsecurity/nfvsec/conf') diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/00_common.conf b/sdv/docker/sdvsecurity/nfvsec/conf/00_common.conf new file mode 100644 index 0000000..fb3ec0d --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/00_common.conf @@ -0,0 +1,20 @@ +import os + +# default log output directory for all logs +LOG_DIR = '/tmp' + +# default log for all "small" executables +LOG_FILE_DEFAULT = 'csure.log' + +ROOT_DIR = os.path.normpath(os.path.join( + os.path.dirname(os.path.realpath(__file__)), '../')) + +RESULTS_PATH = '/tmp' + +# 'debug', 'info', 'warning', 'error', 'critical' +VERBOSITY = 'warning' + +# One of 'docker' (rhosp, devstack-kolla), 'k8s' (airship), 'legacy' (devstack, tungsten-fabric, fuel) +DEPLOYMENT = 'k8s' + +EXCLUDE_MODULES = [''] diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/01_horizon.conf b/sdv/docker/sdvsecurity/nfvsec/conf/01_horizon.conf new file mode 100644 index 0000000..e184143 --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/01_horizon.conf @@ -0,0 +1,16 @@ + +HORIZON_DICT_KEYS = ['DISALLOW_IFRAME_EMBED', 'CSRF_COOKIE_SECURE', + 'SESSION_COOKIE_SECURE', 'SESSION_COOKIE_HTTPONLY', + 'PASSWORD_AUTOCOMPLETE', 'DISABLE_PASSWORD_REVEAL', + 'ENFORCE_PASSWORD_CHECK', 'PASSWORD_VALIDATOR', + 'SECURE_PROXY_SSL_HEADER'] + +HORIZON_LOCAL_SETTINGS = "/etc/openstack-dashboard/local_settings" + +HOP_FILES = ['/etc/openstack-dashboard/local_settings'] + +HORIZON_APACHE_FILES = ['/etc/openstack-dashboard/local_setting', + '/etc/openstack-dashboard/nova_policy.json', + '/etc/openstack-dashboard/cinder_policy.json', + '/etc/openstack-dashboard/keystone_policy.json', + '/etc/openstack-dashboard/neutron_policy.json'] diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/02_keystone.conf b/sdv/docker/sdvsecurity/nfvsec/conf/02_keystone.conf new file mode 100644 index 0000000..2b33f9d --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/02_keystone.conf @@ -0,0 +1,11 @@ + +KEYSTONE_CONF_FILE = '/etc/keystone/keystone.conf' + +KSP_FILES = ['/etc/keystone/keystone.conf', + '/etc/keystone/keystone-paste.ini', + '/etc/keystone/policy.json', + '/etc/keystone/logging.conf', + '/etc/keystone/ssl/certs/signing_cert.pem', + '/etc/keystone/ssl/private/signing_key.pem', + '/etc/keystone/ssl/certs/ca.pem'] + diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/03_nova.conf b/sdv/docker/sdvsecurity/nfvsec/conf/03_nova.conf new file mode 100644 index 0000000..4c86111 --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/03_nova.conf @@ -0,0 +1,7 @@ + +NOVA_CONF_FILE = '/etc/nova/nova.conf' + +NOP_FILES = ['/etc/nova/nova.conf', + '/etc/nova/api-paste.ini', + '/etc/nova/policy.json', + '/etc/nova/rootwrap.conf'] diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/04_cinder.conf b/sdv/docker/sdvsecurity/nfvsec/conf/04_cinder.conf new file mode 100644 index 0000000..438ec02 --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/04_cinder.conf @@ -0,0 +1,7 @@ + +CINDER_CONF_FILE = '/etc/cinder/cinder.conf' + +CIP_FILES = ['/etc/cinder/cinder.conf', + '/etc/cinder/api-paste.ini', +# '/etc/cinder/policy.json', + '/etc/cinder/rootwrap.conf'] diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/05_neutron.conf b/sdv/docker/sdvsecurity/nfvsec/conf/05_neutron.conf new file mode 100644 index 0000000..0012da2 --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/05_neutron.conf @@ -0,0 +1,7 @@ + +NEUTRON_CONF_FILE = '/etc/neutron/neutron.conf' + +NEP_FILES = ['/etc/neutron/neutron.conf', + '/etc/neutron/api-paste.ini', + '/etc/neutron/policy.json', + '/etc/neutron/rootwrap.conf'] diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/10_access.conf b/sdv/docker/sdvsecurity/nfvsec/conf/10_access.conf new file mode 100644 index 0000000..aa804c4 --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/10_access.conf @@ -0,0 +1,7 @@ +USER = 'heat-admin' +PRIVATE_KEY_FILE = '/conf/cloud.key' +PASSWORD = 'admin123' +ACCESS_TYPE = 'key' +HOST = '192.168.1.98' + +K8S_CONFIG_FILEPATH = '/conf/k8sconfig' diff --git a/sdv/docker/sdvsecurity/nfvsec/conf/__init__.py b/sdv/docker/sdvsecurity/nfvsec/conf/__init__.py new file mode 100644 index 0000000..c2eaf0b --- /dev/null +++ b/sdv/docker/sdvsecurity/nfvsec/conf/__init__.py @@ -0,0 +1,223 @@ +"""Settings and configuration handlers. + +Settings will be loaded from several .conf files +and any user provided settings file. +""" + +# pylint: disable=invalid-name + +import copy +import os +import re +import logging +import pprint +#import ast +#import netaddr + +_LOGGER = logging.getLogger(__name__) + +_PARSE_PATTERN = r'(#[A-Z]+)(\(([^(),]+)(,([0-9]+))?\))?' + +class Settings(object): + """Holding class for settings. + """ + def __init__(self): + pass + + def _eval_param(self, param): + # pylint: disable=invalid-name + """ Helper function for expansion of references to parameters + """ + if isinstance(param, str): + # evaluate every #PARAM reference inside parameter itself + macros = re.findall(r'#PARAM\((([\w\-]+)(\[[\w\[\]\-\'\"]+\])*)\)', param) + if macros: + for macro in macros: + # pylint: disable=eval-used + try: + tmp_val = str(eval("self.getValue('{}'){}".format(macro[1], macro[2]))) + param = param.replace('#PARAM({})'.format(macro[0]), tmp_val) + # silently ignore that option required by PARAM macro can't be evaluated; + # It is possible, that referred parameter will be constructed during runtime + # and re-read later. + except IndexError: + pass + except AttributeError: + pass + return param + elif isinstance(param, (list, tuple)): + tmp_list = [] + for item in param: + tmp_list.append(self._eval_param(item)) + return tmp_list + elif isinstance(param, dict): + tmp_dict = {} + for (key, value) in param.items(): + tmp_dict[key] = self._eval_param(value) + return tmp_dict + else: + return param + + def getValue(self, attr): + """Return a settings item value + """ + if attr in self.__dict__: + master_value = getattr(self, attr) + return self._eval_param(master_value) + else: + raise AttributeError("%r object has no attribute %r" % + (self.__class__, attr)) + + def hasValue(self, attr): + """Return true if key exists + """ + if attr in self.__dict__: + return True + return False + + def __setattr__(self, name, value): + """Set a value + """ + # skip non-settings. this should exclude built-ins amongst others + if not name.isupper(): + return + + # we can assume all uppercase keys are valid settings + super(Settings, self).__setattr__(name, value) + + def setValue(self, name, value): + """Set a value + """ + if name is not None and value is not None: + super(Settings, self).__setattr__(name, value) + + + def load_from_file(self, path): + """Update ``settings`` with values found in module at ``path``. + """ + import imp + + custom_settings = imp.load_source('custom_settings', path) + + for key in dir(custom_settings): + if getattr(custom_settings, key) is not None: + setattr(self, key, getattr(custom_settings, key)) + + def load_from_dir(self, dir_path): + """Update ``settings`` with contents of the .conf files at ``path``. + + Each file must be named Nfilename.conf, where N is a single or + multi-digit decimal number. The files are loaded in ascending order of + N - so if a configuration item exists in more that one file the setting + in the file with the largest value of N takes precedence. + + :param dir_path: The full path to the dir from which to load the .conf + files. + + :returns: None + """ + regex = re.compile("^(?P[0-9]+)(?P[a-z]?)_.*.conf$") + + def get_prefix(filename): + """ + Provide a suitable function for sort's key arg + """ + match_object = regex.search(os.path.basename(filename)) + return [int(match_object.group('digit_part')), + match_object.group('alfa_part')] + + # get full file path to all files & dirs in dir_path + file_paths = os.listdir(dir_path) + file_paths = [os.path.join(dir_path, x) for x in file_paths] + + # filter to get only those that are a files, with a leading + # digit and end in '.conf' + file_paths = [x for x in file_paths if os.path.isfile(x) and + regex.search(os.path.basename(x))] + + # sort ascending on the leading digits and afla (e.g. 03_, 05a_) + file_paths.sort(key=get_prefix) + + # load settings from each file in turn + for filepath in file_paths: + self.load_from_file(filepath) + + def load_from_dict(self, conf): + """ + Update ``settings`` with values found in ``conf``. + + Unlike the other loaders, this is case insensitive. + """ + for key in conf: + if conf[key] is not None: + if isinstance(conf[key], dict): + # recursively update dict items + setattr(self, key.upper(), + merge_spec(getattr(self, key.upper()), conf[key])) + else: + setattr(self, key.upper(), conf[key]) + + def restore_from_dict(self, conf): + """ + Restore ``settings`` with values found in ``conf``. + + Method will drop all configuration options and restore their + values from conf dictionary + """ + self.__dict__.clear() + tmp_conf = copy.deepcopy(conf) + for key in tmp_conf: + self.setValue(key, tmp_conf[key]) + + def load_from_env(self): + """ + Update ``settings`` with values found in the environment. + """ + for key in os.environ: + setattr(self, key, os.environ[key]) + + def __str__(self): + """Provide settings as a human-readable string. + + This can be useful for debug. + + Returns: + A human-readable string. + """ + tmp_dict = {} + for key in self.__dict__: + tmp_dict[key] = self.getValue(key) + + return pprint.pformat(tmp_dict) + + +settings = Settings() + +def merge_spec(orig, new): + """Merges ``new`` dict with ``orig`` dict, and returns orig. + + This takes into account nested dictionaries. Example: + + >>> old = {'foo': 1, 'bar': {'foo': 2, 'bar': 3}} + >>> new = {'foo': 6, 'bar': {'foo': 7}} + >>> merge_spec(old, new) + {'foo': 6, 'bar': {'foo': 7, 'bar': 3}} + + You'll notice that ``bar.bar`` is not removed. This is the desired result. + """ + for key in orig: + if key not in new: + continue + + # Not allowing derived dictionary types for now + # pylint: disable=unidiomatic-typecheck + if type(orig[key]) == dict: + orig[key] = merge_spec(orig[key], new[key]) + else: + orig[key] = new[key] + + for key in new: + if key not in orig: + orig[key] = new[key] + + return orig -- cgit 1.2.3-korg