|

Private endpoint storage Bicep: el DNS que rompe todo

En pocas palabras: El error 403 en un private endpoint de Azure Storage con Bicep casi siempre es DNS, no networking: sin la zona privatelink.blob linkeada a la VNet y el DNS zone group en el endpoint, el cliente resuelve la IP pública y Azure rechaza la conexión.

Un private endpoint storage Bicep que devuelve 403 casi nunca tiene un problema de red: el cliente resuelve la IP pública de la cuenta de Storage en vez de la privada, y con publicNetworkAccess en Disabled, Azure rechaza la conexión directo. Así lo repasa un posteo técnico publicado el 7 de noviembre de 2026 en Dev.to, que desarma el problema pieza por pieza.

Un private endpoint storage Bicep es el conjunto de recursos de Azure (el private endpoint, una Private DNS Zone, el link a la VNet y el DNS zone group) que se despliegan como código con Bicep para que una cuenta de Azure Storage con acceso público deshabilitado sea alcanzable solo desde una red privada. El patrón resuelve un problema de nombres: sin la zona privatelink correcta linkeada, el cliente nunca encuentra la IP privada del endpoint.

En 30 segundos

  • El error 403 en un private endpoint de Storage casi siempre es DNS, no networking: el cliente resuelve la IP pública en vez de la privada.
  • El patrón Bicep necesita cinco piezas: storage account bloqueada, Private DNS Zone, VNet link, private endpoint y DNS zone group.
  • Cada sub-recurso (blob, file, queue, table, dfs) tiene su propia zona privatelink: si usás varios, repetís los pasos 2 a 5 para cada uno.
  • En redes on-premises hay que forwardear la zona pública blob.core.windows.net, no la privatelink, hacia Azure DNS Private Resolver.
  • En landing zones grandes, Microsoft recomienda centralizar las zonas y automatizar el DNS zone group con políticas DeployIfNotExists.

¿Por qué un private endpoint de Storage devuelve 403 aunque la red esté bien configurada?

El 403 aparece porque Azure, al crear el private endpoint, convierte el registro público de la cuenta en un CNAME hacia privatelink.blob.core.windows.net. Si el cliente resuelve esa zona desde una Private DNS Zone, recibe la IP privada del endpoint y todo funciona. Si no puede, la consulta cae al DNS público, el cliente recibe la IP pública de la cuenta, y como el acceso público está deshabilitado, la request se rechaza.

Ponele que apagás el acceso público en tu cuenta de Storage, agregás el private endpoint, hacés el deploy, y tu app te devuelve 403 “This request is not authorized to perform this operation” (sí, en serio, ese mensaje tan poco útil). La reacción típica es revisar NSGs, firewalls, reglas de red.

Mala idea. La red está perfecta. El DNS, no.

El tema es que ese fallback a DNS público es silencioso: nadie te avisa que la resolución fue a parar afuera de tu Private DNS Zone. El nslookup desde tu laptop (fuera de la VNet) va a devolver la IP pública igual, y eso confunde más todavía. Relacionado: los errores más comunes al armar redes hub and spoke.

Las cinco piezas necesarias para que el private endpoint funcione

El patrón que funciona tiene cinco piezas: una storage account con acceso público deshabilitado, una Private DNS Zone privatelink.<sufijo>, un link de esa zona a la VNet, el private endpoint apuntando al sub-recurso correspondiente y un DNS zone group en el endpoint para que Azure cree y mantenga el registro A sin que lo toques a mano.

  • Storage account bloqueada. publicNetworkAccess en Disabled, allowBlobPublicAccess en false y allowSharedKeyAccess en false, para forzar autenticación con Entra ID y RBAC.
  • Private DNS Zone del sub-recurso. Se nombra privatelink.blob.<sufijo>, usando environment().suffixes.storage en vez de hardcodear core.windows.net.
  • VNet link. La zona no hace nada hasta que la linkeás a la VNet desde la que resuelven tus clientes.
  • El private endpoint. Apunta al storage account con groupIds: [‘blob’] (o el sub-recurso que corresponda).
  • DNS zone group. Es el paso que casi todos los ejemplos se saltean. Sin él, terminás creando registros A a mano, y quedan desactualizados en silencio si la IP del endpoint cambia.

