---
title: "Domain and TLS"
description: "Set up the NGINX ingress controller, DNS record, and cert-manager issuer the platform needs before the Helm install."
source: "Guardian Platform"
---
Prepare the cluster entry point: an NGINX ingress controller, a DNS record for the platform domain, and a cert-manager issuer that requests Let's Encrypt certificates for the platform's ingresses.

## Before You Begin

- `kubectl` and `helm` access to the cluster, and the `guardian` namespace (`kubectl create namespace guardian`)
- A clone of the [deployment repository](./prerequisites.mdx#deployment-repository) for the helper scripts
- A domain you control, such as `guardian.example.com`, and access to its DNS zone
- An email address for Let's Encrypt expiry notices
- Inbound port 80 from the internet to the ingress controller, which the Let's Encrypt HTTP-01 challenge uses

Without public HTTP-01, such as in an air-gapped cluster or with a private CA, skip Steps 3 and 4: create the TLS secret named by each ingress's `tls[].secretName` yourself with `kubectl create secret tls` (`guardian-tls` in [Chart Values](./chart-values.mdx), `<env>-tls-secret` from `guardctl`), and remove the `cert-manager.io/issuer` annotations from `values.yaml`.

All four platform services share the one domain under different paths. Auth0 and Microsoft Entra ID need no extra domain. Keycloak can run on the same domain under `/keycloak` or on its own domain, such as `keycloak.example.com`; see [Keycloak](./keycloak.mdx).

## Step 1: Install the NGINX Ingress Controller

Skip this step if the cluster already runs ingress-nginx. From the deployment repository root, run [`scripts/nginx.sh`](https://github.com/eqtylab/deployment/blob/main/scripts/nginx.sh):

```bash
./scripts/nginx.sh   # installs into ingress-nginx; change with --namespace
```

The script also sets the Azure load balancer health probe path to `/healthz`. On AKS, set the `service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path` annotation to `/healthz` on the controller service if you install the controller another way.

## Step 2: Create the DNS Record

Find the controller's external address in the `EXTERNAL-IP` column:

```bash
kubectl get svc -n ingress-nginx ingress-nginx-controller
```

Create a record for the platform domain that points at that address:

| Cloud | `EXTERNAL-IP` shows                         | Record type |
| ----- | ------------------------------------------- | ----------- |
| EKS   | A hostname, such as `xxx.elb.amazonaws.com` | CNAME       |
| AKS   | An IP address                               | A           |
| GKE   | An IP address                               | A           |

If Keycloak runs on its own domain, add a second record of the same type for it.

Create the record now: the certificate request at install fails until the domain resolves to the controller. Confirm it resolves before you install:

```bash
dig +short guardian.example.com
```

## Step 3: Install cert-manager

Skip this step if the cluster already runs cert-manager. Run [`scripts/cert-issuer.sh`](https://github.com/eqtylab/deployment/blob/main/scripts/cert-issuer.sh), which installs cert-manager with its CRDs:

```bash
./scripts/cert-issuer.sh   # installs into cert-manager; change with --namespace
```

## Step 4: Create the Let's Encrypt Issuer

The platform's ingresses use a namespace-scoped Issuer named `letsencrypt-prod` in the release namespace:

```bash
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: letsencrypt-prod
  namespace: guardian
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <your-email>   # Let's Encrypt expiry notices
    privateKeySecretRef:
      name: letsencrypt-production
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
EOF
```

To share one issuer across namespaces, create a ClusterIssuer instead: set `kind: ClusterIssuer` and remove `metadata.namespace`. Then use the `cert-manager.io/cluster-issuer` annotation in Step 5 in place of `cert-manager.io/issuer`.

## Step 5: Point the Ingresses at the Issuer

The ingress blocks in [Chart Values](./chart-values.mdx) already name the `letsencrypt-prod` Issuer and share one `guardian-tls` secret across all four services. If you renamed the Issuer or created a ClusterIssuer, match the annotation on each. cert-manager requests the certificate when the chart is installed and renews it before it expires.

If you enable the Gateway Stack ingress, set `gateway-stack.ingress.className: nginx` and add DNS records for its hosts that aren't the platform domain. Add `cert-manager.io/issuer: "letsencrypt-prod"` under `gateway-stack.ingress.annotations`, or set `gateway-stack.ingress.certManager.clusterIssuer` to the ClusterIssuer name. The Gateway Stack names its own TLS secrets.

## Verify

After the [Helm Deployment](./helm-deployment.mdx), check the certificate in the release namespace. It is named after the `secretName` in your values (`guardctl` uses `<env>-tls-secret`):

```bash
kubectl get certificate -n guardian
```

```
NAME           READY   SECRET         AGE
guardian-tls   True    guardian-tls   2m
```

Then open `https://guardian.example.com` and confirm the browser trusts the certificate.

## Troubleshooting

If `READY` stays `False`, read the events in `kubectl describe certificate guardian-tls -n guardian`.

| Symptom                              | Likely cause                                              | Resolution                                                                                                |
| ------------------------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Issuer `letsencrypt-prod` not found  | Issuer in another namespace, or annotation kind mismatch  | Create the Issuer in the release namespace, or match `issuer` vs `cluster-issuer` to the kind you created |
| HTTP-01 challenge pending or failing | DNS not propagated, or port 80 not reachable              | Confirm `dig` returns the controller address and the controller accepts traffic on port 80                |
| Let's Encrypt rate limit error       | Too many certificate requests for the domain              | Wait for the limit to reset; test repeated installs against the Let's Encrypt staging server              |
| No Certificate resource exists       | cert-manager not running, or ingress lacks the annotation | Check `kubectl get pods -n cert-manager` and the ingress annotations in `values.yaml`                     |

## Related

- [Helm Deployment](./helm-deployment.mdx) — installing the chart once the domain and issuer are ready
- [Chart Values](./chart-values.mdx) — the full ingress block for each service
- [Keycloak](./keycloak.mdx) — the Keycloak ingress host and path
- [Prerequisites](./prerequisites.mdx) — the other cluster and cloud resources to prepare