Vicen Moreno

Pro Googler

Follow me on GitHub

Azure AD Access Reviews - Gobierno automático de permisos

Cuando necesitas revisar quién tiene acceso a qué, trimestre tras trimestre

El problema

Organización con 2000+ empleados y colaboradores externos. Permisos otorgados pero nunca revocados.

Auditoría reveló:

  • 200+ empleados ex-empleados aún con acceso
  • Consultores con acceso meses después de terminar proyecto
  • Grupos con 50+ miembros, nadie sabía por qué
  • Acceso a aplicaciones críticas sin justificación

Solución: Azure AD Access Reviews automáticas.

¿Qué son Access Reviews?

Procesos periódicos donde owners/managers revisan y aprueban o remueven acceso de usuarios.

Trimestre → Access Review created
          → Reviewers notificados
          → Reviewers aprueban/niegan accesos
          → Accesos denegados se remueven automáticamente

Caso 1: Review de grupos de Azure AD

Crear Access Review para grupo

Azure AD → Identity Governance → Access reviews → New access review

Nombre: Quarterly Review - VPN Access Group
Descripción: Review usuarios con acceso VPN

Start date: 2024-01-01
Frequency: Quarterly
Duration: 14 days

Scope:
  - Users and groups
  - Select group: VPN-Users

Reviewers:
  - Group owner(s)
  - Fallback: IT Security Team

Settings:
  - Upon completion → Auto apply results to resource
  - If reviewers don't respond → Remove access
  - Enable reviewer decision helpers → Show last sign-in date

Via PowerShell (automatizar creación)

Install-Module Microsoft.Graph.Identity.Governance

Connect-MgGraph -Scopes "AccessReview.ReadWrite.All"

$params = @{
    displayName = "Quarterly VPN Access Review"
    descriptionForAdmins = "Review VPN access quarterly"
    descriptionForReviewers = "Please review if these users still need VPN access"
    scope = @{
        "@odata.type" = "#microsoft.graph.accessReviewQueryScope"
        query = "/groups/[group-id]/members"
        queryType = "MicrosoftGraph"
    }
    reviewers = @(
        @{
            query = "/groups/[group-id]/owners"
            queryType = "MicrosoftGraph"
        }
    )
    settings = @{
        recurrence = @{
            pattern = @{
                type = "absoluteMonthly"
                interval = 3
            }
            range = @{
                type = "noEnd"
                startDate = "2024-01-01"
            }
        }
        instanceDurationInDays = 14
        autoApplyDecisionsEnabled = $true
        defaultDecisionEnabled = $true
        defaultDecision = "Deny"
        recommendationsEnabled = $true
    }
}

New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $params

Caso 2: Review de App Roles

Revisar quién tiene roles de aplicaciones (admin, contributor, etc.):

$params = @{
    displayName = "Review of Application Administrators"
    scope = @{
        "@odata.type" = "#microsoft.graph.accessReviewQueryScope"
        query = "/servicePrincipals/[app-id]/appRoleAssignedTo"
        queryType = "MicrosoftGraph"
    }
    reviewers = @(
        @{
            query = "./manager"
            queryType = "MicrosoftGraph"
        }
    )
    fallbackReviewers = @(
        @{
            query = "/users/[security-admin-id]"
            queryType = "MicrosoftGraph"
        }
    )
    settings = @{
        instanceDurationInDays = 7
        recurrence = @{
            pattern = @{
                type = "absoluteMonthly"
                interval = 6
            }
        }
        autoApplyDecisionsEnabled = $true
        defaultDecisionEnabled = $true
        defaultDecision = "Deny"
        justificationRequiredOnApproval = $true
    }
}

New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $params

Caso 3: Review de Guest Users

Revisar usuarios externos (B2B):

Access review settings:
  Scope: All guest users
  Reviewers: Resource owners
  Frequency: Monthly
  Duration: 7 days
  Auto-apply: Yes
  Default decision: Remove access
  Require justification: Yes
