diff --git a/charts/plane-ce/questions.yml b/charts/plane-ce/questions.yml index e07059df..77ee6fc9 100644 --- a/charts/plane-ce/questions.yml +++ b/charts/plane-ce/questions.yml @@ -633,3 +633,72 @@ 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.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" diff --git a/charts/plane-ce/templates/_helpers.tpl b/charts/plane-ce/templates/_helpers.tpl index e88dc789..b8878061 100644 --- a/charts/plane-ce/templates/_helpers.tpl +++ b/charts/plane-ce/templates/_helpers.tpl @@ -131,3 +131,132 @@ Caller must nindent to the correct depth. {{- end -}} {{- end -}} {{- 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 93f3c7e2..56df00a1 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..bfce0e8f 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: @@ -66,6 +67,16 @@ 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 }} + {{- end }} readinessProbe: failureThreshold: 30 httpGet: @@ -77,6 +88,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..bf33e81f 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: @@ -43,8 +44,18 @@ 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 }} + {{- 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..d2323fde 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: @@ -61,7 +62,13 @@ spec: - 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: {{ .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..941b5fe7 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: @@ -31,8 +32,18 @@ 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 }} + {{- 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 ea26179c..19921bdc 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..0d040735 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: @@ -43,7 +44,17 @@ 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 }} + {{- 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 1995455f..9437f1af 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: "" @@ -227,7 +229,81 @@ 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: '' + # 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: {} + # 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: '' @@ -236,6 +312,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