Skip to main content

Webhook module

Delivers a certificate payload to any HTTP endpoint — the escape hatch for targets that don't fit one of the other modules. You configure the URL, headers, payload template, timeout, and retry count; the receiving system owns persistence and rollback.

Overview

  • Method: HTTP POST.
  • Auth: native bearer_token credential (Authorization: Bearer …, replaces any plaintext Authorization header) and/or native HMAC signing (X-Signature-256), plus any custom static headers.
  • Body: default JSON payload, or a custom Go template.
  • Private key: opt-in — the default excludes it; set IncludePrivateKey only for receivers that actually need it.
  • Retry: configurable count with exponential backoff.
  • Rollback: re-send. Rollback re-POSTs the previous retained version to the endpoint; the receiver re-processes it. It cannot un-call the POST that already fired.

Prerequisites

  • An HTTP endpoint reachable from the backend.
  • Authentication expectation on the receiving side that you can satisfy with static HTTP headers, or with the built-in HMAC signing: attach an hmac_secret credential to the target (slot signing) and every request carries an X-Signature-256: sha256=<hex> header (HMAC-SHA256 over the exact body). OAuth flows are not built for you.

Create a webhook target

  1. Settings → Distribution → TargetsNew. Module: Webhook.
  2. Fields:
    • URL — the endpoint (HTTPS recommended).
    • Headers — map of name → value. Typical: Authorization: Bearer ... or X-Signature: sha256=....
    • Timeout (seconds) — default 30.
    • Retry count — default 0 (no retries). Exponential backoff between attempts when set.
    • Include private key — off by default. Turn on only when the receiver genuinely needs it.
    • Template — optional Go template for a custom body. When empty, the default JSON payload (below) is sent.
    • Concurrency — for fan-out to many webhooks, parallelism cap.
  3. Save → health check sends an HTTP HEAD to the URL. Any response counts as reachable — the status code is not evaluated, so only a transport error marks the target unhealthy.

Default JSON payload

{
"certificate_id": "...",
"distribution_id": "...",
"cert_fingerprint": "<sha256 hex>",
"domains": ["example.com", "www.example.com"],
"cert_pem": "-----BEGIN CERTIFICATE-----...",
"chain_pem": "-----BEGIN CERTIFICATE-----...",
"fullchain_pem": "-----BEGIN CERTIFICATE-----...",
"private_key_pem": "-----BEGIN PRIVATE KEY-----..." // only if include_private_key
}

Custom body templates

The template is a Go text/template, but its variables are not the JSON payload keys above. Exactly these are available:

.CertificateID · .Fingerprint · .Domains · .CertPEM · .ChainPEM · .FullChainPEM · .JobID — plus .PrivateKeyPEM, only when Include private key is on.

A name outside this list renders as an empty string rather than erroring, so a template referencing e.g. .DistributionID silently posts blanks. Example — a GitLab CI variable update body:

{
"variable_type": "env_var",
"key": "TLS_FULLCHAIN",
"value": {{ .FullChainPEM | printf "%q" }},
"protected": true
}

printf "%q" ensures the PEM — which contains newlines — becomes a JSON-safe quoted string.

Retry semantics

  • Retries fire on any non-2xx status, transport errors, and read timeouts — a 4xx retries the same request too (the receiver may be mid-deploy; a genuine caller bug simply exhausts the retry budget and fails).
  • Between attempts the backend sleeps with exponential backoff (1s, 2s, 4s, …).
  • Beyond the configured retry_count, the target is marked failed with a retry-eligible classification (io_transient for HTTP errors, network for connection failures) — both qualify for per-target retry at the distribution level.

Security notes

  • HTTPS is not enforced. base_url is checked only for non-emptiness — an http:// endpoint is accepted and used as-is, sending the certificate, and the private key when include_private_key is on, in cleartext. Use https:// yourself for anything beyond a loopback endpoint.
  • Outbound network policy applies: cloud-metadata and link-local addresses are blocked, preventing a misconfigured URL from hitting 169.254.169.254.
  • If include_private_key is ON, review the receiver's handling carefully. The private key is encrypted at rest in CertAutoPilot; after POST, confidentiality depends on the receiver.
  • HMAC signing is built in: attach an hmac_secret credential (slot signing) and the module sends X-Signature-256: sha256=<hex HMAC-SHA256 of the body> — verify it on the receiver with a constant-time compare before trusting the payload.

Rollback

Rollback re-POSTs the previous retained version to the endpoint through the module's normal Execute path — the receiver processes the re-delivered payload. It cannot un-call the POST that already fired, so the receiver should treat re-delivery idempotently. RollbackAvailable is true when an eligible previous version exists.

Troubleshooting

Webhook times out

Increase the timeout or check whether the receiver is slow-processing the payload synchronously. Async receivers should return 202 Accepted immediately.

"400 Bad Request" after a template change

Template syntax errors surface at dry-run, which renders the payload and fails if the template is broken. The request body is deliberately never logged — it carries the certificate, and the private key when include_private_key is on — so only the receiver's sanitised response body appears in the job log.

"network policy blocked"

The URL resolves to a metadata IP or a link-local address. Switch to a public DNS name or an explicit allowlist entry.

See also

Headers CertAutoPilot sets

Every request carries two headers beyond your custom ones:

HeaderValue
X-Idempotency-Key<certificate_id>-<fingerprint> — stable across retries and across a rollback re-send of the same version. This is what a receiver should deduplicate on.
X-TimestampRFC3339 send time.

Fourteen header names are dropped from your custom map if you set them (hop-by-hop and content-framing headers that the client owns).

Redirects are followed

Unlike the Vault, Cloudflare and Azure Key Vault clients — which refuse redirects outright — the webhook client follows up to 3 redirects. A redirect re-sends the certificate body and any bearer token to the redirect target, so only point a webhook at hosts you control.

Retry semantics

retry_count is the total number of attempts, not extra retries: 0 (the default) and 1 both mean a single attempt; set 3 for two retries after the first. Backoff between attempts is exponential.