Published

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
  • ExternalName versus MESH_EXTERNAL
  • Registry service → Envoy ext_authz cluster

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
  • External provider
    • Sources: ServiceEntry, WorkloadEntry
    • Internal provider name: External

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 registryz in 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().

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 provider
  • Name, Namespace- Originating resource identity
  • hostname- Canonical hostname used by Istio
  • ports- Ports and protocols understood by Istio
  • clusterVIPs- Per-cluster virtual IPs
  • defaultAddress- Primary service address
  • Resolution- Endpoint resolution strategy
  • MeshExternal- Whether the model represents an external service

Kubernetes-specific fields:

  • LabelSelectors- Workload selector from the Service
  • ClusterExternalAddresses- Per-cluster external addresses
  • ClusterExternalPorts- Service-port-to-NodePort mappings
  • PassthroughTargetPorts- Target-port translation for passthrough services
  • NodeLocal- internalTrafficPolicy: Local
  • TrafficDistribution- Kubernetes traffic-distribution preference
  • PublishNotReadyAddresses- 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 appProtocol to 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 ServiceEntry
  • ObjectName- ServiceEntry resource name
  • Name, hostname- Modeled service identity
  • ExportTo- Namespaces allowed to import the service - Default: visible to all namespaces
  • MeshExternal: true- Corresponds to location: MESH_EXTERNAL
  • Resolution: 2- Passthrough in 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 from backend
  • ./*- Services in frontend
  • security/*- Services from security

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:

  1. Proxy configuration namespace
  2. Kubernetes provider when all candidates are in other namespaces
  3. 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:

  1. Kubernetes service over non-Kubernetes service
  2. Existing first-added model when otherwise equivalent
  3. Port merging when the models are compatible

Combined Sidecar Priority

  1. Choose one namespace per hostname.
    1. Proxy namespace
    2. Kubernetes provider for other namespaces
    3. First namespace encountered when otherwise equivalent
  2. Resolve providers inside that namespace.
    1. Kubernetes over External
    2. First-added model when otherwise equivalent
    3. Merge compatible ports

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.local returns api.github.com as its CNAME target.
  • Istio registry
    • Provider: Kubernetes
    • Resolution: Alias
    • ClusterIP: none
  • 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

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
ResourcePrimary purposeIstio model
Kubernetes ExternalNameKubernetes-local aliasAlias/frontend for a concrete service
ServiceEntry + MESH_EXTERNALRegister an external backendIndependent service/backend

How Services Enter the Registry

Kubernetes Service

  1. Watch resources.
    • Service
    • Namespace
    • Node
    • Pod
    • EndpointSlice
c.services = kclient.NewFiltered[*v1.Service](client, filter)
registerHandlers(c, c.services, "Services", c.onServiceEvent, nil)
  1. Convert the Service.
svcConv := kube.ConvertService(
    *curr,
    c.opts.DomainSuffix,
    c.Cluster(),
    c.meshWatcher.Mesh(),
)
  1. Update the controller cache.
c.servicesMap[currConv.Hostname] = currConv
  1. Report the service event.
c.opts.XDSUpdater.SvcUpdate(
    shard,
    string(currConv.Hostname),
    namespace,
    event,
)

Source:

ServiceEntry

  1. Register configuration handlers.
configController.RegisterEventHandler(
    gvk.ServiceEntry,
    s.serviceEntryHandler,
)
configController.RegisterEventHandler(
    gvk.WorkloadEntry,
    s.workloadEntryHandler,
)
  1. Convert the ServiceEntry.
convertedServices := convertServices(currentConfig)
  1. Update service and instance indexes.
s.services.updateServices(key, convertedServices)
s.serviceInstances.updateServiceEntryInstances(
    key,
    serviceInstancesByConfig,
)
  1. Report the service event.
s.XdsUpdater.SvcUpdate(
    shard,
    string(service.Hostname),
    service.Attributes.Namespace,
    model.EventAdd,
)

Source:

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:

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

2. Look Up the Service

hostname, cluster, err := model.LookupCluster(
    push,
    config.Service,
    port,
)

LookupCluster:

  • 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 to envoy_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.hosts must import the authorization service.
  • Gateway
    • Services referenced by MeshConfig.ExtensionProviders are treated as required services.
    • See extraServicesForProxy.

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

  1. Registry
    • Run istioctl x internal-debug registryz.
    • Confirm hostname, namespace, port, provider, and resolution.
  2. Hostname ambiguity
    • Use <namespace>/<hostname> when multiple namespaces contain the hostname.
  3. Visibility
    • Confirm exportTo.
  4. Sidecar scope
    • Confirm Sidecar.egress.hosts.
  5. CDS
    • Confirm outbound|<port>||<hostname> exists.
  6. EDS
    • Confirm healthy endpoints and expected locality.
  7. Filter
    • Confirm envoy_grpc.cluster_name matches the CDS cluster.
  8. 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
    1. Proxy namespace
    2. Kubernetes provider
    3. First model encountered when otherwise equivalent
  • ExternalName- Primarily an alias
  • ServiceEntry- Explicit Istio service/backend
  • envoyExtAuthzGrpc- Resolves through the Service Index - Uses outbound|port||hostname- Calls the named cluster directly from the filter - Uses normal endpoints, locality, and traffic policy

References