aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/index.rst2
-rw-r--r--docs/security/userguide.rst32
-rw-r--r--sdv/docker/sdvsecurity/Dockerfile12
-rw-r--r--sdv/docker/sdvsecurity/k8sconfig20
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/00_common.conf20
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/01_horizon.conf16
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/02_keystone.conf11
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/03_nova.conf7
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/04_cinder.conf7
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/05_neutron.conf7
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/10_access.conf7
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/conf/__init__.py223
-rwxr-xr-xsdv/docker/sdvsecurity/nfvsec/os-checklist848
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/utils/__init__.py16
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/utils/k8sclient.py58
-rw-r--r--sdv/docker/sdvsecurity/nfvsec/utils/sshclient.py419
-rw-r--r--sdv/docker/sdvsecurity/pki/admin-key.pem27
-rw-r--r--sdv/docker/sdvsecurity/pki/admin.pem21
-rw-r--r--sdv/docker/sdvsecurity/pki/cluster-ca.pem20
-rw-r--r--sdv/docker/sdvsecurity/security.conf4
20 files changed, 1777 insertions, 0 deletions
diff --git a/docs/index.rst b/docs/index.rst
index ae0729f..b10a1d6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,3 +21,5 @@ OPNFV CIRV-SDV
state/development/index
state/development/result_api
state/development/settings
+ urls/userguide
+ security/userguide
diff --git a/docs/security/userguide.rst b/docs/security/userguide.rst
new file mode 100644
index 0000000..c071c4c
--- /dev/null
+++ b/docs/security/userguide.rst
@@ -0,0 +1,32 @@
+*****************************
+CIRV-SDV: Security Validation
+*****************************
+
+Kali Release:
+Openstack security checking, as described here: https://docs.openstack.org/security-guide/checklist.html is implemented.
+
+This version supports following deployments:
+
+1. Triple-O (RHOSP - openstack services run as containers)
+2. Openstack on Kubernetes (Ex: Airship)
+3. Legacy - Devstack (openstack sevices baremetal applications)
+
+Running the container
+#####################
+
+run command docker build -t sdv-security .
+Things to note before building
+
+1. Correct deployment type
+2. Corresponding access information.
+3. Comment out the last line if the container is run interactively.
+
+First first two can be done by adding it in security.conf, or passing them as environment variables.
+
+Running the container
+#####################
+
+It is recommended to run interactively, using the following steps
+
+1. docker run -it sdv-security /bin/bash
+2. ./os-checklist
diff --git a/sdv/docker/sdvsecurity/Dockerfile b/sdv/docker/sdvsecurity/Dockerfile
new file mode 100644
index 0000000..549a422
--- /dev/null
+++ b/sdv/docker/sdvsecurity/Dockerfile
@@ -0,0 +1,12 @@
+# Run this tool as a container.
+FROM python:3.9.2-slim-buster
+
+RUN apt-get update && apt-get -y install vim && pip install paramiko kubernetes scp
+COPY nfvsec /nfvsec/
+COPY k8sconfig /conf/
+COPY pki /conf/pki/
+COPY security.conf /conf/
+
+WORKDIR /nfvsec
+
+CMD python os-checklist
diff --git a/sdv/docker/sdvsecurity/k8sconfig b/sdv/docker/sdvsecurity/k8sconfig
new file mode 100644
index 0000000..e9d1d3d
--- /dev/null
+++ b/sdv/docker/sdvsecurity/k8sconfig
@@ -0,0 +1,20 @@
+apiVersion: v1
+clusters:
+- cluster:
+# certificate-authority: pki/cluster-ca.pem
+ insecure-skip-tls-verify: true
+ server: https://kubernetes:6553
+ name: kubernetes
+contexts:
+- context:
+ cluster: kubernetes
+ user: admin
+ name: admin@kubernetes
+current-context: admin@kubernetes
+kind: Config
+preferences: {}
+users:
+- name: admin
+ user:
+ client-certificate: pki/admin.pem
+ client-key: pki/admin-key.pem
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<digit_part>[0-9]+)(?P<alfa_part>[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
diff --git a/sdv/docker/sdvsecurity/nfvsec/os-checklist b/sdv/docker/sdvsecurity/nfvsec/os-checklist
new file mode 100755
index 0000000..e022ffd
--- /dev/null
+++ b/sdv/docker/sdvsecurity/nfvsec/os-checklist
@@ -0,0 +1,848 @@
+#!/usr/bin/env python3
+
+# Copyright 2021 Spirent Communications.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+NFV Security Check
+"""
+from __future__ import print_function
+
+import logging
+import os
+import sys
+import argparse
+import time
+import datetime
+import io
+import configparser
+from collections import OrderedDict
+from conf import settings
+from utils import sshclient
+from utils import k8sclient
+
+VERBOSITY_LEVELS = {
+ 'debug': logging.DEBUG,
+ 'info': logging.INFO,
+ 'warning': logging.WARNING,
+ 'error': logging.ERROR,
+ 'critical': logging.CRITICAL
+}
+
+_CURR_DIR = os.path.dirname(os.path.realpath(__file__))
+_LOGGER = logging.getLogger()
+
+# pylint: disable=too-few-public-methods, no-self-use
+class PseudoFile(io.RawIOBase):
+ """
+ Handle ssh command output.
+ """
+ def __init__(self, filename):
+ self.fname = filename
+
+ def write(self, chunk):
+ """
+ Write to file
+ """
+ #if "error" in chunk:
+ # return
+ with open(self.fname, "a+") as fref:
+ fref.write(chunk)
+
+class MultiOrderedDict(OrderedDict):
+ """
+ For Duplicate Keys
+ """
+ def __setitem__(self, key, value):
+ if isinstance(value, list) and key in self:
+ self[key].extend(value)
+ else:
+ super(MultiOrderedDict, self).__setitem__(key, value)
+
+def parse_arguments():
+ """
+ Parse command line arguments.
+ """
+ parser = argparse.ArgumentParser(prog=__file__, formatter_class=
+ argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument('--version', action='version', version='%(prog)s 0.1')
+ parser.add_argument('--list-clouds', action='store_true',
+ help='list the supported clouds ')
+ args = vars(parser.parse_args())
+ return args
+
+def configure_logging(level):
+ """Configure logging.
+ """
+ name, ext = os.path.splitext(settings.getValue('LOG_FILE_DEFAULT'))
+ rename_default = "{name}_{uid}{ex}".format(name=name,
+ uid=settings.getValue(
+ 'LOG_TIMESTAMP'),
+ ex=ext)
+ log_file_default = os.path.join(
+ settings.getValue('RESULTS_PATH'), rename_default)
+ _LOGGER.setLevel(logging.DEBUG)
+ stream_logger = logging.StreamHandler(sys.stdout)
+ stream_logger.setLevel(VERBOSITY_LEVELS[level])
+ stream_logger.setFormatter(logging.Formatter(
+ '[%(levelname)-5s] %(asctime)s : (%(name)s) - %(message)s'))
+ _LOGGER.addHandler(stream_logger)
+ file_logger = logging.FileHandler(filename=log_file_default)
+ file_logger.setLevel(logging.DEBUG)
+ file_logger.setFormatter(logging.Formatter(
+ '%(asctime)s : %(message)s'))
+ _LOGGER.addHandler(file_logger)
+
+def handle_list_options(args):
+ """ Process --list cli arguments if needed
+
+ :param args: A dictionary with all CLI arguments
+ """
+ if args['list_clouds']:
+ print("WIP - Clouds")
+ sys.exit(0)
+
+def check_horizon_hardening(deployment, client):
+ '''
+ DISALLOW_IFRAME_EMBED parameter set to True
+ CSRF_COOKIE_SECURE parameter set to True
+ SESSION_COOKIE_SECURE parameter set to True
+ SESSION_COOKIE_HTTPONLY parameter set to True
+ PASSWORD_AUTOCOMPLETE set to False
+ DISABLE_PASSWORD_REVEAL set to True
+ ENFORCE_PASSWORD_CHECK set to True
+ '''
+ result = True
+ horizonkeys = OrderedDict()
+ dictkeys = ['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']
+ for dks in dictkeys:
+ horizonkeys[dks] = 'UNSET - NOT OK'
+
+ actualfile = settings.getValue('HORIZON_LOCAL_SETTINGS')
+ filename = "./horizon-conf.conf"
+ if 'k8s' not in deployment:
+ with PseudoFile(filename) as hor:
+ if 'legacy' in deployment:
+ cmd = 'sudo cat ' + actualfile
+ elif 'docker' in deployment:
+ cmd = 'sudo docker exec horizon cat ' + actualfile
+ client.run(cmd, stdout=hor, timeout=0)
+ else:
+ cmd = ['cat', actualfile]
+ pod = client.get_pod('openstack', 'horizon')
+ resp = client.execute(pod, cmd)
+ if resp:
+ with open(filename, "w+", encoding="utf-8") as fref:
+ fref.write(resp)
+
+ if (not os.path.isfile(filename) or
+ os.stat(filename).st_size == 0):
+ print("Could not read file from the container")
+ return False
+
+ with open(filename, "r") as fil:
+ for lin in fil:
+ if lin.startswith('DISALLOW_IFRAME_EMBED'):
+ if 'True' in lin:
+ horizonkeys['DISALLOW_IFRAME_EMBED'] = "OK"
+ else:
+ horizonkeys['DISALLOW_IFRAME_EMBED'] = "NOT OK"
+ result = False
+ if lin.startswith('CSRF_COOKIE_SECURE'):
+ if 'True' in lin:
+ horizonkeys['CSRF_COOKIE_SECURE'] = "OK"
+ else:
+ horizonkeys['CSRF_COOKIE_SECURE'] = "NOT OK"
+ result = False
+ if lin.startswith('SESSION_COOKIE_SECURE'):
+ if 'True' in lin:
+ horizonkeys['SESSION_COOKIE_SECURE'] = "OK"
+ else:
+ horizonkeys['SESSION_COOKIE_SECURE'] = "NOT OK"
+ result = False
+ if lin.startswith('SESSION_COOKIE_HTTPONLY'):
+ if 'True' in lin:
+ horizonkeys['SESSION_COOKIE_HTTPONLY'] = "OK"
+ else:
+ horizonkeys['SESSION_COOKIE_HTTPONLY'] = "NOT OK"
+ result = False
+ if lin.startswith('PASSWORD_AUTOCOMPLETE'):
+ if 'False' in lin:
+ horizonkeys['PASSWORD_AUTOCOMPLETE'] = "OK"
+ else:
+ horizonkeys['PASSWORD_AUTOCOMPLETE'] = "NOT OK"
+ result = False
+ if lin.startswith('DISABLE_PASSWORD_REVEAL'):
+ if 'True' in lin:
+ horizonkeys['DISABLE_PASSWORD_REVEAL'] = "OK"
+ else:
+ horizonkeys['DISABLE_PASSWORD_REVEAL'] = "NOT OK"
+ result = False
+ if lin.startswith('ENFORCE_PASSWORD_CHECK'):
+ if 'True' in lin:
+ horizonkeys['ENFORCE_PASSWORD_CHECK'] = "OK"
+ else:
+ horizonkeys['ENFORCE_PASSWORD_CHECK'] = "NOT OK"
+ result = False
+ if lin.startswith('PASSWORD_VALIDATOR'):
+ if 'regex' in lin:
+ horizonkeys['PASSWORD_VALIDATOR'] = "OK"
+ else:
+ horizonkeys['PASSWORD_VALIDATOR'] = "NOT OK"
+ result = False
+ if lin.startswith('SECURE_PROXY_SSL_HEADER'):
+ if 'HTTP_X_FORWARDED_PROTO' in lin and 'https' in lin:
+ horizonkeys['SECURE_PROXY_SSL_HEADER'] = "OK"
+ else:
+ horizonkeys['SECURE_PROXY_SSL_HEADER'] = "NOT OK"
+ result = False
+ for key in horizonkeys:
+ print("The KEY: '" + key + "' is " + horizonkeys[key])
+ return result
+
+def check_neutron_hardening(deployment, client):
+ '''
+ Section:Parameter:Expected-Value
+ keystone_authtoken:auth_protocol:https
+ keystone_authtoken:identity_uri:https://....
+ :use_ssl:True
+ :auth_strategy:keystone
+ '''
+ config = configparser.ConfigParser()
+ result = True
+ actualfile = settings.getValue('NEUTRON_CONF_FILE')
+ filename = "./neutron-conf.conf"
+ if 'k8s' not in deployment:
+ with PseudoFile(filename) as hor:
+ if 'legacy' in deployment:
+ cmd = 'sudo cat ' + actualfile
+ elif 'docker' in deployment:
+ cmd = 'sudo docker exec neutron_api cat ' + actualfile
+ client.run(cmd, stdout=hor, timeout=0)
+ else:
+ cmd = ['cat', actualfile]
+ pod = client.get_pod('openstack', 'neutron-server')
+ resp = client.execute(pod, cmd)
+ if resp:
+ with open(filename, "w+", encoding="utf-8") as fref:
+ fref.write(resp)
+
+ if (not os.path.isfile(filename) or
+ os.stat(filename).st_size == 0):
+ print("Could not read file from the container")
+ return False
+
+ config.read(filename)
+
+ if (config.has_option('keystone_authtoken', 'auth_protocol') and
+ config.has_option('keystone_authtoken','identity_uri')):
+ if (config.get('keystone_authtoken', 'auth_protocol') != 'https' and
+ not config.get('keystone_authtoken','identity_uri').startswith('https')):
+ print('Authentication token is not secured ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','use_ssl'):
+ if not config.get('DEFAULT','use_ssl'):
+ print('SSL is not used ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','auth_strategy'):
+ if config.get('DEFAULT','auth_strategy') != 'keystone':
+ print('Authentication strategy should be keystone ... NOT OK')
+ result = False
+ return result
+
+
+def check_cinder_hardening(deployment, client):
+ '''
+ Section:Parameter:Expected-Value
+ keystone_authtoken:auth_protocol:https
+ keystone_authtoken:identity_uri:https://....
+ :nova_api_insecure:False
+ :glance_api_insecure:False
+ :nas_secure_file_permissions:auto
+ :nas_secure_file_operations:auto
+ :auth_strategy:keystone
+ :osapi_max_request_body_size:114688 OR
+ oslo_middleware:max_request_body_size:114688
+ '''
+ result = True
+ config = configparser.ConfigParser()
+ actualfile = settings.getValue('CINDER_CONF_FILE')
+ filename = "./cinder-conf.conf"
+ if 'k8s' not in deployment:
+ with PseudoFile(filename) as hor:
+ if 'legacy' in deployment:
+ cmd = 'sudo cat ' + actualfile
+ elif 'docker' in deployment:
+ cmd = 'sudo docker exec cinder_api cat ' + actualfile
+ client.run(cmd, stdout=hor, timeout=0)
+ else:
+ cmd = ['cat', actualfile]
+ pod = client.get_pod('openstack', 'cinder-api')
+ resp = client.execute(pod, cmd)
+ if resp:
+ with open(filename, "w+", encoding="utf-8") as fref:
+ fref.write(resp)
+
+ if (not os.path.isfile(filename) or
+ os.stat(filename).st_size == 0):
+ print("Could not read file from the container")
+ return False
+
+ config.read(filename)
+ if (config.has_option('keystone_authtoken','auth_protocol') and
+ config.has_option('keystone_authtoken','identity_uri')):
+ if (config.get('keystone_authtoken','auth_protocol') != 'https' and
+ config.get('keystone_authtoken','identity_uri').startswith('https')):
+ print('Authentication token is not secured ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','nova_api_insecure'):
+ if config.get('DEFAULT','nova_api_insecure'):
+ print('Cinder-Nova API is insecure ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','nas_secure_file_operations'):
+ if config.get('DEFAULT','nas_secure_file_operations') != 'auto':
+ print('NAS Secure File is False ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','nas_secure_file_permissions'):
+ if config.get('DEFAULT','nas_secure_file_permissions') != 'auto':
+ print('NAS secure file permissions ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','auth_strategy'):
+ if config.get('DEFAULT','auth_strategy') != 'keystone':
+ print('Authentication strategy should be keystone')
+ result = False
+ if config.has_option('DEFAULT','glance_api_insecure'):
+ if config.get('DEFAULT','glance_api_insecure') != 'False' :
+ print('Cinder-Glance API is insecure ... NOT OK ')
+ result = False
+ if (config.has_option('DEFAULT','osapi_max_request_body_size') and
+ config.has_option('oslo_middleware','max_request_body_size')):
+ if (config.get('DEFAULT','osapi_max_request_body_size') != 114688 or
+ config.get('oslo_middleware','max_request_body_size') != 114688):
+ print('MAX Request Body Size is not 114688 ... NOT OK')
+ result = False
+ return result
+
+
+
+def check_nova_hardening(deployment, client):
+ result = True
+ config = configparser.ConfigParser(allow_no_value=True, interpolation=None, strict=False)
+ #config = configparser.RawConfigParser(dict_type=MultiOrderedDict, strict=False)
+ actualfile = settings.getValue('NOVA_CONF_FILE')
+ filename = "./nova-conf.conf"
+ if 'k8s' not in deployment:
+ with PseudoFile(filename) as hor:
+ if 'legacy' in deployment:
+ cmd = 'sudo cat ' + actualfile
+ elif 'docker' in deployment:
+ cmd = 'sudo docker exec nova_api cat ' + actualfile
+ client.run(cmd, stdout=hor, timeout=0)
+ else:
+ cmd = ['cat', actualfile]
+ pod = client.get_pod('openstack', 'nova-api-osapi')
+ resp = client.execute(pod, cmd)
+ if resp:
+ with open(filename, "w+", encoding="utf-8") as fref:
+ fref.write(resp)
+ if (not os.path.isfile(filename) or
+ os.stat(filename).st_size == 0):
+ print("Could not read file from the container")
+ return False
+
+ config.read([filename])
+ if config.has_option('DEFAULT','auth_strategy'):
+ if config.get('DEFAULT','auth_strategy') != 'keystone':
+ print('Authentication strategy should be keystone ... NOT OK')
+ result = False
+ if (config.has_option('keystone_authtoken','auth_protocol') and
+ config.has_option('keystone_authtoken','identity_uri')):
+ if (config.get('keystone_authtoken','auth_protocol') != 'https' and
+ not config.get('keystone_authtoken','identity_uri').startswith('https')):
+ print('Authentication token is not secured ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','glance_api_insecure'):
+ if config.get('DEFAULT','glance_api_insecure'):
+ print('Glance-Nova API is insecure ... NOT OK')
+ result = False
+ return result
+
+def check_keystone_hardening(deployment, client):
+ '''
+ https://static.open-scap.org/ssg-guides/ssg-rhosp13-guide-stig.html
+ /etc/keystone/keystone.conf
+ Section:Parameter:Expected-Value
+ token:hash_algorithm:SHA256
+ ssl:enable:True
+ NA:max_request_body_size:default/114688/some-value
+ security_compliance:disable_user_account_days_inactive:some-value
+ security_compliance:lockout_failure_attempts:some-value
+ security_compliance:lockout_duration:some-value
+ DEFAULT:admin_token:disabled
+ *** If lockout_failure_attempts is enabled and lockout_duration is left undefined,
+ users will be locked out indefinitely until the user is explicitly re-enabled ***
+ [/etc/keystone/keystone-paste.ini]
+ filter:admin_token_auth:AdminTokenAuthMiddleware:not-exist
+ '''
+ result = True
+ #config = configparser.ConfigParser()
+ config = configparser.ConfigParser(allow_no_value=True, interpolation=None, strict=False)
+ actualfile = settings.getValue('KEYSTONE_CONF_FILE')
+ filename = "./keystone-conf.conf"
+ if 'k8s' not in deployment:
+ with PseudoFile(filename) as hor:
+ if 'legacy' in deployment:
+ cmd = 'sudo cat ' + actualfile
+ elif 'docker' in deployment:
+ cmd = 'sudo docker exec keystone cat ' + actualfile
+ client.run(cmd, stdout=hor, timeout=0)
+ else:
+ cmd = ['cat', actualfile]
+ pod = client.get_pod('openstack', 'keystone-api')
+ resp = client.execute(pod, cmd)
+ if resp:
+ with open(filename, "w+", encoding="utf-8") as fref:
+ fref.write(resp)
+ if (not os.path.isfile(filename) or
+ os.stat(filename).st_size == 0):
+ print("Could not read file from the container")
+ return False
+
+ config.read(filename)
+ if config.has_option('token','hash_algorithm'):
+ if config.get('token','hash_algorithm') != 'SHA256':
+ print('Hash Algorithm is NOT SHA256 ... NOT OK')
+ result = False
+ if config.has_option('ssl','enable'):
+ if not config.get('ssl','enable'):
+ print('SSL is not enabled ... NOT OK')
+ result = False
+ if config.has_option('DEFAULT','max_request_body_size'):
+ if not config.get('DEFAULT','max_request_body_size'):
+ print('MAX request Body Size is not specified ... NOT OK')
+ result = False
+ if (config.has_option('security_compliance','disable_user_account_days_inactive') and
+ not config.has_option('security_compliance','lockout_failure_attempts') and
+ not config.has_option('security_compliance','lockout_duration')):
+ if (not config.get('security_compliance','disable_user_account_days_inactive') and
+ not config.get('security_compliance','lockout_failure_attempts') and
+ not config.get('security_compliance','lockout_duration')):
+ print("Security Compliance configurations are not correct ... NOT OK")
+ result = False
+ if config.has_option('DEFAULT','admin_token'):
+ if config.get('DEFAULT','admin_token') != 'disabled':
+ print("Admin Token is not disabled ... NOT OK")
+ result = False
+ return result
+
+
+
+def check_sixfourzero(deployment, client):
+ """
+ Check 644
+ """
+ result = True
+ status = -1
+ stderror = "Failed to Stat file"
+ filenames = (settings.getValue('KSP_FILES') +
+ settings.getValue('NOP_FILES') +
+ settings.getValue('NEP_FILES') +
+ settings.getValue('CIP_FILES'))
+ # https://stackoverflow.com/questions/1861836/checking-file-permissions-in-linux-with-python
+ for filename in filenames:
+ if 'cinder' in filename:
+ container = 'cinder_api'
+ podn = 'cinder-api'
+ elif 'neutron' in filename:
+ container = 'neutron_api'
+ podn = 'neutron-server'
+ elif 'nova' in filename:
+ container = 'nova_api'
+ podn = 'nova-api-osapi'
+ elif 'keystone' in filename:
+ container = 'keystone'
+ podn = 'keystone-api'
+ else:
+ print("Bad File Name ")
+ return False
+ if 'legacy' in deployment:
+ cmd = "sudo stat -c '%a' " + filename
+ elif 'docker' in deployment:
+ cmd = "sudo docker exec " + container + " stat -c '%a' " + filename
+ elif 'k8s' in deployment:
+ cmd = ['stat', '-c', '"%a"', filename]
+ pod = client.get_pod('openstack', podn)
+ else:
+ print("Deployment not supported")
+ return False
+ if 'k8s' not in deployment:
+ status, stdout, stderror = client.execute(cmd)
+ else:
+ stdout = client.execute(pod, cmd)
+ if stdout:
+ status = 0
+ if status == 0:
+ if '640' not in stdout:
+ print('The File {} has wrong permission - NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderror)
+ result = False
+ return result
+
+def check_ug_keystone(deployment, client):
+ """
+ UG of Keystone
+ """
+ result = True
+ statusu = statusg = -1
+ stderroru = stderrorg = "Failed to Stat file"
+ filenames = settings.getValue('KSP_FILES')
+ for filename in filenames:
+ if 'legacy' in deployment:
+ cmdu = "sudo stat -c '%U' " + filename
+ cmdg = "sudo stat -c '%G' " + filename
+ elif 'docker' in deployment:
+ cmdu = "sudo docker exec keystone stat -c '%U' " + filename
+ cmdg = "sudo docker exec keystone stat -c '%G' " + filename
+ elif 'k8s' in deployment:
+ pod = client.get_pod('openstack', 'keystone-api')
+ cmdu = ['stat', '-c' '"%U"', filename]
+ cmdg = ['stat', '-c' '"%G"', filename]
+ else:
+ print("Deployment type not supported")
+ return False
+ if 'k8s' not in deployment:
+ statusu, stdoutu, stderroru = client.execute(cmdu)
+ statusg, stdoutg, stderrorg = client.execute(cmdg)
+ else:
+ stdoutu = client.execute(pod, cmdu)
+ stdoutg = client.execute(pod, cmdg)
+ if stdoutu:
+ statusu = 0
+ if stdoutg:
+ statusg = 0
+ if statusu == 0:
+ if ('keystone' not in stdoutu and
+ 'root' not in stdoutu):
+ print('The user of File {} is not keystone ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderroru)
+ if statusg == 0:
+ if 'keystone' not in stdoutg:
+ print(filename)
+ print('The group ownership of file {} is not keystone ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderrorg)
+ return result
+
+
+def check_ug_root_apache(deployment, client):
+ """
+ UG of Apache
+ """
+ result = True
+ statusu = statusg = -1
+ stderroru = stderrorg = "Failed to Stat file"
+ filenames = settings.getValue('HORIZON_APACHE_FILES')
+ for filename in filenames:
+ if 'legacy' in deployment:
+ cmdu = "sudo stat -c '%U' " + filename
+ cmdg = "sudo stat -c '%G' " + filename
+ elif 'docker' in deployment:
+ cmdu = "sudo docker exec horizon stat -c '%U' " + filename
+ cmdg = "sudo docker exec horizon stat -c '%G' " + filename
+ elif 'k8s' in deployment:
+ pod = client.get_pod('openstack', 'horizon')
+ cmdu = ['stat', '-c' '"%U"', filename]
+ cmdg = ['stat', '-c' '"%G"', filename]
+ else:
+ print("Deployment type not supported")
+ return False
+ if 'k8s' not in deployment:
+ statusu, stdoutu, stderroru = client.execute(cmdu)
+ statusg, stdoutg, stderrorg = client.execute(cmdg)
+ else:
+ stdoutu = client.execute(pod, cmdu)
+ stdoutg = client.execute(pod, cmdg)
+ if stdoutu:
+ statusu = 0
+ if stdoutg:
+ statusg = 0
+
+ if statusu == 0:
+ if 'root' not in stdoutu:
+ print('The user of File {} is not root ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderroru)
+ if statusg == 0:
+ if 'apache' not in stdoutg:
+ print(filename)
+ print('The group ownership of file {} is not Apache ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderrorg)
+ return result
+
+
+def check_ug_root_nova(deployment, client):
+ """
+ UG of Nova
+ """
+ result = True
+ statusu = statusg = -1
+ stderroru = stderrorg = "Failed to Stat file"
+ filenames = settings.getValue('NOP_FILES')
+ for filename in filenames:
+ if 'legacy' in deployment:
+ cmdu = "sudo stat -c '%U' " + filename
+ cmdg = "sudo stat -c '%G' " + filename
+ elif 'docker' in deployment:
+ cmdu = "sudo docker exec nova_api stat -c '%U' " + filename
+ cmdg = "sudo docker exec nova_api stat -c '%G' " + filename
+ elif 'k8s' in deployment:
+ pod = client.get_pod('openstack', 'nova-api-osapi')
+ cmdu = ['stat', '-c' '"%U"', filename]
+ cmdg = ['stat', '-c' '"%G"', filename]
+ else:
+ print("Deployment type not supported")
+ return False
+ if 'k8s' not in deployment:
+ statusu, stdoutu, stderroru = client.execute(cmdu)
+ statusg, stdoutg, stderrorg = client.execute(cmdg)
+ else:
+ stdoutu = client.execute(pod, cmdu)
+ stdoutg = client.execute(pod, cmdg)
+ if stdoutu:
+ statusu = 0
+ if stdoutg:
+ statusg = 0
+
+ if statusu == 0:
+ if 'root' not in stdoutu:
+ print('The user of File {} is not root ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderroru)
+ if statusg == 0:
+ if 'nova' not in stdoutg:
+ print(filename)
+ print('The group ownership of file {} is not nova ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderrorg)
+ return result
+
+
+def check_ug_root_neutron(deployment, client):
+ """
+ UG of Neutron
+ """
+ result = True
+ statusu = statusg = -1
+ stderroru = stderrorg = "Failed to Stat file"
+ # https://stackoverflow.com/questions/927866/how-to-get-the-owner-and-group-of-a-folder-with-python-on-a-linux-machine
+ filenames = settings.getValue('NEP_FILES')
+ for filename in filenames:
+ if 'legacy' in deployment:
+ cmdu = "sudo stat -c '%U' " + filename
+ cmdg = "sudo stat -c '%G' " + filename
+ elif 'docker' in deployment:
+ cmdu = "sudo docker exec neutron_api stat -c '%U' " + filename
+ cmdg = "sudo docker exec neutron_api stat -c '%G' " + filename
+ elif 'k8s' in deployment:
+ pod = client.get_pod('openstack', 'neutron-server')
+ cmdu = ['stat', '-c' '"%U"', filename]
+ cmdg = ['stat', '-c' '"%G"', filename]
+ else:
+ print("Deployment type not supported")
+ return False
+ if 'k8s' not in deployment:
+ statusu, stdoutu, stderroru = client.execute(cmdu)
+ statusg, stdoutg, stderrorg = client.execute(cmdg)
+ else:
+ stdoutu = client.execute(pod, cmdu)
+ stdoutg = client.execute(pod, cmdg)
+ if stdoutu:
+ statusu = 0
+ if stdoutg:
+ statusg = 0
+
+ if statusu == 0:
+ if 'root' not in stdoutu:
+ print('The user of File {} is not root ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderroru)
+ if statusg == 0:
+ if ('neutron' not in stdoutg and
+ 'root' not in stdoutg):
+ print(filename)
+ print('The group ownership of file {} is not neutron ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderrorg)
+ return result
+
+
+def check_ug_root_cinder(deployment, client):
+ """
+ UG of Cinder
+ """
+ result = True
+ statusu = statusg = -1
+ statusg = -1
+ stderroru = "Failed to Stat file"
+ stderrorg = "Failed to Stat file"
+ # https://stackoverflow.com/questions/927866/how-to-get-the-owner-and-group-of-a-folder-with-python-on-a-linux-machine
+ filenames = settings.getValue('CIP_FILES')
+ for filename in filenames:
+ if 'legacy' in deployment:
+ cmdu = "sudo stat -c '%U' " + filename
+ cmdg = "sudo stat -c '%G' " + filename
+ elif 'docker' in deployment:
+ cmdu = "sudo docker exec cinder_api stat -c '%U' " + filename
+ cmdu = "sudo docker exec cinder_api stat -c '%G' " + filename
+ elif 'k8s' in deployment:
+ pod = client.get_pod('openstack', 'cinder-api')
+ cmdu = ['stat', '-c' '"%U"', filename]
+ cmdg = ['stat', '-c' '"%G"', filename]
+ else:
+ print("Deployment type not supported")
+ return False
+ if 'k8s' not in deployment:
+ statusu, stdoutu, stderroru = client.execute(cmdu)
+ statusg, stdoutg, stderrorg = client.execute(cmdg)
+ else:
+ stdoutu = client.execute(pod, cmdu)
+ stdoutg = client.execute(pod, cmdg)
+ if stdoutu:
+ statusu = 0
+ if stdoutg:
+ statusg = 0
+ if statusu == 0:
+ if 'root' not in stdoutu:
+ print('The user of File {} is not root ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderroru)
+ if statusg == 0:
+ if 'cinder' not in stdoutg:
+ print(filename)
+ print('The group ownership of file {} is not cinder ... NOT OK'.format(filename))
+ result = False
+ else:
+ print(stderrorg)
+ return result
+
+def testing_k8s(k8s):
+ """
+ Testing Kubernetes
+ """
+ pod = k8s.get_pod('ceph', 'ingress')
+ if pod:
+ print(pod.metadata.name)
+ print(pod.metadata.namespace)
+ response = k8s.execute(pod, 'ls')
+ print(response)
+
+
+def main():
+ """Main function.
+ """
+ client = None
+ args = parse_arguments()
+
+ # define the timestamp to be used by logs and results
+ date = datetime.datetime.fromtimestamp(time.time())
+ timestamp = date.strftime('%Y-%m-%d_%H-%M-%S')
+ settings.setValue('LOG_TIMESTAMP', timestamp)
+
+
+ # configure settings
+ settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
+
+ # Read default config file.
+ def_conf_file = '/conf/security.conf'
+ if os.path.exists(def_conf_file):
+ settings.load_from_file(def_conf_file)
+
+ # Read from Environment
+ settings.load_from_env()
+
+
+ # if required, handle list-* operations
+ handle_list_options(args)
+
+ configure_logging(settings.getValue('VERBOSITY'))
+ deployment = settings.getValue('DEPLOYMENT')
+ if ('docker' in deployment or
+ 'podman' in deployment or
+ 'legacy' in deployment):
+ print('Deployment is Docker')
+ if 'key' in settings.getValue('ACCESS_TYPE'):
+ client = sshclient.SSH(host = settings.getValue('HOST'),
+ user = settings.getValue('USER'),
+ key_filename = settings.getValue('PRIVATE_KEY_FILE'))
+ elif 'k8s' in deployment:
+ client = k8sclient.K8sClient()
+
+ # Run checks
+ if check_ug_root_cinder(deployment, client):
+ print('UG-ROOT-CINDER PASSED')
+ else:
+ print('UG-ROOT-CINDER FAILED')
+ if check_ug_root_neutron(deployment, client):
+ print('UG-ROOT-NEUTRON PASSED')
+ else:
+ print('UG-ROOT-NEUTRON FAILED')
+ if check_ug_keystone(deployment, client):
+ print('UG-KEYSTONE-KEYSTONE PASSED')
+ else:
+ print('UG-ROOT-KEYSTONE FAILED')
+ if check_ug_root_nova(deployment, client):
+ print('UG-ROOT-NOVA PASSED')
+ else:
+ print('UG-ROOT-NOVA FAILED')
+ if check_sixfourzero(deployment, client):
+ print('ALL FILE PERMISSIONS ARE STRICT')
+ else:
+ print('SOME FILE PERMISSIONS ARE NOT STRICT')
+ if check_nova_hardening(deployment, client):
+ print('NOVA HARDENING PASSED')
+ else:
+ print('NOVA HARDENING FAILED')
+ if check_cinder_hardening(deployment, client):
+ print('CINDER HARDENING PASSED')
+ else:
+ print('CINDER HARDENING FAILED')
+ if check_neutron_hardening(deployment, client):
+ print('NEUTRON HARDENING PASSED')
+ else:
+ print('NEUTRON HARDENING FAILED')
+ if check_keystone_hardening(deployment, client):
+ print('KEYSTONE HARDENING PASSED')
+ else:
+ print('KEYSTONE HARDENING FAILED')
+ if check_horizon_hardening(deployment, client):
+ print('HORIZON HARDENING PASSED')
+ else:
+ print('HORIZON HARDENING FAILED')
+
+if __name__ == "__main__":
+ main()
diff --git a/sdv/docker/sdvsecurity/nfvsec/utils/__init__.py b/sdv/docker/sdvsecurity/nfvsec/utils/__init__.py
new file mode 100644
index 0000000..16e9790
--- /dev/null
+++ b/sdv/docker/sdvsecurity/nfvsec/utils/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2015 Intel Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tools package.
+"""
diff --git a/sdv/docker/sdvsecurity/nfvsec/utils/k8sclient.py b/sdv/docker/sdvsecurity/nfvsec/utils/k8sclient.py
new file mode 100644
index 0000000..b1eba5a
--- /dev/null
+++ b/sdv/docker/sdvsecurity/nfvsec/utils/k8sclient.py
@@ -0,0 +1,58 @@
+"""
+Kubernetes cluster api helper functions
+"""
+
+
+import time
+
+from kubernetes import client, config
+from kubernetes.client import Configuration
+from kubernetes.client.api import core_v1_api
+from kubernetes.client.rest import ApiException
+from kubernetes.stream import stream
+
+from kubernetes.stream import stream
+import logging
+from conf import settings # pylint: disable=import-error
+
+
+class K8sClient():
+ """
+ Class for controlling the pod through PAPI
+ """
+
+ def __init__(self):
+ """
+ Initialisation function.
+ """
+ self._logger = logging.getLogger(__name__)
+ config.load_kube_config(settings.getValue('K8S_CONFIG_FILEPATH'))
+ self.api = client.CoreV1Api()
+
+ def get_pod(self, namespace, name):
+ """
+ Returns json details any one pod with matching label
+
+ :param namespace: namespace to use
+ :param namespace: name of the pod (Longest possible).
+ :return: pod details
+ """
+ api_response = self.api.list_namespaced_pod(namespace)
+ for pod in api_response.items:
+ #print(pod.metadata.name)
+ if pod.metadata.name.startswith(name):
+ return pod
+ return None
+
+
+ def execute(self, pod, cmd):
+ """
+ Executes `cmd` inside `pod` and returns response
+ :param pod: pod object
+ :param cmd: command to execute inside pod
+ :return: response from pod
+ """
+ response = stream(self.api.connect_get_namespaced_pod_exec,
+ pod.metadata.name, pod.metadata.namespace, command=cmd,
+ stderr=True, stdin=False, stdout=True, tty=False)
+ return response
diff --git a/sdv/docker/sdvsecurity/nfvsec/utils/sshclient.py b/sdv/docker/sdvsecurity/nfvsec/utils/sshclient.py
new file mode 100644
index 0000000..cc1f521
--- /dev/null
+++ b/sdv/docker/sdvsecurity/nfvsec/utils/sshclient.py
@@ -0,0 +1,419 @@
+"""High level ssh library.
+
+Usage examples:
+
+Execute command and get output:
+
+ ssh = sshclient.SSH("root", "example.com", port=33)
+ status, stdout, stderr = ssh.execute("ps ax")
+ if status:
+ raise Exception("Command failed with non-zero status.")
+ print(stdout.splitlines())
+
+Execute command with huge output:
+
+ class PseudoFile(io.RawIOBase):
+ def write(chunk):
+ if "error" in chunk:
+ email_admin(chunk)
+
+ ssh = SSH("root", "example.com")
+ with PseudoFile() as p:
+ ssh.run("tail -f /var/log/syslog", stdout=p, timeout=False)
+
+Execute local script on remote side:
+
+ ssh = sshclient.SSH("user", "example.com")
+
+ with open("~/myscript.sh", "r") as stdin_file:
+ status, out, err = ssh.execute('/bin/sh -s "arg1" "arg2"',
+ stdin=stdin_file)
+
+Upload file:
+
+ ssh = SSH("user", "example.com")
+ # use rb for binary files
+ with open("/store/file.gz", "rb") as stdin_file:
+ ssh.run("cat > ~/upload/file.gz", stdin=stdin_file)
+
+"""
+from __future__ import print_function
+#import io
+import logging
+import os
+import re
+import select
+import socket
+import time
+
+import paramiko
+#from oslo_utils import encodeutils
+from scp import SCPClient
+import six
+
+NON_NONE_DEFAULT = object()
+
+def try_int(s, *args):
+ """Convert to integer if possible."""
+ #pylint: disable=invalid-name
+ try:
+ return int(s)
+ except (TypeError, ValueError):
+ return args[0] if args else s
+
+class SSH():
+ """Represent ssh connection."""
+ #pylint: disable=no-member
+
+ SSH_PORT = paramiko.config.SSH_PORT
+ DEFAULT_WAIT_TIMEOUT = 120
+
+ @staticmethod
+ def gen_keys(key_filename, bit_count=2048):
+ """
+ Generate Keys
+ """
+ rsa_key = paramiko.RSAKey.generate(bits=bit_count, progress_func=None)
+ rsa_key.write_private_key_file(key_filename)
+ print("Writing %s ..." % key_filename)
+ with open('.'.join([key_filename, "pub"]), "w") as pubkey_file:
+ pubkey_file.write(rsa_key.get_name())
+ pubkey_file.write(' ')
+ pubkey_file.write(rsa_key.get_base64())
+ pubkey_file.write('\n')
+
+ @staticmethod
+ def get_class():
+ """
+ Get Class object
+ """
+ # must return static class name, anything else
+ # refers to the calling class
+ # i.e. the subclass, not the superclass
+ return SSH
+
+ @classmethod
+ def get_arg_key_map(cls):
+ """
+ Get Key Map
+ """
+ return {
+ 'user': ('user', NON_NONE_DEFAULT),
+ 'host': ('ip', NON_NONE_DEFAULT),
+ 'port': ('ssh_port', cls.SSH_PORT),
+ 'pkey': ('pkey', None),
+ 'key_filename': ('key_filename', None),
+ 'password': ('password', None),
+ 'name': ('name', None),
+ }
+
+ #pylint: disable=too-many-arguments
+ def __init__(self, user, host, port=None, pkey=None,
+ key_filename=None, password=None, name=None):
+ """Initialize SSH client.
+ :param user: ssh username
+ :param host: hostname or ip address of remote ssh server
+ :param port: remote ssh port
+ :param pkey: RSA or DSS private key (not supported)
+ :param key_filename: private key filename
+ :param password: password
+ """
+ self.name = name
+ if name:
+ self.log = logging.getLogger(__name__ + '.' + self.name)
+ else:
+ self.log = logging.getLogger(__name__)
+
+ self.wait_timeout = self.DEFAULT_WAIT_TIMEOUT
+ self.user = user
+ self.host = host
+ # everybody wants to debug this in the caller, do it here instead
+ self.log.debug("user:%s host:%s", user, host)
+
+ # we may get text port from YAML, convert to int
+ self.port = try_int(port, self.SSH_PORT)
+ self.pkey = pkey
+ self.password = password
+ self.key_filename = key_filename
+ self._client = False
+ # paramiko loglevel debug will output ssh protocl debug
+ # we don't ever really want that unless we are debugging paramiko
+ # ssh issues
+ if os.environ.get("PARAMIKO_DEBUG", "").lower() == "true":
+ logging.getLogger("paramiko").setLevel(logging.DEBUG)
+ else:
+ logging.getLogger("paramiko").setLevel(logging.WARN)
+
+ @property
+ def is_connected(self):
+ """
+ Return true if connected
+ """
+ return bool(self._client)
+
+ def _get_client(self):
+ """
+ Get Client object
+ """
+ if self.is_connected:
+ return self._client
+ try:
+ self._client = paramiko.SSHClient()
+ self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self._client.connect(self.host, username=self.user,
+ port=self.port, pkey=self.pkey,
+ key_filename=self.key_filename,
+ password=self.password,
+ allow_agent=False, look_for_keys=False,
+ timeout=1)
+ return self._client
+ except (paramiko.BadHostKeyException,
+ paramiko.AuthenticationException,
+ paramiko.ChannelException,
+ paramiko.SSHException,
+ socket.error) as error:
+ message = ("Exception %(exception_type)s was raised "
+ "during connect. Exception value is: %(exception)r" %
+ {"exception": error, "exception_type": type(error)})
+ self._client = False
+ print(message)
+
+ def close(self):
+ """
+ Close connection
+ """
+ if self._client:
+ self._client.close()
+ self._client = False
+
+ #pylint: disable=too-many-arguments
+ def run(self, cmd, stdin=None, stdout=None, stderr=None,
+ raise_on_error=True, timeout=3600,
+ keep_stdin_open=False, pty=False):
+ """Execute specified command on the server.
+
+ :param cmd: Command to be executed.
+ :type cmd: str
+ :param stdin: Open file or string to pass to stdin.
+ :param stdout: Open file to connect to stdout.
+ :param stderr: Open file to connect to stderr.
+ :param raise_on_error: If False then exit code will be return. If True
+ then exception will be raized if non-zero code.
+ :param timeout: Timeout in seconds for command execution.
+ Default 1 hour. No timeout if set to 0.
+ :param keep_stdin_open: don't close stdin on empty reads
+ :type keep_stdin_open: bool
+ :param pty: Request a pseudo terminal for this connection.
+ This allows passing control characters.
+ Default False.
+ :type pty: bool
+ """
+
+ client = self._get_client()
+
+ if isinstance(stdin, six.string_types):
+ stdin = six.moves.StringIO(stdin)
+
+ return self._run(client, cmd, stdin=stdin, stdout=stdout,
+ stderr=stderr, raise_on_error=raise_on_error,
+ timeout=timeout,
+ keep_stdin_open=keep_stdin_open, pty=pty)
+
+ #pylint: disable=too-many-arguments,too-many-nested-blocks
+ def _run(self, client, cmd, stdin=None, stdout=None, stderr=None,
+ raise_on_error=True, timeout=3600,
+ keep_stdin_open=False, pty=False):
+ """
+ Actual Run function (internal)
+ """
+
+ transport = client.get_transport()
+ session = transport.open_session()
+ if pty:
+ session.get_pty()
+ session.exec_command(cmd)
+ start_time = time.time()
+
+ # encode on transmit, decode on receive
+ #data_to_send = encodeutils.safe_encode("", incoming='utf-8')
+ data_to_send = "".encode('utf-8', 'strict')
+ stderr_data = None
+
+ # If we have data to be sent to stdin then `select' should also
+ # check for stdin availability.
+ if stdin and not stdin.closed:
+ writes = [session]
+ else:
+ writes = []
+
+ while True:
+ # Block until data can be read/write.
+ expe = select.select([session], writes, [session], 1)[2]
+
+ if session.recv_ready():
+ #data = encodeutils.safe_decode(session.recv(4096), 'utf-8')
+ data = session.recv(8192).decode('utf-8', 'strict')
+ self.log.debug("stdout: %r", data)
+ if stdout is not None:
+ stdout.write(data)
+ continue
+
+ if session.recv_stderr_ready():
+ #stderr_data = encodeutils.safe_decode(
+ # session.recv_stderr(4096), 'utf-8')
+ stderr_data = session.recv_stderr(4096).decode(
+ 'utf=8', 'strict')
+ self.log.debug("stderr: %r", stderr_data)
+ if stderr is not None:
+ stderr.write(stderr_data)
+ continue
+
+ if session.send_ready():
+ if stdin is not None and not stdin.closed:
+ if not data_to_send:
+ stdin_txt = stdin.read(4096)
+ if stdin_txt is None:
+ stdin_txt = ''
+ #data_to_send = encodeutils.safe_encode(
+ # stdin_txt, incoming='utf-8')
+ data_to_send = stdin_txt.encode('utf-8', 'strict')
+ if not data_to_send:
+ # we may need to keep stdin open
+ if not keep_stdin_open:
+ stdin.close()
+ session.shutdown_write()
+ writes = []
+ if data_to_send:
+ sent_bytes = session.send(data_to_send)
+ # LOG.debug("sent: %s" % data_to_send[:sent_bytes])
+ data_to_send = data_to_send[sent_bytes:]
+
+ if session.exit_status_ready():
+ break
+
+ if timeout and (time.time() - timeout) > start_time:
+ message = ('Timeout executing command %(cmd)s on host %(host)s'
+ % {"cmd": cmd, "host": self.host})
+ raise socket.timeout(message)
+ if expe:
+ raise paramiko.SSHException('Socket error')
+
+ exit_status = session.recv_exit_status()
+ if exit_status != 0 and raise_on_error:
+ fmt = "Command '%(cmd)s' failed with exit_status %(status)d."
+ details = fmt % {"cmd": cmd, "status": exit_status}
+ if stderr_data:
+ details += " Last stderr data: '%s'." % stderr_data
+ raise paramiko.SSHException(details)
+ return exit_status
+
+ def execute(self, cmd, stdin=None, timeout=3600, raise_on_error=False):
+ """Execute the specified command on the server.
+
+ :param cmd: (str) Command to be executed.
+ :param stdin: (StringIO) Open file to be sent on process stdin.
+ :param timeout: (int) Timeout for execution of the command.
+ :param raise_on_error: (bool) If True, then an SSHError will be raised
+ when non-zero exit code.
+
+ :returns: tuple (exit_status, stdout, stderr)
+ """
+ stdout = six.moves.StringIO()
+ stderr = six.moves.StringIO()
+
+ exit_status = self.run(cmd, stderr=stderr,
+ stdout=stdout, stdin=stdin,
+ timeout=timeout, raise_on_error=raise_on_error)
+ stdout.seek(0)
+ stderr.seek(0)
+ return exit_status, stdout.read(), stderr.read()
+
+
+ def put(self, files, remote_path=b'.', recursive=False):
+ """
+ Push Files to remote
+ """
+ client = self._get_client()
+
+ with SCPClient(client.get_transport()) as scp:
+ scp.put(files, remote_path, recursive)
+
+ def get(self, remote_path, local_path='/tmp/', recursive=True):
+ """
+ Get Files from remote
+ """
+ client = self._get_client()
+
+ with SCPClient(client.get_transport()) as scp:
+ scp.get(remote_path, local_path, recursive)
+
+ # keep shell running in the background, e.g. screen
+ def send_command(self, command):
+ """
+ Execute in Background
+ """
+ client = self._get_client()
+ client.exec_command(command, get_pty=True)
+
+ def _put_file_sftp(self, localpath, remotepath, mode=None):
+ """
+ Transfer file (internal)
+ """
+ client = self._get_client()
+
+ with client.open_sftp() as sftp:
+ sftp.put(localpath, remotepath)
+ if mode is None:
+ mode = 0o777 & os.stat(localpath).st_mode
+ sftp.chmod(remotepath, mode)
+
+ TILDE_EXPANSIONS_RE = re.compile("(^~[^/]*/)?(.*)")
+
+ def _put_file_shell(self, localpath, remotepath, mode=None):
+ # quote to stop wordpslit
+ """
+ Transfer file (Internal) by ssh
+ """
+ tilde, remotepath = self.TILDE_EXPANSIONS_RE.match(remotepath).groups()
+ if not tilde:
+ tilde = ''
+ cmd = ['cat > %s"%s"' % (tilde, remotepath)]
+ if mode is not None:
+ # use -- so no options
+ cmd.append('chmod -- 0%o %s"%s"' % (mode, tilde, remotepath))
+
+ with open(localpath, "rb") as localfile:
+ # only chmod on successful cat
+ self.run("&& ".join(cmd), stdin=localfile)
+
+ def put_file(self, localpath, remotepath, mode=None):
+ """Copy specified local file to the server.
+
+ :param localpath: Local filename.
+ :param remotepath: Remote filename.
+ :param mode: Permissions to set after upload
+ """
+ try:
+ self._put_file_sftp(localpath, remotepath, mode=mode)
+ except (paramiko.SSHException, socket.error):
+ self._put_file_shell(localpath, remotepath, mode=mode)
+
+ def put_file_obj(self, file_obj, remotepath, mode=None):
+ """
+ CHange file permission
+ """
+ client = self._get_client()
+
+ with client.open_sftp() as sftp:
+ sftp.putfo(file_obj, remotepath)
+ if mode is not None:
+ sftp.chmod(remotepath, mode)
+
+ def get_file_obj(self, remotepath, file_obj):
+ """
+ Get File Object
+ """
+ client = self._get_client()
+
+ with client.open_sftp() as sftp:
+ sftp.getfo(remotepath, file_obj)
diff --git a/sdv/docker/sdvsecurity/pki/admin-key.pem b/sdv/docker/sdvsecurity/pki/admin-key.pem
new file mode 100644
index 0000000..f529932
--- /dev/null
+++ b/sdv/docker/sdvsecurity/pki/admin-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAv0HetyZsqmVlZ1GQeki8azRhLYtskCX+lljgEvabvIHrvDeZ
+CXFNBKbaWiMovLjdxQGrhBhcW2KnUJ0FvvtrCrOiLD/HoFjiKkIoeu84kcVYqBr1
+gD3eWEay0OMXSTjHNq1+kBjhAJHzMGNQFyWs5u4iyj5OGyAB0FgaZCwEDkAjP0z6
+7+AH6dhDSq4yox3elUvOgkSEr8m+BBsqc47RtrzRb4UZHyls7m116Pylx1jIdpYn
+/hkrLzWp217qkD8eFwSVOuI1LE/RJdyfniZh5dDDe6zG/ikVJBJyvnVlHC6XmCQ/
+54IoXsiRQ7UvFuCZhwRjOQJNbXLgTI9XvEPyPwIDAQABAoIBAGkG7hvbgO7tsYrU
+MqfES5v/bqIJH3vlMXI0qhAf1pPlMKPyUBrsWiQE0IGRLyy7xCUMbriifA0/Fqxh
+HierfzOoQ5VTDPfT95bHL92agpEiMzZVX52l2/TUFhw5qK7v/A3dplPRJbYfb9GR
+qAieCt3hxb8UCeZBZF2aFBwQQ8Xio7y34ICMSoXhZrMSiKERs84updqxYe97gYFQ
+dmQQXAOMid2vorMJU+SYa8mtEUh8jBqB/MBKR6nFwt9LG5sYzLO4F/ZXm3NocTzX
+TxpFVUY7AqbvcYgbfqub6cf0DSu59KA/nOc/HtQl4PQNg28ZUaCZ2IYAVdv5Nm1J
+Qgo1bskCgYEAwuaQhfrv0BLFAKCzQg9nlncFNc2ioE6gNrpUlJ16ykfIodkJ6Txq
+zd0wlqIJyMJxTLTYEPfkg0zfopFHI1adpjllAygTujLIZ6zml2B5xepgM1N7FthH
+vqUZcqwx4sdt+bH9TzmyqkyWJtyjz3gl0wkIE5TS2/wZclO07nW8ikMCgYEA+zbq
+TOft++qmyvyZcKlgclROF/lb+J4nzqDrudGQJe+ShH2J2ERA2hRmpVFBwdBjV8Uy
+7lAczFUMiVr+gzarMYpDojevVTBYPT12L3643mUZMQRoDwc1Ikp1TywJTNrfW5+D
+93OoxGfDxIy8i0xhf5vbpzMp0Ujz1bVr5ZzGLlUCgYBijt+ksQnWabdvotQjYtDa
+WOOsmolTkY8ZPc6JvL0cT4KYvb1yUZgc9G6ereBOwm0zAfyFUCYhc51fgyG7MBDW
+vw1itECNlyKasueEw0exGt189wk2uzZEpQ6iW4t6h6kIbMaQ6rd7wPDpRAgeYT6X
+YmjQJiEfF3PyyXukXqtyKwKBgGYjf8PhELu6PjKN3X0XAKW7bT0GH8TH2PuEJhOZ
+BvjFnVbNC/yoU43XeZyBmzDIH3wSK5EFdZAGjGBUZOLImXY89x6/cUQ7scTEdob9
+CQEzLbjmR8DEd3AHwgZ8qxNacjH4e1li5x1j6w7BAe/JEGMn5XjnrnrvJGShrMrg
+oRs5AoGBAIi56f3KznZ5K4HVvXMNtRsez+c7RWmnqf2fcFWGWDE/Azvy2vEFv+jU
+Fwmd26/u3qYdCuaO4R1keUcYF2pLwmD5KUBiIf1MYaytbnkIQNuzlNsR9C82D4Gt
+rgtS/WzJh/0BzvUzfUEXQOuVx764jBJ962exzVmV6esFw+n2WqbL
+-----END RSA PRIVATE KEY-----
diff --git a/sdv/docker/sdvsecurity/pki/admin.pem b/sdv/docker/sdvsecurity/pki/admin.pem
new file mode 100644
index 0000000..7810fd1
--- /dev/null
+++ b/sdv/docker/sdvsecurity/pki/admin.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDYDCCAkigAwIBAgIUP2xj3piYvuNWi2CZNF2+rFFvdAYwDQYJKoZIhvcNAQEL
+BQAwKjETMBEGA1UEChMKS3ViZXJuZXRlczETMBEGA1UEAxMKa3ViZXJuZXRlczAe
+Fw0yMDA5MTUwMDE2MDBaFw0yMTA5MTUwMDE2MDBaMCkxFzAVBgNVBAoTDnN5c3Rl
+bTptYXN0ZXJzMQ4wDAYDVQQDEwVhZG1pbjCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL9B3rcmbKplZWdRkHpIvGs0YS2LbJAl/pZY4BL2m7yB67w3mQlx
+TQSm2lojKLy43cUBq4QYXFtip1CdBb77awqzoiw/x6BY4ipCKHrvOJHFWKga9YA9
+3lhGstDjF0k4xzatfpAY4QCR8zBjUBclrObuIso+ThsgAdBYGmQsBA5AIz9M+u/g
+B+nYQ0quMqMd3pVLzoJEhK/JvgQbKnOO0ba80W+FGR8pbO5tdej8pcdYyHaWJ/4Z
+Ky81qdte6pA/HhcElTriNSxP0SXcn54mYeXQw3usxv4pFSQScr51ZRwul5gkP+eC
+KF7IkUO1LxbgmYcEYzkCTW1y4EyPV7xD8j8CAwEAAaN/MH0wDgYDVR0PAQH/BAQD
+AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA
+MB0GA1UdDgQWBBROGiXsY3bYbp2j8TweLqKXcOMo1jAfBgNVHSMEGDAWgBSFLj/5
+uiFO8GNv0zRPZT05buvXfzANBgkqhkiG9w0BAQsFAAOCAQEAj2wStkU+XQ/fKR8Z
+SXfU5HrdzqZg4RbIWPfAeznut2/1NPU0dhgNLJ7rLV0ioa61tOpmT6r9cTaGrCYu
+MeB5dLiStskEHRgmhAKTzWh8/iW7oLSfFowRLnjeXUc2gL0AnHNgyxiab4KLY+8E
+0qkUz8gofa+74yV4fRY9zRsRi/V+qmuruaqJxC8KNxTyEIm7d1C/H5Kcd/CFyX2X
+tcbozoPtyCWcbnVTgtmKrvdW7OGzzN1padCLrGWF7pkesXJcSHcqdBrNtKVXi+qp
+rcNXGzUt5cQgm7CdvLFuuJ7ZHOvAldYCm8FbWD2ym+Ewiq13z5OCmrVG5XonFlIU
+DkjqUA==
+-----END CERTIFICATE-----
diff --git a/sdv/docker/sdvsecurity/pki/cluster-ca.pem b/sdv/docker/sdvsecurity/pki/cluster-ca.pem
new file mode 100644
index 0000000..2d36033
--- /dev/null
+++ b/sdv/docker/sdvsecurity/pki/cluster-ca.pem
@@ -0,0 +1,20 @@
+----BEGIN CERTIFICATE-----
+MIIDSDCCAjCgAwIBAgIUe1GZtZIRpWZD7NyS2LfIijIg9tIwDQYJKoZIhvcNAQEL
+BQAwKjETMBEGA1UEChMKS3ViZXJuZXRlczETMBEGA1UEAxMKa3ViZXJuZXRlczAe
+Fw0yMDA5MTUwMDE2MDBaFw0yNTA5MTQwMDE2MDBaMCoxEzARBgNVBAoTCkt1YmVy
+bmV0ZXMxEzARBgNVBAMTCmt1YmVybmV0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCgRtxVqPCUny6juVnt13gYgGkWhG8NSuvoJY8t2EDytyOx0ft1
+whj6ZnTKjAsxQewuwG4Y2GXS35Ik7Qrk0gBXebN1Waz/Mnh4n3CJtsYU79jc96kz
+zGsvkTAuHypfFkBNj/vgMJhFIRb/Nd00e+peDqNCwq9Zq9/4UXdvYCO9zOhI7EKk
+i1dDiQdjDi0XT5Yt/qhhUa9FNUl2luoNWlRaNl6/gLBYmRRdRY4JpC9hxsToD8Mi
+IkWbeTQfKkUvmkKTgC8ruGVvLYjJTXoqimvCIiSYckEPkVIBlHjqz0L/Lq8+lVOf
+rLklXHsYAxOIa3KWIO94Kae+A6qYKivhYIGHAgMBAAGjZjBkMA4GA1UdDwEB/wQE
+AwIBBjASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBSFLj/5uiFO8GNv0zRP
+ZT05buvXfzAfBgNVHSMEGDAWgBSFLj/5uiFO8GNv0zRPZT05buvXfzANBgkqhkiG
+9w0BAQsFAAOCAQEAFlChgib90xuazjZq4ckyf4eTc6WuhzKPaPCL90TKelBwr9RV
+0ormq7Lve8KEDJsQh9jieh+BUiyH0wUk75PdpuBxFl9FCPSVRq+vFa+tmt8PDfvQ
+8hM7KC5BEk7iJR3aTsV+QbGlHkoy/5dxUpUGnzF4HYnhf5qVtfV245q3urclXgrU
+h8GjaMStyFSHgC1SMfvIWTZOT5/1gzDkwGzQrt9OMnX6j28MIayiuzPfLgOIMpWv
+MKZSGWspgLQZg/3GFwXuKy++a6WUqbSep3xA0bujk+04R4JnpOjpmC9/aTp4Z/44
+1XVrPu7cIZ+p2Uk53MPOIuAgNufyr+QAmVDqag==
+-----END CERTIFICATE-----
diff --git a/sdv/docker/sdvsecurity/security.conf b/sdv/docker/sdvsecurity/security.conf
new file mode 100644
index 0000000..4621d6a
--- /dev/null
+++ b/sdv/docker/sdvsecurity/security.conf
@@ -0,0 +1,4 @@
+# Custom Configuration. Use this file for customization before building.
+
+# One of 'docker' (rhosp, devstack-kolla), 'k8s' (airship), 'legacy' (devstack, tungsten-fabric, fuel)
+DEPLOYMENT = 'k8s'