From 1261e238aa7201feec6adfe559e3a0a39dff06b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 12:44:05 -0700 Subject: [PATCH] Add vaultwarden app (migrated from docker on 10.10.0.5) - Pinned to 1.37.0 so the migration is not also an upgrade - ADMIN_TOKEN rotated to an Argon2id PHC string, stored as a SealedSecret. The old token was plaintext AND set in config.json, which overrides env -- both that key and signups_allowed were removed during migration so the Deployment is the source of truth. signups had been silently enabled. - /admin keeps Authentik forward-auth at priority 1000 (Traefik's default priority is rule length; the old docker router used 10 vs an implicit 24 and was therefore bypassed). - vault.aleshym.co stays private: absent from the k3s public catchall and from the Docker Traefik wan entrypoint. Co-Authored-By: Claude Opus 5 --- apps/vaultwarden/kustomization.yaml | 11 ++++ apps/vaultwarden/namespace.yaml | 4 ++ apps/vaultwarden/vaultwarden-admin.yaml | 47 ++++++++++++++ apps/vaultwarden/vaultwarden-deployment.yaml | 64 +++++++++++++++++++ apps/vaultwarden/vaultwarden-ingress.yaml | 29 +++++++++ apps/vaultwarden/vaultwarden-pvc.yaml | 13 ++++ .../vaultwarden/vaultwarden-sealedsecret.yaml | 13 ++++ .../vaultwarden-secret.yaml.example | 13 ++++ apps/vaultwarden/vaultwarden-service.yaml | 12 ++++ 9 files changed, 206 insertions(+) create mode 100644 apps/vaultwarden/kustomization.yaml create mode 100644 apps/vaultwarden/namespace.yaml create mode 100644 apps/vaultwarden/vaultwarden-admin.yaml create mode 100644 apps/vaultwarden/vaultwarden-deployment.yaml create mode 100644 apps/vaultwarden/vaultwarden-ingress.yaml create mode 100644 apps/vaultwarden/vaultwarden-pvc.yaml create mode 100644 apps/vaultwarden/vaultwarden-sealedsecret.yaml create mode 100644 apps/vaultwarden/vaultwarden-secret.yaml.example create mode 100644 apps/vaultwarden/vaultwarden-service.yaml diff --git a/apps/vaultwarden/kustomization.yaml b/apps/vaultwarden/kustomization.yaml new file mode 100644 index 0000000..e57c45a --- /dev/null +++ b/apps/vaultwarden/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - vaultwarden-sealedsecret.yaml + - vaultwarden-pvc.yaml + - vaultwarden-deployment.yaml + - vaultwarden-service.yaml + - vaultwarden-ingress.yaml + - vaultwarden-admin.yaml diff --git a/apps/vaultwarden/namespace.yaml b/apps/vaultwarden/namespace.yaml new file mode 100644 index 0000000..6fc17a5 --- /dev/null +++ b/apps/vaultwarden/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vaultwarden diff --git a/apps/vaultwarden/vaultwarden-admin.yaml b/apps/vaultwarden/vaultwarden-admin.yaml new file mode 100644 index 0000000..7f21cc6 --- /dev/null +++ b/apps/vaultwarden/vaultwarden-admin.yaml @@ -0,0 +1,47 @@ +# /admin gets Authentik forward-auth on top of the ADMIN_TOKEN. +# +# Bitwarden clients (browser extension, phone) use /api, /identity and +# /notifications/hub and CANNOT follow an SSO redirect, so this is deliberately +# path-scoped -- never blanket-auth this host. +# +# Priority must EXCEED the Ingress route's default, which Traefik derives from +# the rule's character length. Setting a low-but-nonzero number is the trap that +# left this same /admin panel unprotected under Docker (priority=10 vs an +# implicit 24). 1000 is unambiguous. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: authentik + namespace: vaultwarden +spec: + forwardAuth: + # Resolves to 10.10.0.5 only because coredns-custom forwards aleshym.co to + # Pi-hole; the node's own resolver (1.1.1.1) returns the public IP. + address: https://auth.aleshym.co/outpost.goauthentik.io/auth/traefik + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + - X-authentik-groups + - X-authentik-email + - X-authentik-name + - X-authentik-uid +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: vaultwarden-admin + namespace: vaultwarden +spec: + entryPoints: + - websecure + routes: + - kind: Rule + priority: 1000 + match: Host(`vault.aleshym.co`) && PathPrefix(`/admin`) + middlewares: + - name: authentik + services: + - name: vaultwarden + port: 80 + tls: + secretName: vaultwarden-tls diff --git a/apps/vaultwarden/vaultwarden-deployment.yaml b/apps/vaultwarden/vaultwarden-deployment.yaml new file mode 100644 index 0000000..1473816 --- /dev/null +++ b/apps/vaultwarden/vaultwarden-deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vaultwarden + namespace: vaultwarden +spec: + replicas: 1 + strategy: + # SQLite on a RWO volume - never run two pods against it. + type: Recreate + selector: + matchLabels: + app: vaultwarden + template: + metadata: + labels: + app: vaultwarden + spec: + containers: + - name: vaultwarden + # Pinned deliberately: this migration should not also be an upgrade. + image: vaultwarden/server:1.37.0 + ports: + - name: http + containerPort: 80 + env: + - name: DOMAIN + value: https://vault.aleshym.co + # config.json used to override these; both keys were removed from it + # during the migration so the Deployment is the source of truth. + - name: SIGNUPS_ALLOWED + value: "false" + - name: INVITATIONS_ALLOWED + value: "false" + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: vaultwarden-secret + key: ADMIN_TOKEN + volumeMounts: + - name: data + mountPath: /data + livenessProbe: + httpGet: + path: /alive + port: http + initialDelaySeconds: 20 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /alive + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 25m + memory: 96Mi + limits: + memory: 512Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: vaultwarden-data diff --git a/apps/vaultwarden/vaultwarden-ingress.yaml b/apps/vaultwarden/vaultwarden-ingress.yaml new file mode 100644 index 0000000..4e0c9f1 --- /dev/null +++ b/apps/vaultwarden/vaultwarden-ingress.yaml @@ -0,0 +1,29 @@ +# vault.aleshym.co is PRIVATE. It is deliberately absent from the k3s +# `catchall-to-docker-apps` public host list and from the Docker Traefik `wan` +# entrypoint, so the internet has no route to it. Pi-hole resolves the name to +# 10.10.0.100 for LAN/VPN clients. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vaultwarden + namespace: vaultwarden + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + ingressClassName: traefik + rules: + - host: vault.aleshym.co + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vaultwarden + port: + name: http + tls: + - hosts: + - vault.aleshym.co + secretName: vaultwarden-tls diff --git a/apps/vaultwarden/vaultwarden-pvc.yaml b/apps/vaultwarden/vaultwarden-pvc.yaml new file mode 100644 index 0000000..4f300c4 --- /dev/null +++ b/apps/vaultwarden/vaultwarden-pvc.yaml @@ -0,0 +1,13 @@ +# local-path has reclaimPolicy: Delete -- deleting this PVC destroys the vault. +# Data is tiny (12Mi today); 2Gi is headroom for attachments/sends. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: vaultwarden-data + namespace: vaultwarden +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/apps/vaultwarden/vaultwarden-sealedsecret.yaml b/apps/vaultwarden/vaultwarden-sealedsecret.yaml new file mode 100644 index 0000000..f416a7d --- /dev/null +++ b/apps/vaultwarden/vaultwarden-sealedsecret.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: vaultwarden-secret + namespace: vaultwarden +spec: + encryptedData: + ADMIN_TOKEN: AgA6mXlIUSyYrbKTJ0y5sW+ihtQoSg2B6XYoTadlp7JvHEojnsF11mlX5JuiZgqlWlb0o9WakC4HnXCo04t4sX98ZZaEvumkC8ALIiTCG9R9K7/b3GP5h2kmEY0kpZDRVbhex4wBEYdjlirqGMpubT4slwlwGeoNnMugC95+IE8CAjFJW87IaQR03J4zumFNG9E4AFqgprm5HCxDi9UHeY77gECDcOko0TVXSvMmdnwtymjgSWKG7t8Ry3dkkS+j/cS1v1IoJkvqINi2b9+oP1C3M0dV+YZyyE+1KAlgwVnR+RxTUWFmZ6rWWK/C1/a79hqcGNjXzL5YWCUewzvIBcsERRNMMQqb63Q0KvCXNPheC5z+aNz6PPEaToyrNbmHgx6973SwNrtIeaEj8FAkAoNHZFwk9kJ+bjhVyrMZ+A/V/xw5STMPn3B4dbJYv/PfjXOuCySIZKtdFzM3DWAMK3aGzS2cvDefTofLc1HpoXr7ZFvUnNGrs9kjpbcnKt3HImhx4b/moddywOeaicn0kQMYCXoq5TmJLyma10bQl37JV19sFyya054OMGT5mOyqCKLmm7tZxC3T5TuCDyRSj7uM9suGcW7RSpn7gxa10ismSRzSoqSO8Af+Ilo69AYdiwW341P6h5GQOLhUbUA9G+ho8Q5rgEh7tkp9qpz0QYEKqsmtd4vlzg4jn2c0c1fSK29I9shMyKusyVdADkk/sunkYdqRG6kdAGGmS71DocuSvN8faP8DIEqw49tlI59TBCSmPqF1kEg5xBErkq/9+HUx5SffxEUn1SEdlftnQWQ9ECiPZNw7LhVl9Ritv99UuvtL + template: + metadata: + name: vaultwarden-secret + namespace: vaultwarden diff --git a/apps/vaultwarden/vaultwarden-secret.yaml.example b/apps/vaultwarden/vaultwarden-secret.yaml.example new file mode 100644 index 0000000..b956a64 --- /dev/null +++ b/apps/vaultwarden/vaultwarden-secret.yaml.example @@ -0,0 +1,13 @@ +# Template only -- the real values live in vaultwarden-sealedsecret.yaml. +# ADMIN_TOKEN is an Argon2id PHC string (vaultwarden hash --preset owasp), +# NOT a plaintext token. Generate a replacement with: +# docker run --rm python:3.12-alpine sh -c \ +# 'pip install -q argon2-cffi && python -c "..."' +apiVersion: v1 +kind: Secret +metadata: + name: vaultwarden-secret + namespace: vaultwarden +type: Opaque +stringData: + ADMIN_TOKEN: '$argon2id$v=19$m=19456,t=2,p=1$REPLACE$REPLACE' diff --git a/apps/vaultwarden/vaultwarden-service.yaml b/apps/vaultwarden/vaultwarden-service.yaml new file mode 100644 index 0000000..6baccdb --- /dev/null +++ b/apps/vaultwarden/vaultwarden-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: vaultwarden + namespace: vaultwarden +spec: + selector: + app: vaultwarden + ports: + - name: http + port: 80 + targetPort: http