Compare commits
	
		
			92 Commits
		
	
	
		
			v0.1.0
			...
			82b684e460
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 82b684e460 | ||
|  | 8ec698f50e | ||
|  | 9b8fe1d8ef | ||
|  | 516f2a34cf | ||
| 361d0866e9 | |||
| 9d4ade904e | |||
| 8c3714f7e0 | |||
| 36ae5cc602 | |||
| d908419b78 | |||
| 2d399ff8ce | |||
| c753737497 | |||
| 886fe3783d | |||
|  | 18a47f8ad2 | ||
|  | e405734e72 | ||
| 8bf4292991 | |||
|  | b149b26485 | ||
|  | 5263a811e1 | ||
|  | 4b59ff1aac | ||
|  | ad1cc9f646 | ||
| 0f518ab28d | |||
| 1bf2a24cf2 | |||
| a73e8ff982 | |||
|  | 54a4ffa212 | ||
| 16040bf87a | |||
|  | 9c1c7417e1 | ||
|  | 0f9ca0869c | ||
|  | 6fbf060044 | ||
|  | 3bb40cdcb4 | ||
|  | 219c9d0413 | ||
|  | 4f92bfe86a | ||
|  | 640333cfc7 | ||
|  | 6a907f149f | ||
|  | 3db74524ca | ||
|  | e49df1fb4d | ||
|  | bb3ca7573b | ||
| 097712c6c6 | |||
|  | 3845fd8045 | ||
|  | 3caacac98a | ||
|  | beeca5a6b6 | ||
|  | 2d4c8ec14b | ||
| 10cc864275 | |||
| 689a6e5bae | |||
|  | 4e23b67f5d | ||
|  | f4d05fdd0f | ||
|  | 48bc422974 | ||
|  | 41d4959422 | ||
|  | c2116c24ec | ||
|  | 67692b372f | ||
| 8a6219718a | |||
|  | a10f6b3c9a | ||
|  | 56657df85a | ||
|  | 6a324e66da | ||
|  | 6081374696 | ||
|  | a3cec12284 | ||
| 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 | ||
| a2f248c6ff | |||
| 15a7258d99 | |||
| 06efcf1a55 | |||
|  | 2c9d1794a5 | ||
| 0fd7abdcc0 | |||
| 11fb42769c | |||
| aec384c78e | |||
| 7d4f01caf6 | |||
| fc4e561f29 | |||
| 3a69b38ad6 | 
							
								
								
									
										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" | ||||||
							
								
								
									
										68
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										68
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,17 +1,20 @@ | |||||||
| name: Release Charts | name: Release Charts | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   release: |   push: | ||||||
|     types: [published] |     branches: | ||||||
|  |       - main | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   release: |   release: | ||||||
|     permissions: |     permissions: | ||||||
|  |       id-token: write | ||||||
|       contents: write |       contents: write | ||||||
|  |       packages: write | ||||||
|     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 | ||||||
|  |  | ||||||
| @@ -26,6 +29,63 @@ jobs: | |||||||
|           version: v3.10.0 |           version: v3.10.0 | ||||||
|  |  | ||||||
|       - name: Run chart-releaser |       - name: Run chart-releaser | ||||||
|         uses: helm/chart-releaser-action@v1.4.1 |         uses: helm/chart-releaser-action@v1.5.0 | ||||||
|  |         with: | ||||||
|  |           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.33.3 | ||||||
|  |         id: app_version | ||||||
|  |         with: | ||||||
|  |           cmd: yq '.appVersion' charts/bitwarden-crd-operator/Chart.yaml | ||||||
|  |  | ||||||
|  |       - name: "GHCR Login" | ||||||
|  |         uses: docker/login-action@v2 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: lerentis | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v2 | ||||||
|  |        | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v2 | ||||||
|  |  | ||||||
|  |       - name: "GHCR Build and Push" | ||||||
|  |         id: docker_build | ||||||
|  |         uses: docker/build-push-action@v4 | ||||||
|  |         with: | ||||||
|  |           push: true | ||||||
|  |           platforms: linux/amd64,linux/arm64 | ||||||
|  |           tags: ghcr.io/lerentis/bitwarden-crd-operator:${{ steps.app_version.outputs.result }} | ||||||
|  |  | ||||||
|  |       - name: Create SBOM | ||||||
|  |         uses: anchore/sbom-action@v0 | ||||||
|  |         with: | ||||||
|  |           image: ghcr.io/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: true | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								.github/workflows/test-and-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/test-and-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | name: Lint and Test | ||||||
|  |  | ||||||
|  | on: pull_request | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   lint-test: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           fetch-depth: 0 | ||||||
|  |  | ||||||
|  |       - name: Set up Helm | ||||||
|  |         uses: azure/setup-helm@v3 | ||||||
|  |         with: | ||||||
|  |           version: v3.11.2 | ||||||
|  |  | ||||||
|  |       - uses: actions/setup-python@v4 | ||||||
|  |         with: | ||||||
|  |           python-version: '3.9' | ||||||
|  |           check-latest: true | ||||||
|  |  | ||||||
|  |       - name: Set up chart-testing | ||||||
|  |         uses: helm/chart-testing-action@v2.4.0 | ||||||
|  |  | ||||||
|  |       - name: Run chart-testing (list-changed) | ||||||
|  |         id: list-changed | ||||||
|  |         run: | | ||||||
|  |           changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) | ||||||
|  |           if [[ -n "$changed" ]]; then | ||||||
|  |             echo "changed=true" >> "$GITHUB_OUTPUT" | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |       - name: Run chart-testing (lint) | ||||||
|  |         if: steps.list-changed.outputs.changed == 'true' | ||||||
|  |         run: ct lint --target-branch ${{ github.event.repository.default_branch }} | ||||||
|  |  | ||||||
|  |   pr-build: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v2 | ||||||
|  |        | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v2 | ||||||
|  |  | ||||||
|  |       - name: "GHCR Build" | ||||||
|  |         id: docker_build | ||||||
|  |         uses: docker/build-push-action@v4 | ||||||
|  |         with: | ||||||
|  |           push: false | ||||||
|  |           platforms: linux/amd64,linux/arm64 | ||||||
|  |           tags: ghcr.io/lerentis/bitwarden-crd-operator:dev | ||||||
|  |  | ||||||
							
								
								
									
										59
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,33 +1,48 @@ | |||||||
| FROM alpine:latest as builder | FROM alpine:3.17.3 | ||||||
|  |  | ||||||
| ARG BW_VERSION=2022.8.0 | LABEL org.opencontainers.image.source=https://github.com/Lerentis/bitwarden-crd-operator | ||||||
|  | LABEL org.opencontainers.image.description="Kubernetes Operator to create k8s secrets from bitwarden" | ||||||
|  | LABEL org.opencontainers.image.licenses=MIT | ||||||
|  |  | ||||||
| RUN apk add wget unzip | ARG PYTHON_VERSION=3.10.11-r0 | ||||||
|  | ARG PIP_VERSION=22.3.1-r1 | ||||||
|  | ARG GCOMPAT_VERSION=1.1.0-r0 | ||||||
|  | ARG LIBCRYPTO_VERSION=3.0.8-r4 | ||||||
|  | ARG BW_VERSION=2023.1.0 | ||||||
|  |  | ||||||
| RUN cd /tmp && wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip && \ | COPY requirements.txt /requirements.txt | ||||||
|     unzip /tmp/bw-linux-${BW_VERSION}.zip |  | ||||||
|  |  | ||||||
| FROM ubuntu:jammy |  | ||||||
|  |  | ||||||
| COPY --from=builder /tmp/bw /usr/local/bin/bw |  | ||||||
| COPY requirements.txt requirements.txt |  | ||||||
|  |  | ||||||
| RUN set -eux; \ | RUN set -eux; \ | ||||||
|     groupadd -r bw-operator ; \ |     apk add --virtual build-dependencies wget unzip; \ | ||||||
|     useradd -r -g bw-operator -s /sbin/nologin bw-operator; \ |     ARCH="$(apk --print-arch)"; \ | ||||||
|  |     case "${ARCH}" in \ | ||||||
|  |        aarch64|arm64) \ | ||||||
|  |           apk add npm; \ | ||||||
|  |           npm install -g @bitwarden/cli@${BW_VERSION}; \ | ||||||
|  |          ;; \ | ||||||
|  |        amd64|x86_64) \ | ||||||
|  |           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; \ | ||||||
|  |          ;; \ | ||||||
|  |        *) \ | ||||||
|  |          echo "Unsupported arch: ${ARCH}"; \ | ||||||
|  |          exit 1; \ | ||||||
|  |          ;; \ | ||||||
|  |     esac; \ | ||||||
|  |     apk del --purge build-dependencies; \ | ||||||
|  |     addgroup -S -g 1000 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; \ |     apk add gcc musl-dev libstdc++ gcompat=${GCOMPAT_VERSION} python3=${PYTHON_VERSION} py3-pip=${PIP_VERSION} libcrypto3=${LIBCRYPTO_VERSION} libssl3=${LIBCRYPTO_VERSION}; \ | ||||||
|     apt-get update; \ |     pip install -r /requirements.txt --no-warn-script-location; \ | ||||||
|     apt-get install -y --no-install-recommends python3 python3-pip; \ |     rm /requirements.txt; \ | ||||||
|     apt-get clean; |     apk del --purge gcc musl-dev libstdc++; | ||||||
|  |  | ||||||
| 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" ] |  | ||||||
|   | |||||||
							
								
								
									
										118
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| # Bitwarden CRD Operator | # Bitwarden CRD Operator | ||||||
|  |  | ||||||
| [](https://drone.uploadfilter24.eu/lerentis/bitwarden-crd-operator) | [](https://drone.uploadfilter24.eu/lerentis/bitwarden-crd-operator) [](https://artifacthub.io/packages/search?repo=lerentis) | ||||||
|  |  | ||||||
| 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 |  | ||||||
|  |  | ||||||
| For now a few secrets need to be passed to helm. I will change this in the future to give the option to also use a kubernetes secret for this. | ## 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. | ||||||
| Expose these to the operator as described in this example: | Expose these to the operator as described in this example: | ||||||
| @@ -26,20 +29,34 @@ env: | |||||||
|     value: "YourSuperSecurePassword" |     value: "YourSuperSecurePassword" | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | you can also create a secret manually with these information and reference the existing secret like this in the `values.yaml`: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | externalConfigSecret: | ||||||
|  |   enabled: true | ||||||
|  |   name: "my-existing-secret" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | the helm template will use all environment variables from this secret, so make sure to prepare this secret with the key value pairs as described above. | ||||||
|  |  | ||||||
| `BW_HOST` can be omitted if you are using the Bitwarden SaaS offering. | `BW_HOST` can be omitted if you are using the Bitwarden SaaS offering. | ||||||
|  |  | ||||||
| After that it is a basic helm deployment: | After that it is a basic helm deployment: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
|  | helm repo add bitwarden-operator https://lerentis.github.io/bitwarden-crd-operator | ||||||
|  | helm repo update  | ||||||
| kubectl create namespace bw-operator | kubectl create namespace bw-operator | ||||||
| helm upgrade --install --namespace bw-operator -f chart/bitwarden-crd-operator/values.yaml bw-operator chart/bitwarden-crd-operator | helm upgrade --install --namespace bw-operator -f values.yaml bw-operator bitwarden-operator/bitwarden-crd-operator | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ## BitwardenSecret | ||||||
|  |  | ||||||
| And you are set to create your first secret using this operator. For that you need to add a CRD Object like this to your cluster: | And you are set to create your first secret using this operator. For that you need to add a CRD Object like this to your cluster: | ||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta2" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: BitwardenSecret | kind: BitwardenSecret | ||||||
| metadata: | metadata: | ||||||
|   name: name-of-your-management-object |   name: name-of-your-management-object | ||||||
| @@ -48,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" | ||||||
| @@ -73,10 +92,89 @@ metadata: | |||||||
| type: Opaque | type: Opaque | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ## RegistryCredential | ||||||
|  |  | ||||||
|  | For managing registry credentials, or pull secrets, you can create another kind of object to let the operator create these as well for you: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: RegistryCredential | ||||||
|  | metadata: | ||||||
|  |   name: name-of-your-management-object | ||||||
|  | spec: | ||||||
|  |   usernameRef: nameOfTheFieldInBitwarden # for example username | ||||||
|  |   passwordRef: nameOfTheFieldInBitwarden # for example password | ||||||
|  |   registry: "docker.io" | ||||||
|  |   id: "A Secret ID from bitwarden" | ||||||
|  |   name: "Name of the secret to be created" | ||||||
|  |   namespace: "Namespace of the secret to be created" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The resulting secret looks something like this: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | data: | ||||||
|  |   .dockerconfigjson: "base64 encoded json auth string for your registry" | ||||||
|  | kind: Secret | ||||||
|  | metadata: | ||||||
|  |   annotations: | ||||||
|  |     managed: bitwarden-secrets.lerentis.uploadfilter24.eu | ||||||
|  |     managedObject: bw-operator/test | ||||||
|  |   name: name-of-your-management-object | ||||||
|  |   namespace: default | ||||||
|  | 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 | ||||||
| [] offer option to use a existing secret in helm chart   | - [x] offer option to use a existing secret in helm chart | ||||||
| [x] host chart on gh pages   | - [x] host chart on gh pages | ||||||
| [x] write release pipeline   | - [x] write release pipeline | ||||||
| [x] maybe extend spec to offer modification of keys as well | - [x] maybe extend spec to offer modification of keys as well | ||||||
|   | |||||||
| @@ -1,90 +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): |  | ||||||
|     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') |  | ||||||
|  |  | ||||||
| @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('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.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_object["login"][_secret_key].encode("utf-8")), "utf-8") |  | ||||||
|  |  | ||||||
|     obj = api.create_namespaced_secret( |  | ||||||
|         secret_namespace, secret |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     logger.info(f"Secret {secret_namespace}/{secret_name} is 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}!") |  | ||||||
| @@ -4,6 +4,99 @@ description: Deploy the Bitwarden CRD Operator | |||||||
|  |  | ||||||
| type: application | type: application | ||||||
|  |  | ||||||
| version: 0.1.0 | version: "v0.7.2" | ||||||
|  |  | ||||||
| appVersion: "0.1.0" | appVersion: "0.6.2" | ||||||
|  |  | ||||||
|  | keywords: | ||||||
|  |   - operator | ||||||
|  |   - bitwarden | ||||||
|  |   - vaultwarden | ||||||
|  |  | ||||||
|  | icon: https://lerentis.github.io/bitwarden-crd-operator/logo.png | ||||||
|  |  | ||||||
|  | home: https://lerentis.github.io/bitwarden-crd-operator/ | ||||||
|  |  | ||||||
|  | sources: | ||||||
|  |   - https://github.com/Lerentis/bitwarden-crd-operator | ||||||
|  |  | ||||||
|  | kubeVersion: ">= 1.23.0-0" | ||||||
|  |  | ||||||
|  | maintainers: | ||||||
|  |   - name: lerentis | ||||||
|  |     email: lerentis+helm@uploadfilter24.eu | ||||||
|  |  | ||||||
|  | annotations: | ||||||
|  |   artifacthub.io/links: | | ||||||
|  |     - name: Chart Source | ||||||
|  |       url: https://github.com/Lerentis/bitwarden-crd-operator | ||||||
|  |   artifacthub.io/crds: | | ||||||
|  |     - kind: BitwardenSecret | ||||||
|  |       version: v1beta4 | ||||||
|  |       name: bitwarden-secret | ||||||
|  |       displayName: Bitwarden Secret | ||||||
|  |       description: Management Object to create secrets from bitwarden | ||||||
|  |     - kind: RegistryCredential | ||||||
|  |       version: v1beta4 | ||||||
|  |       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/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/operator: "true" | ||||||
|  |   artifacthub.io/containsSecurityUpdates: "false" | ||||||
|  |   artifacthub.io/changes: | | ||||||
|  |     - kind: fixed | ||||||
|  |       description: "Fallback to npm package on ARM as native bitwarden cli package is not compatible with ARM" | ||||||
|  |   artifacthub.io/images: | | ||||||
|  |     - name: bitwarden-crd-operator | ||||||
|  |       image: ghcr.io/lerentis/bitwarden-crd-operator:0.6.2 | ||||||
|   | |||||||
							
								
								
									
										172
									
								
								charts/bitwarden-crd-operator/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								charts/bitwarden-crd-operator/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | # Bitwarden CRD Operator | ||||||
|  |  | ||||||
|  | [](https://drone.uploadfilter24.eu/lerentis/bitwarden-crd-operator) [](https://artifacthub.io/packages/search?repo=lerentis) | ||||||
|  |  | ||||||
|  | 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:   | ||||||
|  | > 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. | ||||||
|  | Expose these to the operator as described in this example: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | env: | ||||||
|  |   - name: BW_HOST | ||||||
|  |     value: "https://bitwarden.your.tld.org" | ||||||
|  |   - name: BW_CLIENTID | ||||||
|  |     value: "user.your-client-id" | ||||||
|  |   - name: BW_CLIENTSECRET | ||||||
|  |     value: "YoUrCliEntSecRet" | ||||||
|  |   - name: BW_PASSWORD | ||||||
|  |     value: "YourSuperSecurePassword" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | you can also create a secret manually with these information and reference the existing secret like this in the `values.yaml`: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | externalConfigSecret: | ||||||
|  |   enabled: true | ||||||
|  |   name: "my-existing-secret" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | the helm template will use all environment variables from this secret, so make sure to prepare this secret with the key value pairs as described above. | ||||||
|  |  | ||||||
|  | `BW_HOST` can be omitted if you are using the Bitwarden SaaS offering. | ||||||
|  |  | ||||||
|  | After that it is a basic helm deployment: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | helm repo add bitwarden-operator https://lerentis.github.io/bitwarden-crd-operator | ||||||
|  | helm repo update  | ||||||
|  | kubectl create namespace bw-operator | ||||||
|  | helm upgrade --install --namespace bw-operator -f values.yaml bw-operator bitwarden-operator/bitwarden-crd-operator | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## BitwardenSecret | ||||||
|  |  | ||||||
|  | And you are set to create your first secret using this operator. For that you need to add a CRD Object like this to your cluster: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: BitwardenSecret | ||||||
|  | metadata: | ||||||
|  |   name: name-of-your-management-object | ||||||
|  | spec: | ||||||
|  |   content: | ||||||
|  |     - element: | ||||||
|  |         secretName: nameOfTheFieldInBitwarden # for example username | ||||||
|  |         secretRef: nameOfTheKeyInTheSecretToBeCreated  | ||||||
|  |         secretScope: login # for custom entries on bitwarden use 'fields'  | ||||||
|  |     - element: | ||||||
|  |         secretName: nameOfAnotherFieldInBitwarden # for example password | ||||||
|  |         secretRef: nameOfAnotherKeyInTheSecretToBeCreated  | ||||||
|  |         secretScope: login # for custom entries on bitwarden use 'fields'  | ||||||
|  |   id: "A Secret ID from bitwarden" | ||||||
|  |   name: "Name of the secret to be created" | ||||||
|  |   namespace: "Namespace of the secret to be created" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The ID can be extracted from the browser when you open a item the ID is in the URL. The resulting secret looks something like this: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | data: | ||||||
|  |   nameOfTheKeyInTheSecretToBeCreated: "base64 encoded value of TheFieldInBitwarden" | ||||||
|  |   nameOfAnotherKeyInTheSecretToBeCreated: "base64 encoded value of AnotherFieldInBitwarden" | ||||||
|  | kind: Secret | ||||||
|  | metadata: | ||||||
|  |   annotations: | ||||||
|  |     managed: bitwarden-secrets.lerentis.uploadfilter24.eu | ||||||
|  |     managedObject: bw-operator/test | ||||||
|  |   name: name-of-your-management-object | ||||||
|  |   namespace: default | ||||||
|  | type: Opaque | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## RegistryCredential | ||||||
|  |  | ||||||
|  | For managing registry credentials, or pull secrets, you can create another kind of object to let the operator create these as well for you: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: RegistryCredential | ||||||
|  | metadata: | ||||||
|  |   name: name-of-your-management-object | ||||||
|  | spec: | ||||||
|  |   usernameRef: nameOfTheFieldInBitwarden # for example username | ||||||
|  |   passwordRef: nameOfTheFieldInBitwarden # for example password | ||||||
|  |   registry: "docker.io" | ||||||
|  |   id: "A Secret ID from bitwarden" | ||||||
|  |   name: "Name of the secret to be created" | ||||||
|  |   namespace: "Namespace of the secret to be created" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The resulting secret looks something like this: | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | data: | ||||||
|  |   .dockerconfigjson: "base64 encoded json auth string for your registry" | ||||||
|  | kind: Secret | ||||||
|  | metadata: | ||||||
|  |   annotations: | ||||||
|  |     managed: bitwarden-secrets.lerentis.uploadfilter24.eu | ||||||
|  |     managedObject: bw-operator/test | ||||||
|  |   name: name-of-your-management-object | ||||||
|  |   namespace: default | ||||||
|  | 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. | ||||||
| @@ -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: v1beta2 |     - 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 | ||||||
							
								
								
									
										44
									
								
								charts/bitwarden-crd-operator/crds/registry-credentials.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								charts/bitwarden-crd-operator/crds/registry-credentials.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | --- | ||||||
|  | apiVersion: apiextensions.k8s.io/v1 | ||||||
|  | kind: CustomResourceDefinition | ||||||
|  | metadata: | ||||||
|  |   name: registry-credentials.lerentis.uploadfilter24.eu | ||||||
|  | spec: | ||||||
|  |   scope: Namespaced | ||||||
|  |   group: lerentis.uploadfilter24.eu | ||||||
|  |   names: | ||||||
|  |     kind: RegistryCredential | ||||||
|  |     plural: registry-credentials | ||||||
|  |     singular: registry-credential | ||||||
|  |     shortNames: | ||||||
|  |       - rgc | ||||||
|  |   versions: | ||||||
|  |     - name: v1beta4 | ||||||
|  |       served: true | ||||||
|  |       storage: true | ||||||
|  |       schema: | ||||||
|  |         openAPIV3Schema: | ||||||
|  |           type: object | ||||||
|  |           properties: | ||||||
|  |             spec: | ||||||
|  |               type: object | ||||||
|  |               properties: | ||||||
|  |                 usernameRef: | ||||||
|  |                   type: string | ||||||
|  |                 passwordRef: | ||||||
|  |                   type: string | ||||||
|  |                 registry: | ||||||
|  |                   type: string | ||||||
|  |                 id: | ||||||
|  |                   type: string | ||||||
|  |                 namespace: | ||||||
|  |                   type: string | ||||||
|  |                 name: | ||||||
|  |                   type: string | ||||||
|  |               required: | ||||||
|  |                 - id | ||||||
|  |                 - namespace | ||||||
|  |                 - name | ||||||
|  |                 - usernameRef | ||||||
|  |                 - passwordRef | ||||||
|  |                 - registry | ||||||
| @@ -1 +1,2 @@ | |||||||
| Have fun :)  | Bitwarden CRD Operator has been deployed.  | ||||||
|  | Go ahead and create some secrets directly from bitwarden | ||||||
| @@ -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"] |   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"] | ||||||
|   | |||||||
| @@ -33,10 +33,15 @@ spec: | |||||||
|             {{- toYaml .Values.securityContext | nindent 12 }} |             {{- toYaml .Values.securityContext | nindent 12 }} | ||||||
|           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" |           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" | ||||||
|           imagePullPolicy: {{ .Values.image.pullPolicy }} |           imagePullPolicy: {{ .Values.image.pullPolicy }} | ||||||
|           {{- with .Values.env }} |  | ||||||
|           env: |           env: | ||||||
|  |           {{- with .Values.env }} | ||||||
|             {{- . | toYaml | trim | nindent 12 }} |             {{- . | toYaml | trim | nindent 12 }} | ||||||
|           {{- end }} |           {{- end }} | ||||||
|  |           {{- if .Values.externalConfigSecret.enabled }} | ||||||
|  |           envFrom: | ||||||
|  |             - secretRef: | ||||||
|  |                 name: {{ .Values.externalConfigSecret.name }} | ||||||
|  |           {{- end }} | ||||||
|           ports: |           ports: | ||||||
|             - name: http |             - name: http | ||||||
|               containerPort: 8080 |               containerPort: 8080 | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| replicaCount: 1 | replicaCount: 1 | ||||||
|  |  | ||||||
| image: | image: | ||||||
|   repository: lerentis/bitwarden-crd-operator |   repository: ghcr.io/lerentis/bitwarden-crd-operator | ||||||
|   pullPolicy: IfNotPresent |   pullPolicy: IfNotPresent | ||||||
|   # Overrides the image tag whose default is the chart appVersion. |   # Overrides the image tag whose default is the chart appVersion. | ||||||
|   # tag: "0.1.0" |   # tag: "0.1.0" | ||||||
| @@ -14,15 +14,20 @@ imagePullSecrets: [] | |||||||
| nameOverride: "" | nameOverride: "" | ||||||
| fullnameOverride: "" | fullnameOverride: "" | ||||||
|  |  | ||||||
| #env: | # env: | ||||||
| #  - name: BW_HOST | #   - name: BW_HOST | ||||||
| #    value: "define_it" | #     value: "define_it" | ||||||
| #  - name: BW_CLIENTID | #   - name: BW_CLIENTID | ||||||
| #    value: "define_it" | #     value: "define_it" | ||||||
| #  - name: BW_CLIENTSECRET | #   - name: BW_CLIENTSECRET | ||||||
| #    value: "define_it" | #     value: "define_it" | ||||||
| #  - name: BW_PASSWORD | #   - name: BW_PASSWORD | ||||||
| #    value: "define_id" | #     value: "define_id" | ||||||
|  |  | ||||||
|  | externalConfigSecret: | ||||||
|  |   enabled: false | ||||||
|  |  | ||||||
|  |   name: "" | ||||||
|  |  | ||||||
| serviceAccount: | serviceAccount: | ||||||
|   # Specifies whether a service account should be created |   # Specifies whether a service account should be created | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								example.yaml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								example.yaml
									
									
									
									
									
								
							| @@ -1,5 +1,5 @@ | |||||||
| --- | --- | ||||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta2" | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
| kind: BitwardenSecret | kind: BitwardenSecret | ||||||
| metadata: | metadata: | ||||||
|   name: test |   name: test | ||||||
| @@ -8,9 +8,25 @@ 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" | ||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: BitwardenSecret | ||||||
|  | metadata: | ||||||
|  |   name: test-scope | ||||||
|  | spec: | ||||||
|  |   content: | ||||||
|  |     - element: | ||||||
|  |         secretName: public_key | ||||||
|  |         secretRef: pubKey  | ||||||
|  |         secretScope: fields | ||||||
|  |   id: "466fc4b0-ffca-4444-8d88-b59d4de3d928" | ||||||
|  |   name: "test-scope" | ||||||
|  |   namespace: "default" | ||||||
							
								
								
									
										12
									
								
								example_dockerlogin.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								example_dockerlogin.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | --- | ||||||
|  | apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||||
|  | kind: RegistryCredential | ||||||
|  | metadata: | ||||||
|  |   name: test | ||||||
|  | spec: | ||||||
|  |   usernameRef: "username" | ||||||
|  |   passwordRef: "password" | ||||||
|  |   registry: "docker.io" | ||||||
|  |   id: "3b249ec7-9ce7-440a-9558-f34f3ab10680" | ||||||
|  |   name: "test-regcred" | ||||||
|  |   namespace: "default" | ||||||
							
								
								
									
										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,3 +1,3 @@ | |||||||
| kopf | kopf==1.36.1 | ||||||
| kubernetes | kubernetes==26.1.0 | ||||||
| jinja2 | 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 os | ||||||
|  | import kopf | ||||||
|  |  | ||||||
|  | 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(logger, f"config server {os.getenv('BW_HOST')}") | ||||||
|  |         except BaseException: | ||||||
|  |             logger.warn("Received non-zero exit code from server config") | ||||||
|  |             logger.warn("This is expected from startup") | ||||||
|  |             pass | ||||||
|  |     else: | ||||||
|  |         logger.info("BW_HOST not set. Assuming SaaS installation") | ||||||
|  |     command_wrapper(logger, "login --apikey") | ||||||
|  |     unlock_bw(logger) | ||||||
							
								
								
									
										163
									
								
								src/dockerlogin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/dockerlogin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | |||||||
|  | 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 = get_secret_from_bitwarden(logger, 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["data"], | ||||||
|  |         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') | ||||||
|  |  | ||||||
|  |     old_config = None | ||||||
|  |     old_secret_name = None | ||||||
|  |     old_secret_namespace = None | ||||||
|  |     if 'kopf.zalando.org/last-handled-configuration' in body.metadata.annotations: | ||||||
|  |         old_config = json.loads( | ||||||
|  |             body.metadata.annotations['kopf.zalando.org/last-handled-configuration']) | ||||||
|  |         old_secret_name = old_config['spec'].get('name') | ||||||
|  |         old_secret_namespace = old_config['spec'].get('namespace') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     if old_config is not None and ( | ||||||
|  |             old_secret_name != secret_name or old_secret_namespace != secret_namespace): | ||||||
|  |         # If the name of the secret or the namespace of the secret is different | ||||||
|  |         # We have to delete the secret an recreate it | ||||||
|  |         logger.info("Secret name or namespace changed, let's recreate it") | ||||||
|  |         delete_managed_secret( | ||||||
|  |             old_config['spec'], | ||||||
|  |             name, | ||||||
|  |             namespace, | ||||||
|  |             logger, | ||||||
|  |             **kwargs) | ||||||
|  |         create_managed_registry_secret(spec, name, namespace, logger, **kwargs) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     unlock_bw(logger) | ||||||
|  |     logger.info(f"Locking up secret with ID: {id}") | ||||||
|  |     secret_json_object = get_secret_from_bitwarden(logger, 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["data"], | ||||||
|  |         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 BaseException: | ||||||
|  |         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 BaseException: | ||||||
|  |         logger.warn( | ||||||
|  |             f"Could not delete secret {secret_namespace}/{secret_name}!") | ||||||
							
								
								
									
										147
									
								
								src/kv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/kv.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | 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": | ||||||
|  |                 value = parse_login_scope(secret_json, _secret_key) | ||||||
|  |                 if value is None: | ||||||
|  |                     raise Exception( | ||||||
|  |                         f"Field {_secret_key} has no value in bitwarden secret") | ||||||
|  |                 secret.data[_secret_ref] = str(base64.b64encode( | ||||||
|  |                     value.encode("utf-8")), "utf-8") | ||||||
|  |             if _secret_scope == "fields": | ||||||
|  |                 value = parse_fields_scope(secret_json, _secret_key) | ||||||
|  |                 if value is None: | ||||||
|  |                     raise Exception( | ||||||
|  |                         f"Field {_secret_key} has no value in bitwarden secret") | ||||||
|  |                 secret.data[_secret_ref] = str(base64.b64encode( | ||||||
|  |                     value.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 = get_secret_from_bitwarden(logger, 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') | ||||||
|  |     old_config = None | ||||||
|  |     old_secret_name = None | ||||||
|  |     old_secret_namespace = None | ||||||
|  |     if 'kopf.zalando.org/last-handled-configuration' in body.metadata.annotations: | ||||||
|  |         old_config = json.loads( | ||||||
|  |             body.metadata.annotations['kopf.zalando.org/last-handled-configuration']) | ||||||
|  |         old_secret_name = old_config['spec'].get('name') | ||||||
|  |         old_secret_namespace = old_config['spec'].get('namespace') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     if old_config is not None and ( | ||||||
|  |             old_secret_name != secret_name or old_secret_namespace != secret_namespace): | ||||||
|  |         # If the name of the secret or the namespace of the secret is different | ||||||
|  |         # We have to delete the secret an recreate it | ||||||
|  |         logger.info("Secret name or namespace changed, let's recreate it") | ||||||
|  |         delete_managed_secret( | ||||||
|  |             old_config['spec'], | ||||||
|  |             name, | ||||||
|  |             namespace, | ||||||
|  |             logger, | ||||||
|  |             **kwargs) | ||||||
|  |         create_managed_secret(spec, name, namespace, logger, body, **kwargs) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     unlock_bw(logger) | ||||||
|  |     logger.info(f"Locking up secret with ID: {id}") | ||||||
|  |     secret_json_object = get_secret_from_bitwarden(logger, 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 BaseException: | ||||||
|  |         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 BaseException: | ||||||
|  |         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
									
								
							
							
								
								
									
										11
									
								
								src/lookups/bitwarden_lookup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/lookups/bitwarden_lookup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import json | ||||||
|  |  | ||||||
|  | from utils.utils import get_secret_from_bitwarden, parse_fields_scope, parse_login_scope | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def bitwarden_lookup(id, scope, field): | ||||||
|  |     _secret_json = get_secret_from_bitwarden(None, id) | ||||||
|  |     if scope == "login": | ||||||
|  |         return parse_login_scope(_secret_json, field) | ||||||
|  |     if scope == "fields": | ||||||
|  |         return parse_fields_scope(_secret_json, field) | ||||||
							
								
								
									
										138
									
								
								src/template.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/template.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | import kopf | ||||||
|  | import base64 | ||||||
|  | import kubernetes | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | 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') | ||||||
|  |  | ||||||
|  |     old_config = None | ||||||
|  |     old_secret_name = None | ||||||
|  |     old_secret_namespace = None | ||||||
|  |     if 'kopf.zalando.org/last-handled-configuration' in body.metadata.annotations: | ||||||
|  |         old_config = json.loads( | ||||||
|  |             body.metadata.annotations['kopf.zalando.org/last-handled-configuration']) | ||||||
|  |         old_secret_name = old_config['spec'].get('name') | ||||||
|  |         old_secret_namespace = old_config['spec'].get('namespace') | ||||||
|  |     secret_name = spec.get('name') | ||||||
|  |     secret_namespace = spec.get('namespace') | ||||||
|  |  | ||||||
|  |     if old_config is not None and ( | ||||||
|  |             old_secret_name != secret_name or old_secret_namespace != secret_namespace): | ||||||
|  |         # If the name of the secret or the namespace of the secret is different | ||||||
|  |         # We have to delete the secret an recreate it | ||||||
|  |         logger.info("Secret name or namespace changed, let's recreate it") | ||||||
|  |         delete_managed_secret( | ||||||
|  |             old_config['spec'], | ||||||
|  |             name, | ||||||
|  |             namespace, | ||||||
|  |             logger, | ||||||
|  |             **kwargs) | ||||||
|  |         create_managed_secret(spec, name, namespace, logger, body, **kwargs) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     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 BaseException: | ||||||
|  |         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 BaseException: | ||||||
|  |         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
									
								
							
							
								
								
									
										53
									
								
								src/utils/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/utils/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import os | ||||||
|  | import json | ||||||
|  | import subprocess | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BitwardenCommandException(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_secret_from_bitwarden(logger, id): | ||||||
|  |     return command_wrapper(logger, command=f"get item {id}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def unlock_bw(logger): | ||||||
|  |     status_output = command_wrapper(logger, "status", False) | ||||||
|  |     status = status_output['data']['template']['status'] | ||||||
|  |     if status == 'unlocked': | ||||||
|  |         logger.info("Already unlocked") | ||||||
|  |         return | ||||||
|  |     token_output = command_wrapper(logger, "unlock --passwordenv BW_PASSWORD") | ||||||
|  |     os.environ["BW_SESSION"] = token_output["data"]["raw"] | ||||||
|  |     logger.info("Signin successful. Session exported") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def command_wrapper(logger, command, use_success: bool = True): | ||||||
|  |     system_env = dict(os.environ) | ||||||
|  |     sp = subprocess.Popen( | ||||||
|  |         [f"bw --response {command}"], | ||||||
|  |         stdout=subprocess.PIPE, | ||||||
|  |         stderr=subprocess.PIPE, | ||||||
|  |         close_fds=True, | ||||||
|  |         shell=True, | ||||||
|  |         env=system_env) | ||||||
|  |     out, err = sp.communicate() | ||||||
|  |     resp = json.loads(out.decode(encoding='UTF-8')) | ||||||
|  |     if "DEBUG" in system_env: | ||||||
|  |         logger.info(resp) | ||||||
|  |     if resp["success"] != None and (not use_success or (use_success and resp["success"] == True)): | ||||||
|  |         return resp | ||||||
|  |     logger.warn(resp) | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def parse_login_scope(secret_json, key): | ||||||
|  |     return secret_json["data"]["login"][key] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def parse_fields_scope(secret_json, key): | ||||||
|  |     if "fields" not in secret_json["data"]: | ||||||
|  |         return None | ||||||
|  |     for entry in secret_json["data"]["fields"]: | ||||||
|  |         if entry['name'] == key: | ||||||
|  |             return entry['value'] | ||||||
		Reference in New Issue
	
	Block a user