From d8bee2e02982f5157d88a054ac24766cd827734c Mon Sep 17 00:00:00 2001 From: Tobias Trabelsi Date: Sat, 26 Nov 2022 13:49:57 +0100 Subject: [PATCH 1/3] work in progress to support raw template types --- README.md | 5 +++ charts/bitwarden-crd-operator/Chart.yaml | 10 +++--- .../bitwarden-crd-operator/crds/template.yaml | 34 ++++++++++++++++++ .../templates/clusterrole.yaml | 2 +- logo.png | Bin 0 -> 31780 bytes requirements.txt | 1 + src/bitwardenCrdOperator.py | 20 +---------- src/dockerlogin.py | 2 +- src/filters/__init__.py | 0 src/filters/bitwarden_filter.py | 8 +++++ src/kv.py | 2 +- src/template.py | 7 ++++ src/utils/__init__.py | 0 src/utils/utils.py | 20 +++++++++++ 14 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 charts/bitwarden-crd-operator/crds/template.yaml create mode 100644 logo.png create mode 100644 src/filters/__init__.py create mode 100644 src/filters/bitwarden_filter.py create mode 100644 src/template.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/utils.py diff --git a/README.md b/README.md index f28e73c..8cd0f09 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,14 @@ Bitwarden CRD Operator is a kubernetes Operator based on [kopf](https://github.com/nolar/kopf/). The goal is to create kubernetes native secret objects from bitwarden. +

+ Bitwarden CRD Operator Logo +

+ > DISCLAIMER: > This project is still very work in progress :) + ## Getting started You will need a `ClientID` and `ClientSecret` ([where to get these](https://bitwarden.com/help/personal-api-key/)) as well as your password. diff --git a/charts/bitwarden-crd-operator/Chart.yaml b/charts/bitwarden-crd-operator/Chart.yaml index 47562a6..e8bd95e 100644 --- a/charts/bitwarden-crd-operator/Chart.yaml +++ b/charts/bitwarden-crd-operator/Chart.yaml @@ -4,9 +4,9 @@ description: Deploy the Bitwarden CRD Operator type: application -version: "v0.3.2" +version: "v0.4.0" -appVersion: "0.3.0" +appVersion: "0.4.0" keywords: - operator @@ -69,10 +69,10 @@ annotations: artifacthub.io/license: MIT artifacthub.io/operator: "true" artifacthub.io/changes: | - - kind: changed - description: "Switched to Alpine image" - kind: added - description: "Added CRDs Example to artifactshub" + description: "Added Template CRD" + - kind: added + description: "Added logo" artifacthub.io/images: | - name: bitwarden-crd-operator image: lerentis/bitwarden-crd-operator:0.3.0 diff --git a/charts/bitwarden-crd-operator/crds/template.yaml b/charts/bitwarden-crd-operator/crds/template.yaml new file mode 100644 index 0000000..9a5dd75 --- /dev/null +++ b/charts/bitwarden-crd-operator/crds/template.yaml @@ -0,0 +1,34 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: bitwarden-templates.lerentis.uploadfilter24.eu +spec: + scope: Namespaced + group: lerentis.uploadfilter24.eu + names: + kind: BitwardenTemplate + plural: bitwarden-templates + singular: bitwarden-template + shortNames: + - bwt + versions: + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + template: + type: string + namespace: + type: string + name: + type: string + required: + - template + - namespace + - name diff --git a/charts/bitwarden-crd-operator/templates/clusterrole.yaml b/charts/bitwarden-crd-operator/templates/clusterrole.yaml index d015f4e..71857bf 100644 --- a/charts/bitwarden-crd-operator/templates/clusterrole.yaml +++ b/charts/bitwarden-crd-operator/templates/clusterrole.yaml @@ -4,7 +4,7 @@ metadata: name: {{ include "bitwarden-crd-operator.serviceAccountName" . }}-role rules: - apiGroups: ["lerentis.uploadfilter24.eu"] - resources: ["bitwarden-secrets", "registry-credentials"] + resources: ["bitwarden-secrets", "registry-credentials", "bitwarden-templates"] verbs: ["get", "watch", "list", "create", "delete", "patch", "update"] - apiGroups: [""] resources: ["secrets"] diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..513ff57c3c72954df0caf96c3492c01eb773613f GIT binary patch literal 31780 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 7b030cb..9851629 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ kopf==1.35.6 kubernetes==25.3.0 +Jinja2==3.1.2 diff --git a/src/bitwardenCrdOperator.py b/src/bitwardenCrdOperator.py index 4eee3b7..4c3be14 100755 --- a/src/bitwardenCrdOperator.py +++ b/src/bitwardenCrdOperator.py @@ -1,26 +1,8 @@ #!/usr/bin/env python3 import kopf import os -import subprocess - -def get_secret_from_bitwarden(logger, id): - logger.info(f"Locking up secret with ID: {id}") - return command_wrapper(logger, f"get item {id}") - -def unlock_bw(logger): - token_output = command_wrapper(logger, "unlock --passwordenv BW_PASSWORD") - tokens = token_output.split('"')[1::2] - os.environ["BW_SESSION"] = tokens[1] - logger.info("Signin successful. Session exported") - -def command_wrapper(logger, command): - system_env = dict(os.environ) - sp = subprocess.Popen([f"bw {command}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=True, env=system_env) - out, err = sp.communicate() - if err: - logger.warn(f"Error during bw cli invokement: {err}") - return out.decode(encoding='UTF-8') +from utils.utils import command_wrapper, unlock_bw @kopf.on.startup() def bitwarden_signin(logger, **kwargs): diff --git a/src/dockerlogin.py b/src/dockerlogin.py index 4171c27..94fd48b 100644 --- a/src/dockerlogin.py +++ b/src/dockerlogin.py @@ -3,7 +3,7 @@ import kubernetes import base64 import json -from bitwardenCrdOperator import unlock_bw, get_secret_from_bitwarden +from utils.utils import unlock_bw, get_secret_from_bitwarden def create_dockerlogin(logger, secret, secret_json, username_ref, password_ref, registry): secret.type = "dockerconfigjson" diff --git a/src/filters/__init__.py b/src/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/filters/bitwarden_filter.py b/src/filters/bitwarden_filter.py new file mode 100644 index 0000000..cf0183c --- /dev/null +++ b/src/filters/bitwarden_filter.py @@ -0,0 +1,8 @@ +from utils.utils import get_secret_from_bitwarden + + +def datetime_format(value, format="%H:%M %d-%m-%y"): + return value.strftime(format) + +def bitwarden_lookup(value, id, field): + pass \ No newline at end of file diff --git a/src/kv.py b/src/kv.py index 59e97b1..9b65b78 100644 --- a/src/kv.py +++ b/src/kv.py @@ -3,7 +3,7 @@ import kubernetes import base64 import json -from bitwardenCrdOperator import unlock_bw, get_secret_from_bitwarden +from utils.utils import unlock_bw, get_secret_from_bitwarden def create_kv(secret, secret_json, content_def): secret.type = "Opaque" diff --git a/src/template.py b/src/template.py new file mode 100644 index 0000000..ca5d40e --- /dev/null +++ b/src/template.py @@ -0,0 +1,7 @@ +import kopf +from filters.bitwarden_filter import bitwarden_lookup +from jinja2 import Environment + + +Environment.filters["bitwarden"] = bitwarden_lookup + diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/utils.py b/src/utils/utils.py new file mode 100644 index 0000000..86e0472 --- /dev/null +++ b/src/utils/utils.py @@ -0,0 +1,20 @@ +import os +import subprocess + +def get_secret_from_bitwarden(logger, id): + logger.info(f"Locking up secret with ID: {id}") + return command_wrapper(logger, f"get item {id}") + +def unlock_bw(logger): + token_output = command_wrapper(logger, "unlock --passwordenv BW_PASSWORD") + tokens = token_output.split('"')[1::2] + os.environ["BW_SESSION"] = tokens[1] + logger.info("Signin successful. Session exported") + +def command_wrapper(logger, command): + system_env = dict(os.environ) + sp = subprocess.Popen([f"bw {command}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=True, env=system_env) + out, err = sp.communicate() + if err: + logger.warn(f"Error during bw cli invokement: {err}") + return out.decode(encoding='UTF-8') \ No newline at end of file From cb793a7490be00924bb5fcf52252f77f94fd916a Mon Sep 17 00:00:00 2001 From: Tobias Trabelsi Date: Sat, 26 Nov 2022 18:55:42 +0100 Subject: [PATCH 2/3] wip for jinja template type --- Dockerfile | 2 +- charts/bitwarden-crd-operator/Chart.yaml | 27 ++++++- .../crds/bitwarden-secrets.yaml | 1 + ...template.yaml => bitwarden-templates.yaml} | 4 + .../crds/registry-credentials.yaml | 1 + example_template.yaml | 19 +++++ src/dockerlogin.py | 4 +- src/filters/bitwarden_filter.py | 8 -- src/kv.py | 4 +- src/{filters => lookups}/__init__.py | 0 src/lookups/bitwarden_lookup.py | 5 ++ src/template.py | 73 ++++++++++++++++++- src/utils/utils.py | 3 +- 13 files changed, 131 insertions(+), 20 deletions(-) rename charts/bitwarden-crd-operator/crds/{template.yaml => bitwarden-templates.yaml} (90%) create mode 100644 example_template.yaml delete mode 100644 src/filters/bitwarden_filter.py rename src/{filters => lookups}/__init__.py (100%) create mode 100644 src/lookups/bitwarden_lookup.py diff --git a/Dockerfile b/Dockerfile index 962a902..85c284e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ COPY --chown=bw-operator:bw-operator src /home/bw-operator USER bw-operator ENTRYPOINT [ "kopf", "run", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ] -CMD [ "/home/bw-operator/bitwardenCrdOperator.py", "/home/bw-operator/kv.py", "/home/bw-operator/dockerlogin.py" ] +CMD [ "/home/bw-operator/bitwardenCrdOperator.py", "/home/bw-operator/kv.py", "/home/bw-operator/dockerlogin.py", "/home/bw-operator/template.py"] diff --git a/charts/bitwarden-crd-operator/Chart.yaml b/charts/bitwarden-crd-operator/Chart.yaml index e8bd95e..1167885 100644 --- a/charts/bitwarden-crd-operator/Chart.yaml +++ b/charts/bitwarden-crd-operator/Chart.yaml @@ -13,6 +13,8 @@ keywords: - bitwarden - vaultwarden +icon: https://lerentis.github.io/bitwarden-crd-operator/logo.png + home: https://lerentis.github.io/bitwarden-crd-operator/ sources: @@ -39,6 +41,11 @@ annotations: name: registry-credential displayName: Regestry Credentials description: Management Object to create regestry secrets from bitwarden + - kind: BitwardenTemplate + version: v1beta1 + name: bitwarden-template + displayName: Bitwarden Template + description: Management Object to create secrets from a jinja template with a bitwarden lookup artifacthub.io/crdsExamples: | - apiVersion: lerentis.uploadfilter24.eu/v1beta3 kind: BitwardenSecret @@ -66,6 +73,24 @@ annotations: id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" name: "test-regcred" namespace: "default" + - apiVersion: "lerentis.uploadfilter24.eu/v1beta1" + kind: BitwardenTemplate + metadata: + name: test + spec: + filename: "config.yaml" + name: "test-regcred" + namespace: "default" + template: | + --- + api: + enabled: True + key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "key") }} + allowCrossOrigin: false + apps: + "some.app.identifier:some_version": + pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "public_key") }} + enabled: true artifacthub.io/license: MIT artifacthub.io/operator: "true" artifacthub.io/changes: | @@ -75,4 +100,4 @@ annotations: description: "Added logo" artifacthub.io/images: | - name: bitwarden-crd-operator - image: lerentis/bitwarden-crd-operator:0.3.0 + image: lerentis/bitwarden-crd-operator:0.4.0 diff --git a/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml b/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml index 38b026c..f58e8fd 100644 --- a/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml +++ b/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: diff --git a/charts/bitwarden-crd-operator/crds/template.yaml b/charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml similarity index 90% rename from charts/bitwarden-crd-operator/crds/template.yaml rename to charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml index 9a5dd75..004b70e 100644 --- a/charts/bitwarden-crd-operator/crds/template.yaml +++ b/charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: @@ -22,6 +23,8 @@ spec: spec: type: object properties: + filename: + type: string template: type: string namespace: @@ -29,6 +32,7 @@ spec: name: type: string required: + - filename - template - namespace - name diff --git a/charts/bitwarden-crd-operator/crds/registry-credentials.yaml b/charts/bitwarden-crd-operator/crds/registry-credentials.yaml index fd7bcb3..5d6171c 100644 --- a/charts/bitwarden-crd-operator/crds/registry-credentials.yaml +++ b/charts/bitwarden-crd-operator/crds/registry-credentials.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: diff --git a/example_template.yaml b/example_template.yaml new file mode 100644 index 0000000..a488bcc --- /dev/null +++ b/example_template.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: "lerentis.uploadfilter24.eu/v1beta1" +kind: BitwardenTemplate +metadata: + name: test +spec: + filename: "config.yaml" + name: "test-regcred" + namespace: "default" + template: | + --- + api: + enabled: True + key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "key") }} + allowCrossOrigin: false + apps: + "some.app.identifier:some_version": + pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "public_key") }} + enabled: true \ No newline at end of file diff --git a/src/dockerlogin.py b/src/dockerlogin.py index 94fd48b..0928231 100644 --- a/src/dockerlogin.py +++ b/src/dockerlogin.py @@ -33,8 +33,8 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): secret_namespace = spec.get('namespace') unlock_bw(logger) - - secret_json_object = json.loads(get_secret_from_bitwarden(logger, id)) + logger.info(f"Locking up secret with ID: {id}") + secret_json_object = json.loads(get_secret_from_bitwarden(id)) api = kubernetes.client.CoreV1Api() diff --git a/src/filters/bitwarden_filter.py b/src/filters/bitwarden_filter.py deleted file mode 100644 index cf0183c..0000000 --- a/src/filters/bitwarden_filter.py +++ /dev/null @@ -1,8 +0,0 @@ -from utils.utils import get_secret_from_bitwarden - - -def datetime_format(value, format="%H:%M %d-%m-%y"): - return value.strftime(format) - -def bitwarden_lookup(value, id, field): - pass \ No newline at end of file diff --git a/src/kv.py b/src/kv.py index 9b65b78..cb4f9b2 100644 --- a/src/kv.py +++ b/src/kv.py @@ -27,8 +27,8 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): secret_namespace = spec.get('namespace') unlock_bw(logger) - - secret_json_object = json.loads(get_secret_from_bitwarden(logger, id)) + logger.info(f"Locking up secret with ID: {id}") + secret_json_object = json.loads(get_secret_from_bitwarden(id)) api = kubernetes.client.CoreV1Api() diff --git a/src/filters/__init__.py b/src/lookups/__init__.py similarity index 100% rename from src/filters/__init__.py rename to src/lookups/__init__.py diff --git a/src/lookups/bitwarden_lookup.py b/src/lookups/bitwarden_lookup.py new file mode 100644 index 0000000..559b937 --- /dev/null +++ b/src/lookups/bitwarden_lookup.py @@ -0,0 +1,5 @@ +from utils.utils import get_secret_from_bitwarden + +def bitwarden_lookup(id, field): + _secret_json = get_secret_from_bitwarden(id) + return _secret_json["login"][field] \ No newline at end of file diff --git a/src/template.py b/src/template.py index ca5d40e..4a00029 100644 --- a/src/template.py +++ b/src/template.py @@ -1,7 +1,72 @@ -import kopf -from filters.bitwarden_filter import bitwarden_lookup -from jinja2 import Environment +import kopf +import base64 +import kubernetes + +from utils.utils import unlock_bw +from lookups.bitwarden_lookup import bitwarden_lookup +from jinja2 import Environment, BaseLoader -Environment.filters["bitwarden"] = bitwarden_lookup +lookup_func_dict = { + "bitwarden_lookup": bitwarden_lookup, +} +def render_template(template): + jinja_template = Environment(loader=BaseLoader()).from_string(template) + jinja_template.globals.update(lookup_func_dict) + return jinja_template.render() + +def create_template_secret(secret, filename, template): + secret.type = "Opaque" + secret.data = {} + secret.data[filename] = str(base64.b64encode(render_template(template).encode("utf-8")), "utf-8") + return secret + +@kopf.on.create('bitwarden-templates.lerentis.uploadfilter24.eu') +def create_managed_secret(spec, name, namespace, logger, body, **kwargs): + + template = spec.get('template') + filename = spec.get('filename') + secret_name = spec.get('name') + secret_namespace = spec.get('namespace') + + unlock_bw(logger) + + api = kubernetes.client.CoreV1Api() + + annotations = { + "managed": "bitwarden-templates.lerentis.uploadfilter24.eu", + "managedObject": f"{namespace}/{name}" + } + secret = kubernetes.client.V1Secret() + secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) + secret = create_managed_secret(secret, filename, template) + + obj = api.create_namespaced_secret( + secret_namespace, secret + ) + + logger.info(f"Secret {secret_namespace}/{secret_name} has been created") + +@kopf.on.update('bitwarden-templates.lerentis.uploadfilter24.eu') +def my_handler(spec, old, new, diff, **_): + pass + +@kopf.on.delete('bitwarden-templates.lerentis.uploadfilter24.eu') +def delete_managed_secret(spec, name, namespace, logger, **kwargs): + secret_name = spec.get('name') + secret_namespace = spec.get('namespace') + api = kubernetes.client.CoreV1Api() + + try: + api.delete_namespaced_secret(secret_name, secret_namespace) + logger.info(f"Secret {secret_namespace}/{secret_name} has been deleted") + except: + logger.warn(f"Could not delete secret {secret_namespace}/{secret_name}!") + +#if __name__ == '__main__': +# tpl = """ +# Calling the 'bitwarden_lookup' function: +# {{ bitwarden_lookup(2, 2) }} +# """ +# print(render_template(tpl)) \ No newline at end of file diff --git a/src/utils/utils.py b/src/utils/utils.py index 86e0472..7700a3f 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -1,8 +1,7 @@ import os import subprocess -def get_secret_from_bitwarden(logger, id): - logger.info(f"Locking up secret with ID: {id}") +def get_secret_from_bitwarden(id): return command_wrapper(logger, f"get item {id}") def unlock_bw(logger): From d316c8567e60f0ef234d33293f2a75db3ddb03cb Mon Sep 17 00:00:00 2001 From: Tobias Trabelsi Date: Sat, 26 Nov 2022 21:33:31 +0100 Subject: [PATCH 3/3] working jinja template type --- charts/bitwarden-crd-operator/Chart.yaml | 22 +++++++++++-------- .../crds/bitwarden-secrets.yaml | 4 +++- .../crds/bitwarden-templates.yaml | 2 +- .../crds/registry-credentials.yaml | 2 +- example.yaml | 4 +++- example_dockerlogin.yaml | 2 +- example_template.yaml | 8 +++---- src/bitwardenCrdOperator.py | 9 ++++++-- src/dockerlogin.py | 8 +++---- src/kv.py | 17 +++++++++----- src/lookups/bitwarden_lookup.py | 13 +++++++---- src/template.py | 10 ++++----- src/utils/utils.py | 21 +++++++++++++----- 13 files changed, 78 insertions(+), 44 deletions(-) diff --git a/charts/bitwarden-crd-operator/Chart.yaml b/charts/bitwarden-crd-operator/Chart.yaml index 1167885..51f7e58 100644 --- a/charts/bitwarden-crd-operator/Chart.yaml +++ b/charts/bitwarden-crd-operator/Chart.yaml @@ -32,12 +32,12 @@ annotations: url: https://github.com/Lerentis/bitwarden-crd-operator artifacthub.io/crds: | - kind: BitwardenSecret - version: v1beta3 + version: v1beta4 name: bitwarden-secret displayName: Bitwarden Secret description: Management Object to create secrets from bitwarden - kind: RegistryCredential - version: v1beta3 + version: v1beta4 name: registry-credential displayName: Regestry Credentials description: Management Object to create regestry secrets from bitwarden @@ -47,7 +47,7 @@ annotations: displayName: Bitwarden Template description: Management Object to create secrets from a jinja template with a bitwarden lookup artifacthub.io/crdsExamples: | - - apiVersion: lerentis.uploadfilter24.eu/v1beta3 + - apiVersion: lerentis.uploadfilter24.eu/v1beta4 kind: BitwardenSecret metadata: name: test @@ -62,7 +62,7 @@ annotations: id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" name: "test-secret" namespace: "default" - - apiVersion: lerentis.uploadfilter24.eu/v1beta3 + - apiVersion: lerentis.uploadfilter24.eu/v1beta4 kind: RegistryCredential metadata: name: test @@ -73,7 +73,7 @@ annotations: id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" name: "test-regcred" namespace: "default" - - apiVersion: "lerentis.uploadfilter24.eu/v1beta1" + - apiVersion: "lerentis.uploadfilter24.eu/v1beta4" kind: BitwardenTemplate metadata: name: test @@ -85,19 +85,23 @@ annotations: --- api: enabled: True - key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "key") }} + key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "key") }} allowCrossOrigin: false apps: "some.app.identifier:some_version": - pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "public_key") }} - enabled: true + pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "public_key") }} + enabled: true artifacthub.io/license: MIT artifacthub.io/operator: "true" artifacthub.io/changes: | - kind: added - description: "Added Template CRD" + description: "Added Template type" - kind: added description: "Added logo" + - kind: changed + description: "BitwardenSecret now requires a 'secretScope' to be defined. Can eigher be 'login' or 'fields'" + - kind: fixed + description: "fixed hardcoded reference to 'login' even tho secrets could also be in 'fields' scope" artifacthub.io/images: | - name: bitwarden-crd-operator image: lerentis/bitwarden-crd-operator:0.4.0 diff --git a/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml b/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml index f58e8fd..4e420c9 100644 --- a/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml +++ b/charts/bitwarden-crd-operator/crds/bitwarden-secrets.yaml @@ -13,7 +13,7 @@ spec: shortNames: - bws versions: - - name: v1beta3 + - name: v1beta4 served: true storage: true schema: @@ -35,6 +35,8 @@ spec: type: string secretRef: type: string + secretScope: + type: string required: - secretName id: diff --git a/charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml b/charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml index 004b70e..fa2212c 100644 --- a/charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml +++ b/charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml @@ -13,7 +13,7 @@ spec: shortNames: - bwt versions: - - name: v1beta1 + - name: v1beta4 served: true storage: true schema: diff --git a/charts/bitwarden-crd-operator/crds/registry-credentials.yaml b/charts/bitwarden-crd-operator/crds/registry-credentials.yaml index 5d6171c..c3f4ffb 100644 --- a/charts/bitwarden-crd-operator/crds/registry-credentials.yaml +++ b/charts/bitwarden-crd-operator/crds/registry-credentials.yaml @@ -13,7 +13,7 @@ spec: shortNames: - rgc versions: - - name: v1beta3 + - name: v1beta4 served: true storage: true schema: diff --git a/example.yaml b/example.yaml index 670e6f3..a6e789e 100644 --- a/example.yaml +++ b/example.yaml @@ -1,5 +1,5 @@ --- -apiVersion: "lerentis.uploadfilter24.eu/v1beta3" +apiVersion: "lerentis.uploadfilter24.eu/v1beta4" kind: BitwardenSecret metadata: name: test @@ -8,9 +8,11 @@ spec: - element: secretName: username secretRef: nameofUser + secretScope: login - element: secretName: password secretRef: passwordOfUser + secretScope: login id: "88781348-c81c-4367-9801-550360c21295" name: "test-secret" namespace: "default" \ No newline at end of file diff --git a/example_dockerlogin.yaml b/example_dockerlogin.yaml index 4e34b17..f5f12d0 100644 --- a/example_dockerlogin.yaml +++ b/example_dockerlogin.yaml @@ -1,5 +1,5 @@ --- -apiVersion: "lerentis.uploadfilter24.eu/v1beta3" +apiVersion: "lerentis.uploadfilter24.eu/v1beta4" kind: RegistryCredential metadata: name: test diff --git a/example_template.yaml b/example_template.yaml index a488bcc..646dc4a 100644 --- a/example_template.yaml +++ b/example_template.yaml @@ -1,19 +1,19 @@ --- -apiVersion: "lerentis.uploadfilter24.eu/v1beta1" +apiVersion: "lerentis.uploadfilter24.eu/v1beta4" kind: BitwardenTemplate metadata: name: test spec: filename: "config.yaml" - name: "test-regcred" + name: "test-template" namespace: "default" template: | --- api: enabled: True - key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "key") }} + key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "key") }} allowCrossOrigin: false apps: "some.app.identifier:some_version": - pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "public_key") }} + pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "public_key") }} enabled: true \ No newline at end of file diff --git a/src/bitwardenCrdOperator.py b/src/bitwardenCrdOperator.py index 4c3be14..bb1ce72 100755 --- a/src/bitwardenCrdOperator.py +++ b/src/bitwardenCrdOperator.py @@ -7,9 +7,14 @@ from utils.utils import command_wrapper, unlock_bw @kopf.on.startup() def bitwarden_signin(logger, **kwargs): if 'BW_HOST' in os.environ: - command_wrapper(logger, f"config server {os.getenv('BW_HOST')}") + try: + command_wrapper(f"config server {os.getenv('BW_HOST')}") + except: + logger.warn("Revieved none zero exit code from server config") + logger.warn("This is expected from startup") + pass else: logger.info(f"BW_HOST not set. Assuming SaaS installation") - command_wrapper(logger, "login --apikey") + command_wrapper("login --apikey") unlock_bw(logger) diff --git a/src/dockerlogin.py b/src/dockerlogin.py index 0928231..095d322 100644 --- a/src/dockerlogin.py +++ b/src/dockerlogin.py @@ -23,7 +23,7 @@ def create_dockerlogin(logger, secret, secret_json, username_ref, password_ref, secret.data[".dockerconfigjson"] = str(base64.b64encode(json.dumps(auths_dict).encode("utf-8")), "utf-8") return secret -@kopf.on.create('registry-credentials.lerentis.uploadfilter24.eu') +@kopf.on.create('registry-credential.lerentis.uploadfilter24.eu') def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): username_ref = spec.get('usernameRef') password_ref = spec.get('passwordRef') @@ -39,7 +39,7 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): api = kubernetes.client.CoreV1Api() annotations = { - "managed": "registry-credentials.lerentis.uploadfilter24.eu", + "managed": "registry-credential.lerentis.uploadfilter24.eu", "managedObject": f"{namespace}/{name}" } secret = kubernetes.client.V1Secret() @@ -52,11 +52,11 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): logger.info(f"Registry Secret {secret_namespace}/{secret_name} has been created") -@kopf.on.update('registry-credentials.lerentis.uploadfilter24.eu') +@kopf.on.update('registry-credential.lerentis.uploadfilter24.eu') def my_handler(spec, old, new, diff, **_): pass -@kopf.on.delete('registry-credentials.lerentis.uploadfilter24.eu') +@kopf.on.delete('registry-credential.lerentis.uploadfilter24.eu') def delete_managed_secret(spec, name, namespace, logger, **kwargs): secret_name = spec.get('name') secret_namespace = spec.get('namespace') diff --git a/src/kv.py b/src/kv.py index cb4f9b2..598d3d6 100644 --- a/src/kv.py +++ b/src/kv.py @@ -3,7 +3,7 @@ import kubernetes import base64 import json -from utils.utils import unlock_bw, get_secret_from_bitwarden +from utils.utils import unlock_bw, get_secret_from_bitwarden, parse_login_scope, parse_fields_scope def create_kv(secret, secret_json, content_def): secret.type = "Opaque" @@ -15,10 +15,15 @@ def create_kv(secret, secret_json, content_def): _secret_key = value if key == "secretRef": _secret_ref = value - secret.data[_secret_ref] = str(base64.b64encode(secret_json["login"][_secret_key].encode("utf-8")), "utf-8") + if key == "secretScope": + _secret_scope = value + if _secret_scope == "login": + secret.data[_secret_ref] = str(base64.b64encode(parse_login_scope(secret_json, _secret_key).encode("utf-8")), "utf-8") + if _secret_scope == "fields": + secret.data[_secret_ref] = str(base64.b64encode(parse_fields_scope(secret_json, _secret_key).encode("utf-8")), "utf-8") return secret -@kopf.on.create('bitwarden-secrets.lerentis.uploadfilter24.eu') +@kopf.on.create('bitwarden-secret.lerentis.uploadfilter24.eu') def create_managed_secret(spec, name, namespace, logger, body, **kwargs): content_def = body['spec']['content'] @@ -33,7 +38,7 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): api = kubernetes.client.CoreV1Api() annotations = { - "managed": "bitwarden-secrets.lerentis.uploadfilter24.eu", + "managed": "bitwarden-secret.lerentis.uploadfilter24.eu", "managedObject": f"{namespace}/{name}" } secret = kubernetes.client.V1Secret() @@ -46,11 +51,11 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): logger.info(f"Secret {secret_namespace}/{secret_name} has been created") -@kopf.on.update('bitwarden-secrets.lerentis.uploadfilter24.eu') +@kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu') def my_handler(spec, old, new, diff, **_): pass -@kopf.on.delete('bitwarden-secrets.lerentis.uploadfilter24.eu') +@kopf.on.delete('bitwarden-secret.lerentis.uploadfilter24.eu') def delete_managed_secret(spec, name, namespace, logger, **kwargs): secret_name = spec.get('name') secret_namespace = spec.get('namespace') diff --git a/src/lookups/bitwarden_lookup.py b/src/lookups/bitwarden_lookup.py index 559b937..d15adb0 100644 --- a/src/lookups/bitwarden_lookup.py +++ b/src/lookups/bitwarden_lookup.py @@ -1,5 +1,10 @@ -from utils.utils import get_secret_from_bitwarden +import json -def bitwarden_lookup(id, field): - _secret_json = get_secret_from_bitwarden(id) - return _secret_json["login"][field] \ No newline at end of file +from utils.utils import get_secret_from_bitwarden, parse_fields_scope, parse_login_scope + +def bitwarden_lookup(id, scope, field): + _secret_json = json.loads(get_secret_from_bitwarden(id)) + if scope == "login": + return parse_login_scope(_secret_json, field) + if scope == "fields": + return parse_fields_scope(_secret_json, field) \ No newline at end of file diff --git a/src/template.py b/src/template.py index 4a00029..e3abcb9 100644 --- a/src/template.py +++ b/src/template.py @@ -22,7 +22,7 @@ def create_template_secret(secret, filename, template): secret.data[filename] = str(base64.b64encode(render_template(template).encode("utf-8")), "utf-8") return secret -@kopf.on.create('bitwarden-templates.lerentis.uploadfilter24.eu') +@kopf.on.create('bitwarden-template.lerentis.uploadfilter24.eu') def create_managed_secret(spec, name, namespace, logger, body, **kwargs): template = spec.get('template') @@ -35,12 +35,12 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): api = kubernetes.client.CoreV1Api() annotations = { - "managed": "bitwarden-templates.lerentis.uploadfilter24.eu", + "managed": "bitwarden-template.lerentis.uploadfilter24.eu", "managedObject": f"{namespace}/{name}" } secret = kubernetes.client.V1Secret() secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) - secret = create_managed_secret(secret, filename, template) + secret = create_template_secret(secret, filename, template) obj = api.create_namespaced_secret( secret_namespace, secret @@ -48,11 +48,11 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): logger.info(f"Secret {secret_namespace}/{secret_name} has been created") -@kopf.on.update('bitwarden-templates.lerentis.uploadfilter24.eu') +@kopf.on.update('bitwarden-template.lerentis.uploadfilter24.eu') def my_handler(spec, old, new, diff, **_): pass -@kopf.on.delete('bitwarden-templates.lerentis.uploadfilter24.eu') +@kopf.on.delete('bitwarden-template.lerentis.uploadfilter24.eu') def delete_managed_secret(spec, name, namespace, logger, **kwargs): secret_name = spec.get('name') secret_namespace = spec.get('namespace') diff --git a/src/utils/utils.py b/src/utils/utils.py index 7700a3f..9c1f543 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -1,19 +1,30 @@ import os import subprocess +class BitwardenCommandException(Exception): + pass + def get_secret_from_bitwarden(id): - return command_wrapper(logger, f"get item {id}") + return command_wrapper(command=f"get item {id}") def unlock_bw(logger): - token_output = command_wrapper(logger, "unlock --passwordenv BW_PASSWORD") + token_output = command_wrapper("unlock --passwordenv BW_PASSWORD") tokens = token_output.split('"')[1::2] os.environ["BW_SESSION"] = tokens[1] logger.info("Signin successful. Session exported") -def command_wrapper(logger, command): +def command_wrapper(command): system_env = dict(os.environ) sp = subprocess.Popen([f"bw {command}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=True, env=system_env) out, err = sp.communicate() if err: - logger.warn(f"Error during bw cli invokement: {err}") - return out.decode(encoding='UTF-8') \ No newline at end of file + raise BitwardenCommandException(err) + return out.decode(encoding='UTF-8') + +def parse_login_scope(secret_json, key): + return secret_json["login"][key] + +def parse_fields_scope(secret_json, key): + for entry in secret_json["fields"]: + if entry['name'] == key: + return entry['value']