Skip to content

Azure Security

Azure Firewall Premium: inspección TLS, Threat Intelligence y reglas avanzadas

Resumen

Azure Firewall Premium añade inspección TLS, Threat Intelligence y reglas personalizadas para proteger redes cloud y híbridas. Este post va directo: cómo activar TLS inspection, usar Threat Intelligence, crear reglas avanzadas y monitorizar tráfico. Para admins y arquitectos que quieren seguridad real en producción.

¿Qué es Azure Firewall Premium?

  • Firewall gestionado, escalable y centralizado
  • Inspección TLS (decrypted traffic)
  • Threat Intelligence (bloqueo IPs maliciosas)
  • FQDN filtering, IDPS, reglas de aplicación y red
  • Integración con Log Analytics y Sentinel

Arquitectura / Cómo funciona

flowchart LR
    Internet --> FW[Azure Firewall Premium]
    FW --> VNet1[Prod VNet]
    FW --> VNet2[Dev VNet]
    FW --> OnPrem[On-Premises]
    FW -.-> LogAnalytics[Log Analytics]
    FW -.-> Sentinel[Azure Sentinel]

Activar Firewall Premium y TLS Inspection

  1. Crear Firewall Premium:
    RESOURCE_GROUP="rg-fw-prod"
    LOCATION="westeurope"
    FW_NAME="fw-premium-prod"
    VNET_NAME="vnet-fw-prod"
    SUBNET_NAME="AzureFirewallSubnet"
    
    az network firewall create \
      --name $FW_NAME \
      --resource-group $RESOURCE_GROUP \
      --location $LOCATION \
      --sku Premium
    
  2. Crear certificado para TLS inspection:
    # Generar certificado raíz (usando OpenSSL)
    openssl req -x509 -newkey rsa:4096 -keyout tls.key -out tls.crt -days 365 -nodes -subj "/CN=AzureFirewallTLS"
    # Subir a Key Vault
    az keyvault certificate import \
      --vault-name kv-fw-prod \
      --name fw-tls-root \
      --file tls.crt
    
  3. Configurar TLS inspection:
    az network firewall policy create \
      --name fw-policy-premium \
      --resource-group $RESOURCE_GROUP \
      --location $LOCATION \
      --sku Premium
    az network firewall policy tls-inspection create \
      --policy-name fw-policy-premium \
      --resource-group $RESOURCE_GROUP \
      --key-vault-secret-id <KV_CERT_SECRET_ID>
    

Threat Intelligence

  • Dos modos: "Alert" (solo avisa) y "Deny" (bloquea IPs maliciosas)
    az network firewall policy threat-intel-set \
      --policy-name fw-policy-premium \
      --resource-group $RESOURCE_GROUP \
      --action Deny
    

Reglas avanzadas

  • Application rules: FQDN, URL, TLS
  • Network rules: IP, port, protocol
  • IDPS: detección y bloqueo de ataques
    # Application rule: solo permite https://github.com
    az network firewall policy rule-collection-group application-rule-collection create \
      --policy-name fw-policy-premium \
      --resource-group $RESOURCE_GROUP \
      --name app-allow-github \
      --priority 100 \
      --action Allow \
      --rules '[{"name":"AllowGithub","sourceAddresses":["*"],"protocols":[{"protocolType":"Https","port":443}],"targetFqdns":["github.com"]}]'
    
    # Network rule: solo permite TCP 443 a IP específica
    az network firewall policy rule-collection-group network-rule-collection create \
      --policy-name fw-policy-premium \
      --resource-group $RESOURCE_GROUP \
      --name net-allow-prod \
      --priority 200 \
      --action Allow \
      --rules '[{"name":"AllowProd443","sourceAddresses":["10.0.0.0/24"],"destinationAddresses":["20.30.40.50"],"destinationPorts":["443"],"protocols":["TCP"]}]'
    

