diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 5d10324..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,42 +0,0 @@ -stages: - - build - - release - - publish - -variables: - IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME - -build: - stage: build - image: docker:stable-dind - script: - - echo "Logging to GitLab Container Registry with CI credentials..." - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - - docker build --tag $IMAGE_TAG . - - docker push $IMAGE_TAG - -semantic-release: - stage: release - image: enix/semantic-release:gitlab - only: - - master - except: - - tags - script: - - semantic-release --ci - -publish-docker-hub: - stage: publish - only: - refs: - - /^v([0-9]|[1-9][0-9]*)\.([0-9]|[1-9][0-9]*)\.([0-9]|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/ - image: docker:stable - script: - - echo "Logging to GitLab Container Registry with CI credentials..." - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - - docker pull $IMAGE_TAG - - docker tag $IMAGE_TAG $DOCKER_HUB_REPOSITORY/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME:1} - - echo "Logging to Docker Hub Registry with Enix credentials..." - - docker login -u "$DOCKER_HUB_USER" -p "$DOCKER_HUB_PASSWORD" - - echo "Push release $CI_COMMIT_REF_NAME ..." - - docker push $DOCKER_HUB_REPOSITORY/$CI_PROJECT_NAME:${CI_COMMIT_REF_NAME:1} \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index 43c1384..0000000 --- a/.releaserc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - ["@semantic-release/gitlab", { - "gitlabUrl": "https://gitlab.enix.io" - }] - ] -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a279cd1..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.7 - -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt - -COPY x509-exporter . - -EXPOSE 9090 - -ENTRYPOINT [ "python", "./x509-exporter" ] -CMD ["--port=9090"] \ No newline at end of file diff --git a/fixtures/kubelet-embedded.conf b/fixtures/kubelet-embedded.conf deleted file mode 100644 index 2b80d2d..0000000 --- a/fixtures/kubelet-embedded.conf +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNE1USXlOakV6TWpjek5Gb1hEVEk0TVRJeU16RXpNamN6TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBUEtYCkZKM0pRUDFlS1UvazMvV2sweWFLSEQvT2Y1cVdrMlAzMVV1aW9tNko5UlM2TWxOeFdERkJlcnF6UWlzQ0g2aisKYStzM3hpM2dmUXVlVnF6VCtQelRpNWgzSStHcFRnajlmdTh6WXkxbURiK3RveXRqSHVzOUU2RVF4ZnF0SGRUeAoza2RESlplVWJCZUdNSXlSR3FMeXNPT1BWdXB3UWI3MDFlZ1FyWHdtbExvcjJmaUxQR0FieUdBQjByY0ViQ1pECmh2MzA1bTZTRXY3WU9KTjRvVkwxTGpZRk5FdlpCVXFJTFIvS25DeGNKc3BXa2pQSU9tQi9VWElMbExjaElEaGwKQ2FaZWlvZWI3WlByY2Jaem5qYkhPY2lZaUlTSTUwSis2MXl3eHoxZzBKUDJpZnRiZmMwTC9QWVQ2SDl4MS94cQo0K1c1QUpXVmVWeVFJZWhmcDZjQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMTDhCNWxqTkhycGo0a0kxL0ZBSmZqaDhBa3UKTzdpMkhLR2RHZ0ZybUdMeUdaRTNSRERDa3I2UFpRdlVYQkhmTzM2dDc3S21wYW1FOTUrYTJkOWt6Yld3TUl3NQpSMERlKy95eHJDa3BRcTVma2lJQW9VMEIra1lGdmdmb1llMm9RUjBVSXN1SWNJcVNvQmtnUjlGQXYzdDBHV25iClZZa1VKOTlxQm5HT09OU1RLc09Ib29RemkrVnNJN0M3S1BvZTMzNkJaVnlWTTRHS1dJSnlNSTdPKysvUnFVK2YKa0FxQnVzcHBuZGxXREZsalZwNk5ydkhiZjRaaWxDbEluUVN0NHg3N0h6V0FSamo3Y3ZNb01nSEpEVXZJQS9NRwpIZEFYYjJ2N3VpZjhLTzFKK0Zza0tQdmVDdWNCeDhIUDlGTlIxNnZSSDVkZFBPK0xEU0xCMVNZZ3UrYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= - server: https://127.0.0.1:6443 - name: default-cluster -contexts: -- context: - cluster: default-cluster - namespace: default - user: default-auth - name: default-context -current-context: default-context -kind: Config -preferences: {} -users: -- name: default-auth - user: - client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNE1USXlOakV6TWpjek5Gb1hEVEk0TVRJeU16RXpNamN6TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBUEtYCkZKM0pRUDFlS1UvazMvV2sweWFLSEQvT2Y1cVdrMlAzMVV1aW9tNko5UlM2TWxOeFdERkJlcnF6UWlzQ0g2aisKYStzM3hpM2dmUXVlVnF6VCtQelRpNWgzSStHcFRnajlmdTh6WXkxbURiK3RveXRqSHVzOUU2RVF4ZnF0SGRUeAoza2RESlplVWJCZUdNSXlSR3FMeXNPT1BWdXB3UWI3MDFlZ1FyWHdtbExvcjJmaUxQR0FieUdBQjByY0ViQ1pECmh2MzA1bTZTRXY3WU9KTjRvVkwxTGpZRk5FdlpCVXFJTFIvS25DeGNKc3BXa2pQSU9tQi9VWElMbExjaElEaGwKQ2FaZWlvZWI3WlByY2Jaem5qYkhPY2lZaUlTSTUwSis2MXl3eHoxZzBKUDJpZnRiZmMwTC9QWVQ2SDl4MS94cQo0K1c1QUpXVmVWeVFJZWhmcDZjQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMTDhCNWxqTkhycGo0a0kxL0ZBSmZqaDhBa3UKTzdpMkhLR2RHZ0ZybUdMeUdaRTNSRERDa3I2UFpRdlVYQkhmTzM2dDc3S21wYW1FOTUrYTJkOWt6Yld3TUl3NQpSMERlKy95eHJDa3BRcTVma2lJQW9VMEIra1lGdmdmb1llMm9RUjBVSXN1SWNJcVNvQmtnUjlGQXYzdDBHV25iClZZa1VKOTlxQm5HT09OU1RLc09Ib29RemkrVnNJN0M3S1BvZTMzNkJaVnlWTTRHS1dJSnlNSTdPKysvUnFVK2YKa0FxQnVzcHBuZGxXREZsalZwNk5ydkhiZjRaaWxDbEluUVN0NHg3N0h6V0FSamo3Y3ZNb01nSEpEVXZJQS9NRwpIZEFYYjJ2N3VpZjhLTzFKK0Zza0tQdmVDdWNCeDhIUDlGTlIxNnZSSDVkZFBPK0xEU0xCMVNZZ3UrYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= - client-key-data: bG9sCg== diff --git a/fixtures/kubelet-paths.conf b/fixtures/kubelet-paths.conf deleted file mode 100644 index b0c1b01..0000000 --- a/fixtures/kubelet-paths.conf +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNE1USXlOakV6TWpjek5Gb1hEVEk0TVRJeU16RXpNamN6TkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBUEtYCkZKM0pRUDFlS1UvazMvV2sweWFLSEQvT2Y1cVdrMlAzMVV1aW9tNko5UlM2TWxOeFdERkJlcnF6UWlzQ0g2aisKYStzM3hpM2dmUXVlVnF6VCtQelRpNWgzSStHcFRnajlmdTh6WXkxbURiK3RveXRqSHVzOUU2RVF4ZnF0SGRUeAoza2RESlplVWJCZUdNSXlSR3FMeXNPT1BWdXB3UWI3MDFlZ1FyWHdtbExvcjJmaUxQR0FieUdBQjByY0ViQ1pECmh2MzA1bTZTRXY3WU9KTjRvVkwxTGpZRk5FdlpCVXFJTFIvS25DeGNKc3BXa2pQSU9tQi9VWElMbExjaElEaGwKQ2FaZWlvZWI3WlByY2Jaem5qYkhPY2lZaUlTSTUwSis2MXl3eHoxZzBKUDJpZnRiZmMwTC9QWVQ2SDl4MS94cQo0K1c1QUpXVmVWeVFJZWhmcDZjQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMTDhCNWxqTkhycGo0a0kxL0ZBSmZqaDhBa3UKTzdpMkhLR2RHZ0ZybUdMeUdaRTNSRERDa3I2UFpRdlVYQkhmTzM2dDc3S21wYW1FOTUrYTJkOWt6Yld3TUl3NQpSMERlKy95eHJDa3BRcTVma2lJQW9VMEIra1lGdmdmb1llMm9RUjBVSXN1SWNJcVNvQmtnUjlGQXYzdDBHV25iClZZa1VKOTlxQm5HT09OU1RLc09Ib29RemkrVnNJN0M3S1BvZTMzNkJaVnlWTTRHS1dJSnlNSTdPKysvUnFVK2YKa0FxQnVzcHBuZGxXREZsalZwNk5ydkhiZjRaaWxDbEluUVN0NHg3N0h6V0FSamo3Y3ZNb01nSEpEVXZJQS9NRwpIZEFYYjJ2N3VpZjhLTzFKK0Zza0tQdmVDdWNCeDhIUDlGTlIxNnZSSDVkZFBPK0xEU0xCMVNZZ3UrYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= - server: https://127.0.0.1:6443 - name: default-cluster -contexts: -- context: - cluster: default-cluster - namespace: default - user: default-auth - name: default-context -current-context: default-context -kind: Config -preferences: {} -users: -- name: default-auth - user: - client-certificate: ./one-cert.pem - client-key: ./does-not-exist.pem diff --git a/fixtures/one-cert.pem b/fixtures/one-cert.pem deleted file mode 100644 index 3e6c56c..0000000 --- a/fixtures/one-cert.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl -cm5ldGVzMB4XDTE4MTIyNjEzMjczNFoXDTI4MTIyMzEzMjczNFowFTETMBEGA1UE -AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPKX -FJ3JQP1eKU/k3/Wk0yaKHD/Of5qWk2P31Uuiom6J9RS6MlNxWDFBerqzQisCH6j+ -a+s3xi3gfQueVqzT+PzTi5h3I+GpTgj9fu8zYy1mDb+toytjHus9E6EQxfqtHdTx -3kdDJZeUbBeGMIyRGqLysOOPVupwQb701egQrXwmlLor2fiLPGAbyGAB0rcEbCZD -hv305m6SEv7YOJN4oVL1LjYFNEvZBUqILR/KnCxcJspWkjPIOmB/UXILlLchIDhl -CaZeioeb7ZPrcbZznjbHOciYiISI50J+61ywxz1g0JP2iftbfc0L/PYT6H9x1/xq -4+W5AJWVeVyQIehfp6cCAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB -/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALL8B5ljNHrpj4kI1/FAJfjh8Aku -O7i2HKGdGgFrmGLyGZE3RDDCkr6PZQvUXBHfO36t77KmpamE95+a2d9kzbWwMIw5 -R0De+/yxrCkpQq5fkiIAoU0B+kYFvgfoYe2oQR0UIsuIcIqSoBkgR9FAv3t0GWnb -VYkUJ99qBnGOONSTKsOHooQzi+VsI7C7KPoe336BZVyVM4GKWIJyMI7O++/RqU+f -kAqBusppndlWDFljVp6NrvHbf4ZilClInQSt4x77HzWARjj7cvMoMgHJDUvIA/MG -HdAXb2v7uif8KO1J+FskKPveCucBx8HP9FNR16vRH5ddPO+LDSLB1SYgu+c= ------END CERTIFICATE----- diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d7d1ccc..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pyOpenSSL -prometheus_client -pyasn1 -PyYAML \ No newline at end of file diff --git a/x509-exporter b/x509-exporter deleted file mode 100755 index fcff585..0000000 --- a/x509-exporter +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import logging -import os -import time -import base64 -import yaml - -from OpenSSL import crypto - -from prometheus_client import start_http_server -from prometheus_client.core import GaugeMetricFamily, REGISTRY - -from pyasn1.type.useful import GeneralizedTime - - -def parse_args(): - """Return the parsed arguments.""" - parser = argparse.ArgumentParser() - - parser.add_argument( - '-f', - '--watch-file', - action='append', - metavar='FILE_PATH', - help='one or more x509 certificate file') - parser.add_argument( - '-d', - '--watch-dir', - action='append', - metavar='DIR_PATH', - help='one or more directory which contains x509 certificate files') - parser.add_argument( - '-k', - '--watch-kubeconf', - action='append', - metavar='KUBE_PATH', - help='one or more Kubernetes client configuration (kind Config) which' - ' contains embedded x509 certificates or PEM file paths') - parser.add_argument( - '-p', - '--port', - default=9090, - metavar='PORT', - type=int, - help='prometheus exporter listening port') - - parser.add_argument( - '--debug', - action='store_true', - help='enable debug mode') - - return parser.parse_args() - - -def yaml_parser(fileobj): - """Returns a PyYAML parser avoiding warnings.""" - try: - parser = yaml.load(fileobj, Loader=yaml.FullLoader) - except AttributeError: - parser = yaml.load(fileobj) - - return parser - - -def parse_x509(filepath=None, b64=None, plain=None): - """x509 parsing helper.""" - if filepath: - plain = open(filepath, 'rt').read() - elif b64: - plain = base64.b64decode(b64) - - return crypto.load_certificate(crypto.FILETYPE_PEM, plain) - - -def walk_x509_files_and_dirs(args): - """Compile a deduped list of files.""" - x509_filepaths = set([]) - if args.watch_file: - for filename in args.watch_file: - try: - os.stat(filename) - x509_filepaths.add(filename) - except: - logging.warning("not able to stat file: %s", filename) - if args.watch_dir: - for directory in args.watch_dir: - for filename in os.listdir(directory): - filepath = os.path.join(directory, filename) - if os.path.isfile(filepath): - x509_filepaths.add(filepath) - return x509_filepaths - - -def walk_kubeconf_files(args): - """Compile a deduped list of Kubernetes client configurations.""" - kubeconf_filepaths = set([]) - if args.watch_kubeconf: - for filename in args.watch_kubeconf: - try: - with open(filename, 'r') as kubeconf: - conf = yaml_parser(kubeconf) - if conf['kind'] == 'Config' and conf['apiVersion'] == 'v1': - kubeconf_filepaths.add(filename) - except: - logging.warning("not able to parse file: %s", filename) - return kubeconf_filepaths - - -def check_certificates(x509_filepaths): - """Verify file content and sanitize the filepaths.""" - valid_certificates = [] - for x509_filepath in x509_filepaths: - try: - parse_x509(filepath=x509_filepath) - logging.info("valid x509 certificate detected: %s", - x509_filepath) - valid_certificates += [x509_filepath] - except: - logging.warning("not able to parse: %s", x509_filepath) - return valid_certificates - - -class X509Collector: - """a prometheus x509 collector.""" - - def __init__(self, x509_filepaths, kubeconf_filepaths): - """Take a set of filepaths as argument.""" - self.x509_filepaths = x509_filepaths - self.kubeconf_filepaths = kubeconf_filepaths - - def serialize_x509name(self, x509name): - """Helper to serialize a x509Name as a string.""" - return b' '.join( - b'='.join(component) for component in x509name.get_components() - ).decode('utf-8') - - def add_x509name_components_labels(self, x509name, labels, prefix): - """Add x509 name components to label list.""" - if x509name.C: - labels[prefix + '_C'] = x509name.C - if x509name.ST: - labels[prefix + '_ST'] = x509name.ST - if x509name.L: - labels[prefix + '_L'] = x509name.L - if x509name.O: - labels[prefix + '_O'] = x509name.O - if x509name.OU: - labels[prefix + '_OU'] = x509name.OU - if x509name.CN: - labels[prefix + '_CN'] = x509name.CN - - def build_labels(self, x509, filepath, embedded=None): - """Return prometheus metric labels.""" - labels = { - 'filename': os.path.basename(filepath), - 'filepath': filepath, - 'serial_number': "%s" % x509.get_serial_number(), - } - if embedded: - labels.update(dict(zip(('embedded_kind', 'embedded_key'), - embedded))) - self.add_x509name_components_labels( - x509.get_issuer(), - labels, "issuer") - self.add_x509name_components_labels( - x509.get_subject(), - labels, "subject") - return labels - - def build_metrics(self, x509): - """Return prometheus metrics.""" - x509_not_before_asn1_time = GeneralizedTime(x509.get_notBefore()) - x509_not_before_ts = x509_not_before_asn1_time.asDateTime.timestamp() - - x509_not_after_asn1_time = GeneralizedTime(x509.get_notAfter()) - x509_not_after_ts = x509_not_after_asn1_time.asDateTime.timestamp() - - expired = float(x509.has_expired()) - - return (x509_not_before_ts, x509_not_after_ts, expired) - - def collect(self): - """The main Prometheus collector function.""" - kubex509_filepaths = [] - for kubeconf_filepath in self.kubeconf_filepaths: - try: - with open(kubeconf_filepath, 'r') as kubeconf: - conf = yaml_parser(kubeconf) - cert_roots = [('cluster', 'certificate-authority'), - ('user', 'client-certificate')] - for item_type, key_base in cert_roots: - for item in conf.get("{}s".format(item_type), []): - name = item.get("name") - item_data = item.get(item_type) - if not name or not item_data: - continue - - if key_base in item_data: - kubex509_filepaths.append(item_data.get(key_base)) - - mbed_key = "{}-data".format(key_base) - if mbed_key in item_data: - x509 = parse_x509(b64=item_data.get(mbed_key)) - labels = self.build_labels(x509, kubeconf_filepath, - embedded=(item_type, name)) - yield from self.emit_x509(x509, labels) - except: - logging.warning("not able to parse and compute: %s", - kubeconf_filepath) - - for x509_filepath in self.x509_filepaths + kubex509_filepaths: - try: - x509 = parse_x509(filepath=x509_filepath) - yield from self.emit_x509(x509, - self.build_labels(x509, x509_filepath)) - except: - logging.warning("not able to parse and compute: %s", - x509_filepath) - - def emit_x509(self, x509, labels): - """Produce Prometheus metrics.""" - not_before, not_after, expired = self.build_metrics(x509) - - metric = GaugeMetricFamily( - 'x509_cert_not_before', - "x509 certificate not_before timestamp", - labels=labels.keys()) - metric.add_metric(labels.values(), not_before) - yield metric - - metric = GaugeMetricFamily( - 'x509_cert_not_after', - "x509 certificate not_after timestamp", - labels=labels.keys()) - metric.add_metric(labels.values(), not_after) - yield metric - - metric = GaugeMetricFamily( - 'x509_cert_expired', - "x509 certificate expiration boolean", - labels=labels.keys()) - metric.add_metric(labels.values(), expired) - yield metric - - -LOOP_SLEEP_TIME = 300 - - -def main(): - """x509-exporter main function.""" - args = parse_args() - logging.basicConfig( - level=logging.DEBUG if args.debug else logging.INFO, - format='%(asctime)s %(levelname)s %(message)s') - - x509_filepaths = walk_x509_files_and_dirs(args) - x509_filepaths = check_certificates(x509_filepaths) - kubeconfig_filepaths = walk_kubeconf_files(args) - logging.info("monitoring %s certificate(s) and %s kubeconfig(s)", - len(x509_filepaths), len(kubeconfig_filepaths)) - REGISTRY.register(X509Collector(x509_filepaths, kubeconfig_filepaths)) - logging.info("starting exporter on port %d", args.port) - start_http_server(args.port) - while True: - time.sleep(LOOP_SLEEP_TIME) - - -if __name__ == '__main__': - main()