Password expiry notification for Microsoft Entra ID - hardened, self-hosted, multi-tenant
9.3K
Password Expiry Notification Tool for Microsoft Entra ID (Azure AD).
PwNotify checks your Entra ID users via Microsoft Graph, computes when each password expires, and sends staged reminder e-mails — to the primary mailbox, the alternate (SSPR) address, or both. Self-hosted, hardened, and driven by a modern admin UI with a first-run setup wizard.
docker pull amslertec/pwnotify:0.3.10
sendMail or SMTP, switchable at runtime.no-new-privileges, all capabilities dropped, SBOM + provenance
attestations, multi-arch (amd64 + arm64).| Tag | Meaning |
|---|---|
0.3.10 | Pinned release (recommended for production) |
latest | Most recent release |
Multi-arch manifests: linux/amd64 and linux/arm64.
You need two files on the target server: docker-compose-prod.yml and example.env.
The commands below download them straight from the public repository — no clone required.
mkdir pwnotify && cd pwnotify
# Download the two files straight to their final names (public repo, no auth needed):
curl -fsSL https://raw.githubusercontent.com/amslertec/pwnotify/main/docker-compose-prod.yml -o docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/amslertec/pwnotify/main/example.env -o .env
# -> edit .env: POSTGRES_PASSWORD, PWNOTIFY_RUNTIME_DB_PASSWORD, PWNOTIFY_BASE_URL,
# PWNOTIFY_BIND, PWNOTIFY_COOKIE_SECURE
docker compose pull
docker compose up -d
docker compose ps # wait for "healthy"
Then open the app — the first-run setup wizard guides you through database check → admin account → Microsoft Graph (with a built-in Entra registration guide) → mail backend.
Can't reach it /
ERR_CONNECTION_REFUSED? The container binds to127.0.0.1:8080by default (localhost only) — correct behind a reverse proxy, but unreachable from another machine. For direct LAN access set in.env:PWNOTIFY_BIND=0.0.0.0:8080,PWNOTIFY_BASE_URL=http://<server-ip>:8080,PWNOTIFY_COOKIE_SECURE=false, thendocker compose up -dagain. The bundled PostgreSQL stays on the internal Docker network.
PwNotify needs a PostgreSQL database. If you already have one:
docker run -d --name pwnotify \
-p 127.0.0.1:8080:8080 \
-e PWNOTIFY_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/pwnotify" \
-e PWNOTIFY_BASE_URL="https://pwnotify.example.com" \
-e PWNOTIFY_COOKIE_SECURE=true \
-v pwnotify-data:/data \
--read-only --tmpfs /tmp \
--security-opt no-new-privileges:true --cap-drop ALL \
amslertec/pwnotify:0.3.10
Environment variables are an initial seed only — after the first start everything is managed in the database via the Settings UI. Secrets are Fernet-encrypted at rest.
| Variable | Default | Description |
|---|---|---|
PWNOTIFY_DATABASE_URL | — | Required. Async DSN, e.g. postgresql+asyncpg://user:pass@db:5432/pwnotify |
PWNOTIFY_RUNTIME_DB_PASSWORD | — | Required. Password for the non-superuser pwnotify_runtime DB role used for tenant queries (defence in depth for RLS). Provisioned automatically on start. Startup fails fast if unset |
PWNOTIFY_SECRET_KEY | auto | Fernet master key. Empty → generated into /data/secret.key |
PWNOTIFY_BASE_URL | http://localhost:8080 | Public URL (e-mail links, cookies, OIDC redirect) |
PWNOTIFY_COOKIE_SECURE | true | Require HTTPS cookies (set false for plain HTTP) |
PWNOTIFY_BIND | 127.0.0.1:8080 | Host bind address. 0.0.0.0:8080 for direct LAN access; keep localhost behind a reverse proxy |
PWNOTIFY_TRUSTED_PROXIES | 127.0.0.1 | Proxy peers allowed to set X-Forwarded-For. Set your proxy's IP/subnet, otherwise the audit log and rate limiter see the proxy IP, not the real client |
PWNOTIFY_ALLOWED_HOSTS | — | Optional comma-separated Host-header allow-list (empty = no check) |
PWNOTIFY_IDLE_TIMEOUT_MIN | 30 | Sign out after this many minutes of inactivity (0 = off) |
PWNOTIFY_REFRESH_TOKEN_TTL_DAYS | 14 | Maximum session lifetime |
PWNOTIFY_TIMEZONE | Europe/Zurich | Scheduler timezone |
PWNOTIFY_LOG_LEVEL | INFO | DEBUG | INFO | WARNING | ERROR |
PWNOTIFY_LOG_JSON | true | JSON logs (false = colored console) |
PWNOTIFY_ADMIN_USERNAME / _PASSWORD | — | Optional first-admin seed (else via wizard) |
PWNOTIFY_GRAPH_TENANT_ID / _CLIENT_ID / _CLIENT_SECRET | — | Optional Graph seed |
PWNOTIFY_MAIL_BACKEND | graph | graph | smtp |
PWNOTIFY_MAIL_FROM | — | Sender address |
See example.env for the full list.
| Port | 8080 (HTTP, app + API) |
Volume /data | Fernet key, uploaded logos/favicons, avatars — back this up |
DB volume pgdata | PostgreSQL data directory |
Create an app registration (client-credentials) with these application permissions and grant admin consent:
| Permission | Purpose |
|---|---|
User.Read.All | Read users, UPN, last password change (+ SSO profile photos) |
Domain.Read.All | Read per-domain password validity |
Mail.Send | Send reminder e-mails via Graph |
GroupMember.Read.All | Optional — only for group-scoped sync, Microsoft SSO, and/or group-based Teams assignment |
The setup wizard and the Settings → Graph tab contain a step-by-step guide.
This image is built for a Docker Scout A rating:
65532.--read-only + a small tmpfs).uv, tests, or VCS metadata in the runtime.mode=max) attestations attached to the manifest.Inspect the attestations yourself:
docker scout cves amslertec/pwnotify:0.3.10 # vulnerability report
docker buildx imagetools inspect amslertec/pwnotify:0.3.10 # platforms + attestations
docker scout sbom amslertec/pwnotify:0.3.10 # software bill of materials
docker compose -f docker-compose-prod.yml pull
docker compose -f docker-compose-prod.yml up -d
Database schema migrations (Alembic) run automatically on start.
Existing deployments upgrading to 0.2.7+: set PWNOTIFY_RUNTIME_DB_PASSWORD (a strong
value) in .env, then pull and up -d as above. The migration creates a dedicated
non-superuser DB role (pwnotify_runtime) for tenant queries automatically — no manual SQL.
Startup fails fast if the variable is missing.
Back up two things: the /data volume (Fernet key + uploads) and the PostgreSQL
database.
# Database dump
docker compose -f docker-compose-prod.yml exec db \
pg_dump -U pwnotify pwnotify > pwnotify-backup.sql
# /data volume (tar)
docker run --rm -v pwnotify_data:/data -v "$PWD":/backup alpine \
tar czf /backup/pwnotify-data.tgz -C /data .
If you lose
/data/secret.keywithout aPWNOTIFY_SECRET_KEYset, the encrypted secrets in the database can no longer be decrypted. Keep it (or an explicit key) safe.
MIT © amslertec — source: https://github.com/amslertec/pwnotify
Content type
Image
Digest
sha256:3879741e8…
Size
66.8 MB
Last updated
about 2 months ago
docker pull amslertec/pwnotify