Problema

En entornos de Azure AD (ahora Entra ID) es habitual que, con el tiempo, el número de App Registrations, Enterprise Applications y Managed Identities crezca sin control. Cada nueva aplicación lleva credenciales, permisos y, a menudo, un propietario que nunca se registra formalmente. Cuando el catálogo supera cientos de objetos, la consola nativa obliga a abrir múltiples blades para responder preguntas básicas:

  • ¿Quién es el propietario?
  • ¿Qué secretos expiran pronto?
  • ¿Cuál es la última fecha de inicio de sesión?
  • ¿Qué aplicaciones tienen permisos de alto riesgo sobre Microsoft Graph?

La falta de una vista única dificulta la higiene periódica, genera licencias innecesarias de Entra ID Governance y aumenta el riesgo de que credenciales caducadas o permisos excesivos permanezcan sin supervisión.

Causa

  1. Acumulación sin proceso de onboarding – Los equipos crean aplicaciones directamente en el portal o mediante scripts sin un flujo de registro de propietario ni de revisión de permisos.
  2. Dependencia de la UI de Entra – La consola está diseñada para gestión puntual, no para auditoría masiva; cada consulta requiere una llamada a Graph que se ejecuta bajo demanda.
  3. Falta de automatización de expiraciones – Los secretos y certificados se generan con fechas arbitrarias y no se integran con alertas o procesos de rotación.
  4. Licenciamiento de Governance – Las funcionalidades de Access Reviews y PIM son potentes pero costosas; muchos equipos solo necesitan una vista resumida y acciones ligeras.
  5. Escasa visibilidad de riesgos – Los permisos de alto riesgo y la cobertura de Conditional Access no se exponen en un panel consolidado, lo que dificulta la priorización de mitigaciones.

Solución

Construir un portal de gobernanza auto‑alojado que se ejecute dentro del mismo tenant. El enfoque se basa en tres pilares:

Pilar Qué hace Por qué es útil
Azure App Service + EasyAuth Aloja una SPA o API estática que muestra los datos. EasyAuth protege la API con la identidad del propio App Service. El coste es predecible (B2≈ $30/mes) y no se necesita infraestructura externa.
Managed Identity + Microsoft Graph La identidad asignada al App Service solicita tokens y consulta Graph sin secretos. Elimina la rotación de credenciales y permite aplicar el principio de menor privilegio mediante roles de Graph.
Cache + Timer Functions Un Azure Function con temporizador pre‑carga los datos críticos (propietarios, expiraciones, últimos inicios) en Azure Table/Blob o Redis. Evita llamadas en tiempo real a Graph, reduciendo latencia y evitando bloqueos por límites de tasa.

Paso a paso

  1. Crear el App Service con identidad gestionada

    az group create -n GovPortalRG -l eastus
    az appservice plan create -g GovPortalRG -n GovPlan --sku B2
    az webapp create -g GovPortalRG -n govportal-<unique> -p GovPlan --runtime "node|18-lts"
    az webapp identity assign -g GovPortalRG -n govportal-<unique>
    
  2. Conceder permisos de Graph a la identidad

    az ad app permission add --id <appId_del_AppService> \
      --api 00000003-0000-0000-c000-000000000000 \
      --api-permissions \
      Application.ReadWrite.All \
      Directory.Read.All \
      RoleManagement.ReadWrite.Directory
    az ad app permission grant --id <appId_del_AppService> --api 00000003-0000-0000-c000-000000000000 --expires never
    
  3. Implementar la función de pre‑carga (Node.js o Python). Cada 15 min ejecuta consultas como:

    • GET /applications?$filter=appId ne null&$select=appId,displayName,owners
    • GET /servicePrincipals?$filter=appId ne null&$select=appId,displayName,passwordCredentials,signInActivity

    Los resultados se guardan en una tabla GovCache con una columna partitionKey = entityType y rowKey = objectId.

  4. Desplegar la UI (React, Vue o simple HTML). La UI consume una API interna (/api/summary) que lee directamente del cache y muestra:

    • Lista de aplicaciones con columnas: propietario, expiración de credencial, último inicio, nivel de riesgo.
    • Filtros por “sin propietario”, “credencial expirada”, “permiso de Graph de alto riesgo”.
    • Botón “Solicitar cambio de propietario” que escribe una petición en una cola de Storage; un script posterior procesa la solicitud y actualiza Graph.
  5. Opcional: Notificaciones por correo
    Configura SendGrid (clave API) y una Function que revisa el cache cada hora, enviando alertas a los dueños de credenciales próximas a expirar.

