Skip to content
2 changes: 1 addition & 1 deletion charts/plane-enterprise/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Meet Plane. An Enterprise software development tool to manage issue

type: application

version: 3.3.0
version: 3.4.0
appVersion: "3.1.1"

home: https://plane.so/
Expand Down
181 changes: 181 additions & 0 deletions charts/plane-enterprise/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,186 @@ The default value is `"traefik"`. If you are switching to a standard ingress con
| `ingress.enabled` | `true` | Master switch — set to `false` to render neither template. |
| `ingress.ingressClass` | `traefik` | Selects which template is active (see table above). |
| `ingress.traefik.maxRequestBodyBytes` | `20971520` | Max request body size for Traefik's buffering middleware. Ignored when not using Traefik. |
| `ingress.traefik.entryPoints` | `[]` | Traefik entrypoints for the `IngressRoute`. Empty means derive from your SSL settings — see below. Ignored when not using Traefik. |
| `ingress.ingress_annotations` | `{}` | Standard `Ingress` annotations. Ignored when `ingressClass` starts with `traefik`. |

### TLS options: choosing how HTTPS is handled

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` binds to — both from whether **this chart** terminates TLS;
2. the scheme of every URL Plane is told about itself — `WEB_URL`,
`APP_BASE_URL`, `PI_BASE_URL`, `PLANE_FRONTEND_URL`, `PLANE_API_HOST`,
`PLANE_OAUTH_REDIRECT_URI`, `SILO_API_BASE_URL`, `EXPORT_DOWNLOAD_BASE_URL`.
(`CORS_ALLOWED_ORIGINS` always lists both schemes and is unaffected.)

Find the row that matches your environment:

| Your setup | Set | Entrypoint | `tls:` block | App URLs |
| --- | --- | :---: | :---: | :---: |
| 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.

#### Option 1 — No TLS, plain HTTP

The default. Nothing to set; leave the `ssl` block alone and Plane is reachable at
`http://<licenseDomain>`:

```yaml
license:
licenseDomain: plane.example.com
ingress:
ingressClass: traefik
```

Good for a quick trial, an air-gapped or internal network, or while you are still
sorting out DNS and certificates. **Read the entrypoint caveat below before
relying on it** — and terminate TLS somewhere before exposing Plane on the public
internet.

#### Option 2 — Bring your own certificate

Create a `kubernetes.io/tls` Secret in the release namespace and name it:

```bash
kubectl create secret tls my-tls-secret \
--cert=fullchain.pem --key=privkey.pem -n plane-ns
```

```yaml
ssl:
tls_secret_name: my-tls-secret
```

#### Option 3 — Let cert-manager issue the certificate

Requires cert-manager in the cluster. **Both** flags are needed — `createIssuer`
alone creates an Issuer but no Certificate, and the chart then treats the install
as having no certificate at all:

```yaml
ssl:
createIssuer: true
generateCerts: true
issuer: http # or cloudflare / digitalocean
email: you@example.com
# token: <dns-provider-api-token> # required for cloudflare / digitalocean
```

The Certificate is written to `<release-name>-ssl-cert` and the `IngressRoute`
references it.

#### 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 every app URL
`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 route stays 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 route 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://` URLs to itself
while being served over HTTPS, breaking OAuth callbacks and export download
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 <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.

#### Upgrading from 3.3.0 or earlier

If you configure TLS through `ssl.tls_secret_name` or `ssl.generateCerts` +
`ssl.createIssuer`, the rendered ingress is unchanged and no action is needed.

One case needs a value added. Earlier releases always bound the Traefik
`IngressRoute` 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 —
both settings, since `externalTermination` alone leaves the route on `web`:

```yaml
ssl:
externalTermination: true
ingress:
traefik:
entryPoints: ['websecure']
```

## Installing Plane

