diff --git a/charts/plane-ce/Chart.yaml b/charts/plane-ce/Chart.yaml index f173b2df..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.3 +version: 1.7.0 appVersion: "1.4.1" home: https://plane.so diff --git a/charts/plane-ce/README.md b/charts/plane-ce/README.md index fad4dc3c..dc531a13 100644 --- a/charts/plane-ce/README.md +++ b/charts/plane-ce/README.md @@ -398,12 +398,14 @@ The default value is `"traefik"`. If you previously relied on the implicit defau | ssl.email | | | Certificate generation authority needs a valid email id before generating certificate. Required when `ssl.createIssuer=true` | | ssl.generateCerts | false | | After creating the issuers, user can still not create the certificate untill sure of configuration. Setting this to `true` will try to generate SSL certificate and associate with ingress. Applicable only when `ingress.enabled=true` and `ssl.createIssuer=true` | | ssl.tls_secret_name | | | If you have a custom TLS secret name, set this to the name of the secret. Applicable only when `ingress.enabled=true` and `ssl.createIssuer=false` | +| ssl.externalTermination | false | | Set `true` when TLS is terminated **in front of** Plane and this chart manages no certificate — a cloud load balancer, Cloudflare, a service mesh, or a Traefik entrypoint carrying its own cert. Renders `WEB_URL` as `https://` and emits **no** `tls:` block. Does not move the Traefik entrypoint; see [TLS options](#tls-options-choosing-how-https-is-handled), option 4a/4b. | +| ingress.traefik.entryPoints | `[]` | | Traefik entrypoints the `IngressRoute`s attach to. Empty (default) derives them from your TLS settings: `websecure` when this chart terminates TLS, otherwise `web`. Set explicitly if your Traefik renamed the defaults, to serve both schemes, or to select `websecure` for option 4b. A bare string is accepted. **Only used when `ingressClass` starts with `traefik`.** | #### Using Traefik as the ingress controller When `ingress.ingressClass` starts with `traefik`, the chart deploys native Traefik CRDs instead of a standard `Ingress` resource: -- **`IngressRoute`** (`traefik.io/v1alpha1`) — routes traffic to each Plane service via `Host` + `PathPrefix` rules on the `websecure` entrypoint +- **`IngressRoute`** (`traefik.io/v1alpha1`) — routes traffic to each Plane service via `Host` + `PathPrefix` rules, on the entrypoint derived from your TLS settings (see [TLS options](#tls-options-choosing-how-https-is-handled)) - **`Middleware`** (`traefik.io/v1alpha1`) — enforces a request body size limit on every route (default 5 MiB, configurable via `ingress.traefik.maxRequestBodyBytes`) This requires the Traefik Helm chart to be installed with `providers.kubernetesCRD.enabled=true` (enabled by default in Traefik v3), as shown in the pre-requisites above. @@ -420,20 +422,220 @@ ingress: ingressClass: 'nginx' ``` -**TLS** +#### TLS options: choosing how HTTPS is handled -The `IngressRoute` always references a TLS secret. Provide it via `ssl.tls_secret_name`, or let cert-manager generate it by setting `ssl.createIssuer=true` and `ssl.generateCerts=true` (the chart will create the secret named `-ssl-cert` automatically). +TLS is **optional**. Your `ssl.*` settings drive two *separate* derivations — +separate because "users are on HTTPS" and "this chart holds the certificate" are +different facts: + +1. whether a `tls:` block is emitted, and which Traefik entrypoint the + `IngressRoute`s bind to — both from whether **this chart** terminates TLS; +2. the scheme of `WEB_URL`, the URL Plane is told about itself. + (`CORS_ALLOWED_ORIGINS` always lists both schemes and is unaffected.) + +Find the row that matches your environment: + +| Your setup | Set | Entrypoint | `tls:` block | `WEB_URL` | +| --- | --- | :---: | :---: | :---: | +| No certificate yet — trial, internal network | *nothing* (default) | `web` | — | `http://` | +| You already hold a TLS Secret | `ssl.tls_secret_name` | `websecure` | your Secret | `https://` | +| Let cert-manager issue one | `ssl.createIssuer` + `ssl.generateCerts` | `websecure` | `-ssl-cert` | `https://` | +| TLS terminated upstream (ALB, NLB TLS listener, Cloudflare) | `ssl.externalTermination: true` | `web` | — | `https://` | +| TLS terminated by Traefik's own entrypoint | `ssl.externalTermination: true` + `ingress.traefik.entryPoints: ['websecure']` | `websecure` | — | `https://` | + +Only the `tls:` block requires a Secret this chart can actually see, which is why +the last two rows emit none — the chart never names a Secret it does not create. + +Note the last two rows share a scheme but need **opposite entrypoints**: an +upstream terminator forwards cleartext, which arrives on `web`, whereas a Traefik +entrypoint carrying its own certificate serves TLS on `websecure`. That is why +`ssl.externalTermination` sets the URL scheme only and never moves the +entrypoint. + +All three `IngressRoute`s the chart can emit — the app, the MinIO console +(`ingress.minioHost`) and the RabbitMQ console (`ingress.rabbitmqHost`) — follow +the same derivation. + +##### Option 1 — No TLS, plain HTTP + +The default. Nothing to set; leave the `ssl` block alone: + +```yaml +ingress: + enabled: true + ingressClass: traefik + appHost: plane.example.com +``` + +Renders `entryPoints: ['web']`, no `tls:` block, and `WEB_URL: "http://plane.example.com"`. +Good for a trial, an internal network, or while you are still sorting out DNS and +certificates. See the [caveat](#caveat-check-your-traefik-entrypoints-before-relying-on-plain-http) +below before relying on it. + +##### Option 2 — Bring your own certificate + +You already hold a `kubernetes.io/tls` Secret in the release namespace: + +```yaml +ssl: + tls_secret_name: 'my-tls-secret' +``` + +Renders `entryPoints: ['websecure']`, `tls.secretName: my-tls-secret`, and +`WEB_URL: "https://..."`. + +##### Option 3 — Let cert-manager issue the certificate + +Requires cert-manager installed in the cluster and a publicly-resolvable host if +you use the HTTP-01 challenge: ```yaml ssl: - tls_secret_name: "my-tls-secret" # bring your own secret - # — OR — createIssuer: true generateCerts: true - issuer: http + issuer: http # or cloudflare / digitalocean for DNS-01 email: you@example.com ``` +The chart creates an `Issuer` and a `Certificate`, cert-manager writes +`-ssl-cert`, and the routes reference it. **Both** `createIssuer` and +`generateCerts` are required — `generateCerts` alone creates nothing and is +treated as "no TLS". + +For DNS-01 also set `ssl.token` to your provider API token. To test without +burning Let's Encrypt rate limits, point at staging first: + +```yaml +ssl: + server: https://acme-staging-v02.api.letsencrypt.org/directory +``` + +##### Option 4 — TLS terminated in front of Plane + +Use this when something ahead of Plane already terminates TLS and this chart +manages no certificate. `ssl.externalTermination` renders `WEB_URL` as `https://` +and emits no `tls:` block. It does **not** move the entrypoint, so pick the +sub-case that matches where TLS actually ends. + +**4a — an upstream terminator forwards cleartext** (ALB with an ACM cert, NLB +with a TLS listener, Cloudflare, most service meshes). Traffic reaches Traefik as +plain HTTP, so the routes stay on `web` — the default: + +```yaml +ssl: + externalTermination: true +``` + +**4b — Traefik's own entrypoint terminates TLS** (`websecure.http.tls=true`, an +ACME `certResolver`, or a default `TLSStore`). Traffic reaches Traefik as TLS, so +the routes must bind `websecure` as well: + +```yaml +ssl: + externalTermination: true +ingress: + traefik: + entryPoints: ['websecure'] +``` + +Getting the sub-case wrong is a routing failure, not a certificate failure: a +route bound only to `websecure` never matches cleartext arriving on `web`, so +requests 404 instead of reaching Plane. + +Leave `externalTermination` `false` if you set `ssl.tls_secret_name` or +`ssl.generateCerts`; those already imply HTTPS. Use it *only* for TLS this chart +cannot see. Without it, such an install would advertise `http://` to itself while +being served over HTTPS, breaking OAuth callbacks and asset links. + +##### Overriding the entrypoint names + +Only needed if your Traefik installation renamed the default `web` / `websecure` +entrypoints, or you want to serve both schemes at once: + +```yaml +ingress: + traefik: + entryPoints: ['websecure', 'web'] # a bare string also works +``` + +Leave it empty (the default) to derive the entrypoint from the table above. This +setting controls the entrypoint *only* — whether a `tls:` block is emitted still +follows your `ssl.*` configuration. It is also how you select `websecure` for +option 4b, where TLS ends at Traefik itself. + +##### Caveat: check your Traefik entrypoints before relying on plain HTTP + +Many Traefik installations redirect `web` to HTTPS in Traefik's own static +configuration: + +```text +--entryPoints.web.http.redirections.entryPoint.to=:443 +--entryPoints.web.http.redirections.entryPoint.scheme=https +--entryPoints.websecure.http.tls=true +``` + +Check yours with: + +```bash +kubectl get deploy -n \ + -o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep -i redirect +``` + +If the redirection is present, every plain-HTTP request is answered with a +permanent redirect *before* it reaches a route, so Option 1 cannot serve Plane on +that cluster. Either drop the redirection, or use Option 2/3/4. + +##### A note on nginx (`ingress.ingressClass: nginx`) + +The `ssl.*` settings above drive the standard `Ingress` path too — everything in +the table applies except the **Entrypoint** column, which is Traefik-only: + +- Options 2 and 3 emit the `Ingress` `tls:` block, exactly as before. +- Option 4 (`ssl.externalTermination`) emits **no** `tls:` block and only sets + the URL scheme — which is what you want when an ALB, an NLB TLS listener, or + nginx-ingress in front of Plane holds the certificate. + +```yaml +ingress: + ingressClass: nginx + ingress_annotations: { "nginx.ingress.kubernetes.io/proxy-body-size": "5m" } +ssl: + externalTermination: true # ALB/NLB/Cloudflare terminates; no Secret here +``` + +`ingress.ingress_annotations` is optional here — earlier releases called `len` on +it and failed to render with `error calling len: len of nil pointer` when it was +left commented out, so `ingressClass: nginx` needed at least one annotation to +work at all. That is fixed; the annotation above is shown because it is useful, +not because it is required. + +##### Upgrading from 1.6.3 or earlier + +Two changes to be aware of. + +**1. `WEB_URL` now follows your TLS configuration.** Earlier releases hardcoded +`WEB_URL: "http://"` regardless of `ssl.*`, so a TLS-configured install +served Plane over HTTPS while telling the app it lived at `http://`. It is now +`https://` whenever TLS is in effect (options 2, 3 and 4). If you worked around +the old behaviour by overriding `WEB_URL` downstream, drop the override. + +**2. The Traefik routes no longer force TLS.** Earlier releases always bound all +three `IngressRoute`s to `websecure` and always emitted a `tls:` block, even when +no certificate was configured — pointing at a `-ssl-cert` Secret that +was never created, so Traefik fell back to its built-in self-signed certificate. +If you relied on that, or on TLS terminated at Traefik itself, adopt Option 4b: + +```yaml +ssl: + externalTermination: true +ingress: + traefik: + entryPoints: ['websecure'] +``` + +If you configure TLS through `ssl.tls_secret_name` or `ssl.generateCerts` + +`ssl.createIssuer`, the rendered ingress is unchanged and only `WEB_URL` moves. + ### Common Environment Settings | Setting | Default | Required | Description | diff --git a/charts/plane-ce/templates/_helpers.tpl b/charts/plane-ce/templates/_helpers.tpl index 9d81287f..e88dc789 100644 --- a/charts/plane-ce/templates/_helpers.tpl +++ b/charts/plane-ce/templates/_helpers.tpl @@ -55,4 +55,79 @@ 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 }} +{{/* +Returns "true" when THIS CHART has a TLS Secret to point an ingress at: either +the user supplied one via ssl.tls_secret_name, or cert-manager is set up to mint +one (ssl.generateCerts + ssl.createIssuer, which is what gates +templates/certs/certs.yaml). + +Gates the `tls:` blocks and the Traefik entrypoint. Never widen this to cover +externally-terminated TLS -- referencing a Secret that nothing creates is the +bug this helper exists to stop: Traefik answers such a handshake with its +built-in self-signed certificate and logs nothing. +*/}} +{{- define "plane.chartManagedCert" -}} + {{- if or .Values.ssl.tls_secret_name (and .Values.ssl.generateCerts .Values.ssl.createIssuer) -}} + true + {{- end -}} +{{- end -}} + +{{/* +Returns "true" when users reach Plane over https://, whoever terminates it. + +That is either a chart-managed certificate, or ssl.externalTermination for TLS +handled in front of Plane -- a cloud load balancer, Cloudflare, a service mesh, +or a Traefik entrypoint with its own certificate (`websecure.http.tls=true`). +The chart owns no Secret in that second case, so this must NOT be used to emit a +`tls:` block; use plane.chartManagedCert for that. + +Drives ONLY the scheme of the self-referential URLs handed to the app (WEB_URL). + +Deliberately NOT the Traefik entrypoint. "Users are on https" says nothing about +which entrypoint traffic arrives on: an upstream terminator (ALB, NLB TLS +listener, Cloudflare) forwards cleartext, which lands on `web`, while a Traefik +entrypoint carrying its own certificate lands on `websecure`. Those need +opposite entrypoints from the same value, so the entrypoint derives from +plane.chartManagedCert instead and ingress.traefik.entryPoints overrides it. +*/}} +{{- define "plane.tlsEnabled" -}} + {{- if or (eq (include "plane.chartManagedCert" .) "true") .Values.ssl.externalTermination -}} + true + {{- end -}} +{{- end -}} + +{{/* +Traefik entrypoint names for the IngressRoutes. + +Honours an explicit ingress.traefik.entryPoints override (some clusters rename +the defaults, and it is the way to select `websecure` when Traefik's own +entrypoint terminates TLS); otherwise derives them from whether THIS CHART +terminates TLS, so an install with SSL left off is reachable over plain HTTP +instead of serving Traefik's fallback self-signed certificate. + +Keyed on plane.chartManagedCert, NOT plane.tlsEnabled: with TLS terminated +upstream the chart must still bind `web`, because the terminator forwards +cleartext and a route attached only to `websecure` would never match it. + +An empty value is the "derive it" sentinel, never a literal empty list -- the +CRD requires at least one entrypoint. A bare string is accepted and wrapped into +a single-item list, since `--set ingress.traefik.entryPoints=websecure` yields a +scalar and would otherwise render a list-less mapping the CRD rejects. +Caller must nindent to the correct depth. +*/}} +{{- define "plane.traefikEntryPoints" -}} + {{- with .Values.ingress.traefik.entryPoints -}} + {{- if kindIs "string" . -}} + {{- toYaml (list .) -}} + {{- else -}} + {{- toYaml . -}} + {{- end -}} + {{- else -}} + {{- if eq (include "plane.chartManagedCert" $) "true" -}} +- websecure + {{- else -}} +- web + {{- end -}} + {{- end -}} +{{- end -}} diff --git a/charts/plane-ce/templates/config-secrets/app-env.yaml b/charts/plane-ce/templates/config-secrets/app-env.yaml index c15b8e33..93f3c7e2 100644 --- a/charts/plane-ce/templates/config-secrets/app-env.yaml +++ b/charts/plane-ce/templates/config-secrets/app-env.yaml @@ -52,7 +52,9 @@ data: MINIO_ENDPOINT_SSL: {{ .Values.minio.env.minio_endpoint_ssl | default false | ternary "1" "0" | quote }} API_KEY_RATE_LIMIT: {{ .Values.env.api_key_rate_limit | default "60/minute" | quote }} - {{- if .Values.ingress.appHost }} + {{- if and .Values.ingress.appHost (eq (include "plane.tlsEnabled" .) "true") }} + WEB_URL: "https://{{ .Values.ingress.appHost }}" + {{- else if .Values.ingress.appHost }} WEB_URL: "http://{{ .Values.ingress.appHost }}" {{- else }} WEB_URL: "" diff --git a/charts/plane-ce/templates/ingress-traefik.yaml b/charts/plane-ce/templates/ingress-traefik.yaml index 6382f647..30a99a05 100644 --- a/charts/plane-ce/templates/ingress-traefik.yaml +++ b/charts/plane-ce/templates/ingress-traefik.yaml @@ -9,7 +9,7 @@ metadata: namespace: {{ .Release.Namespace }} spec: entryPoints: - - websecure + {{- include "plane.traefikEntryPoints" . | nindent 4 }} routes: @@ -74,8 +74,10 @@ spec: - name: {{ .Release.Name }}-web port: 3000 + {{- if eq (include "plane.chartManagedCert" .) "true" }} tls: secretName: {{ default (printf "%s-ssl-cert" .Release.Name) .Values.ssl.tls_secret_name }} + {{- end }} {{- end }} @@ -90,7 +92,7 @@ metadata: namespace: {{ .Release.Namespace }} spec: entryPoints: - - websecure + {{- include "plane.traefikEntryPoints" . | nindent 4 }} routes: - match: Host(`{{ .Values.ingress.minioHost }}`) kind: Rule @@ -99,8 +101,10 @@ spec: services: - name: {{ .Release.Name }}-minio port: 9090 + {{- if eq (include "plane.chartManagedCert" .) "true" }} tls: secretName: {{ default (printf "%s-ssl-cert" .Release.Name) .Values.ssl.tls_secret_name }} + {{- end }} {{- end }} {{- if and .Values.ingress.enabled (hasPrefix "traefik" .Values.ingress.ingressClass) .Values.rabbitmq.local_setup .Values.ingress.rabbitmqHost }} @@ -114,7 +118,7 @@ metadata: namespace: {{ .Release.Namespace }} spec: entryPoints: - - websecure + {{- include "plane.traefikEntryPoints" . | nindent 4 }} routes: - match: Host(`{{ .Values.ingress.rabbitmqHost }}`) kind: Rule @@ -123,6 +127,8 @@ spec: services: - name: {{ .Release.Name }}-rabbitmq port: 15672 + {{- if eq (include "plane.chartManagedCert" .) "true" }} tls: secretName: {{ default (printf "%s-ssl-cert" .Release.Name) .Values.ssl.tls_secret_name }} + {{- end }} {{- end }} diff --git a/charts/plane-ce/templates/ingress.yaml b/charts/plane-ce/templates/ingress.yaml index 8894efa5..af6505a4 100644 --- a/charts/plane-ce/templates/ingress.yaml +++ b/charts/plane-ce/templates/ingress.yaml @@ -7,9 +7,9 @@ metadata: name: {{ .Release.Name }}-ingress labels: {{- include "plane.commonLabels" $ | nindent 4 }} - {{- if gt (len .Values.ingress.ingress_annotations) 0 }} + {{- with .Values.ingress.ingress_annotations }} annotations: - {{- range $key, $value := .Values.ingress.ingress_annotations }} + {{- range $key, $value := . }} {{ $key }}: {{ $value | quote }} {{- end }} {{- end }} diff --git a/charts/plane-ce/values.yaml b/charts/plane-ce/values.yaml index 0ee47f9f..1995455f 100644 --- a/charts/plane-ce/values.yaml +++ b/charts/plane-ce/values.yaml @@ -18,6 +18,17 @@ ingress: # Traefik-specific options (only used when ingressClass starts with "traefik") traefik: maxRequestBodyBytes: 20971520 # in bytes (default: 20 MiB) + # Traefik entrypoints the IngressRoutes attach to. Leave empty to derive them + # from your SSL settings, which is what you want in almost every case: + # - TLS configured (ssl.tls_secret_name, or generateCerts + createIssuer) + # -> ['websecure'], and a `tls:` block is emitted. + # - No TLS configured -> ['web'], plain HTTP, and NO `tls:` block. Use this + # to reach Plane over http:// while you are still sorting out DNS/certs, + # or when TLS is terminated upstream (cloud LB, Cloudflare, a service mesh). + # Set explicitly only if your Traefik install renamed the default entrypoints, + # e.g. entryPoints: ['websecure', 'web'] or ['https'], or to select + # 'websecure' when Traefik's own entrypoint terminates TLS. + entryPoints: [] # ingress_annotations: { "nginx.ingress.kubernetes.io/proxy-body-size": "5m" } # SSL Configuration - Valid only if ingress.enabled is true @@ -30,6 +41,23 @@ ssl: server: https://acme-v02.api.letsencrypt.org/directory email: plane@example.com generateCerts: false + # Set true when TLS is terminated IN FRONT of Plane and this chart manages no + # certificate of its own -- a cloud load balancer, Cloudflare, a service mesh, + # or a Traefik entrypoint that carries its own cert (websecure.http.tls=true). + # + # The chart then renders WEB_URL with an https:// scheme. No `tls:` block is + # emitted, because there is no Secret for this chart to reference. + # + # This does NOT change the Traefik entrypoint. An upstream terminator (ALB, + # NLB TLS listener, Cloudflare) forwards cleartext, which arrives on `web` -- + # the default. If TLS is instead terminated by Traefik's own entrypoint + # (websecure.http.tls=true), also set: + # ingress.traefik.entryPoints: ['websecure'] + # otherwise the route binds `web` and never matches the TLS traffic. + # + # Leave false if you set tls_secret_name or generateCerts -- those already + # imply https. This is only for TLS this chart cannot see. + externalTermination: false redis: local_setup: true diff --git a/charts/plane-enterprise/Chart.yaml b/charts/plane-enterprise/Chart.yaml index c4986c39..0079cff6 100644 --- a/charts/plane-enterprise/Chart.yaml +++ b/charts/plane-enterprise/Chart.yaml @@ -5,7 +5,7 @@ description: Meet Plane. An Enterprise software development tool to manage issue type: application -version: 3.4.1 +version: 3.4.2 appVersion: "3.1.1" home: https://plane.so/ diff --git a/charts/plane-enterprise/README.md b/charts/plane-enterprise/README.md index 8dfa3fa5..0a035f53 100644 --- a/charts/plane-enterprise/README.md +++ b/charts/plane-enterprise/README.md @@ -249,6 +249,30 @@ If the redirection is present, every plain-HTTP request is answered with a permanent redirect *before* it reaches a route, so Option 1 cannot serve Plane on that cluster. Either drop the redirection, or use Option 2/3/4. +#### A note on nginx (`ingress.ingressClass: nginx`) + +The `ssl.*` settings above drive the standard `Ingress` path too — everything in +the table applies except the **Entrypoint** column, which is Traefik-only: + +- Options 2 and 3 emit the `Ingress` `tls:` block, exactly as before. +- Option 4 (`ssl.externalTermination`) emits **no** `tls:` block and only sets + the URL scheme — which is what you want when an ALB, an NLB TLS listener, or + nginx-ingress in front of Plane holds the certificate. + +```yaml +ingress: + ingressClass: nginx + ingress_annotations: { "nginx.ingress.kubernetes.io/proxy-body-size": "5m" } +ssl: + externalTermination: true # ALB/NLB/Cloudflare terminates; no Secret here +``` + +`ingress.ingress_annotations` is optional here — earlier releases called `len` on +it and failed to render with `error calling len: len of nil pointer` when it was +left commented out, so `ingressClass: nginx` needed at least one annotation to +work at all. That is fixed; the annotation above is shown because it is useful, +not because it is required. + #### Upgrading from 3.3.0 or earlier If you configure TLS through `ssl.tls_secret_name` or `ssl.generateCerts` + diff --git a/charts/plane-enterprise/templates/ingress.yaml b/charts/plane-enterprise/templates/ingress.yaml index 68088a4d..42151786 100644 --- a/charts/plane-enterprise/templates/ingress.yaml +++ b/charts/plane-enterprise/templates/ingress.yaml @@ -7,9 +7,9 @@ metadata: name: {{ .Release.Name }}-ingress labels: {{- include "plane.commonLabels" $ | nindent 4 }} - {{- if gt (len .Values.ingress.ingress_annotations) 0 }} + {{- with .Values.ingress.ingress_annotations }} annotations: - {{- range $key, $value := .Values.ingress.ingress_annotations }} + {{- range $key, $value := . }} {{ $key }}: {{ $value | quote }} {{- end }} {{- end }}