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
The three hostnames
All three derive from
global.domain unless you override them individually.
MCP clients: Claude Code, Claude Desktop, Cursor
An MCP server’s Connect card gives people a command such asclaude mcp add --transport http <name> https://api.<domain>/relay/v1/mcp-servers/<id>/mcp.
Their MCP client then signs in with OAuth by itself: it reads the relay’s
resource metadata on api.<domain>, opens a browser login on auth.<domain>,
and calls the relay with the token it gets back. So both hosts must be
reachable from people’s own machines, not just from inside the cluster,
and each person must be subscribed to the MCP server (its Users tab).
- Ingress on: nothing to set — the chart derives the relay URL, its OAuth
resource and the callback for OAuth-protected MCP servers from
api.<domain>. - You expose the gateway yourself (a load balancer, a VPN hostname): set
services.gateway.publicUrl: https://swarmd-api.example.com, and make Keycloak reachable at its public URL too. - Port-forward only (for example the PrismForce preset): people cannot
connect MCP clients. The Connect card says no relay URL is provisioned, and
helm install/helm upgradeprint a note saying so. Agents in the cluster are not affected.
/realms (logins, tokens, discovery) and
/resources (login page assets). The admin console under /admin is not
exposed; reach it with a port-forward, or set
keycloak.ingress.exposeAdminConsole: true if you need it on the public host.
If you expose Keycloak some other way at the same host, set
keycloak.ingress.enabled: false.
The base configuration
tls.secretNameis a single value shared by all three Ingresses. One certificate has to coverapp.,api.andauth.— a SAN or wildcard cert. If you need a certificate per host, settls.enabled: falseand 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
- Traefik + cert-manager (recommended)
- AWS Load Balancer Controller
Traefik reads standard Ingress objects, so Using Traefik’s own ACME resolver rather than cert-manager? Then Traefik
holds the certificate itself and you don’t want a
className: traefik is most of
the job.tls block at all:Migrating off ingress-nginx
Migrating off ingress-nginx
The chart emits standard Ingress objects, so moving is a values change rather
than a redeployment:
- Install Traefik and confirm its IngressClass exists —
kubectl get ingressclass. - Swap
global.ingress.classNamefromnginxtotraefik. - 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. helm upgrade, then re-point DNS at the Traefik load balancer.
DNS
The chart creates Ingress objects; it does not touch DNS. Point all three names at your ingress controller’s address:*.swarmd.example.com covers all three and anything you add later.
Verifying
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.
Ingress has no ADDRESS
Ingress has no ADDRESS
The controller hasn’t claimed it — usually a
className that matches no
IngressClass in the cluster:404 from the ingress controller
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.Login redirects to an internal URL
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.Long agent calls fail at ~60 seconds
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.Next
Configuration
SMTP, secrets, app settings and optional components.
Presets
Ready-made values files, including HTTPS shapes.
