> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openhands.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# DNS and TLS

> Automate DNS records and TLS certificates with external-dns and cert-manager

OpenHands needs DNS records and TLS certificates for its hostnames. We recommend automating both with
**external-dns** and **cert-manager**, which run on any Kubernetes distribution and support the major
cloud DNS providers. If you can't run them, provision the records and certificates by hand, see
[Manual Setup](#manual-setup).

## Hostnames

OpenHands serves these hostnames, using `openhands.example.com` as the base domain (matching the
[Helm install](/enterprise/k8s-install/installation)):

| Hostname                            | Purpose               |
| ----------------------------------- | --------------------- |
| `app.openhands.example.com`         | Application           |
| `auth.app.openhands.example.com`    | Login (Keycloak)      |
| `runtime-api.openhands.example.com` | Runtime API           |
| `*.runtime.openhands.example.com`   | Per-session sandboxes |

All of these must resolve to your ingress load balancer. The sandbox entry must be a **wildcard**
because each session gets its own subdomain.

## external-dns

external-dns watches your Ingresses and Services and creates the matching DNS records automatically.

* Install it from its [Helm chart](https://kubernetes-sigs.github.io/external-dns/).
* Set `provider` to your DNS provider and grant it access to your zone (the access mechanism is
  provider-specific).
* Recommended settings:

```yaml theme={null}
provider:
  name: aws                   # or google, azure, cloudflare, ...
policy: upsert-only           # only ever create/update, never delete
registry: txt
txtOwnerId: openhands
domainFilters:
  - openhands.example.com     # only manage names under your base domain
```

With `upsert-only` and a TXT registry, external-dns only ever touches records it created.

## cert-manager

cert-manager issues and renews certificates from Let's Encrypt. Use the **DNS-01** challenge, the
only one that can issue the **wildcard** certificate the sandbox hostnames need.

<Steps>
  <Step title="Install cert-manager">
    Install it from its [Helm chart](https://cert-manager.io/docs/installation/helm/), and grant it
    access to your DNS provider so it can solve DNS-01 challenges.
  </Step>

  <Step title="Create a ClusterIssuer">
    The `solvers` block is specific to your DNS provider. The Route 53 solver is shown here.

    ```yaml theme={null}
    apiVersion: cert-manager.io/v1
    kind: ClusterIssuer
    metadata:
      name: letsencrypt-prod
    spec:
      acme:
        server: https://acme-v02.api.letsencrypt.org/directory
        email: you@example.com
        privateKeySecretRef:
          name: letsencrypt-prod
        solvers:
          - dns01:
              route53:                 # swap for cloudDNS, azureDNS, cloudflare, ...
                hostedZoneID: <your-zone-id>
    ```

    <Tip>
      Start with the staging server (`https://acme-staging-v02.api.letsencrypt.org/directory`) while
      you get the setup working (generous rate limits), then switch to production.
    </Tip>
  </Step>

  <Step title="Request a wildcard certificate">
    A single wildcard covers every sandbox host. With Traefik, serve it as the default `TLSStore` so
    no per-ingress TLS config is needed.

    ```yaml theme={null}
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: runtime-wildcard
      namespace: openhands
    spec:
      secretName: runtime-wildcard-tls
      issuerRef:
        name: letsencrypt-prod
        kind: ClusterIssuer
      dnsNames:
        - "*.runtime.openhands.example.com"
    ```

    Issue certificates for the `app`, `auth`, and `runtime-api` hosts the same way.
  </Step>
</Steps>

## Manual Setup

If you don't run external-dns and cert-manager, provision these by hand and point the ingress
controller at them.

**DNS**: create a record for each hostname in the table above, all pointing to your ingress load
balancer (typically a CNAME to the load balancer's hostname, or a cloud DNS alias). The sandbox
record must be the wildcard `*.runtime.openhands.example.com`.

**TLS**: obtain certificates covering those hostnames and load them into the ingress controller as
Kubernetes TLS secrets. A single wildcard isn't enough, because the hostnames sit at different
depths. You need:

* `*.runtime.openhands.example.com` for the sandboxes, and
* certificates for `app.openhands.example.com`, `auth.app.openhands.example.com`, and
  `runtime-api.openhands.example.com` (for example a `*.openhands.example.com` wildcard, which covers
  `app` and `runtime-api`, plus a certificate for `auth.app.openhands.example.com`).

## Next Steps

<CardGroup cols={2}>
  <Card title="Installing Sysbox" icon="cube" href="/enterprise/k8s-install/sysbox">
    Install the sandbox runtime on your sandbox nodes.
  </Card>

  <Card title="Install with Helm" icon="ship" href="/enterprise/k8s-install/installation">
    Deploy OpenHands once the cluster is ready.
  </Card>
</CardGroup>
