, , ,

Bloqueio automático de convidados sem MFA no Microsoft Entra ID

Quem gerencia um tenant Microsoft Entra ID com colaboração B2B ativa conhece bem esse ciclo: o time de negócio convida um fornecedor, o convite fica pendente por dias, o Guest aceita, mas nunca completa o registro de MFA. Alguns esquecem, outros usam apenas o e-mail pontualmente e seguem a vida — e a conta continua ativa no seu tenant, autenticada só por senha, com acesso a Teams, SharePoint e, em muitos casos, aplicações internas. Cada Guest sem MFA é uma porta lateral: credencial fraca em uma identidade que já está dentro do seu diretório.

Para resolver essa demanda clássica de governança de identidades externasdesenvolvi com apoio do GitHub Copilot e disponibilizei em meu GitHub um Runbook de Azure Automation em PowerShell que faz a varredura periódica de um grupo específico de convidados, valida quem realmente tem MFA registrado, bloqueia quem está fora da política (ou apenas simula, em modo DryRun) e envia um relatório em Excel ou CSV por e-mail via Microsoft Graph.

A cada execução, a automação responde a três perguntas que todo time de segurança deveria conseguir responder a qualquer momento:

  1. Quais convidados do grupo monitorado não registraram MFA dentro da janela definida?
  2. O que aconteceu com cada um: foi bloqueadosimulado (DryRun) ou ignorado por ainda estar com convite pendente?
  3. Qual é a lista consolidada — pronta para auditoria — que precisa chegar na caixa do SecOps hoje?

Por que filtrar por grupo em vez de sufixo de UPN?

