Alerts across clients¶
Tenantry evaluates alert rules for all clients after each polling round (every 60 s by default) and notifies the
configured channels. The Alerts page (/alerts) lists the active alerts and the history (last 200 entries); the
dashboard shows the count and the latest entries. Settings › Alerts (/settings/alerts) configures the threshold and the
channels (administrators; other users read).
1. Rules¶
| Kind | Fires when | Severity | Key (deduplication) |
|---|---|---|---|
proxy_unreachable |
A client proxy (gateway, or any node with a client_code) has not been seen for more than 20 minutes. Auth/Proxy nodes are ignored. |
critical | proxy:<id> |
identity_expiring |
Tenantry's Teleport identity file expires in less than 24 h (critical once expired). Only with TELEPORT_IDENTITY_FILE. Under Machine ID (1 h certificates by design) only once expired. |
warning | identity |
identity_not_renewed |
Machine ID (--auth machine-id): the identity file was not rewritten by tbot within 2 x the renewal interval (40 min by default), or does not exist. tbot is probably down; resolves once it writes the file again. |
warning | identity:renewal |
console_down |
A console failed 2 polls in a row (unreachable, TLS, token…) or reports health.status = down. |
critical | console:<id>:down |
console_audit_broken |
A console reports audit.chain = broken. |
critical | console:<id>:audit |
console_dc_down |
A console reports a domain controller down (one alert per DC). |
warning | console:<id>:dc:<fqdn> |
console_upgrade_overdue |
A planned Steward upgrade (Upgrading consoles) is still not visible in the console version after the configured delay (default 7 days). Only evaluated while the console answers its polls. | warning | console:<id>:upgrade |
failed_logins_spike |
A console reports audit.failedLogins24h above the threshold (default 20, Settings › Alerts). |
warning | console:<id>:logins |
certificate_expiring |
The certificate presented by a console (read during its status poll, also when TLS verification fails) or by the Teleport proxy (one TLS handshake per round) expires in less than 14 days; critical when expired or under 3 days. Evaluated only when the certificate was read in this round. See TLS certificates. | warning | console:<id>:cert, teleport:cert |
No decision without data. A rule resolves only when its data was read in this round:
- fleet unreadable (
tctlerror): proxy alerts neither fire nor resolve; - one failed console poll: nothing changes (the second consecutive failure fires
console_down); - console unreachable: its audit / DC / logins alerts keep their state until a successful poll;
- console or proxy removed: its alerts resolve.
2. Deduplication¶
Each condition fires once (a fired history entry and one notification) and resolves once (resolved entry and one
notification), however many rounds it lasts. Values that change while the alert is active (minutes, counts) are refreshed
on the page without a new notification. The active set, the history (200 entries) and the last evaluation date are stored in
/data/alerts-state.json (atomic writes, see Clients and consoles): a restart neither
re-sends the active alerts nor forgets them.
3. Channels¶
Up to 20 channels; each can be disabled without being deleted. All events of one round are batched into one request per channel (a large outage sends one message, not hundreds; text messages list 30 events at most, then "… and N more").
| Type | Settings | Request |
|---|---|---|
| Generic webhook | HTTPS URL (a query string is allowed), optional HMAC secret (8 to 512 characters) | POST JSON (below) |
| ntfy | Server URL (https://ntfy.sh or self-hosted), topic [A-Za-z0-9_-]{1,64}, optional access token |
POST <server>/<topic>, text body, headers Title, Priority (urgent for a critical alert), Tags, Authorization: Bearer <token> |
| Microsoft Teams | Incoming webhook URL (Workflows "Post to a channel when a webhook request is received", or a legacy connector) | POST message with an Adaptive Card 1.4 |
| Slack | Incoming webhook URL | POST {"text": …} |
Secrets are write-only. The URL (Teams/Slack/webhook URLs embed their key), the HMAC secret and the ntfy token are
stored encrypted (AES-256-GCM, SECRETS_ENCRYPTION_KEY), never returned by the API (only the URL origin is shown, e.g.
https://hooks.slack.com, and "secret set"), never logged or audited. On edit, leave the URL empty to keep it, leave the
secret empty to keep it, or tick "remove the secret". Changing the type requires the URL again.
3.1 Webhook payload and signature¶
{
"schema": 1,
"source": "tenantry",
"time": "2026-10-18T10:00:00.000Z",
"events": [
{
"event": "fired",
"text": "[CRITICAL] Steward console down | client acme | console.acme.local | error: timeout",
"alert": {
"key": "console:4f6c…:down", "kind": "console_down", "severity": "critical", "clientCode": "acme",
"subject": "console.acme.local", "params": { "error": "timeout" }, "link": "/clients/acme",
"firedAt": "2026-10-18T10:00:00.000Z"
}
}
]
}
event is fired, resolved (the alert then carries resolvedAt) or test (alert: null). With a secret, two headers
are added:
X-Tenantry-Timestamp: 1792317600
X-Tenantry-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
The receiver recomputes the HMAC over the exact raw body, compares in constant time and refuses a timestamp older than 5 minutes (replay).
3.2 Send test¶
Send test (administrators, 10 per minute) sends one test notification to the channel and shows the result; it is also
recorded in the history (test) and the audit (alerts.channel.test).
4. Delivery and errors¶
Same outbound rules as console polling: https only, TLS verified (system store + NODE_EXTRA_CA_CERTS), no
redirects, 10 s timeout, response read up to 256 KiB and discarded. A 2xx answer is a success. Each history entry shows
the delivery result per channel:
| Error | Meaning |
|---|---|
http_error (+ status) |
The receiver answered non-2xx (401/403: revoked webhook or wrong ntfy token; 404: deleted webhook; 429: rate limit). |
redirect_refused |
3xx: use the final URL. |
tls_error, dns_error, connect_error, timeout |
Network path from Tenantry container to the receiver (egress proxy, firewall, DNS). |
secret_unreadable |
The stored URL or secret cannot be decrypted (key changed): edit the channel and enter them again. |
A failed delivery is not retried (the next transition is notified normally); watch the history, or add a second channel.
5. Audit¶
alerts.settings (threshold), alerts.channel.create, alerts.channel.update (detail: enabled=…, type=…,
url=replaced, secret=replaced|cleared), alerts.channel.delete, alerts.channel.test (outcome error with the error
code on failure). Target = type + channel name; the URL origin appears in the creation detail, never the full URL.
6. API¶
GET /api/alerts (active + history), GET /api/alerts/settings, PUT /api/alerts/settings { failedLoginsThreshold }
(1 to 100,000), POST /api/alerts/channels, PUT /api/alerts/channels/:id, DELETE /api/alerts/channels/:id,
POST /api/alerts/channels/:id/test.