Homeβ€Ί Blogβ€Ί Azure Architecture Series #92 β€” Private Link Service and Consumer Approval…
Azure Architecture Azure Architecture Series

Azure Architecture Series #92 β€” Private Link Service and Consumer Approval

Verified against current vendor documentation on 13 November 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

#91 was the consumer's side: a network interface in your subnet that reaches somebody else's service. This is the other end — what you build when the service being reached is yours.

It is your own service, behind a load balancer

“Azure Private Link service is the reference to your own service… Your service that is running behind Azure Standard Load Balancer can be enabled for Private Link access so that consumers to your service can access it privately from their own VNets.”

Which makes it the first thing in this sequence that is a product decision rather than a networking one. A Private Link service is how a software vendor sells into customers' virtual networks without exposing anything publicly, and the two properties that govern it — visibility and autoApproval — are a commercial access model expressed as two arrays of subscription IDs.

They are easy to conflate, and the documentation separates them in one sentence worth memorising:

Visibility is exposure; auto-approval is approval

“Visibility controls the exposure settings whereas auto-approval controls the approval settings for your service… Auto-approval is a subset of the visibility array.”

One decides whether a consumer can request at all. The other decides whether that request lands in your inbox or connects itself. Getting the second one wrong is how a service that was meant to require a conversation starts accepting connections quietly.

Architecture

Diagram: Azure Private Link service and consumer approval across five bands. The first band establishes what the service is: a reference to your own service running behind an Azure Standard Load Balancer, enabled so consumers can reach it privately from their own virtual networks. Only Standard Load Balancer is supported, not Basic, and only where the backend pool is configured by network interface rather than by IP address. The service must sit in the same region as the virtual network and the load balancer, and a read-only network interface is created for its lifecycle that the customer cannot manage. Azure generates a globally unique alias in three parts: a prefix you choose, a platform-supplied GUID, and a suffix of region dot azure dot privatelinkservice. The prefix cannot be changed after creation. The second band covers the two access controls, which are separate and easily conflated. Visibility controls exposure and has three options from most to least restrictive: role-based access control only, which permits cross-tenant visibility through role assignments; restricted by subscription, for consumption across tenants by a trusted list; and anyone with your alias, which is public to anyone holding the string. Auto-approval controls approval and is explicitly a subset of the visibility array. A connection request from a subscription in the auto-approval list is approved automatically with no provider action. A request from a subscription in the visibility array but not the auto-approval array arrives in Pending state and must be approved by hand. Only approved connections can send traffic. Both lists cap at one hundred subscriptions. The third band covers address translation and what the provider can see. Private Link service performs destination-side network address translation so there is no address conflict between the consumer's space and the provider's, which means the provider's application sees the NAT IP as the source for every packet and never the consumer's real address. Up to eight NAT IP addresses may be assigned per service, at least eight are recommended to be free in the subnet, each adds ports for TCP connections and thus scale, and the last remaining NAT IP cannot be deleted. The escape hatch is TCP Proxy v2, which carries both the real source address and the private endpoint LinkID in a custom type-length-value vector: type PP2_TYPE_AZURE 0xEE in one octet, a two octet length, a one octet subtype 0x01, and a four octet little-endian UINT32 LinkID that matches the linkIdentifier property on the private endpoint connection. Source IP and LinkID together uniquely identify a consumer. The fourth band covers the sharp edges of that escape hatch. Enabling proxy protocol is strictly all-or-nothing in both directions: the request fails if the setting is on and the backend cannot parse the header, and equally fails if the backend expects the header and the setting is off. Once enabled, the header is also added to HTTP and TCP health probes sent to backend virtual machines, and it carries no client information. The setting activates for all load balancers and their backend machines, so enabling it on one Private Link service without enabling it on others that share the same load balancer or backend pool causes health probes to fail. The fifth band covers limits and constraints. Eight hundred private link services per subscription and eight per Standard Load Balancer, which is a hundred fully loaded load balancers; eight NAT IP configurations per service; a thousand private endpoints on one service, so eight thousand across one load balancer's eight services; and a hundred subscriptions in each of the visibility and auto-approval lists. The service supports IPv4 only and TCP and UDP only, and carries an idle timeout of about five minutes, three hundred seconds, which applications must beat with TCP keepalives. An inbound NAT rule of backend pool type needs a load balancing rule configured alongside it. Multiple services may share one load balancer using different frontend IP configurations, a single service may be reached from private endpoints in different virtual networks, subscriptions and Entra tenants, and before the service can be deleted every private endpoint connection on it must be gone. A Direct Connect preview removes the load balancer requirement entirely.
Two lists, one translation nobody can see through, and a protocol that is all-or-nothing in both directions.

