A Docker-based service that monitors Microsoft Entra ID (Azure AD) application secrets and certificates for expiration and sends notifications through multiple channels.
Built using Domain-Driven Design (DDD) and Hexagonal Architecture (Ports and Adapters) patterns, following Python 2025 best practices.
- Automatic Monitoring: Scans all app registrations and service principals in your Entra ID tenant
- Service Principal Support: Monitors both App Registration and Service Principal credentials with separate report sections
- Multiple Notification Channels: Email (SMTP), Microsoft Graph Email, Microsoft Teams, Slack, Generic Webhook
- Configurable Thresholds: Critical, Warning, and Info levels
- Flexible Scheduling: Run once or on a cron schedule
- REST API: Optional API for health checks, reports, and on-demand checks (no credential details exposed)
- Clean Architecture: DDD with hexagonal architecture for maintainability
- Type Safety: Full type hints with Python 3.12+ features
- Async/Await: Non-blocking I/O for better performance
- Docker-Ready: Production-ready containerization
This project follows Hexagonal Architecture (Ports and Adapters) with Domain-Driven Design principles:
src/
├── domain/ # Core business logic (no external dependencies)
│ ├── entities/ # Domain entities with identity
│ │ ├── credential.py # Credential entity
│ │ ├── application.py # Application entity
│ │ └── expiration_report.py # Aggregate root
│ ├── value_objects/ # Immutable value objects
│ │ ├── credential_source.py # App Registration vs Service Principal
│ │ ├── credential_type.py
│ │ ├── expiration_status.py
│ │ ├── notification_level.py
│ │ └── thresholds.py
│ ├── services/ # Domain services
│ │ └── expiration_analyzer.py
│ └── exceptions.py # Domain exceptions
│
├── application/ # Application layer (use cases)
│ ├── ports/ # Interfaces (driven/secondary ports)
│ │ ├── credential_repository.py # Port for data retrieval
│ │ └── notification_sender.py # Port for notifications
│ ├── use_cases/ # Application services
│ │ └── check_expiring_credentials.py
│ └── exceptions.py # Application exceptions
│
├── infrastructure/ # Infrastructure layer (adapters)
│ ├── adapters/
│ │ ├── api/ # REST API adapter (FastAPI)
│ │ │ ├── app.py
│ │ │ └── models.py
│ │ ├── entra_id/ # Entra ID adapter (implements CredentialRepository)
│ │ │ ├── graph_client.py
│ │ │ └── repository.py
│ │ └── notifications/ # Notification adapters (implement NotificationSender)
│ │ ├── email.py # SMTP email
│ │ ├── graph_email.py # Microsoft Graph API email
│ │ ├── teams.py
│ │ ├── slack.py
│ │ └── webhook.py
│ └── config/ # Configuration loading
│ └── settings.py
│
└── main.py # Composition root (dependency injection)
- Domain Layer: Pure business logic with no external dependencies. Contains entities, value objects, and domain services.
- Application Layer: Orchestrates use cases using domain objects. Defines ports (interfaces) for external systems.
- Infrastructure Layer: Implements adapters that fulfill the ports. Contains all external system integrations.
- Composition Root: Wires everything together using dependency injection.
-
Microsoft Entra ID App Registration with:
- API Permission:
Microsoft Graph > Application.Read.All(Application) - API Permission:
Microsoft Graph > Mail.Send(Application) - only if using Graph Email - Admin consent granted
- Client secret created
- API Permission:
-
Python 3.12+ (for local development)
-
Docker and Docker Compose (for containerized deployment)
git clone <repository-url>
cd entra-id-secrets-notification
cp .env.example .env
# Edit .env with your Azure credentials and notification settings# Build and run
docker compose up -d
# View logs
docker compose logs -f
# Stop
docker compose down# Set RUN_MODE=once in .env, then:
docker compose up| Variable | Description |
|---|---|
AZURE_TENANT_ID |
Your Entra ID tenant ID |
AZURE_CLIENT_ID |
App registration client ID |
AZURE_CLIENT_SECRET |
App registration client secret |
| Variable | Default | Description |
|---|---|---|
CRITICAL_THRESHOLD_DAYS |
7 | Days for critical alerts |
WARNING_THRESHOLD_DAYS |
30 | Days for warning alerts |
INFO_THRESHOLD_DAYS |
90 | Days for info alerts |
| Variable | Default | Description |
|---|---|---|
MONITOR_SERVICE_PRINCIPALS |
true | Also monitor Service Principal credentials |
Note: Service Principals are the tenant-local instances of applications. They often have their own secrets/certificates that are different from the App Registration. Both are monitored by default and shown in separate sections in notifications.
| Variable | Default | Description |
|---|---|---|
RUN_MODE |
scheduled | once or scheduled |
CRON_SCHEDULE |
0 8 * * * |
Cron expression |
LOG_LEVEL |
INFO | DEBUG, INFO, WARNING, ERROR |
DRY_RUN |
false | Test mode (no notifications) |
SMTP_ENABLED=true
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=user
SMTP_PASSWORD=pass
SMTP_FROM=noreply@example.com
SMTP_TO=admin@example.com
SMTP_USE_TLS=trueTEAMS_ENABLED=true
TEAMS_WEBHOOK_URL=https://outlook.office.com/webhook/...SLACK_ENABLED=true
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...WEBHOOK_ENABLED=true
WEBHOOK_URL=https://your-endpoint.com/notifySend email via Microsoft Graph API. Useful when SMTP is not available or you want to use Microsoft 365 mailboxes.
Note: Requires
Mail.Sendapplication permission with admin consent. If Graph-specific credentials are not set, it uses the main Azure credentials.
GRAPH_EMAIL_ENABLED=true
GRAPH_EMAIL_FROM=notifications@yourdomain.com
GRAPH_EMAIL_TO=admin@example.com,security@example.com
# Optional: Use different credentials than the main app
GRAPH_EMAIL_TENANT_ID=
GRAPH_EMAIL_CLIENT_ID=
GRAPH_EMAIL_CLIENT_SECRET=
# Save sent emails to Sent Items folder
GRAPH_EMAIL_SAVE_TO_SENT=falseEnable the REST API for health checks, summary reports, and on-demand credential checks.
Security Note: The API does NOT expose credential details - only statistics and summaries.
API_ENABLED=true
API_HOST=0.0.0.0
API_PORT=8080| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check - returns service status |
GET |
/api/v1/report |
Get latest report summary (no credential details) |
POST |
/api/v1/check |
Trigger on-demand credential check |
curl http://localhost:8080/health{
"status": "healthy",
"version": "1.1.0",
"timestamp": "2025-01-15T08:00:00Z"
}curl http://localhost:8080/api/v1/report{
"generated_at": "2025-01-15T08:00:00Z",
"notification_level": "warning",
"summary": "5 credentials requiring attention: 1 expired, 2 critical, 2 warning",
"statistics": {
"total_applications": 3,
"total_credentials": 5,
"expired_count": 1,
"critical_count": 2,
"warning_count": 2,
"healthy_count": 0
},
"thresholds": {
"critical_days": 7,
"warning_days": 30,
"info_days": 90
},
"requires_notification": true
}curl -X POST http://localhost:8080/api/v1/checkWhen API is enabled, interactive documentation is available at:
- Swagger UI:
http://localhost:8080/docs - ReDoc:
http://localhost:8080/redoc
For generic webhook integrations:
{
"event_type": "entra_id_secrets_alert",
"timestamp": "2025-01-15T08:00:00+00:00",
"level": "critical",
"summary": "5 credentials requiring attention: 1 expired, 2 critical, 2 warning",
"statistics": {
"total_applications_affected": 3,
"total_credentials": 5,
"expired_count": 1,
"critical_count": 2,
"warning_count": 2,
"healthy_count": 0
},
"credentials": [
{
"application_id": "app-guid",
"application_name": "My Application",
"credential_id": "credential-guid",
"credential_type": "password",
"display_name": "API Key",
"expiry_date": "2025-01-20T00:00:00+00:00",
"days_until_expiry": 5,
"is_expired": false,
"status": "critical"
}
]
}-
Go to Azure Portal > Microsoft Entra ID > App registrations
-
Click New registration:
- Name:
Entra ID Secrets Monitor - Account types: Single tenant
- Click Register
- Name:
-
Note the Application (client) ID and Directory (tenant) ID
-
Certificates & secrets > Client secrets > New client secret
-
API permissions > Add a permission:
- Microsoft Graph > Application permissions
- Add
Application.Read.All(required for monitoring) - Add
Mail.Send(only if using Microsoft Graph Email notifications) - Click Grant admin consent
- Verify Azure credentials
- Check client secret hasn't expired
- Confirm admin consent was granted
- Enable at least one notification channel
- Check
DRY_RUN=false - Verify webhook URLs
- Ensure
Application.Read.Allpermission - Verify admin consent
docker compose logs -fMIT License - see LICENSE file.