diff options
-rw-r--r-- | .travis.yml | 16 | ||||
-rw-r--r-- | build.sh | 3 | ||||
-rw-r--r-- | docker/security/Dockerfile | 3 | ||||
-rw-r--r-- | docker/security/testcases.yaml | 29 | ||||
-rw-r--r-- | functest_kubernetes/security/__init__.py | 0 | ||||
-rw-r--r-- | functest_kubernetes/security/kube-bench.yaml | 51 | ||||
-rw-r--r-- | functest_kubernetes/security/kube-hunter.yaml | 14 | ||||
-rw-r--r-- | functest_kubernetes/security/security.py | 121 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rw-r--r-- | tox.ini | 2 |
10 files changed, 237 insertions, 4 deletions
diff --git a/.travis.yml b/.travis.yml index dfdaa176..b2996055 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ jobs: --platforms linux/amd64 \ --template ${DOCKER_USERNAME}/functest-kubernetes-core:ARCH-latest \ --target ${DOCKER_USERNAME}/functest-kubernetes-core:latest - - stage: build functest-kubernetes-[healthcheck,cnf] images + - stage: build functest-kubernetes-[healthcheck,cnf,security] images script: sudo -E bash build.sh env: - REPO="${DOCKER_USERNAME}" @@ -46,7 +46,13 @@ jobs: - amd64_dirs="docker/cnf" - arm64_dirs="" - arm_dirs="" - - stage: publish functest-kubernetes-[healthcheck,cnf] manifests + - script: sudo -E bash build.sh + env: + - REPO="${DOCKER_USERNAME}" + - amd64_dirs="docker/security" + - arm64_dirs="" + - arm_dirs="" + - stage: publish functest-kubernetes-[healthcheck,cnf,security] manifests script: > sudo manifest-tool push from-args \ --platforms linux/amd64 \ @@ -58,6 +64,12 @@ jobs: --platforms linux/amd64 \ --template ${DOCKER_USERNAME}/functest-kubernetes-cnf:ARCH-latest \ --target ${DOCKER_USERNAME}/functest-kubernetes-cnf:latest + - script: > + sudo manifest-tool push from-args \ + --platforms linux/amd64 \ + --template \ + ${DOCKER_USERNAME}/functest-kubernetes-security:ARCH-latest \ + --target ${DOCKER_USERNAME}/functest-kubernetes-security:latest - stage: build functest-kubernetes-smoke image script: sudo -E bash build.sh env: @@ -7,7 +7,8 @@ amd64_dirs=${amd64_dirs-"\ docker/core \ docker/healthcheck \ docker/smoke \ -docker/cnf"} +docker/cnf \ +docker/security"} arm64_dirs=${arm64_dirs-${amd64_dirs}} build_opts=(--pull=true --no-cache --force-rm=true) diff --git a/docker/security/Dockerfile b/docker/security/Dockerfile new file mode 100644 index 00000000..915ebd65 --- /dev/null +++ b/docker/security/Dockerfile @@ -0,0 +1,3 @@ +FROM opnfv/functest-kubernetes-core + +COPY testcases.yaml /usr/lib/python3.8/site-packages/xtesting/ci/testcases.yaml diff --git a/docker/security/testcases.yaml b/docker/security/testcases.yaml new file mode 100644 index 00000000..55c0b3be --- /dev/null +++ b/docker/security/testcases.yaml @@ -0,0 +1,29 @@ +--- +tiers: + - + name: security + order: 1 + ci_loop: '(daily)|(weekly)' + description: >- + Set of basic security tests. + testcases: + - + case_name: kube_hunter + project_name: security + criteria: 100 + blocking: false + description: >- + Check that the kubernetes cluster has no known + vulnerabilities + run: + name: 'kube_hunter' + - + case_name: kube_bench + project_name: security + criteria: 100 + blocking: false + description: >- + Check that the kubernetes cluster has no known + vulnerabilities + run: + name: 'kube_bench' diff --git a/functest_kubernetes/security/__init__.py b/functest_kubernetes/security/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/functest_kubernetes/security/__init__.py diff --git a/functest_kubernetes/security/kube-bench.yaml b/functest_kubernetes/security/kube-bench.yaml new file mode 100644 index 00000000..ec42ba16 --- /dev/null +++ b/functest_kubernetes/security/kube-bench.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: kube-bench +spec: + template: + metadata: + labels: + app: kube-bench + spec: + hostPID: true + containers: + - name: kube-bench + image: aquasec/kube-bench:latest + command: ["kube-bench"] + volumeMounts: + - name: var-lib-etcd + mountPath: /var/lib/etcd + readOnly: true + - name: var-lib-kubelet + mountPath: /var/lib/kubelet + readOnly: true + - name: etc-systemd + mountPath: /etc/systemd + readOnly: true + - name: etc-kubernetes + mountPath: /etc/kubernetes + readOnly: true + # /usr/local/mount-from-host/bin is mounted to access kubectl / kubelet, for auto-detecting the Kubernetes version. + # You can omit this mount if you specify --version as part of the command. + - name: usr-bin + mountPath: /usr/local/mount-from-host/bin + readOnly: true + restartPolicy: Never + volumes: + - name: var-lib-etcd + hostPath: + path: "/var/lib/etcd" + - name: var-lib-kubelet + hostPath: + path: "/var/lib/kubelet" + - name: etc-systemd + hostPath: + path: "/etc/systemd" + - name: etc-kubernetes + hostPath: + path: "/etc/kubernetes" + - name: usr-bin + hostPath: + path: "/usr/bin" diff --git a/functest_kubernetes/security/kube-hunter.yaml b/functest_kubernetes/security/kube-hunter.yaml new file mode 100644 index 00000000..ce88c062 --- /dev/null +++ b/functest_kubernetes/security/kube-hunter.yaml @@ -0,0 +1,14 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: kube-hunter +spec: + template: + spec: + containers: + - name: kube-hunter + image: aquasec/kube-hunter + command: ["python", "kube-hunter.py"] + args: ["--pod"] + restartPolicy: Never + backoffLimit: 4 diff --git a/functest_kubernetes/security/security.py b/functest_kubernetes/security/security.py new file mode 100644 index 00000000..33e70f86 --- /dev/null +++ b/functest_kubernetes/security/security.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +# Copyright (c) 2020 Orange and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Define the parent for Kubernetes testing. +""" + +from __future__ import division + +import logging +import time +import yaml + +from kubernetes import client +from kubernetes import config +from kubernetes import watch +import pkg_resources +from xtesting.core import testcase + + +class SecurityTesting(testcase.TestCase): + """Run Security job""" + namespace = 'default' + watch_timeout = 1200 + + __logger = logging.getLogger(__name__) + + def __init__(self, **kwargs): + super(SecurityTesting, self).__init__(**kwargs) + config.load_kube_config() + self.corev1 = client.CoreV1Api() + self.batchv1 = client.BatchV1Api() + self.pod = None + self.job_name = None + + def deploy_job(self): + """Run Security job + + It runs a single security job and then simply prints its output asis. + """ + + assert self.job_name + with open(pkg_resources.resource_filename( + "functest_kubernetes", + "security/{}.yaml".format(self.job_name))) as yfile: + body = yaml.safe_load(yfile) + api_response = self.batchv1.create_namespaced_job( + body=body, namespace="default") + self.__logger.info("Job %s created", api_response.metadata.name) + self.__logger.debug("create_namespaced_job: %s", api_response) + watch_job = watch.Watch() + for event in watch_job.stream( + func=self.batchv1.list_namespaced_job, + namespace=self.namespace, timeout_seconds=self.watch_timeout): + if (event["object"].metadata.name == self.job_name and + event["object"].status.succeeded == 1): + self.__logger.info( + "%s started in %0.2f sec", event['object'].metadata.name, + time.time()-self.start_time) + watch_job.stop() + pods = self.corev1.list_namespaced_pod( + self.namespace, label_selector='job-name={}'.format(self.job_name)) + self.pod = pods.items[0].metadata.name + api_response = self.corev1.read_namespaced_pod_log( + name=self.pod, namespace=self.namespace) + self.__logger.warning("\n\n%s", api_response) + self.result = 100 + + def run(self, **kwargs): + assert self.job_name + self.start_time = time.time() + try: + self.deploy_job() + except client.rest.ApiException: + self.__logger.exception("Cannot run %s", self.job_name) + self.stop_time = time.time() + + def clean(self): + try: + api_response = self.corev1.delete_namespaced_pod( + name=self.pod, namespace=self.namespace) + self.__logger.debug("delete_namespaced_pod: %s", api_response) + except client.rest.ApiException: + pass + try: + api_response = self.batchv1.delete_namespaced_job( + name=self.job_name, namespace=self.namespace) + self.__logger.debug( + "delete_namespaced_deployment: %s", api_response) + except client.rest.ApiException: + pass + + +class KubeHunter(SecurityTesting): + """kube-hunter hunts for security weaknesses in Kubernetes clusters. + + See https://github.com/aquasecurity/kube-hunter for more details + """ + + def __init__(self, **kwargs): + super(KubeHunter, self).__init__(**kwargs) + self.job_name = "kube-hunter" + + +class KubeBench(SecurityTesting): + """kube-bench checks whether Kubernetes is deployed securelyself. + + It runs the checks documented in the CIS Kubernetes Benchmark. + + See https://github.com/aquasecurity/kube-bench for more details + """ + + def __init__(self, **kwargs): + super(KubeBench, self).__init__(**kwargs) + self.job_name = "kube-bench" @@ -12,3 +12,5 @@ xtesting.testcase = k8s_conformance = functest_kubernetes.k8stest:K8sConformanceTest xrally_kubernetes = functest_kubernetes.rally.rally_kubernetes:RallyKubernetes k8s_vims = functest_kubernetes.ims.ims:Vims + kube_hunter = functest_kubernetes.security.security:KubeHunter + kube_bench = functest_kubernetes.security.security:KubeBench @@ -23,7 +23,7 @@ commands = flake8 [testenv:pylint] basepython = python3.8 -commands = pylint --disable=locally-disabled --reports=n functest_kubernetes +commands = pylint --ignore-imports=y --disable=locally-disabled --reports=n functest_kubernetes [testenv:yamllint] basepython = python3.8 |