El Bicep para esto son cinco recursos encadenados: storageAccount, privateDnsZones, virtualNetworkLinks, privateEndpoints y privateDnsZoneGroups, en ese orden, porque cada uno depende del anterior.

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
 name: storageName
 location: location
 kind: 'StorageV2'
 sku: { name: 'Standard_LRS' }
 properties: {
 minimumTlsVersion: 'TLS1_2'
 supportsHttpsTrafficOnly: true
 allowBlobPublicAccess: false
 allowSharedKeyAccess: false
 publicNetworkAccess: 'Disabled'
 networkAcls: { defaultAction: 'Deny', bypass: 'AzureServices' }
 }
}
resource blobDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
 name: 'privatelink.blob.${environment().suffixes.storage}'
 location: 'global'
}

resource blobDnsLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
 parent: blobDnsZone
 name: '${vnet.name}-link'
 location: 'global'
 properties: {
 registrationEnabled: false
 virtualNetwork: { id: vnet.id }
 }
}
resource blobPe 'Microsoft.Network/privateEndpoints@2023-05-01' = {
 name: 'pe-${storageName}-blob'
 location: location
 properties: {
 subnet: { id: privateEndpointSubnetId }
 privateLinkServiceConnections: [{
 name: 'blob'
 properties: { privateLinkServiceId: storage.id, groupIds: ['blob'] }
 }]
 }
}

resource blobPeDns 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = {
 parent: blobPe
 name: 'default'
 properties: {
 privateDnsZoneConfigs: [{ name: 'blob', properties: { privateDnsZoneId: blobDnsZone.id } }]
 }
}

publicNetworkAccess: ‘Disabled’ pesa más que cualquier regla de firewall: nada fuera de tu red se conecta, sin importar lo que digan las networkAcls. Apagar allowSharedKeyAccess obliga a todos a pasar por Entra ID y RBAC, que queda logueado y es revocable.

¿Qué zona DNS privada corresponde a cada sub-recurso de Storage (blob, file, queue, table, dfs)?

Cada sub-recurso de Storage usa su propia zona privatelink: blob, file, queue, table y dfs (Data Lake Gen2) no comparten DNS entre sí. Si tu app usa más de uno, repetís los pasos de zona, link, endpoint y zone group para cada sub-recurso que necesites. Sobre eso hablamos en cómo conectarte por SSH a tus VMs.

Sub-recurso (groupIds)Private DNS Zone
blobprivatelink.blob.core.windows.net
fileprivatelink.file.core.windows.net
queueprivatelink.queue.core.windows.net
tableprivatelink.table.core.windows.net
dfs (Data Lake Gen2)privatelink.dfs.core.windows.net
private endpoint storage bicep diagrama explicativo

¿Cómo se resuelve el DNS de private endpoints desde servidores on-premises?

Con un conditional forwarder sobre la zona pública (blob.core.windows.net), no sobre la privatelink, apuntando a algo dentro de Azure que pueda ver la zona privada. Si solo forwardeás privatelink.blob.core.windows.net, un resolver recursivo upstream puede responder el nombre público antes de que tu servidor on-prem llegue a forwardear el CNAME.

La opción prolija es Azure DNS Private Resolver con un inbound endpoint. Si ya tenés domain controllers o servidores DNS corriendo en Azure, también pueden hacer el trabajo y te ahorran el costo del resolver. Eso sí: el resolver solo responde desde zonas linkeadas a la VNet donde vive, así que en un diseño hub-and-spoke normalmente terminás linkeando cada zona privatelink al hub, tal como lo plantea el Cloud Adoption Framework de Microsoft.

¿Qué pasa si parte de tus clientes todavía pega contra storage accounts sin private endpoint? Ahí sirve resolutionPolicy: ‘NxDomainRedirect’ en el VNet link, para que esos nombres falten en la zona privada y resuelvan por internet como siempre. Ojo: esa propiedad pide una versión de API reciente para virtualNetworkLinks, como 2024-06-01.

