From 6e14da36fc00f08724f6421cde1f102ffc4df461 Mon Sep 17 00:00:00 2001
From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com>
Date: Wed, 10 Jun 2026 14:54:33 +0000
Subject: [PATCH] docs: add private load balancers and DNS-01 credentials guide
---
.../configure/private-load-balancers.mdx | 128 ++++++++++++++++++
1 file changed, 128 insertions(+)
create mode 100644 applications/configure/private-load-balancers.mdx
diff --git a/applications/configure/private-load-balancers.mdx b/applications/configure/private-load-balancers.mdx
new file mode 100644
index 0000000..587c0f6
--- /dev/null
+++ b/applications/configure/private-load-balancers.mdx
@@ -0,0 +1,128 @@
+---
+title: "Private load balancers"
+description: "Expose web services through an internal NGINX ingress controller backed by a private NLB, with TLS certificates issued via the ACME DNS-01 challenge"
+---
+
+
+ Private load balancers are gated behind a feature flag. Contact Porter support to enable internal NLBs and DNS-01 certificate issuance on your cluster.
+
+
+Porter clusters ship with a public load balancer that fronts every web service by default. For workloads that should only be reachable from inside your VPC — internal admin tools, partner integrations, services consumed exclusively by other apps in the cluster — you can attach a **private load balancer** to the cluster. Porter provisions a dedicated internal NGINX ingress controller for it and issues TLS certificates using the ACME DNS-01 challenge, so the certificate flow works even when the load balancer has no public endpoint.
+
+## When to use a private load balancer
+
+Use a private load balancer when:
+
+- The service must not be reachable from the public internet.
+- Traffic should stay inside the VPC, a peered network, or a corporate network reached over a VPN or Direct Connect / Cloud Interconnect / ExpressRoute.
+- You still want a managed TLS certificate on a custom domain, served by NGINX, with automatic renewal.
+
+Stick with the default public load balancer when the service needs to be reachable from the public internet.
+
+## How it works
+
+When you enable a private load balancer on the cluster, Porter installs the following onto your cluster:
+
+- A second `ingress-nginx` controller (chart `ingress-nginx` `v4.14.0`) bound to the ingress class `nginx-private` and fronted by an internal Network Load Balancer.
+- A `ClusterIssuer` that solves ACME challenges via DNS-01 instead of HTTP-01, so the load balancer never has to be reachable from Let's Encrypt's validation servers.
+- The cert-manager components required to drive that issuer.
+
+Web services that select the private ingress class are routed through the internal NLB. Their TLS certificates are issued and renewed automatically by cert-manager using the DNS-01 challenge against your DNS provider.
+
+## Prerequisites
+
+Before you can attach a private load balancer to a cluster, you need:
+
+- A cluster with the private load balancer feature enabled by Porter.
+- A DNS provider that Porter supports for DNS-01 challenges. Today this is **Cloudflare**.
+- An API token from your DNS provider with permission to create and delete `TXT` records on the zones whose certificates the cluster will issue.
+
+### Creating a Cloudflare API token
+
+In the Cloudflare dashboard, create an API token with:
+
+- **Permissions:** `Zone — DNS — Edit`
+- **Zone Resources:** the specific zone(s) whose certificates Porter will issue, or **All zones** if you want one token to cover everything.
+
+Copy the token — Cloudflare only shows it once.
+
+## Storing DNS provider credentials
+
+Porter stores the DNS provider API token as a Kubernetes secret in the cluster's `cert-manager` namespace, where cert-manager reads it to solve DNS-01 challenges. You manage the credentials per cluster through the Porter API.
+
+### Store or rotate credentials
+
+```bash
+curl -X POST \
+ "https://api.porter.run/api/v2/projects/{project_id}/clusters/{cluster_id}/dns/credentials" \
+ -H "Authorization: Bearer $PORTER_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "provider": "cloudflare",
+ "api_token": ""
+ }'
+```
+
+| Field | Description |
+| ----------- | ------------------------------------------------------------------------------------------ |
+| `provider` | The DNS provider to use for DNS-01 challenges. Only `cloudflare` is currently supported. |
+| `api_token` | API token authorizing the DNS provider to create and delete records for the cluster's domains. |
+
+`POST` is idempotent: calling it again with a new token rotates the stored credentials in place.
+
+### Check whether credentials are stored
+
+```bash
+curl "https://api.porter.run/api/v2/projects/{project_id}/clusters/{cluster_id}/dns/credentials" \
+ -H "Authorization: Bearer $PORTER_TOKEN"
+```
+
+The response indicates whether credentials are present and which provider they target:
+
+```json
+{
+ "exists": true,
+ "provider": "cloudflare"
+}
+```
+
+### Remove credentials
+
+```bash
+curl -X DELETE \
+ "https://api.porter.run/api/v2/projects/{project_id}/clusters/{cluster_id}/dns/credentials" \
+ -H "Authorization: Bearer $PORTER_TOKEN"
+```
+
+Removing credentials stops cert-manager from being able to issue or renew certificates that depend on DNS-01. Only delete credentials when you are decommissioning the private load balancer or rotating to a different provider.
+
+## Enabling a private load balancer on the cluster
+
+A private load balancer is declared on the cluster contract as an `AdditionalLoadBalancer` with `network_access` set to `PRIVATE` and a `dns_provider_config` that matches your stored credentials:
+
+```yaml
+additional_load_balancers:
+ - network_access: PRIVATE
+ dns_provider_config:
+ provider: cloudflare
+```
+
+Notes:
+
+- Only `PRIVATE` is supported on additional load balancers today. Non-private entries are ignored.
+- Only one additional load balancer of each network access type is allowed per cluster. Contract writes that include duplicates are rejected.
+- The DNS provider in `dns_provider_config` must match the provider whose credentials you stored on the cluster. Unsupported providers are rejected at contract write time.
+
+When the contract is applied, Porter reconciles the cluster: it installs the private NGINX controller (ingress class `nginx-private`) and the DNS-01 `ClusterIssuer`, and provisions the internal NLB.
+
+## Routing a web service through the private load balancer
+
+Once the private controller is installed, web services on the cluster can opt into it by selecting the `nginx-private` ingress class for their domains. Custom domains attached to those services receive certificates issued via DNS-01 — no public reachability is required.
+
+Public web services on the same cluster continue to use the default public load balancer and the existing HTTP-01 issuer. The two stacks run side by side.
+
+## Troubleshooting
+
+- **`unsupported DNS provider` when writing the contract.** The `provider` in `dns_provider_config` is not one Porter supports yet. Use `cloudflare`.
+- **`internal load balancers are not enabled for this cluster` from the DNS credentials endpoints.** The feature flag is off for this cluster. Contact Porter support to enable it.
+- **Certificate stuck in `Pending` for a private domain.** Confirm that DNS credentials are stored on the cluster (`GET .../dns/credentials` returns `"exists": true`), that the API token still has `Zone — DNS — Edit` permission on the zone, and that the token has not been rotated out of band in your DNS provider.