Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom Script Notifications

The custom notification backend runs a local script (Bash, Python, Go, …) when an ACME event occurs. Use it to integrate with internal ticketing systems, custom logging infrastructure, or alerting pipelines that a plain HTTP webhook cannot satisfy.

Configuration

custom is a named map, like [filter.check]. Two keys switch it on: notify.enabled activates the backend, and notify.custom_enabled selects which scripts run, and in what order.

[notify]
enabled = ["custom"]
custom_enabled = ["ticket-creator"]

[notify.custom.ticket-creator]
script_path = "/etc/acme-proxy/scripts/ticket-creator.sh"
timeout_ms = 10000
args = []
events = ["certificate_issued", "certificate_revoked"]

Three ways to get this wrong, all of which fail at startup rather than at delivery time:

  • Listing "custom" in notify.enabled while custom_enabled is empty.
  • Naming an entry in custom_enabled that has no [notify.custom.<name>] table.
  • Using an entry name outside ^[a-z0-9-]+$ — ticket_creator (underscore) is rejected; ticket-creator is fine.

One process is spawned per enabled script per event.

Reference

script_path (String) — Default: "" | Env: ACME_PROXY_NOTIFY__CUSTOM__<NAME>__SCRIPT_PATH

Path to the executable. Required.

timeout_ms (Integer) — Default: 5000 | Env: ACME_PROXY_NOTIFY__CUSTOM__<NAME>__TIMEOUT_MS

Maximum execution time. A script still running when this expires is killed.

args (Array) — Default: [] | Env: ACME_PROXY_NOTIFY__CUSTOM__<NAME>__ARGS

Static arguments passed to the script on every invocation.

events (Array) — Default: every event | Env: ACME_PROXY_NOTIFY__CUSTOM__<NAME>__EVENTS

Which events this script reacts to. Valid names are profile_mounted, account_created, account_deactivated, certificate_issued, certificate_revoked, challenge_failed, certificates_expiring. An unrecognised name is a startup error.

Execution model and security

  1. Environment clearing (env_clear): the child runs with a scrubbed environment, inheriting only a minimal PATH and the injected ACME_NOTIFY_* variables. The server’s own environment may hold secrets — notify.email.smtp_password, the RFC 2136 TSIG key, the NetBox token — and a notification script has no business reading them.
  2. Zombie prevention (kill_on_drop): the script runs under a Tokio timeout with kill_on_drop(true). A tokio::time::timeout only drops the future, so without this a timed-out script would outlive its deadline and leak a process per event.
  3. Fire and forget: delivery runs in a background task. A failing or hanging script can never delay or fail the ACME response that triggered it; failures are logged (event = "notify_delivery_failed") and dropped, with no retry.

Data passing

The script receives context both ways.

Environment variables

All seven are always set. Ones that do not apply to the event are set to the empty string rather than omitted, so a script can read them unconditionally.

VariableValue
ACME_NOTIFY_HOOKThe event name, e.g. certificate_issued.
ACME_NOTIFY_PROFILEThe profile the event occurred in.
ACME_NOTIFY_CLIENT_IPThe ACME client’s address; empty when no request was in scope (e.g. profile_mounted, or an asynchronous relay completion).
ACME_NOTIFY_ACCOUNT_IDThe account, when the event has one.
ACME_NOTIFY_ORDER_IDThe order, when the event has one.
ACME_NOTIFY_CERT_SERIALThe certificate serial, on certificate_issued / certificate_revoked.
ACME_NOTIFY_IDENTIFIERSComma-joined identifier values. Only populated for certificate_issued.

On certificates_expiring every variable but ACME_NOTIFY_HOOK and ACME_NOTIFY_PROFILE is empty, and that is not an omission: a digest is about a list of certificates spanning however many accounts, so there is no one account, order or serial for a variable to hold. Read the list from the JSON on stdin, which is the channel that carries structure.

On admin_sign_in / admin_credential_changed the only populated variables are ACME_NOTIFY_HOOK, ACME_NOTIFY_PROFILE (__admin__) and ACME_NOTIFY_CLIENT_IP (the web-admin client’s address). The username, recipient, outcome / change, by_self and user_agent are on the stdin JSON — an operator is not an ACME subject, so it has no account or order id.

There is no ACME_NOTIFY_EVENT; the event name is ACME_NOTIFY_HOOK.

JSON on stdin

A JSON object is always written to the script’s standard input. It is the event’s own fields plus a "hook" key naming the event — the same data the templating backends render from. For example, on certificate_issued:

{
  "hook": "certificate_issued",
  "profile": "default",
  "order_id": "…",
  "account_id": "…",
  "cert_serial": "…",
  "identifiers": ["a.example.com", "b.example.com"],
  "client_ip": "203.0.113.5"
}

The exact fields per event are listed in Customizing Templates — the template context and the stdin payload carry the same values.

The issued certificate itself is never passed to a notification script, on stdin or otherwise. Only its serial and the identifiers it covers are available. A script that needs the PEM must fetch it out of band.

Example

#!/bin/bash
# /etc/acme-proxy/scripts/ticket-creator.sh
set -euo pipefail

payload=$(cat)   # the JSON described above

case "$ACME_NOTIFY_HOOK" in
  certificate_issued)
    echo "issued ${ACME_NOTIFY_CERT_SERIAL} for ${ACME_NOTIFY_IDENTIFIERS}" \
      >> /var/log/acme-issuance.log
    ;;
  certificate_revoked)
    # ACME_NOTIFY_IDENTIFIERS is empty for this event — read the payload
    # if you need more than the serial.
    reason=$(echo "$payload" | jq -r '.reason // "unspecified"')
    curl -sS -X POST https://tickets.internal/api/incidents \
      -H 'Content-Type: application/json' \
      -d "{\"serial\":\"$ACME_NOTIFY_CERT_SERIAL\",\"reason\":\"$reason\"}"
    ;;
esac

See Custom Plugins Examples for more.