Skip to content

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.

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.

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.

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: v1
kind: ConfigMap
metadata:
name: external-deps
namespace: <your-tenant>-prod
data:
UPSTREAM_HOST: "api.example.com"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
namespace: <your-tenant>-prod
spec:
template:
spec:
containers:
- name: my-service
envFrom:
- configMapRef:
name: external-deps

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.