Cómo escalar esta configuración en un landing zone con Azure Policy

A escala, el equipo de plataforma crea cada zona privatelink.* una sola vez en una suscripción central de conectividad, la linkea al hub, y usa políticas DeployIfNotExists para que cada private endpoint nuevo agregue su privateDnsZoneGroup sin intervención manual. Los equipos de aplicación despliegan solo el endpoint y nunca tocan DNS.

El Cloud Adoption Framework detalla tres piezas de Azure Policy para esto: una que deniega crear storage accounts si networkAcls.defaultAction no está en Deny, otra que impide crear zonas con el prefijo privatelink fuera de la suscripción central, y una DeployIfNotExists que arma el zone group por vos. Si corrés un landing zone con varias subscripciones, es el enfoque a usar en vez de que cada equipo gestione su propio DNS a mano. Complementá con qué certificación de Azure conviene según tu rol.

¿Patrón simple o landing zone? Un ejemplo hipotético para decidir

Ejemplo hipotético (a modo ilustrativo, no es un caso real): imaginá un equipo de cuatro personas con una sola suscripción de Azure, dos storage accounts y un par de Function Apps. Nadie en ese equipo se dedica a networking full-time: el mismo developer que escribe el backend es el que corre el Bicep cuando hace falta. En ese escenario, el patrón de cinco piezas que vimos arriba alcanza y sobra: un archivo, cinco recursos, cero dependencias de otro equipo.

Ahora imaginá al mismo equipo seis meses después, pero con quince suscripciones (una por proyecto interno) y private endpoints apareciendo todas las semanas en entornos que ese developer original ya ni conoce. Ahí el mismo patrón empieza a fallar, pero no por un problema técnico: alguien se olvida el DNS zone group en un deploy apurado, otro linkea la zona a la VNet que no correspondía, y nadie se entera hasta que llega el primer 403 en producción. El código Bicep seguía siendo correcto; lo que falló fue depender de que cada persona lo ejecute bien, cada vez.

Ese segundo momento es, ni más ni menos, el que describe el Cloud Adoption Framework cuando separa equipo de plataforma de equipos de workload: automatizar con Azure Policy no es un capricho de arquitectos, es la respuesta a que un patrón manual deja de escalar apenas lo tocan más de dos o tres personas distintas.

Criterios para elegir entre el patrón manual y la automatización con Azure Policy

  • Cantidad de subscripciones. Con una o dos, el Bicep de este artículo es suficiente. A partir de ahí, cada subscripción nueva es un lugar más donde alguien puede olvidarse el zone group sin que nadie lo note.
  • Quién corre el deploy. Si siempre es la misma persona o el mismo pipeline, el riesgo de inconsistencia es bajo. Si son equipos distintos sin visibilidad entre sí, conviene que la zona DNS no dependa de que cada uno la configure bien.
  • Frecuencia de private endpoints nuevos. Uno por trimestre se puede revisar a mano sin drama. Varios por semana piden que algo (una política DeployIfNotExists) lo haga por vos, porque la revisión manual no da los tiempos.
  • Si ya existe un equipo de networking central. El modelo del CAF asume que alguien es dueño de las zonas privatelink. Sin ese rol definido, migrar a políticas centralizadas antes de tener a quién asignarle la responsabilidad suele generar más fricción que la que resuelve.

Qué pasa con el private endpoint si falla el storage a otra región (disaster recovery)

Los private endpoints están atados a una región: si el storage falla a la región secundaria, el endpoint y su registro DNS no lo siguen solos. Hay que crear endpoints en la región secundaria de antemano, o tener un runbook probado para actualizar el DNS, y meterlo adentro de los tests de DR.

El Cloud Adoption Framework lo deja claro para instancias PaaS con failover entre regiones: hoy no hay administración automática del ciclo de vida de esos registros DNS. Vos sos el que actualiza.

Errores comunes

