Azure App Secret Expiry Alerts
- 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.
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 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
.envfile. - 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.
Python monitor for Azure App Registration secret and certificate expiry - Slack and email alerts, configurable thresholds, cloud-ready.