EvolitBlogKontakt

Wygaśnięcie sekretów App Registration w Entra ID: jak nie dać się zaskoczyć w produkcji

Sekrety klienta App Registration wygasają bez e-mail alertu — gdy termin minie, wszystkie integracje padają natychmiast. Pokazuję jak skryptem PowerShell monitorować wygasające poświadczenia w całym tenancie i jak bezpiecznie rotować sekrety bez okna awarii.

Wygaśnięcie sekretów App Registration w Entra ID: jak nie dać się zaskoczyć w produkcji

TL;DR: Sekrety klienta i certyfikaty App Registration w Entra ID wygasają bez domyślnego powiadomienia e-mail — gdy termin minie, wszystkie integracje przestają działać natychmiast. W artykule pokazuję jak skryptem PowerShell wyciągnąć raport wygasających poświadczeń z całego tenanta, jak bezpiecznie rotować sekrety bez okna awarii oraz jak wymusić politykę zarządzania poświadczeniami dla nowych aplikacji.

Problem

Scenariusz zna każdy administrator M365 o stażu powyżej roku: poniedziałkowy ranek, telefony od użytkowników że "coś nie działa", przeglądasz logi w Entra i widzisz błąd AADSTS7000215: Invalid client secret provided. Aplikacja działała przez miesiące bez problemu. Co się stało? Sekret klienta App Registration wygasł w niedzielę wieczór.

W środowiskach Microsoft 365 App Registration to mechanizm który pozwala aplikacjom i automatyzacjom uwierzytelniać się w Entra ID i uzyskiwać dostęp do Microsoft Graph, Exchange Online, SharePoint czy Teams. Każda taka rejestracja może mieć poświadczenia w dwóch formach: sekrety klienta (ciągi tekstowe, tzw. client secrets) lub certyfikaty (klucze asymetryczne X.509). Obie formy mają datę wygaśnięcia. Żadna domyślnie nie generuje e-maila alertowego.

W większych organizacjach liczba App Registrations rośnie szybciej niż ktokolwiek planował. Każda integracja z zewnętrznym systemem, każdy skrypt PowerShell używający Graph API, każdy konektor Power Automate, każda aplikacja Azure Functions czy Logic App — wszystkie mogą potrzebować własnej rejestracji z poświadczeniami. W tenancie z kilkuset użytkownikami łatwo skończyć z 40-80 rejestracjami, w środowiskach MSP zarządzających wieloma klientami — setkami.

Problem pogłębia historia samego mechanizmu. Przez lata administratorzy ustawiali sekrety na "Never Expire" — co technicznie oznaczało datę 31 grudnia 2299. Brzmiało bezpiecznie. Ale po zmianie polityki Microsoftu i przy braku żadnych alertów, dziesiątki organizacji odkryły problem dopiero gdy aplikacja przestała działać.

Dlaczego tak się dzieje

Historia ograniczeń czasowych sekretów

Microsoft przez lata zmieniał politykę dotyczącą czasów życia sekretów:

  • Przed marcem 2021: opcja "Never Expire" dostępna w portalu. Technicznie ustawiała datę wygaśnięcia na rok 2299.
  • Marzec/Kwiecień 2021: Microsoft usunął opcję "Never Expire" z portalu Azure. Nowe sekrety wymagały konkretnej daty.
  • Luty 2022: oficjalne ogłoszenie że maksymalny czas życia nowych sekretów to 2 lata. Próba ustawienia dłuższego terminu przez portal powoduje błąd. Przez API wciąż można było obejść limit — ale Microsoft zapowiedział zamknięcie tej furtki.

Organizacje które migrowały z on-premise AD lub budowały integracje przed 2021 rokiem mogą mieć w tenantach sekrety z datą wygaśnięcia 12/31/2299. Wciąż działają, ale są to zapomniane, nigdy nierotowane poświadczenia z pełnym dostępem — dokładnie ten rodzaj ryzyka bezpieczeństwa który Microsoft chciał wyeliminować.

Brak natywnych alertów e-mail

Microsoft nie wysyła żadnego e-maila gdy sekret App Registration jest bliski wygaśnięcia. Jedyny wbudowany mechanizm to zakładka Recommendations w Entra ID portal (entra.microsoft.com > Overview > Recommendations) — ale:

  1. Ostrzega dopiero 30 dni przed terminem wygaśnięcia.
  2. Wymaga aktywnego sprawdzenia portalu — nie ma push notification.
  3. Rekomendacja applicationCredentialExpiry pokazuje tylko App Registrations, nie certyfikaty SAML w Enterprise Applications.
  4. Wymaga licencji Microsoft Entra Workload ID dla pełnego API dostępu.