A primeira tentação, quando se precisa isolar convidados em um tenant, é filtrar por sufixo do UPN (#EXT#) ou por domínio. Funciona… até parar de funcionar. Basta o negócio começar a receber Guests de vários parceiros, ou fazer a colaboração via cross-tenant sync, para o filtro por string virar um pesadelo de manutenção — e ainda deixar passar exceções que deveriam estar isentas do bloqueio.

A segunda tentação é tratar todos os Guests do tenant. Esse caminho é ainda pior: contas de suporte de fornecedor estratégico, auditores externos ou parceiros em fase de onboarding acabam sendo bloqueados no primeiro job. Não dá.

A solução do runbook aposta em algo simples e auditável: membership em grupo do Entra ID como fonte de verdade sobre quem entra no escopo do bloqueio.

AbordagemComo funcionaProblema / Vantagem
Filtro por sufixo de UPNString matching em #EXT# ou domínio❌ Frágil, sem exceção fácil, mistura convidados legítimos
Todos os Guests do tenantConsulta ampla via Graph❌ Sem granularidade; bloqueia parceiros estratégicos
Grupo dedicado do Entra IDMembership em grupo atribuído ou dinâmico✅ Escopo explícito, versionável, exceções fáceis

Como bônus, o mesmo runbook suporta grupos atribuídos (você controla quem entra) e grupos dinâmicos (uma regra tipo (user.userType -eq "Guest") and (user.companyName -eq "AcmeCorp") mantém a lista viva sozinha). O código não muda.

Pré-requisitos

ItemRequisito
Subscription AzureAtiva, com permissão Owner ou Contributor para criar Automation Account
Microsoft Entra IDTenant com grupo de segurança contendo os convidados a monitorar
Conta executoraGlobal Administrator ou Privileged Role Administrator (para conceder permissões de aplicação no Graph)
Mailbox de envioCaixa Exchange Online com licença ativa (E1/E3/E5 ou Exchange Plan)
Módulos PowerShellMicrosoft.Graph.* na Automation Account (opcional: ImportExcel para XLSX)
RuntimePowerShell 7.2 ou 5.1, conforme compatibilidade do ambiente

Para organizar todos os recursos da solução, comecei criando um Resource Group dedicado — no exemplo, rg-governance. Manter Automation Account, identidade gerenciada e agendamento sob o mesmo grupo facilita o controle de custo, o RBAC e a limpeza futura.

Guia de implantação passo a passo

1. Criar a Automation Account

No portal do Azure, vá em Create a resource → Automation. Aponte para o Resource Group da solução e escolha a região — no exemplo, aa-disablegueusers, alinhado à convenção de nomes da conta.

Na aba Advanced, garanta que a Managed Identity → System assigned esteja habilitada. É ela que autentica o runbook sem armazenar nenhuma credencial no código.

Alternativa via Azure CLI:

az automation account create \
--name aa-disablegueusers \
--resource-group rg-governance \
--location eastus \
--sku Basic \
--assign-identity

2. Anotar o Object ID da Managed Identity

Após a criação, abra o recurso e vá em Account Settings → Identity → System assigned. Copie o Object (principal) ID — ele será usado para conceder as permissões do Graph nos próximos passos.

3. Instalar os módulos PowerShell

O runbook depende de módulos do Microsoft Graph SDKA ordem de importação importa por causa das dependências internas do Graph e o runtime precisa bater com o do runbook. Instale nesta sequência, aguardando cada módulo ficar Available antes do próximo:

  1. Microsoft.Graph.Authentication
  2. Microsoft.Graph.Users
  3. Microsoft.Graph.Groups
  4. Microsoft.Graph.Reports
  5. Microsoft.Graph.Users.Actions
  6. ImportExcel (opcional, para gerar XLSX)

No portal, o caminho é Automation Account → Shared Resources → Modules → + Add a module, escolhendo PowerShell Gallery como origem.

Obs.: Importe todos os módulos na sequência

Se preferir automatizar, dá para importar todos os módulos via PowerShell — o método é mais confiável em lote, e o próprio repositório traz um snippet pronto:

# --- Parâmetros ---
$resourceGroupName = "<SEU_RESOURCE_GROUP>"
$automationAccountName = "<SUA_AUTOMATION_ACCOUNT>"
$runtimeVersion = "7.2" # ou "5.1"
# Função auxiliar para importar e aguardar conclusão
function Import-AAModule {
param($Name, $Wait = $true)
$uri = "https://www.powershellgallery.com/api/v2/package/$Name"
Write-Output "Importando $Name ..."
New-AzAutomationModule `
-ResourceGroupName $resourceGroupName `
-AutomationAccountName $automationAccountName `
-Name $Name `
-ContentLinkUri $uri `
-RuntimeVersion $runtimeVersion | Out-Null
if ($Wait) {
do {
Start-Sleep -Seconds 20
$m = Get-AzAutomationModule `
-ResourceGroupName $resourceGroupName `
-AutomationAccountName $automationAccountName `
-Name $Name `
-RuntimeVersion $runtimeVersion
Write-Output " $Name -> $($m.ProvisioningState)"
} while ($m.ProvisioningState -notin @("Succeeded","Failed"))
if ($m.ProvisioningState -eq "Failed") {
throw "Falha ao importar $Name"
}
}
}
Import-AAModule -Name "Microsoft.Graph.Authentication" -Wait $true
Import-AAModule -Name "Microsoft.Graph.Users"
Import-AAModule -Name "Microsoft.Graph.Groups"
Import-AAModule -Name "Microsoft.Graph.Reports"
Import-AAModule -Name "Microsoft.Graph.Users.Actions"
Import-AAModule -Name "ImportExcel"
Write-Output "Concluído."

Tempo estimado: 10 a 15 minutos para os cinco módulos do Graph. Não pule a espera — se um deles ainda estiver Creating quando o próximo começar, é comum ver o runbook falhar depois com Connect-MgGraph is not recognized.

4. Conceder as permissões do Graph à Managed Identity

A Managed Identity precisa de cinco permissões de aplicação no Microsoft Graph para operar:

PermissãoTipoPara que serve
GroupMember.Read.AllGraph (App)Ler os membros do grupo de Guests
User.ReadWrite.AllGraph (App)Alterar accountEnabled = false nos convidados
UserAuthenticationMethod.Read.AllGraph (App)Verificar métodos de MFA por usuário
Mail.SendGraph (App)Enviar o relatório via Graph sendMail
AuditLog.Read.AllGraph (App)Reserva operacional (troubleshooting)

Abra o Cloud Shell, selecione PowerShell e execute o script abaixo, substituindo o nome da Automation Account:

Cloud Shell aberto em modo PowerShell no portal do Azure
Cloud Shell em modo PowerShell — sem necessidade de instalar módulo local.
# Ajustar para o nome real da sua Automation Account
$miObjectId = (Get-AzADServicePrincipal -DisplayName "aa-disablegueusers").Id
Write-Output "Managed Identity Object ID: $miObjectId"
# Obter o Service Principal do Microsoft Graph
$graphSP = Get-AzADServicePrincipal -Filter "displayName eq 'Microsoft Graph'"
# Permissões necessárias
$permissions = @(
"GroupMember.Read.All",
"AuditLog.Read.All",
"Mail.Send",
"User.ReadWrite.All",
"UserAuthenticationMethod.Read.All"
)
foreach ($permName in $permissions) {
$role = $graphSP.AppRole | Where-Object { $_.Value -eq $permName }
if ($role) {
try {
New-AzADServicePrincipalAppRoleAssignment `
-ServicePrincipalId $miObjectId `
-ResourceId $graphSP.Id `
-AppRoleId $role.Id
Write-Output "OK: $permName"
} catch {
Write-Output "Já existe ou falhou: $permName - $($_.Exception.Message)"
}
} else {
Write-Output "Role não encontrada: $permName"
}
}

