Notifications¶
The console can alert your MSP or administrator channels: a generic webhook, a Microsoft Teams or Slack incoming webhook, or e-mail over SMTP. Configuration › Notifications, for an operator with settings.manage.
An administrator channel only. Notifications name domain controllers and operators, and the weekly digest lists accounts of every managed OU (it is built with the console's own read identity, not with the scope of whoever reads it). Point channels only at people who are meant to see the whole client scope: the MSP team, the client's administrators. Never at a channel end users or third parties can read.
No notification ever contains a password, a secret, a token, a webhook URL, a key or an exception message.
Events¶
Each channel subscribes to the events it wants:
| Event | Code | When | Dedup |
|---|---|---|---|
| Audit chain broken | audit_chain_broken |
The latest audit-chain verification (background, Audit:VerifyInterval, or an explicit verification) fails. |
once per first broken row and 24 h |
| Domain controller down | dc_down |
A configured DC goes down in the failover memory (its last contact failed). |
once per DC and 15 min |
| Domain controller recovered | dc_recovered |
A DC that was reported down answers again. | once per DC and 15 min |
| Spike of denied actions | denied_spike |
Audit rows with result denied within the window reach the threshold. |
once per window |
| Spike of failed sign-ins | login_failure_spike |
Failed auth.login rows within the window reach the threshold. |
once per window |
| Teleport operator waiting | teleport_waiting |
A Teleport operator with no console right opens the console (the "no access yet" page). The message has their name and the Teleport roles seen, so an administrator can map one. | once per operator and 24 h |
| Weekly digest | weekly_digest |
Every Monday from 07:00 UTC: counts of locked accounts, passwords expiring within 14 days and accounts inactive for 90 days, with the first 5 of each. Privileged accounts are never listed (same rule as the watch lists). | once per ISO week |
| Temporary membership expired | temporary_membership_expired |
A time-bound group membership reached its date and the console removed the member. Says whether the removal was covered by the delegation of the operator who added it or done as the end of a temporary grant (see Helpdesk). | once per membership |
| Temporary membership not removed | temporary_membership_failed |
An expired temporary membership could not be removed after 3 attempts (TemporaryMemberships:NotifyAfterAttempts). The console keeps retrying. |
once per membership and 24 h |
| Access review reminder | access_review_reminder |
An access review is launched, then every 3 days (AccessReviews:ReminderEvery) while memberships wait for a decision. Names the reviewers. |
per send |
| Access review overdue | access_review_overdue |
An access review is past its due date and still open. | once a day (AccessReviews:OverdueEvery) |
| Approval requested | approval_requested |
An operator submitted an approval request (subscribe the approvers' channel). Names the action, target, requester, reason, ticket and approvers. | per request |
| Approval decided | approval_decided |
A request was approved and executed (with the outcome), rejected, expired or cancelled (subscribe the requesters' channel). | per decision |
| Protection removed | protection_weakened |
A Super administrator removed a Tier 0/1 OU, a sensitive group or a VIP group (critical). | per change |
Thresholds (Configuration › Notifications › Spike thresholds, step-up): denied actions (default 25), failed sign-ins (default 10) and the window (default 10 minutes).
Channels¶
| Kind | Settings | What is sent |
|---|---|---|
| Generic webhook | HTTPS URL, optional HMAC key (16+ characters) | POST, JSON steward.notification/1 (below) |
| Microsoft Teams | HTTPS URL of a Teams Workflows flow "When a Teams webhook request is received" | an Adaptive Card message |
| Slack | HTTPS URL of an incoming webhook | text and blocks (mrkdwn escaped) |
SMTP host, port, starttls or tls (implicit), optional user and password, sender, 1 to 10 recipients |
a plain-text UTF-8 message |
- URLs must be
https://without credentials. Requests never follow redirects and time out afterNotifications:HttpTimeout(10 s). - SMTP always uses TLS: implicit TLS (usually port 465) or STARTTLS (usually 587), which the server must offer (otherwise
smtp_starttls_unavailable; plain text is never used). The server certificate is checked against the system trust store. Authentication (AUTH PLAIN or LOGIN) only ever happens inside TLS. - Secrets are write-only and encrypted at rest: the webhook URL (Teams and Slack URLs are bearer secrets), the HMAC key and the SMTP password are stored with the instance master key (AES-256-GCM, like the technical identities) and never returned by the API: the form only says whether one is set. Leave the field empty to keep it.
- A secret is bound to its destination (clearing rule): a stored secret is never sent anywhere it was not entered for.
Changing the SMTP server, port or user requires the SMTP password again; changing a generic webhook's URL requires its signing
key again. Without it the save is refused (
400, codenotification_secret_required, fieldsmtp.passwordorhmacKey) and nothing changes; tick "Remove the password" / "Remove the signing key" to save the new destination without one. A Teams or Slack URL is the secret itself: it is always entered in full, never combined with the stored one. Send test only ever uses the saved channel (destination and secret as stored): an unsaved change in the form is never tested with a stored secret. - Outbound HTTP requests are never logged with their URL: the
System.Net.Http.HttpClientlog categories are kept at Warning in the console and the password service, and the notification, Teleport JWKS and password-service clients have no request logging. - Every change (create, update, delete, thresholds) needs a step-up and is audited (
notifications.channel_created,notifications.channel_updated,notifications.channel_deleted,notifications.settings) with names, kinds, events, hosts and "secret set / cleared" markers, never the secret itself. At most 10 channels.
Generic webhook payload¶
{
"schema": "steward.notification/1",
"event": "dc_down",
"severity": "warning",
"title": "Domain controller down: dc2.client-a.local",
"text": "The console could not reach dc2.client-a.local; it fails over to the other domain controllers.",
"occurredUtc": "2026-10-18T09:12:00Z",
"test": false,
"console": { "clientCode": "client-a", "title": "Client A" },
"facts": [ { "label": "Domain controller", "value": "dc2.client-a.local" }, { "label": "Last failure", "value": "ldap_81" } ]
}
The weekly digest adds "digest": [{ "list": "locked", "count": 3, "truncated": false, "top": [{ "displayName", "login", "ouPath",
"when" }] }]. Headers: X-Steward-Event (the event code) and, when a key is set:
X-Steward-Timestamp: 1792141920
X-Steward-Signature: sha256=<lowercase hex HMAC-SHA256(key, "<timestamp>.<raw body>")>
Verify it on the receiver by recomputing the HMAC over the timestamp, a dot and the exact body bytes, comparing in constant time, and refusing old timestamps (for example more than 5 minutes) to stop replays.
Delivery¶
- Deduplication: one delivery per channel and dedup key within the event's window (table above), also across restarts.
- Rate limit: at most
Notifications:MaxPerChannelPerHour(30) deliveries per channel per hour; the extra ones are recorded assuppressed(rate_limited). - Retries: a timeout, a network error, HTTP 5xx / 408 / 429 or an SMTP 4xx reply is retried after 30 s, 2 min and 8 min
(
Notifications:RetryBaseDelay, ×4 each time,Notifications:MaxAttempts= 4 attempts). Other errors (HTTP 4xx, SMTP 5xx, TLS) fail at once. - Send test sends a fixed test message to one channel now (no dedup, no retry) and is audited (
notifications.test). - Last deliveries lists the 50 most recent deliveries: event, channel, status (
delivered,failed,pendingwhile retrying,suppressed), attempts and an error code (http_404,timeout,smtp_535,smtp_tls...). Deliveries are kept 30 days (at most 1000).
Messages are written in English (they go to MSP tooling); event codes and facts stay machine-readable.
Settings¶
Notifications section of steward.yaml (or STEWARD_Notifications__*):
| Setting | Default | |
|---|---|---|
MonitorInterval |
00:01:00 |
How often the audit chain, DC health, spikes and the digest are checked. 0 turns the checks off. |
RetryBaseDelay |
00:00:30 |
First retry delay (then ×4). |
MaxAttempts |
4 |
Attempts per delivery. |
MaxPerChannelPerHour |
30 |
Rate limit per channel. |
HttpTimeout |
00:00:10 |
Webhook request timeout. |
DigestDay / DigestHourUtc |
Monday / 7 |
When the weekly digest becomes due. |
Outbound traffic: the console must reach the webhook hosts (HTTPS 443) and the SMTP server. Through a proxy, set the usual
HTTPS_PROXY environment variable.