Skip to content

Ingress on Kestrel

All external HTTP and HTTPS traffic enters Kestrel through Traefik, the cluster’s sole ingress controller. There is no cloud load balancer pool and no alternative ingress class — every tenant uses the same shared edge. This page covers what the Ingress resource looks like, how hostnames work, and how TLS is automated.

Every Ingress should set ingressClassName: traefik explicitly. The traefik IngressClass is the cluster default, so an Ingress created without the field has traefik filled in automatically at admission — but relying on the default makes manifests harder to read and less portable, so set it anyway. An Ingress that names a different class is silently ignored: Traefik only watches resources assigned to its class, and no other ingress controller exists on Kestrel.

Each tenant is assigned a subdomain of kestrel.arbutus.cloud. Tenant hostnames follow the pattern:

<your-tenant>.kestrel.arbutus.cloud
*.<your-tenant>.kestrel.arbutus.cloud

For example, if your tenant is def-profname and your application is my-service, the hostname is my-service.def-profname.kestrel.arbutus.cloud. The bare tenant hostname (def-profname.kestrel.arbutus.cloud) is also available. There is no strict enforcement of this naming convention at admission time, but deviating from it makes traffic harder to trace in Grafana and support tickets harder to triage.

Hostname collision checks are not currently enforced at admission — Capsule accepts an Ingress even if its host is already claimed by another Ingress, inside or outside your tenant. Duplicate hosts make Traefik’s routing for that hostname ambiguous, so treat hostnames as first-come-first-served and stay within your tenant’s subdomain convention above. See Known limitations for the platform stance.

If you are migrating traffic from one namespace to another inside your tenant, delete the old Ingress first (or temporarily change the hostname on the stale copy) before creating the new one — otherwise both Ingresses claim the host and requests may keep landing on the stale copy.

An Ingress with a wildcard host (e.g. host: "*.<your-tenant>.kestrel.arbutus.cloud") is not currently rejected at admission, but do not use one. A wildcard host claims every subdomain under its parent at the shared edge, shadowing other services’ hostnames and breaking the per-host routing the shared edge depends on. See Known limitations.

Use one Ingress (or one Ingress rule) per concrete hostname. If you have a legitimate need for a wildcard range — for example, a platform-style tenant that owns a subtree — open a ticket with RCS first so the routing and DNS implications can be worked through.

TLS certificates are issued automatically by cert-manager using the letsencrypt-prod cluster issuer. Add the annotation and the tls block to your Ingress and cert-manager handles the ACME challenge, certificate issuance, and renewal. No manual certificate management is needed for hosts under your tenant’s kestrel.arbutus.cloud subdomain — the letsencrypt-prod cluster issuer is restricted to those names. Custom domains require your own namespace-scoped Issuer; see TLS certificates for both paths.

The annotations that trigger cert-manager and configure Traefik TLS:

annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"

The tls block names the host and the Secret where the certificate will be stored:

tls:
- hosts:
- my-service.<your-tenant>.kestrel.arbutus.cloud
secretName: my-service-tls

cert-manager creates the Secret automatically — you do not need to pre-create it. The certificate renews before expiry without intervention. See TLS certificates for the full cert-manager details, including what tenants do and do not control.

A minimal working Ingress with TLS. Replace <your-tenant> with your tenant name and my-service with your application name.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-service
namespace: <your-tenant>-prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- my-service.<your-tenant>.kestrel.arbutus.cloud
secretName: my-service-tls
rules:
- host: my-service.<your-tenant>.kestrel.arbutus.cloud
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80

This Ingress routes https://my-service.<your-tenant>.kestrel.arbutus.cloud/ to the my-service ClusterIP Service on port 80. The backend Service must be type: ClusterIP — LoadBalancer and NodePort are blocked. See Service types for why and what to use instead.

Traefik runs in the traefik namespace as a Deployment. Kestrel has no in-cluster load balancer (no kube-vip); external traffic enters the cluster through Cloudflare and is routed to Traefik. The default-deny NetworkPolicy in every tenant namespace includes a pre-approved allow rule for Traefik’s pods, so your workload receives traffic from the ingress controller without any additional NetworkPolicy on your side. See Network model for the full picture of default-deny plus the four pre-approved allow rules.