Para conferir visualmente, vá em Entra ID → Enterprise Applications → All applications → Application Type = Managed Identities → aa-disablegueusers → Permissions. As cinco permissões devem estar listadas.

Atenção ao Mail.Send: como permissão de aplicação, ele permite enviar como qualquer caixa do tenant. Em produção, aplique uma Application Access Policy do Exchange Online para restringir o envio à mailbox específica da automação.

Propagação: permissões recém-concedidas podem levar 15 a 60 minutos para refletir no token da Managed Identity. Se o primeiro teste retornar Forbidden, aguarde e tente de novo.

5. Criar o grupo de Guests monitorado

O runbook lê membros de um grupo de segurança do Entra ID — atribuído ou dinâmico. Meu conselho: sempre que possível, prefira o dinâmico, para não depender de disciplina manual da equipe de service desk.

Em Entra ID → Groups → + New group, preencha:

  • Group type: Security
  • Group name: GR-UsersGuest
  • Membership type: Dynamic User

Na regra dinâmica, use:

(user.userType -eq "Guest")

Ou, se quiser segmentar por parceiro/domínio (recomendado, para ter granularidade):

(user.userType -eq "Guest") and (user.userPrincipalName -contains "fornecedor.com")
Editor da regra dinâmica com a query filtrando userType Guest
Regra dinâmica: convidados entram e saem do grupo automaticamente.

Após salvar, abra o grupo e copie o Object ID — é o valor que vai no parâmetro GuestGroupId do runbook.

6. Importar e publicar o Runbook

Ainda no Automation Account, vá em Process Automation → Runbooks → + Create a runbook.

Preencha o formulário:

  • Name: RumGuestDisable
  • Runbook type: PowerShell
  • Runtime version: 5.1 (ou 7.2, conforme a versão dos módulos que você instalou)

No editor, cole o conteúdo de Runbook-GuestsSemMfa.ps1 e ajuste os defaults do bloco param() com os valores do seu ambiente:

param(
[int] $HoursWithoutMfa = 24,
[bool] $SkipPending = $false,
[string] $GuestGroupId = "<COLE-O-OBJECT-ID-DO-GRUPO-AQUI>",
[string] $SenderUpn = "noreply@seudominio.com",
[string] $To = "secops@seudominio.com; gestao@seudominio.com",
[string] $Cc = "",
[string] $Subject = "Relatório - Guests bloqueados sem MFA",
[bool] $DryRun = $false
)

Salve (Ctrl+S) e clique em Publish.

Bloqueio ocorrerá após a conta esta habilitada por 24 horas, caso não habilite o MFA neste período, a conta recebe a desativação

7. Validação em modo DryRun

Nunca deixe o primeiro teste em produção real. Abra o Test pane, ajuste DryRun = True e HoursWithoutMfa = 1 (para forçar o job a “ver” convidados recentes) e clique em Start.

ParâmetroValor de teste
HoursWithoutMfa1
SkipPendingFalse
GuestGroupId(default já configurado)
SenderUpn(default já configurado)
To(default já configurado)
DryRunTrue ← importante

A saída esperada segue mais ou menos esta ordem:

=== Início do runbook ... ===
Janela sem MFA: 1 h | SkipPending: False | DryRun: True
Conectando ao Microsoft Graph com Managed Identity...
Data/hora de corte: ...
Listando convidados (Guest) do tenant...
Buscando membros do grupo '...'...
Total de convidados considerados após filtro: N
Verificando métodos de autenticação de cada convidado...
SEM MFA: usuario1@...
COM MFA: usuario2@... (2 método(s))
Candidatos a bloqueio (sem MFA e dentro da janela): X
Total processado: X | Bloqueados/Simulados: X
Relatório gerado (CSV): ...
E-mail enviado a partir de '...'.
=== Fim do runbook ... ===

Rumbook publicado:

E o relatório chega na caixa de entrada — CSV ou XLSX, dependendo se o ImportExcel foi instalado — pronto para abrir no Excel e circular com o time.

Confira também a pasta de spam — domínios sem SPF/DKIM completos costumam cair em spam no Hotmail/Gmail no primeiro envio.

8. Agendar a execução em produção

Com o DryRun aprovado, vá em Shared Resources → Schedules → + Add a schedule e crie um agendamento diário — no exemplo, Daily-02h-BRT, começando às 02:00 no fuso de Brasília.

Depois, vincule o schedule ao runbook em Runbook → Schedules → + Add a schedule e configure os parâmetros de produção — desta vez com DryRun = FalseSkipPending = True e HoursWithoutMfa = 24.

9. Alertas para falhas do próprio job (opcional, mas recomendado)

Automação que quebra em silêncio é pior do que não ter automação. Crie um alerta em Monitor → Alerts → Create alert rule apontando para o Automation Account, com o sinal Total Jobs, dimensão Status = Failed, threshold > 0. Vincule a um Action Group com o e-mail do time SecOps.

Como o script funciona (por dentro)

Cada bloco do Runbook-GuestsSemMfa.ps1 tem uma responsabilidade isolada:

BlocoO que faz
1. Connect-MgGraph -IdentityAutentica como Managed Identity no Graph, sem segredos em código
2. Listagem do grupoGet-MgGroupMember traz os convidados do GuestGroupId — funciona para grupos atribuídos e dinâmicos
3. Filtro de janelaSó entra na análise quem foi criado há mais de HoursWithoutMfa horas
4. Verificação de MFAInvoke-MgGraphRequest per-user em /users/{id}/authentication/methods — evita falsos negativos do relatório agregado
5. Ação (bloqueio ou simulação)Se DryRun=False, aplica accountEnabled=false; se True, apenas marca como Simulado
6. Relatório CSV/XLSXImportExcel quando disponível; CSV como fallback (separador ; e BOM UTF-8, Excel PT-BR friendly)
7. sendMail via GraphInvoke-MgGraphRequest + ConvertTo-Json -Depth 20 para serializar o payload sem quebrar

