From 880756ed6ab2fa695ca7e1440ece4868e71b235a Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 6 Aug 2026 13:58:57 +0530 Subject: [PATCH 1/3] docs(self-hosting): add FIPS-enabled deployment guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the FIPS deployment guide for the makeplane/*-commercial-fips image set under the Commercial Edition install methods, matching the sibling pages' hidden/noindex convention (search:false, sidebar:false, robots noindex, added to the LLM ignoreFiles list). Covers the host fips=1 prerequisite (the single most-missed step), the full -commercial-fips image list (and why there is no FIPS AIO image), the docker-compose-fips.yml deploy + verify-fips.sh flow, the FIPS-only config defaults (LDAP_TLS_REQUIRE_CERT, SAML_REJECT_DEPRECATED_ALGORITHM, SECRET_ENCRYPTION_V2, USAGE_ID_DIGEST) with overrides, non-root/OpenShift arbitrary-UID guidance, and an honest scope-of-coverage section (bundled data plane is not FIPS; modules are certified, not Plane as a product). Supersedes makeplane/docs#484 — self-hosting content lives on developers.plane.so. Co-Authored-By: Claude Opus 4.8 --- docs/.vitepress/config.mts | 1 + .../fips-deployment.md | 189 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 docs/self-hosting/methods/install-methods-commercial/fips-deployment.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 7d2ce70..50ccb2f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -89,6 +89,7 @@ export default extendConfig( ignoreFiles: [ "self-hosting/methods/install-methods-commercial/docker-compose.md", "self-hosting/methods/install-methods-commercial/kubernetes.md", + "self-hosting/methods/install-methods-commercial/fips-deployment.md", ], }), ], diff --git a/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md b/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md new file mode 100644 index 0000000..73c9c70 --- /dev/null +++ b/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md @@ -0,0 +1,189 @@ +--- +title: FIPS-enabled deployment +description: Deploy the FIPS variant of Plane Enterprise on a FIPS-enforcing host, including prerequisites, image list, verification, and scope of coverage. +keywords: plane fips, fips 140-3 deployment, plane commercial fips, govcloud plane, federal self-hosting, fips enabled containers +search: false +sidebar: false +head: + - - meta + - name: robots + content: noindex, nofollow +--- + +# FIPS-enabled deployment + +Plane Enterprise publishes a FIPS variant of every application image alongside the standard set. +These images are built on Red Hat UBI 10, apply the system-wide FIPS cryptographic policy, and run +their cryptography against FIPS-validated modules (Red Hat's OpenSSL FIPS provider for the Python +and static services; the Go FIPS 140-3 module for the Go services). They are intended for +deployments that must meet FIPS 140-3 expectations, such as US Federal or GovCloud environments. + +> **The single most important prerequisite:** FIPS mode is a property of the **host**, not of the +> image. A FIPS image on a non-FIPS host starts cleanly and looks identical from the inside while +> providing none of the guarantees. Read [Host prerequisite](#host-prerequisite) first. + +## Images + +The FIPS images use the same names as the standard `-commercial` images with a `-fips` suffix, in +the `makeplane` Docker Hub organization: + +| Service | Image | +| ------------- | --------------------------------------- | +| Backend / API | `makeplane/backend-commercial-fips` | +| Web | `makeplane/web-commercial-fips` | +| Admin | `makeplane/admin-commercial-fips` | +| Space | `makeplane/space-commercial-fips` | +| Live | `makeplane/live-commercial-fips` | +| Silo | `makeplane/silo-commercial-fips` | +| Monitor | `makeplane/monitor-commercial-fips` | +| Email | `makeplane/email-commercial-fips` | +| Plane AI (Pi) | `makeplane/plane-pi-commercial-fips` | +| Proxy | `makeplane/proxy-commercial-fips` | +| Flux | `makeplane/flux-commercial-fips` | +| Node runner | `makeplane/node-runner-commercial-fips` | + +Pin a specific release tag for any accredited deployment rather than tracking `latest` — a known, +fixed image version is part of the audit trail. + +> **Note:** there is no FIPS All-in-One (AIO) image. The AIO image is built on an Alpine base, which +> has no FIPS-validated cryptography, so a FIPS deployment uses the multi-container Compose stack +> below, not the AIO image. + +## Host prerequisite + +The host kernel must be booted in FIPS mode. The container inherits this through +`/proc/sys/crypto/fips_enabled` and **cannot set it itself**. Verify before deploying: + +```bash +cat /proc/sys/crypto/fips_enabled # must print 1 +``` + +To enable FIPS mode on a RHEL-family host (RHEL, Rocky, Alma, Amazon Linux 2023): + +```bash +sudo dnf install -y crypto-policies-scripts +sudo fips-mode-setup --enable +sudo reboot +``` + +`/boot` must be its own filesystem for this to work — it is on the stock cloud images. +Alternatively, boot a vendor FIPS image (a RHEL FIPS AMI, Ubuntu Pro FIPS) or use OpenShift with +FIPS enabled at install time. + +As a safeguard, the shipped Compose file sets `PLANE_REQUIRE_FIPS=1`, so the containers **refuse to +start** if the host is not in FIPS mode. Set it to `0` to downgrade that to a startup warning. + +## Deploy + +The Compose file and its supporting files live in the plane-ee repository under +`deployments/cli/commercial/`: + +- `docker-compose-fips.yml` — the FIPS stack +- `variables.env` — environment template +- `README-FIPS.md` — the authoritative operations reference +- `verify-fips.sh` — the verification script (see [Verify](#verify)) + +```bash +# 1. Confirm the host is in FIPS mode (above). +# 2. Prepare the environment file. +cp variables.env .env +# Edit at least: DOMAIN_NAME, WEB_URL, SECRET_KEY, MACHINE_SIGNATURE. + +# 3. Bring the stack up. +docker compose -f docker-compose-fips.yml up -d +``` + +Each container logs its posture on startup: + +``` +plane: FIPS mode ACTIVE (host kernel reports fips_enabled=1) +``` + +The Go services (monitor, email, proxy) log a corresponding line, for example +`Go FIPS 140-3 module ACTIVE`. + +## Verify + +`verify-fips.sh` asserts the posture across the running stack — the kernel flag inside each +container, that the validated OpenSSL provider is loaded and active, that a non-approved digest is +refused, that Node's `crypto.getFips()` returns 1, and that the Go services report the module. It +exits non-zero if any assertion fails, so it can gate a deployment pipeline: + +```bash +./verify-fips.sh +``` + +## Configuration defaults specific to FIPS images + +The FIPS images default to a stricter security posture than the standard images. Each default is +overridable with an environment variable, in either direction. These matter mainly if you are +moving an existing standard deployment onto the FIPS images; a fresh FIPS install needs none of +them changed. + +| Setting | FIPS default | Standard default | Notes | +| ---------------------------------- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------- | +| `LDAP_TLS_REQUIRE_CERT` | `demand` | `never` | Validates the directory server's TLS certificate. See [LDAP](#ldap-certificate-validation). | +| `SAML_REJECT_DEPRECATED_ALGORITHM` | on | off | Rejects assertions signed with RSA-SHA1. The IdP must sign with SHA-256. | +| `SECRET_ENCRYPTION_V2` | on | off | Writes at-rest secrets as AES-256-GCM instead of the legacy format. Both formats are always readable. | +| `USAGE_ID_DIGEST` | `sha256` | `md5` | Digest for Plane AI usage-ledger keys. A FIPS-mode Postgres refuses `md5()`. | + +### LDAP certificate validation + +On the FIPS images, LDAP TLS certificate validation is on by default. For it to succeed, **both** of +the following must hold: + +1. The directory certificate chains to a trusted CA. For a private or self-signed CA, point + `LDAP_TLS_CA_CERTFILE` at your CA bundle (PEM). +2. The certificate's CN/SAN matches the host in `LDAP_SERVER_URI`. An IP address or short hostname + that is not in the certificate's SAN fails hostname verification **even with the correct CA + bundle** — use the fully qualified name the certificate was issued for. + +Setting `LDAP_TLS_REQUIRE_CERT=never` restores the previous behaviour and logs a warning on every +connection. + +## Running under a non-root or arbitrary UID (OpenShift) + +The FIPS application images run non-root. On plain Kubernetes, set a `securityContext` that pins the +image's built-in user (uid `1000`); FIPS mode itself requires no privilege. On OpenShift, the +`restricted-v2` SCC runs each container as an arbitrary high UID that is always a member of group +`0` — the images' writable directories are group-`0` writable to support exactly this, so no image +change is needed. Run the pods with `runAsGroup: 0` / `fsGroup: 0` so that arbitrary UID keeps write +access. The bundled proxy is the one exception: Caddy binds `:80`/`:443`, which `restricted-v2` +forbids for non-root — front it with an OpenShift Route on high ports, or use a custom SCC that +grants `NET_BIND_SERVICE`. Ingress-based deployments do not use the bundled proxy. + +## Scope of coverage + +**Covered.** The Plane application images run their cryptography against FIPS-validated modules on a +FIPS-enforcing host. Non-approved algorithms are refused. + +**The bundled data plane is not FIPS.** The `postgres`, `valkey`, `rabbitmq`, `minio`, and +`iframely` services in the Compose file are upstream Alpine/musl images with no FIPS-validated +cryptography — there are no FIPS variants of them. They are suitable for evaluation only. For an +accreditable deployment, replace them with externally managed datastores on FIPS endpoints and +repoint the connection variables: + +| Service | Replace with | Variables | +| ------------- | --------------------------------- | --------------------------------------------- | +| `plane-db` | RDS / Aurora PostgreSQL | `DATABASE_URL`, `PGHOST`, `POSTGRES_*` | +| `plane-redis` | ElastiCache (Valkey/Redis) | `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` | +| `plane-mq` | Amazon MQ (RabbitMQ) | `AMQP_URL`, `RABBITMQ_*` | +| `plane-minio` | S3 on a FIPS endpoint, or similar | `AWS_S3_ENDPOINT_URL`, `AWS_*`, `USE_MINIO=0` | + +Then set the corresponding `*_REPLICAS` to `0`, or remove those services, so the bundled ones do +not start. + +**TLS termination.** The bundled proxy (Caddy) is built against a FIPS-validated module, but for an +accredited topology the recommended pattern is to terminate TLS at a validated endpoint in front of +the deployment — such as a FIPS-enabled load balancer — and have the proxy serve HTTP internally. + +**FIPS validation applies to the cryptographic modules, not to Plane as a product.** FIPS 140-3 +certificates are held by the module vendors (Red Hat and the Go project). This deployment ensures +Plane's cryptography _uses_ those validated modules on a compliant host; it does not make Plane +itself a FIPS-certified product. + +## Reference + +The authoritative operations reference — including every environment variable and the migration +notes for moving a standard deployment onto FIPS images — is `README-FIPS.md`, shipped alongside +the Compose file in `deployments/cli/commercial/`. From bb0a9a71632497c9bb9089ba1fb10de6f7db9a30 Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 6 Aug 2026 17:30:10 +0530 Subject: [PATCH 2/3] docs(self-hosting): make the FIPS guide discoverable in the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "FIPS deployment" entry to the "Self-host Plane" sidebar group in both the root and /self-hosting/ sidebars, and drop `search: false` / `sidebar: false` from the page frontmatter so it renders with the nav and is found by in-site (⌘K) search. Keep the robots noindex tag — discoverable within the docs site, not indexed by public search engines (matching its commercial-feature nature). Co-Authored-By: Claude Opus 4.8 --- docs/.vitepress/config.mts | 2 ++ .../methods/install-methods-commercial/fips-deployment.md | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 50ccb2f..ab488b1 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -300,6 +300,7 @@ export default extendConfig( { text: "Self-hosting 101", link: "/self-hosting/self-hosting-101" }, { text: "Docker Compose", link: "/self-hosting/methods/docker-compose" }, { text: "Kubernetes", link: "/self-hosting/methods/kubernetes" }, + { text: "FIPS deployment", link: "/self-hosting/methods/install-methods-commercial/fips-deployment" }, ], }, { @@ -328,6 +329,7 @@ export default extendConfig( { text: "Self-hosting 101", link: "/self-hosting/self-hosting-101" }, { text: "Plane Editions", link: "/self-hosting/editions-and-versions" }, { text: "Plane Architecture", link: "/self-hosting/plane-architecture" }, + { text: "FIPS deployment", link: "/self-hosting/methods/install-methods-commercial/fips-deployment" }, ], }, { diff --git a/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md b/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md index 73c9c70..2bdea50 100644 --- a/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md +++ b/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md @@ -2,8 +2,6 @@ title: FIPS-enabled deployment description: Deploy the FIPS variant of Plane Enterprise on a FIPS-enforcing host, including prerequisites, image list, verification, and scope of coverage. keywords: plane fips, fips 140-3 deployment, plane commercial fips, govcloud plane, federal self-hosting, fips enabled containers -search: false -sidebar: false head: - - meta - name: robots From ffa4807eb55a7d7d634c88be4e9200a55b5011d9 Mon Sep 17 00:00:00 2001 From: Pratapa Lakshmi Date: Thu, 6 Aug 2026 17:35:46 +0530 Subject: [PATCH 3/3] docs(fips): address review feedback on the FIPS deployment guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Host setup: split by distro/version. AL2023 + RHEL 8/9 use fips-mode-setup (AL2023 keeps /boot on root; RHEL needs a mounted separate /boot if present). RHEL 10 removed fips-mode-setup — FIPS must be enabled at install with fips=1; post-install switching is unsupported. - OpenShift: don't set runAsUser/runAsGroup/fsGroup under restricted-v2 — the SCC assigns the UID and allocates fsGroup from the namespace supplemental-group range, so an explicit fsGroup: 0 is rejected unless that range includes 0. The runAsGroup/fsGroup: 0 guidance now applies only to plain Kubernetes. Proxy note reworded to "an SCC that permits NET_BIND_SERVICE". - USAGE_ID_DIGEST: clarify md5 is incompatible under FIPS (sha256 required), not an either-direction override. - verify-fips.sh: attribute to the plane-ee repo and soften wording (it is not committed to this docs repo). - Add the `text` language tag to the startup-log fenced block. Co-Authored-By: Claude Opus 4.8 --- .../fips-deployment.md | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md b/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md index 2bdea50..10a9085 100644 --- a/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md +++ b/docs/self-hosting/methods/install-methods-commercial/fips-deployment.md @@ -56,7 +56,9 @@ The host kernel must be booted in FIPS mode. The container inherits this through cat /proc/sys/crypto/fips_enabled # must print 1 ``` -To enable FIPS mode on a RHEL-family host (RHEL, Rocky, Alma, Amazon Linux 2023): +How you put the host into FIPS mode depends on the distribution and version: + +**Amazon Linux 2023, RHEL 8/9 (and Rocky, Alma)** — enable in place, then reboot: ```bash sudo dnf install -y crypto-policies-scripts @@ -64,9 +66,13 @@ sudo fips-mode-setup --enable sudo reboot ``` -`/boot` must be its own filesystem for this to work — it is on the stock cloud images. -Alternatively, boot a vendor FIPS image (a RHEL FIPS AMI, Ubuntu Pro FIPS) or use OpenShift with -FIPS enabled at install time. +On AL2023 `/boot` lives on the root filesystem, so no separate partition is required. On RHEL/Rocky/Alma with a **separate** `/boot` (or `/boot/efi`) partition, that partition must be mounted so `fips-mode-setup` can update the bootloader. + +**RHEL 10** — `fips-mode-setup` has been **removed**, and switching an already-installed system to FIPS mode is **not supported**. FIPS mode must be enabled **at install time** by adding `fips=1` to the kernel command line (or `fips = true` in a RHEL image-builder blueprint). A post-install `update-crypto-policies --set FIPS` is **not** sufficient for FIPS 140 compliance — the only supported path on a non-FIPS install is reinstalling. + +**Other** — boot a vendor FIPS image (a RHEL FIPS AMI, Ubuntu Pro FIPS), or install OpenShift with FIPS enabled. + +In all cases, the definitive check is the kernel flag above (`/proc/sys/crypto/fips_enabled` = `1`). As a safeguard, the shipped Compose file sets `PLANE_REQUIRE_FIPS=1`, so the containers **refuse to start** if the host is not in FIPS mode. Set it to `0` to downgrade that to a startup warning. @@ -93,7 +99,7 @@ docker compose -f docker-compose-fips.yml up -d Each container logs its posture on startup: -``` +```text plane: FIPS mode ACTIVE (host kernel reports fips_enabled=1) ``` @@ -102,10 +108,11 @@ The Go services (monitor, email, proxy) log a corresponding line, for example ## Verify -`verify-fips.sh` asserts the posture across the running stack — the kernel flag inside each -container, that the validated OpenSSL provider is loaded and active, that a non-approved digest is -refused, that Node's `crypto.getFips()` returns 1, and that the Go services report the module. It -exits non-zero if any assertion fails, so it can gate a deployment pipeline: +`verify-fips.sh` (shipped in plane-ee under `deployments/cli/commercial/`, alongside the Compose +file) checks the posture across the running stack — the kernel flag inside each container, that the +validated OpenSSL provider is loaded and active, that a non-approved digest is refused, that Node's +`crypto.getFips()` returns 1, and that the Go services report the module. It is designed to exit +non-zero when a check does not hold, so it can gate a deployment pipeline: ```bash ./verify-fips.sh @@ -118,12 +125,12 @@ overridable with an environment variable, in either direction. These matter main moving an existing standard deployment onto the FIPS images; a fresh FIPS install needs none of them changed. -| Setting | FIPS default | Standard default | Notes | -| ---------------------------------- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------- | -| `LDAP_TLS_REQUIRE_CERT` | `demand` | `never` | Validates the directory server's TLS certificate. See [LDAP](#ldap-certificate-validation). | -| `SAML_REJECT_DEPRECATED_ALGORITHM` | on | off | Rejects assertions signed with RSA-SHA1. The IdP must sign with SHA-256. | -| `SECRET_ENCRYPTION_V2` | on | off | Writes at-rest secrets as AES-256-GCM instead of the legacy format. Both formats are always readable. | -| `USAGE_ID_DIGEST` | `sha256` | `md5` | Digest for Plane AI usage-ledger keys. A FIPS-mode Postgres refuses `md5()`. | +| Setting | FIPS default | Standard default | Notes | +| ---------------------------------- | ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `LDAP_TLS_REQUIRE_CERT` | `demand` | `never` | Validates the directory server's TLS certificate. See [LDAP](#ldap-certificate-validation). | +| `SAML_REJECT_DEPRECATED_ALGORITHM` | on | off | Rejects assertions signed with RSA-SHA1. The IdP must sign with SHA-256. | +| `SECRET_ENCRYPTION_V2` | on | off | Writes at-rest secrets as AES-256-GCM instead of the legacy format. Both formats are always readable. | +| `USAGE_ID_DIGEST` | `sha256` (required) | `md5` | Digest for Plane AI usage-ledger keys. Under FIPS this is **not** an "either direction" override: a FIPS-mode Postgres refuses `md5()`, so `sha256` is required and `md5` is incompatible. | ### LDAP certificate validation @@ -141,14 +148,22 @@ connection. ## Running under a non-root or arbitrary UID (OpenShift) -The FIPS application images run non-root. On plain Kubernetes, set a `securityContext` that pins the -image's built-in user (uid `1000`); FIPS mode itself requires no privilege. On OpenShift, the -`restricted-v2` SCC runs each container as an arbitrary high UID that is always a member of group -`0` — the images' writable directories are group-`0` writable to support exactly this, so no image -change is needed. Run the pods with `runAsGroup: 0` / `fsGroup: 0` so that arbitrary UID keeps write -access. The bundled proxy is the one exception: Caddy binds `:80`/`:443`, which `restricted-v2` -forbids for non-root — front it with an OpenShift Route on high ports, or use a custom SCC that -grants `NET_BIND_SERVICE`. Ingress-based deployments do not use the bundled proxy. +The FIPS application images run non-root, and FIPS mode itself requires no privilege. How you set +the pod security context depends on the platform: + +**Plain Kubernetes.** Pin the image's built-in user with `runAsUser: 1000`. If you run under a +different UID, also set `runAsGroup: 0` and `fsGroup: 0` so that UID keeps write access through the +images' group-`0`-writable directories. + +**OpenShift (`restricted-v2`).** Do **not** set `runAsUser`, `runAsGroup`, or `fsGroup` yourself. The +SCC assigns an arbitrary high UID that is a member of group `0`, and it allocates `fsGroup` from the +namespace's `openshift.io/sa.scc.supplemental-groups` range — an explicit `fsGroup: 0` is rejected +unless that range includes `0`. No image change or group override is needed: the images' writable +directories are already group-`0` writable, which is exactly what the assigned UID needs. + +The bundled proxy is the one exception: Caddy binds `:80`/`:443`, which `restricted-v2` forbids for a +non-root process. Front it with an OpenShift Route (running Caddy on high ports), or grant it an SCC +that permits `NET_BIND_SERVICE`. Ingress-based deployments do not use the bundled proxy. ## Scope of coverage