Compare commits
64 Commits
25ebf35835
...
a2186ab3aa
Author | SHA1 | Date | |
---|---|---|---|
|
a2186ab3aa | ||
9f4264d355 | |||
620d0f0b18 | |||
ac0bc2d89d | |||
f45e9ed6a4 | |||
1d147aad9a | |||
|
e31899b7f2 | ||
|
116c1d9c4e | ||
|
0e08d3cc5e | ||
|
a58f4762d9 | ||
|
f287bd80a1 | ||
|
17fdd4a977 | ||
|
2fde884ad5 | ||
|
cf186e84fb | ||
|
bf5a0b2484 | ||
|
3ba8f49b39 | ||
|
6a8945af21 | ||
|
6160723a72 | ||
|
7527163f26 | ||
|
c6ee9fdc39 | ||
|
313cf7d6e9 | ||
|
8649c4e865 | ||
|
23037bafc1 | ||
|
be98eb9b88 | ||
|
80d8db6924 | ||
|
d6c207ba82 | ||
|
fc37a12737 | ||
|
58b990db2a | ||
|
f3cba82c9f | ||
|
f7a0f43cab | ||
|
382b6776ce | ||
|
94bc6b10b1 | ||
53dae0aaaf | |||
41a085c475 | |||
|
70546b7484 | ||
|
ae5b39bbcb | ||
|
02dfca5a44 | ||
|
31cba57a1a | ||
|
f0a9258b71 | ||
|
63e6f8ab7b | ||
|
9fe5bde4e8 | ||
|
7e0a5b6b57 | ||
|
b7ef2480be | ||
|
25a825b712 | ||
|
963446d9dc | ||
|
bd000cc23a | ||
|
72bb525e9a | ||
|
0bb67e4503 | ||
|
3f35179983 | ||
|
68ffb94870 | ||
|
ddf13aae1c | ||
|
f63e0ac090 | ||
|
39a49ab95b | ||
|
187da26b30 | ||
|
62a2b488d2 | ||
|
bec7476ace | ||
|
d629fa600f | ||
|
ba8c35da9f | ||
|
e85ea8357a | ||
|
69d1af8ba5 | ||
|
293ac2a0b0 | ||
|
5c8d10b060 | ||
|
57b6d69b6b | ||
|
0e33c33415 |
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -29,34 +29,34 @@ jobs:
|
||||
version: v3.10.0
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.5.0
|
||||
uses: helm/chart-releaser-action@v1.6.0
|
||||
with:
|
||||
charts_dir: charts
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Get app version from chart
|
||||
uses: mikefarah/yq@v4.34.2
|
||||
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@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: lerentis
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: "GHCR Build and Push"
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@ -77,7 +77,7 @@ jobs:
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v1
|
||||
|
||||
- name: Download SBOM from github action
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ANCHORE_SBOM_ACTION_PRIOR_ARTIFACT }}
|
||||
|
||||
|
12
.github/workflows/test-and-lint.yml
vendored
12
.github/workflows/test-and-lint.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -16,13 +16,13 @@ jobs:
|
||||
with:
|
||||
version: v3.11.2
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
check-latest: true
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.4.0
|
||||
uses: helm/chart-testing-action@v2.6.1
|
||||
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
@ -40,14 +40,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: "GHCR Build"
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
12
Dockerfile
12
Dockerfile
@ -1,13 +1,13 @@
|
||||
FROM alpine:3.18.0
|
||||
FROM alpine:3.19.0
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/Lerentis/bitwarden-crd-operator
|
||||
LABEL org.opencontainers.image.description="Kubernetes Operator to create k8s secrets from bitwarden"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
|
||||
ARG PYTHON_VERSION=3.11.4-r0
|
||||
ARG PIP_VERSION=23.1.2-r0
|
||||
ARG GCOMPAT_VERSION=1.1.0-r1
|
||||
ARG LIBCRYPTO_VERSION=3.1.0-r4
|
||||
ARG PYTHON_VERSION=3.11.6-r1
|
||||
ARG PIP_VERSION=23.3.1-r0
|
||||
ARG GCOMPAT_VERSION=1.1.0-r4
|
||||
ARG LIBCRYPTO_VERSION=3.1.4-r2
|
||||
ARG BW_VERSION=2023.1.0
|
||||
|
||||
COPY requirements.txt /requirements.txt
|
||||
@ -38,7 +38,7 @@ RUN set -eux; \
|
||||
mkdir -p /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}; \
|
||||
pip install -r /requirements.txt --no-warn-script-location; \
|
||||
pip install -r /requirements.txt --no-warn-script-location --break-system-packages; \
|
||||
rm /requirements.txt; \
|
||||
apk del --purge gcc musl-dev libstdc++;
|
||||
|
||||
|
27
Makefile
Normal file
27
Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
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}
|
44
README.md
44
README.md
@ -56,7 +56,7 @@ And you are set to create your first secret using this operator. For that you ne
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: BitwardenSecret
|
||||
metadata:
|
||||
name: name-of-your-management-object
|
||||
@ -73,6 +73,8 @@ spec:
|
||||
id: "A Secret ID from bitwarden"
|
||||
name: "Name of the secret to be created"
|
||||
namespace: "Namespace of the secret to be created"
|
||||
labels: # 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:
|
||||
@ -87,6 +89,8 @@ metadata:
|
||||
annotations:
|
||||
managed: bitwarden-secrets.lerentis.uploadfilter24.eu
|
||||
managedObject: bw-operator/test
|
||||
labels:
|
||||
key: value
|
||||
name: name-of-your-management-object
|
||||
namespace: default
|
||||
type: Opaque
|
||||
@ -98,7 +102,7 @@ For managing registry credentials, or pull secrets, you can create another kind
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: RegistryCredential
|
||||
metadata:
|
||||
name: name-of-your-management-object
|
||||
@ -109,6 +113,8 @@ spec:
|
||||
id: "A Secret ID from bitwarden"
|
||||
name: "Name of the secret to be created"
|
||||
namespace: "Namespace of the secret to be created"
|
||||
labels: # Optional
|
||||
key: value
|
||||
```
|
||||
|
||||
The resulting secret looks something like this:
|
||||
@ -122,6 +128,8 @@ metadata:
|
||||
annotations:
|
||||
managed: bitwarden-secrets.lerentis.uploadfilter24.eu
|
||||
managedObject: bw-operator/test
|
||||
labels:
|
||||
key: value
|
||||
name: name-of-your-management-object
|
||||
namespace: default
|
||||
type: dockerconfigjson
|
||||
@ -129,11 +137,11 @@ type: dockerconfigjson
|
||||
|
||||
## BitwardenTemplate
|
||||
|
||||
One of the more freely defined types that can be used with this operator you can just pass a whole template:
|
||||
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:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: BitwardenTemplate
|
||||
metadata:
|
||||
name: name-of-your-management-object
|
||||
@ -141,15 +149,17 @@ 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"
|
||||
labels: # Optional
|
||||
key: value
|
||||
template: |
|
||||
---
|
||||
api:
|
||||
enabled: True
|
||||
key: {{ bitwarden_lookup("A Secret ID from bitwarden", "login or fields", "name of a field in bitwarden") }}
|
||||
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", "name of a field in bitwarden") }}
|
||||
pubkey: {{ bitwarden_lookup("A Secret ID from bitwarden", "login or fields or attachment", "name of a field in bitwarden") }}
|
||||
enabled: true
|
||||
```
|
||||
|
||||
@ -164,17 +174,25 @@ 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
|
||||
```
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
## Short Term Roadmap
|
||||
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.
|
||||
|
||||
- [ ] support more types
|
||||
- [x] offer option to use a existing secret in helm chart
|
||||
- [x] host chart on gh pages
|
||||
- [x] write release pipeline
|
||||
- [x] maybe extend spec to offer modification of keys as well
|
||||
## 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.
|
||||
|
@ -4,9 +4,9 @@ description: Deploy the Bitwarden CRD Operator
|
||||
|
||||
type: application
|
||||
|
||||
version: "v0.7.4"
|
||||
version: "v0.11.0"
|
||||
|
||||
appVersion: "0.6.4"
|
||||
appVersion: "0.10.0"
|
||||
|
||||
keywords:
|
||||
- operator
|
||||
@ -32,22 +32,22 @@ annotations:
|
||||
url: https://github.com/Lerentis/bitwarden-crd-operator
|
||||
artifacthub.io/crds: |
|
||||
- kind: BitwardenSecret
|
||||
version: v1beta4
|
||||
version: v1beta5
|
||||
name: bitwarden-secret
|
||||
displayName: Bitwarden Secret
|
||||
description: Management Object to create secrets from bitwarden
|
||||
- kind: RegistryCredential
|
||||
version: v1beta4
|
||||
version: v1beta5
|
||||
name: registry-credential
|
||||
displayName: Regestry Credentials
|
||||
description: Management Object to create regestry secrets from bitwarden
|
||||
- kind: BitwardenTemplate
|
||||
version: v1beta1
|
||||
version: v1beta5
|
||||
name: bitwarden-template
|
||||
displayName: Bitwarden Template
|
||||
description: Management Object to create secrets from a jinja template with a bitwarden lookup
|
||||
artifacthub.io/crdsExamples: |
|
||||
- apiVersion: lerentis.uploadfilter24.eu/v1beta4
|
||||
- apiVersion: lerentis.uploadfilter24.eu/v1beta5
|
||||
kind: BitwardenSecret
|
||||
metadata:
|
||||
name: test
|
||||
@ -62,7 +62,9 @@ annotations:
|
||||
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
name: "test-secret"
|
||||
namespace: "default"
|
||||
- apiVersion: lerentis.uploadfilter24.eu/v1beta4
|
||||
labels:
|
||||
key: value
|
||||
- apiVersion: lerentis.uploadfilter24.eu/v1beta5
|
||||
kind: RegistryCredential
|
||||
metadata:
|
||||
name: test
|
||||
@ -73,7 +75,9 @@ annotations:
|
||||
id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
name: "test-regcred"
|
||||
namespace: "default"
|
||||
- apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
labels:
|
||||
key: value
|
||||
- apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: BitwardenTemplate
|
||||
metadata:
|
||||
name: test
|
||||
@ -81,6 +85,8 @@ annotations:
|
||||
filename: "config.yaml"
|
||||
name: "test-regcred"
|
||||
namespace: "default"
|
||||
labels:
|
||||
key: value
|
||||
template: |
|
||||
---
|
||||
api:
|
||||
@ -89,14 +95,24 @@ annotations:
|
||||
allowCrossOrigin: false
|
||||
apps:
|
||||
"some.app.identifier:some_version":
|
||||
pubkey: {{ bitwarden_lookup("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "fields", "public_key") }}
|
||||
pubkey: {{ bitwarden_lookup("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "attachment", "public_key") }}
|
||||
enabled: true
|
||||
artifacthub.io/license: MIT
|
||||
artifacthub.io/operator: "true"
|
||||
artifacthub.io/containsSecurityUpdates: "false"
|
||||
artifacthub.io/changes: |
|
||||
- kind: fixed
|
||||
description: "Fixed bitwarden installation"
|
||||
- kind: changed
|
||||
description: "Added the possibility to add labels to generated secrets"
|
||||
- kind: changed
|
||||
description: "Updated Alpine to 3.19.0"
|
||||
- kind: changed
|
||||
description: "Update Python to 3.11.6-r1"
|
||||
- kind: changed
|
||||
description: "Update pip to 23.3.1-r0"
|
||||
- kind: changed
|
||||
description: "Update gcompat to 1.1.0-r4"
|
||||
- kind: changed
|
||||
description: "Update libcrypto3 to 3.1.4-r2"
|
||||
artifacthub.io/images: |
|
||||
- name: bitwarden-crd-operator
|
||||
image: ghcr.io/lerentis/bitwarden-crd-operator:0.6.4
|
||||
image: ghcr.io/lerentis/bitwarden-crd-operator:0.10.0
|
||||
|
@ -15,7 +15,8 @@ spec:
|
||||
versions:
|
||||
- name: v1beta4
|
||||
served: true
|
||||
storage: true
|
||||
storage: false
|
||||
deprecated: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
@ -49,3 +50,42 @@ spec:
|
||||
- id
|
||||
- namespace
|
||||
- name
|
||||
- name: v1beta5
|
||||
served: true
|
||||
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
|
||||
labels:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- id
|
||||
- namespace
|
||||
- name
|
||||
|
@ -15,7 +15,8 @@ spec:
|
||||
versions:
|
||||
- name: v1beta4
|
||||
served: true
|
||||
storage: true
|
||||
storage: false
|
||||
deprecated: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
@ -36,3 +37,29 @@ spec:
|
||||
- template
|
||||
- namespace
|
||||
- name
|
||||
- name: v1beta5
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
properties:
|
||||
filename:
|
||||
type: string
|
||||
template:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
labels:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- filename
|
||||
- template
|
||||
- namespace
|
||||
- name
|
||||
|
@ -15,7 +15,8 @@ spec:
|
||||
versions:
|
||||
- name: v1beta4
|
||||
served: true
|
||||
storage: true
|
||||
storage: false
|
||||
deprecated: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
@ -42,3 +43,35 @@ spec:
|
||||
- usernameRef
|
||||
- passwordRef
|
||||
- registry
|
||||
- name: v1beta5
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
properties:
|
||||
usernameRef:
|
||||
type: string
|
||||
passwordRef:
|
||||
type: string
|
||||
registry:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
labels:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- id
|
||||
- namespace
|
||||
- name
|
||||
- usernameRef
|
||||
- passwordRef
|
||||
- registry
|
||||
|
@ -50,10 +50,20 @@ spec:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
|
||||
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
|
||||
successThreshold: {{ .Values.livenessProbe.successThreshold }}
|
||||
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
|
||||
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
|
||||
successThreshold: {{ .Values.readinessProbe.successThreshold }}
|
||||
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
@ -15,6 +15,10 @@ nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# env:
|
||||
# - name: BW_FORCE_SYNC
|
||||
# value: "false"
|
||||
# - name: BW_SYNC_INTERVAL
|
||||
# value: "900"
|
||||
# - name: BW_HOST
|
||||
# value: "define_it"
|
||||
# - name: BW_CLIENTID
|
||||
@ -23,6 +27,8 @@ fullnameOverride: ""
|
||||
# value: "define_it"
|
||||
# - name: BW_PASSWORD
|
||||
# value: "define_id"
|
||||
## - name: BW_RELOGIN_INTERVAL
|
||||
## value: "3600"
|
||||
|
||||
externalConfigSecret:
|
||||
enabled: false
|
||||
@ -51,6 +57,20 @@ securityContext: {}
|
||||
# runAsNonRoot: true
|
||||
# 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: {}
|
||||
# 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: BitwardenSecret
|
||||
metadata:
|
||||
name: test
|
||||
@ -16,8 +16,11 @@ spec:
|
||||
id: "88781348-c81c-4367-9801-550360c21295"
|
||||
name: "test-secret"
|
||||
namespace: "default"
|
||||
labels:
|
||||
key: value
|
||||
app: example-app
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: BitwardenSecret
|
||||
metadata:
|
||||
name: test-scope
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: RegistryCredential
|
||||
metadata:
|
||||
name: test
|
||||
@ -9,4 +9,7 @@ spec:
|
||||
registry: "docker.io"
|
||||
id: "3b249ec7-9ce7-440a-9558-f34f3ab10680"
|
||||
name: "test-regcred"
|
||||
namespace: "default"
|
||||
namespace: "default"
|
||||
labels:
|
||||
namespace: default
|
||||
tenant: example-team
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta4"
|
||||
apiVersion: "lerentis.uploadfilter24.eu/v1beta5"
|
||||
kind: BitwardenTemplate
|
||||
metadata:
|
||||
name: test
|
||||
@ -7,6 +7,9 @@ spec:
|
||||
filename: "config.yaml"
|
||||
name: "test-template"
|
||||
namespace: "default"
|
||||
labels:
|
||||
key: value
|
||||
app: example-app
|
||||
template: |
|
||||
---
|
||||
api:
|
||||
|
@ -1,3 +1,4 @@
|
||||
kopf==1.36.1
|
||||
kopf==1.36.2
|
||||
kubernetes==26.1.0
|
||||
Jinja2==3.1.2
|
||||
schedule==1.2.1
|
17
skaffold.yaml
Normal file
17
skaffold.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
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
|
@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import kopf
|
||||
import schedule
|
||||
import time
|
||||
import threading
|
||||
|
||||
from utils.utils import command_wrapper, unlock_bw
|
||||
from utils.utils import command_wrapper, unlock_bw, sync_bw
|
||||
|
||||
|
||||
@kopf.on.startup()
|
||||
def bitwarden_signin(logger, **kwargs):
|
||||
if 'BW_HOST' in os.environ:
|
||||
try:
|
||||
@ -18,3 +19,29 @@ def bitwarden_signin(logger, **kwargs):
|
||||
logger.info("BW_HOST not set. Assuming SaaS installation")
|
||||
command_wrapper(logger, "login --apikey")
|
||||
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,7 +3,7 @@ import kubernetes
|
||||
import base64
|
||||
import json
|
||||
|
||||
from utils.utils import unlock_bw, get_secret_from_bitwarden
|
||||
from utils.utils import unlock_bw, get_secret_from_bitwarden, bw_sync_interval
|
||||
|
||||
|
||||
def create_dockerlogin(
|
||||
@ -13,7 +13,7 @@ def create_dockerlogin(
|
||||
username_ref,
|
||||
password_ref,
|
||||
registry):
|
||||
secret.type = "dockerconfigjson"
|
||||
secret.type = "kubernetes.io/dockerconfigjson"
|
||||
secret.data = {}
|
||||
auths_dict = {}
|
||||
registry_dict = {}
|
||||
@ -26,7 +26,8 @@ def create_dockerlogin(
|
||||
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
|
||||
registry_dict[registry] = reg_auth_dict
|
||||
auths_dict["auths"] = registry_dict
|
||||
@ -43,6 +44,7 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
|
||||
id = spec.get('id')
|
||||
secret_name = spec.get('name')
|
||||
secret_namespace = spec.get('namespace')
|
||||
labels = spec.get('labels')
|
||||
|
||||
unlock_bw(logger)
|
||||
logger.info(f"Locking up secret with ID: {id}")
|
||||
@ -54,9 +56,13 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
|
||||
"managed": "registry-credential.lerentis.uploadfilter24.eu",
|
||||
"managedObject": f"{namespace}/{name}"
|
||||
}
|
||||
|
||||
if not labels:
|
||||
labels = {}
|
||||
|
||||
secret = kubernetes.client.V1Secret()
|
||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
||||
name=secret_name, annotations=annotations)
|
||||
name=secret_name, annotations=annotations, labels=labels)
|
||||
secret = create_dockerlogin(
|
||||
logger,
|
||||
secret,
|
||||
@ -65,7 +71,7 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
|
||||
password_ref,
|
||||
registry)
|
||||
|
||||
obj = api.create_namespaced_secret(
|
||||
api.create_namespaced_secret(
|
||||
secret_namespace, secret
|
||||
)
|
||||
|
||||
@ -74,7 +80,7 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs):
|
||||
|
||||
|
||||
@kopf.on.update('registry-credential.lerentis.uploadfilter24.eu')
|
||||
@kopf.timer('registry-credential.lerentis.uploadfilter24.eu', interval=900)
|
||||
@kopf.timer('registry-credential.lerentis.uploadfilter24.eu', interval=bw_sync_interval)
|
||||
def update_managed_registry_secret(
|
||||
spec,
|
||||
status,
|
||||
@ -90,6 +96,7 @@ def update_managed_registry_secret(
|
||||
id = spec.get('id')
|
||||
secret_name = spec.get('name')
|
||||
secret_namespace = spec.get('namespace')
|
||||
labels = spec.get('labels')
|
||||
|
||||
old_config = None
|
||||
old_secret_name = None
|
||||
@ -126,9 +133,13 @@ def update_managed_registry_secret(
|
||||
"managed": "registry-credential.lerentis.uploadfilter24.eu",
|
||||
"managedObject": f"{namespace}/{name}"
|
||||
}
|
||||
|
||||
if not labels:
|
||||
labels = {}
|
||||
|
||||
secret = kubernetes.client.V1Secret()
|
||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
||||
name=secret_name, annotations=annotations)
|
||||
name=secret_name, annotations=annotations, labels=labels)
|
||||
secret = create_dockerlogin(
|
||||
logger,
|
||||
secret,
|
||||
@ -137,7 +148,7 @@ def update_managed_registry_secret(
|
||||
password_ref,
|
||||
registry)
|
||||
try:
|
||||
obj = api.replace_namespaced_secret(
|
||||
api.replace_namespaced_secret(
|
||||
name=secret_name,
|
||||
body=secret,
|
||||
namespace="{}".format(secret_namespace))
|
||||
|
22
src/kv.py
22
src/kv.py
@ -3,8 +3,7 @@ import kubernetes
|
||||
import base64
|
||||
import json
|
||||
|
||||
from utils.utils import unlock_bw, get_secret_from_bitwarden, parse_login_scope, parse_fields_scope
|
||||
|
||||
from utils.utils import unlock_bw, get_secret_from_bitwarden, parse_login_scope, parse_fields_scope, bw_sync_interval
|
||||
|
||||
def create_kv(secret, secret_json, content_def):
|
||||
secret.type = "Opaque"
|
||||
@ -42,6 +41,7 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
|
||||
id = spec.get('id')
|
||||
secret_name = spec.get('name')
|
||||
secret_namespace = spec.get('namespace')
|
||||
labels = spec.get('labels')
|
||||
|
||||
unlock_bw(logger)
|
||||
logger.info(f"Locking up secret with ID: {id}")
|
||||
@ -53,12 +53,16 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
|
||||
"managed": "bitwarden-secret.lerentis.uploadfilter24.eu",
|
||||
"managedObject": f"{namespace}/{name}"
|
||||
}
|
||||
|
||||
if not labels:
|
||||
labels = {}
|
||||
|
||||
secret = kubernetes.client.V1Secret()
|
||||
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)
|
||||
|
||||
obj = api.create_namespaced_secret(
|
||||
api.create_namespaced_secret(
|
||||
namespace="{}".format(secret_namespace),
|
||||
body=secret
|
||||
)
|
||||
@ -67,7 +71,7 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
|
||||
|
||||
|
||||
@kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu')
|
||||
@kopf.timer('bitwarden-secret.lerentis.uploadfilter24.eu', interval=900)
|
||||
@kopf.timer('bitwarden-secret.lerentis.uploadfilter24.eu', interval=bw_sync_interval)
|
||||
def update_managed_secret(
|
||||
spec,
|
||||
status,
|
||||
@ -89,6 +93,7 @@ def update_managed_secret(
|
||||
old_secret_namespace = old_config['spec'].get('namespace')
|
||||
secret_name = spec.get('name')
|
||||
secret_namespace = spec.get('namespace')
|
||||
labels = spec.get('labels')
|
||||
|
||||
if old_config is not None and (
|
||||
old_secret_name != secret_name or old_secret_namespace != secret_namespace):
|
||||
@ -115,13 +120,16 @@ def update_managed_secret(
|
||||
"managedObject": f"{namespace}/{name}"
|
||||
}
|
||||
|
||||
if not labels:
|
||||
labels = {}
|
||||
|
||||
secret = kubernetes.client.V1Secret()
|
||||
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)
|
||||
|
||||
try:
|
||||
obj = api.replace_namespaced_secret(
|
||||
api.replace_namespaced_secret(
|
||||
name=secret_name,
|
||||
body=secret,
|
||||
namespace="{}".format(secret_namespace))
|
||||
|
@ -1,11 +1,16 @@
|
||||
import json
|
||||
|
||||
from utils.utils import get_secret_from_bitwarden, parse_fields_scope, parse_login_scope
|
||||
from utils.utils import get_secret_from_bitwarden, get_attachment, parse_fields_scope, parse_login_scope
|
||||
|
||||
|
||||
def bitwarden_lookup(id, scope, field):
|
||||
_secret_json = get_secret_from_bitwarden(None, id)
|
||||
if scope == "login":
|
||||
return parse_login_scope(_secret_json, field)
|
||||
if scope == "fields":
|
||||
return parse_fields_scope(_secret_json, field)
|
||||
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)
|
||||
|
@ -3,28 +3,25 @@ import base64
|
||||
import kubernetes
|
||||
import json
|
||||
|
||||
from utils.utils import unlock_bw
|
||||
from lookups.bitwarden_lookup import bitwarden_lookup
|
||||
from utils.utils import unlock_bw, bw_sync_interval
|
||||
from lookups.bitwarden_lookup import BitwardenLookupHandler
|
||||
from jinja2 import Environment, BaseLoader
|
||||
|
||||
|
||||
lookup_func_dict = {
|
||||
"bitwarden_lookup": bitwarden_lookup,
|
||||
}
|
||||
|
||||
|
||||
def render_template(template):
|
||||
def render_template(logger, template):
|
||||
jinja_template = Environment(loader=BaseLoader()).from_string(template)
|
||||
jinja_template.globals.update(lookup_func_dict)
|
||||
jinja_template.globals.update({
|
||||
"bitwarden_lookup": BitwardenLookupHandler(logger).bitwarden_lookup,
|
||||
})
|
||||
return jinja_template.render()
|
||||
|
||||
|
||||
def create_template_secret(secret, filename, template):
|
||||
def create_template_secret(logger, secret, filename, template):
|
||||
secret.type = "Opaque"
|
||||
secret.data = {}
|
||||
secret.data[filename] = str(
|
||||
base64.b64encode(
|
||||
render_template(template).encode("utf-8")),
|
||||
render_template(logger, template).encode("utf-8")),
|
||||
"utf-8")
|
||||
return secret
|
||||
|
||||
@ -36,6 +33,7 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
|
||||
filename = spec.get('filename')
|
||||
secret_name = spec.get('name')
|
||||
secret_namespace = spec.get('namespace')
|
||||
labels = spec.get('labels')
|
||||
|
||||
unlock_bw(logger)
|
||||
|
||||
@ -45,12 +43,16 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
|
||||
"managed": "bitwarden-template.lerentis.uploadfilter24.eu",
|
||||
"managedObject": f"{namespace}/{name}"
|
||||
}
|
||||
|
||||
if not labels:
|
||||
labels = {}
|
||||
|
||||
secret = kubernetes.client.V1Secret()
|
||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
||||
name=secret_name, annotations=annotations)
|
||||
secret = create_template_secret(secret, filename, template)
|
||||
name=secret_name, annotations=annotations, labels=labels)
|
||||
secret = create_template_secret(logger, secret, filename, template)
|
||||
|
||||
obj = api.create_namespaced_secret(
|
||||
api.create_namespaced_secret(
|
||||
secret_namespace, secret
|
||||
)
|
||||
|
||||
@ -58,7 +60,7 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs):
|
||||
|
||||
|
||||
@kopf.on.update('bitwarden-template.lerentis.uploadfilter24.eu')
|
||||
@kopf.timer('bitwarden-template.lerentis.uploadfilter24.eu', interval=900)
|
||||
@kopf.timer('bitwarden-template.lerentis.uploadfilter24.eu', interval=bw_sync_interval)
|
||||
def update_managed_secret(
|
||||
spec,
|
||||
status,
|
||||
@ -72,6 +74,7 @@ def update_managed_secret(
|
||||
filename = spec.get('filename')
|
||||
secret_name = spec.get('name')
|
||||
secret_namespace = spec.get('namespace')
|
||||
labels = spec.get('labels')
|
||||
|
||||
old_config = None
|
||||
old_secret_name = None
|
||||
@ -106,13 +109,17 @@ def update_managed_secret(
|
||||
"managed": "bitwarden-template.lerentis.uploadfilter24.eu",
|
||||
"managedObject": f"{namespace}/{name}"
|
||||
}
|
||||
|
||||
if not labels:
|
||||
labels = {}
|
||||
|
||||
secret = kubernetes.client.V1Secret()
|
||||
secret.metadata = kubernetes.client.V1ObjectMeta(
|
||||
name=secret_name, annotations=annotations)
|
||||
secret = create_template_secret(secret, filename, template)
|
||||
name=secret_name, annotations=annotations, labels=labels)
|
||||
secret = create_template_secret(logger, secret, filename, template)
|
||||
|
||||
try:
|
||||
obj = api.replace_namespaced_secret(
|
||||
api.replace_namespaced_secret(
|
||||
name=secret_name,
|
||||
body=secret,
|
||||
namespace="{}".format(secret_namespace))
|
||||
|
@ -1,16 +1,48 @@
|
||||
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):
|
||||
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']
|
||||
@ -22,16 +54,22 @@ def unlock_bw(logger):
|
||||
logger.info("Signin successful. Session exported")
|
||||
|
||||
|
||||
def command_wrapper(logger, command, use_success: bool = True):
|
||||
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 {command}"],
|
||||
[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'))
|
||||
|
Loading…
Reference in New Issue
Block a user