From 77dfec24743dcd5c82dfcaf22a999381e844fe77 Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 13 Aug 2026 23:24:12 +0530 Subject: [PATCH 1/3] feat(plane-ce): the same externalized-secret contract as plane-enterprise (1.7.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The community chart had five whole-Secret hooks and nothing else, which left four values with no way out of values.yaml: the registry password, the Postgres DSN, the live server signing key, and the Cloudflare DNS token. Two of those had no hook at all, and the other two could only be moved by taking over an entire Secret and owning its config keys too. This ports the plane-enterprise contract so both charts present one surface to whatever supplies the Secrets: - serviceAccount.{create,name,annotations,podLabels,cloudIdentity} across all 12 workloads, so the pods can carry a cloud identity or run as a ServiceAccount managed outside the chart. The default name is unchanged, so the default render is untouched. - dockerRegistry.existingSecret, which did not exist — the imagePullSecret name was hardcoded, so a shared registry token could not be supplied at all. - external_secrets.ssl_token_existingSecret for the cert-manager DNS-01 token. - external_secrets.app_keys_existingSecret for SECRET_KEY and LIVE_SERVER_SECRET_KEY. LIVE_SERVER_SECRET_KEY is rendered into both the app and live Secrets and the two must agree, so one Secret makes that structural rather than something to remember. - external_secrets.storage for an S3-compatible backend with no workload identity. - env.requireExplicitSecrets, to fail the render rather than fall back to this chart's published example keys. The AWS keys in the remote-storage branch are now omitted when unset instead of rendered empty. Both halves of that matter and for different reasons: an empty AWS_ACCESS_KEY_ID is found first in the SDK credential chain and used, so it beats an attached pod identity and fails every request; and an empty AWS_REGION is *present*, so a code-side default never applies and a signed request goes out with no region at all. The app-keys Secret is mounted only on Plane's own services — api, worker, beat-worker, live and the migrator. The bundled MinIO, Postgres and RabbitMQ are the datastores themselves and take only their own credentials; the first pass over-applied it to all three, and MinIO's bucket-init job additionally uses a different indent depth, which the render caught. Rotation caveat, stated plainly rather than implied: the community image configures its database with a DSN, so there is no discrete-parts path and a managed rotation cannot reach it. The database/redis/broker credential mirrors are deliberately not added here — shipping a values surface the image cannot consume would be worse than the gap. That needs upstream app support first. Verified with hack/: the resolved environment of every container is byte-identical on the default render; eight configurations render valid YAML including all hooks at once and the legacy groups; with app_keys set neither signing key remains in any rendered Secret; in the keyless case the AWS keys and region are absent rather than empty; and requireExplicitSecrets catches a blanked key. Co-Authored-By: Claude Opus 5 (1M context) --- charts/plane-ce/Chart.yaml | 2 +- charts/plane-ce/templates/_helpers.tpl | 130 +++++++++++++++++- .../templates/certs/cert-issuers.yaml | 7 +- .../templates/config-secrets/app-env.yaml | 6 +- .../templates/config-secrets/doc-store.yaml | 21 ++- .../config-secrets/docker-registry.yaml | 4 +- .../templates/config-secrets/live-env.yaml | 4 +- .../plane-ce/templates/service-account.yaml | 15 +- .../templates/workloads/admin.deployment.yaml | 5 +- .../templates/workloads/api.deployment.yaml | 10 +- .../workloads/beat-worker.deployment.yaml | 10 +- .../templates/workloads/live.deployment.yaml | 6 +- .../templates/workloads/migrator.job.yaml | 10 +- .../templates/workloads/minio.stateful.yaml | 9 +- .../workloads/postgres.stateful.yaml | 5 +- .../workloads/rabbitmq.stateful.yaml | 5 +- .../templates/workloads/redis.stateful.yaml | 5 +- .../templates/workloads/space.deployment.yaml | 5 +- .../templates/workloads/web.deployment.yaml | 5 +- .../workloads/worker.deployment.yaml | 10 +- charts/plane-ce/values.yaml | 71 ++++++++++ 21 files changed, 305 insertions(+), 40 deletions(-) diff --git a/charts/plane-ce/Chart.yaml b/charts/plane-ce/Chart.yaml index 5d45ab7f..e8bc52bf 100644 --- a/charts/plane-ce/Chart.yaml +++ b/charts/plane-ce/Chart.yaml @@ -5,7 +5,7 @@ description: Meet Plane. An open-source software development tool to manage issu type: application -version: 1.6.2 +version: 1.7.0 appVersion: "1.4.1" home: https://plane.so diff --git a/charts/plane-ce/templates/_helpers.tpl b/charts/plane-ce/templates/_helpers.tpl index 9d81287f..f44493aa 100644 --- a/charts/plane-ce/templates/_helpers.tpl +++ b/charts/plane-ce/templates/_helpers.tpl @@ -55,4 +55,132 @@ Call with a dict carrying the root context and the component values: {{- with .values.annotations }} annotations: {{ toYaml . | nindent 4 }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} +{{/* +================================================================================ +ServiceAccount and cloud identity +================================================================================ +*/}} + +{{/* +Name of the ServiceAccount every workload runs as. Defaults to the release-scoped +account the chart creates; override to run as one you manage yourself (created by +Terraform or Crossplane, already bound to a cloud IAM role, or the target of an EKS Pod +Identity association). +*/}} +{{- define "plane.serviceAccountName" -}} +{{- .Values.serviceAccount.name | default (printf "%s-srv-account" .Release.Name) -}} +{{- end -}} + +{{/* +Returns "true" when the chart should render the ServiceAccount itself. Skipped when it +is managed outside the chart and only referenced here. +*/}} +{{- define "plane.createServiceAccount" -}} +{{- if .Values.serviceAccount.create -}} +true +{{- end -}} +{{- end -}} + +{{/* +Pod-template labels some workload-identity implementations require — Azure Workload +Identity needs azure.workload.identity/use on the pod. Indentation is baked in for the +pod-template label position, so call it bare. +*/}} +{{- define "plane.serviceAccountPodLabels" -}} +{{- with .Values.serviceAccount.podLabels }} +{{- toYaml . | nindent 8 }} +{{- end }} +{{- end -}} + +{{/* +================================================================================ +Externalized secrets +================================================================================ +*/}} + +{{/* +Resolve a secret value with an optional insecure fallback. + +Returns .value when set; otherwise fails the render when env.requireExplicitSecrets is +true, and falls back when it is false (the historical behaviour, kept so existing +installs keep working). The fallbacks this chart ships are PUBLIC CONSTANTS — any real +install must supply its own. + +Call with: (dict "context" $ "name" "env.secret_key" "value" .Values.env.secret_key "fallback" "...") +*/}} +{{- define "plane.secretValue" -}} +{{- if .value -}} +{{- .value -}} +{{- else if .context.Values.env.requireExplicitSecrets -}} +{{- required (printf "%s has no value. Set it in values.yaml, supply it through external_secrets.app_keys_existingSecret, or set env.requireExplicitSecrets=false to fall back to the chart's insecure default." .name) nil -}} +{{- else -}} +{{- .fallback -}} +{{- end -}} +{{- end -}} + +{{/* +envFrom entry for the Secret carrying the shared signing keys (SECRET_KEY, +LIVE_SERVER_SECRET_KEY). Renders nothing unless +external_secrets.app_keys_existingSecret is set. + +These keys are duplicated across the app and live Secrets and must agree for the two to +talk to each other, so pointing both at one Secret makes that agreement structural. +While it is set the chart emits neither key itself. + +SECRET_KEY additionally derives the key that encrypts the instance-configuration rows, +so it must never be rotated on a running instance. + +Indentation is baked in for the container envFrom position, so call it bare. +*/}} +{{- define "plane.appKeysSecretRef" -}} +{{- with .Values.external_secrets.app_keys_existingSecret }} + - secretRef: + name: {{ . }} + optional: false +{{- end }} +{{- end -}} + +{{/* +Returns "true" when object-storage credentials come from an externally managed Secret. +Never true while the bundled MinIO is deployed, which supplies its own. +*/}} +{{- define "plane.externalStorage" -}} +{{- if and .Values.external_secrets.storage.secretName (not .Values.minio.local_setup) -}} +true +{{- end -}} +{{- end -}} + +{{/* +Emit one env entry sourced from a key inside an externally managed Secret. +Emits a leading newline so call sites can use a left-trim marker without swallowing the +separator from the previous entry. +*/}} +{{- define "plane.secretKeyEnv" }} +- name: {{ .name }} + valueFrom: + secretKeyRef: + name: {{ .secret }} + key: {{ .key }} +{{- end -}} + +{{/* +Object-storage credentials as explicit env entries, which win over envFrom. + +On a cloud, prefer a pod identity and leave this unset: with env.aws_access_key empty +the chart omits those variables entirely, which is what lets the SDK credential chain +reach the pod's role. Use this for an S3-compatible backend with no workload identity. + +Caller must indent to the correct depth (env list items). +*/}} +{{- define "plane.storageCredsEnv" -}} +{{- $st := .Values.external_secrets.storage -}} +{{- if include "plane.externalStorage" . }} +{{- with $st.accessKeyIdKey }} +{{- include "plane.secretKeyEnv" (dict "name" "AWS_ACCESS_KEY_ID" "secret" $st.secretName "key" .) }} +{{- end }} +{{- with $st.secretAccessKeyKey }} +{{- include "plane.secretKeyEnv" (dict "name" "AWS_SECRET_ACCESS_KEY" "secret" $st.secretName "key" .) }} +{{- end }} +{{- end }} +{{- end -}} diff --git a/charts/plane-ce/templates/certs/cert-issuers.yaml b/charts/plane-ce/templates/certs/cert-issuers.yaml index fb8296c0..9f3f5f98 100644 --- a/charts/plane-ce/templates/certs/cert-issuers.yaml +++ b/charts/plane-ce/templates/certs/cert-issuers.yaml @@ -1,4 +1,6 @@ +{{- $issuerTokenSecret := .Values.external_secrets.ssl_token_existingSecret | default (printf "%s-issuer-api-token-secret" .Release.Name) }} {{- if and .Values.ingress.enabled .Values.ssl.createIssuer (empty .Values.ssl.tls_secret_name) }} +{{- if empty .Values.external_secrets.ssl_token_existingSecret }} apiVersion: v1 kind: Secret @@ -11,6 +13,7 @@ type: Opaque stringData: api-token: {{ .Values.ssl.token | default "default-api-token" | quote }} +{{- end }} --- apiVersion: cert-manager.io/v1 kind: Issuer @@ -30,14 +33,14 @@ spec: - dns01: cloudflare: apiTokenSecretRef: - name: {{ .Release.Name }}-issuer-api-token-secret + name: {{ $issuerTokenSecret }} key: api-token {{- end }} {{- if eq .Values.ssl.issuer "digitalocean" }} - dns01: digitalocean: tokenSecretRef: - name: {{ .Release.Name }}-issuer-api-token-secret + name: {{ $issuerTokenSecret }} key: api-token {{- end }} {{- if eq .Values.ssl.issuer "http" }} diff --git a/charts/plane-ce/templates/config-secrets/app-env.yaml b/charts/plane-ce/templates/config-secrets/app-env.yaml index c15b8e33..e1c4c354 100644 --- a/charts/plane-ce/templates/config-secrets/app-env.yaml +++ b/charts/plane-ce/templates/config-secrets/app-env.yaml @@ -8,8 +8,10 @@ metadata: labels: {{- include "plane.commonLabels" $ | nindent 4 }} stringData: - SECRET_KEY: {{ .Values.env.secret_key | default "60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5" | quote }} - LIVE_SERVER_SECRET_KEY: {{ .Values.env.live_server_secret_key | default "htbqvBJAgpm9bzvf3r4urJer0ENReatceh" | quote }} + {{- if empty .Values.external_secrets.app_keys_existingSecret }} + SECRET_KEY: {{ include "plane.secretValue" (dict "context" $ "name" "env.secret_key" "value" .Values.env.secret_key "fallback" "60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5") | quote }} + LIVE_SERVER_SECRET_KEY: {{ include "plane.secretValue" (dict "context" $ "name" "env.live_server_secret_key" "value" .Values.env.live_server_secret_key "fallback" "htbqvBJAgpm9bzvf3r4urJer0ENReatceh") | quote }} + {{- end }} {{- if .Values.redis.local_setup }} REDIS_URL: "redis://{{ .Release.Name }}-redis.{{ .Release.Namespace }}.svc.{{ .Values.env.default_cluster_domain | default "cluster.local" }}:6379/" diff --git a/charts/plane-ce/templates/config-secrets/doc-store.yaml b/charts/plane-ce/templates/config-secrets/doc-store.yaml index a8d45500..31dfc095 100644 --- a/charts/plane-ce/templates/config-secrets/doc-store.yaml +++ b/charts/plane-ce/templates/config-secrets/doc-store.yaml @@ -19,10 +19,23 @@ stringData: AWS_S3_ENDPOINT_URL: http://{{ .Release.Name }}-minio:9000 {{- else }} USE_MINIO: "0" - AWS_ACCESS_KEY_ID: {{ .Values.env.aws_access_key | default "" | quote }} - AWS_SECRET_ACCESS_KEY: {{ .Values.env.aws_secret_access_key | default "" | quote }} - AWS_S3_ENDPOINT_URL: {{ .Values.env.aws_s3_endpoint_url | quote }} - AWS_REGION: {{ .Values.env.aws_region | default "" | quote }} + {{/* Rendered only when supplied. Leaving these unset is the preferred configuration on + a cloud: the SDK then walks its default credential chain and picks up the pod's + identity. An empty AWS_ACCESS_KEY_ID would be found first and used, failing every + request; and an empty AWS_REGION is *present*, so a code-side default never + applies and a signed request would go out with no region. */}} + {{- with .Values.env.aws_access_key }} + AWS_ACCESS_KEY_ID: {{ . | quote }} + {{- end }} + {{- with .Values.env.aws_secret_access_key }} + AWS_SECRET_ACCESS_KEY: {{ . | quote }} + {{- end }} + {{- with .Values.env.aws_s3_endpoint_url }} + AWS_S3_ENDPOINT_URL: {{ . | quote }} + {{- end }} + {{- with .Values.env.aws_region }} + AWS_REGION: {{ . | quote }} + {{- end }} {{- end }} --- {{- end }} \ No newline at end of file diff --git a/charts/plane-ce/templates/config-secrets/docker-registry.yaml b/charts/plane-ce/templates/config-secrets/docker-registry.yaml index 9819c77a..ffe867a9 100644 --- a/charts/plane-ce/templates/config-secrets/docker-registry.yaml +++ b/charts/plane-ce/templates/config-secrets/docker-registry.yaml @@ -1,3 +1,4 @@ +{{- if empty .Values.dockerRegistry.existingSecret }} {{- if .Values.dockerRegistry.enabled }} apiVersion: v1 @@ -11,4 +12,5 @@ data: .dockerconfigjson: {{ include "imagePullSecret" .}} type: kubernetes.io/dockerconfigjson -{{- end }} \ No newline at end of file +{{- end }} +{{- end }} diff --git a/charts/plane-ce/templates/config-secrets/live-env.yaml b/charts/plane-ce/templates/config-secrets/live-env.yaml index 6d176ed7..f970cf05 100644 --- a/charts/plane-ce/templates/config-secrets/live-env.yaml +++ b/charts/plane-ce/templates/config-secrets/live-env.yaml @@ -8,7 +8,9 @@ metadata: labels: {{- include "plane.commonLabels" $ | nindent 4 }} stringData: - LIVE_SERVER_SECRET_KEY: {{ .Values.env.live_server_secret_key | default "htbqvBJAgpm9bzvf3r4urJer0ENReatceh" | quote }} + {{- if empty .Values.external_secrets.app_keys_existingSecret }} + LIVE_SERVER_SECRET_KEY: {{ include "plane.secretValue" (dict "context" $ "name" "env.live_server_secret_key" "value" .Values.env.live_server_secret_key "fallback" "htbqvBJAgpm9bzvf3r4urJer0ENReatceh") | quote }} + {{- end }} {{- if .Values.redis.local_setup }} REDIS_URL: "redis://{{ .Release.Name }}-redis.{{ .Release.Namespace }}.svc.cluster.local:6379/" {{- else }} diff --git a/charts/plane-ce/templates/service-account.yaml b/charts/plane-ce/templates/service-account.yaml index 6e4e04b6..c9a02ef3 100644 --- a/charts/plane-ce/templates/service-account.yaml +++ b/charts/plane-ce/templates/service-account.yaml @@ -1,12 +1,23 @@ +{{- if include "plane.createServiceAccount" . }} apiVersion: v1 automountServiceAccountToken: true kind: ServiceAccount metadata: namespace: {{ .Release.Namespace }} - name: {{ .Release.Name }}-srv-account + name: {{ include "plane.serviceAccountName" . }} labels: {{- include "plane.commonLabels" $ | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + {{/* Cloud workload-identity bindings go here — eks.amazonaws.com/role-arn (IRSA), + iam.gke.io/gcp-service-account, azure.workload.identity/client-id. */}} + annotations: {{ toYaml . | nindent 4 }} + {{- end }} {{- if .Values.dockerRegistry.enabled }} imagePullSecrets: +{{- if .Values.dockerRegistry.existingSecret }} + - name: {{ .Values.dockerRegistry.existingSecret }} +{{- else }} - name: {{ .Release.Name }}-docker-registry-credentials -{{- end}} \ No newline at end of file +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/plane-ce/templates/workloads/admin.deployment.yaml b/charts/plane-ce/templates/workloads/admin.deployment.yaml index 93711c9a..8f2f7a84 100644 --- a/charts/plane-ce/templates/workloads/admin.deployment.yaml +++ b/charts/plane-ce/templates/workloads/admin.deployment.yaml @@ -38,6 +38,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-admin {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -55,6 +56,6 @@ spec: memory: {{ .Values.admin.memoryLimit | default "1000Mi" | quote }} cpu: {{ .Values.admin.cpuLimit | default "500m" | quote}} {{- include "plane.podScheduling" .Values.admin }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- \ No newline at end of file diff --git a/charts/plane-ce/templates/workloads/api.deployment.yaml b/charts/plane-ce/templates/workloads/api.deployment.yaml index 0daf2279..0431468a 100644 --- a/charts/plane-ce/templates/workloads/api.deployment.yaml +++ b/charts/plane-ce/templates/workloads/api.deployment.yaml @@ -38,6 +38,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-api {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -57,6 +58,7 @@ spec: command: - ./bin/docker-entrypoint-api.sh envFrom: + {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -66,6 +68,10 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- with (include "plane.storageCredsEnv" .) }} + env: +{{ trim . | indent 10 }} + {{- end }} readinessProbe: failureThreshold: 30 httpGet: @@ -77,6 +83,6 @@ spec: timeoutSeconds: 1 {{- include "plane.podScheduling" .Values.api }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- \ No newline at end of file diff --git a/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml b/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml index 637bd667..af0bb2b3 100644 --- a/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml +++ b/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml @@ -15,6 +15,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-beat-worker {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -34,6 +35,7 @@ spec: command: - ./bin/docker-entrypoint-beat.sh envFrom: + {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -44,7 +46,11 @@ spec: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- with (include "plane.storageCredsEnv" .) }} + env: +{{ trim . | indent 10 }} + {{- end }} {{- include "plane.podScheduling" .Values.beatworker }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- \ No newline at end of file diff --git a/charts/plane-ce/templates/workloads/live.deployment.yaml b/charts/plane-ce/templates/workloads/live.deployment.yaml index f6045e5e..09a77bda 100644 --- a/charts/plane-ce/templates/workloads/live.deployment.yaml +++ b/charts/plane-ce/templates/workloads/live.deployment.yaml @@ -38,6 +38,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-live {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -55,6 +56,7 @@ spec: memory: {{ .Values.live.memoryLimit | default "1000Mi" | quote }} cpu: {{ .Values.live.cpuLimit | default "500m" | quote}} envFrom: + {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-live-vars optional: false @@ -62,6 +64,6 @@ spec: name: {{ if not (empty .Values.external_secrets.live_env_existingSecret) }}{{ .Values.external_secrets.live_env_existingSecret }}{{ else }}{{ .Release.Name }}-live-secrets{{ end }} optional: false {{- include "plane.podScheduling" .Values.live }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- diff --git a/charts/plane-ce/templates/workloads/migrator.job.yaml b/charts/plane-ce/templates/workloads/migrator.job.yaml index c84530ab..57759ef9 100644 --- a/charts/plane-ce/templates/workloads/migrator.job.yaml +++ b/charts/plane-ce/templates/workloads/migrator.job.yaml @@ -12,6 +12,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-api-migrate {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -22,6 +23,7 @@ spec: - ./bin/docker-entrypoint-migrator.sh imagePullPolicy: {{ .Values.api.pullPolicy | default "Always" }} envFrom: + {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -31,8 +33,12 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- with (include "plane.storageCredsEnv" .) }} + env: +{{ trim . | indent 10 }} + {{- end }} restartPolicy: OnFailure - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} {{- include "plane.podScheduling" .Values.api }} --- diff --git a/charts/plane-ce/templates/workloads/minio.stateful.yaml b/charts/plane-ce/templates/workloads/minio.stateful.yaml index ddf5e329..35411686 100644 --- a/charts/plane-ce/templates/workloads/minio.stateful.yaml +++ b/charts/plane-ce/templates/workloads/minio.stateful.yaml @@ -41,6 +41,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-minio {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} spec: containers: - image: {{ .Values.minio.image }} @@ -63,8 +64,8 @@ spec: name: pvc-{{ .Release.Name }}-minio-vol subPath: '' {{- include "plane.podScheduling" .Values.minio }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim @@ -119,7 +120,7 @@ spec: imagePullPolicy: {{ .Values.minio.pullPolicy }} name: {{ .Release.Name }}-minio-bucket {{- include "plane.podScheduling" .Values.minio }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} terminationGracePeriodSeconds: 120 {{- end }} \ No newline at end of file diff --git a/charts/plane-ce/templates/workloads/postgres.stateful.yaml b/charts/plane-ce/templates/workloads/postgres.stateful.yaml index 1f4ed489..bee68fa0 100644 --- a/charts/plane-ce/templates/workloads/postgres.stateful.yaml +++ b/charts/plane-ce/templates/workloads/postgres.stateful.yaml @@ -37,6 +37,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-pgdb {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} spec: containers: - image: {{ .Values.postgres.image }} @@ -55,8 +56,8 @@ spec: name: pvc-{{ .Release.Name }}-pgdb-vol subPath: '' {{- include "plane.podScheduling" .Values.postgres }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim diff --git a/charts/plane-ce/templates/workloads/rabbitmq.stateful.yaml b/charts/plane-ce/templates/workloads/rabbitmq.stateful.yaml index 916a1ca6..f770fdff 100644 --- a/charts/plane-ce/templates/workloads/rabbitmq.stateful.yaml +++ b/charts/plane-ce/templates/workloads/rabbitmq.stateful.yaml @@ -41,6 +41,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-rabbitmq {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} spec: containers: - image: {{ .Values.rabbitmq.image }} @@ -57,8 +58,8 @@ spec: name: pvc-{{ .Release.Name }}-rabbitmq-vol subPath: '' {{- include "plane.podScheduling" .Values.rabbitmq }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim diff --git a/charts/plane-ce/templates/workloads/redis.stateful.yaml b/charts/plane-ce/templates/workloads/redis.stateful.yaml index 3b742d3e..3e300a44 100644 --- a/charts/plane-ce/templates/workloads/redis.stateful.yaml +++ b/charts/plane-ce/templates/workloads/redis.stateful.yaml @@ -39,6 +39,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-redis {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} spec: containers: - image: {{ .Values.redis.image }} @@ -51,8 +52,8 @@ spec: name: pvc-{{ .Release.Name }}-redis-vol subPath: '' {{- include "plane.podScheduling" .Values.redis }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim diff --git a/charts/plane-ce/templates/workloads/space.deployment.yaml b/charts/plane-ce/templates/workloads/space.deployment.yaml index d2517322..b1a6bcac 100644 --- a/charts/plane-ce/templates/workloads/space.deployment.yaml +++ b/charts/plane-ce/templates/workloads/space.deployment.yaml @@ -38,6 +38,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-space {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -55,6 +56,6 @@ spec: memory: {{ .Values.space.memoryLimit | default "1000Mi" | quote }} cpu: {{ .Values.space.cpuLimit | default "500m" | quote}} {{- include "plane.podScheduling" .Values.space }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- \ No newline at end of file diff --git a/charts/plane-ce/templates/workloads/web.deployment.yaml b/charts/plane-ce/templates/workloads/web.deployment.yaml index 5ebc2f31..d681b36d 100644 --- a/charts/plane-ce/templates/workloads/web.deployment.yaml +++ b/charts/plane-ce/templates/workloads/web.deployment.yaml @@ -38,6 +38,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-web {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -55,6 +56,6 @@ spec: memory: {{ .Values.web.memoryLimit | default "1000Mi" | quote }} cpu: {{ .Values.web.cpuLimit | default "500m" | quote}} {{- include "plane.podScheduling" .Values.web }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- \ No newline at end of file diff --git a/charts/plane-ce/templates/workloads/worker.deployment.yaml b/charts/plane-ce/templates/workloads/worker.deployment.yaml index e6a080d2..0532e729 100644 --- a/charts/plane-ce/templates/workloads/worker.deployment.yaml +++ b/charts/plane-ce/templates/workloads/worker.deployment.yaml @@ -15,6 +15,7 @@ spec: labels: app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-worker {{- include "plane.commonLabels" $ | nindent 8 }} + {{- include "plane.serviceAccountPodLabels" . }} annotations: timestamp: {{ now | quote }} spec: @@ -34,6 +35,7 @@ spec: command: - ./bin/docker-entrypoint-worker.sh envFrom: + {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -43,7 +45,11 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- with (include "plane.storageCredsEnv" .) }} + env: +{{ trim . | indent 10 }} + {{- end }} {{- include "plane.podScheduling" .Values.worker }} - serviceAccount: {{ .Release.Name }}-srv-account - serviceAccountName: {{ .Release.Name }}-srv-account + serviceAccount: {{ include "plane.serviceAccountName" . }} + serviceAccountName: {{ include "plane.serviceAccountName" . }} --- \ No newline at end of file diff --git a/charts/plane-ce/values.yaml b/charts/plane-ce/values.yaml index 87fc8416..ea258308 100644 --- a/charts/plane-ce/values.yaml +++ b/charts/plane-ce/values.yaml @@ -2,6 +2,8 @@ planeVersion: v1.4.1 dockerRegistry: enabled: false + # Name of an existing Secret holding docker registry credentials. + existingSecret: '' host: "index.docker.io/v1/" loginid: "" password: "" @@ -198,7 +200,72 @@ beatworker: tolerations: [] affinity: {} +# ============================================================ +# ServiceAccount / cloud workload identity +# ============================================================ +# All workloads run as one ServiceAccount. Annotate it to give the pods a cloud identity, +# which is the preferred way to reach object storage — no access keys in the cluster: +# +# AWS IRSA: eks.amazonaws.com/role-arn: arn:aws:iam:::role/ +# AWS EKS Pod Identity: no annotation — create the association against this +# ServiceAccount's name instead +# GCP Workload Identity: iam.gke.io/gcp-service-account: @.iam.gserviceaccount.com +# Azure Workload Id: azure.workload.identity/client-id: +# plus podLabels: { azure.workload.identity/use: "true" } +# +# With an identity attached, leave env.aws_access_key / aws_secret_access_key empty: the +# chart then omits those variables entirely so the SDK's default credential chain picks +# up the pod's role. An empty value would beat the chain and fail every request. +serviceAccount: + # Set false to reference a ServiceAccount managed outside the chart. + create: true + # Defaults to "-srv-account". + name: '' + annotations: {} + # Extra pod-template labels (Azure Workload Identity requires one). + podLabels: {} + # Declares that this ServiceAccount is bound to a cloud identity configured OUT OF + # BAND. The chart cannot detect that — a Pod Identity association is an EKS API object + # keyed on cluster + namespace + service account, invisible to the pod spec. Setting + # this changes no rendered output; it only enables warnings about the interactions an + # attached identity creates. + cloudIdentity: false + +# ============================================================ +# Externalized secrets +# ============================================================ +# Every hook takes the NAME of a Kubernetes Secret you supply, so any backend works — +# External Secrets Operator against a cloud secret manager, sealed-secrets, or a Secret +# you create by hand. The chart never renders ExternalSecret resources itself, which is +# what keeps it backend-agnostic. +# +# Because the chart only takes names, sharing is entirely your choice: point several +# environments at one Secret where the credential really is shared (a registry token), +# and at per-environment Secrets everywhere else. The values look the same either way. external_secrets: + # Shared signing keys: SECRET_KEY and LIVE_SERVER_SECRET_KEY. Both are duplicated + # across the app and live Secrets today and must agree for those services to talk to + # each other, so one Secret makes that structural. + # + # WARNING — SECRET_KEY derives the key that encrypts the instance-configuration rows + # (SMTP password, OAuth client secrets). Changing it makes them undecryptable, and it + # fails silently. Keep it in a Secret with no rotation schedule, and per environment: + # it is bound to one database's ciphertext. + app_keys_existingSecret: '' + + # DNS-01 API token for the cert-manager Issuer (ssl.issuer cloudflare/digitalocean). + # Must contain the key `api-token`. Naturally shared — one token usually covers a + # whole wildcard domain. + ssl_token_existingSecret: '' + + storage: + # Object-storage credentials. On a cloud, leave this unset and give the pod an + # identity instead (see serviceAccount above). Use it for an S3-compatible backend + # with no workload identity. Ignored while the bundled MinIO is deployed. + secretName: '' + accessKeyIdKey: '' # -> AWS_ACCESS_KEY_ID + secretAccessKeyKey: '' # -> AWS_SECRET_ACCESS_KEY + # Name of the existing Kubernetes Secret resource; see README for more details rabbitmq_existingSecret: '' pgdb_existingSecret: '' @@ -207,6 +274,10 @@ external_secrets: live_env_existingSecret: '' env: + # Fail the render instead of falling back to this chart's PUBLIC example values for + # SECRET_KEY and LIVE_SERVER_SECRET_KEY. Recommended for anything real; defaults to + # false so existing installs keep rendering. + requireExplicitSecrets: false # NEXT_PUBLIC_DEPLOY_URL: "" # REDIS From 8c376e3f6599b1b56012a8976505d96da4a50090 Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 13 Aug 2026 23:54:54 +0530 Subject: [PATCH 2/3] feat(plane-ce): expose the 1.7.0 secret hooks in questions.yml Same gap as plane-enterprise: the hooks existed only in values.yaml, so the Rancher UI offered no way to reach them. Adds live_env_existingSecret (already in values but never surfaced), the two shared key groups, the storage credential Secret, the registry pull secret, the ServiceAccount block and env.requireExplicitSecrets. The SECRET_KEY description spells out the consequence rather than the mechanism: it derives the key that encrypts the instance-configuration rows, so changing it makes the stored SMTP password and OAuth client secrets undecryptable, and it fails silently. --- charts/plane-ce/questions.yml | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/charts/plane-ce/questions.yml b/charts/plane-ce/questions.yml index 9c865e31..5f1df292 100644 --- a/charts/plane-ce/questions.yml +++ b/charts/plane-ce/questions.yml @@ -628,3 +628,78 @@ questions: type: string default: "" group: "External Secrets" +- variable: external_secrets.live_env_existingSecret + label: "Live Envs Secrets File Name" + type: string + default: "" + group: "External Secrets" + +# --- Shared key groups -------------------------------------------------------- +- variable: external_secrets.app_keys_existingSecret + label: "Signing Keys Secret" + description: "SECRET_KEY and LIVE_SERVER_SECRET_KEY. SECRET_KEY derives the key that encrypts the instance-configuration rows — SMTP password, OAuth client secrets — so changing it makes them undecryptable, silently. Keep it per environment and never rotate it." + type: string + default: "" + group: "External Secrets" +- variable: external_secrets.ssl_token_existingSecret + label: "DNS-01 Token Secret" + description: "cert-manager DNS provider API token, under the key api-token. Usually one token covers a whole wildcard domain, so this is safe to share." + type: string + default: "" + group: "External Secrets" +- variable: external_secrets.storage.secretName + label: "Object Storage Credential Secret" + description: "Leave empty on a cloud and annotate the ServiceAccount instead — a pod identity beats a stored access key. Ignored while the bundled MinIO is deployed." + type: string + default: "" + group: "External Secrets" +- variable: external_secrets.storage.accessKeyIdKey + label: "Storage Access Key ID Key" + type: string + default: "" + group: "External Secrets" + show_if: "external_secrets.storage.secretName!=" +- variable: external_secrets.storage.secretAccessKeyKey + label: "Storage Secret Access Key Key" + type: string + default: "" + group: "External Secrets" + show_if: "external_secrets.storage.secretName!=" +- variable: dockerRegistry.existingSecret + label: "Existing Image Pull Secret" + description: "Use a pull secret that already exists in the namespace instead of building one from the login below." + type: string + default: "" + group: "Docker Registry" + +# --- Cloud identity ----------------------------------------------------------- +- variable: serviceAccount.create + label: "Create the ServiceAccount" + description: "Turn off to run as a ServiceAccount managed outside this chart." + type: boolean + default: true + group: "Service Account" +- variable: serviceAccount.name + label: "ServiceAccount Name" + description: "Defaults to -srv-account." + type: string + default: "" + group: "Service Account" +- variable: serviceAccount.annotations + label: "ServiceAccount Annotations" + description: "Where a workload identity binding goes: eks.amazonaws.com/role-arn for IRSA, iam.gke.io/gcp-service-account for GKE, azure.workload.identity/client-id for Azure. EKS Pod Identity needs no annotation — it binds by name." + type: string + default: "" + group: "Service Account" +- variable: serviceAccount.cloudIdentity + label: "ServiceAccount Has a Cloud Identity" + description: "Advisory. Turns on the install notes that catch a static credential shadowing the identity." + type: boolean + default: false + group: "Service Account" +- variable: env.requireExplicitSecrets + label: "Refuse to Render Default Signing Keys" + description: "Fails the render instead of falling back to this chart's PUBLIC example values for SECRET_KEY and LIVE_SERVER_SECRET_KEY." + type: boolean + default: false + group: "Service Account" From 1c7ff849e15dbde0efbc334ab2edbffa2bbb502e Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 27 Aug 2026 16:27:04 +0530 Subject: [PATCH 3/3] fix(plane-ce): let the app-keys Secret win, and stop offering annotations in Rancher Two review findings, both real. plane.appKeysSecretRef was FIRST in envFrom on all five workloads that use it. envFrom is later-source-wins, so an operator setting app_keys_existingSecret alongside app_env_existingSecret or live_env_existingSecret would have the latter override SECRET_KEY and LIVE_SERVER_SECRET_KEY. That defeats the hook's only purpose: those two keys are rendered into both the app and live Secrets and must agree, and an override landing on one workload but not the other breaks live-server auth with no error anywhere. Moved last on api, worker, beat-worker, live and the migrator, with the reason recorded at each call site so it does not drift back. questions.yml declared serviceAccount.annotations as type: string. Rancher sends that as a scalar and the template toYaml's it straight into metadata.annotations, which Kubernetes requires to be a map -- so a non-empty answer produced an invalid ServiceAccount. No Rancher question type yields a map, so the field cannot be offered there safely; the question is removed and values.yaml now says the field is Helm-only. Helm users are unaffected either way, and the "" default meant this only ever broke through the UI. values.yaml also now states what the shared account implies: every workload uses it, datastores included, so an annotation binding a cloud role grants that role to postgres, redis, rabbitmq and minio as well. serviceAccount.name is the way out. Verified: app-keys is last in all five envFrom chains with both hooks set; default render is still 0 lines different from master; a sentinel credential appears 0 times with the hook set; lint clean, 32 documents. Co-Authored-By: Claude Opus 5 (1M context) --- charts/plane-ce/questions.yml | 6 ------ charts/plane-ce/templates/workloads/api.deployment.yaml | 7 ++++++- .../templates/workloads/beat-worker.deployment.yaml | 7 ++++++- charts/plane-ce/templates/workloads/live.deployment.yaml | 7 ++++++- charts/plane-ce/templates/workloads/migrator.job.yaml | 7 ++++++- .../plane-ce/templates/workloads/worker.deployment.yaml | 7 ++++++- charts/plane-ce/values.yaml | 9 +++++++++ 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/charts/plane-ce/questions.yml b/charts/plane-ce/questions.yml index b2a3137d..77ee6fc9 100644 --- a/charts/plane-ce/questions.yml +++ b/charts/plane-ce/questions.yml @@ -690,12 +690,6 @@ questions: type: string default: "" group: "Service Account" -- variable: serviceAccount.annotations - label: "ServiceAccount Annotations" - description: "Where a workload identity binding goes: eks.amazonaws.com/role-arn for IRSA, iam.gke.io/gcp-service-account for GKE, azure.workload.identity/client-id for Azure. EKS Pod Identity needs no annotation — it binds by name." - type: string - default: "" - group: "Service Account" - variable: serviceAccount.cloudIdentity label: "ServiceAccount Has a Cloud Identity" description: "Advisory. Turns on the install notes that catch a static credential shadowing the identity." diff --git a/charts/plane-ce/templates/workloads/api.deployment.yaml b/charts/plane-ce/templates/workloads/api.deployment.yaml index 0431468a..bfce0e8f 100644 --- a/charts/plane-ce/templates/workloads/api.deployment.yaml +++ b/charts/plane-ce/templates/workloads/api.deployment.yaml @@ -58,7 +58,6 @@ spec: command: - ./bin/docker-entrypoint-api.sh envFrom: - {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -68,6 +67,12 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- /* LAST in envFrom, deliberately. envFrom is later-source-wins, so this has to + follow app_env/live_env: those may carry their own SECRET_KEY, and this hook + exists precisely to make the app and live copies agree. First would let a + stale key in an operator-supplied Secret win on one workload and not the + other, which breaks live-server auth with no error. */}} + {{- include "plane.appKeysSecretRef" . }} {{- with (include "plane.storageCredsEnv" .) }} env: {{ trim . | indent 10 }} diff --git a/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml b/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml index af0bb2b3..bf33e81f 100644 --- a/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml +++ b/charts/plane-ce/templates/workloads/beat-worker.deployment.yaml @@ -35,7 +35,6 @@ spec: command: - ./bin/docker-entrypoint-beat.sh envFrom: - {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -45,6 +44,12 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- /* LAST in envFrom, deliberately. envFrom is later-source-wins, so this has to + follow app_env/live_env: those may carry their own SECRET_KEY, and this hook + exists precisely to make the app and live copies agree. First would let a + stale key in an operator-supplied Secret win on one workload and not the + other, which breaks live-server auth with no error. */}} + {{- include "plane.appKeysSecretRef" . }} {{- with (include "plane.storageCredsEnv" .) }} env: diff --git a/charts/plane-ce/templates/workloads/live.deployment.yaml b/charts/plane-ce/templates/workloads/live.deployment.yaml index 09a77bda..d2323fde 100644 --- a/charts/plane-ce/templates/workloads/live.deployment.yaml +++ b/charts/plane-ce/templates/workloads/live.deployment.yaml @@ -56,13 +56,18 @@ spec: memory: {{ .Values.live.memoryLimit | default "1000Mi" | quote }} cpu: {{ .Values.live.cpuLimit | default "500m" | quote}} envFrom: - {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-live-vars optional: false - secretRef: name: {{ if not (empty .Values.external_secrets.live_env_existingSecret) }}{{ .Values.external_secrets.live_env_existingSecret }}{{ else }}{{ .Release.Name }}-live-secrets{{ end }} optional: false + {{- /* LAST in envFrom, deliberately. envFrom is later-source-wins, so this has to + follow app_env/live_env: those may carry their own SECRET_KEY, and this hook + exists precisely to make the app and live copies agree. First would let a + stale key in an operator-supplied Secret win on one workload and not the + other, which breaks live-server auth with no error. */}} + {{- include "plane.appKeysSecretRef" . }} {{- include "plane.podScheduling" .Values.live }} serviceAccount: {{ include "plane.serviceAccountName" . }} serviceAccountName: {{ include "plane.serviceAccountName" . }} diff --git a/charts/plane-ce/templates/workloads/migrator.job.yaml b/charts/plane-ce/templates/workloads/migrator.job.yaml index 57759ef9..941b5fe7 100644 --- a/charts/plane-ce/templates/workloads/migrator.job.yaml +++ b/charts/plane-ce/templates/workloads/migrator.job.yaml @@ -23,7 +23,6 @@ spec: - ./bin/docker-entrypoint-migrator.sh imagePullPolicy: {{ .Values.api.pullPolicy | default "Always" }} envFrom: - {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -33,6 +32,12 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- /* LAST in envFrom, deliberately. envFrom is later-source-wins, so this has to + follow app_env/live_env: those may carry their own SECRET_KEY, and this hook + exists precisely to make the app and live copies agree. First would let a + stale key in an operator-supplied Secret win on one workload and not the + other, which breaks live-server auth with no error. */}} + {{- include "plane.appKeysSecretRef" . }} {{- with (include "plane.storageCredsEnv" .) }} env: {{ trim . | indent 10 }} diff --git a/charts/plane-ce/templates/workloads/worker.deployment.yaml b/charts/plane-ce/templates/workloads/worker.deployment.yaml index 0532e729..0d040735 100644 --- a/charts/plane-ce/templates/workloads/worker.deployment.yaml +++ b/charts/plane-ce/templates/workloads/worker.deployment.yaml @@ -35,7 +35,6 @@ spec: command: - ./bin/docker-entrypoint-worker.sh envFrom: - {{- include "plane.appKeysSecretRef" . }} - configMapRef: name: {{ .Release.Name }}-app-vars optional: false @@ -45,6 +44,12 @@ spec: - secretRef: name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }} optional: false + {{- /* LAST in envFrom, deliberately. envFrom is later-source-wins, so this has to + follow app_env/live_env: those may carry their own SECRET_KEY, and this hook + exists precisely to make the app and live copies agree. First would let a + stale key in an operator-supplied Secret win on one workload and not the + other, which breaks live-server auth with no error. */}} + {{- include "plane.appKeysSecretRef" . }} {{- with (include "plane.storageCredsEnv" .) }} env: {{ trim . | indent 10 }} diff --git a/charts/plane-ce/values.yaml b/charts/plane-ce/values.yaml index 372a3415..9437f1af 100644 --- a/charts/plane-ce/values.yaml +++ b/charts/plane-ce/values.yaml @@ -250,6 +250,15 @@ serviceAccount: create: true # Defaults to "-srv-account". name: '' + # A map, and Helm-values only -- not offered in the Rancher UI, because no Rancher + # question type yields a map and a scalar answer renders an invalid ServiceAccount. + # Where a workload identity binding goes: eks.amazonaws.com/role-arn for IRSA, + # iam.gke.io/gcp-service-account for GKE, azure.workload.identity/client-id for Azure. + # EKS Pod Identity needs no annotation -- it binds by name. + # + # NOTE: every workload shares this account, the datastores included. An annotation here + # grants the role to postgres, redis, rabbitmq and minio too. Point serviceAccount.name + # at your own least-privilege account if that matters. annotations: {} # Extra pod-template labels (Azure Workload Identity requires one). podLabels: {}