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

# Ingress and TLS

> Give Swarmd real hostnames — worked examples for Traefik and AWS Load Balancer Controller.

# Ingress and TLS

Ingress is **off by default**, because a first install shouldn't need DNS.
Once you want real hostnames, the chart renders three Ingress objects for
you.

## Supported controllers

| Controller                       | `className` | Status                                                                        |
| -------------------------------- | ----------- | ----------------------------------------------------------------------------- |
| **Traefik**                      | `traefik`   | **Supported — the default, and what every Swarmd-operated environment runs.** |
| **AWS Load Balancer Controller** | `alb`       | Supported on EKS.                                                             |
| ingress-nginx                    | `nginx`     | **Not supported. Not recommended.**                                           |

<Warning>
  **Do not build a new deployment on ingress-nginx.** Upstream has wound the
  project down, so it is not somewhere to put a platform you intend to run.

  The chart emits plain Ingress objects, so it will *render* against
  ingress-nginx and largely work — but we do not test that path, and we will
  not fix issues specific to it. If you are already on ingress-nginx, plan a
  move to Traefik; the only thing that changes in this chart is `className` and
  the annotation block.
</Warning>

***

## The three hostnames

| Host            | Serves                            | Who talks to it                          | Override                        |
| --------------- | --------------------------------- | ---------------------------------------- | ------------------------------- |
| `app.<domain>`  | Platform UI                       | Browsers                                 | `ui.platform.ingress.host`      |
| `api.<domain>`  | Gateway — every REST and A2A call | Agents, SDKs, your apps                  | `services.gateway.ingress.host` |
| `auth.<domain>` | Keycloak                          | Browsers during login; agents for tokens | `keycloak.ingress.host`         |

All three derive from `global.domain` unless you override them individually.

<Warning>
  **`auth.<domain>` must be reachable from outside the cluster.** It is
  tempting to keep Keycloak internal, but browsers get redirected there during
  login and agents fetch tokens from it. When ingress is on, the chart switches
  Keycloak's issuer to the public URL — tokens minted with an internal issuer
  would be rejected by anything validating them from outside.
</Warning>

***

## The base configuration

```yaml theme={null}
global:
  domain: swarmd.example.com

  ingress:
    enabled: true
    className: traefik         # must match an IngressClass in your cluster
    annotations: {}            # applied to all three Ingresses
    tls:
      enabled: true
      secretName: swarmd-tls   # must already exist — the chart issues nothing
```

Two things to know before the examples:

* **`tls.secretName` is a single value shared by all three Ingresses.** One
  certificate has to cover `app.`, `api.` and `auth.` — a SAN or wildcard
  cert. If you need a certificate per host, set `tls.enabled: false` and drive
  TLS from your controller's annotations instead (that's what the AWS example
  below does).
* **Annotations merge**, with per-component winning over `global`. So you can
  set a shared block globally and add one annotation to just the gateway.

***

## Worked examples

<Tabs>
  <Tab title="Traefik + cert-manager (recommended)">
    Traefik reads standard Ingress objects, so `className: traefik` is most of
    the job.

    ```yaml theme={null}
    global:
      domain: swarmd.example.com
      ingress:
        enabled: true
        className: traefik
        tls:
          enabled: true
          secretName: swarmd-tls
        annotations:
          traefik.ingress.kubernetes.io/router.entrypoints: websecure
          # Attach middleware by <namespace>-<name>@kubernetescrd
          traefik.ingress.kubernetes.io/router.middlewares: swarmd-compress@kubernetescrd
    ```

    Using Traefik's own ACME resolver rather than cert-manager? Then Traefik
    holds the certificate itself and you don't want a `tls` block at all:

    ```yaml theme={null}
    global:
      ingress:
        enabled: true
        className: traefik
        tls:
          enabled: false     # Traefik terminates with its own cert store
        annotations:
          traefik.ingress.kubernetes.io/router.entrypoints: websecure
          traefik.ingress.kubernetes.io/router.tls: "true"
          traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
    ```

    <Warning>
      **Raise `respondingTimeouts.readTimeout` in Traefik's static config.** A
      multi-hop agent chain routinely runs 40–60 seconds, and the relay holds a
      conversation send for up to 100 s before returning `202`. Leave Traefik at a
      60 s default and chains fail at the proxy that the platform would have
      completed — as a client timeout, with nothing in the Swarmd logs to explain
      it.
    </Warning>
  </Tab>

  <Tab title="AWS Load Balancer Controller">
    On EKS the ALB terminates TLS with an ACM certificate, so there is **no
    Kubernetes TLS Secret** — `tls.enabled` stays `false` and everything is
    driven by annotations.

    ```yaml theme={null}
    global:
      domain: swarmd.example.com
      ingress:
        enabled: true
        className: alb
        tls:
          enabled: false     # ACM on the ALB, not a K8s Secret
        annotations:
          alb.ingress.kubernetes.io/scheme: internet-facing
          alb.ingress.kubernetes.io/target-type: ip
          alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
          alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:123456789012:certificate/abcd-1234
          alb.ingress.kubernetes.io/ssl-redirect: "443"
          alb.ingress.kubernetes.io/healthcheck-path: /actuator/health
          # Long agent chains — the ALB default is 60s.
          alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=300
          # Put all three hosts on ONE ALB instead of three.
          alb.ingress.kubernetes.io/group.name: swarmd
    ```

    <Warning>
      **Set `group.name`.** Without it the controller provisions a separate ALB per
      Ingress — three load balancers, three sets of DNS, three bills. With it, all
      three hostnames share one.
    </Warning>

    <Accordion title="Health check path">
      `/actuator/health` is right for the gateway. The platform UI is a static
      front end and doesn't serve it — if the UI target group goes unhealthy, set a
      UI-specific override:

      ```yaml theme={null}
      ui:
        platform:
          ingress:
            annotations:
              alb.ingress.kubernetes.io/healthcheck-path: /
      ```

      Per-component annotations merge over the global block, so this replaces just
      that one key for the UI.
    </Accordion>

    <Accordion title="Internal-only ALB">
      For a private deployment reachable only inside the VPC:

      ```yaml theme={null}
            alb.ingress.kubernetes.io/scheme: internal
      ```

      Remember `auth.<domain>` still has to resolve and be reachable from wherever
      your users' browsers are — a private ALB means VPN or Direct Connect for them
      too.
    </Accordion>
  </Tab>
