From fe22adc77c41869b4b750dc6070d494a57df96b7 Mon Sep 17 00:00:00 2001 From: akshat5302 Date: Tue, 15 Sep 2026 13:04:43 +0530 Subject: [PATCH] docs: add custom SSL certificate guide for Docker Compose, airgapped and Kubernetes Replace the "custom certificates are not supported" note on the SSL page with a full guide for the ssl/ folder auto-detection shipped in Plane v3.2.0 (INFRA-868), add a Kubernetes section for ssl.tls_secret_name, and cross-link the airgapped Docker, airgapped Kubernetes and Kubernetes install pages. Co-Authored-By: WOZCODE --- docs/self-hosting/govern/configure-ssl.md | 180 +++++++++++++++++- .../methods/airgapped-edition-kubernetes.md | 2 + .../self-hosting/methods/airgapped-edition.md | 51 +++++ .../install-methods-commercial/kubernetes.md | 2 + docs/self-hosting/methods/kubernetes.md | 2 + 5 files changed, 231 insertions(+), 6 deletions(-) diff --git a/docs/self-hosting/govern/configure-ssl.md b/docs/self-hosting/govern/configure-ssl.md index d2159769..168b84d2 100644 --- a/docs/self-hosting/govern/configure-ssl.md +++ b/docs/self-hosting/govern/configure-ssl.md @@ -1,15 +1,15 @@ --- title: Set up SSL -description: Configure SSL/TLS certificates for Plane. Setup HTTPS encryption for secure self-hosted Plane deployment. -keywords: plane ssl, https setup, tls certificate, ssl configuration, lets encrypt, secure deployment, self-hosting security +description: Configure SSL/TLS certificates for Plane. Set up HTTPS with Let's Encrypt or bring your own certificate from a corporate CA on Docker Compose, airgapped and Kubernetes deployments. +keywords: plane ssl, https setup, tls certificate, ssl configuration, lets encrypt, custom ssl certificate, corporate ca, self-signed certificate, kubernetes tls secret, secure deployment, self-hosting security --- # Set up SSL -This guide shows you how to configure SSL/TLS certificates for your self-hosted Plane instance. Plane handles certificate provisioning and renewal automatically using Let's Encrypt. +This guide shows you how to configure SSL/TLS certificates for your self-hosted Plane instance. Plane handles certificate provisioning and renewal automatically using Let's Encrypt. If your instance runs behind an internal PKI, in an airgapped network, or must use a certificate issued by your own CA, you can [use your own certificate](#use-your-own-certificate) instead. ::: info -**Applies to:** Docker deployments of Plane Commercial Edition without an external reverse proxy. +**Applies to:** Docker deployments of Plane Commercial Edition without an external reverse proxy. The [Kubernetes section](#kubernetes) covers the `plane-enterprise` Helm chart. If you're using an external reverse proxy (nginx, Caddy, Traefik) or a load balancer, configure SSL there instead and skip this guide. ::: @@ -108,6 +108,174 @@ You should see a response with `HTTP/2 200` or `HTTP/1.1 200` and SSL-related he Visit your Plane instance in a browser at `https://plane.yourcompany.com`. You should see a secure connection (padlock icon) without certificate warnings. -## Using custom SSL certificates +## Use your own certificate -Custom SSL certificates (from a corporate CA or purchased certificates) are not currently supported in Plane's deployment. +Use this when your instance runs behind an internal PKI, in an airgapped network, or must use a certificate issued by a specific CA. Plane's built-in proxy serves your certificate instead of requesting one from Let's Encrypt. + +::: info +**Available in Plane v3.2.0 and later** for Docker Compose (Prime CLI), airgapped, Podman, Portainer and Docker AIO deployments of the Commercial Edition. Kubernetes deployments use a TLS Secret instead — see [Kubernetes](#kubernetes) below. +::: + +### How it works + +There is nothing to configure. The proxy container mounts the `ssl/` folder of your install directory read-only at `/ssl`. On startup it looks for a certificate and key pair there: + +| File | Content | +| ---------- | ---------------------------------------------------------------- | +| `cert.pem` | PEM-encoded **full chain** certificate (leaf plus intermediates) | +| `key.pem` | PEM-encoded private key matching `cert.pem` | + +The certbot names `fullchain.pem` and `privkey.pem` are accepted as well. When both files are present and valid, the proxy serves them. When the folder is empty, Let's Encrypt is used as described above. Any problem with the files is logged as a warning and the proxy still starts, so a bad certificate never blocks startup. + +### Place the certificate and key + +For a Prime CLI install the install directory is `/opt/plane`: + +```bash +sudo mkdir -p /opt/plane/ssl +sudo cp fullchain.pem /opt/plane/ssl/cert.pem +sudo cp privkey.pem /opt/plane/ssl/key.pem +sudo chmod 600 /opt/plane/ssl/key.pem +``` + +The proxy runs as root, so `root:root` files with mode `600` on the key are readable. If you run the proxy as another user, make sure that user can read both files. + +::: tip +If your certificate is in DER or PKCS#12 format, convert it to PEM first: + +```bash +openssl x509 -inform der -in cert.der -out cert.pem +openssl pkcs12 -in bundle.pfx -nodes -out combined.pem # then split into cert and key +``` + +::: + +### Set the domain + +Edit `/opt/plane/plane.env` and set the domain the certificate was issued for. `CERT_EMAIL` is not needed: + +```bash +SITE_ADDRESS=plane.yourcompany.com +WEB_URL=https://plane.yourcompany.com +``` + +::: warning +The certificate is ignored if `SITE_ADDRESS` is still a plain-HTTP address such as `localhost:80`, `:80`, or an `http://` URL. The proxy cannot serve TLS on those. +::: + +### Apply and verify + +Recreate the proxy so it picks up the mount and the new address: + +```bash +sudo prime-cli restart +``` + +The proxy log confirms the certificate is in use: + +```bash +docker compose -f /opt/plane/docker-compose.yml logs proxy | grep -i "ssl certificate" +``` + +```text +Custom SSL certificate enabled: /ssl/cert.pem (key: /ssl/key.pem) +``` + +Check that the issuer is your CA rather than Let's Encrypt: + +```bash +openssl s_client -connect plane.yourcompany.com:443 -servername plane.yourcompany.com /dev/null \ + | openssl x509 -noout -subject -issuer -dates +``` + +### Rotate the certificate + +Replace both files in `ssl/` and restart. The proxy reads the files at startup, so a running container keeps serving the old certificate until it is restarted: + +```bash +sudo cp new-fullchain.pem /opt/plane/ssl/cert.pem +sudo cp new-privkey.pem /opt/plane/ssl/key.pem +sudo prime-cli restart +``` + +### Go back to Let's Encrypt + +Remove both files from `ssl/`, set `CERT_EMAIL`, and restart. Let's Encrypt provisioning resumes automatically. + +### Troubleshooting + +Every problem is logged by the proxy as a single line starting with `WARNING: custom SSL certificate ignored:` followed by the reason. + +| Log message or symptom | Cause and fix | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `both cert.pem and key.pem are required` | Only one of the two files is present. Add the missing file or remove both. | +| `files exist in /ssl but are not readable` | Fix file permissions on the host. The proxy needs read access to both files. | +| `... is not a PEM encoded certificate` / `... private key` | Convert the files to PEM as shown above. | +| `SITE_ADDRESS is 'localhost:80' (plain HTTP)` | Set `SITE_ADDRESS` to your domain. | +| Browser reports an incomplete chain | `cert.pem` must contain the full chain, not the leaf alone. | +| Still serving the Let's Encrypt certificate | Look for a `WARNING: custom SSL certificate ignored` line, or restart the proxy if it was not restarted after the copy. | +| Key does not match certificate | Compare `openssl x509 -noout -modulus -in cert.pem \| md5sum` with `openssl rsa -noout -modulus -in key.pem \| md5sum`. | + +### Airgapped deployments + +An airgapped host cannot reach Let's Encrypt, so a certificate from your internal CA is the only way to serve HTTPS. The mechanism is the same, with the install directory being the setup directory you chose during installation. See [Use your own SSL certificate](/self-hosting/methods/airgapped-edition#use-your-own-ssl-certificate) in the airgapped Docker guide for the exact steps. + +## Kubernetes + +The `plane-enterprise` Helm chart does not run the built-in proxy. TLS terminates at your ingress controller, so the `ssl/` folder does not apply. The chart supports three modes in its `ssl` values block: + +| Your setup | Set | Who holds the certificate | +| ------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- | +| Bring your own certificate | `ssl.tls_secret_name` | A `kubernetes.io/tls` Secret you create | +| Let cert-manager issue one | `ssl.createIssuer: true` + `ssl.generateCerts: true` | A chart-created Secret (Let's Encrypt via cert-manager) | +| TLS terminated in front of the cluster (ALB, NLB, Cloudflare) | `ssl.externalTermination: true` | The load balancer. Configure the certificate there, not in the chart. | + +### Use your own certificate on Kubernetes + +1. Create a TLS Secret in the release namespace from the full chain certificate and key: + + ```bash + kubectl create secret tls plane-tls \ + --cert=fullchain.pem --key=privkey.pem -n plane-ns + ``` + +2. Reference it in your `values.yaml` and make sure the other modes are off: + + ```yaml + license: + licenseDomain: plane.yourcompany.com + ingress: + enabled: true + ssl: + tls_secret_name: plane-tls + createIssuer: false + generateCerts: false + externalTermination: false + ``` + +3. Apply the change: + + ```bash + helm upgrade --install plane-app makeplane/plane-enterprise \ + --namespace plane-ns -f values.yaml --wait + ``` + +The chart then renders a `tls` block on the Ingress (nginx) or `tls.secretName` on the Traefik `IngressRoute`, and switches every URL Plane uses for itself to `https://`. If you set `ingress.minioHost` or `ingress.rabbitmqHost`, those hosts are added to the TLS block too, so the certificate must cover them as Subject Alternative Names. On OpenShift, Routes use the router's wildcard certificate by default; set `ingress.openshift.externalCertificate` to your Secret name to serve your own (OpenShift 4.16 or later). + +**Rotate the certificate** by updating the Secret. Ingress controllers reload certificates on Secret changes, so no `helm upgrade` is needed: + +```bash +kubectl create secret tls plane-tls --cert=new-fullchain.pem --key=new-privkey.pem \ + -n plane-ns --dry-run=client -o yaml | kubectl apply -f - +``` + +**Troubleshooting** + +| Symptom | Cause and fix | +| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Browser gets the controller's default certificate | The Secret is missing, in another namespace, or not type `kubernetes.io/tls` with `tls.crt` and `tls.key`. Check `kubectl get secret`. | +| Incomplete chain | `tls.crt` must contain the full chain. | +| Certificate warning on the MinIO or RabbitMQ console hosts | Those hosts are in the TLS block. Add them as SANs or unset `ingress.minioHost` / `ingress.rabbitmqHost`. | +| App links and OAuth redirects still use `http://` | The URL scheme is derived from `ssl.tls_secret_name`. Confirm it is set with `helm get values plane-app -n plane-ns`. | + +Outbound trust for an S3-compatible store signed by a private CA is configured separately with `airgapped.s3Secrets`. See [CA certificate configuration](/self-hosting/methods/kubernetes#ca-certificate-configuration-for-airgapped-deployments-only). diff --git a/docs/self-hosting/methods/airgapped-edition-kubernetes.md b/docs/self-hosting/methods/airgapped-edition-kubernetes.md index baac2328..09480f43 100644 --- a/docs/self-hosting/methods/airgapped-edition-kubernetes.md +++ b/docs/self-hosting/methods/airgapped-edition-kubernetes.md @@ -290,3 +290,5 @@ Consider these alternatives: ## Additional configuration For more advanced Plane configuration options, refer to the [Kubernetes documentation](https://developers.plane.so/self-hosting/methods/kubernetes#configuration-settings). + +**Serve HTTPS with your own certificate.** An airgapped cluster cannot reach Let's Encrypt, so set `ssl.generateCerts: false` and instead create a `kubernetes.io/tls` Secret from a certificate issued by your internal CA, then set `ssl.tls_secret_name` to its name. See [Use your own certificate on Kubernetes](/self-hosting/govern/configure-ssl#use-your-own-certificate-on-kubernetes). diff --git a/docs/self-hosting/methods/airgapped-edition.md b/docs/self-hosting/methods/airgapped-edition.md index 622eb783..59d74f2e 100644 --- a/docs/self-hosting/methods/airgapped-edition.md +++ b/docs/self-hosting/methods/airgapped-edition.md @@ -207,3 +207,54 @@ Consider these alternatives: You now have Plane running in your air-gapped environment. If you run into any issues, check the logs using the commands above, or reach out to our support team for assistance. 3. [Activate your license key](/self-hosting/manage/manage-licenses/activate-airgapped) + +## Use your own SSL certificate + +An airgapped host cannot reach Let's Encrypt, so Plane starts on plain HTTP. To serve HTTPS, use a certificate issued by your internal CA. Available in Plane v3.2.0 and later. + +The `proxy` service mounts the `ssl/` folder of your install directory read-only at `/ssl`. The install directory is the directory that holds `docker-compose.yml` and `plane.env`. + +1. **Place the certificate and key** + + ```bash + cd /path/to/plane + mkdir -p ssl + cp fullchain.pem ssl/cert.pem # PEM, full chain (leaf plus intermediates) + cp privkey.pem ssl/key.pem # PEM private key matching cert.pem + chmod 600 ssl/key.pem + ``` + + The certificate must cover the domain you set in `plane.env`, and be issued by a CA your users' browsers trust. + +2. **Switch `plane.env` to HTTPS** + + ```bash + SITE_ADDRESS=plane.yourcompany.com + APP_PROTOCOL=https + WEB_URL=https://plane.yourcompany.com + CORS_ALLOWED_ORIGINS=https://plane.yourcompany.com + ``` + + If `SITE_ADDRESS` is left as a plain-HTTP address (`localhost:80`, `:80`, or an `http://` URL) the certificate is ignored with a warning. + +3. **Recreate the proxy** + + ```bash + docker compose --env-file plane.env up -d --force-recreate proxy + ``` + + If you also changed `WEB_URL` or `CORS_ALLOWED_ORIGINS`, recreate the services that read them: + + ```bash + docker compose --env-file plane.env up -d --force-recreate api web space admin live + ``` + +4. **Verify** + + ```bash + docker compose --env-file plane.env logs proxy | grep -i "ssl certificate" + ``` + + Expect `Custom SSL certificate enabled: /ssl/cert.pem (key: /ssl/key.pem)`. If you see `WARNING: custom SSL certificate ignored:` instead, the reason follows on the same line. Then open `https://plane.yourcompany.com` from a machine that trusts your CA. + +To rotate the certificate, replace both files in `ssl/` and recreate the proxy. The `ssl/` folder is kept across upgrades. For file formats, permissions and the full troubleshooting table, see [Use your own certificate](/self-hosting/govern/configure-ssl#use-your-own-certificate). diff --git a/docs/self-hosting/methods/install-methods-commercial/kubernetes.md b/docs/self-hosting/methods/install-methods-commercial/kubernetes.md index 6c4b4054..63f16f20 100644 --- a/docs/self-hosting/methods/install-methods-commercial/kubernetes.md +++ b/docs/self-hosting/methods/install-methods-commercial/kubernetes.md @@ -692,6 +692,8 @@ To configure the external secrets for your application, you need to define speci | 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 | +**Bring your own certificate.** Create a `kubernetes.io/tls` Secret in the release namespace and set `ssl.tls_secret_name` to its name; leave `ssl.createIssuer` and `ssl.generateCerts` at `false`. The chart adds the Secret to the ingress TLS block and renders all app URLs with `https://`. See [Use your own certificate on Kubernetes](/self-hosting/govern/configure-ssl#use-your-own-certificate-on-kubernetes) for the steps, rotation and troubleshooting. + #### Common Environment Settings | Setting | Default | Required | Description | diff --git a/docs/self-hosting/methods/kubernetes.md b/docs/self-hosting/methods/kubernetes.md index 98286933..f5609346 100644 --- a/docs/self-hosting/methods/kubernetes.md +++ b/docs/self-hosting/methods/kubernetes.md @@ -690,6 +690,8 @@ To configure the external secrets for your application, you need to define speci | 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 | +**Bring your own certificate.** Create a `kubernetes.io/tls` Secret in the release namespace and set `ssl.tls_secret_name` to its name; leave `ssl.createIssuer` and `ssl.generateCerts` at `false`. The chart adds the Secret to the ingress TLS block and renders all app URLs with `https://`. See [Use your own certificate on Kubernetes](/self-hosting/govern/configure-ssl#use-your-own-certificate-on-kubernetes) for the steps, rotation and troubleshooting. + #### Common Environment Settings | Setting | Default | Required | Description |