Inside Istio's Service Registry
Istio needs the following information to route traffic:
- Available services
- Service hostnames and addresses
- Ports and protocols
- Backend endpoints
- Service visibility for each proxy
This information is stored in the internal service registry.
Kubernetes resources ─┐
├─> Service registry
Istio resources ──────┘
│
▼
Proxy-specific scope
│
▼
xDS configuration
(CDS, EDS, LDS, RDS, ...)
│
▼
Envoy clusters + endpoints
Focus of this post:
- Kubernetes Service and ServiceEntry registry models
- Service visibility and hostname conflicts
ExternalNameversusMESH_EXTERNAL- Registry service → Envoy
ext_authzcluster
Implementation references use Istio 1.28.2. Internal fields and debug endpoints are not stable APIs.
What the Service Registry Contains
- Kubernetes provider
- Sources:
Service,Pod,EndpointSlice - Internal provider name:
Kubernetes
- Sources:
- External provider
- Sources:
ServiceEntry,WorkloadEntry - Internal provider name:
External
- Sources:
External identifies the registry provider:
- It does not automatically mean the service is outside the mesh.
- A ServiceEntry can use:
-
MESH_EXTERNAL -
MESH_INTERNAL
-
The registry provides the source data used to generate:
- CDS
- Envoy clusters
- EDS
- Cluster endpoints
- LDS/RDS
- Listeners and routes that reference those services
References:
Inspecting the Registry
CLI
istioctl x internal-debug registryz > registryz.json
Debug dashboard
istioctl dashboard istiod-debug deployment/istiod -n istio-system
- Open
registryzin the dashboard. - List endpoints supported by the installed version:
istioctl x internal-debug list
Common internal endpoints:
-
registryz -
clusterz -
endpointz -
edsz -
configz -
push_status -
syncz
Security note:
- Internal endpoints can expose:
- Service names
- IP addresses
- Labels
- Control-plane configuration
- Do not expose them publicly.
Implementation:
registryz- Serializes
ServiceDiscovery.Services().
- Serializes
Reading Registry Entries
Kubernetes Service
Shortened example:
{
"Attributes": {
"ServiceRegistry": "Kubernetes",
"Name": "test-server",
"Namespace": "test",
"LabelSelectors": {
"app": "test-server",
},
"Type": "ClusterIP",
"ExternalName": "",
"NodeLocal": false,
"TrafficDistribution": 1,
"PublishNotReadyAddresses": false,
},
"ports": [
{
"name": "http",
"port": 8080,
"protocol": "HTTP",
},
{
"name": "metrics",
"port": 8081,
"protocol": "UnsupportedProtocol",
},
],
"hostname": "test-server.test.svc.cluster.local",
"clusterVIPs": {
"Addresses": {
"Kubernetes": ["192.0.2.10"],
},
},
"defaultAddress": "192.0.2.10",
"Resolution": 0,
"MeshExternal": false,
}
Common fields:
ServiceRegistry- Registry providerName,Namespace- Originating resource identityhostname- Canonical hostname used by Istioports- Ports and protocols understood by IstioclusterVIPs- Per-cluster virtual IPsdefaultAddress- Primary service addressResolution- Endpoint resolution strategyMeshExternal- Whether the model represents an external service
Kubernetes-specific fields:
LabelSelectors- Workload selector from the ServiceClusterExternalAddresses- Per-cluster external addressesClusterExternalPorts- Service-port-to-NodePort mappingsPassthroughTargetPorts- Target-port translation for passthrough servicesNodeLocal-internalTrafficPolicy: LocalTrafficDistribution- Kubernetes traffic-distribution preferencePublishNotReadyAddresses- Whether not-ready endpoints are published
Istio 1.28.2 enum examples:
Resolution: 0-ClientSideLB- Envoy selects an endpoint from its local pool.TrafficDistribution: 1-PreferSameZone
Definitions: pilot/pkg/model/service.go
UnsupportedProtocol:
- Does not mean the port is unusable.
- Istio could not infer a supported application protocol.
- Traffic is generally treated as opaque TCP.
- Use a conventional port name or
appProtocolto make it explicit.
ServiceEntry
Shortened example:
{
"Attributes": {
"ServiceRegistry": "External",
"Name": "api.github.com",
"Namespace": "test",
"ExportTo": {
"test": {},
},
"ObjectName": "github-api",
},
"ports": [
{
"name": "https",
"port": 443,
"protocol": "HTTPS",
},
],
"hostname": "api.github.com",
"clusterVIPs": {
"Addresses": null,
},
"defaultAddress": "0.0.0.0",
"Resolution": 2,
"MeshExternal": true,
}
Important fields:
Namespace- Namespace containing the ServiceEntryObjectName- ServiceEntry resource nameName,hostname- Modeled service identityExportTo- Namespaces allowed to import the service - Default: visible to all namespacesMeshExternal: true- Corresponds tolocation: MESH_EXTERNALResolution: 2-Passthroughin this example - Depends on the ServiceEntry configuration
clusterVIPs.Addresses: null:
- Means no frontend virtual IP was configured.
- Does not mean the service has no endpoints.
- Addresses
- Frontend IPs matched by Envoy
- Endpoints
- Backends selected by Envoy
Visibility and Hostname Conflicts
The registry is global, but each proxy receives a scoped view.
Visibility Controls
exportTo- Service owner controls which namespaces can see the service.Sidecar.spec.egress.hosts- Workload owner controls which visible services a sidecar imports.
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
name: frontend-egress
namespace: frontend
spec:
egress:
- hosts:
- "backend/*"
- "./*"
- "security/*"
outboundTrafficPolicy:
mode: ALLOW_ANY
workloadSelector:
labels:
app: frontend-api
Imported services:
backend/*- Services frombackend./*- Services infrontendsecurity/*- Services fromsecurity
ALLOW_ANY:
- Controls unknown outbound destinations.
- Does not restore registered services omitted by
egress.hosts. - A filter referencing a named cluster still requires that cluster in CDS.
Namespace Selection
When the same hostname exists in multiple imported namespaces, selectServices chooses one namespace.
Priority with unified sidecar scoping:
- Proxy configuration namespace
- Kubernetes provider when all candidates are in other namespaces
- First service encountered when otherwise equivalent
First encountered:
- Means the internal visible-service order.
- Does not mean YAML order.
- Must not be used as a stable ownership rule.
Simplified logic:
for _, svc := range importedServices {
current := namespaceProvider{
Namespace: svc.Attributes.Namespace,
KubernetesService: svc.Attributes.ServiceRegistry == Kubernetes,
}
selected, found := validServices[svc.Hostname]
switch {
case !found:
validServices[svc.Hostname] = current
case current.Namespace == configNamespace:
validServices[svc.Hostname] = current
case !selected.KubernetesService &&
current.KubernetesService &&
selected.Namespace != configNamespace:
validServices[svc.Hostname] = current
}
}
Provider Selection and Port Merging
The selected namespace can still contain multiple models for one hostname.
appendSidecarServices applies:
- Kubernetes service over non-Kubernetes service
- Existing first-added model when otherwise equivalent
- Port merging when the models are compatible
Combined Sidecar Priority
- Choose one namespace per hostname.
- Proxy namespace
- Kubernetes provider for other namespaces
- First namespace encountered when otherwise equivalent
- Resolve providers inside that namespace.
- Kubernetes over
External - First-added model when otherwise equivalent
- Merge compatible ports
- Kubernetes over
The final tie can be influenced by:
Sidecar selection priority is not registry registration priority.
ExternalName or MESH_EXTERNAL?
Kubernetes ExternalName
apiVersion: v1
kind: Service
metadata:
name: github-api
namespace: app
spec:
type: ExternalName
externalName: api.github.com
ports:
- name: https
port: 443
protocol: TCP
- Kubernetes DNS
github-api.app.svc.cluster.localreturnsapi.github.comas its CNAME target.
- Istio registry
- Provider:
Kubernetes - Resolution:
Alias - ClusterIP: none
- Provider:
- Istio model
- Additional frontend name for a concrete service
- Not an independent backend
- Good fit
- DNS-level location transparency
- Keep a Kubernetes-local name for an external implementation
- Important limitation
- The concrete target should already be known to Istio for reliable HTTP/TLS alias routing.
- See ExternalName traffic routing.
ServiceEntry with MESH_EXTERNAL
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
name: github-api
namespace: app
spec:
hosts:
- api.github.com
location: MESH_EXTERNAL
resolution: DNS
ports:
- number: 443
name: https
protocol: TLS
- Explicitly registers
api.github.com. - Creates an independent Istio service/backend.
- Can receive:
- Dedicated Envoy cluster
- VirtualService policy
- DestinationRule policy
| Resource | Primary purpose | Istio model |
|---|---|---|
Kubernetes ExternalName | Kubernetes-local alias | Alias/frontend for a concrete service |
ServiceEntry + MESH_EXTERNAL | Register an external backend | Independent service/backend |
How Services Enter the Registry
Kubernetes Service
- Watch resources.
- Service
- Namespace
- Node
- Pod
- EndpointSlice
c.services = kclient.NewFiltered[*v1.Service](client, filter)
registerHandlers(c, c.services, "Services", c.onServiceEvent, nil)
- Convert the Service.
svcConv := kube.ConvertService(
*curr,
c.opts.DomainSuffix,
c.Cluster(),
c.meshWatcher.Mesh(),
)
- Update the controller cache.
c.servicesMap[currConv.Hostname] = currConv
- Report the service event.
c.opts.XDSUpdater.SvcUpdate(
shard,
string(currConv.Hostname),
namespace,
event,
)
Source:
- Kubernetes registry
controller.go - Kubernetes registry
conversion.go
ServiceEntry
- Register configuration handlers.
configController.RegisterEventHandler(
gvk.ServiceEntry,
s.serviceEntryHandler,
)
configController.RegisterEventHandler(
gvk.WorkloadEntry,
s.workloadEntryHandler,
)
- Convert the ServiceEntry.
convertedServices := convertServices(currentConfig)
- Update service and instance indexes.
s.services.updateServices(key, convertedServices)
s.serviceInstances.updateServiceEntryInstances(
key,
serviceInstancesByConfig,
)
- Report the service event.
s.XdsUpdater.SvcUpdate(
shard,
string(service.Hostname),
service.Attributes.Namespace,
model.EventAdd,
)
Source:
- ServiceEntry registry event registration
- ServiceEntry registry update handler
Aggregating Registries
Example controllers:
- One Kubernetes registry controller per cluster
- One ServiceEntry registry controller
Aggregate behavior:
- Kubernetes services
- Same hostname across clusters → one multicluster model
- Per-cluster VIPs are merged.
- Non-Kubernetes services
- Appended without Kubernetes multicluster hostname merging.
for _, registry := range registries {
services := registry.Services()
if registry.Provider() != Kubernetes {
result = append(result, services...)
continue
}
for _, service := range services {
if firstKubernetesServiceWithHostname(service.Hostname) {
result = append(result, service)
} else {
mergeClusterVIPs(existingService, service)
}
}
}
Source:
- Aggregate
Services() - Aggregate
Run()
Registry Service to ext_authz Cluster
Provider configuration:
meshConfig:
extensionProviders:
- name: external-authz
envoyExtAuthzGrpc:
service: ext-authz.security.svc.cluster.local
port: 8085
An AuthorizationPolicy with action: CUSTOM adds an Envoy ext_authz filter.
Generated cluster reference:
{
"envoy_grpc": {
"cluster_name": "outbound|8085||ext-authz.security.svc.cluster.local",
"authority": "ext-authz.security.svc.cluster.local"
}
}
Key point:
- The filter directly references an Envoy cluster.
- RDS is not used to select the authorization destination.
1. Resolve the Provider
processExtensionProvider- Validates the provider.
- Dispatches gRPC providers to
buildExtAuthzGRPC.
2. Look Up the Service
hostname, cluster, err := model.LookupCluster(
push,
config.Service,
port,
)
- Searches
push.ServiceIndex.HostnameAndNamespace. - Supports:
<hostname>
<namespace>/<hostname>
- Namespace omitted
- Hostname must resolve to exactly one namespace.
- Same hostname in multiple namespaces
- Use the namespace-qualified form.
Cluster key:
cluster = BuildSubsetKey(
TrafficDirectionOutbound,
"", // no subset
service.Hostname,
port,
)
Result:
outbound|8085||ext-authz.security.svc.cluster.local
3. Configure the Filter
generateGRPCConfig- Writes the cluster name toenvoy_grpc.cluster_name. - Uses the service hostname as the gRPC authority.- Envoy
EnvoyGrpc- Sends the authorization request through the named cluster.
4. Ensure the Cluster Exists
- Service lookup alone is insufficient.
- The proxy must also receive the outbound cluster through CDS.
- Default sidecar
- Imports all visible services.
- Restrictive Sidecar
egress.hostsmust import the authorization service.
- Gateway
- Services referenced by
MeshConfig.ExtensionProvidersare treated as required services. - See
extraServicesForProxy.
- Services referenced by
Verification:
# Filter references the expected cluster.
istioctl proxy-config listener <pod> -n <namespace> -o json
# CDS contains the outbound cluster.
istioctl proxy-config cluster <pod> -n <namespace> \
--fqdn ext-authz.security.svc.cluster.local
# EDS contains healthy endpoints.
istioctl proxy-config endpoint <pod> -n <namespace> \
--cluster 'outbound|8085||ext-authz.security.svc.cluster.local'
Does ext_authz Go Through the Mesh?
Yes, in the configuration and data-plane sense:
- Uses an Istio-generated outbound cluster.
- Uses registry endpoints.
- Can apply:
- Endpoint health
- Load balancing
- Locality behavior
- Connection-pool settings
- TLS settings
- DestinationRule policy
It does not use the application outbound path:
Application request
└─> iptables
└─> outbound listener
└─> route
└─> upstream cluster
The authorization path is:
Inbound request
└─> Envoy ext_authz filter
└─> CheckRequest
└─> named outbound cluster
└─> authorization-service endpoint
Important differences:
- Envoy creates the authorization call internally.
- The call is not reinjected through iptables.
- RDS is not required to choose the destination.
- If the authorization workload has a sidecar:
- The connection still reaches its inbound data plane.
- Normal mTLS and inbound policy apply.
Envoy sends a CheckRequest, which can include:
- Peer identities and addresses
- Destination information
- HTTP method and path
- Selected headers
- Metadata
- Optional request-body bytes
It is not necessarily the complete original request.
Locality and Traffic Policy
The provider uses normal Istio cluster-building behavior.
Possible endpoint-selection inputs:
- Registry endpoint locality
- Kubernetes traffic-distribution hints
- Mesh or DestinationRule
localityLbSetting - Outlier detection and failover
- Subset policy
- TLS policy
There is no separate ext_authz locality algorithm:
- The filter selects a cluster.
- The cluster selects an endpoint.
Best debugging targets:
- Generated cluster
- Cluster endpoints
- Applicable traffic policy
Debugging Checklist
- Registry
- Run
istioctl x internal-debug registryz. - Confirm hostname, namespace, port, provider, and resolution.
- Run
- Hostname ambiguity
- Use
<namespace>/<hostname>when multiple namespaces contain the hostname.
- Use
- Visibility
- Confirm
exportTo.
- Confirm
- Sidecar scope
- Confirm
Sidecar.egress.hosts.
- Confirm
- CDS
- Confirm
outbound|<port>||<hostname>exists.
- Confirm
- EDS
- Confirm healthy endpoints and expected locality.
- Filter
- Confirm
envoy_grpc.cluster_namematches the CDS cluster.
- Confirm
- Policy and workload
- Check DestinationRule, mTLS, readiness, and fail-open/fail-closed behavior.
Takeaways
- Service registry
- Bridge between platform discovery and Envoy configuration
- Kubernetes and ServiceEntry controllers
- Build separate service models
- Aggregate controller
- Combines registry views
- Proxy scoping
- Decides which services each proxy receives
- Hostname conflict priority
- Proxy namespace
- Kubernetes provider
- First model encountered when otherwise equivalent
ExternalName- Primarily an aliasServiceEntry- Explicit Istio service/backendenvoyExtAuthzGrpc- Resolves through the Service Index - Usesoutbound|port||hostname- Calls the named cluster directly from the filter - Uses normal endpoints, locality, and traffic policy