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.
“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 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
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
| Setting | Who can request | Intended 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
| Resource | Limit |
|---|---|
| Private link services per subscription | 800 |
| Private link services per Standard Load Balancer | 8 |
| NAT IP configurations per private link service | 8 |
| Private endpoints on the same private link service | 1000 |
| Subscriptions in the visibility setting | 100 |
| Subscriptions in the auto-approval setting | 100 |
| Idle timeout | 300 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:
“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 service | Backend parses the header? | Result |
|---|---|---|
| Enabled | No | “The request will fail” |
| Disabled | Yes — 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:
“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
| Decision | Choose | Because |
|---|---|---|
| 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.
#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
- What is Azure Private Link service? — the Standard Load Balancer requirement and the NAT IP subnet recommendation, the alias composition and its immutable prefix and region suffix, the three visibility options and the auto-approval subset rule with the manual-approval path, the connection states and per-endpoint provider control, destination-side NAT and the NAT IP appearing as source, the eight-NAT-IP ceiling and the enforced last-address floor, TCP Proxy v2 with its custom TLV and LinkID, the failure cases in both directions, the health-probe inclusion and the all-load-balancers activation scope, the read-only network interface, the same-region requirement, reach across virtual networks, subscriptions and tenants, the NIC-versus-IP backend pool restriction, IPv4 and TCP/UDP only, the 300-second idle timeout, the inbound NAT rule requirement, and the Direct Connect preview
- Azure subscription and service limits, networking section — private link services per subscription and per Standard Load Balancer, IP configurations per private link service, private endpoints on the same private link service, and subscriptions allowed in the visibility and auto-approval settings
- Private Link Service Direct Connect — the preview that removes the load balancer requirement, with its known limitations (no claims drawn from it)
- What is a private endpoint? — the consumer side of this connection, covered in #91 (no claims drawn from it)
- Azure Load Balancer overview — the Standard Load Balancer this service is built on (no claims drawn from it)
- The PROXY protocol specification — the header format a provider's backend must parse (no claims drawn from it)
Comments