Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion charts/plane-ce/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
214 changes: 208 additions & 6 deletions charts/plane-ce/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,14 @@ The default value is `"traefik"`. If you previously relied on the implicit defau
| ssl.email | <plane@example.com> | | 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) options 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.
Expand All @@ -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 `<release>-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` | `<release>-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
`<release>-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 <traefik-ns> <traefik-deployment> \
-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
```

> **Known issue, unrelated to TLS:** `ingress.ingress_annotations` is commented
> out in the shipped `values.yaml`, and the `Ingress` template calls `len` on it,
> so `ingressClass: nginx` fails to render with
> `error calling len: len of nil pointer` unless you set at least one annotation.
> Passing any annotation — as above — works around it.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

##### 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://<appHost>"` 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 `<release>-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 |
Expand Down
77 changes: 76 additions & 1 deletion charts/plane-ce/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
{{- 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 -}}
4 changes: 3 additions & 1 deletion charts/plane-ce/templates/config-secrets/app-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
Expand Down
Loading