Compare commits
	
		
			28 Commits
		
	
	
		
			v0.2.0
			...
			40f76a8bdb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 40f76a8bdb | |||
| 8546855412 | |||
| 938ddd1bb6 | |||
| 5adc7785d0 | |||
| df1fdcbb14 | |||
| 3328016da4 | |||
| fdd3808c7e | |||
|  | fee8dfb97a | ||
|  | 2611231c8a | ||
| 884476606c | |||
|  | 92c51a21d0 | ||
| d316c8567e | |||
| cb793a7490 | |||
| d8bee2e029 | |||
|  | 12edc8445f | ||
|  | df7b9fd043 | ||
| 058e9b918f | |||
| 13c4b999d2 | |||
|  | f11d726cfe | ||
|  | 53443df46c | ||
|  | ef0f2633e0 | ||
|  | d13bcfd10c | ||
|  | 5e808df6ac | ||
| 69e56ef4f5 | |||
|  | 4c11c7d03a | ||
|  | b99111587d | ||
|  | e6c644c200 | ||
|  | 21baed2d92 | 
							
								
								
									
										15
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | # To get started with Dependabot version updates, you'll need to specify which | ||||||
|  | # package ecosystems to update and where the package manifests are located. | ||||||
|  | # Please see the documentation for all configuration options: | ||||||
|  | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates | ||||||
|  |  | ||||||
|  | version: 2 | ||||||
|  | updates: | ||||||
|  |   - package-ecosystem: "pip" | ||||||
|  |     directory: "/" | ||||||
|  |     schedule: | ||||||
|  |       interval: "weekly" | ||||||
|  |   - package-ecosystem: "github-actions" | ||||||
|  |     directory: "/" | ||||||
|  |     schedule: | ||||||
|  |       interval: "weekly" | ||||||
							
								
								
									
										36
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,7 +12,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v3 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|  |  | ||||||
| @@ -32,3 +32,37 @@ jobs: | |||||||
|           charts_dir: charts |           charts_dir: charts | ||||||
|         env: |         env: | ||||||
|           CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" |           CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" | ||||||
|  |  | ||||||
|  |       - name: Get app version from chart | ||||||
|  |         uses: mikefarah/yq@v4.30.6 | ||||||
|  |         id: app_version | ||||||
|  |         with: | ||||||
|  |           cmd: yq '.appVersion' charts/bitwarden-crd-operator/Chart.yaml | ||||||
|  |  | ||||||
|  |       - name: Create SBOM | ||||||
|  |         uses: anchore/sbom-action@v0 | ||||||
|  |         with: | ||||||
|  |           image: lerentis/bitwarden-crd-operator:${{ steps.app_version.outputs.result }} | ||||||
|  |          | ||||||
|  |       - name: Publish SBOM | ||||||
|  |         uses: anchore/sbom-action/publish-sbom@v0 | ||||||
|  |         with: | ||||||
|  |           sbom-artifact-match: ".*\\.spdx\\.json" | ||||||
|  |  | ||||||
|  |       - name: Get Latest Tag | ||||||
|  |         id: previoustag | ||||||
|  |         uses: WyriHaximus/github-action-get-previous-tag@v1 | ||||||
|  |  | ||||||
|  |       - name: Download SBOM from github action | ||||||
|  |         uses: actions/download-artifact@v3 | ||||||
|  |         with: | ||||||
|  |           name: ${{ env.ANCHORE_SBOM_ACTION_PRIOR_ARTIFACT }} | ||||||
|  |  | ||||||
|  |       - name: Add SBOM to release | ||||||
|  |         uses: svenstaro/upload-release-action@v2 | ||||||
|  |         with: | ||||||
|  |           repo_token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           file_glob: true | ||||||
|  |           file: lerentis-bitwarden-crd-operator_*.spdx.json | ||||||
|  |           tag:  ${{ steps.previoustag.outputs.tag }} | ||||||
|  |           overwrite: false | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,53 +1,34 @@ | |||||||
| FROM alpine:latest as builder | FROM alpine:latest as builder | ||||||
|  |  | ||||||
| ARG BW_VERSION=2022.8.0 | ARG BW_VERSION=2022.11.0 | ||||||
|  |  | ||||||
| RUN apk add wget unzip | RUN apk add wget unzip | ||||||
|  |  | ||||||
| RUN cd /tmp && wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip && \ | RUN cd /tmp && wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip && \ | ||||||
|     unzip /tmp/bw-linux-${BW_VERSION}.zip |     unzip /tmp/bw-linux-${BW_VERSION}.zip | ||||||
|  |  | ||||||
| #FROM alpine:3.18 as run | FROM alpine:3.17 | ||||||
| # |  | ||||||
| #RUN set -eux; \ |  | ||||||
| #    groupadd -r bw-operator ; \ |  | ||||||
| #    useradd -r -g bw-operator -s /sbin/nologin bw-operator; \ |  | ||||||
| #    mkdir -p /home/bw-operator; \ |  | ||||||
| #    chown -R bw-operator /home/bw-operator; \ |  | ||||||
| #    chmod +x /usr/local/bin/bw; \ |  | ||||||
| #    apk add libstdc++ python3 py-pip |  | ||||||
| #COPY --chown=bw-operator:bw-operator bitwarden-crd-operator.py /home/bw-operator/bitwarden-crd-operator.py |  | ||||||
| # |  | ||||||
| #USER bw-operator |  | ||||||
| # |  | ||||||
| #RUN set -eux; \ |  | ||||||
| #    pip install -r requirements.txt --no-warn-script-location |  | ||||||
| # |  | ||||||
| #ENTRYPOINT [ "/home/bw-operator/.local/bin/kopf", "run", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ] |  | ||||||
| #CMD [ "/home/bw-operator/bitwarden-crd-operator.py" ] |  | ||||||
|  |  | ||||||
| FROM ubuntu:jammy | ARG PYTHON_VERSION=3.10.9-r1 | ||||||
|  | ARG PIP_VERSION=22.3.1-r1 | ||||||
|  | ARG GCOMPAT_VERSION=1.1.0-r0 | ||||||
|  |  | ||||||
| COPY --from=builder /tmp/bw /usr/local/bin/bw | COPY --from=builder /tmp/bw /usr/local/bin/bw | ||||||
| COPY requirements.txt requirements.txt | COPY requirements.txt requirements.txt | ||||||
|  |  | ||||||
| RUN set -eux; \ | RUN set -eux; \ | ||||||
|     groupadd -r bw-operator ; \ |     addgroup -S -g 1000 bw-operator; \ | ||||||
|     useradd -r -g bw-operator -s /sbin/nologin bw-operator; \ |     adduser -S -D -u 1000 -G bw-operator bw-operator; \ | ||||||
|     mkdir -p /home/bw-operator; \ |     mkdir -p /home/bw-operator; \ | ||||||
|     chown -R bw-operator /home/bw-operator; \ |     chown -R bw-operator /home/bw-operator; \ | ||||||
|     chmod +x /usr/local/bin/bw; \ |     chmod +x /usr/local/bin/bw; \ | ||||||
|     apt-get update; \ |     apk add gcc musl-dev libstdc++ gcompat=${GCOMPAT_VERSION} python3=${PYTHON_VERSION} py-pip=${PIP_VERSION}; \ | ||||||
|     apt-get upgrade -y; \ |     pip install -r requirements.txt --no-warn-script-location; \ | ||||||
|     apt-get install -y --no-install-recommends python3 python3-pip; \ |     apk del --purge gcc musl-dev libstdc++; | ||||||
|     apt-get clean; |  | ||||||
|  |  | ||||||
| COPY --chown=bw-operator:bw-operator bitwarden-crd-operator.py /home/bw-operator/bitwarden-crd-operator.py | COPY --chown=bw-operator:bw-operator src /home/bw-operator | ||||||
|  |  | ||||||
| USER bw-operator | USER bw-operator | ||||||
|  |  | ||||||
| RUN set -eux; \ | ENTRYPOINT [ "kopf", "run", "--log-format=json", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ] | ||||||
|     pip install -r requirements.txt --no-warn-script-location | CMD [ "/home/bw-operator/bitwardenCrdOperator.py", "/home/bw-operator/kv.py", "/home/bw-operator/dockerlogin.py", "/home/bw-operator/template.py"] | ||||||
|  |  | ||||||
| ENTRYPOINT [ "/home/bw-operator/.local/bin/kopf", "run", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ] |  | ||||||
| CMD [ "/home/bw-operator/bitwarden-crd-operator.py" ] |  | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								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 is a kubernetes Operator based on [kopf](https://github.com/nolar/kopf/). The goal is to create kubernetes native secret objects from bitwarden. | ||||||
|  |  | ||||||
|  | <p align="center"> | ||||||
|  |   <img src="logo.png" alt="Bitwarden CRD Operator Logo" width="200"/> | ||||||
|  | </p> | ||||||
|  |  | ||||||
| > DISCLAIMER:   | > DISCLAIMER:   | ||||||
| > This project is still very work in progress :) | > This project is still very work in progress :) | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Getting started | ## 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. | You will need a `ClientID` and `ClientSecret` ([where to get these](https://bitwarden.com/help/personal-api-key/)) as well as your password. | ||||||
| @@ -51,7 +56,7 @@ And you are set to create your first secret using this operator. For that you ne | |||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta3" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: BitwardenSecret | kind: BitwardenSecret | ||||||
| metadata: | metadata: | ||||||
|   name: name-of-your-management-object |   name: name-of-your-management-object | ||||||
| @@ -60,9 +65,11 @@ spec: | |||||||
|     - element: |     - element: | ||||||
|         secretName: nameOfTheFieldInBitwarden # for example username |         secretName: nameOfTheFieldInBitwarden # for example username | ||||||
|         secretRef: nameOfTheKeyInTheSecretToBeCreated  |         secretRef: nameOfTheKeyInTheSecretToBeCreated  | ||||||
|  |         secretScope: login # for custom entries on bitwarden use 'fields'  | ||||||
|     - element: |     - element: | ||||||
|         secretName: nameOfAnotherFieldInBitwarden # for example password |         secretName: nameOfAnotherFieldInBitwarden # for example password | ||||||
|         secretRef: nameOfAnotherKeyInTheSecretToBeCreated  |         secretRef: nameOfAnotherKeyInTheSecretToBeCreated  | ||||||
|  |         secretScope: login # for custom entries on bitwarden use 'fields'  | ||||||
|   id: "A Secret ID from bitwarden" |   id: "A Secret ID from bitwarden" | ||||||
|   name: "Name of the secret to be created" |   name: "Name of the secret to be created" | ||||||
|   namespace: "Namespace of the secret to be created" |   namespace: "Namespace of the secret to be created" | ||||||
| @@ -91,7 +98,7 @@ For managing registry credentials, or pull secrets, you can create another kind | |||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta3" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: RegistryCredential | kind: RegistryCredential | ||||||
| metadata: | metadata: | ||||||
|   name: name-of-your-management-object |   name: name-of-your-management-object | ||||||
| @@ -120,6 +127,50 @@ metadata: | |||||||
| type: dockerconfigjson | type: dockerconfigjson | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ## BitwardenTemplate | ||||||
|  |  | ||||||
|  | One of the more freely defined types that can be used with this operator you can just pass a whole template: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: BitwardenTemplate | ||||||
|  | metadata: | ||||||
|  |   name: name-of-your-management-object | ||||||
|  | spec: | ||||||
|  |   filename: "Key of the secret to be created" | ||||||
|  |   name: "Name of the secret to be created" | ||||||
|  |   namespace: "Namespace of the secret to be created" | ||||||
|  |   template: | | ||||||
|  |     --- | ||||||
|  |     api: | ||||||
|  |       enabled: True | ||||||
|  |       key: {{ bitwarden_lookup("A Secret ID from bitwarden", "login or fields", "name of a field in bitwarden") }} | ||||||
|  |       allowCrossOrigin: false | ||||||
|  |       apps: | ||||||
|  |         "some.app.identifier:some_version": | ||||||
|  |           pubkey: {{ bitwarden_lookup("A Secret ID from bitwarden", "login or fields", "name of a field in bitwarden") }} | ||||||
|  |           enabled: true | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This will result in something like the following object: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | data: | ||||||
|  |   Key of the secret to be created: "base64 encoded and rendered template with secrets injected directly from bitwarden" | ||||||
|  | kind: Secret | ||||||
|  | metadata: | ||||||
|  |   annotations: | ||||||
|  |     managed: bitwarden-template.lerentis.uploadfilter24.eu | ||||||
|  |     managedObject: namespace/name-of-your-management-object | ||||||
|  |   name: Name of the secret to be created | ||||||
|  |   namespace: Namespace of the secret to be created | ||||||
|  | type: Opaque | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | please note that the rendering engine for this template is jinja2, with an addition of a custom `bitwarden_lookup` function, so there are more possibilities to inject here. | ||||||
|  |  | ||||||
| ## Short Term Roadmap | ## Short Term Roadmap | ||||||
|  |  | ||||||
| - [ ] support more types | - [ ] support more types | ||||||
|   | |||||||
| @@ -1,153 +0,0 @@ | |||||||
| #!/usr/bin/env python3 |  | ||||||
| import kopf |  | ||||||
| import kubernetes |  | ||||||
| import base64 |  | ||||||
| import os |  | ||||||
| import subprocess |  | ||||||
| import json |  | ||||||
|  |  | ||||||
| from pprint import pprint |  | ||||||
|  |  | ||||||
| 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') |  | ||||||
|  |  | ||||||
| def create_kv(secret, secret_json, content_def): |  | ||||||
|     secret.type = "Opaque" |  | ||||||
|     secret.data = {} |  | ||||||
|     for eleml in content_def: |  | ||||||
|         for k, elem in eleml.items(): |  | ||||||
|             for key,value in elem.items(): |  | ||||||
|                 if key == "secretName": |  | ||||||
|                     _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") |  | ||||||
|     return secret |  | ||||||
|  |  | ||||||
| def create_dockerlogin(logger, secret, secret_json, username_ref, password_ref, registry): |  | ||||||
|     secret.type = "dockerconfigjson" |  | ||||||
|     secret.data = {} |  | ||||||
|     auths_dict = {} |  | ||||||
|     registry_dict = {} |  | ||||||
|     reg_auth_dict = {} |  | ||||||
|  |  | ||||||
|     _username = secret_json["login"][username_ref] |  | ||||||
|     logger.info(f"Creating login with username: {_username}") |  | ||||||
|     _password = secret_json["login"][password_ref] |  | ||||||
|     cred_field = str(base64.b64encode(f"{_username}:{_password}".encode("utf-8")), "utf-8") |  | ||||||
|  |  | ||||||
|     reg_auth_dict["auth"] = cred_field |  | ||||||
|     registry_dict[registry] = reg_auth_dict |  | ||||||
|     auths_dict["auths"] = registry_dict |  | ||||||
|     secret.data[".dockerconfigjson"] = str(base64.b64encode(json.dumps(auths_dict).encode("utf-8")), "utf-8") |  | ||||||
|     return secret |  | ||||||
|  |  | ||||||
| @kopf.on.startup() |  | ||||||
| def bitwarden_signin(logger, **kwargs): |  | ||||||
|     if 'BW_HOST' in os.environ: |  | ||||||
|         command_wrapper(logger, f"config server {os.getenv('BW_HOST')}") |  | ||||||
|     else: |  | ||||||
|         logger.info(f"BW_HOST not set. Assuming SaaS installation") |  | ||||||
|     command_wrapper(logger, "login --apikey") |  | ||||||
|     unlock_bw(logger) |  | ||||||
|  |  | ||||||
| @kopf.on.create('registry-credentials.lerentis.uploadfilter24.eu') |  | ||||||
| def create_managed_registry_secret(spec, name, namespace, logger, body, **kwargs): |  | ||||||
|     username_ref = spec.get('usernameRef') |  | ||||||
|     password_ref = spec.get('passwordRef') |  | ||||||
|     registry = spec.get('registry') |  | ||||||
|     id = spec.get('id') |  | ||||||
|     secret_name = spec.get('name') |  | ||||||
|     secret_namespace = spec.get('namespace') |  | ||||||
|  |  | ||||||
|     unlock_bw(logger) |  | ||||||
|      |  | ||||||
|     secret_json_object = json.loads(get_secret_from_bitwarden(logger, id)) |  | ||||||
|  |  | ||||||
|     api = kubernetes.client.CoreV1Api() |  | ||||||
|  |  | ||||||
|     annotations = { |  | ||||||
|         "managed": "registry-credentials.lerentis.uploadfilter24.eu", |  | ||||||
|         "managedObject": f"{namespace}/{name}" |  | ||||||
|     } |  | ||||||
|     secret = kubernetes.client.V1Secret() |  | ||||||
|     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) |  | ||||||
|     secret = create_dockerlogin(logger, secret, secret_json_object, username_ref, password_ref, registry)    |  | ||||||
|  |  | ||||||
|     obj = api.create_namespaced_secret( |  | ||||||
|         secret_namespace, secret |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     logger.info(f"Registry Secret {secret_namespace}/{secret_name} has been created") |  | ||||||
|  |  | ||||||
| @kopf.on.create('bitwarden-secrets.lerentis.uploadfilter24.eu') |  | ||||||
| def create_managed_secret(spec, name, namespace, logger, body, **kwargs): |  | ||||||
|  |  | ||||||
|     content_def = body['spec']['content'] |  | ||||||
|     id = spec.get('id') |  | ||||||
|     secret_name = spec.get('name') |  | ||||||
|     secret_namespace = spec.get('namespace') |  | ||||||
|  |  | ||||||
|     unlock_bw(logger) |  | ||||||
|      |  | ||||||
|     secret_json_object = json.loads(get_secret_from_bitwarden(logger, id)) |  | ||||||
|  |  | ||||||
|     api = kubernetes.client.CoreV1Api() |  | ||||||
|  |  | ||||||
|     annotations = { |  | ||||||
|         "managed": "bitwarden-secrets.lerentis.uploadfilter24.eu", |  | ||||||
|         "managedObject": f"{namespace}/{name}" |  | ||||||
|     } |  | ||||||
|     secret = kubernetes.client.V1Secret() |  | ||||||
|     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) |  | ||||||
|     secret = create_kv(secret, secret_json_object, content_def)    |  | ||||||
|  |  | ||||||
|     obj = api.create_namespaced_secret( |  | ||||||
|         secret_namespace, secret |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     logger.info(f"Secret {secret_namespace}/{secret_name} has been created") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @kopf.on.update('bitwarden-secrets.lerentis.uploadfilter24.eu') |  | ||||||
| def my_handler(spec, old, new, diff, **_): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
| @kopf.on.delete('bitwarden-secrets.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}!") |  | ||||||
|  |  | ||||||
| @kopf.on.delete('registry-credentials.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}!") |  | ||||||
| @@ -4,15 +4,17 @@ description: Deploy the Bitwarden CRD Operator | |||||||
|  |  | ||||||
| type: application | type: application | ||||||
|  |  | ||||||
| version: "v0.3.0" | version: "v0.5.0" | ||||||
|  |  | ||||||
| appVersion: "0.2.0" | appVersion: "0.5.0" | ||||||
|  |  | ||||||
| keywords: | keywords: | ||||||
|   - operator |   - operator | ||||||
|   - bitwarden |   - bitwarden | ||||||
|   - vaultwarden |   - vaultwarden | ||||||
|  |  | ||||||
|  | icon: https://lerentis.github.io/bitwarden-crd-operator/logo.png | ||||||
|  |  | ||||||
| home: https://lerentis.github.io/bitwarden-crd-operator/ | home: https://lerentis.github.io/bitwarden-crd-operator/ | ||||||
|  |  | ||||||
| sources: | sources: | ||||||
| @@ -30,20 +32,74 @@ annotations: | |||||||
|       url: https://github.com/Lerentis/bitwarden-crd-operator |       url: https://github.com/Lerentis/bitwarden-crd-operator | ||||||
|   artifacthub.io/crds: | |   artifacthub.io/crds: | | ||||||
|     - kind: BitwardenSecret |     - kind: BitwardenSecret | ||||||
|       version: v1beta3 |       version: v1beta4 | ||||||
|       name: bitwarden-secret |       name: bitwarden-secret | ||||||
|       displayName: Bitwarden Secret |       displayName: Bitwarden Secret | ||||||
|       description: Management Object to create secrets from bitwarden |       description: Management Object to create secrets from bitwarden | ||||||
|     - kind: RegistryCredential |     - kind: RegistryCredential | ||||||
|       version: v1beta3 |       version: v1beta4 | ||||||
|       name: registry-credential |       name: registry-credential | ||||||
|       displayName: Regestry Credentials |       displayName: Regestry Credentials | ||||||
|       description: Management Object to create regestry secrets from bitwarden |       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/v1beta4 | ||||||
|  |       kind: BitwardenSecret | ||||||
|  |       metadata: | ||||||
|  |         name: test | ||||||
|  |       spec: | ||||||
|  |         content: | ||||||
|  |           - element: | ||||||
|  |               secretName: username | ||||||
|  |               secretRef: nameofUser  | ||||||
|  |           - element: | ||||||
|  |               secretName: password | ||||||
|  |               secretRef: passwordOfUser  | ||||||
|  |         id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" | ||||||
|  |         name: "test-secret" | ||||||
|  |         namespace: "default" | ||||||
|  |     - apiVersion: lerentis.uploadfilter24.eu/v1beta4 | ||||||
|  |       kind: RegistryCredential | ||||||
|  |       metadata: | ||||||
|  |         name: test | ||||||
|  |       spec: | ||||||
|  |         usernameRef: "username" | ||||||
|  |         passwordRef: "password" | ||||||
|  |         registry: "docker.io" | ||||||
|  |         id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" | ||||||
|  |         name: "test-regcred" | ||||||
|  |         namespace: "default" | ||||||
|  |     - apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  |       kind: BitwardenTemplate | ||||||
|  |       metadata: | ||||||
|  |         name: test | ||||||
|  |       spec: | ||||||
|  |         filename: "config.yaml" | ||||||
|  |         name: "test-regcred" | ||||||
|  |         namespace: "default" | ||||||
|  |         template: | | ||||||
|  |           --- | ||||||
|  |           api: | ||||||
|  |             enabled: True | ||||||
|  |             key: {{ bitwarden_lookup("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "fields", "key") }} | ||||||
|  |             allowCrossOrigin: false | ||||||
|  |             apps: | ||||||
|  |               "some.app.identifier:some_version": | ||||||
|  |                 pubkey: {{ bitwarden_lookup("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "fields", "public_key") }} | ||||||
|  |                 enabled: true   | ||||||
|   artifacthub.io/license: MIT |   artifacthub.io/license: MIT | ||||||
|   artifacthub.io/operator: "true"   |   artifacthub.io/operator: "true"   | ||||||
|   artifacthub.io/changes: | |   artifacthub.io/changes: | | ||||||
|     - kind: added |     - kind: added | ||||||
|       description: "Added support for regestry credentials" |       description: "Implemented update handling" | ||||||
|  |     - kind: changed | ||||||
|  |       description: "Changed default logging structure to json logging" | ||||||
|  |     - kind: changed | ||||||
|  |       description: "Secrets are periodically updated every 15 minutes" | ||||||
|   artifacthub.io/images: | |   artifacthub.io/images: | | ||||||
|     - name: bitwarden-crd-operator |     - name: bitwarden-crd-operator | ||||||
|       image: lerentis/bitwarden-crd-operator:0.2.0 |       image: lerentis/bitwarden-crd-operator:0.5.0 | ||||||
|   | |||||||
| @@ -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 is a kubernetes Operator based on [kopf](https://github.com/nolar/kopf/). The goal is to create kubernetes native secret objects from bitwarden. | ||||||
|  |  | ||||||
|  | <p align="center"> | ||||||
|  |   <img src="https://github.com/Lerentis/bitwarden-crd-operator/blob/main/logo.png?raw=true" alt="Bitwarden CRD Operator Logo" width="200"/> | ||||||
|  | </p> | ||||||
|  |  | ||||||
| > DISCLAIMER:   | > DISCLAIMER:   | ||||||
| > This project is still very work in progress :) | > This project is still very work in progress :) | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Getting started | ## 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. | You will need a `ClientID` and `ClientSecret` ([where to get these](https://bitwarden.com/help/personal-api-key/)) as well as your password. | ||||||
| @@ -51,7 +56,7 @@ And you are set to create your first secret using this operator. For that you ne | |||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta3" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: BitwardenSecret | kind: BitwardenSecret | ||||||
| metadata: | metadata: | ||||||
|   name: name-of-your-management-object |   name: name-of-your-management-object | ||||||
| @@ -60,9 +65,11 @@ spec: | |||||||
|     - element: |     - element: | ||||||
|         secretName: nameOfTheFieldInBitwarden # for example username |         secretName: nameOfTheFieldInBitwarden # for example username | ||||||
|         secretRef: nameOfTheKeyInTheSecretToBeCreated  |         secretRef: nameOfTheKeyInTheSecretToBeCreated  | ||||||
|  |         secretScope: login # for custom entries on bitwarden use 'fields'  | ||||||
|     - element: |     - element: | ||||||
|         secretName: nameOfAnotherFieldInBitwarden # for example password |         secretName: nameOfAnotherFieldInBitwarden # for example password | ||||||
|         secretRef: nameOfAnotherKeyInTheSecretToBeCreated  |         secretRef: nameOfAnotherKeyInTheSecretToBeCreated  | ||||||
|  |         secretScope: login # for custom entries on bitwarden use 'fields'  | ||||||
|   id: "A Secret ID from bitwarden" |   id: "A Secret ID from bitwarden" | ||||||
|   name: "Name of the secret to be created" |   name: "Name of the secret to be created" | ||||||
|   namespace: "Namespace of the secret to be created" |   namespace: "Namespace of the secret to be created" | ||||||
| @@ -91,7 +98,7 @@ For managing registry credentials, or pull secrets, you can create another kind | |||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta3" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: RegistryCredential | kind: RegistryCredential | ||||||
| metadata: | metadata: | ||||||
|   name: name-of-your-management-object |   name: name-of-your-management-object | ||||||
| @@ -120,10 +127,46 @@ metadata: | |||||||
| type: dockerconfigjson | type: dockerconfigjson | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Short Term Roadmap | ## BitwardenTemplate | ||||||
|  |  | ||||||
| - [ ] support more types | One of the more freely defined types that can be used with this operator you can just pass a whole template: | ||||||
| - [x] offer option to use a existing secret in helm chart |  | ||||||
| - [x] host chart on gh pages | ```yaml | ||||||
| - [x] write release pipeline | --- | ||||||
| - [x] maybe extend spec to offer modification of keys as well | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: BitwardenTemplate | ||||||
|  | metadata: | ||||||
|  |   name: name-of-your-management-object | ||||||
|  | spec: | ||||||
|  |   filename: "Key of the secret to be created" | ||||||
|  |   name: "Name of the secret to be created" | ||||||
|  |   namespace: "Namespace of the secret to be created" | ||||||
|  |   template: | | ||||||
|  |     --- | ||||||
|  |     api: | ||||||
|  |       enabled: True | ||||||
|  |       key: {{ bitwarden_lookup("A Secret ID from bitwarden", "login or fields", "name of a field in bitwarden") }} | ||||||
|  |       allowCrossOrigin: false | ||||||
|  |       apps: | ||||||
|  |         "some.app.identifier:some_version": | ||||||
|  |           pubkey: {{ bitwarden_lookup("A Secret ID from bitwarden", "login or fields", "name of a field in bitwarden") }} | ||||||
|  |           enabled: true | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This will result in something like the following object: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | data: | ||||||
|  |   Key of the secret to be created: "base64 encoded and rendered template with secrets injected directly from bitwarden" | ||||||
|  | kind: Secret | ||||||
|  | metadata: | ||||||
|  |   annotations: | ||||||
|  |     managed: bitwarden-template.lerentis.uploadfilter24.eu | ||||||
|  |     managedObject: namespace/name-of-your-management-object | ||||||
|  |   name: Name of the secret to be created | ||||||
|  |   namespace: Namespace of the secret to be created | ||||||
|  | type: Opaque | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | please note that the rendering engine for this template is jinja2, with an addition of a custom `bitwarden_lookup` function, so there are more possibilities to inject here. | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | --- | ||||||
| apiVersion: apiextensions.k8s.io/v1 | apiVersion: apiextensions.k8s.io/v1 | ||||||
| kind: CustomResourceDefinition | kind: CustomResourceDefinition | ||||||
| metadata: | metadata: | ||||||
| @@ -12,7 +13,7 @@ spec: | |||||||
|     shortNames: |     shortNames: | ||||||
|       - bws |       - bws | ||||||
|   versions: |   versions: | ||||||
|     - name: v1beta3 |     - name: v1beta4 | ||||||
|       served: true |       served: true | ||||||
|       storage: true |       storage: true | ||||||
|       schema: |       schema: | ||||||
| @@ -34,6 +35,8 @@ spec: | |||||||
|                             type: string |                             type: string | ||||||
|                           secretRef: |                           secretRef: | ||||||
|                             type: string |                             type: string | ||||||
|  |                           secretScope: | ||||||
|  |                             type: string | ||||||
|                         required: |                         required: | ||||||
|                           - secretName |                           - secretName | ||||||
|                 id: |                 id: | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								charts/bitwarden-crd-operator/crds/bitwarden-templates.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | --- | ||||||
|  | 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: v1beta4 | ||||||
|  |       served: true | ||||||
|  |       storage: true | ||||||
|  |       schema: | ||||||
|  |         openAPIV3Schema: | ||||||
|  |           type: object | ||||||
|  |           properties: | ||||||
|  |             spec: | ||||||
|  |               type: object | ||||||
|  |               properties: | ||||||
|  |                 filename: | ||||||
|  |                   type: string | ||||||
|  |                 template: | ||||||
|  |                   type: string | ||||||
|  |                 namespace: | ||||||
|  |                   type: string | ||||||
|  |                 name: | ||||||
|  |                   type: string | ||||||
|  |               required: | ||||||
|  |                 - filename | ||||||
|  |                 - template | ||||||
|  |                 - namespace | ||||||
|  |                 - name | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | --- | ||||||
| apiVersion: apiextensions.k8s.io/v1 | apiVersion: apiextensions.k8s.io/v1 | ||||||
| kind: CustomResourceDefinition | kind: CustomResourceDefinition | ||||||
| metadata: | metadata: | ||||||
| @@ -12,7 +13,7 @@ spec: | |||||||
|     shortNames: |     shortNames: | ||||||
|       - rgc |       - rgc | ||||||
|   versions: |   versions: | ||||||
|     - name: v1beta3 |     - name: v1beta4 | ||||||
|       served: true |       served: true | ||||||
|       storage: true |       storage: true | ||||||
|       schema: |       schema: | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ metadata: | |||||||
|   name: {{ include "bitwarden-crd-operator.serviceAccountName" . }}-role |   name: {{ include "bitwarden-crd-operator.serviceAccountName" . }}-role | ||||||
| rules: | rules: | ||||||
| - apiGroups: ["lerentis.uploadfilter24.eu"] | - apiGroups: ["lerentis.uploadfilter24.eu"] | ||||||
|   resources: ["bitwarden-secrets", "registry-credentials"] |   resources: ["bitwarden-secrets", "registry-credentials", "bitwarden-templates"] | ||||||
|   verbs: ["get", "watch", "list", "create", "delete", "patch", "update"] |   verbs: ["get", "watch", "list", "create", "delete", "patch", "update"] | ||||||
| - apiGroups: [""] | - apiGroups: [""] | ||||||
|   resources: ["secrets"] |   resources: ["secrets"] | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta3" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: BitwardenSecret | kind: BitwardenSecret | ||||||
| metadata: | metadata: | ||||||
|   name: test |   name: test | ||||||
| @@ -8,9 +8,11 @@ spec: | |||||||
|     - element: |     - element: | ||||||
|         secretName: username |         secretName: username | ||||||
|         secretRef: nameofUser  |         secretRef: nameofUser  | ||||||
|  |         secretScope: login | ||||||
|     - element: |     - element: | ||||||
|         secretName: password |         secretName: password | ||||||
|         secretRef: passwordOfUser  |         secretRef: passwordOfUser  | ||||||
|  |         secretScope: login | ||||||
|   id: "88781348-c81c-4367-9801-550360c21295" |   id: "88781348-c81c-4367-9801-550360c21295" | ||||||
|   name: "test-secret" |   name: "test-secret" | ||||||
|   namespace: "default" |   namespace: "default" | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta3" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: RegistryCredential | kind: RegistryCredential | ||||||
| metadata: | metadata: | ||||||
|   name: test |   name: test | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								example_template.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								example_template.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: BitwardenTemplate | ||||||
|  | metadata: | ||||||
|  |   name: test | ||||||
|  | spec: | ||||||
|  |   filename: "config.yaml" | ||||||
|  |   name: "test-template" | ||||||
|  |   namespace: "default" | ||||||
|  |   template: | | ||||||
|  |     --- | ||||||
|  |     api: | ||||||
|  |       enabled: True | ||||||
|  |       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", "fields", "public_key") }} | ||||||
|  |           enabled: true | ||||||
| @@ -1,2 +1,3 @@ | |||||||
| kopf | kopf==1.36.0 | ||||||
| kubernetes | kubernetes==25.3.0 | ||||||
|  | Jinja2==3.1.2 | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								src/bitwardenCrdOperator.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										20
									
								
								src/bitwardenCrdOperator.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | import kopf | ||||||
|  | import os | ||||||
|  |  | ||||||
|  | from utils.utils import command_wrapper, unlock_bw | ||||||
|  |  | ||||||
|  | @kopf.on.startup() | ||||||
|  | def bitwarden_signin(logger, **kwargs): | ||||||
|  |     if 'BW_HOST' in os.environ: | ||||||
|  |         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("login --apikey") | ||||||
|  |     unlock_bw(logger) | ||||||
|  |  | ||||||
							
								
								
									
										100
									
								
								src/dockerlogin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/dockerlogin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | import kopf | ||||||
|  | import kubernetes | ||||||
|  | import base64 | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | 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" | ||||||
|  |     secret.data = {} | ||||||
|  |     auths_dict = {} | ||||||
|  |     registry_dict = {} | ||||||
|  |     reg_auth_dict = {} | ||||||
|  |  | ||||||
|  |     _username = secret_json["login"][username_ref] | ||||||
|  |     logger.info(f"Creating login with username: {_username}") | ||||||
|  |     _password = secret_json["login"][password_ref] | ||||||
|  |     cred_field = str(base64.b64encode(f"{_username}:{_password}".encode("utf-8")), "utf-8") | ||||||
|  |  | ||||||
|  |     reg_auth_dict["auth"] = cred_field | ||||||
|  |     registry_dict[registry] = reg_auth_dict | ||||||
|  |     auths_dict["auths"] = registry_dict | ||||||
|  |     secret.data[".dockerconfigjson"] = str(base64.b64encode(json.dumps(auths_dict).encode("utf-8")), "utf-8") | ||||||
|  |     return secret | ||||||
|  |  | ||||||
|  | @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') | ||||||
|  |     registry = spec.get('registry') | ||||||
|  |     id = spec.get('id') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     unlock_bw(logger) | ||||||
|  |     logger.info(f"Locking up secret with ID: {id}") | ||||||
|  |     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||||
|  |  | ||||||
|  |     api = kubernetes.client.CoreV1Api() | ||||||
|  |  | ||||||
|  |     annotations = { | ||||||
|  |         "managed": "registry-credential.lerentis.uploadfilter24.eu", | ||||||
|  |         "managedObject": f"{namespace}/{name}" | ||||||
|  |     } | ||||||
|  |     secret = kubernetes.client.V1Secret() | ||||||
|  |     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||||
|  |     secret = create_dockerlogin(logger, secret, secret_json_object, username_ref, password_ref, registry)    | ||||||
|  |  | ||||||
|  |     obj = api.create_namespaced_secret( | ||||||
|  |         secret_namespace, secret | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     logger.info(f"Registry Secret {secret_namespace}/{secret_name} has been created") | ||||||
|  |  | ||||||
|  | @kopf.on.update('registry-credential.lerentis.uploadfilter24.eu') | ||||||
|  | @kopf.timer('registry-credential.lerentis.uploadfilter24.eu', interval=900) | ||||||
|  | def update_managed_registry_secret(spec, status, name, namespace, logger, body, **kwargs): | ||||||
|  |  | ||||||
|  |     username_ref = spec.get('usernameRef') | ||||||
|  |     password_ref = spec.get('passwordRef') | ||||||
|  |     registry = spec.get('registry') | ||||||
|  |     id = spec.get('id') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     unlock_bw(logger) | ||||||
|  |     logger.info(f"Locking up secret with ID: {id}") | ||||||
|  |     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||||
|  |  | ||||||
|  |     api = kubernetes.client.CoreV1Api() | ||||||
|  |  | ||||||
|  |     annotations = { | ||||||
|  |         "managed": "registry-credential.lerentis.uploadfilter24.eu", | ||||||
|  |         "managedObject": f"{namespace}/{name}" | ||||||
|  |     } | ||||||
|  |     secret = kubernetes.client.V1Secret() | ||||||
|  |     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||||
|  |     secret = create_dockerlogin(logger, secret, secret_json_object, username_ref, password_ref, registry) | ||||||
|  |     try: | ||||||
|  |         obj = api.replace_namespaced_secret( | ||||||
|  |             name=secret_name, | ||||||
|  |             body=secret, | ||||||
|  |             namespace="{}".format(secret_namespace)) | ||||||
|  |         logger.info(f"Secret {secret_namespace}/{secret_name} has been updated") | ||||||
|  |     except: | ||||||
|  |         logger.warn( | ||||||
|  |             f"Could not update secret {secret_namespace}/{secret_name}!") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @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') | ||||||
|  |     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}!") | ||||||
							
								
								
									
										109
									
								
								src/kv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/kv.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | |||||||
|  | import kopf | ||||||
|  | import kubernetes | ||||||
|  | import base64 | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | 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" | ||||||
|  |     secret.data = {} | ||||||
|  |     for eleml in content_def: | ||||||
|  |         for k, elem in eleml.items(): | ||||||
|  |             for key, value in elem.items(): | ||||||
|  |                 if key == "secretName": | ||||||
|  |                     _secret_key = value | ||||||
|  |                 if key == "secretRef": | ||||||
|  |                     _secret_ref = value | ||||||
|  |                 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-secret.lerentis.uploadfilter24.eu') | ||||||
|  | def create_managed_secret(spec, name, namespace, logger, body, **kwargs): | ||||||
|  |  | ||||||
|  |     content_def = body['spec']['content'] | ||||||
|  |     id = spec.get('id') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     unlock_bw(logger) | ||||||
|  |     logger.info(f"Locking up secret with ID: {id}") | ||||||
|  |     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||||
|  |  | ||||||
|  |     api = kubernetes.client.CoreV1Api() | ||||||
|  |  | ||||||
|  |     annotations = { | ||||||
|  |         "managed": "bitwarden-secret.lerentis.uploadfilter24.eu", | ||||||
|  |         "managedObject": f"{namespace}/{name}" | ||||||
|  |     } | ||||||
|  |     secret = kubernetes.client.V1Secret() | ||||||
|  |     secret.metadata = kubernetes.client.V1ObjectMeta( | ||||||
|  |         name=secret_name, annotations=annotations) | ||||||
|  |     secret = create_kv(secret, secret_json_object, content_def) | ||||||
|  |  | ||||||
|  |     obj = api.create_namespaced_secret( | ||||||
|  |         namespace="{}".format(secret_namespace), | ||||||
|  |         body=secret | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     logger.info(f"Secret {secret_namespace}/{secret_name} has been created") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu') | ||||||
|  | @kopf.timer('bitwarden-secret.lerentis.uploadfilter24.eu', interval=900) | ||||||
|  | def update_managed_secret(spec, status, name, namespace, logger, body, **kwargs): | ||||||
|  |  | ||||||
|  |     content_def = body['spec']['content'] | ||||||
|  |     id = spec.get('id') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     unlock_bw(logger) | ||||||
|  |     logger.info(f"Locking up secret with ID: {id}") | ||||||
|  |     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||||
|  |  | ||||||
|  |     api = kubernetes.client.CoreV1Api() | ||||||
|  |  | ||||||
|  |     annotations = { | ||||||
|  |         "managed": "bitwarden-secret.lerentis.uploadfilter24.eu", | ||||||
|  |         "managedObject": f"{namespace}/{name}" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     secret = kubernetes.client.V1Secret() | ||||||
|  |     secret.metadata = kubernetes.client.V1ObjectMeta( | ||||||
|  |         name=secret_name, annotations=annotations) | ||||||
|  |     secret = create_kv(secret, secret_json_object, content_def) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         obj = api.replace_namespaced_secret( | ||||||
|  |             name=secret_name, | ||||||
|  |             body=secret, | ||||||
|  |             namespace="{}".format(secret_namespace)) | ||||||
|  |         logger.info(f"Secret {secret_namespace}/{secret_name} has been updated") | ||||||
|  |     except: | ||||||
|  |         logger.warn( | ||||||
|  |             f"Could not update secret {secret_namespace}/{secret_name}!") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @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') | ||||||
|  |     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}!") | ||||||
							
								
								
									
										0
									
								
								src/lookups/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/lookups/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										10
									
								
								src/lookups/bitwarden_lookup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/lookups/bitwarden_lookup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | 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) | ||||||
							
								
								
									
										92
									
								
								src/template.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/template.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | import kopf | ||||||
|  | import base64 | ||||||
|  | import kubernetes | ||||||
|  |  | ||||||
|  | from utils.utils import unlock_bw | ||||||
|  | from lookups.bitwarden_lookup import bitwarden_lookup | ||||||
|  | from jinja2 import Environment, BaseLoader | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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-template.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-template.lerentis.uploadfilter24.eu", | ||||||
|  |         "managedObject": f"{namespace}/{name}" | ||||||
|  |     } | ||||||
|  |     secret = kubernetes.client.V1Secret() | ||||||
|  |     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||||
|  |     secret = create_template_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-template.lerentis.uploadfilter24.eu') | ||||||
|  | @kopf.timer('bitwarden-template.lerentis.uploadfilter24.eu', interval=900) | ||||||
|  | def update_managed_secret(spec, status, 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-template.lerentis.uploadfilter24.eu", | ||||||
|  |         "managedObject": f"{namespace}/{name}" | ||||||
|  |     } | ||||||
|  |     secret = kubernetes.client.V1Secret() | ||||||
|  |     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||||
|  |     secret = create_template_secret(secret, filename, template) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         obj = api.replace_namespaced_secret( | ||||||
|  |             name=secret_name, | ||||||
|  |             body=secret, | ||||||
|  |             namespace="{}".format(secret_namespace)) | ||||||
|  |         logger.info(f"Secret {secret_namespace}/{secret_name} has been updated") | ||||||
|  |     except: | ||||||
|  |         logger.warn( | ||||||
|  |             f"Could not update secret {secret_namespace}/{secret_name}!") | ||||||
|  |  | ||||||
|  | @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') | ||||||
|  |     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}!") | ||||||
							
								
								
									
										0
									
								
								src/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										30
									
								
								src/utils/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/utils/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import os | ||||||
|  | import subprocess | ||||||
|  |  | ||||||
|  | class BitwardenCommandException(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  | def get_secret_from_bitwarden(id): | ||||||
|  |     return command_wrapper(command=f"get item {id}") | ||||||
|  |  | ||||||
|  | def unlock_bw(logger): | ||||||
|  |     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(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: | ||||||
|  |         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'] | ||||||
		Reference in New Issue
	
	Block a user