Vale destacar duas particularidades técnicas que costumam tropeçar quem reproduz esse padrão. Primeiro, a verificação de MFA per-user em vez do relatório agregado Get-MgReportAuthenticationMethodUserRegistrationDetail: o relatório é eventualmente consistente, com atraso de horas — o suficiente para bloquear um Guest que acabou de registrar MFA e sair mal na foto. Consultar /authentication/methods direto no usuário custa uma chamada a mais por convidado, mas dá o estado real. Segundo, o envio via Invoke-MgGraphRequest em vez de Send-MgUserMail: o cmdlet SDK serializa o payload de forma inconsistente em alguns runtimes e devolve o clássico StartObject error — usar REST direto elimina o problema.

Resolução de problemas

SintomaCausa provávelSolução
Connect-MgGraph is not recognizedMódulos ainda em Creating ou runtime erradoConfirme Available em todos e o runtime do runbook igual ao dos módulos
403 Forbidden logo na primeira chamadaPermissões da MI ainda não propagaramAguarde 15–60 min após o New-AzADServicePrincipalAppRoleAssignment
sendMail → ForbiddenMail.Send não estava no token cacheado pelo Connect-MgGraphRode o runbook novamente após a propagação; se persistir, pegue token fresco via REST
sendMail → MailboxNotEnabledForRESTAPIRemetente sem licença Exchange OnlineAtribua E1/E3/E5 ao SenderUpn ou use uma caixa compartilhada licenciada
400 Bad Request no sendMailPayload JSON inválido (estrutura ou -Depth baixo)Confirme o ConvertTo-Json -Depth 20 e a estrutura message.toRecipients[].emailAddress.address
Relatório sai como CSV mesmo com Excel esperadoImportExcel não instaladoInstale o módulo opcional na Automation Account
Test pane trava em [switch]Runbook usando [switch] em vez de [bool]Use [bool] $DryRun; o Test pane não lida bem com [switch]
E-mail cai no spamDomínio remetente sem SPF/DKIM/DMARCConfigure os registros DNS do domínio do SenderUpn

Boas práticas de produção

Antes de levar isso para o ambiente real, alguns cuidados fazem diferença. Sempre valide primeiro com DryRun=True, especialmente após qualquer mudança de escopo do grupo — nada pior do que bloquear parceiro estratégico no primeiro job de produção. Restrinja o Mail.Send com uma Application Access Policy no Exchange Online, limitando a Managed Identity a enviar apenas pela mailbox de automação; sem isso, ela tecnicamente pode enviar como qualquer caixa do tenant. Marque SkipPending=True em produção — convite pendente é status transitório, não falha de política. Agende fora do horário comercial e configure alerta em cima do Total Jobs Failed, para que uma quebra silenciosa não vire dívida acumulada. Revise trimestralmente as permissões da MI, e complemente a governança com Access Reviews do próprio Entra ID e políticas de Conditional Access exigindo MFA para Guests — o runbook é o mecanismo de enforcement de última milha, não substitui o controle preventivo.

Contribua com a comunidade!

Este repositório é open-source (licença MIT) e focado na evolução contínua dos profissionais de nuvem. Se você identificar uma melhoria — suporte a múltiplos grupos, integração com um webhook de ITSM para abrir ticket automaticamente antes do bloqueio, exportação para uma tabela em Application Insights / Sentinel, ou até uma variante que notifica o convidado por e-mail antes do bloqueio dando 24 h para registrar MFA — fique à vontade para abrir uma Issue ou enviar um Pull Request.

Se este projeto te ajudou a fortalecer a governança de identidades externas do seu tenant, acesse o link github.com/ErickMedeiros/runbookGuestsSemMfa, deixe uma ⭐ e ajude a fazer com que ele chegue a mais profissionais!

Forte abraço pessoal 👊