diff --git a/k8s-ng/core-state/values.yaml b/k8s-ng/core-state/values.yaml index 339390d..66ca5b5 100644 --- a/k8s-ng/core-state/values.yaml +++ b/k8s-ng/core-state/values.yaml @@ -53,3 +53,8 @@ apps: namespace: keycloak wave: 3 skipDryRunOnMissingResource: true # ExternalSecret/Certificate CRDs come from earlier waves + - name: secret-stores + path: k8s-ng/core/secret-stores + namespace: external-secrets + wave: 4 # reads the Vaultwarden vault, so it comes after vaultwarden + skipDryRunOnMissingResource: true # ClusterSecretStore CRD comes from the external-secrets app diff --git a/k8s-ng/core/external-secrets/kustomization.yaml b/k8s-ng/core/external-secrets/kustomization.yaml index 3f85211..2700635 100644 --- a/k8s-ng/core/external-secrets/kustomization.yaml +++ b/k8s-ng/core/external-secrets/kustomization.yaml @@ -1,10 +1,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -# Enable after the Vaultwarden bot account exists and the bitwarden-cli -# secret is created (see the header of bitwarden-store.yaml): -# resources: -# - bitwarden-store.yaml +# The Vaultwarden bridge and the ClusterSecretStores are in ../secret-stores (they need Vaultwarden). helmCharts: - name: external-secrets diff --git a/k8s-ng/core/external-secrets/Dockerfile-bitwarden-cli b/k8s-ng/core/secret-stores/Dockerfile-bitwarden-cli similarity index 100% rename from k8s-ng/core/external-secrets/Dockerfile-bitwarden-cli rename to k8s-ng/core/secret-stores/Dockerfile-bitwarden-cli diff --git a/k8s-ng/core/external-secrets/bitwarden-store.yaml b/k8s-ng/core/secret-stores/bitwarden-cli.yaml similarity index 76% rename from k8s-ng/core/external-secrets/bitwarden-store.yaml rename to k8s-ng/core/secret-stores/bitwarden-cli.yaml index 5d7f8f1..7ce9186 100644 --- a/k8s-ng/core/external-secrets/bitwarden-store.yaml +++ b/k8s-ng/core/secret-stores/bitwarden-cli.yaml @@ -1,15 +1,11 @@ -# --- -# apiVersion: v1 -# kind: Secret -# metadata: -# name: bitwarden-cli -# namespace: external-secrets -# data: -# BW_HOST: base64(url) -# BW_USERNAME: base64(name) -# BW_PASSWORD: base64(pass) -# Vaultwarden bot - 81212111-6350-4069-8bcf-19a67d3964a5 ---- +# Bridge between External Secrets and Vaultwarden: `bw serve` logged in as the +# bot account. Needs the Secret bitwarden-cli (created by hand; the bot logs in +# with email + password, so keep password login enabled for it, i.e. no +# SSO_ONLY, or switch the image to API-key login): +# kubectl -n external-secrets create secret generic bitwarden-cli \ +# --from-literal=BW_HOST=https://vw.btwiusearch.net \ +# --from-literal=BW_USERNAME= --from-literal=BW_PASSWORD= +# Image source: Dockerfile-bitwarden-cli in this directory. apiVersion: apps/v1 kind: Deployment metadata: @@ -37,7 +33,7 @@ spec: containers: - name: bitwarden-cli image: ultradesu/bitwarden-client:2025.12.1 - imagePullPolicy: Always + imagePullPolicy: IfNotPresent resources: requests: memory: "128Mi" @@ -98,7 +94,6 @@ metadata: labels: app.kubernetes.io/instance: bitwarden-cli app.kubernetes.io/name: bitwarden-cli - annotations: spec: type: ClusterIP ports: @@ -110,8 +105,9 @@ spec: app.kubernetes.io/name: bitwarden-cli app.kubernetes.io/instance: bitwarden-cli --- -kind: NetworkPolicy +# Only the External Secrets controller may talk to the bridge: it serves the whole vault. apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy metadata: name: external-secret-2-bw-cli spec: @@ -125,29 +121,3 @@ spec: matchLabels: app.kubernetes.io/instance: external-secrets app.kubernetes.io/name: external-secrets - ---- -apiVersion: external-secrets.io/v1 -kind: ClusterSecretStore -metadata: - name: vaultwarden-login -spec: - provider: - webhook: - url: "http://bitwarden-cli:8087/object/item/{{ .remoteRef.key }}" - headers: - Content-Type: application/json - result: - jsonPath: "$.data.{{ .remoteRef.property }}" ---- -apiVersion: external-secrets.io/v1 -kind: ClusterSecretStore -metadata: - name: vaultwarden-fields -spec: - provider: - webhook: - url: "http://bitwarden-cli:8087/object/item/{{ .remoteRef.key }}" - result: - jsonPath: "$.data.fields[?@.name==\"{{ .remoteRef.property }}\"].value" - diff --git a/k8s-ng/core/secret-stores/kustomization.yaml b/k8s-ng/core/secret-stores/kustomization.yaml new file mode 100644 index 0000000..ce58770 --- /dev/null +++ b/k8s-ng/core/secret-stores/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - bitwarden-cli.yaml + - stores.yaml diff --git a/k8s-ng/core/secret-stores/stores.yaml b/k8s-ng/core/secret-stores/stores.yaml new file mode 100644 index 0000000..5c96ed1 --- /dev/null +++ b/k8s-ng/core/secret-stores/stores.yaml @@ -0,0 +1,47 @@ +# ClusterSecretStores in front of the Vaultwarden vault that belongs to the bot +# account (the vault holds server secrets only, nothing personal). +# +# `key` may be either +# - the item UUID (old style, changes whenever items are imported again), or +# - the item path: the item NAME, which by convention is the full path +# k8s// e.g. k8s/cert-manager/cloudflare-api-token +# Names are looked up by exact match (a name that is a substring of another +# item name, login or URL is fine). Do not use double quotes in names. +# +# vaultwarden-login: `property` is a path inside the item: login.username, +# login.password, notes, fields[0].value, ... +# vaultwarden-fields: `property` is the NAME of a custom field of the item. +# +# data: +# - secretKey: password +# sourceRef: +# storeRef: {name: vaultwarden-login, kind: ClusterSecretStore} +# remoteRef: +# key: k8s/cert-manager/cloudflare-api-token +# property: login.password +# +# The bridge syncs the vault every couple of minutes, so a new item becomes +# visible to ESO after that. +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: vaultwarden-login +spec: + provider: + webhook: + url: 'http://bitwarden-cli.external-secrets.svc:8087/{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}object/item/{{ .remoteRef.key }}{{ else }}list/object/items?search={{ .remoteRef.key }}{{ end }}' + headers: + Content-Type: application/json + result: + jsonPath: '{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}$.data.{{ .remoteRef.property }}{{ else }}$.data.data[?@.name=="{{ .remoteRef.key }}"].{{ .remoteRef.property }}{{ end }}' +--- +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: vaultwarden-fields +spec: + provider: + webhook: + url: 'http://bitwarden-cli.external-secrets.svc:8087/{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}object/item/{{ .remoteRef.key }}{{ else }}list/object/items?search={{ .remoteRef.key }}{{ end }}' + result: + jsonPath: '{{ if regexMatch "^[0-9a-fA-F-]{36}$" .remoteRef.key }}$.data.fields[?@.name=="{{ .remoteRef.property }}"].value{{ else }}$.data.data[?@.name=="{{ .remoteRef.key }}"].fields[?@.name=="{{ .remoteRef.property }}"].value{{ end }}'