Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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), 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.
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
```

`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://<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