diff options
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' |