Visibility and auto-approval: what each decides

Consumer's subscription is…Can they request?What happens
Outside the visibility setting No No exposure — the service is not reachable to request
In the visibility array, not in auto-approval Yes Connection created Pending — “The service provider must manually approve”
In the auto-approval array Yes “the connection is automatically approved… Service providers don't need to manually approve”

The three visibility settings

SettingWho can requestIntended for
Role-based access control only (most restrictive) Those with RBAC permissions — and “Cross tenant visibility is permitted through role-based access control” “private consumption from different virtual networks that you own”
Restricted by subscription A named list — up to 100 subscriptions “consumed across different tenants… a limited set of subscriptions that you trust”
Anyone with your alias (least restrictive) Anyone holding the alias string “make your service public”

Limits

ResourceLimit
Private link services per subscription800
Private link services per Standard Load Balancer8
NAT IP configurations per private link service8
Private endpoints on the same private link service1000
Subscriptions in the visibility setting100
Subscriptions in the auto-approval setting100
Idle timeout300 seconds (~5 minutes)

Composed: a subscription's 800 services is 100 fully loaded load balancers, and one load balancer's eight services can back 8000 private endpoint connections.

Why This Architecture Holds Up

Because auto-approval is a subset, and that is the whole safety property

The two arrays are not independent. “Auto-approval is a subset of the visibility array” — so a subscription cannot be auto-approved without also being visible, and the visibility list is the outer boundary of who can reach you at all.

That ordering is what makes the design safe, and it also means the two settings fail differently. Widen visibility and you get connection requests — noise, but each one still needs a decision. Widen auto-approval and you get connections, with no decision and no notification. The failure is silent in exactly one direction.

The sharpest version of this is the combination the portal makes easy: visibility set to “Anyone with your alias” alongside a populated auto-approval list. That is a defensible configuration — a public service that pre-approves known customers — but the alias is “a globally unique read-only string” shared “offline”, which is to say it is a bearer token with no expiry. Anyone who has ever been sent it can request a connection forever.

And the names reinforce the confusion rather than resolving it. “Role-based access control only” is the most restrictive visibility setting, yet it is the one that “permits” cross-tenant access — because RBAC, not a subscription list, is doing the gating. A reader scanning for “which option keeps this inside my tenant” can pick the wrong one from the label.

Because the provider cannot see who is calling

This is the architectural consequence that shapes everything a provider can build:

Every consumer arrives as one of your own addresses

“Private Link service performs destination side NAT-ing… This NAT ensures that there's no IP conflict between source (consumer side) and destination (service provider) address space. On the destination or service provider side, the NAT IP address displays as source IP for all packets received by your service.”

So a provider's application sees traffic from a handful of its own private addresses, with no way to tell one customer from another. Every per-tenant control that would normally key on source address — rate limiting, IP allow lists, audit logging, abuse attribution — has nothing to key on.

This is the same SNAT trade this series keeps meeting, and it is worth noting how differently it lands each time. #79 found Azure Firewall's SNAT hiding the source the perimeter existed to read — damage. #83 found SNAT mandatory to make return traffic symmetric — a prerequisite. Here it is neither: it is a deliberate isolation guarantee, because without it two customers with overlapping RFC 1918 ranges could not both connect. The translation is the feature.

The documented way back is TCP Proxy v2, and what it carries is more than the address: “In addition to source IP address, proxy protocol header also carries the LinkID of the private endpoint. Combination of source IP address and LinkID can help service providers uniquely identify their consumers.”

That is the mechanism a multi-tenant provider actually needs, and it is worth being precise that it is the LinkID doing the work. The source address alone is a NAT IP shared by everyone; the LinkID is per-private-endpoint and matches linkIdentifier on the connection object, so it joins back to a specific approved consumer. A provider that enables proxy protocol and then reads only the source IP has gained nothing.

Because proxy protocol is all-or-nothing in both directions

Enabling it is not a safe default you can switch on and tune later. Both mismatches fail:

Setting on the Private Link serviceBackend parses the header?Result
EnabledNo“The request will fail”
DisabledYes — expects it“The request will fail”