Monitorización y logging

  • Integrar con Log Analytics:
    az monitor diagnostic-settings create \
      --resource /subscriptions/$SUB_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/azureFirewalls/$FW_NAME \
      --workspace $LOG_ANALYTICS_ID \
      --name fw-logs \
      --logs '[{"category":"AzureFirewallApplicationRule","enabled":true},{"category":"AzureFirewallNetworkRule","enabled":true},{"category":"AzureFirewallDnsProxy","enabled":true}]'
    
  • Queries KQL útiles:
    // Top 10 destinos bloqueados
    AzureDiagnostics
    | where Category == "AzureFirewallNetworkRule"
    | where action_s == "Deny"
    | summarize Count = count() by destination_ip_s
    | top 10 by Count desc
    
    // Detección de ataques IDPS
    AzureDiagnostics
    | where Category == "AzureFirewallIdpsSignature"
    | summarize Count = count() by signature_name_s
    | top 10 by Count desc
    

Buenas prácticas

  • Usar TLS inspection solo para tráfico interno/confidencial
  • Mantener certificados actualizados en Key Vault
  • Threat Intelligence en modo "Deny" para entornos críticos
  • Revisar logs y alertas semanalmente
  • Segmentar reglas por entorno (dev, prod)
  • Integrar con Sentinel para correlación de incidentes

Costes

  • Firewall Premium: ~$1,500/mes por unidad
  • Log Analytics: ~$2.30/GB
  • Sentinel: ~$2.46/GB

Referencias

Microsoft Defender for Cloud CSPM: Governance y Attack Paths

Resumen

Defender CSPM (Cloud Security Posture Management) proporciona visibilidad avanzada de riesgos en entornos Azure, AWS y GCP mediante governance rules (asignación automática de owners y SLAs), attack path analysis (modelado de rutas de ataque explotables), Cloud Security Explorer (queries para detectar misconfiguraciones) y risk prioritization de recommendations. Plan Defender CSPM ~$12/recurso/mes.

Azure Firewall vs NSG: Cuándo usar cada uno

Resumen

NSG filtra a nivel de subnet/NIC, Azure Firewall es un firewall centralizado con threat intelligence. No son excluyentes, se complementan.

Introducción

[Contenido técnico detallado a desarrollar]

Configuración básica

# Comandos de ejemplo
RG="my-rg"
LOCATION="westeurope"

# Comandos Azure CLI
az group create --name $RG --location $LOCATION

Casos de uso

  • Caso 1: [Descripción]
  • Caso 2: [Descripción]
  • Caso 3: [Descripción]

Buenas prácticas

  • Práctica 1: Descripción
  • Práctica 2: Descripción
  • Práctica 3: Descripción

Monitoreo y troubleshooting

# Comandos de diagnóstico
az monitor metrics list --resource ...

Referencias

Conditional Access: Políticas esenciales para Zero Trust

Resumen

Conditional Access es el corazón de Zero Trust en Azure. aquí están las 5 políticas que deberías implementar HOY en tu tenant.

¿Qué es Conditional Access?

Conditional Access evalúa signals en tiempo real para decidir si permitir, bloquear o requerir MFA en un acceso:

Signals: - Usuario/grupo - Ubicación (IP ranges) - Dispositivo (managed, compliant) - Aplicación - Riesgo (Azure AD Identity Protection)

Decisiones: - Permitir - Bloquear - Requiere MFA - Requiere dispositivo compliant - Requiere hybrid Azure AD join

Prerequisitos

# Verificar licencias (P1 mínimo)
az ad signed-in-user show --query "assignedLicenses[].skuId"

# Crear grupo de exclusión (break-glass accounts)
az ad group create \
  --display-name "CA-Exclusion-Emergency" \
  --mail-nickname "ca-exclusion"

Break-glass accounts

SIEMPRE excluye 2 cuentas de emergencia de todas las políticas CA. Si algo falla, necesitas acceso.

Política 1: MFA para todos los admins

# Esta política la creas desde el portal por ser más visual
# Portal → Azure AD → Security → Conditional Access

Configuración: - Usuarios: Todos los roles admin (Global Admin, Security Admin, etc.) - Cloud apps: Todas las aplicaciones - Condiciones: Ninguna - Grant: Require MFA - Estado: Report-only (primero testea)

JSON de ejemplo (via API):

