From 060c78fd91241af1a638b570b769db133a9e7f04 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Mon, 10 Dec 2018 15:33:04 -0800 Subject: [PATCH 1/2] Fix Kubernetes Destination Plugin The Kubernetes plugin was broken. There were two major issues: * The server certificate was entered in a string input making it impossible (as far as I know) to enter a valid PEM certificate. * The base64 encoding calls were passing strings where bytes were expected. The fix to the first issue depends on #2218 and a change in the options structure. I've also included some improved input validation and logging. --- lemur/plugins/lemur_kubernetes/plugin.py | 92 +++++++++++++++--------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index ee466596..a640a677 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -11,12 +11,14 @@ .. moduleauthor:: Mikhail Khodorovskiy """ import base64 -import os -import urllib -import requests import itertools +import os -from lemur.certificates.models import Certificate +import requests +from flask import current_app + +from lemur.common.defaults import common_name +from lemur.common.utils import parse_certificate from lemur.plugins.bases import DestinationPlugin DEFAULT_API_VERSION = 'v1' @@ -26,21 +28,32 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): # _resolve_uri(k8s_base_uri, namespace, kind, name, api_ver=DEFAULT_API_VERSION) url = _resolve_uri(k8s_base_uri, namespace, kind) + current_app.logger.debug("K8S POST request URL: %s", url) create_resp = k8s_api.post(url, json=data) + current_app.logger.debug("K8S POST response: %s", create_resp) if 200 <= create_resp.status_code <= 299: return None - elif create_resp.json()['reason'] != 'AlreadyExists': - return create_resp.content + else: + json = create_resp.json() + if 'reason' in json: + if json['reason'] != 'AlreadyExists': + return create_resp.content + else: + return create_resp.content - update_resp = k8s_api.put(_resolve_uri(k8s_base_uri, namespace, kind, name), json=data) + url = _resolve_uri(k8s_base_uri, namespace, kind, name) + current_app.logger.debug("K8S PUT request URL: %s", url) + + update_resp = k8s_api.put(url, json=data) + current_app.logger.debug("K8S PUT response: %s", update_resp) if not 200 <= update_resp.status_code <= 299: return update_resp.content - return + return None def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION,): @@ -61,6 +74,12 @@ def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_V ])) +# Performs Base64 encoding of string to string using the base64.b64encode() function +# which encodes bytes to bytes. +def base64encode(string): + return base64.b64encode(string.encode()).decode() + + class KubernetesDestinationPlugin(DestinationPlugin): title = 'Kubernetes' slug = 'kubernetes-destination' @@ -74,28 +93,28 @@ class KubernetesDestinationPlugin(DestinationPlugin): 'name': 'kubernetesURL', 'type': 'str', 'required': True, - 'validation': '@(https?|http)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s]*)?$@iS', + 'validation': 'https?://[a-zA-Z0-9.-]+(?::[0-9]+)?', 'helpMessage': 'Must be a valid Kubernetes server URL!', }, { 'name': 'kubernetesAuthToken', 'type': 'str', 'required': True, - 'validation': '/^$|\s+/', + 'validation': '[0-9a-zA-Z-_.]+', 'helpMessage': 'Must be a valid Kubernetes server Token!', }, { 'name': 'kubernetesServerCertificate', - 'type': 'str', + 'type': 'textarea', 'required': True, - 'validation': '/^$|\s+/', + 'validation': '-----BEGIN CERTIFICATE-----[a-zA-Z0-9/+\\s\\r\\n]+-----END CERTIFICATE-----', 'helpMessage': 'Must be a valid Kubernetes server Certificate!', }, { 'name': 'kubernetesNamespace', 'type': 'str', 'required': True, - 'validation': '/^$|\s+/', + 'validation': '[a-z0-9]([-a-z0-9]*[a-z0-9])?', 'helpMessage': 'Must be a valid Kubernetes Namespace!', }, @@ -106,33 +125,38 @@ class KubernetesDestinationPlugin(DestinationPlugin): def upload(self, name, body, private_key, cert_chain, options, **kwargs): - k8_bearer = self.get_option('kubernetesAuthToken', options) - k8_cert = self.get_option('kubernetesServerCertificate', options) - k8_namespace = self.get_option('kubernetesNamespace', options) - k8_base_uri = self.get_option('kubernetesURL', options) + try: + k8_bearer = self.get_option('kubernetesAuthToken', options) + k8_cert = self.get_option('kubernetesServerCertificate', options) + k8_namespace = self.get_option('kubernetesNamespace', options) + k8_base_uri = self.get_option('kubernetesURL', options) - k8s_api = K8sSession(k8_bearer, k8_cert) + k8s_api = K8sSession(k8_bearer, k8_cert) - cert = Certificate(body=body) + cn = common_name(parse_certificate(body)) - # in the future once runtime properties can be passed-in - use passed-in secret name - secret_name = 'certs-' + urllib.quote_plus(cert.name) + # in the future once runtime properties can be passed-in - use passed-in secret name + secret_name = 'certs-' + cn - err = ensure_resource(k8s_api, k8s_base_uri=k8_base_uri, namespace=k8_namespace, kind="secret", name=secret_name, data={ - 'apiVersion': 'v1', - 'kind': 'Secret', - 'metadata': { - 'name': secret_name, - }, - 'data': { - 'combined.pem': base64.b64encode(body + private_key), - 'ca.crt': base64.b64encode(cert_chain), - 'service.key': base64.b64encode(private_key), - 'service.crt': base64.b64encode(body), - } - }) + err = ensure_resource(k8s_api, k8s_base_uri=k8_base_uri, namespace=k8_namespace, kind="secret", name=secret_name, data={ + 'apiVersion': 'v1', + 'kind': 'Secret', + 'metadata': { + 'name': secret_name, + }, + 'data': { + 'combined.pem': base64encode('%s\n%s' % (body, private_key)), + 'ca.crt': base64encode(cert_chain), + 'service.key': base64encode(private_key), + 'service.crt': base64encode(body), + } + }) + except Exception as e: + current_app.logger.exception("Exception in upload") + raise e if err is not None: + current_app.logger.debug("Error deploying resource: %s", err) raise Exception("Error uploading secret: " + err) From e7313da03e5d234b8829c7981d654a6e04dfb6b3 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Tue, 18 Dec 2018 22:24:48 -0500 Subject: [PATCH 2/2] Minor changes for code review suggestions. --- lemur/plugins/lemur_kubernetes/plugin.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index a640a677..4601592a 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -35,14 +35,8 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): if 200 <= create_resp.status_code <= 299: return None - - else: - json = create_resp.json() - if 'reason' in json: - if json['reason'] != 'AlreadyExists': - return create_resp.content - else: - return create_resp.content + elif create_resp.json().get('reason', '') != 'AlreadyExists': + return create_resp.content url = _resolve_uri(k8s_base_uri, namespace, kind, name) current_app.logger.debug("K8S PUT request URL: %s", url) @@ -53,7 +47,7 @@ def ensure_resource(k8s_api, k8s_base_uri, namespace, kind, name, data): if not 200 <= update_resp.status_code <= 299: return update_resp.content - return None + return def _resolve_ns(k8s_base_uri, namespace, api_ver=DEFAULT_API_VERSION,): @@ -152,8 +146,8 @@ class KubernetesDestinationPlugin(DestinationPlugin): } }) except Exception as e: - current_app.logger.exception("Exception in upload") - raise e + current_app.logger.exception("Exception in upload: {}".format(e), exc_info=True) + raise if err is not None: current_app.logger.debug("Error deploying resource: %s", err)