There is no degraded mode and no negotiation. The two sides must be changed together, which for a service already carrying traffic means a coordinated cutover rather than a toggle.

Two consequences make that cutover wider than it looks. The first reaches the health checks:

“Once proxy protocol setting is enabled, proxy protocol header will also be included in HTTP/TCP health probes from host to the backend virtual machines. Client information isn't contained in the header.”

So the backend must parse a header on probe traffic too — probes that carry no client information, so the parser has to tolerate a header with nothing useful in it. #83 established what happens when a health probe response is wrong: the instance is removed from service while being perfectly healthy. This is a way to cause that across an entire backend pool with one property change.

The second is that the setting is not scoped where its name suggests:

The setting is per-service; the effect is per-load-balancer

“TCP Proxy v2 configuration on a Private Link service activates for all load balancers and their backend VMs. If TCP Proxy v2 is configured on one PLS, configure it on other PLS resources if they are sharing the same load balancer or backend pool, otherwise health probes will fail.”

Up to 8 Private Link services can share one Standard Load Balancer. Enabling proxy protocol on one of them changes the behaviour of all eight, and the failure surfaces on the seven nobody touched. That is a blast radius that does not follow the resource boundary, and the documentation flags it twice — once as a note near the top and once in the limitations list — which is usually how often something has to be reported before it gets written down.

Because the NAT pool is the scaling unit, and it has a floor as well as a ceiling

The NAT addresses are not just for address-conflict avoidance; they are the throughput budget: “service providers can assign up to eight NAT IP addresses per Private Link service. With each NAT IP address, you can assign more ports for your TCP connections and thus scale out.”

Which makes the recommendation at creation time a capacity decision disguised as a subnet-sizing one: “It's recommended to have at least eight NAT IP addresses available in the subnet.” Provision the subnet for one and you cannot scale the service later without moving it.

There is a floor too, and it is unusual in being enforced rather than advised: “you must maintain at least one NAT IP address once configured. You will be restricted from deleting the last remaining NAT IP to ensure that active connections aren't impacted.” Azure refuses the operation rather than letting a provider cut off its own customers — which is the right behaviour and worth knowing before a cleanup script discovers it.

Because the load balancer requirements exclude a common configuration

Standard Load Balancer only, which is expected. The second requirement is not:

“Supported only on Standard Load Balancer where backend pool is configured by NIC. Not supported on Standard Load Balancer where backend pool is configured by IP address.”

Backend pools by IP address are the normal choice for several architectures — backends that are not Azure NICs, or pools managed as addresses rather than resources. A load balancer built that way cannot carry a Private Link service, and the discovery comes at the point of creating the service rather than when the pool was designed.

Three smaller constraints belong with it. “Supports IPv4 traffic only” — the fourth IPv6 gap this series has recorded, after Azure Firewall's DNAT rules, its virtual-hub deployment and the DNS Private Resolver's subnets. “Supports TCP and UDP traffic only.” And an idle timeout that is short enough to matter: 300 seconds, with the explicit instruction that “applications connecting through Private Link Service must use TCP Keepalives lower than that time.” A five-minute idle cut is well inside the range where a connection-pooling client will hold a socket open and assume it works.

Because the alias is permanent and carries your region

The alias is how consumers find you, and two of its properties are fixed at creation. “Prefix is the service name. You can pick your own prefix. After ‘Alias’ is created, you can't change it, so select your prefix appropriately.”

And the suffix is “appended by Azure: region.azure.privatelinkservice”, so the full alias names the region the service lives in. A consumer reading an alias learns where the provider deployed. For a provider that would rather not advertise its regional footprint, that is a disclosure with no setting to turn it off — and combined with immutability, a rename or a region change means a new alias and every consumer reconnecting.

The deletion rule has the same shape: “before you delete the service, ensure that there are no private endpoint connections associated with it. You can reject all connections and delete the service.” The service cannot be removed while anyone is attached, so decommissioning is an explicit act of disconnecting customers rather than a deletion that cascades.

Because reach is wide and the tenant boundary is not a barrier

It is worth being clear how far an approved connection reaches, because it is further than a virtual network: “A single Private Link Service can be accessed from multiple Private Endpoints belonging to different virtual networks, subscriptions and/or Microsoft Entra tenants”, and each of those endpoints is reachable “from the same virtual network and regionally peered virtual networks… globally peered virtual networks and on premises using private VPN or ExpressRoute.”

