Compare commits
No commits in common. "main" and "v0.2.1" have entirely different histories.
63
.github/workflows/release.yml
vendored
63
.github/workflows/release.yml
vendored
@ -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
|
||||||
|
|
||||||
@ -24,68 +22,13 @@ jobs:
|
|||||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||||
|
|
||||||
- name: Install Helm
|
- name: Install Helm
|
||||||
uses: azure/setup-helm@v4
|
uses: azure/setup-helm@v3
|
||||||
with:
|
with:
|
||||||
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.44.3
|
|
||||||
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@v6
|
|
||||||
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
|
|
||||||
|
66
.github/workflows/test-and-lint.yml
vendored
66
.github/workflows/test-and-lint.yml
vendored
@ -1,66 +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@v4
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
- name: Install ah cli
|
|
||||||
run: |
|
|
||||||
export AH_VERSION=1.17.0
|
|
||||||
curl -LO https://github.com/artifacthub/hub/releases/download/v${AH_VERSION}/ah_${AH_VERSION}_linux_amd64.tar.gz
|
|
||||||
tar -xf ah_${AH_VERSION}_linux_amd64.tar.gz
|
|
||||||
chmod +x ./ah
|
|
||||||
sudo mv ./ah /usr/bin/ah
|
|
||||||
rm LICENSE
|
|
||||||
- name: ah lint
|
|
||||||
run: |
|
|
||||||
ah lint
|
|
||||||
|
|
||||||
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@v6
|
|
||||||
with:
|
|
||||||
push: false
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: ghcr.io/lerentis/bitwarden-crd-operator:dev
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -166,5 +166,3 @@ lib
|
|||||||
lib64
|
lib64
|
||||||
|
|
||||||
myvalues.yaml
|
myvalues.yaml
|
||||||
|
|
||||||
.vscode
|
|
66
Dockerfile
66
Dockerfile
@ -1,35 +1,55 @@
|
|||||||
FROM alpine:3.20.3
|
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.12.6-r0
|
RUN apk add wget unzip
|
||||||
ARG PIP_VERSION=24.0-r2
|
|
||||||
ARG GCOMPAT_VERSION=1.1.0-r4
|
|
||||||
ARG LIBCRYPTO_VERSION=3.3.2-r0
|
|
||||||
ARG BW_VERSION=2024.7.2
|
|
||||||
ARG NODE_VERSION=20.15.1-r0
|
|
||||||
|
|
||||||
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.18 as run
|
||||||
|
#
|
||||||
|
#RUN set -eux; \
|
||||||
|
# groupadd -r bw-operator ; \
|
||||||
|
# useradd -r -g bw-operator -s /sbin/nologin bw-operator; \
|
||||||
|
# mkdir -p /home/bw-operator; \
|
||||||
|
# chown -R bw-operator /home/bw-operator; \
|
||||||
|
# chmod +x /usr/local/bin/bw; \
|
||||||
|
# apk add libstdc++ python3 py-pip
|
||||||
|
#COPY --chown=bw-operator:bw-operator bitwarden-crd-operator.py /home/bw-operator/bitwarden-crd-operator.py
|
||||||
|
#
|
||||||
|
#USER bw-operator
|
||||||
|
#
|
||||||
|
#RUN set -eux; \
|
||||||
|
# pip install -r requirements.txt --no-warn-script-location
|
||||||
|
#
|
||||||
|
#ENTRYPOINT [ "/home/bw-operator/.local/bin/kopf", "run", "--all-namespaces", "--liveness=http://0.0.0.0:8080/healthz" ]
|
||||||
|
#CMD [ "/home/bw-operator/bitwarden-crd-operator.py" ]
|
||||||
|
|
||||||
|
FROM ubuntu:jammy
|
||||||
|
|
||||||
|
COPY --from=builder /tmp/bw /usr/local/bin/bw
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
apk update; \
|
groupadd -r bw-operator ; \
|
||||||
apk del nodejs-current; \
|
useradd -r -g bw-operator -s /sbin/nologin bw-operator; \
|
||||||
apk add nodejs=${NODE_VERSION} npm; \
|
|
||||||
npm install -g @bitwarden/cli@${BW_VERSION}; \
|
|
||||||
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; \
|
||||||
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 --break-system-packages; \
|
apt-get update; \
|
||||||
rm /requirements.txt; \
|
apt-get upgrade -y; \
|
||||||
apk del --purge gcc musl-dev libstdc++;
|
apt-get install -y --no-install-recommends python3 python3-pip; \
|
||||||
|
apt-get clean; \
|
||||||
|
apt-get -y autoremove; \
|
||||||
|
pip install -r requirements.txt; \
|
||||||
|
rm requirements.txt; \
|
||||||
|
pip cache purge; \
|
||||||
|
rm -rf /root/.cache;
|
||||||
|
|
||||||
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" ]
|
||||||
|
27
Makefile
27
Makefile
@ -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}
|
|
112
README.md
112
README.md
@ -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="logo.png" alt="Bitwarden CRD Operator Logo" width="200"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> DISCLAIMER:
|
> DISCLAIMER:
|
||||||
> This project is still very work in progress :)
|
> This project is still very work in progress :)
|
||||||
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
You will need a `ClientID` and `ClientSecret` ([where to get these](https://bitwarden.com/help/personal-api-key/)) as well as your password.
|
You will need a `ClientID` and `ClientSecret` ([where to get these](https://bitwarden.com/help/personal-api-key/)) as well as your password.
|
||||||
@ -56,29 +51,21 @@ And you are set to create your first secret using this operator. For that you ne
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
|
||||||
kind: BitwardenSecret
|
kind: BitwardenSecret
|
||||||
metadata:
|
metadata:
|
||||||
name: name-of-your-management-object
|
name: name-of-your-management-object
|
||||||
spec:
|
spec:
|
||||||
content:
|
content:
|
||||||
- element:
|
- element:
|
||||||
secretName: nameOfTheFieldInBitwarden # for example username or filename
|
secretName: nameOfTheFieldInBitwarden # for example username
|
||||||
secretRef: nameOfTheKeyInTheSecretToBeCreated
|
secretRef: nameOfTheKeyInTheSecretToBeCreated
|
||||||
secretScope: login # for custom entries on bitwarden use 'fields, for attachments use attachment'
|
|
||||||
- element:
|
- element:
|
||||||
secretName: nameOfAnotherFieldInBitwarden # for example password or filename
|
secretName: nameOfAnotherFieldInBitwarden # for example password
|
||||||
secretRef: nameOfAnotherKeyInTheSecretToBeCreated
|
secretRef: nameOfAnotherKeyInTheSecretToBeCreated
|
||||||
secretScope: login # for custom entries on bitwarden use 'fields, for attachments use attachment'
|
|
||||||
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"
|
||||||
secretType: # Optional (Default: Opaque)
|
|
||||||
namespace: "Namespace of the secret to be created"
|
namespace: "Namespace of the secret to be created"
|
||||||
labels: # Optional
|
|
||||||
key: value
|
|
||||||
annotations: # Optional
|
|
||||||
key: value
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
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:
|
||||||
@ -93,8 +80,6 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
managed: bitwarden-secrets.lerentis.uploadfilter24.eu
|
managed: bitwarden-secrets.lerentis.uploadfilter24.eu
|
||||||
managedObject: bw-operator/test
|
managedObject: bw-operator/test
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
name: name-of-your-management-object
|
name: name-of-your-management-object
|
||||||
namespace: default
|
namespace: default
|
||||||
type: Opaque
|
type: Opaque
|
||||||
@ -106,7 +91,7 @@ For managing registry credentials, or pull secrets, you can create another kind
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
|
||||||
kind: RegistryCredential
|
kind: RegistryCredential
|
||||||
metadata:
|
metadata:
|
||||||
name: name-of-your-management-object
|
name: name-of-your-management-object
|
||||||
@ -117,10 +102,6 @@ spec:
|
|||||||
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"
|
||||||
labels: # Optional
|
|
||||||
key: value
|
|
||||||
annotations: # Optional
|
|
||||||
key: value
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting secret looks something like this:
|
The resulting secret looks something like this:
|
||||||
@ -134,88 +115,15 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
managed: bitwarden-secrets.lerentis.uploadfilter24.eu
|
managed: bitwarden-secrets.lerentis.uploadfilter24.eu
|
||||||
managedObject: bw-operator/test
|
managedObject: bw-operator/test
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
name: name-of-your-management-object
|
name: name-of-your-management-object
|
||||||
namespace: default
|
namespace: default
|
||||||
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/v1beta8"
|
- [x] maybe extend spec to offer modification of keys as well
|
||||||
kind: BitwardenTemplate
|
|
||||||
metadata:
|
|
||||||
name: name-of-your-management-object
|
|
||||||
spec:
|
|
||||||
name: "Name of the secret to be created"
|
|
||||||
secretType: # Optional (Default: Opaque)
|
|
||||||
namespace: "Namespace of the secret to be created"
|
|
||||||
labels: # Optional
|
|
||||||
key: value
|
|
||||||
annotations: # Optional
|
|
||||||
key: value
|
|
||||||
content:
|
|
||||||
- element:
|
|
||||||
filename: config.yaml
|
|
||||||
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
|
|
||||||
- element:
|
|
||||||
filename: config2.yaml
|
|
||||||
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: false
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
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 `3600` seconds.
|
|
||||||
|
@ -4,23 +4,21 @@ description: Deploy the Bitwarden CRD Operator
|
|||||||
|
|
||||||
type: application
|
type: application
|
||||||
|
|
||||||
version: "v0.15.0"
|
version: "v0.3.1"
|
||||||
|
|
||||||
appVersion: "0.14.0"
|
appVersion: "0.2.1"
|
||||||
|
|
||||||
keywords:
|
keywords:
|
||||||
- operator
|
- operator
|
||||||
- bitwarden
|
- bitwarden
|
||||||
- vaultwarden
|
- vaultwarden
|
||||||
|
|
||||||
icon: https://lerentis.github.io/bitwarden-crd-operator/logo.png
|
|
||||||
|
|
||||||
home: https://lerentis.github.io/bitwarden-crd-operator/
|
home: https://lerentis.github.io/bitwarden-crd-operator/
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/Lerentis/bitwarden-crd-operator
|
- https://github.com/Lerentis/bitwarden-crd-operator
|
||||||
|
|
||||||
kubeVersion: ">= 1.28.0-0"
|
kubeVersion: '>= 1.23.0-0'
|
||||||
|
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: lerentis
|
- name: lerentis
|
||||||
@ -32,94 +30,20 @@ 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: v1beta8
|
version: v1beta3
|
||||||
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: v1beta8
|
version: v1beta3
|
||||||
name: registry-credential
|
name: registry-credential
|
||||||
displayName: Regestry Credentials
|
displayName: Regestry Credentials
|
||||||
description: Management Object to create regestry secrets from bitwarden
|
description: Management Object to create regestry secrets from bitwarden
|
||||||
- kind: BitwardenTemplate
|
|
||||||
version: v1beta8
|
|
||||||
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/v1beta8
|
|
||||||
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"
|
|
||||||
secretType: Obaque #Optional
|
|
||||||
namespace: "default"
|
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
annotations:
|
|
||||||
key: value
|
|
||||||
- apiVersion: lerentis.uploadfilter24.eu/v1beta8
|
|
||||||
kind: RegistryCredential
|
|
||||||
metadata:
|
|
||||||
name: test
|
|
||||||
spec:
|
|
||||||
usernameRef: "username"
|
|
||||||
passwordRef: "password"
|
|
||||||
registry: "docker.io"
|
|
||||||
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
||||||
name: "test-regcred"
|
|
||||||
namespace: "default"
|
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
annotations:
|
|
||||||
key: value
|
|
||||||
- apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
|
||||||
kind: BitwardenTemplate
|
|
||||||
metadata:
|
|
||||||
name: test
|
|
||||||
spec:
|
|
||||||
name: "test-regcred"
|
|
||||||
secretType: Obaque #Optional
|
|
||||||
namespace: "default"
|
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
annotations:
|
|
||||||
key: value
|
|
||||||
content:
|
|
||||||
- element:
|
|
||||||
filename: "config.yaml"
|
|
||||||
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", "attachment", "public_key") }}
|
|
||||||
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: changed
|
- kind: changed
|
||||||
description: "BitwardenTemplate can now handle multiple files"
|
description: "Mainternence update and image rebuild to include upstream fixes"
|
||||||
- kind: changed
|
|
||||||
description: "Removed long deprecated versions"
|
|
||||||
- kind: changed
|
|
||||||
description: "Update kubernetes from v29.0.0 to v30.1.0"
|
|
||||||
- kind: changed
|
|
||||||
description: "Update alpine from 3.20.2 to 3.20.3"
|
|
||||||
artifacthub.io/images: |
|
artifacthub.io/images: |
|
||||||
- name: bitwarden-crd-operator
|
- name: bitwarden-crd-operator
|
||||||
image: ghcr.io/lerentis/bitwarden-crd-operator:0.14.0
|
image: lerentis/bitwarden-crd-operator:0.2.1
|
||||||
|
@ -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.
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
apiVersion: apiextensions.k8s.io/v1
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
@ -13,52 +12,7 @@ spec:
|
|||||||
shortNames:
|
shortNames:
|
||||||
- bws
|
- bws
|
||||||
versions:
|
versions:
|
||||||
- name: v1beta7
|
- name: v1beta3
|
||||||
served: true
|
|
||||||
storage: false
|
|
||||||
deprecated: 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
|
|
||||||
secretType:
|
|
||||||
type: string
|
|
||||||
labels:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
annotations:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- namespace
|
|
||||||
- name
|
|
||||||
- name: v1beta8
|
|
||||||
served: true
|
served: true
|
||||||
storage: true
|
storage: true
|
||||||
schema:
|
schema:
|
||||||
@ -80,8 +34,6 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
secretRef:
|
secretRef:
|
||||||
type: string
|
type: string
|
||||||
secretScope:
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- secretName
|
- secretName
|
||||||
id:
|
id:
|
||||||
@ -90,14 +42,6 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
secretType:
|
|
||||||
type: string
|
|
||||||
labels:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
annotations:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- namespace
|
- namespace
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
---
|
|
||||||
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: v1beta7
|
|
||||||
served: true
|
|
||||||
storage: false
|
|
||||||
deprecated: true
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
filename:
|
|
||||||
type: string
|
|
||||||
template:
|
|
||||||
type: string
|
|
||||||
namespace:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretType:
|
|
||||||
type: string
|
|
||||||
labels:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
annotations:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
required:
|
|
||||||
- filename
|
|
||||||
- template
|
|
||||||
- namespace
|
|
||||||
- name
|
|
||||||
- name: v1beta8
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
namespace:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
secretType:
|
|
||||||
type: string
|
|
||||||
content:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
element:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
filename:
|
|
||||||
type: string
|
|
||||||
template:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- filename
|
|
||||||
- template
|
|
||||||
labels:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
annotations:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
required:
|
|
||||||
- namespace
|
|
||||||
- name
|
|
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
apiVersion: apiextensions.k8s.io/v1
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
@ -13,43 +12,7 @@ spec:
|
|||||||
shortNames:
|
shortNames:
|
||||||
- rgc
|
- rgc
|
||||||
versions:
|
versions:
|
||||||
- name: v1beta7
|
- name: v1beta3
|
||||||
served: true
|
|
||||||
storage: false
|
|
||||||
deprecated: 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
|
|
||||||
labels:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
annotations:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- namespace
|
|
||||||
- name
|
|
||||||
- usernameRef
|
|
||||||
- passwordRef
|
|
||||||
- registry
|
|
||||||
- name: v1beta8
|
|
||||||
served: true
|
served: true
|
||||||
storage: true
|
storage: true
|
||||||
schema:
|
schema:
|
||||||
@ -71,12 +34,6 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
labels:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
annotations:
|
|
||||||
type: object
|
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- namespace
|
- namespace
|
||||||
|
@ -4,7 +4,7 @@ metadata:
|
|||||||
name: {{ include "bitwarden-crd-operator.serviceAccountName" . }}-role
|
name: {{ include "bitwarden-crd-operator.serviceAccountName" . }}-role
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: ["lerentis.uploadfilter24.eu"]
|
- apiGroups: ["lerentis.uploadfilter24.eu"]
|
||||||
resources: ["bitwarden-secrets", "registry-credentials", "bitwarden-templates"]
|
resources: ["bitwarden-secrets", "registry-credentials"]
|
||||||
verbs: ["get", "watch", "list", "create", "delete", "patch", "update"]
|
verbs: ["get", "watch", "list", "create", "delete", "patch", "update"]
|
||||||
- apiGroups: [""]
|
- apiGroups: [""]
|
||||||
resources: ["secrets"]
|
resources: ["secrets"]
|
||||||
|
@ -8,8 +8,6 @@ spec:
|
|||||||
{{- if not .Values.autoscaling.enabled }}
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
replicas: {{ .Values.replicaCount }}
|
replicas: {{ .Values.replicaCount }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
strategy:
|
|
||||||
type: {{ .Values.deploymentStrategy }}
|
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
{{- include "bitwarden-crd-operator.selectorLabels" . | nindent 6 }}
|
{{- include "bitwarden-crd-operator.selectorLabels" . | nindent 6 }}
|
||||||
@ -52,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 }}
|
||||||
|
@ -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,13 +14,7 @@ imagePullSecrets: []
|
|||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
fullnameOverride: ""
|
fullnameOverride: ""
|
||||||
|
|
||||||
deploymentStrategy: "Recreate"
|
|
||||||
|
|
||||||
#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
|
||||||
@ -29,8 +23,6 @@ deploymentStrategy: "Recreate"
|
|||||||
# 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
|
||||||
@ -59,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
|
||||||
|
25
example.yaml
25
example.yaml
@ -1,39 +1,16 @@
|
|||||||
---
|
---
|
||||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
|
||||||
kind: BitwardenSecret
|
kind: BitwardenSecret
|
||||||
metadata:
|
metadata:
|
||||||
name: test
|
name: test
|
||||||
namespace: default
|
|
||||||
spec:
|
spec:
|
||||||
content:
|
content:
|
||||||
- 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"
|
||||||
secretType: Opaque
|
|
||||||
namespace: "default"
|
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
app: example-app
|
|
||||||
annotations:
|
|
||||||
custom.annotation: is-used
|
|
||||||
---
|
|
||||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
|
||||||
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"
|
namespace: "default"
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
apiVersion: "lerentis.uploadfilter24.eu/v1beta3"
|
||||||
kind: RegistryCredential
|
kind: RegistryCredential
|
||||||
metadata:
|
metadata:
|
||||||
name: test
|
name: test
|
||||||
@ -10,8 +10,3 @@ spec:
|
|||||||
id: "3b249ec7-9ce7-440a-9558-f34f3ab10680"
|
id: "3b249ec7-9ce7-440a-9558-f34f3ab10680"
|
||||||
name: "test-regcred"
|
name: "test-regcred"
|
||||||
namespace: "default"
|
namespace: "default"
|
||||||
labels:
|
|
||||||
namespace: default
|
|
||||||
tenant: example-team
|
|
||||||
annotations:
|
|
||||||
custom.annotation: is-used
|
|
@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta8"
|
|
||||||
kind: BitwardenTemplate
|
|
||||||
metadata:
|
|
||||||
name: test
|
|
||||||
spec:
|
|
||||||
name: "test-template"
|
|
||||||
namespace: "default"
|
|
||||||
labels:
|
|
||||||
key: value
|
|
||||||
app: example-app
|
|
||||||
annotations:
|
|
||||||
custom.annotation: is-used
|
|
||||||
content:
|
|
||||||
- element:
|
|
||||||
filename: config.yaml
|
|
||||||
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
|
|
||||||
- element:
|
|
||||||
filename: config2.yaml
|
|
||||||
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: false
|
|
@ -1,4 +1,2 @@
|
|||||||
kopf==1.37.2
|
kopf==1.35.6
|
||||||
kubernetes==30.1.0
|
kubernetes==24.2.0
|
||||||
Jinja2==3.1.4
|
|
||||||
schedule==1.2.2
|
|
@ -1,57 +0,0 @@
|
|||||||
apiVersion: skaffold/v4beta9
|
|
||||||
kind: Config
|
|
||||||
metadata:
|
|
||||||
name: bitwarden-crd-operator
|
|
||||||
build:
|
|
||||||
tagPolicy:
|
|
||||||
sha256: {}
|
|
||||||
artifacts:
|
|
||||||
- image: ghcr.io/lerentis/bitwarden-crd-operator
|
|
||||||
docker:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
deploy:
|
|
||||||
helm:
|
|
||||||
releases:
|
|
||||||
- name: bitwarden-crd-operator
|
|
||||||
chartPath: charts/bitwarden-crd-operator
|
|
||||||
valuesFiles:
|
|
||||||
- ./charts/bitwarden-crd-operator/myvalues.yaml
|
|
||||||
setValueTemplates:
|
|
||||||
image.repository: "{{.IMAGE_REPO_ghcr_io_lerentis_bitwarden_crd_operator}}"
|
|
||||||
image.tag: "{{.IMAGE_TAG_ghcr_io_lerentis_bitwarden_crd_operator}}@{{.IMAGE_DIGEST_ghcr_io_lerentis_bitwarden_crd_operator}}"
|
|
||||||
hooks:
|
|
||||||
after:
|
|
||||||
- host:
|
|
||||||
command:
|
|
||||||
- kubectl
|
|
||||||
- apply
|
|
||||||
- -f
|
|
||||||
- ./example*.yaml
|
|
||||||
- host:
|
|
||||||
command:
|
|
||||||
- sleep
|
|
||||||
- '5'
|
|
||||||
- host:
|
|
||||||
command:
|
|
||||||
- kubectl
|
|
||||||
- get
|
|
||||||
- secret
|
|
||||||
- test-regcred
|
|
||||||
- host:
|
|
||||||
command:
|
|
||||||
- kubectl
|
|
||||||
- get
|
|
||||||
- secret
|
|
||||||
- test-scope
|
|
||||||
- host:
|
|
||||||
command:
|
|
||||||
- kubectl
|
|
||||||
- get
|
|
||||||
- secret
|
|
||||||
- test-secret
|
|
||||||
- host:
|
|
||||||
command:
|
|
||||||
- kubectl
|
|
||||||
- get
|
|
||||||
- secret
|
|
||||||
- test-template
|
|
@ -1,47 +1,33 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
|
||||||
import kopf
|
import kopf
|
||||||
import schedule
|
import os
|
||||||
import time
|
import subprocess
|
||||||
import threading
|
|
||||||
|
|
||||||
from utils.utils import command_wrapper, unlock_bw, sync_bw
|
def get_secret_from_bitwarden(logger, id):
|
||||||
|
logger.info(f"Locking up secret with ID: {id}")
|
||||||
|
return command_wrapper(logger, f"get item {id}")
|
||||||
|
|
||||||
|
def unlock_bw(logger):
|
||||||
|
token_output = command_wrapper(logger, "unlock --passwordenv BW_PASSWORD")
|
||||||
|
tokens = token_output.split('"')[1::2]
|
||||||
|
os.environ["BW_SESSION"] = tokens[1]
|
||||||
|
logger.info("Signin successful. Session exported")
|
||||||
|
|
||||||
|
def command_wrapper(logger, command):
|
||||||
|
system_env = dict(os.environ)
|
||||||
|
sp = subprocess.Popen([f"bw {command}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=True, env=system_env)
|
||||||
|
out, err = sp.communicate()
|
||||||
|
if err:
|
||||||
|
logger.warn(f"Error during bw cli invokement: {err}")
|
||||||
|
return out.decode(encoding='UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
|
||||||
command_wrapper(logger, f"config server {os.getenv('BW_HOST')}")
|
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:
|
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(logger, "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()
|
|
||||||
|
@ -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 bitwardenCrdOperator 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,21 +15,15 @@ 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-credentials.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')
|
||||||
password_ref = spec.get('passwordRef')
|
password_ref = spec.get('passwordRef')
|
||||||
@ -44,144 +31,32 @@ 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')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
|
|
||||||
unlock_bw(logger)
|
unlock_bw(logger)
|
||||||
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(logger, id))
|
||||||
|
|
||||||
api = kubernetes.client.CoreV1Api()
|
api = kubernetes.client.CoreV1Api()
|
||||||
|
|
||||||
annotations = {
|
annotations = {
|
||||||
"managed": "registry-credential.lerentis.uploadfilter24.eu",
|
"managed": "registry-credentials.lerentis.uploadfilter24.eu",
|
||||||
"managedObject": f"{namespace}/{name}"
|
"managedObject": f"{namespace}/{name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
obj = api.create_namespaced_secret(
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
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-credentials.lerentis.uploadfilter24.eu')
|
||||||
|
def my_handler(spec, old, new, diff, **_):
|
||||||
|
pass
|
||||||
|
|
||||||
@kopf.on.update('registry-credential.lerentis.uploadfilter24.eu')
|
@kopf.on.delete('registry-credentials.lerentis.uploadfilter24.eu')
|
||||||
@kopf.timer('registry-credential.lerentis.uploadfilter24.eu', interval=bw_sync_interval)
|
|
||||||
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')
|
|
||||||
labels = spec.get('labels')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
|
|
||||||
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}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not labels:
|
|
||||||
labels = {}
|
|
||||||
|
|
||||||
secret = kubernetes.client.V1Secret()
|
|
||||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
|
||||||
name=secret_name, annotations=annotations, labels=labels)
|
|
||||||
secret = create_dockerlogin(
|
|
||||||
logger,
|
|
||||||
secret,
|
|
||||||
secret_json_object["data"],
|
|
||||||
username_ref,
|
|
||||||
password_ref,
|
|
||||||
registry)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
try:
|
|
||||||
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 as e:
|
|
||||||
logger.warn(
|
|
||||||
f"Could not update secret {secret_namespace}/{secret_name}!")
|
|
||||||
logger.warn(
|
|
||||||
f"Exception: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
secret_name = spec.get('name')
|
secret_name = spec.get('name')
|
||||||
secret_namespace = spec.get('namespace')
|
secret_namespace = spec.get('namespace')
|
||||||
@ -189,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}!")
|
|
||||||
|
172
src/kv.py
172
src/kv.py
@ -3,9 +3,10 @@ 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, get_attachment, bw_sync_interval
|
from bitwardenCrdOperator import unlock_bw, get_secret_from_bitwarden
|
||||||
|
|
||||||
def create_kv(logger, id, secret, secret_json, content_def):
|
def create_kv(secret, secret_json, content_def):
|
||||||
|
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():
|
||||||
@ -14,175 +15,42 @@ def create_kv(logger, id, secret, secret_json, content_def):
|
|||||||
_secret_key = value
|
_secret_key = value
|
||||||
if key == "secretRef":
|
if key == "secretRef":
|
||||||
_secret_ref = value
|
_secret_ref = value
|
||||||
if key == "secretScope":
|
secret.data[_secret_ref] = str(base64.b64encode(secret_json["login"][_secret_key].encode("utf-8")), "utf-8")
|
||||||
_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")
|
|
||||||
if _secret_scope == "attachment":
|
|
||||||
value = get_attachment(logger, id, _secret_key)
|
|
||||||
if value is None:
|
|
||||||
raise Exception(
|
|
||||||
f"Attachment {_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-secrets.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):
|
||||||
|
|
||||||
content_def = body['spec']['content']
|
content_def = body['spec']['content']
|
||||||
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')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
custom_secret_type = spec.get('secretType')
|
|
||||||
|
|
||||||
unlock_bw(logger)
|
unlock_bw(logger)
|
||||||
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(logger, id))
|
||||||
|
|
||||||
api = kubernetes.client.CoreV1Api()
|
api = kubernetes.client.CoreV1Api()
|
||||||
|
|
||||||
annotations = {
|
annotations = {
|
||||||
"managed": "bitwarden-secret.lerentis.uploadfilter24.eu",
|
"managed": "bitwarden-secrets.lerentis.uploadfilter24.eu",
|
||||||
"managedObject": f"{namespace}/{name}"
|
"managedObject": f"{namespace}/{name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not custom_secret_type:
|
|
||||||
custom_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
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.type = custom_secret_type
|
|
||||||
secret = create_kv(logger, id, secret, secret_json_object, content_def)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
obj = api.create_namespaced_secret(
|
||||||
# Is not in the same namespace as the generated secret
|
secret_namespace, secret
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
api.create_namespaced_secret(
|
|
||||||
namespace="{}".format(secret_namespace),
|
|
||||||
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-secrets.lerentis.uploadfilter24.eu')
|
||||||
|
def my_handler(spec, old, new, diff, **_):
|
||||||
|
pass
|
||||||
|
|
||||||
@kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu')
|
@kopf.on.delete('bitwarden-secrets.lerentis.uploadfilter24.eu')
|
||||||
@kopf.timer('bitwarden-secret.lerentis.uploadfilter24.eu', interval=bw_sync_interval)
|
|
||||||
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
|
|
||||||
old_secret_type = 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')
|
|
||||||
old_secret_type = old_config['spec'].get('secretType')
|
|
||||||
secret_name = spec.get('name')
|
|
||||||
secret_namespace = spec.get('namespace')
|
|
||||||
labels = spec.get('labels')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
custom_secret_type = spec.get('secretType')
|
|
||||||
|
|
||||||
if not custom_secret_type:
|
|
||||||
custom_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
if not old_secret_type:
|
|
||||||
old_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
if old_config is not None and (
|
|
||||||
old_secret_name != secret_name or old_secret_namespace != secret_namespace or old_secret_type != custom_secret_type):
|
|
||||||
# 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, namespace or type 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}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not labels:
|
|
||||||
labels = {}
|
|
||||||
|
|
||||||
secret = kubernetes.client.V1Secret()
|
|
||||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
|
||||||
name=secret_name, annotations=annotations, labels=labels)
|
|
||||||
secret.type = custom_secret_type
|
|
||||||
secret = create_kv(logger, id, secret, secret_json_object, content_def)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
try:
|
|
||||||
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 as e:
|
|
||||||
logger.warn(
|
|
||||||
f"Could not update secret {secret_namespace}/{secret_name}!")
|
|
||||||
logger.warn(
|
|
||||||
f"Exception: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
secret_name = spec.get('name')
|
secret_name = spec.get('name')
|
||||||
secret_namespace = spec.get('namespace')
|
secret_namespace = spec.get('namespace')
|
||||||
@ -190,8 +58,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}!")
|
|
@ -1,16 +0,0 @@
|
|||||||
from utils.utils import get_secret_from_bitwarden, get_attachment, parse_fields_scope, parse_login_scope
|
|
||||||
|
|
||||||
|
|
||||||
class BitwardenLookupHandler:
|
|
||||||
|
|
||||||
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":
|
|
||||||
return parse_login_scope(_secret_json, field)
|
|
||||||
if scope == "fields":
|
|
||||||
return parse_fields_scope(_secret_json, field)
|
|
336
src/template.py
336
src/template.py
@ -1,336 +0,0 @@
|
|||||||
import kopf
|
|
||||||
import base64
|
|
||||||
import kubernetes
|
|
||||||
import json
|
|
||||||
|
|
||||||
from utils.utils import unlock_bw, bw_sync_interval
|
|
||||||
from lookups.bitwarden_lookup import BitwardenLookupHandler
|
|
||||||
from jinja2 import Environment, BaseLoader
|
|
||||||
|
|
||||||
|
|
||||||
def render_template(logger, template):
|
|
||||||
jinja_template = Environment(loader=BaseLoader()).from_string(template)
|
|
||||||
jinja_template.globals.update({
|
|
||||||
"bitwarden_lookup": BitwardenLookupHandler(logger).bitwarden_lookup,
|
|
||||||
})
|
|
||||||
return jinja_template.render()
|
|
||||||
|
|
||||||
|
|
||||||
def create_template_secret(logger, secret, filename, template):
|
|
||||||
secret.data = {}
|
|
||||||
secret.data[filename] = str(
|
|
||||||
base64.b64encode(
|
|
||||||
render_template(logger, template).encode("utf-8")),
|
|
||||||
"utf-8")
|
|
||||||
return secret
|
|
||||||
|
|
||||||
def create_template_obj(logger, secret, content_def):
|
|
||||||
secret.data = {}
|
|
||||||
for eleml in content_def:
|
|
||||||
for k, elem in eleml.items():
|
|
||||||
for key, value in elem.items():
|
|
||||||
if key == "filename":
|
|
||||||
_file_name = value
|
|
||||||
if key == "template":
|
|
||||||
_template = value
|
|
||||||
secret.data[_file_name] = str(
|
|
||||||
base64.b64encode(
|
|
||||||
render_template(logger, _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')
|
|
||||||
if template is not None:
|
|
||||||
create_beta7_secret(spec, name, namespace, logger, body, **kwargs)
|
|
||||||
secret_name = spec.get('name')
|
|
||||||
secret_namespace = spec.get('namespace')
|
|
||||||
custom_secret_type = spec.get('secretType')
|
|
||||||
labels = spec.get('labels')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
content_def = spec.get('content')
|
|
||||||
|
|
||||||
unlock_bw(logger)
|
|
||||||
|
|
||||||
api = kubernetes.client.CoreV1Api()
|
|
||||||
|
|
||||||
annotations = {
|
|
||||||
"managed": "bitwarden-template.lerentis.uploadfilter24.eu",
|
|
||||||
"managedObject": f"{namespace}/{name}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not custom_secret_type:
|
|
||||||
custom_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
if not labels:
|
|
||||||
labels = {}
|
|
||||||
|
|
||||||
secret = kubernetes.client.V1Secret()
|
|
||||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
|
||||||
name=secret_name, annotations=annotations, labels=labels)
|
|
||||||
secret.type = custom_secret_type
|
|
||||||
secret = create_template_obj(logger, secret, content_def)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
api.create_namespaced_secret(
|
|
||||||
namespace="{}".format(secret_namespace),
|
|
||||||
body=secret
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Secret {secret_namespace}/{secret_name} has been created")
|
|
||||||
|
|
||||||
|
|
||||||
def create_beta7_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')
|
|
||||||
labels = spec.get('labels')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
custom_secret_type = spec.get('secretType')
|
|
||||||
|
|
||||||
unlock_bw(logger)
|
|
||||||
|
|
||||||
api = kubernetes.client.CoreV1Api()
|
|
||||||
|
|
||||||
annotations = {
|
|
||||||
"managed": "bitwarden-template.lerentis.uploadfilter24.eu",
|
|
||||||
"managedObject": f"{namespace}/{name}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not custom_secret_type:
|
|
||||||
custom_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
if not labels:
|
|
||||||
labels = {}
|
|
||||||
|
|
||||||
secret = kubernetes.client.V1Secret()
|
|
||||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
|
||||||
name=secret_name, annotations=annotations, labels=labels)
|
|
||||||
secret.type = custom_secret_type
|
|
||||||
secret = create_template_secret(logger, secret, filename, template)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
api.create_namespaced_secret(
|
|
||||||
secret_namespace, secret
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Secret {secret_namespace}/{secret_name} has been created")
|
|
||||||
|
|
||||||
def update_beta7_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')
|
|
||||||
labels = spec.get('labels')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
custom_secret_type = spec.get('secretType')
|
|
||||||
|
|
||||||
if not custom_secret_type:
|
|
||||||
custom_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
old_config = None
|
|
||||||
old_secret_name = None
|
|
||||||
old_secret_namespace = None
|
|
||||||
old_secret_type = 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')
|
|
||||||
old_secret_type = old_config['spec'].get('secretType')
|
|
||||||
secret_name = spec.get('name')
|
|
||||||
secret_namespace = spec.get('namespace')
|
|
||||||
|
|
||||||
if not old_secret_type:
|
|
||||||
old_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
if old_config is not None and (
|
|
||||||
old_secret_name != secret_name or old_secret_namespace != secret_namespace or old_secret_type != custom_secret_type):
|
|
||||||
# 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}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not labels:
|
|
||||||
labels = {}
|
|
||||||
|
|
||||||
secret = kubernetes.client.V1Secret()
|
|
||||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
|
||||||
name=secret_name, annotations=annotations, labels=labels)
|
|
||||||
secret.type = custom_secret_type
|
|
||||||
secret = create_template_secret(logger, secret, filename, template)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
try:
|
|
||||||
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 as e:
|
|
||||||
logger.warn(
|
|
||||||
f"Could not update secret {secret_namespace}/{secret_name}!")
|
|
||||||
logger.warn(
|
|
||||||
f"Exception: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@kopf.on.update('bitwarden-template.lerentis.uploadfilter24.eu')
|
|
||||||
@kopf.timer('bitwarden-template.lerentis.uploadfilter24.eu', interval=bw_sync_interval)
|
|
||||||
def update_managed_secret(
|
|
||||||
spec,
|
|
||||||
status,
|
|
||||||
name,
|
|
||||||
namespace,
|
|
||||||
logger,
|
|
||||||
body,
|
|
||||||
**kwargs):
|
|
||||||
|
|
||||||
template = spec.get('template')
|
|
||||||
if template is not None:
|
|
||||||
update_beta7_secret(spec, status, name, namespace, logger, body, **kwargs)
|
|
||||||
secret_name = spec.get('name')
|
|
||||||
secret_namespace = spec.get('namespace')
|
|
||||||
labels = spec.get('labels')
|
|
||||||
custom_annotations = spec.get('annotations')
|
|
||||||
custom_secret_type = spec.get('secretType')
|
|
||||||
content_def = spec.get('content')
|
|
||||||
|
|
||||||
if not custom_secret_type:
|
|
||||||
custom_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
old_config = None
|
|
||||||
old_secret_name = None
|
|
||||||
old_secret_namespace = None
|
|
||||||
old_secret_type = 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')
|
|
||||||
old_secret_type = old_config['spec'].get('secretType')
|
|
||||||
secret_name = spec.get('name')
|
|
||||||
secret_namespace = spec.get('namespace')
|
|
||||||
|
|
||||||
if not old_secret_type:
|
|
||||||
old_secret_type = 'Opaque'
|
|
||||||
|
|
||||||
if old_config is not None and (
|
|
||||||
old_secret_name != secret_name or old_secret_namespace != secret_namespace or old_secret_type != custom_secret_type):
|
|
||||||
# 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}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if custom_annotations:
|
|
||||||
annotations.update(custom_annotations)
|
|
||||||
|
|
||||||
if not labels:
|
|
||||||
labels = {}
|
|
||||||
|
|
||||||
secret = kubernetes.client.V1Secret()
|
|
||||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
|
||||||
name=secret_name, annotations=annotations, labels=labels)
|
|
||||||
secret.type = custom_secret_type
|
|
||||||
secret = create_template_obj(logger, secret, content_def)
|
|
||||||
|
|
||||||
# Garbage collection will delete the generated secret if the owner
|
|
||||||
# Is not in the same namespace as the generated secret
|
|
||||||
if secret_namespace == namespace:
|
|
||||||
kopf.append_owner_reference(secret)
|
|
||||||
|
|
||||||
try:
|
|
||||||
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 as e:
|
|
||||||
logger.warn(
|
|
||||||
f"Could not update secret {secret_namespace}/{secret_name}!")
|
|
||||||
logger.warn(
|
|
||||||
f"Exception: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@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}!")
|
|
@ -1,91 +0,0 @@
|
|||||||
import os
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import distutils
|
|
||||||
|
|
||||||
bw_sync_interval = float(os.environ.get(
|
|
||||||
'BW_SYNC_INTERVAL', 900))
|
|
||||||
|
|
||||||
class BitwardenCommandException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_secret_from_bitwarden(logger, id, force_sync=False):
|
|
||||||
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):
|
|
||||||
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, raw: bool = False):
|
|
||||||
system_env = dict(os.environ)
|
|
||||||
response_flag = "--raw" if raw else "--response"
|
|
||||||
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()
|
|
||||||
if err:
|
|
||||||
logger.warn(err)
|
|
||||||
return None
|
|
||||||
if raw:
|
|
||||||
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):
|
|
||||||
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']
|
|
Loading…
Reference in New Issue
Block a user