diff --git a/k8s-ng/external-secrets/Dockerfile-bitwarden-cli b/k8s-ng/external-secrets/Dockerfile-bitwarden-cli new file mode 100644 index 0000000..6f159a3 --- /dev/null +++ b/k8s-ng/external-secrets/Dockerfile-bitwarden-cli @@ -0,0 +1,26 @@ +FROM debian:sid + +ENV BW_CLI_VERSION=2025.12.1 + +RUN apt update && \ + apt install -y wget unzip && \ + wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_CLI_VERSION}/bw-linux-${BW_CLI_VERSION}.zip && \ + unzip bw-linux-${BW_CLI_VERSION}.zip && \ + chmod +x bw && \ + mv bw /usr/local/bin/bw && \ + rm -rfv *.zip && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Build entrypoint script +RUN echo '#!/bin/sh\n\ +set -e\n\ +bw config server ${BW_HOST}\n\ +export BW_SESSION=$(bw login ${BW_USER} --passwordenv BW_PASSWORD --raw)\n\ +bw unlock --check\n\ +echo "Running \`bw server\` on port 8087"\n\ +bw serve --hostname 0.0.0.0\n' > /entrypoint.sh && \ + chmod +x /entrypoint.sh + +CMD ["/entrypoint.sh"] + diff --git a/k8s-ng/external-secrets/app.yaml b/k8s-ng/external-secrets/app.yaml new file mode 100644 index 0000000..fe5f9ae --- /dev/null +++ b/k8s-ng/external-secrets/app.yaml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets + namespace: argocd +spec: + project: core + destination: + namespace: external-secrets + server: https://kubernetes.default.svc + source: + repoURL: https://gt.hexor.cy/ab/homelab.git + targetRevision: main + path: k8s-ng/external-secrets + syncPolicy: + automated: + selfHeal: true + prune: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/k8s-ng/external-secrets/bitwarden-store.yaml b/k8s-ng/external-secrets/bitwarden-store.yaml new file mode 100644 index 0000000..5d7f8f1 --- /dev/null +++ b/k8s-ng/external-secrets/bitwarden-store.yaml @@ -0,0 +1,153 @@ +# --- +# 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 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bitwarden-cli + labels: + reloader.stakater.com/auto: "true" + app.kubernetes.io/instance: bitwarden-cli + app.kubernetes.io/name: bitwarden-cli +spec: + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: bitwarden-cli + app.kubernetes.io/instance: bitwarden-cli + template: + metadata: + labels: + app.kubernetes.io/name: bitwarden-cli + app.kubernetes.io/instance: bitwarden-cli + spec: + nodeSelector: + kubernetes.io/arch: amd64 + containers: + - name: bitwarden-cli + image: ultradesu/bitwarden-client:2025.12.1 + imagePullPolicy: Always + resources: + requests: + memory: "128Mi" + cpu: "300m" + limits: + memory: "512Mi" + cpu: "1000m" + env: + - name: BW_HOST + valueFrom: + secretKeyRef: + name: bitwarden-cli + key: BW_HOST + - name: BW_USER + valueFrom: + secretKeyRef: + name: bitwarden-cli + key: BW_USERNAME + - name: BW_PASSWORD + valueFrom: + secretKeyRef: + name: bitwarden-cli + key: BW_PASSWORD + ports: + - name: http + containerPort: 8087 + protocol: TCP + livenessProbe: + exec: + command: + - wget + - -q + - http://127.0.0.1:8087/sync + - --post-data='' + initialDelaySeconds: 20 + failureThreshold: 3 + timeoutSeconds: 1 + periodSeconds: 120 + readinessProbe: + tcpSocket: + port: 8087 + initialDelaySeconds: 20 + failureThreshold: 3 + timeoutSeconds: 1 + periodSeconds: 10 + startupProbe: + tcpSocket: + port: 8087 + initialDelaySeconds: 10 + failureThreshold: 30 + timeoutSeconds: 1 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: bitwarden-cli + labels: + app.kubernetes.io/instance: bitwarden-cli + app.kubernetes.io/name: bitwarden-cli + annotations: +spec: + type: ClusterIP + ports: + - port: 8087 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: bitwarden-cli + app.kubernetes.io/instance: bitwarden-cli +--- +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: external-secret-2-bw-cli +spec: + podSelector: + matchLabels: + app.kubernetes.io/instance: bitwarden-cli + app.kubernetes.io/name: bitwarden-cli + ingress: + - from: + - podSelector: + 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/external-secrets/kustomization.yaml b/k8s-ng/external-secrets/kustomization.yaml new file mode 100644 index 0000000..a70ce92 --- /dev/null +++ b/k8s-ng/external-secrets/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - app.yaml + # Enable after the Vaultwarden bot account exists and the bitwarden-cli + # secret is created (see the header of bitwarden-store.yaml): + # - bitwarden-store.yaml + +helmCharts: + - name: external-secrets + repo: https://charts.external-secrets.io + version: 2.11.0 + releaseName: external-secrets + namespace: external-secrets + valuesFile: values.yaml + includeCRDs: true diff --git a/k8s-ng/external-secrets/values.yaml b/k8s-ng/external-secrets/values.yaml new file mode 100644 index 0000000..1b4551c --- /dev/null +++ b/k8s-ng/external-secrets/values.yaml @@ -0,0 +1 @@ +installCRDs: true diff --git a/k8s-ng/postgresql/databases/vaultwarden.yaml b/k8s-ng/postgresql/databases/vaultwarden.yaml new file mode 100644 index 0000000..5ad4a02 --- /dev/null +++ b/k8s-ng/postgresql/databases/vaultwarden.yaml @@ -0,0 +1,81 @@ +# Database "vaultwarden": password, role, database and read access for the app. +# To add another database copy this file and replace the name. + +# Generated once; the role and the application both read this Secret. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: vaultwarden-db + namespace: psql +spec: + refreshPolicy: CreatedOnce + target: + name: vaultwarden-db + template: + type: kubernetes.io/basic-auth + metadata: + labels: + cnpg.io/reload: "true" + data: + username: vaultwarden + password: "{{ .password }}" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: db-password +--- +apiVersion: postgresql.cnpg.io/v1 +kind: DatabaseRole +metadata: + name: main-vaultwarden + namespace: psql +spec: + cluster: + name: main + name: vaultwarden + login: true + passwordSecret: + name: vaultwarden-db +--- +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: main-vaultwarden + namespace: psql +spec: + cluster: + name: main + name: vaultwarden + owner: vaultwarden + databaseReclaimPolicy: retain +--- +# Lets the vaultwarden namespace read only its own credentials Secret. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: vaultwarden-db-reader + namespace: psql +rules: + - apiGroups: [""] + resources: [secrets] + resourceNames: [vaultwarden-db] + verbs: [get, list, watch] + - apiGroups: [authorization.k8s.io] + resources: [selfsubjectrulesreviews] + verbs: [create] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: vaultwarden-db-reader + namespace: psql +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: vaultwarden-db-reader +subjects: + - kind: ServiceAccount + name: db-credentials-reader + namespace: vaultwarden diff --git a/k8s-ng/postgresql/kustomization.yaml b/k8s-ng/postgresql/kustomization.yaml index 87f4075..caa7c7f 100644 --- a/k8s-ng/postgresql/kustomization.yaml +++ b/k8s-ng/postgresql/kustomization.yaml @@ -4,3 +4,5 @@ kind: Kustomization resources: - app.yaml - cluster.yaml + - password-generator.yaml + - databases/vaultwarden.yaml diff --git a/k8s-ng/postgresql/password-generator.yaml b/k8s-ng/postgresql/password-generator.yaml new file mode 100644 index 0000000..c0307d2 --- /dev/null +++ b/k8s-ng/postgresql/password-generator.yaml @@ -0,0 +1,12 @@ +# Random passwords for database roles. Referenced by the ExternalSecrets in databases/. +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: db-password + namespace: psql +spec: + length: 40 + digits: 10 + symbols: 0 # the password ends up inside a connection URL + noUpper: false + allowRepeat: true diff --git a/k8s-ng/vaultwarden/app.yaml b/k8s-ng/vaultwarden/app.yaml new file mode 100644 index 0000000..a458356 --- /dev/null +++ b/k8s-ng/vaultwarden/app.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vaultwarden + namespace: argocd +spec: + project: apps + destination: + namespace: vaultwarden + server: https://kubernetes.default.svc + source: + repoURL: https://gt.hexor.cy/ab/homelab.git + targetRevision: main + path: k8s-ng/vaultwarden + syncPolicy: + automated: + selfHeal: true + prune: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - SkipDryRunOnMissingResource=true # the external-secrets Application installs the CRDs diff --git a/k8s-ng/vaultwarden/deployment.yaml b/k8s-ng/vaultwarden/deployment.yaml new file mode 100644 index 0000000..9fbfaa6 --- /dev/null +++ b/k8s-ng/vaultwarden/deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vaultwarden + labels: + app: vaultwarden +spec: + replicas: 1 + strategy: + type: Recreate # the RWO local volume cannot be mounted by two pods + selector: + matchLabels: + app: vaultwarden + template: + metadata: + labels: + app: vaultwarden + spec: + containers: + - name: vaultwarden + image: vaultwarden/server:1.37.3 + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 512Mi + env: + - name: DOMAIN + value: https://vw.hexor.cy + - name: ORG_GROUPS_ENABLED + value: "true" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: vaultwarden-db-url + key: DATABASE_URL + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: admin-token + key: ADMIN_TOKEN + ports: + - name: http + containerPort: 80 + protocol: TCP + readinessProbe: + httpGet: + path: /alive + port: http + periodSeconds: 10 + livenessProbe: + httpGet: + path: /alive + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: vaultwarden-data diff --git a/k8s-ng/vaultwarden/ingress.yaml b/k8s-ng/vaultwarden/ingress.yaml new file mode 100644 index 0000000..f7ce97a --- /dev/null +++ b/k8s-ng/vaultwarden/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vaultwarden + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + rules: + - host: vw.hexor.cy + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vaultwarden + port: + number: 80 + tls: + - secretName: vaultwarden-tls + hosts: + - vw.hexor.cy diff --git a/k8s-ng/vaultwarden/kustomization.yaml b/k8s-ng/vaultwarden/kustomization.yaml new file mode 100644 index 0000000..fa23277 --- /dev/null +++ b/k8s-ng/vaultwarden/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - app.yaml + - secrets.yaml + - pvc.yaml + - deployment.yaml + - service.yaml + - ingress.yaml diff --git a/k8s-ng/vaultwarden/pvc.yaml b/k8s-ng/vaultwarden/pvc.yaml new file mode 100644 index 0000000..ef62396 --- /dev/null +++ b/k8s-ng/vaultwarden/pvc.yaml @@ -0,0 +1,12 @@ +# Attachments, sends, icon cache and RSA keys live here; the database is in PostgreSQL. +# A local-path volume is bound to one node: the pod stays on the node it first ran on. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: vaultwarden-data +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: + storage: 2Gi diff --git a/k8s-ng/vaultwarden/secrets.yaml b/k8s-ng/vaultwarden/secrets.yaml new file mode 100644 index 0000000..a835864 --- /dev/null +++ b/k8s-ng/vaultwarden/secrets.yaml @@ -0,0 +1,78 @@ +# Read-only access to the vaultwarden-db Secret in the psql namespace +# (see postgresql/databases/vaultwarden.yaml for the other half). +apiVersion: v1 +kind: ServiceAccount +metadata: + name: db-credentials-reader +--- +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: psql-credentials +spec: + provider: + kubernetes: + remoteNamespace: psql + server: + caProvider: + type: ConfigMap + name: kube-root-ca.crt + key: ca.crt + auth: + serviceAccount: + name: db-credentials-reader +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: vaultwarden-db-url +spec: + refreshInterval: 1h + secretStoreRef: + kind: SecretStore + name: psql-credentials + target: + name: vaultwarden-db-url + template: + data: + DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@main-rw.psql.svc:5432/vaultwarden" + data: + - secretKey: username + remoteRef: + key: vaultwarden-db + property: username + - secretKey: password + remoteRef: + key: vaultwarden-db + property: password +--- +# Admin panel token (/admin). Generated once; read it with: +# kubectl -n vaultwarden get secret admin-token -o jsonpath='{.data.ADMIN_TOKEN}' | base64 -d +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: admin-token +spec: + length: 48 + digits: 10 + symbols: 0 + noUpper: false + allowRepeat: true +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: admin-token +spec: + refreshPolicy: CreatedOnce + target: + name: admin-token + template: + data: + ADMIN_TOKEN: "{{ .password }}" + dataFrom: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: admin-token diff --git a/k8s-ng/vaultwarden/service.yaml b/k8s-ng/vaultwarden/service.yaml new file mode 100644 index 0000000..a6effdc --- /dev/null +++ b/k8s-ng/vaultwarden/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: vaultwarden +spec: + selector: + app: vaultwarden + ports: + - protocol: TCP + port: 80 + targetPort: http