acme-proxy 0.2.0

An ACME (RFC 8555) server that issues from a local CA, relays to an upstream CA, or delegates to a script
Documentation
# Notifications

The `notify` subsystem alerts operators on lifecycle events within the ACME
server.

## Supported events

| Event | Fired when |
| --- | --- |
| `profile_mounted` | A profile is initialized at startup. |
| `account_created` | A client registers a new account. |
| `account_deactivated` | An account is deactivated. |
| `certificate_issued` | An order is finalized and a certificate is minted. |
| `certificate_revoked` | A certificate is revoked, via the ACME API or the admin CLI. |
| `challenge_failed` | A domain-control validation attempt fails. |

These six names are the only valid values wherever a backend's `events` list is
configured. An unrecognised name is a **startup error**, not a silently ignored
entry.

## Backends

- **[Email]email.md** — SMTP, via `lettre`.
- **[Webhook]webhook.md** — any HTTP endpoint, with the URL, method, headers
  and body all configured. This is how Slack, Mattermost, Teams, Telegram and
  Matrix are reached: they differ in those four values and nothing else, so
  each is a configuration entry rather than a backend of its own.
- **[Custom Script]custom.md** — shell out to a local script, for a channel
  that is not an HTTP request at all.

Email and webhook render their messages with MiniJinja templates you can
override; see [Customizing Templates](templates.md).

## Configuration

```toml
[notify]
# Which backends are active. Empty (the default) means no notifications at all.
enabled = ["email", "webhook"]

# Which [notify.webhook.<name>] entries to POST to, when "webhook" is listed
# above.
webhook_enabled = ["slack"]

# Which [notify.custom.<name>] entries to run, when "custom" is listed above.
custom_enabled = []

# Optional directory of template overrides, checked per template file before
# falling back to the compiled-in default.
template_dir = "/etc/acme-proxy/templates"
```

### Reference

**`enabled`** (`Array`) — *Default: `[]` | Env: `ACME_PROXY_NOTIFY__ENABLED`*

Active backends: any of `email`, `webhook`, `custom`. Empty means the
subsystem is off. `"mattermost"` was removed in favour of `webhook` and is
refused by name.

**`webhook_enabled`** (`Array`) — *Default: `[]` | Env: `ACME_PROXY_NOTIFY__WEBHOOK_ENABLED`*

Which entries under `[notify.webhook.<name>]` to POST to, and in what order.
Listing `"webhook"` in `enabled` while leaving this empty is a startup error,
as is naming an entry that has no table.

**`custom_enabled`** (`Array`) — *Default: `[]` | Env: `ACME_PROXY_NOTIFY__CUSTOM_ENABLED`*

Which entries under `[notify.custom.<name>]` to run, and in what order. The
same two startup errors apply.

**`template_dir`** (`String`) — *Default: `""` | Env: `ACME_PROXY_NOTIFY__TEMPLATE_DIR`*

Directory searched for template overrides. Lookup is **per file**, so overriding
one message (say `email/certificate_issued.body.j2`) leaves every other message
at its compiled-in default. Empty means defaults only.

Each backend additionally takes its own `events` list and `timeout_ms`; see the
backend pages.

## Delivery semantics

Dispatch is **fire-and-forget**: the event is written to the durable job queue
and the ACME response proceeds immediately. A notification backend can never
delay or fail the request that triggered it.

Delivery itself is a `notify_deliver` job, one row **per backend per event**, so
one flaky webhook is retried without re-sending through an email backend that
already succeeded. Two consequences worth planning around:

- **A row outlives the process that wrote it.** A notification generated moments
  before a restart is delivered by whoever starts next, rather than lost. There
  is no drain at shutdown to configure or wait for.
- **A failure is retried, unless it never could have worked.** A refused SMTP
  connection, a timeout, a 429 or a 5xx from a webhook goes back in the queue
  under `jobs.max_attempts` and the shared backoff. A template that does not
  render, a `url` that does not parse and any other 4xx are refused on the
  first attempt — retrying would reach the same answer four more times and
  delay the log line saying so.

Every attempt logs `notify_delivered` or `notify_delivery_failed`. When the
attempts run out, one `notify_delivery_abandoned` says the notification is
genuinely lost — that is the line to alert on. Because the `custom` backend's
contract is an exit code with no way to say "never retry", **every** failure of
a custom script is treated as retryable.