30 dni to mało czasu jeśli rotacja wymaga koordynacji z właścicielem aplikacji, przejścia przez Change Management i testów regresji. Organizacje które zarządzają tym profesjonalnie potrzebują 60-90 dni ostrzeżenia.

Rozwiązanie krok po kroku

Krok 1: Zainstaluj moduł Microsoft Graph PowerShell

Install-Module Microsoft.Graph -Scope CurrentUser -Force
# Zweryfikuj instalację
Get-InstalledModule Microsoft.Graph | Select-Object Name, Version

Krok 2: Raport wygasających sekretów — wszystkie App Registrations

Poniższy skrypt pobiera wszystkie App Registrations i wyświetla poświadczenia wygasające w ciągu określonej liczby dni. Kluczowy szczegół: parametr -Property jest obowiązkowy — bez passwordCredentials,keyCredentials cmdlet zwróci puste listy nawet gdy poświadczenia istnieją.

Connect-MgGraph -Scopes "Application.Read.All"

$thresholdDays = 60  # Ostrzegaj 60 dni przed wygaśnięciem
$threshold = (Get-Date).AddDays($thresholdDays)
$today = Get-Date

$apps = Get-MgApplication -All -Property "id,displayName,appId,passwordCredentials,keyCredentials"

$report = foreach ($app in $apps) {
    # Sprawdź sekrety klienta (passwordCredentials)
    foreach ($secret in $app.PasswordCredentials) {
        if ($secret.EndDateTime -and $secret.EndDateTime -lt $threshold) {
            [PSCustomObject]@{
                AppName    = $app.DisplayName
                AppId      = $app.AppId
                Type       = "ClientSecret"
                SecretName = $secret.DisplayName
                ExpireDate = $secret.EndDateTime.ToString("yyyy-MM-dd")
                DaysLeft   = [math]::Round(($secret.EndDateTime - $today).TotalDays)
                Status     = if ($secret.EndDateTime -lt $today) { "WYGASŁ" } else { "WYGASA" }
            }
        }
    }
    # Sprawdź certyfikaty (keyCredentials)
    foreach ($cert in $app.KeyCredentials) {
        if ($cert.EndDateTime -and $cert.EndDateTime -lt $threshold) {
            [PSCustomObject]@{
                AppName    = $app.DisplayName
                AppId      = $app.AppId
                Type       = "Certificate"
                SecretName = $cert.DisplayName
                ExpireDate = $cert.EndDateTime.ToString("yyyy-MM-dd")
                DaysLeft   = [math]::Round(($cert.EndDateTime - $today).TotalDays)
                Status     = if ($cert.EndDateTime -lt $today) { "WYGASŁ" } else { "WYGASA" }
            }
        }
    }
}

if ($report) {
    $report | Sort-Object DaysLeft | Format-Table -AutoSize
    Write-Host "Łącznie znaleziono: $($report.Count) wygasających poświadczeń"
} else {
    Write-Host "Brak wygasających poświadczeń w ciągu $thresholdDays dni."
}

Krok 3: Raport certyfikatów SAML — Enterprise Applications

App Registrations to tylko połowa historii. Aplikacje enterprise z sekcji Enterprise Applications (np. integracje SAML z systemami HR, SSO do Salesforce) mają certyfikaty przechowywane jako Service Principals — osobny obiekt w Entra ID. Nie są widoczne przez Get-MgApplication.

Connect-MgGraph -Scopes "Application.Read.All"

$threshold = (Get-Date).AddDays(60)
$today = Get-Date

$sps = Get-MgServicePrincipal -All `
    -Property "id,displayName,appId,keyCredentials,passwordCredentials" `
    -Filter "servicePrincipalType eq Application"

$report = foreach ($sp in $sps) {
    foreach ($cert in $sp.KeyCredentials) {
        if ($cert.EndDateTime -and $cert.EndDateTime -lt $threshold) {
            [PSCustomObject]@{
                AppName    = $sp.DisplayName
                AppId      = $sp.AppId
                Type       = "ServicePrincipal-KeyCredential"
                ExpireDate = $cert.EndDateTime.ToString("yyyy-MM-dd")
                DaysLeft   = [math]::Round(($cert.EndDateTime - $today).TotalDays)
                Status     = if ($cert.EndDateTime -lt $today) { "WYGASŁ" } else { "WYGASA" }
            }
        }
    }
}

$report | Sort-Object DaysLeft | Format-Table -AutoSize