{
  "displayName": "CA001: Require MFA for administrators",
  "state": "enabledForReportingButNotEnforced",
  "conditions": {
    "users": {
      "includeRoles": [
        "62e90394-69f5-4237-9190-012177145e10",  // Global Admin
        "194ae4cb-b126-40b2-bd5b-6091b380977d"   // Security Admin
      ],
      "excludeGroups": ["break-glass-group-id"]
    },
    "applications": {
      "includeApplications": ["All"]
    }
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": ["mfa"]
  }
}

Política 2: Bloquear legacy authentication

Legacy auth (IMAP, POP3, SMTP) no soporta MFA → vector de ataque.

{
  "displayName": "CA002: Block legacy authentication",
  "state": "enabled",
  "conditions": {
    "users": {
      "includeUsers": ["All"],
      "excludeGroups": ["break-glass-group-id"]
    },
    "applications": {
      "includeApplications": ["All"]
    },
    "clientAppTypes": [
      "exchangeActiveSync",
      "other"  // Incluye IMAP, POP3, SMTP
    ]
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": ["block"]
  }
}

Política 3: Requiere managed devices para apps corporativas

{
  "displayName": "CA003: Require compliant device for corporate apps",
  "state": "enabled",
  "conditions": {
    "users": {
      "includeUsers": ["All"],
      "excludeGroups": ["break-glass-group-id"]
    },
    "applications": {
      "includeApplications": [
        "00000003-0000-0000-c000-000000000000",  // Microsoft Graph
        "Office365"
      ]
    },
    "platforms": {
      "includePlatforms": ["windows", "macOS", "iOS", "android"]
    }
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": [
      "compliantDevice",
      "domainJoinedDevice"
    ]
  }
}

Política 4: Bloquear acceso desde países no autorizados

# Crear Named Location
az rest --method PUT \
  --url 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations' \
  --body '{
    "@odata.type": "#microsoft.graph.countryNamedLocation",
    "displayName": "Blocked Countries",
    "countriesAndRegions": ["KP", "IR", "SY"],
    "includeUnknownCountriesAndRegions": false
  }'

Política:

{
  "displayName": "CA004: Block access from blocked countries",
  "state": "enabled",
  "conditions": {
    "users": {
      "includeUsers": ["All"],
      "excludeGroups": ["break-glass-group-id", "travelers-group-id"]
    },
    "applications": {
      "includeApplications": ["All"]
    },
    "locations": {
      "includeLocations": ["blocked-countries-location-id"]
    }
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": ["block"]
  }
}

Política 5: MFA para acceso desde fuera de red corporativa

# Crear Named Location para IPs corporativas
az rest --method POST \
  --url 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations' \
  --body '{
    "@odata.type": "#microsoft.graph.ipNamedLocation",
    "displayName": "Corporate Network",
    "isTrusted": true,
    "ipRanges": [
      {"@odata.type": "#microsoft.graph.iPv4CidrRange", "cidrAddress": "203.0.113.0/24"},
      {"@odata.type": "#microsoft.graph.iPv4CidrRange", "cidrAddress": "198.51.100.0/24"}
    ]
  }'

Política:

{
  "displayName": "CA005: Require MFA for external access",
  "state": "enabled",
  "conditions": {
    "users": {
      "includeUsers": ["All"],
      "excludeGroups": ["break-glass-group-id"]
    },
    "applications": {
      "includeApplications": ["All"]
    },
    "locations": {
      "includeLocations": ["Any"],
      "excludeLocations": ["corporate-network-location-id"]
    }
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": ["mfa"]
  }
}

Testing con Report-Only mode

# Ver reportes de impacto
# Portal → Azure AD → Sign-in logs → Conditional Access tab

Analiza: - ¿Cuántos usuarios impactados? - ¿Algún servicio crítico bloqueado? - ¿Break-glass accounts funcionando?

Después de 7-14 días de monitoring → cambiar a enabled.

Monitoreo continuo

Query en Log Analytics:

SigninLogs
| where TimeGenerated > ago(24h)
| where ConditionalAccessStatus != "notApplied"
| summarize count() by ConditionalAccessStatus, ConditionalAccessPolicies
| order by count_ desc

Alerta para fallos:

