Why LoadBalancer and NodePort are blocked
Kestrel blocks three Kubernetes service types at the admission layer. Submitting a Service manifest with any of these types is rejected before the resource is created — you see a Capsule denial in kubectl output, not a runtime failure. This page explains each blocked type, why it is blocked, and the sanctioned alternative.
LoadBalancer
Section titled “LoadBalancer”Blocked by: Capsule Tenant.spec.serviceOptions.allowedServices.loadBalancer: false.
A Service with type: LoadBalancer is rejected at admission. Kestrel does not provision cloud load balancers per workload — the cluster sits behind a single shared edge (Traefik), not a cloud LB pool, so the resource shape is disabled at the tenant boundary.
What to use instead: expose HTTP and HTTPS workloads through an Ingress with ingressClassName: traefik. Traefik handles TLS termination and routing at the shared edge. See Ingress on Kestrel for the complete recipe.
NodePort
Section titled “NodePort”Blocked by: Capsule Tenant.spec.serviceOptions.allowedServices.nodePort: false.
A Service with type: NodePort is rejected at admission. Node ports pin traffic to individual cluster nodes and bypass the shared edge — on a multi-tenant cluster this creates routing ambiguity and port-range contention between tenants.
What to use instead: the same Ingress path as above for external HTTP/HTTPS traffic. For pod-to-pod traffic within the cluster, use ClusterIP services — they work out of the box within your tenant’s namespaces. See Network model for the intra-tenant allow rule that makes this possible.
ExternalName
Section titled “ExternalName”Blocked by: Capsule Tenant.spec.serviceOptions.allowedServices.externalName: false.
A Service with type: ExternalName is rejected at admission. ExternalName services are a CNAME-style aliasing feature that obscures external dependencies in a way that complicated audit work — the dependency is invisible in the manifest and only surfaces at DNS resolution time.
What to use instead: put the external hostname in a ConfigMap and inject it into the workload through an environment variable, or use a Pod hostAliases entry for the in-pod /etc/hosts override. Both approaches keep the dependency visible in the manifest you own.
apiVersion: v1kind: ConfigMapmetadata: name: external-deps namespace: <your-tenant>-proddata: UPSTREAM_HOST: "api.example.com"---apiVersion: apps/v1kind: Deploymentmetadata: name: my-service namespace: <your-tenant>-prodspec: template: spec: containers: - name: my-service envFrom: - configMapRef: name: external-depsThe pattern: ClusterIP + Ingress
Section titled “The pattern: ClusterIP + Ingress”On Kestrel, the only service type tenants use is ClusterIP — either the default (omit type entirely) or explicitly type: ClusterIP. For workloads that need external access, pair the ClusterIP Service with an Ingress resource. The combination gives you:
- External HTTPS routing via Traefik at the shared edge
- Automatic TLS via cert-manager (no manual certificate management)
- Tenant-scoped hostname collision protection via Capsule
See Ingress on Kestrel for the full Ingress example with TLS, and Known limitations for the complete list of admission-layer restrictions.