Krok 4: Bezpieczna rotacja sekretu klienta

Rotacja musi następować w ściśle określonej kolejności. Odwrócenie kroków 2 i 3 powoduje przerwę w działaniu aplikacji.

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

$appId = "TWOJ-APP-ID-TUTAJ"  # Użyj appId (nie objectId)

# Pobierz obiekt aplikacji
$app = Get-MgApplication -Filter "appId eq "
Write-Host "Aplikacja: $($app.DisplayName) | Aktualna liczba sekretów: $($app.PasswordCredentials.Count)/48"

# 1. Utwórz nowy sekret
$endDate = (Get-Date).AddMonths(6)
$newSecret = Add-MgApplicationPassword -ApplicationId $app.Id `
    -PasswordCredential @{
        DisplayName = "Rotated-$(Get-Date -Format yyyy-MM-dd)"
        EndDateTime = $endDate
    }

# KRYTYCZNE: Wartość sekretu widoczna jest TYLKO RAZ
Write-Host "=== ZAPISZ TERAZ ==="
Write-Host "Nowy sekret: $($newSecret.SecretText)"
Write-Host "KeyId: $($newSecret.KeyId)"
Write-Host "Wygasa: $($newSecret.EndDateTime)"
Write-Host "===================="

# 2. Zaktualizuj konfigurację aplikacji nowym sekretem
# 3. Przetestuj że aplikacja działa
# 4. Odczekaj minimum 30 minut
# 5. Dopiero wtedy usuń stary sekret:
# Remove-MgApplicationPassword -ApplicationId $app.Id -KeyId "STARY-KEY-ID"

Krok 5: Wymuś politykę zarządzania poświadczeniami

Application Management Policy pozwala ograniczyć tworzenie sekretów klienta lub skrócić maksymalny czas życia certyfikatów dla nowych aplikacji:

Connect-MgGraph -Scopes 'Policy.ReadWrite.All'
Import-Module Microsoft.Graph.Identity.SignIns

$params = @{
    isEnabled = $true
    applicationRestrictions = @{
        keyCredentials = @(
            @{
                restrictionType = "asymmetricKeyLifetime"
                state           = "enabled"
                maxLifetime     = "P180D"  # ISO 8601: 180 dni
                restrictForAppsCreatedAfterDateTime = [DateTime]::Parse("2025-01-01T00:00:00Z")
            }
        )
    }
}

Update-MgPolicyDefaultAppManagementPolicy -BodyParameter $params
Write-Host "Polityka aktywna. Nowe App Registrations (po 2025-01-01) mają max certyfikat 180 dni."

Krok 6: Automatyczne powiadomienia przez Logic Apps

Dla środowisk gdzie cotygodniowy manual to za mało — Logic App z triggerem Recurrence (np. co poniedziałek o 8:00) pobierająca dane z Graph API:

https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials

Kluczowe szczegóły implementacji:

  • Utwórz dedykowaną App Registration z uprawnieniem Application.Read.All (application permissions, nie delegated)
  • Włącz paginację w akcji HTTP — domyślny limit Graph API to 999 rekordów. Dla tenantów z 1000+ App Registrations wymagana obsługa @odata.nextLink
  • Filtruj endDateTime < dzisiaj + 60 dni w pętli For Each
  • Wyślij skonsolidowany e-mail z tabelą zamiast jednego maila per sekret

Typowe pułapki i edge cases

Pułapka 1: Sekrety "2299" — pozornie wieczne, faktycznie zapomniane

W starszych tenantach znajdziesz App Registrations z datą wygaśnięcia 31/12/2299. Technicznie działają. Praktycznie: to poświadczenia nierotowane od 3-5+ lat, stworzone przez programistów którzy już dawno odeszli. Znajdź je:

Connect-MgGraph -Scopes "Application.Read.All"
$legacyDate = Get-Date "2200-01-01"

Get-MgApplication -All -Property "displayName,appId,passwordCredentials" |
    ForEach-Object {
        $app = $_
        $app.PasswordCredentials | Where-Object { $_.EndDateTime -gt $legacyDate } |
        ForEach-Object {
            [PSCustomObject]@{
                App        = $app.DisplayName
                AppId      = $app.AppId
                SecretName = $_.DisplayName
                FakeExpiry = $_.EndDateTime
            }
        }
    }

Dla każdego wyniku: sprawdź w Sign-in logs czy aplikacja jest aktywnie używana, następnie zaplanuj rotację lub decommission całej rejestracji.

Pułapka 2: Limit 48 sekretów na App Registration

Każda App Registration ma limit 48 password credentials. Jeśli przy każdej rotacji dodajesz nowy sekret ale nie usuwasz starego, w pewnym momencie trafisz na ten limit. Kolejna rotacja skończy się błędem InvalidKeyCredentialsCount. Nie ma ostrzeżenia gdy zbliżasz się do limitu.

Skrypt z Kroku 4 wyświetla aktualną liczbę sekretów ($app.PasswordCredentials.Count/48). Każda operacja rotacji powinna kończyć się usunięciem starego sekretu.

Pułapka 3: Wartość sekretu widoczna tylko raz — zapisz do Key Vault

Wartość client secret jest dostępna wyłącznie w momencie tworzenia — zarówno przez portal jak i API. Kolejne odczyty zwracają null. To prowadzi do sytuacji gdzie tworzy się nowy sekret "bo nie wiadomo gdzie jest stary" — co dodaje kolejny wpis do limitu 48.

Jedyne bezpieczne rozwiązanie to Azure Key Vault:

# Po Add-MgApplicationPassword — natychmiast zapisz do Key Vault
$kvName     = "twoj-keyvault"
$secretName = "$($app.DisplayName)-appsecret"
$secureVal  = ConvertTo-SecureString $newSecret.SecretText -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $kvName -Name $secretName -SecretValue $secureVal
Write-Host "Sekret zapisany do Key Vault: $kvName/$secretName"

Pułapka 4: Opóźnienie propagacji po rotacji

Po dodaniu nowego sekretu i zaktualizowaniu konfiguracji aplikacji istnieje okno propagacji — Microsoft dokumentuje do 15 minut zanim nowe poświadczenia będą działać we wszystkich datacenter. Jeśli usuniesz stary sekret zbyt szybko po dodaniu nowego, możesz trafić w moment gdy ani stary (usunięty) ani nowy (jeszcze nie propagowany) nie jest akceptowany.

Bezpieczna strategia: po zweryfikowaniu że nowy sekret działa (sprawdź Sign-in logs w Entra — powinieneś zobaczyć aktywność z nowym Key ID), utrzymaj oba sekrety przez minimum 30-60 minut przed usunięciem starego.

Pułapka 5: Application Management Policy działa tylko prospektywnie

Parametr restrictForAppsCreatedAfterDateTime oznacza że polityka dotyczy tylko aplikacji stworzonych po tej dacie. Istniejące App Registrations mogą nadal dodawać nowe sekrety bez ograniczeń — polityka ich nie blokuje retroaktywnie. Polityka jest cenna dla budowania dobrych nawyków od teraz, ale nie zastępuje audytu istniejących rejestracji.

Alternatywa: Managed Identities jako docelowe rozwiązanie

Najlepszy sekret to taki którego nie trzeba zarządzać. Dla aplikacji działających na Azure (Azure Functions, App Service, Automation Runbooks, Virtual Machines) Managed Identity eliminuje problem całkowicie — aplikacja uwierzytelnia się do Graph API bez żadnego sekretu ani certyfikatu. Microsoft automatycznie rotuje poświadczenia w tle, bez udziału administratora.

Jeśli masz App Registrations używane przez aplikacje Azure, migracja na Managed Identity powinna być na roadmapie. Dla aplikacji zewnętrznych (on-premise, inni dostawcy chmury) sekrety pozostają jedyną opcją — tutaj wdrożenie monitoringu z Kroku 2 jest niezbędne.

Podsumowanie

  • Entra ID nie wysyła e-maili gdy sekret wygasa. Entra Recommendations ostrzega 30 dni wcześniej tylko jeśli aktywnie sprawdzasz portal — za mało na profesjonalne zarządzanie zmianą.
  • Get-MgApplication wymaga jawnego -Property passwordCredentials,keyCredentials — bez tego pola są puste i skrypt nie znajdzie niczego nawet jeśli wszystko wygasa jutro.
  • Service Principals to osobna kwestia — certyfikaty SAML i poświadczenia Enterprise Applications monitorujesz przez Get-MgServicePrincipal, nie przez Get-MgApplication.
  • Rotacja ma kolejność: stwórz nowy sekret → zaktualizuj konfigurację → zweryfikuj w logach → odczekaj 30-60 min → usuń stary.
  • Limit 48 sekretów jest realny i bez ostrzeżeń — czyść stare poświadczenia po każdej rotacji.
  • Application Management Policy ogranicza nowe aplikacje, nie istniejące — nie zastępuje audytu historycznych sekretów.
  • Docelowo: Managed Identity dla aplikacji Azure eliminuje problem całkowicie.