az monitor scheduled-query create \
  --resource-group $RG \
  --name ca-policy-failures \
  --scopes /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/my-law \
  --condition "count 'SigninLogs | where ConditionalAccessStatus == \"failure\"' > 10" \
  --window-size 5m \
  --evaluation-frequency 5m \
  --action /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.Insights/actionGroups/security-team

Política avanzada: Risk-based con Identity Protection

Requiere Azure AD P2:

{
  "displayName": "CA006: Block high risk sign-ins",
  "state": "enabled",
  "conditions": {
    "users": {
      "includeUsers": ["All"],
      "excludeGroups": ["break-glass-group-id"]
    },
    "applications": {
      "includeApplications": ["All"]
    },
    "signInRiskLevels": ["high"]
  },
  "grantControls": {
    "operator": "OR",
    "builtInControls": ["block"]
  }
}

Buenas prácticas

  • Naming convention: CA###: [Acción] for [Condición]
  • Report-only primero: Nunca actives sin testear
  • Exclusiones documentadas: Justifica cada grupo excluido
  • Review trimestral: Las políticas se vuelven obsoletas
  • Combinación de políticas: No crees una mega-política, divide por propósito
  • What If tool: Usa el simulador antes de activar

Errores comunes

  • Bloquear sin break-glass accounts
  • No testear en report-only mode
  • Aplicar a "All apps" sin excluir Azure Management
  • No documentar políticas

Referencias

Azure Bastion: SSH y RDP sin exponer IPs públicas

Resumen

Azure Bastion te permite conectarte a tus VMs sin asignarles IP pública. Funciona como jump server managed que accedes desde el portal de Azure. Ideal para cumplir con políticas de seguridad estrictas.

¿Qué es Azure Bastion?

Azure Bastion es un servicio PaaS que despliegas en tu VNet y proporciona:

  • Conectividad RDP/SSH segura sobre SSL (puerto 443)
  • Sin necesidad de IP pública en las VMs
  • Sin agentes ni software cliente
  • Protección contra port scanning y zero-day exploits

Arquitectura

graph LR
    A[Usuario] -->|HTTPS 443| B[Azure Bastion]
    B -->|Private IP| C[VM Linux SSH]
    B -->|Private IP| D[VM Windows RDP]
    B -.subnet dedicado.- E[AzureBastionSubnet]

Despliegue básico

# Variables
RG="my-rg"
LOCATION="westeurope"
VNET_NAME="my-vnet"
BASTION_NAME="my-bastion"

# Crear subnet específica para Bastion (nombre obligatorio)
az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $VNET_NAME \
  --name AzureBastionSubnet \
  --address-prefixes 10.0.255.0/26

# Crear IP pública para Bastion
az network public-ip create \
  --resource-group $RG \
  --name ${BASTION_NAME}-pip \
  --sku Standard \
  --location $LOCATION

# Crear Azure Bastion
az network bastion create \
  --resource-group $RG \
  --name $BASTION_NAME \
  --public-ip-address ${BASTION_NAME}-pip \
  --vnet-name $VNET_NAME \
  --location $LOCATION

Requisitos de subnet

  • El subnet DEBE llamarse AzureBastionSubnet
  • Mínimo /26 (64 IPs)
  • No puede tener NSG restrictivo

Conexión a VMs

Desde el Portal

  1. Ir a la VM → Connect → Bastion
  2. Introducir credenciales
  3. Conectar en el navegador

Desde CLI (requiere Standard SKU)

# Conectar a VM Linux
az network bastion ssh \
  --resource-group $RG \
  --name $BASTION_NAME \
  --target-resource-id /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/my-linux-vm \
  --auth-type password \
  --username azureuser

# Conectar a VM Windows (requiere native client)
az network bastion rdp \
  --resource-group $RG \
  --name $BASTION_NAME \
  --target-resource-id /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/my-windows-vm

SKUs disponibles

SKU Características Precio aprox.
Basic RDP/SSH desde portal ~140€/mes
Standard + CLI access, file copy, IP-based connection ~140€/mes + instancias
Premium + Kerberos auth, session recording En preview

Standard SKU: Funcionalidades avanzadas

