1 Commits

Author SHA1 Message Date
43773c33e8 Merge pull request 'jinja templates' (#6) from feature/tt/raw-templates into main
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Reviewed-on: #6
2022-11-26 20:38:19 +00:00
22 changed files with 151 additions and 955 deletions

View File

@ -8,13 +8,11 @@ on:
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@v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
@ -29,63 +27,8 @@ jobs:
version: v3.10.0 version: v3.10.0
- name: Run chart-releaser - name: Run chart-releaser
uses: helm/chart-releaser-action@v1.6.0 uses: helm/chart-releaser-action@v1.4.1
with: with:
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.40.5
id: app_version
with:
cmd: yq '.appVersion' charts/bitwarden-crd-operator/Chart.yaml
- name: "GHCR Login"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: lerentis
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: "GHCR Build and Push"
id: docker_build
uses: docker/build-push-action@v5
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@v4
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

View File

@ -1,55 +0,0 @@
name: Lint and Test
on: pull_request
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: v3.11.2
- uses: actions/setup-python@v5
with:
python-version: '3.9'
check-latest: true
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.6.1
- 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@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: "GHCR Build"
id: docker_build
uses: docker/build-push-action@v5
with:
push: false
platforms: linux/amd64,linux/arm64
tags: ghcr.io/lerentis/bitwarden-crd-operator:dev

2
.gitignore vendored
View File

@ -166,5 +166,3 @@ lib
lib64 lib64
myvalues.yaml myvalues.yaml
.vscode

View File

@ -1,50 +1,30 @@
FROM alpine:3.18.4 FROM alpine:latest as builder
LABEL org.opencontainers.image.source=https://github.com/Lerentis/bitwarden-crd-operator ARG BW_VERSION=2022.8.0
LABEL org.opencontainers.image.description="Kubernetes Operator to create k8s secrets from bitwarden"
LABEL org.opencontainers.image.licenses=MIT
ARG PYTHON_VERSION=3.11.6-r0 RUN apk add wget unzip
ARG PIP_VERSION=23.1.2-r0
ARG GCOMPAT_VERSION=1.1.0-r1
ARG LIBCRYPTO_VERSION=3.1.3-r0
ARG BW_VERSION=2023.1.0
COPY requirements.txt /requirements.txt 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
FROM alpine:3.17
COPY --from=builder /tmp/bw /usr/local/bin/bw
COPY requirements.txt requirements.txt
RUN set -eux; \ RUN set -eux; \
apk add --virtual build-dependencies wget unzip; \
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; \
mv /tmp/bw /usr/local/bin/bw; \
chmod +x /usr/local/bin/bw; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
apk del --purge build-dependencies; \
addgroup -S -g 1000 bw-operator; \ addgroup -S -g 1000 bw-operator; \
adduser -S -D -u 1000 -G bw-operator 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; \
apk add gcc musl-dev libstdc++ gcompat=${GCOMPAT_VERSION} python3=${PYTHON_VERSION} py3-pip=${PIP_VERSION} libcrypto3=${LIBCRYPTO_VERSION}; \ chmod +x /usr/local/bin/bw; \
pip install -r /requirements.txt --no-warn-script-location; \ apk add gcc musl-dev libstdc++ gcompat python3 py-pip; \
rm /requirements.txt; \ pip install -r requirements.txt --no-warn-script-location; \
apk del --purge gcc musl-dev libstdc++; apk del --purge gcc musl-dev libstdc++;
COPY --chown=bw-operator:bw-operator src /home/bw-operator COPY --chown=bw-operator:bw-operator src /home/bw-operator
USER bw-operator USER bw-operator
ENTRYPOINT [ "kopf", "run", "--log-format=json", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ] ENTRYPOINT [ "kopf", "run", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ]
CMD [ "/home/bw-operator/bitwardenCrdOperator.py", "/home/bw-operator/kv.py", "/home/bw-operator/dockerlogin.py", "/home/bw-operator/template.py"] CMD [ "/home/bw-operator/bitwardenCrdOperator.py", "/home/bw-operator/kv.py", "/home/bw-operator/dockerlogin.py", "/home/bw-operator/template.py"]

View File

@ -1,27 +0,0 @@
deployment_name ?= bitwarden-crd-operator
namespace ?= bitwarden-crd-operator
label_filter = -l app.kubernetes.io/instance=bitwarden-crd-operator -l app.kubernetes.io/name=bitwarden-crd-operator
create-namespace:
kubectl create namespace ${namespace}
dev:
skaffold dev -n ${namespace}
run:
skaffold run -n ${namespace}
pods:
kubectl -n ${namespace} get pods
desc-pods:
kubectl -n ${namespace} describe pod ${label_filter}
delete-pods-force:
kubectl -n ${namespace} delete pod ${label_filter} --force
exec:
kubectl -n ${namespace} exec -it deployment/${deployment_name} -- sh
logs:
kubectl -n ${namespace} logs -f --tail 30 deployment/${deployment_name}

View File

@ -56,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/v1beta4" apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
kind: BitwardenSecret kind: BitwardenSecret
metadata: metadata:
name: name-of-your-management-object name: name-of-your-management-object
@ -65,11 +65,9 @@ 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"
@ -98,7 +96,7 @@ For managing registry credentials, or pull secrets, you can create another kind
```yaml ```yaml
--- ---
apiVersion: "lerentis.uploadfilter24.eu/v1beta4" apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
kind: RegistryCredential kind: RegistryCredential
metadata: metadata:
name: name-of-your-management-object name: name-of-your-management-object
@ -127,60 +125,10 @@ metadata:
type: dockerconfigjson type: dockerconfigjson
``` ```
## BitwardenTemplate ## Short Term Roadmap
One of the more freely defined types that can be used with this operator you can just pass a whole template. Also the lookup function `bitwarden_lookup` is available to reference parts of the secret: - [ ] support more types
- [x] offer option to use a existing secret in helm chart
```yaml - [x] host chart on gh pages
--- - [x] write release pipeline
apiVersion: "lerentis.uploadfilter24.eu/v1beta4" - [x] maybe extend spec to offer modification of keys as well
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 or attachment", "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 or attachment", "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
```
The signature of `bitwarden_lookup` is `(item_id, scope, field)`:
- `item_id`: The item ID of the secret in Bitwarden
- `scope`: one of `login`, `fields` or `attachment`
- `field`:
- when `scope` is `login`: either `username` or `password`
- when `scope` is `fields`: the name of a custom field
- when `scope` is `attachment`: the filename of a file attached to the item
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.
## Configurations parameters
The operator uses the bitwarden cli in the background and does not communicate to the api directly. The cli mirrors the credential store locally but doesn't sync it on every get request. Instead it will sync each secret every 15 minutes (900 seconds). You can adjust the interval by setting `BW_SYNC_INTERVAL` in the values. If your secrets update very very frequently, you can force the operator to do a sync before each get by setting `BW_FORCE_SYNC="true"`. You might run into rate limits if you do this too frequent.
Additionally the bitwarden cli session may expire at some time. In order to create a new session, the login command is triggered from time to time. In what interval exactly can be configured with the env `BW_RELOGIN_INTERVAL` which defaults to 3600s.

View File

@ -4,9 +4,9 @@ description: Deploy the Bitwarden CRD Operator
type: application type: application
version: "v0.11.0" version: "v0.4.0"
appVersion: "0.10.0" appVersion: "0.4.0"
keywords: keywords:
- operator - operator
@ -20,7 +20,7 @@ home: https://lerentis.github.io/bitwarden-crd-operator/
sources: sources:
- https://github.com/Lerentis/bitwarden-crd-operator - https://github.com/Lerentis/bitwarden-crd-operator
kubeVersion: ">= 1.23.0-0" kubeVersion: '>= 1.23.0-0'
maintainers: maintainers:
- name: lerentis - name: lerentis
@ -32,22 +32,22 @@ 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: v1beta5 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: v1beta5 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 - kind: BitwardenTemplate
version: v1beta5 version: v1beta1
name: bitwarden-template name: bitwarden-template
displayName: Bitwarden Template displayName: Bitwarden Template
description: Management Object to create secrets from a jinja template with a bitwarden lookup description: Management Object to create secrets from a jinja template with a bitwarden lookup
artifacthub.io/crdsExamples: | artifacthub.io/crdsExamples: |
- apiVersion: lerentis.uploadfilter24.eu/v1beta5 - apiVersion: lerentis.uploadfilter24.eu/v1beta4
kind: BitwardenSecret kind: BitwardenSecret
metadata: metadata:
name: test name: test
@ -62,9 +62,7 @@ annotations:
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
name: "test-secret" name: "test-secret"
namespace: "default" namespace: "default"
labels: - apiVersion: lerentis.uploadfilter24.eu/v1beta4
key: value
- apiVersion: lerentis.uploadfilter24.eu/v1beta5
kind: RegistryCredential kind: RegistryCredential
metadata: metadata:
name: test name: test
@ -75,9 +73,7 @@ annotations:
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
name: "test-regcred" name: "test-regcred"
namespace: "default" namespace: "default"
labels: - apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
key: value
- apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
kind: BitwardenTemplate kind: BitwardenTemplate
metadata: metadata:
name: test name: test
@ -85,24 +81,27 @@ annotations:
filename: "config.yaml" filename: "config.yaml"
name: "test-regcred" name: "test-regcred"
namespace: "default" namespace: "default"
labels:
key: value
template: | template: |
--- ---
api: api:
enabled: True enabled: True
key: {{ bitwarden_lookup("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "fields", "key") }} key: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "key") }}
allowCrossOrigin: false allowCrossOrigin: false
apps: apps:
"some.app.identifier:some_version": "some.app.identifier:some_version":
pubkey: {{ bitwarden_lookup("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "attachment", "public_key") }} pubkey: {{ bitwarden_lookup("466fc4b0-ffca-4444-8d88-b59d4de3d928", "fields", "public_key") }}
enabled: true enabled: true
artifacthub.io/license: MIT artifacthub.io/license: MIT
artifacthub.io/operator: "true" artifacthub.io/operator: "true"
artifacthub.io/containsSecurityUpdates: "false"
artifacthub.io/changes: | artifacthub.io/changes: |
- kind: added
description: "Added Template type"
- kind: added
description: "Added logo"
- kind: changed - kind: changed
description: "Added the possibility to add labels to generated secrets" description: "BitwardenSecret now requires a 'secretScope' to be defined. Can eigher be 'login' or 'fields'"
- kind: fixed
description: "fixed hardcoded reference to 'login' even tho secrets could also be in 'fields' scope"
artifacthub.io/images: | artifacthub.io/images: |
- name: bitwarden-crd-operator - name: bitwarden-crd-operator
image: ghcr.io/lerentis/bitwarden-crd-operator:0.10.0 image: lerentis/bitwarden-crd-operator:0.4.0

View File

@ -4,14 +4,9 @@
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.
@ -56,7 +51,7 @@ And you are set to create your first secret using this operator. For that you ne
```yaml ```yaml
--- ---
apiVersion: "lerentis.uploadfilter24.eu/v1beta4" apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
kind: BitwardenSecret kind: BitwardenSecret
metadata: metadata:
name: name-of-your-management-object name: name-of-your-management-object
@ -65,11 +60,9 @@ 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"
@ -98,7 +91,7 @@ For managing registry credentials, or pull secrets, you can create another kind
```yaml ```yaml
--- ---
apiVersion: "lerentis.uploadfilter24.eu/v1beta4" apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
kind: RegistryCredential kind: RegistryCredential
metadata: metadata:
name: name-of-your-management-object name: name-of-your-management-object
@ -127,46 +120,10 @@ metadata:
type: dockerconfigjson type: dockerconfigjson
``` ```
## BitwardenTemplate ## Short Term Roadmap
One of the more freely defined types that can be used with this operator you can just pass a whole template: - [ ] support more types
- [x] offer option to use a existing secret in helm chart
```yaml - [x] host chart on gh pages
--- - [x] write release pipeline
apiVersion: "lerentis.uploadfilter24.eu/v1beta4" - [x] maybe extend spec to offer modification of keys as well
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.

View File

@ -14,42 +14,6 @@ spec:
- bws - bws
versions: versions:
- name: v1beta4 - name: v1beta4
served: false
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
content:
type: array
items:
type: object
properties:
element:
type: object
properties:
secretName:
type: string
secretRef:
type: string
secretScope:
type: string
required:
- secretName
id:
type: string
namespace:
type: string
name:
type: string
required:
- id
- namespace
- name
- name: v1beta5
served: true served: true
storage: true storage: true
schema: schema:
@ -81,22 +45,6 @@ spec:
type: string type: string
name: name:
type: string type: string
labels:
type: array
items:
type: object
properties:
json:
x-kubernetes-preserve-unknown-fields: true
type: object
properties:
spec:
type: object
properties:
foo:
type: string
bar:
type: string
required: required:
- id - id
- namespace - namespace

View File

@ -14,29 +14,6 @@ spec:
- bwt - bwt
versions: versions:
- name: v1beta4 - name: v1beta4
served: false
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
- name: v1beta5
served: true served: true
storage: true storage: true
schema: schema:
@ -54,22 +31,6 @@ spec:
type: string type: string
name: name:
type: string type: string
labels:
type: array
items:
type: object
properties:
json:
x-kubernetes-preserve-unknown-fields: true
type: object
properties:
spec:
type: object
properties:
foo:
type: string
bar:
type: string
required: required:
- filename - filename
- template - template

View File

@ -14,35 +14,6 @@ spec:
- rgc - rgc
versions: versions:
- name: v1beta4 - name: v1beta4
served: false
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
- name: v1beta5
served: true served: true
storage: true storage: true
schema: schema:
@ -64,22 +35,6 @@ spec:
type: string type: string
name: name:
type: string type: string
labels:
type: array
items:
type: object
properties:
json:
x-kubernetes-preserve-unknown-fields: true
type: object
properties:
spec:
type: object
properties:
foo:
type: string
bar:
type: string
required: required:
- id - id
- namespace - namespace

View File

@ -50,20 +50,10 @@ spec:
httpGet: httpGet:
path: /healthz path: /healthz
port: http port: http
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
successThreshold: {{ .Values.livenessProbe.successThreshold }}
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /healthz path: /healthz
port: http port: http
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
successThreshold: {{ .Values.readinessProbe.successThreshold }}
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}

View File

@ -5,7 +5,7 @@
replicaCount: 1 replicaCount: 1
image: image:
repository: ghcr.io/lerentis/bitwarden-crd-operator repository: 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,11 +14,7 @@ imagePullSecrets: []
nameOverride: "" nameOverride: ""
fullnameOverride: "" fullnameOverride: ""
# env: #env:
# - name: BW_FORCE_SYNC
# value: "false"
# - name: BW_SYNC_INTERVAL
# value: "900"
# - name: BW_HOST # - name: BW_HOST
# value: "define_it" # value: "define_it"
# - name: BW_CLIENTID # - name: BW_CLIENTID
@ -27,8 +23,6 @@ fullnameOverride: ""
# value: "define_it" # value: "define_it"
# - name: BW_PASSWORD # - name: BW_PASSWORD
# value: "define_id" # value: "define_id"
## - name: BW_RELOGIN_INTERVAL
## value: "3600"
externalConfigSecret: externalConfigSecret:
enabled: false enabled: false
@ -57,20 +51,6 @@ securityContext: {}
# runAsNonRoot: true # runAsNonRoot: true
# runAsUser: 1000 # runAsUser: 1000
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
livenessProbe:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
resources: {} resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious # We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little # choice for the user. This also increases chances charts run on environments with little

View File

@ -1,5 +1,5 @@
--- ---
apiVersion: "lerentis.uploadfilter24.eu/v1beta5" apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
kind: BitwardenSecret kind: BitwardenSecret
metadata: metadata:
name: test name: test
@ -16,21 +16,3 @@ spec:
id: "88781348-c81c-4367-9801-550360c21295" id: "88781348-c81c-4367-9801-550360c21295"
name: "test-secret" name: "test-secret"
namespace: "default" namespace: "default"
labels:
- key: value
---
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
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"
labels:
- key: value

View File

@ -1,4 +1,3 @@
kopf==1.36.2 kopf==1.35.6
kubernetes==26.1.0 kubernetes==25.3.0
Jinja2==3.1.2 Jinja2==3.1.2
schedule==1.2.1

View File

@ -1,17 +0,0 @@
apiVersion: skaffold/v4beta5
kind: Config
metadata:
name: bitwarden-crd-operator
build:
artifacts:
- image: ghcr.io/lerentis/bitwarden-crd-operator
docker:
dockerfile: Dockerfile
deploy:
helm:
releases:
- name: bitwarden-crd-operator
chartPath: charts/bitwarden-crd-operator
valuesFiles:
- env/values.yaml
version: v0.7.4

View File

@ -1,47 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import kopf import kopf
import schedule import os
import time
import threading
from utils.utils import command_wrapper, unlock_bw, sync_bw from utils.utils import command_wrapper, unlock_bw
@kopf.on.startup()
def bitwarden_signin(logger, **kwargs): def bitwarden_signin(logger, **kwargs):
if 'BW_HOST' in os.environ: if 'BW_HOST' in os.environ:
try: try:
command_wrapper(logger, f"config server {os.getenv('BW_HOST')}") command_wrapper(f"config server {os.getenv('BW_HOST')}")
except BaseException: except:
logger.warn("Received non-zero exit code from server config") logger.warn("Revieved none zero exit code from server config")
logger.warn("This is expected from startup") logger.warn("This is expected from startup")
pass pass
else: else:
logger.info("BW_HOST not set. Assuming SaaS installation") logger.info(f"BW_HOST not set. Assuming SaaS installation")
command_wrapper(logger, "login --apikey") command_wrapper("login --apikey")
unlock_bw(logger) unlock_bw(logger)
def run_continuously(interval=30):
cease_continuous_run = threading.Event()
class ScheduleThread(threading.Thread):
@classmethod
def run(cls):
while not cease_continuous_run.is_set():
schedule.run_pending()
time.sleep(interval)
continuous_thread = ScheduleThread()
continuous_thread.start()
return cease_continuous_run
@kopf.on.startup()
def load_schedules(logger, **kwargs):
bitwarden_signin(logger)
logger.info("Loading schedules")
bw_relogin_interval = float(os.environ.get('BW_RELOGIN_INTERVAL', 3600))
bw_sync_interval = float(os.environ.get('BW_SYNC_INTERVAL', 900))
schedule.every(bw_relogin_interval).seconds.do(bitwarden_signin, logger=logger)
logger.info(f"relogin scheduled every {bw_relogin_interval} seconds")
schedule.every(bw_sync_interval).seconds.do(sync_bw, logger=logger)
logger.info(f"sync scheduled every {bw_relogin_interval} seconds")
stop_run_continuously = run_continuously()

View File

@ -3,17 +3,10 @@ import kubernetes
import base64 import base64
import json import json
from utils.utils import unlock_bw, get_secret_from_bitwarden, bw_sync_interval from utils.utils import unlock_bw, get_secret_from_bitwarden
def create_dockerlogin(logger, secret, secret_json, username_ref, password_ref, registry):
def create_dockerlogin( secret.type = "dockerconfigjson"
logger,
secret,
secret_json,
username_ref,
password_ref,
registry):
secret.type = "kubernetes.io/dockerconfigjson"
secret.data = {} secret.data = {}
auths_dict = {} auths_dict = {}
registry_dict = {} registry_dict = {}
@ -22,20 +15,14 @@ def create_dockerlogin(
_username = secret_json["login"][username_ref] _username = secret_json["login"][username_ref]
logger.info(f"Creating login with username: {_username}") logger.info(f"Creating login with username: {_username}")
_password = secret_json["login"][password_ref] _password = secret_json["login"][password_ref]
cred_field = str( cred_field = str(base64.b64encode(f"{_username}:{_password}".encode("utf-8")), "utf-8")
base64.b64encode(
f"{_username}:{_password}".encode("utf-8")),
"utf-8")
reg_auth_dict["username"] = _username
reg_auth_dict["password"] = _password
reg_auth_dict["auth"] = cred_field reg_auth_dict["auth"] = cred_field
registry_dict[registry] = reg_auth_dict registry_dict[registry] = reg_auth_dict
auths_dict["auths"] = registry_dict auths_dict["auths"] = registry_dict
secret.data[".dockerconfigjson"] = str(base64.b64encode( secret.data[".dockerconfigjson"] = str(base64.b64encode(json.dumps(auths_dict).encode("utf-8")), "utf-8")
json.dumps(auths_dict).encode("utf-8")), "utf-8")
return secret return secret
@kopf.on.create('registry-credential.lerentis.uploadfilter24.eu') @kopf.on.create('registry-credential.lerentis.uploadfilter24.eu')
def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
username_ref = spec.get('usernameRef') username_ref = spec.get('usernameRef')
@ -44,11 +31,10 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
id = spec.get('id') id = spec.get('id')
secret_name = spec.get('name') secret_name = spec.get('name')
secret_namespace = spec.get('namespace') secret_namespace = spec.get('namespace')
labels = spec.get('labels')
unlock_bw(logger) unlock_bw(logger)
logger.info(f"Locking up secret with ID: {id}") logger.info(f"Locking up secret with ID: {id}")
secret_json_object = get_secret_from_bitwarden(logger, id) secret_json_object = json.loads(get_secret_from_bitwarden(id))
api = kubernetes.client.CoreV1Api() api = kubernetes.client.CoreV1Api()
@ -56,103 +42,19 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
"managed": "registry-credential.lerentis.uploadfilter24.eu", "managed": "registry-credential.lerentis.uploadfilter24.eu",
"managedObject": f"{namespace}/{name}" "managedObject": f"{namespace}/{name}"
} }
if not labels:
labels = {}
secret = kubernetes.client.V1Secret() secret = kubernetes.client.V1Secret()
secret.metadata = kubernetes.client.V1ObjectMeta( secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations)
name=secret_name, annotations=annotations, labels=labels) secret = create_dockerlogin(logger, secret, secret_json_object, username_ref, password_ref, registry)
secret = create_dockerlogin(
logger,
secret,
secret_json_object["data"],
username_ref,
password_ref,
registry)
api.create_namespaced_secret( obj = api.create_namespaced_secret(
secret_namespace, secret secret_namespace, secret
) )
logger.info( logger.info(f"Registry Secret {secret_namespace}/{secret_name} has been created")
f"Registry Secret {secret_namespace}/{secret_name} has been created")
@kopf.on.update('registry-credential.lerentis.uploadfilter24.eu') @kopf.on.update('registry-credential.lerentis.uploadfilter24.eu')
@kopf.timer('registry-credential.lerentis.uploadfilter24.eu', interval=bw_sync_interval) def my_handler(spec, old, new, diff, **_):
def update_managed_registry_secret( pass
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') @kopf.on.delete('registry-credential.lerentis.uploadfilter24.eu')
def delete_managed_secret(spec, name, namespace, logger, **kwargs): def delete_managed_secret(spec, name, namespace, logger, **kwargs):
@ -162,8 +64,6 @@ def delete_managed_secret(spec, name, namespace, logger, **kwargs):
try: try:
api.delete_namespaced_secret(secret_name, secret_namespace) api.delete_namespaced_secret(secret_name, secret_namespace)
logger.info( logger.info(f"Secret {secret_namespace}/{secret_name} has been deleted")
f"Secret {secret_namespace}/{secret_name} has been deleted") except:
except BaseException: logger.warn(f"Could not delete secret {secret_namespace}/{secret_name}!")
logger.warn(
f"Could not delete secret {secret_namespace}/{secret_name}!")

109
src/kv.py
View File

@ -3,14 +3,14 @@ import kubernetes
import base64 import base64
import json import json
from utils.utils import unlock_bw, get_secret_from_bitwarden, parse_login_scope, parse_fields_scope, bw_sync_interval from utils.utils import unlock_bw, get_secret_from_bitwarden, parse_login_scope, parse_fields_scope
def create_kv(secret, secret_json, content_def): def create_kv(secret, secret_json, content_def):
secret.type = "Opaque" secret.type = "Opaque"
secret.data = {} secret.data = {}
for eleml in content_def: for eleml in content_def:
for k, elem in eleml.items(): for k, elem in eleml.items():
for key, value in elem.items(): for key,value in elem.items():
if key == "secretName": if key == "secretName":
_secret_key = value _secret_key = value
if key == "secretRef": if key == "secretRef":
@ -18,22 +18,11 @@ def create_kv(secret, secret_json, content_def):
if key == "secretScope": if key == "secretScope":
_secret_scope = value _secret_scope = value
if _secret_scope == "login": if _secret_scope == "login":
value = parse_login_scope(secret_json, _secret_key) secret.data[_secret_ref] = str(base64.b64encode(parse_login_scope(secret_json, _secret_key).encode("utf-8")), "utf-8")
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": if _secret_scope == "fields":
value = parse_fields_scope(secret_json, _secret_key) secret.data[_secret_ref] = str(base64.b64encode(parse_fields_scope(secret_json, _secret_key).encode("utf-8")), "utf-8")
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 return secret
@kopf.on.create('bitwarden-secret.lerentis.uploadfilter24.eu') @kopf.on.create('bitwarden-secret.lerentis.uploadfilter24.eu')
def create_managed_secret(spec, name, namespace, logger, body, **kwargs): def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
@ -41,11 +30,10 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
id = spec.get('id') id = spec.get('id')
secret_name = spec.get('name') secret_name = spec.get('name')
secret_namespace = spec.get('namespace') secret_namespace = spec.get('namespace')
labels = spec.get('labels')
unlock_bw(logger) unlock_bw(logger)
logger.info(f"Locking up secret with ID: {id}") logger.info(f"Locking up secret with ID: {id}")
secret_json_object = get_secret_from_bitwarden(logger, id) secret_json_object = json.loads(get_secret_from_bitwarden(id))
api = kubernetes.client.CoreV1Api() api = kubernetes.client.CoreV1Api()
@ -53,88 +41,19 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
"managed": "bitwarden-secret.lerentis.uploadfilter24.eu", "managed": "bitwarden-secret.lerentis.uploadfilter24.eu",
"managedObject": f"{namespace}/{name}" "managedObject": f"{namespace}/{name}"
} }
if not labels:
labels = {}
secret = kubernetes.client.V1Secret() secret = kubernetes.client.V1Secret()
secret.metadata = kubernetes.client.V1ObjectMeta( secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations)
name=secret_name, annotations=annotations, labels=labels)
secret = create_kv(secret, secret_json_object, content_def) secret = create_kv(secret, secret_json_object, content_def)
api.create_namespaced_secret( obj = api.create_namespaced_secret(
namespace="{}".format(secret_namespace), secret_namespace, secret
body=secret
) )
logger.info(f"Secret {secret_namespace}/{secret_name} has been created") logger.info(f"Secret {secret_namespace}/{secret_name} has been created")
@kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu') @kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu')
@kopf.timer('bitwarden-secret.lerentis.uploadfilter24.eu', interval=bw_sync_interval) def my_handler(spec, old, new, diff, **_):
def update_managed_secret( pass
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') @kopf.on.delete('bitwarden-secret.lerentis.uploadfilter24.eu')
def delete_managed_secret(spec, name, namespace, logger, **kwargs): def delete_managed_secret(spec, name, namespace, logger, **kwargs):
@ -144,8 +63,6 @@ def delete_managed_secret(spec, name, namespace, logger, **kwargs):
try: try:
api.delete_namespaced_secret(secret_name, secret_namespace) api.delete_namespaced_secret(secret_name, secret_namespace)
logger.info( logger.info(f"Secret {secret_namespace}/{secret_name} has been deleted")
f"Secret {secret_namespace}/{secret_name} has been deleted") except:
except BaseException: logger.warn(f"Could not delete secret {secret_namespace}/{secret_name}!")
logger.warn(
f"Could not delete secret {secret_namespace}/{secret_name}!")

View File

@ -1,15 +1,9 @@
from utils.utils import get_secret_from_bitwarden, get_attachment, parse_fields_scope, parse_login_scope import json
from utils.utils import get_secret_from_bitwarden, parse_fields_scope, parse_login_scope
class BitwardenLookupHandler: def bitwarden_lookup(id, scope, field):
_secret_json = json.loads(get_secret_from_bitwarden(id))
def __init__(self, logger) -> None:
self.logger = logger
def bitwarden_lookup(self, id, scope, field):
if scope == "attachment":
return get_attachment(self.logger, id, field)
_secret_json = get_secret_from_bitwarden(self.logger, id)
if scope == "login": if scope == "login":
return parse_login_scope(_secret_json, field) return parse_login_scope(_secret_json, field)
if scope == "fields": if scope == "fields":

View File

@ -1,31 +1,27 @@
import kopf import kopf
import base64 import base64
import kubernetes import kubernetes
import json
from utils.utils import unlock_bw, bw_sync_interval from utils.utils import unlock_bw
from lookups.bitwarden_lookup import BitwardenLookupHandler from lookups.bitwarden_lookup import bitwarden_lookup
from jinja2 import Environment, BaseLoader from jinja2 import Environment, BaseLoader
def render_template(logger, template): lookup_func_dict = {
"bitwarden_lookup": bitwarden_lookup,
}
def render_template(template):
jinja_template = Environment(loader=BaseLoader()).from_string(template) jinja_template = Environment(loader=BaseLoader()).from_string(template)
jinja_template.globals.update({ jinja_template.globals.update(lookup_func_dict)
"bitwarden_lookup": BitwardenLookupHandler(logger).bitwarden_lookup,
})
return jinja_template.render() return jinja_template.render()
def create_template_secret(secret, filename, template):
def create_template_secret(logger, secret, filename, template):
secret.type = "Opaque" secret.type = "Opaque"
secret.data = {} secret.data = {}
secret.data[filename] = str( secret.data[filename] = str(base64.b64encode(render_template(template).encode("utf-8")), "utf-8")
base64.b64encode(
render_template(logger, template).encode("utf-8")),
"utf-8")
return secret return secret
@kopf.on.create('bitwarden-template.lerentis.uploadfilter24.eu') @kopf.on.create('bitwarden-template.lerentis.uploadfilter24.eu')
def create_managed_secret(spec, name, namespace, logger, body, **kwargs): def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
@ -33,7 +29,6 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
filename = spec.get('filename') filename = spec.get('filename')
secret_name = spec.get('name') secret_name = spec.get('name')
secret_namespace = spec.get('namespace') secret_namespace = spec.get('namespace')
labels = spec.get('labels')
unlock_bw(logger) unlock_bw(logger)
@ -43,14 +38,9 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
"managed": "bitwarden-template.lerentis.uploadfilter24.eu", "managed": "bitwarden-template.lerentis.uploadfilter24.eu",
"managedObject": f"{namespace}/{name}" "managedObject": f"{namespace}/{name}"
} }
if not labels:
labels = {}
secret = kubernetes.client.V1Secret() secret = kubernetes.client.V1Secret()
secret.metadata = kubernetes.client.V1ObjectMeta( secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations)
name=secret_name, annotations=annotations, labels=labels) secret = create_template_secret(secret, filename, template)
secret = create_template_secret(logger, secret, filename, template)
obj = api.create_namespaced_secret( obj = api.create_namespaced_secret(
secret_namespace, secret secret_namespace, secret
@ -58,72 +48,9 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
logger.info(f"Secret {secret_namespace}/{secret_name} has been created") logger.info(f"Secret {secret_namespace}/{secret_name} has been created")
@kopf.on.update('bitwarden-template.lerentis.uploadfilter24.eu') @kopf.on.update('bitwarden-template.lerentis.uploadfilter24.eu')
@kopf.timer('bitwarden-template.lerentis.uploadfilter24.eu', interval=bw_sync_interval) def my_handler(spec, old, new, diff, **_):
def update_managed_secret( pass
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(logger, 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') @kopf.on.delete('bitwarden-template.lerentis.uploadfilter24.eu')
def delete_managed_secret(spec, name, namespace, logger, **kwargs): def delete_managed_secret(spec, name, namespace, logger, **kwargs):
@ -133,8 +60,13 @@ def delete_managed_secret(spec, name, namespace, logger, **kwargs):
try: try:
api.delete_namespaced_secret(secret_name, secret_namespace) api.delete_namespaced_secret(secret_name, secret_namespace)
logger.info( logger.info(f"Secret {secret_namespace}/{secret_name} has been deleted")
f"Secret {secret_namespace}/{secret_name} has been deleted") except:
except BaseException: logger.warn(f"Could not delete secret {secret_namespace}/{secret_name}!")
logger.warn(
f"Could not delete secret {secret_namespace}/{secret_name}!") #if __name__ == '__main__':
# tpl = """
# Calling the 'bitwarden_lookup' function:
# {{ bitwarden_lookup(2, 2) }}
# """
# print(render_template(tpl))

View File

@ -1,91 +1,30 @@
import os import os
import json
import subprocess import subprocess
import distutils
bw_sync_interval = float(os.environ.get(
'BW_SYNC_INTERVAL', 900))
class BitwardenCommandException(Exception): class BitwardenCommandException(Exception):
pass pass
def get_secret_from_bitwarden(id):
def get_secret_from_bitwarden(logger, id, force_sync=False): return command_wrapper(command=f"get item {id}")
sync_bw(logger, force=force_sync)
return command_wrapper(logger, command=f"get item {id}")
def sync_bw(logger, force=False):
def _sync(logger):
status_output = command_wrapper(logger, command=f"sync")
logger.info(f"Sync successful {status_output}")
return
if force:
_sync(logger)
return
global_force_sync = bool(distutils.util.strtobool(
os.environ.get('BW_FORCE_SYNC', "false")))
if global_force_sync:
logger.debug("Running forced sync")
status_output = _sync(logger)
logger.info(f"Sync successful {status_output}")
else:
logger.debug("Running scheduled sync")
status_output = _sync(logger)
logger.info(f"Sync successful {status_output}")
def get_attachment(logger, id, name):
return command_wrapper(logger, command=f"get attachment {name} --itemid {id}", raw=True)
def unlock_bw(logger): def unlock_bw(logger):
status_output = command_wrapper(logger, "status", False) token_output = command_wrapper("unlock --passwordenv BW_PASSWORD")
status = status_output['data']['template']['status'] tokens = token_output.split('"')[1::2]
if status == 'unlocked': os.environ["BW_SESSION"] = tokens[1]
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") logger.info("Signin successful. Session exported")
def command_wrapper(command):
def command_wrapper(logger, command, use_success: bool = True, raw: bool = False):
system_env = dict(os.environ) system_env = dict(os.environ)
response_flag = "--raw" if raw else "--response" sp = subprocess.Popen([f"bw {command}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=True, env=system_env)
sp = subprocess.Popen(
[f"bw {response_flag} {command}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
close_fds=True,
shell=True,
env=system_env)
out, err = sp.communicate() out, err = sp.communicate()
if err: if err:
logger.warn(err) raise BitwardenCommandException(err)
return None
if raw:
return out.decode(encoding='UTF-8') return out.decode(encoding='UTF-8')
if "DEBUG" in system_env:
logger.info(out.decode(encoding='UTF-8'))
resp = json.loads(out.decode(encoding='UTF-8'))
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): def parse_login_scope(secret_json, key):
return secret_json["data"]["login"][key] return secret_json["login"][key]
def parse_fields_scope(secret_json, key): def parse_fields_scope(secret_json, key):
if "fields" not in secret_json["data"]: for entry in secret_json["fields"]:
return None
for entry in secret_json["data"]["fields"]:
if entry['name'] == key: if entry['name'] == key:
return entry['value'] return entry['value']