Azure App Secret Expiry Alerts

TLDR
  • Built a Python monitoring tool that checks Azure App Registration secrets and certificates for upcoming expiration.
  • Authenticates via service principal or managed identity and queries Microsoft Graph API.
  • Sends alerts through Slack and email with configurable thresholds at 90, 60, 30, 14, and 7 days.
  • Single-file script, no framework dependencies, runs anywhere Python runs - locally, in Azure Automation, or as an AWS Lambda.
334
Lines of code
2
Alert channels
5
Thresholds
0
Dependencies beyond stdlib

The problem

Azure App Registration secrets and certificates expire. When they do, whatever depends on them breaks - authentication flows, API integrations, automated pipelines. The Azure Portal does not send proactive notifications when these credentials are approaching their end dates. You either check manually, or you find out when something stops working in production.

For a tenant with a handful of app registrations, manual tracking is manageable. For anything beyond that, it is not. I wanted something that would check every app registration in the tenant on a daily schedule, compare expiry dates against configurable thresholds, and push alerts to where the team actually sees them - Slack and email.

How it works

The monitor is a single Python script. It authenticates to Azure, pulls all app registrations from the Microsoft Graph API, inspects every secret and certificate on each registration, and flags anything expiring within the configured thresholds. If it finds expiring credentials, it sends alerts. If everything is healthy, it logs that and exits.

  .env config          Microsoft Graph API         Slack webhook
      |                       |                         ^
      v                       v                         |
  monitor.py  ---->  Fetch app registrations  ---->  Send alerts
      |                       |                         |
      v                       v                         v
  Azure Auth          Check expiry dates           SMTP email
  (SP / MI)           against thresholds           delivery

The entire flow runs in seconds. There are no background processes, no state to manage, no database. It is designed to run once, do its job, and exit - which makes it straightforward to schedule on any platform.

Authentication

The script supports two authentication methods: service principal credentials for running outside Azure, and managed identity for running inside Azure. The choice is a single environment variable.

def get_access_token() -> str:
    method = os.getenv("AZURE_AUTH_METHOD", "service_principal").lower()
    scope = "https://graph.microsoft.com/.default"

    if method == "managed_identity":
        credential = ManagedIdentityCredential()
    else:
        credential = ClientSecretCredential(tenant, client_id, client_secret)

    token = credential.get_token(scope)
    return token.token

For service principal authentication, the script needs three values: tenant ID, client ID, and client secret. For managed identity, it needs nothing - Azure handles the credential exchange automatically. In both cases, the identity must have the Application.Read.All Microsoft Graph application permission with admin consent.

Querying Microsoft Graph

The script fetches all app registrations in the tenant through the Microsoft Graph /applications endpoint. It handles pagination automatically - if the tenant has more than 999 registrations, it follows the @odata.nextLink URL until all results are retrieved.

Optional include and exclude filters let you scope monitoring to specific apps by ID or display name pattern. By default, every app registration in the tenant is checked.

Expiry detection

Each app registration can have multiple secrets (passwordCredentials) and certificates (keyCredentials). The script iterates through all of them, computes how many days remain until expiration, and compares against the configured thresholds.

for cred in app.get("passwordCredentials", []):
    end = _parse_date(cred.get("endDateTime"))
    if end is None:
        continue
    days_left = (end - now).days
    if days_left <= max_threshold:
        alerts.append({
            "app_name": app.get("displayName"),
            "credential_type": "Secret",
            "expires": end.strftime("%Y-%m-%d"),
            "days_left": days_left,
        })

The default thresholds are 90, 60, 30, 14, and 7 days. A credential 14 days from expiration will appear in the alert. A credential 91 days out will not. Already-expired credentials are flagged as well, since discovering a silently expired secret is often how teams find out about this problem in the first place.

Notifications

Alerts go out through two channels, both optional and independently configurable.

Slack

Slack notifications use Block Kit for structured formatting. Each alert includes the app name, credential type, app ID, expiry date, and days remaining. Urgency levels - Critical, Warning, or Notice - are assigned based on how close the credential is to expiration. A timestamp footer identifies when the report was generated.

Email

Email alerts are sent via SMTP with STARTTLS. The body is an HTML table with color-coded status indicators: red for credentials expiring within 7 days, amber for 30 days, and blue for longer-range notices. Multiple recipients are supported via a comma-separated list.

Configuration

Everything is driven by environment variables loaded from a .env file. No command-line arguments, no config file format to learn. The repository includes a .env.template with every variable documented.

The core settings cover Azure authentication, optional app filtering, alert thresholds, Slack webhook configuration, and SMTP email settings. Sensible defaults mean the minimal configuration is three values: tenant ID, client ID, and client secret.

Deployment options

The script is designed to run on a schedule. The right deployment depends on what infrastructure you already have.

  • Local or on-premises - Schedule via cron on Linux or Task Scheduler on Windows. The machine needs Python 3.10+, network access to Microsoft Graph, and credentials stored in the .env file.
  • Azure Automation Account - The most natural fit. Upload the script as a Python Runbook, use a managed identity to eliminate stored credentials, and attach a daily schedule. The free tier covers 500 minutes per month, which is more than enough.
  • Azure Functions - Use a timer trigger if you prefer a code-first deployment model. Same managed identity approach for authentication.
  • AWS Lambda + EventBridge - Package the script with its dependencies, create an EventBridge rule for daily execution, and store secrets in AWS Secrets Manager or Parameter Store.

The script itself does not care where it runs. It reads environment variables, makes HTTP requests, and exits. Everything else is a deployment concern.

What I would add next

The current version covers the core use case: know before things break. A few additions that would make it more useful in a larger environment:

  • Microsoft Teams webhook support - Not every team lives in Slack. An Adaptive Card notification would be the Teams-native approach.
  • State tracking - Right now, the script alerts on every run if a credential is within threshold. Tracking what has already been reported would prevent duplicate notifications and allow escalation logic.
  • Multi-tenant support - For managed service providers monitoring multiple Azure AD tenants from a single deployment.

View the project on GitHub

Python monitor for Azure App Registration secret and certificate expiry - Slack and email alerts, configurable thresholds, cloud-ready.

View on GitHub