# Actualizar a Standard SKU y habilitar features
az network bastion update \
  --resource-group $RG \
  --name $BASTION_NAME \
  --sku Standard \
  --enable-tunneling true \
  --enable-ip-connect true

# Escalar instancias (2-50) para alta disponibilidad
az network bastion update \
  --resource-group $RG \
  --name $BASTION_NAME \
  --scale-units 3

Nuevas capacidades con Standard: - Native client support: Conectar con tu cliente SSH/RDP local vía az network bastion - IP-based connection: Conectar a cualquier IP dentro de la VNet - File transfer: Upload/download archivos vía tunnel - Tunneling: Crear túneles SSH para port forwarding - Shareable link: Generar URLs para acceso temporal (requiere Premium)

Copiar archivos (Standard SKU con tunneling)

# Crear tunnel SSH para file transfer
az network bastion tunnel \
  --resource-group $RG \
  --name $BASTION_NAME \
  --target-resource-id /subscriptions/{sub}/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/my-vm \
  --resource-port 22 \
  --port 2222

# En otra terminal: upload archivo
scp -P 2222 local-file.txt azureuser@localhost:/home/azureuser/

# Download archivo
scp -P 2222 azureuser@localhost:/home/azureuser/remote-file.txt ./

Tunnel para RDP:

# Crear tunnel RDP (puerto 3389)
az network bastion tunnel \
  --resource-group $RG \
  --name $BASTION_NAME \
  --target-resource-id /subscriptions/{sub}/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/my-windows-vm \
  --resource-port 3389 \
  --port 13389

# Conectar con cliente RDP local a localhost:13389
mstsc /v:localhost:13389

Monitoreo

# Ver métricas de sesiones
az monitor metrics list \
  --resource /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.Network/bastionHosts/$BASTION_NAME \
  --metric "Sessions"

# Diagnostic logs
az monitor diagnostic-settings create \
  --resource /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.Network/bastionHosts/$BASTION_NAME \
  --name bastion-logs \
  --workspace /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/my-law \
  --logs '[{"category": "BastionAuditLogs", "enabled": true}]'

Buenas prácticas

  • NSG en subnets de VMs: Permite solo tráfico desde AzureBastionSubnet
  • JIT Access: Combina con Microsoft Defender for Cloud JIT
  • Session recording: Habilita en Premium SKU para compliance
  • Firewall rules: Bastion necesita acceso saliente a Internet (servicios Azure)
  • Disaster recovery: Despliega Bastion en múltiples regiones

Restricciones de red (NSG)

El subnet AzureBastionSubnet requiere reglas NSG específicas:

# Crear NSG para AzureBastionSubnet
az network nsg create \
  --resource-group $RG \
  --name ${BASTION_NAME}-nsg

# Inbound: HTTPS desde Internet
az network nsg rule create \
  --resource-group $RG \
  --nsg-name ${BASTION_NAME}-nsg \
  --name AllowHttpsInbound \
  --priority 100 \
  --source-address-prefixes Internet \
  --destination-port-ranges 443 \
  --protocol Tcp \
  --access Allow \
  --direction Inbound

# Inbound: GatewayManager
az network nsg rule create \
  --resource-group $RG \
  --nsg-name ${BASTION_NAME}-nsg \
  --name AllowGatewayManager \
  --priority 110 \
  --source-address-prefixes GatewayManager \
  --destination-port-ranges 443 \
  --protocol Tcp \
  --access Allow \
  --direction Inbound

# Inbound: Bastion internal communication
az network nsg rule create \
  --resource-group $RG \
  --nsg-name ${BASTION_NAME}-nsg \
  --name AllowBastionHostCommunication \
  --priority 120 \
  --source-address-prefixes VirtualNetwork \
  --destination-port-ranges 8080 5701 \
  --protocol Tcp \
  --access Allow \
  --direction Inbound

# Outbound: SSH/RDP a VMs
az network nsg rule create \
  --resource-group $RG \
  --nsg-name ${BASTION_NAME}-nsg \
  --name AllowSshRdpOutbound \
  --priority 100 \
  --destination-address-prefixes VirtualNetwork \
  --destination-port-ranges 22 3389 \
  --protocol Tcp \
  --access Allow \
  --direction Outbound

