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_tokencredential (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
IncludePrivateKeyonly 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_secretcredential to the target (slotsigning) and every request carries anX-Signature-256: sha256=<hex>header (HMAC-SHA256 over the exact body). OAuth flows are not built for you.
Create a webhook target
- Settings → Distribution → Targets → New. Module: Webhook.
- Fields:
- URL — the endpoint (HTTPS recommended).
- Headers — map of name → value. Typical:
Authorization: Bearer ...orX-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.
- Save → health check sends an HTTP
HEADto 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_transientfor HTTP errors,networkfor connection failures) — both qualify for per-target retry at the distribution level.
Security notes
- HTTPS is not enforced.
base_urlis checked only for non-emptiness — anhttp://endpoint is accepted and used as-is, sending the certificate, and the private key wheninclude_private_keyis on, in cleartext. Usehttps://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_keyis 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_secretcredential (slotsigning) and the module sendsX-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:
| Header | Value |
|---|---|
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-Timestamp | RFC3339 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).
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.