Servidores SQL con firewall abierto
Encuentra servidores de Azure SQL con una regla de firewall abierta a todo Internet o a todos los servicios de Azure, incluidos los de otros clientes.
Ejemplo en México
Inmobiliaria en Tijuana, 70 personas
Un desarrollador externo pidió abrir todo el firewall para terminar una migración y no se sabe si se cerró.
.\get-sql-firewall-abierto.ps1 -RutaCsv .\sql-firewall.csvEncuentra la regla abierta a todo Internet en el servidor de ventas y la cambia por la IP de la oficina.
Cómo implementarlo
- Corre el script y empieza por las filas con Apertura en "Todo Internet".
- Revisa AccesoRedPublica: si está en Disabled, esas reglas no permiten entrar por el punto de conexión público.
- Averigua qué aplicación necesita conectarse y limita la regla a sus IP, o usa Private Link.
- Borra la regla abierta en una ventana controlada y confirma que las aplicaciones siguen funcionando.
Ver el código completo de get-sql-firewall-abierto.ps1
<#
.SYNOPSIS
¿Qué servidores SQL permiten conexiones desde todo Internet o desde todos los servicios de Azure?
.DESCRIPTION
Lista los servidores de Azure SQL con Resource Graph y revisa sus reglas de firewall. Reporta la regla 0.0.0.0-255.255.255.255 y la 0.0.0.0-0.0.0.0. Solo lectura: no cambia reglas.
.PARAMETER Suscripciones
IDs o nombres de suscripción. Si se omite, usa todas a las que tienes acceso.
.PARAMETER RutaCsv
Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\get-sql-firewall-abierto.ps1 -RutaCsv .\sql-firewall.csv
.NOTES
Permisos: Lector (Reader) en cada suscripción
Rol mínimo: Lector (Reader)
Licencia: Basta una suscripción de Azure; no requiere plan adicional.
Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en una suscripción de pruebas.
#>
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()][string[]]$Suscripciones,
[string]$RutaCsv
)
$ErrorActionPreference = 'Stop'
if (-not (Get-Command Connect-AzAccount -ErrorAction SilentlyContinue)) {
throw 'Falta el módulo. Instálalo con: Install-Module Az.Accounts -Scope CurrentUser'
}
if (-not (Get-AzContext)) { $null = Connect-AzAccount }
function InvocarAzure($Metodo, $Ruta, $Cuerpo) {
$parametros = @{ Method = $Metodo }
if ($Ruta -like 'http*') { $parametros.Uri = $Ruta } else { $parametros.Path = $Ruta }
if ($Cuerpo) { $parametros.Payload = $Cuerpo | ConvertTo-Json -Depth 10 }
# Invoke-AzRestMethod no lanza error con 4xx o 5xx. 429 es una limitación temporal: se reintenta.
for ($intento = 1; ; $intento++) {
$respuesta = Invoke-AzRestMethod @parametros
if ($respuesta.StatusCode -ne 429 -or $intento -ge 4) { break }
Start-Sleep -Seconds 30
}
if ($respuesta.StatusCode -ge 400) { throw "Azure respondió $($respuesta.StatusCode) en ${Ruta}: $($respuesta.Content)" }
if ($respuesta.Content) { $respuesta.Content | ConvertFrom-Json -AsHashtable }
}
function ListarPaginado($Ruta) {
while ($Ruta) {
$pagina = InvocarAzure GET $Ruta
$pagina.value
$Ruta = $pagina.nextLink
}
}
$activas = @(ListarPaginado '/subscriptions?api-version=2022-12-01' | Where-Object {
$_.state -notin 'Disabled', 'Deleted' -and
(-not $Suscripciones -or $_.subscriptionId -in $Suscripciones -or $_.displayName -in $Suscripciones)
})
if (-not $activas) { throw 'No hay suscripciones activas a las que tengas acceso con ese filtro.' }
$noEncontradas = @($Suscripciones | Where-Object { $_ -and $_ -notin $activas.subscriptionId -and $_ -notin $activas.displayName })
if ($noEncontradas) { throw "No se encontró o no está activa: $($noEncontradas -join ', '). Revisa -Suscripciones." }
$nombres = @{}
foreach ($s in $activas) { $nombres[$s.subscriptionId] = $s.displayName }
function ConsultarGrafo($Consulta) {
$ids = @($activas.subscriptionId)
# Resource Graph admite hasta 1,000 suscripciones por consulta.
for ($i = 0; $i -lt $ids.Count; $i += 1000) {
$lote = @($ids[$i..([Math]::Min($i + 999, $ids.Count - 1))])
$cuerpo = @{ query = $Consulta; subscriptions = $lote; options = @{ '$top' = 1000 } }
do {
$pagina = InvocarAzure POST '/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01' $cuerpo
$pagina.data
$cuerpo.options['$skipToken'] = $pagina.'$skipToken'
} while ($pagina.'$skipToken')
}
}
$consulta = 'resources | where type =~ "microsoft.sql/servers" | project id, name, resourceGroup, subscriptionId, location, accesoPublico = tostring(properties.publicNetworkAccess)'
$sinConsultar = @()
$filas = @(foreach ($servidor in ConsultarGrafo $consulta) {
try {
$reglas = @(ListarPaginado "$($servidor.id)/firewallRules?api-version=2025-01-01")
} catch {
Write-Warning "No se pudo consultar $($servidor.name): $_"
$sinConsultar += $servidor.name
continue
}
foreach ($regla in $reglas) {
$inicio = $regla.properties.startIpAddress
$fin = $regla.properties.endIpAddress
$apertura = if ($inicio -eq '0.0.0.0' -and $fin -eq '255.255.255.255') { 'Todo Internet' }
elseif ($inicio -eq '0.0.0.0' -and $fin -eq '0.0.0.0') { 'Todos los servicios de Azure' }
if ($apertura) {
[pscustomobject]@{
Suscripcion = $nombres[$servidor.subscriptionId]
GrupoRecursos = $servidor.resourceGroup
Servidor = $servidor.name
Regla = $regla.name
Apertura = $apertura
IpInicio = $inicio
IpFin = $fin
AccesoRedPublica = $servidor.accesoPublico
Id = $servidor.id
}
}
}
})
Write-Host "Reglas abiertas: $($filas.Count)"
$filas | Group-Object Apertura | ForEach-Object { Write-Host " $($_.Name): $($_.Count)" }
if ($sinConsultar) {
Write-Host " No se pudo consultar: $(@($sinConsultar).Count)"
$filas += @($sinConsultar | ForEach-Object { [pscustomobject]@{ Suscripcion = ''; GrupoRecursos = ''; Servidor = $_; Regla = ''; Apertura = 'No se pudo consultar'; IpInicio = ''; IpFin = ''; AccesoRedPublica = ''; Id = '' } })
}
if ($filas.Count -eq 0) {
Write-Host 'Sin resultados.'
# Sin esto, el CSV de una corrida anterior parecería el resultado de esta.
if ($RutaCsv) { Set-Content -Path $RutaCsv -Value '' -Encoding utf8BOM }
} elseif ($RutaCsv) {
# UTF-8 con BOM para que Excel en español muestre bien los acentos.
$filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM
Write-Host "CSV guardado en $RutaCsv"
} else {
$filas | Format-Table -AutoSize
}
# El reporte ya quedó escrito; el error avisa que está incompleto.
if ($sinConsultar) { throw "Reporte incompleto, no se pudo consultar: $(@($sinConsultar) -join ', ')" }