# Outbound: Azure Cloud (servicios de Azure)
az network nsg rule create \
  --resource-group $RG \
  --nsg-name ${BASTION_NAME}-nsg \
  --name AllowAzureCloudOutbound \
  --priority 110 \
  --destination-address-prefixes AzureCloud \
  --destination-port-ranges 443 \
  --protocol Tcp \
  --access Allow \
  --direction Outbound

# Asociar NSG al subnet
az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $VNET_NAME \
  --name AzureBastionSubnet \
  --network-security-group ${BASTION_NAME}-nsg

Costos

Ejemplo en West Europe: - Basic: ~€140/mes (fijo) - Standard: ~€140/mes + ~€9/instancia adicional/mes - Tráfico outbound: Estándar Azure

Ahorro

Si solo necesitas acceso ocasional, considera apagar/encender Bastion. Pagas solo por las horas que está running.

Alternativas

  • Azure VPN Gateway: Para acceso permanente desde on-premises
  • Azure Virtual WAN: Para topologías hub-spoke complejas
  • Just-in-Time Access: Para exposición temporal de puertos

Referencias

Rotación automática de secretos en Azure Key Vault

Resumen

Rotar secretos manualmente es error-prone y tedioso. Azure Key Vault soporta rotación automatizada mediante Event Grid + Azure Functions. Aquí te muestro cómo implementarla paso a paso.

¿Por qué rotar secretos?

  • Compliance: PCI-DSS, SOC2 exigen rotación periódica
  • Seguridad: Limita ventana de exposición si hay leak
  • Best practice: NIST recomienda rotación cada 90 días

Keys vs Secrets

  • Keys criptográficas: Tienen rotación nativa con rotation policy
  • Secrets (passwords, API keys): Requieren Event Grid + Function App

Este artículo cubre secrets. Para keys, ver Configure key rotation.

Arquitectura de rotación automática

Azure Key Vault usa Event Grid para notificar cuando un secreto está cerca de expirar:

graph LR
    A[Key Vault Secret] -->|30 días antes expiración| B[Event Grid]
    B -->|SecretNearExpiry event| C[Function App]
    C -->|Genera nuevo secret| D[Servicio Externo/SQL]
    C -->|Actualiza secreto| A

Proceso: 1. Key Vault publica evento SecretNearExpiry 30 días antes de expiración 2. Event Grid llama a Function App vía HTTP POST 3. Function genera nuevo secreto y actualiza el servicio 4. Function actualiza Key Vault con nueva versión del secreto

Implementación: Rotar SQL Server password

1. Crear secreto con fecha de expiración

1. Crear secreto con fecha de expiración

# Variables
RG="my-rg"
KV_NAME="my-keyvault"
SECRET_NAME="sql-admin-password"
SQL_SERVER="my-sql-server"

# Crear secreto con expiración 90 días
EXPIRY_DATE=$(date -u -d "+90 days" +'%Y-%m-%dT%H:%M:%SZ')

az keyvault secret set \
  --vault-name $KV_NAME \
  --name $SECRET_NAME \
  --value "InitialP@ssw0rd!" \
  --expires $EXPIRY_DATE

2. Desplegar Function App de rotación

Usar template oficial de Microsoft:

# Deploy ARM template con Function App preconfigurada
az deployment group create \
  --resource-group $RG \
  --template-uri https://raw.githubusercontent.com/Azure-Samples/KeyVault-Rotation-SQLPassword-Csharp/main/ARM-Templates/Function/azuredeploy.json \
  --parameters \
    sqlServerName=$SQL_SERVER \
    keyVaultName=$KV_NAME \
    functionAppName="${KV_NAME}-rotation-func" \
    secretName=$SECRET_NAME \
    repoUrl="https://github.com/Azure-Samples/KeyVault-Rotation-SQLPassword-Csharp.git"

Este template despliega: - Function App con managed identity - Event Grid subscription a SecretNearExpiry - Access policy de Key Vault para la función - Código de rotación preconfigura do

3. Código de la función (incluido en el template)