$params = @{
    displayName = "Monthly Guest User Review"
    scope = @{
        "@odata.type" = "#microsoft.graph.accessReviewQueryScope"
        query = "/users?$filter=userType eq 'Guest'"
        queryType = "MicrosoftGraph"
    }
    reviewers = @(
        @{
            query = "/users/[sponsor-id]"  # Usuario que invitó
            queryType = "MicrosoftGraph"
        }
    )
    settings = @{
        instanceDurationInDays = 7
        recurrence = @{
            pattern = @{
                type = "absoluteMonthly"
                interval = 1
            }
        }
        autoApplyDecisionsEnabled = $true
        defaultDecisionEnabled = $true
        defaultDecision = "Deny"
        justificationRequiredOnApproval = $true
        recommendationsEnabled = $true
    }
}

Workflow del Reviewer

1. Notificación por email

Subject: [Action Required] Access Review: VPN Access Group

You have been assigned to review access for the VPN Access Group.

Review period: Jan 1 - Jan 14, 2024

[Start Review Button]

2. Portal de review

Access Reviews → My Access Reviews → VPN Access Group

Users to review (20):
┌──────────────────────────────────────────────────────────┐
│ Juan Pérez                                               │
│ Email: juan@empresa.com                                  │
│ Last sign-in: 2 days ago                                 │
│ Recommendation: Approve (active user)                    │
│                                                           │
│ [Approve] [Deny] [Don't know]                            │
│ Justification: _____________________________             │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│ María González                                            │
│ Email: maria@empresa.com                                  │
│ Last sign-in: Never                                       │
│ Recommendation: Deny (inactive user)                      │
│                                                           │
│ [Approve] [Deny] [Don't know]                            │
│ Justification: _____________________________             │
└──────────────────────────────────────────────────────────┘

3. Decisiones bulk

// Script para aprobar/denegar en bulk via Graph API
const reviewDecisions = [
  { userId: 'user1-id', decision: 'Approve', justification: 'Active employee' },
  { userId: 'user2-id', decision: 'Deny', justification: 'No longer needs access' },
];

for (const decision of reviewDecisions) {
  await graphClient
    .identityGovernance
    .accessReviews
    .definitions(reviewDefinitionId)
    .instances(instanceId)
    .decisions(decision.userId)
    .patch({
      decision: decision.decision,
      justification: decision.justification,
    });
}

Decisiones automáticas

Baseline: Remover usuarios inactivos

settings = @{
    defaultDecisionEnabled = $true
    defaultDecision = "Deny"  # Si reviewer no responde
    inactiveUserRecommendationsEnabled = $true
    inactiveUserRecommendationsDays = 90  # Inactivos >90 días → Deny
}

Machine Learning recommendations

Azure AD analiza patrones de acceso:

User: Juan
Last sign-in to this app: 120 days ago
Peers with similar role: 95% don't have this access
ML Recommendation: DENY
settings = @{
    recommendationsEnabled = $true
    recommendationLookBackDuration = "P30D"  # 30 días de lookback
}

Notificaciones customizadas

Email templates

# Personalizar emails
$params = @{
    displayName = "Custom Access Review"
    settings = @{
        mailNotificationsEnabled = $true
        reminderNotificationsEnabled = $true
        applyActions = @(
            @{
                "@odata.type" = "#microsoft.graph.removeAccessApplyAction"
            }
        )
    }
    additionalNotificationRecipients = @(
        @{
            notificationRecipients = @(
                @{
                    emailAddress = "security-team@empresa.com"
                }
            )
            notificationTemplateType = "CompletedAdditionalRecipients"
        }
    )
}

Reminders

Day 1: Initial notification
Day 7: Reminder (50% completion)
Day 12: Final reminder (deadline approaching)
Day 14: Review closes
Day 15: Auto-apply decisions

Reporting y Analytics

Ver resultados de reviews

# Obtener resultados de access review
$reviewId = "[review-id]"

$decisions = Get-MgIdentityGovernanceAccessReviewDefinitionInstanceDecision `
    -AccessReviewScheduleDefinitionId $reviewId `
    -AccessReviewInstanceId $instanceId

foreach ($decision in $decisions) {
    [PSCustomObject]@{
        User = $decision.Principal.DisplayName
        Decision = $decision.Decision
        Justification = $decision.Justification
        ReviewedBy = $decision.ReviewedBy.DisplayName
        ReviewedDate = $decision.ReviewedDateTime
    }
}

Dashboard en Power BI

// Query para Azure Monitor
AuditLogs
| where OperationName startswith "Review access"
| summarize
    Approved = countif(Result == "success" and Properties.decision == "Approve"),
    Denied = countif(Result == "success" and Properties.decision == "Deny"),
    NotReviewed = countif(Result == "timeout")
  by bin(TimeGenerated, 1d), ReviewName = tostring(Properties.reviewName)
| render timechart

Automatización post-review

Remover usuario de todos los grupos si denegado

# Trigger cuando access review completa
$webhook = Register-AzureRMEventHubConsumerGroup ... # Configurar webhook

# Handler
function Handle-AccessReviewCompleted {
    param($reviewResults)

    foreach ($result in $reviewResults) {
        if ($result.Decision -eq "Deny") {
            $userId = $result.UserId

            # Remover de TODOS los grupos
            $groups = Get-MgUserMemberOf -UserId $userId

            foreach ($group in $groups) {
                Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $userId
                Write-Log "Removed $userId from group $($group.DisplayName)"
            }

            # Deshabilitar cuenta si es guest
            $user = Get-MgUser -UserId $userId
            if ($user.UserType -eq "Guest") {
                Update-MgUser -UserId $userId -AccountEnabled:$false
            }

            # Notificar
            Send-MailMessage -To $user.Mail -Subject "Access Removed" `
                -Body "Your access has been removed following access review."
        }
    }
}

Integration con Conditional Access

Bloquear acceso si review pendiente:

# Crear CA policy
New-AzureADMSConditionalAccessPolicy -DisplayName "Block if Access Review Pending" `
    -Conditions @{
        Users = @{
            IncludeUsers = "All"
        }
        Applications = @{
            IncludeApplications = "app-id"
        }
    } `
    -GrantControls @{
        Operator = "AND"
        BuiltInControls = @("mfa")
        CustomAuthenticationFactors = @()
        TermsOfUse = @()
    } `
    -SessionControls @{
        SignInFrequency = @{
            Value = 1
            Type = "hours"
            IsEnabled = $true
        }
    }

Mejores prácticas

1. Frecuencia apropiada

  • Grupos críticos (admins, finance): Mensual
  • Grupos estándar: Trimestral
  • Guest users: Mensual
  • App roles: Semestral

2. Reviewers correctos

Group owners → Saben quién necesita acceso
Managers → Para acceso de empleados
Resource owners → Para aplicaciones específicas
Fallback → Security team (si owner no responde)

3. Enable recommendations

ML recommendations mejoran accuracy en 40%:

recommendationsEnabled = $true

4. Require justification

justificationRequiredOnApproval = $true

Auditoría completa de por qué se aprobó acceso.

5. Auto-apply decisions

autoApplyDecisionsEnabled = $true

Sin esto, decisiones son manuales (no escala).

Monitoreo

// Access reviews no completados a tiempo
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName == "Complete access review"
| where Result == "timeout"
| summarize count() by ReviewName = tostring(Properties.reviewName)
| order by count_ desc

Alerta si completion rate <80%:

# Alert rule
$condition = @{
    query = "AuditLogs | where OperationName == 'Complete access review' | summarize CompletionRate = countif(Result == 'success') * 100.0 / count()"
    threshold = 80
    operator = "LessThan"
}

Resultados

Antes (sin Access Reviews):

  • 200+ cuentas zombies
  • 0 revisión de permisos
  • Compliance audits fallaban
  • Riesgo de seguridad alto

Después (con Access Reviews):

  • 95% de cuentas activas (200 removidas automáticamente)
  • Revisión trimestral automática
  • Compliance audits passing
  • Reducción de ataque surface 40%

Tiempo de IT:

  • Setup inicial: 8 horas
  • Mantenimiento: 2 horas/trimestre
  • ROI positivo desde trimestre 2

¿Usas Access Reviews en tu organización? ¿Qué frecuencia funciona mejor?


 Anterior      Posterior

Por Vicente José Moreno Escobar el 12 de noviembre de 2021
Archivado en: Azure   Azure AD   Governance



Puedes disfrutar de otros artículos como éste en el archivo del sitio.