Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4de6ff6
docs: design for mail-stack monitoring (postfix/dovecot/rspamd/greylist)
MalteKiefer Jul 16, 2026
17b8c2b
docs: implementation plan for mail-stack monitoring
MalteKiefer Jul 16, 2026
fcbbdc4
feat(apitypes): mail report types in ingest snapshot
MalteKiefer Jul 16, 2026
d191f61
feat(agent): mail collector config
MalteKiefer Jul 16, 2026
84d0ba9
feat(agent/mail): systemd unit state helper
MalteKiefer Jul 16, 2026
3711294
feat(agent/mail): postfix queue depth via spool file count
MalteKiefer Jul 16, 2026
0d3b8bc
feat(agent/mail): rspamd stat client
MalteKiefer Jul 16, 2026
60e689b
feat(agent/mail): local port + TLS expiry probe
MalteKiefer Jul 16, 2026
7bfb263
feat(agent): mail-stack collector (gated on detection)
MalteKiefer Jul 16, 2026
d18b1e2
feat(db): mail status + metrics_mail (migration 0035)
MalteKiefer Jul 16, 2026
bf3969b
feat(store): persist + read mail status
MalteKiefer Jul 17, 2026
a70265d
feat(api): ingest mail report + host mail endpoint
MalteKiefer Jul 17, 2026
a38abc0
feat(web): host-detail mail status tab
MalteKiefer Jul 17, 2026
12f6447
feat(alerts): mail queue metric-threshold
MalteKiefer Jul 17, 2026
3860ebe
feat(alerts): mail_service_down condition
MalteKiefer Jul 17, 2026
ae31a95
feat(web): mail suggested-alert rules
MalteKiefer Jul 17, 2026
537a985
docs: mail-stack monitoring
MalteKiefer Jul 17, 2026
f26cdb4
fix(mail): satisfy golangci-lint (ctx-aware dials, errors.Is, gofumpt…
MalteKiefer Jul 17, 2026
74ed7c8
fix(mail): checked TLS type assertion + http.MethodGet (golangci)
MalteKiefer Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ plane without running a hosted SaaS.
- **Server** — Go 1.26 with chi + huma for an OpenAPI-typed REST surface,
embeds the React SPA, talks to TimescaleDB via pgx.
- **Agent** — Go binary, tiny systemd-hardened footprint, ships metrics +
inventory + login events + package state + firewall/CrowdSec snapshots.
inventory + login events + package state + firewall/CrowdSec snapshots +
mail-stack status (postfix/dovecot/rspamd/postgrey service state, Postfix
queue depths, Rspamd statistics, and port/TLS reachability) when a mail
stack is detected on the host.
- **Database** — TimescaleDB hypertables with retention policies for every
metrics table; `audit_log` and `alert_history` are bounded too.
- **Web UI** — React 19 + Vite + Tailwind 3, dark/light themes, lazy-loaded
Expand Down
164 changes: 164 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,23 @@ components:
- since
- events
type: object
HostMailOutputBody:
additionalProperties: false
properties:
$schema:
description: A URL to the JSON Schema for this object.
examples:
- https://mon.kiefer-networks.de/schemas/HostMailOutputBody.json
format: uri
readOnly: true
type: string
detected:
type: boolean
report:
$ref: "#/components/schemas/MailReport"
required:
- detected
type: object
HostPackageUpdatesOutputBody:
additionalProperties: false
properties:
Expand Down Expand Up @@ -1778,6 +1795,9 @@ components:
type:
- array
- "null"
mail:
$ref: "#/components/schemas/MailReport"
description: Mail stack status (postfix/dovecot/rspamd)
nics:
items:
$ref: "#/components/schemas/NetSample"
Expand Down Expand Up @@ -2263,6 +2283,71 @@ components:
required:
- ok
type: object
MailPortCheck:
additionalProperties: false
properties:
cert_not_after:
format: date-time
type: string
cert_trusted:
type: boolean
open:
type: boolean
port:
format: int64
type: integer
proto:
maxLength: 16
type: string
tls:
type: boolean
required:
- port
- proto
- open
- tls
- cert_trusted
type: object
MailReport:
additionalProperties: false
properties:
ports:
items:
$ref: "#/components/schemas/MailPortCheck"
type:
- array
- "null"
queue:
$ref: "#/components/schemas/PostfixQueue"
rspamd:
$ref: "#/components/schemas/RspamdStat"
services:
items:
$ref: "#/components/schemas/MailService"
type:
- array
- "null"
time:
format: date-time
type: string
required:
- time
type: object
MailService:
additionalProperties: false
properties:
active:
type: boolean
name:
maxLength: 64
type: string
sub_state:
maxLength: 32
type: string
required:
- name
- active
type: object
Manifest:
additionalProperties: false
properties:
Expand Down Expand Up @@ -2813,6 +2898,7 @@ components:
- audit_action
- host_flap
- unexpected_reboot
- mail_service_down
type: string
created_at:
format: date-time
Expand Down Expand Up @@ -2921,6 +3007,7 @@ components:
- audit_action
- host_flap
- unexpected_reboot
- mail_service_down
type: string
required:
- condition_type
Expand Down Expand Up @@ -3069,6 +3156,7 @@ components:
- audit_action
- host_flap
- unexpected_reboot
- mail_service_down
type: string
enabled:
type: boolean
Expand Down Expand Up @@ -3512,6 +3600,31 @@ components:
- available_version
- is_security
type: object
PostfixQueue:
additionalProperties: false
properties:
active:
format: int64
type: integer
deferred:
format: int64
type: integer
hold:
format: int64
type: integer
incoming:
format: int64
type: integer
total:
format: int64
type: integer
required:
- active
- deferred
- hold
- incoming
- total
type: object
RenamePasskeyRequest:
additionalProperties: false
properties:
Expand Down Expand Up @@ -3585,6 +3698,30 @@ components:
required:
- revoked
type: object
RspamdStat:
additionalProperties: false
properties:
greylisted:
format: int64
type: integer
learned:
format: int64
type: integer
reachable:
type: boolean
rejected:
format: int64
type: integer
scanned:
format: int64
type: integer
required:
- reachable
- scanned
- rejected
- greylisted
- learned
type: object
SearchPackagesOutputBody:
additionalProperties: false
properties:
Expand Down Expand Up @@ -6184,6 +6321,33 @@ paths:
summary: Recent login/auth events for a host
tags:
- security
/v1/hosts/{id}/mail:
get:
operationId: get-host-mail
parameters:
- description: host UUID
in: path
name: id
required: true
schema:
description: host UUID
type: string
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/HostMailOutputBody"
description: OK
default:
content:
application/problem+json:
schema:
$ref: "#/components/schemas/ErrorModel"
description: Error
summary: Latest mail-stack status (postfix/dovecot/rspamd) for a host
tags:
- mail
/v1/hosts/{id}/metrics/disk:
get:
operationId: host-disk-metrics
Expand Down
74 changes: 74 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,80 @@ once the manifest is parsed.

---

## 4b. Mail-stack monitoring

The agent automatically detects whether a host runs a mail stack by querying
systemd for `postfix.service`, `dovecot.service`, `rspamd.service`, and
`postgrey.service`. If at least one unit is present the agent collects a
`MailReport` on every ingest tick and ships it alongside the regular metrics
payload. Hosts with no mail units produce no mail payload and no Mail tab.

### Mail tab (host detail page)

Open a host's detail page and select the **Mail** tab. It shows:

- **Service state** — up/down for each detected unit (postfix, dovecot,
rspamd, postgrey). A red indicator means the unit is inactive or failed.
- **Postfix queue** — active, deferred, and total message counts read from
the Postfix spool directory.
- **Rspamd statistics** — scanned/rejected/greylist counts from the Rspamd
HTTP stat endpoint.
- **Port/TLS reachability** — TCP dial result and TLS handshake for SMTP
(25, 587), IMAP (143), and IMAPS (993), scoped to the units actually
present.

### Agent config keys (`/etc/mon-agent/config.yaml`)

```yaml
mail:
enabled: true # set false to disable entirely
rspamd_stat_url: http://127.0.0.1:11334/stat # default; override if rspamd
# listens on a non-standard address
```

`enabled` defaults to `true`; operators on non-mail hosts can set `false` to
suppress the detection pass entirely. `rspamd_stat_url` is optional and
defaults to `http://127.0.0.1:11334/stat`.

### Alert rules

Two condition types cover the mail stack:

| `condition_type` | Trigger | Suggested severity |
| --- | --- | --- |
| `mail_service_down` | A tracked mail service transitions to inactive/failed. Fires per-service; one dedup key per host + service name. | `critical` |
| `metric_threshold` with `metric: mail_queue_deferred` | Deferred queue depth exceeds the configured threshold (absolute message count). | `warning` |
| `metric_threshold` with `metric: mail_queue_total` | Total queue depth exceeds threshold. | `warning` |

Example rule body (POST `/v1/notification_rules`):

```json
{
"name": "Mail service down",
"condition_type": "mail_service_down",
"severity": "critical",
"notification_channel_ids": ["<channel-uuid>"]
}
```

```json
{
"name": "Postfix deferred queue high",
"condition_type": "metric_threshold",
"metric": "mail_queue_deferred",
"threshold": 50,
"comparator": ">",
"severity": "warning",
"notification_channel_ids": ["<channel-uuid>"]
}
```

The suggested alert rules shown in the SPA notification-rules form pre-fill
`mail_service_down` and a `mail_queue_deferred` threshold of 50. Adjust the
threshold to match your traffic profile.

---

## 5. Investigations

Open a `psql` shell first (see §3).
Expand Down
Loading
Loading