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 must set ingressClassName: traefik. Omitting the field or using a different value results in the Ingress being silently ignored — Traefik only watches resources that explicitly name it. There is no default ingress class configured at the cluster level, so “leave it blank and hope” does not work.

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.

Capsule enforces hostnameCollisionScope: Tenant — an Ingress whose host already exists on another Ingress within the same tenant is rejected at admission. Two different tenants cannot shadow each other because cross-tenant routing is already walled off at the network layer.

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. See Known limitations — Hostname collision scope for the Capsule field that controls this.

An Ingress with a wildcard host (e.g. host: "*.<your-tenant>.kestrel.arbutus.cloud") is rejected at admission by Capsule (allowWildcardHostnames: false). Wildcard hostnames would let one tenant shadow every subdomain under a parent, which breaks the per-host routing guarantees the shared edge depends on.

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 and they can add an explicit allow. See Known limitations — Wildcard hostnames.

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 networking/tls 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: ClusterIPLoadBalancer 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.