1. Open Terminal or any other command-line app that has access to Kubernetes tools on your local system.
Expand Down Expand Up @@ -870,13 +1048,16 @@ Note: When the email service is enabled, the cert-issuer will be automatically c
| ingress.rabbitmqHost | | | Based on above configuration, if you want to expose the `rabbitmq` web console to set of users, use this key to set the `host` mapping or leave it as `EMPTY` to not expose interface. |
| ingress.ingressClass | nginx | Yes | Kubernetes cluster setup comes with various options of `ingressClass`. Based on your setup, set this value to the right one (eg. nginx, traefik, etc). Leave it to default in case you are using external ingress provider. |
| ingress.ingress_annotations | `{ "nginx.ingress.kubernetes.io/proxy-body-size": "5m" }` | | Ingress controllers comes with various configuration options which can be passed as annotations. Setting this value lets you change the default value to user required. |
| ingress.traefik.entryPoints | `[]` | | Traefik entrypoints the `IngressRoute` binds to. Leave empty to derive them from your `ssl.*` settings (`websecure` when TLS is configured, otherwise `web`). Set explicitly only if your Traefik renamed the default entrypoints, e.g. `['websecure','web']`. Ignored unless `ingressClass` starts with `traefik` |
| ingress.traefik.maxRequestBodyBytes | 20971520 | | Max request body size in bytes for Traefik's buffering middleware (upload size limit). Ignored unless `ingressClass` starts with `traefik` |
| ssl.createIssuer | false | | Kubernets cluster setup supports creating `issuer` type resource. After deployment, this is step towards creating secure access to the ingress url. Issuer is required for you generate SSL certifiate. Kubernetes can be configured to use any of the certificate authority to generate SSL (depending on CertManager configuration). Set it to `true` to create the issuer. Applicable only when `ingress.enabled=true` |
| ssl.issuer | http | | CertManager configuration allows user to create issuers using `http` or any of the other DNS Providers like `cloudflare`, `digitalocean`, etc. As of now Plane supports `http`, `cloudflare`, `digitalocean` |
| ssl.token | | | To create issuers using DNS challenge, set the issuer api token of dns provider like cloudflare`or`digitalocean`(not required for http) |
| ssl.server | <https://acme-v02.api.letsencrypt.org/directory> | | Issuer creation configuration need the certificate generation authority server url. Default URL is the `Let's Encrypt` server |
| 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 to `true` when TLS is terminated in front of Plane and this chart manages no certificate (cloud load balancer, Cloudflare, service mesh, or a Traefik entrypoint carrying its own cert). All app URLs are rendered `https://`; no `tls:` block is emitted and the Traefik entrypoint is unchanged (stays `web` unless you also set `ingress.traefik.entryPoints: ['websecure']` — see Option 4b). Leave `false` if you set `ssl.tls_secret_name` or `ssl.generateCerts`. See [TLS options](#tls-options-choosing-how-https-is-handled) |

### Common Environment Settings

Expand Down
16 changes: 16 additions & 0 deletions charts/plane-enterprise/questions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1753,6 +1753,22 @@ questions:
group: "Ingress"
show_if: "ssl.createIssuer=false"

- variable: ssl.externalTermination
label: "TLS Terminated in Front of Plane"
description: "Enable when TLS is terminated before traffic reaches Plane (cloud load balancer, Cloudflare, service mesh, or a Traefik entrypoint carrying its own cert) and this chart manages no certificate. App URLs are rendered https:// but no tls block is emitted. Leave off if you set a TLS secret name or generate certificates. See the chart README's 'TLS options' section."
type: boolean
default: false
group: "Ingress"
show_if: "ingress.enabled=true"

- variable: ingress.traefik.entryPoints
label: "Traefik Entrypoints Override"
description: "Traefik entrypoints the IngressRoute binds to, e.g. 'websecure'. Leave empty to derive from the SSL settings (websecure when this chart manages a certificate, otherwise web). Required as 'websecure' when TLS is terminated by Traefik's own entrypoint. Ignored unless the ingress class is traefik."
type: string
default: ""
group: "Ingress"
show_if: "ingress.enabled=true"

- variable: external_secrets.rabbitmq_existingSecret
label: "RabbitMQ Secrets File Name"
type: string
Expand Down
76 changes: 76 additions & 0 deletions charts/plane-enterprise/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,79 @@ reports its own service.name). Call with a dict and nindent, e.g.
value: {{ .service | quote }}
{{- 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. Never widen this to cover externally-terminated TLS --
referencing a Secret that nothing creates is the bug this helper exists to stop.
*/}}
{{- 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 every self-referential URL handed to the app
(APP_BASE_URL, PLANE_FRONTEND_URL, PLANE_OAUTH_REDIRECT_URI,
EXPORT_DOWNLOAD_BASE_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 IngressRoute.

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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{{- end -}}
{{- end -}}
{{- end -}}
4 changes: 2 additions & 2 deletions charts/plane-enterprise/templates/config-secrets/app-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ data:
CELERY_BROKER_POOL_LIMIT: {{ .Values.env.celery_broker_pool_limit | quote }}
{{- if .Values.env.web_url}}
WEB_URL: {{ .Values.env.web_url | default "" | quote }}
{{- else if or .Values.ssl.tls_secret_name (and .Values.ssl.createIssuer .Values.ssl.generateCerts) }}
{{- else if eq (include "plane.tlsEnabled" .) "true" }}
WEB_URL: "https://{{ .Values.license.licenseDomain }}"
{{- else }}
WEB_URL: "http://{{ .Values.license.licenseDomain }}"
{{- end }}

LIVE_BASE_URL: "http://{{ .Release.Name }}-live.{{ .Release.Namespace }}.svc.{{ .Values.env.default_cluster_domain | default "cluster.local" }}:3000/"
LIVE_BASE_PATH: "/live"
{{- if or .Values.ssl.tls_secret_name (and .Values.ssl.createIssuer .Values.ssl.generateCerts) }}
{{- if eq (include "plane.tlsEnabled" .) "true" }}
PI_BASE_URL: "https://{{ .Values.license.licenseDomain }}"
{{- else }}
PI_BASE_URL: "http://{{ .Values.license.licenseDomain }}"
Expand Down
Loading