Alternativas prácticas

  • Azure Static Web Apps en lugar de App Service: menor coste, pero requiere que la API sea Azure Functions separada.
  • Power Platform (Power Apps + Power Automate) para equipos sin experiencia en desarrollo; sin embargo, la flexibilidad y control de costos son menores.
  • Uso de Azure Monitor + Log Analytics para generar alertas basadas en logs de sign‑in, pero no provee una UI consolidada.

Cuándo aplicar esta solución

Se recomienda cuando se cumplen al menos dos de los siguientes criterios:

  • Más de 100 App Registrations o Enterprise Apps y la consola de Entra se vuelve lenta para auditorías.
  • Se necesita visibilidad rápida de credenciales próximas a expirar sin comprar licencias de Entra Governance.
  • El equipo tiene experiencia básica con Azure CLI/Functions y prefiere mantener los datos dentro del tenant.

No es adecuado si:

  • Ya se dispone de licencias de Entra ID Governance y se usan Access Reviews de forma intensiva.
  • La organización requiere un control de cambios en tiempo real (mutaciones directas) sin auditoría intermedia.
  • El presupuesto es extremadamente limitado y no se puede asignar al menos $25/mes a un App Service.

Código

# 1. Grupo y plan
az group create -n GovPortalRG -l eastus
az appservice plan create -g GovPortalRG -n GovPlan --sku B2

# 2. WebApp con identidad gestionada
az webapp create -g GovPortalRG -n govportal-$(openssl rand -hex 4) -p GovPlan --runtime "node|18-lts"
az webapp identity assign -g GovPortalRG -n govportal-$(openssl rand -hex 4)

# 3. Permisos Graph (ejemplo con Azure AD PowerShell)
APP_ID=$(az webapp show -g GovPortalRG -n govportal-$(openssl rand -hex 4) --query identity.principalId -o tsv)
az ad app permission add --id $APP_ID \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions Application.ReadWrite.All Directory.Read.All RoleManagement.ReadWrite.Directory
az ad app permission grant --id $APP_ID --api 00000003-0000-0000-c000-000000000000 --expires never

Verificación

  1. Identidad asignada – En el portal, la pestaña Identity del WebApp debe mostrar “System assigned: On”.
  2. Token de Graph – Desde la Function, ejecuta az account get-access-token --resource https://graph.microsoft.com y verifica que el scp incluya los permisos otorgados.
  3. Cache poblada – Consulta la tabla GovCache con Azure Storage Explorer; debería haber filas bajo applications y servicePrincipals.
  4. UI funcional – Accede a https://govportal-<unique>.azurewebsites.net; la tabla debe cargar en menos de 2 s y los filtros deben reflejar los datos del cache.
  5. Alertas – Si configuraste SendGrid, verifica que recibes un correo cuando una credencial tenga menos de 7 días para expirar.

Notas adicionales

  • Versiones de token – EasyAuth rechaza tokens v1 cuando la aplicación espera v2. Asegúrate de que la configuración del App Service incluya tokenStore.enabled = true y requestedAccessTokenVersion = 2.
  • Límites de Graph – La llamada GET /servicePrincipals está sujeta a paginación y a un límite de 10 000 objetos por petición. La función de pre‑carga debe manejar @odata.nextLink.
  • Auditoría – Cada solicitud de cambio de propietario se escribe en una cola de Storage; conserva la cola por al menos 30 días para cumplir con requisitos de auditoría.
  • Escalado – Si el número de objetos supera los 10 000, considera usar Azure Cosmos DB como cache en lugar de Table Storage para evitar particiones costosas.
  • Seguridad – Limita el acceso a la UI mediante Azure AD Conditional Access (por ejemplo, solo usuarios con rol Security Reader).

Con este enfoque se obtiene una visión centralizada, de bajo coste y totalmente bajo control del propio tenant, lo que permite ejecutar rutinas de higiene sin depender de licencias premium ni de la UI fragmentada de Entra ID.