</Tabs>

<Accordion title="Migrating off ingress-nginx">
  The chart emits standard Ingress objects, so moving is a values change rather
  than a redeployment:

  1. Install Traefik and confirm its IngressClass exists —
     `kubectl get ingressclass`.
  2. Swap `global.ingress.className` from `nginx` to `traefik`.
  3. Replace the `nginx.ingress.kubernetes.io/*` annotations with the Traefik
     equivalents above. The one that matters is the read timeout — it moves
     from a per-Ingress annotation to Traefik's static config.
  4. `helm upgrade`, then re-point DNS at the Traefik load balancer.

  Run both controllers side by side during the cutover if you can: the two
  IngressClasses are independent, so nothing conflicts.
</Accordion>

***

## DNS

The chart creates Ingress objects; it does not touch DNS. Point all three
names at your ingress controller's address:

```
app.swarmd.example.com   → <ingress IP or ALB hostname>
api.swarmd.example.com   → <same>
auth.swarmd.example.com  → <same>
```

A wildcard `*.swarmd.example.com` covers all three and anything you add later.

<Tip>
  Testing on minikube without DNS? Use `nip.io`, which resolves any
  `<anything>.<ip>.nip.io` to that IP:

  ```bash theme={null}
  helm upgrade swarmd ... \
    --set global.ingress.enabled=true \
    --set global.domain=$(minikube ip).nip.io \
    --set global.ingress.tls.enabled=false
  ```
</Tip>

***

## Verifying

```bash theme={null}
kubectl -n swarmd get ingress
```

```
NAME                  CLASS     HOSTS                    ADDRESS         PORTS
swarmd-gateway        traefik   api.swarmd.example.com   203.0.113.10    80, 443
swarmd-platform-ui    traefik   app.swarmd.example.com   203.0.113.10    80, 443
swarmd-keycloak       traefik   auth.swarmd.example.com  203.0.113.10    80, 443
```

Then check each hop:

```bash theme={null}
# Gateway is up and terminating TLS
curl -sS -o /dev/null -w '%{http_code}\n' https://api.swarmd.example.com/actuator/health

# Keycloak is serving the realm with the PUBLIC issuer
curl -sS https://auth.swarmd.example.com/realms/swarmd/.well-known/openid-configuration \
  | jq -r .issuer
```

That last one is the check worth doing. It must print
`https://auth.swarmd.example.com/realms/swarmd`. If it prints an in-cluster
service URL, ingress was enabled without Keycloak picking up the public
hostname, and every externally-issued token will fail audience validation.

<AccordionGroup>
  <Accordion title="Ingress has no ADDRESS">
    The controller hasn't claimed it — usually a `className` that matches no
    IngressClass in the cluster:

    ```bash theme={null}
    kubectl get ingressclass
    ```
  </Accordion>

  <Accordion title="404 from the ingress controller">
    The controller is answering but has no rule for that host. Check the Host
    header matches exactly — `api.<domain>`, not the bare domain — and that DNS
    points where you think.
  </Accordion>

  <Accordion title="Login redirects to an internal URL">
    Keycloak is advertising the wrong issuer. Confirm `global.ingress.enabled` is
    `true` (not just annotations set), and re-check the discovery document above.
  </Accordion>

  <Accordion title="Long agent calls fail at ~60 seconds">
    Proxy timeout, not Swarmd. Raise it — `respondingTimeouts.readTimeout` on
    Traefik, `idle_timeout.timeout_seconds` on ALB. The relay holds a
    conversation send for up to 100 s before returning `202`.
  </Accordion>
</AccordionGroup>

***

## Next

<CardGroup cols={2}>
  <Card title="Configuration" icon="sliders" href="/self-hosting/configuration">
    SMTP, secrets, app settings and optional components.
  </Card>

  <Card title="Presets" icon="layer-group" href="/self-hosting/presets">
    Ready-made values files, including HTTPS shapes.
  </Card>
</CardGroup>