3. Código de la función (incluido en el template)

La función C# incluida en el template maneja: - Recibe evento SecretNearExpiry de Event Grid - Extrae nombre del secreto y versión - Genera nuevo password aleatorio - Actualiza SQL Server con nuevo password - Crea nueva versión del secreto en Key Vault

// Código simplificado (el template incluye implementación completa)
[FunctionName("AKVSQLRotation")]
public static void Run([EventGridTrigger]EventGridEvent eventGridEvent)
{
    var secretName = eventGridEvent.Subject;
    var keyVaultName = ExtractVaultName(eventGridEvent.Topic);

    // Rotar password
    SecretRotator.RotateSecret(log, secretName, keyVaultName);
}

Implementación: Rotar Storage Account keys

Para servicios con dos sets de credenciales (primary/secondary keys):

# Deploy template para Storage Account rotation
az deployment group create \
  --resource-group $RG \
  --template-uri https://raw.githubusercontent.com/Azure-Samples/KeyVault-Rotation-StorageAccountKey-PowerShell/master/ARM-Templates/Function/azuredeploy.json \
  --parameters \
    storageAccountName=$STORAGE_ACCOUNT \
    keyVaultName=$KV_NAME \
    functionAppName="${KV_NAME}-storage-rotation"

# Crear secret con metadata para rotación
EXPIRY_DATE=$(date -u -d "+60 days" +'%Y-%m-%dT%H:%M:%SZ')
STORAGE_KEY=$(az storage account keys list -n $STORAGE_ACCOUNT --query "[0].value" -o tsv)

az keyvault secret set \
  --vault-name $KV_NAME \
  --name storageKey \
  --value "$STORAGE_KEY" \
  --tags CredentialId=key1 ProviderAddress="/subscriptions/{sub}/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$STORAGE_ACCOUNT" ValidityPeriodDays=60 \
  --expires $EXPIRY_DATE

Estrategia dual-key: 1. Key1 almacenada en Key Vault 2. Evento SecretNearExpiry activa rotación 3. Function regenera Key2 en Storage Account 4. Actualiza secreto en Key Vault con Key2 5. Próxima rotación alterna a Key1

Monitoreo de rotaciones

# Ver versiones de un secreto
az keyvault secret list-versions \
  --vault-name $KV_NAME \
  --name $SECRET_NAME \
  --query "[].{Version:id, Created:attributes.created, Expires:attributes.expires}"

# Ver última actualización
az keyvault secret show \
  --vault-name $KV_NAME \
  --name $SECRET_NAME \
  --query "attributes.{Updated:updated, Expires:expires, Enabled:enabled}"

# Ver logs de rotación en Function App
az monitor app-insights query \
  --app ${KV_NAME}-rotation-func \
  --analytics-query "traces | where message contains 'Rotation' | top 10 by timestamp desc"

Notificaciones por email

# Action Group para alertas
az monitor action-group create \
  --resource-group $RG \
  --name secret-rotation-alerts \
  --short-name SecRot \
  --email-receiver EmailAdmin admin@company.com

# Alert rule
az monitor metrics alert create \
  --resource-group $RG \
  --name secret-rotation-failed \
  --scopes /subscriptions/{sub-id}/resourceGroups/$RG/providers/Microsoft.KeyVault/vaults/$KV_NAME \
  --condition "count SecretRotationFailed > 0" \
  --action secret-rotation-alerts

Buenas prácticas

  • Overlap period: Mantén versión anterior válida 7-30 días
  • Testing: Rota primero en entorno de dev/staging
  • Documentación: Registra qué servicios usan cada secreto
  • Backup: Exporta secretos críticos a offline storage encriptado
  • Notificaciones: Configura alertas para rotaciones fallidas

Secretos hardcodeados

La rotación no sirve de nada si tienes secretos hardcodeados en código o config files. Usa referencias a Key Vault (@Microsoft.KeyVault(SecretUri=...) en App Settings).

Templates oficiales

Microsoft proporciona templates ARM completos para diferentes escenarios: - SQL password rotation - Storage Account keys rotation - Adaptables para otros servicios (Cosmos DB, Redis, APIs externas)

Referencias