Armás el Bicep, lo probás en una subscripción de test, todo resuelve perfecto desde la VM de bastion, lo mandás a producción y ahí es cuando te enterás que el resolver del hub nunca tuvo linkeada esa zona, así que medio equipo se queda con 403 justo el día del golive. Cubrimos ese tema en detalle en las diferencias entre Microsoft y GitHub.

  • Olvidar el DNS zone group. Sin él, el registro A queda a mano y se desactualiza en silencio si cambia la IP del endpoint. Va siempre en el mismo deploy que el private endpoint.
  • Forwardear la zona privatelink desde on-premises en vez de la pública. El conditional forwarder va sobre blob.core.windows.net, no sobre privatelink.blob.core.windows.net.
  • Repetir la DNS Zone solo para un sub-recurso cuando se usan varios. Blob, file, queue, table y dfs tienen zonas separadas. Si tu app usa Blob y Queue, necesitás dos zonas, dos links, dos endpoints y dos zone groups.
  • No planear DR para los private endpoints. Son recursos regionales. Si no creaste el endpoint en la región secundaria antes del failover, te enterás en el peor momento, con el storage principal ya caído.

Podés permitir tu IP “temporalmente” en el firewall para debuggear desde el portal o con az storage, aunque todos sabemos cómo terminan esos “temporalmente”. Mejor trabajar desde una VM o un servicio dentro (o peereado) de la VNet.

Preguntas Frecuentes

¿Por qué mi app recibe un error 403 después de crear un private endpoint en Azure Storage?

Porque el cliente no resuelve la zona privatelink.blob.core.windows.net y termina usando la IP pública de la cuenta. Con publicNetworkAccess en Disabled, Azure rechaza cualquier conexión que llegue por esa IP pública, así que el arreglo es DNS, no reglas de red.

¿Cómo se configura la DNS zone group en un private endpoint con Bicep?

Se declara como un recurso Microsoft.Network/privateEndpoints/privateDnsZoneGroups con name ‘default’, como parent del private endpoint, apuntando con privateDnsZoneId a la Private DNS Zone del sub-recurso. Ese recurso le dice a Azure que cree y mantenga el registro A solo.

¿Qué zona DNS privada corresponde a cada sub-recurso de Storage (blob, file, queue, table, dfs)?

Blob usa privatelink.blob.core.windows.net, file usa privatelink.file.core.windows.net, queue usa privatelink.queue.core.windows.net, table usa privatelink.table.core.windows.net y dfs (Data Lake Gen2) usa privatelink.dfs.core.windows.net. Cada una necesita su propia zona, link y zone group.

¿Cómo se resuelve el DNS de private endpoints desde servidores on-premises?

Con un conditional forwarder sobre la zona pública (blob.core.windows.net, no la privatelink) apuntando a un Azure DNS Private Resolver o a un servidor DNS en Azure con acceso a la zona privada. Esa zona privatelink también tiene que estar linkeada a la VNet donde vive el resolver.

¿Qué pasa con los private endpoints de Storage en un failover de disaster recovery?

No migran solos: están atados a la región donde se crearon, así que un failover del storage a la región secundaria no mueve el endpoint ni actualiza el DNS. Hay que crear el endpoint en la región secundaria por adelantado o correr un runbook de DNS como parte del plan de DR.

Conclusión

El 403 en un private endpoint de Azure Storage rara vez es un problema de red: en la gran mayoría de los casos es DNS mal resuelto. El patrón de cinco piezas (storage bloqueada, zona privatelink, VNet link, endpoint y zone group) cubre el caso de un solo workload; a partir de varias subscripciones, conviene migrar a zonas centralizadas con Azure Policy, como lo plantea el Cloud Adoption Framework. Lo que no cambia es el checklist de siempre: un nslookup desde cada segmento de red (spoke, hub, on-premises) tiene que devolver la IP privada, y el plan de DR tiene que incluir el DNS, no solo el storage. Si administrás infraestructura propia además de Azure, en donweb.com podés resolver hosting y dominios para el resto del stack sin salir de Argentina.

Fuentes

Te puede interesar...