So one approval can expose the service to a consumer's entire connected estate, including their on-premises networks. The approval is per private endpoint, not per machine or per network — and #89's point about links applies here in mirror image: the unit you approve is coarser than the unit you might want to control.

The provider does keep per-connection control after the fact: “Multiple private endpoints can connect to the same Private Link service and the service provider can control the state for individual private endpoints”, with approve, reject and delete all available. Revocation is per endpoint and immediate in effect, since “only connections that are approved can send traffic”.

One preview changes the shape of all of this and is worth knowing exists: “Private Link Service Direct Connect, which allows you to connect to any privately routable destination IP address” — “connecting to a privately routable destination IP address without requiring a load balancer.” The Standard Load Balancer requirement, which is the first thing this whole design asks for, becomes optional.

Key Architecture Decisions

DecisionChooseBecause
Who may connect without you noticing Keep auto-approval small and deliberate Widening visibility produces requests you still decide on. Widening auto-approval produces connections with no decision and no notification.
Visibility for a service that must stay in-tenant Read the options, not the labels “Role-based access control only” is the most restrictive setting and is also the one that permits cross-tenant access, via RBAC.
Treating the alias as a secret Do not With “Anyone with your alias” visibility it is a bearer token with no expiry, shared offline, and anyone ever sent it can request forever.
Per-tenant rate limiting, allow lists or audit Key on LinkID, not source IP Destination-side NAT means every consumer arrives as one of your own NAT addresses. Only the LinkID distinguishes them, and it matches linkIdentifier on the connection.
Enabling TCP Proxy v2 A coordinated cutover, not a toggle Requests fail if the setting is on and the backend cannot parse, and equally if the backend expects a header the setting is not sending.
Proxy protocol with services sharing a load balancer Enable on all of them together The setting “activates for all load balancers and their backend VMs”, so one service's change fails health probes on the up-to-eight sharing that balancer.
Backend health probes under proxy protocol Parse a header with no client data Probes carry the header too, and “client information isn't contained” in it. A parser that assumes content will fail the whole pool.
Sizing the NAT subnet Leave room for eight from the start NAT addresses are the port and scale budget, capped at eight, and the recommendation is eight available at creation. Retrofitting means moving the service.
Load balancer backend pool type By NIC Backend pools configured by IP address are explicitly unsupported, and the discovery comes when creating the service rather than when designing the pool.
Client connection handling TCP keepalives under 300 seconds The idle timeout is ~5 minutes, and a pooling client will otherwise hold a socket it believes is live.
Naming the service Choose the prefix once The alias cannot be changed after creation, and its Azure-appended suffix discloses the region to every consumer.
Decommissioning Reject every connection first The service cannot be deleted while private endpoint connections exist, so retirement is an explicit disconnection of customers.
Scale planning 8 services per load balancer is the binding number 800 per subscription is 100 fully loaded balancers; each service takes 1000 endpoints, so one balancer backs 8000 connections.
If IPv6 or a protocol other than TCP/UDP is required Private Link service is not the mechanism IPv4 only, TCP and UDP only β€” stated plainly in the limitations.

Closing Thought

The consumer side of Private Link is a networking feature. The provider side is a product one. What visibility and autoApproval encode is a go-to-market model — private to my own tenants, restricted to named customers, or open to anyone holding a string — and the technical settings are thin enough that the commercial decision is the whole of it.

The property to be careful with is the one whose failure is quiet. Visibility that is too broad produces requests, and a request is a prompt to think. Auto-approval that is too broad produces connections, and a connection asks nothing of anyone. Those two lists sit next to each other in the same resource, with the second defined as a subset of the first, and only one of them has a human in the loop by construction.

The thing I would design around from the start, though, is that the provider never sees who is calling. Destination-side NAT is not a limitation to work around — it is what lets two customers with the same private address range both connect, which is the entire reason this works as a multi-tenant product. But it means every per-customer control a SaaS normally builds on source address has nothing underneath it. The LinkID in the proxy protocol header is the only durable identity on the wire, and getting it requires a protocol change that must land on both sides at once and that reaches further than the resource it is set on. That is worth deciding before the first customer connects, not after the hundredth.

Next in this series

#93 steps back to compare: service endpoints versus private endpoints — two mechanisms with similar names that solve different problems, and the one that does not give you a private address at all.

Official Azure Reference

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent