v0.4.0 — Custom webhook authentication (Bearer + mTLS)
Minor release. Closes the long-standing gap in the custom-webhook authentication story: the channel now ships with two additive auth mechanisms — a single arbitrary HTTP header (Bearer / API key / HMAC-style; #7) and full mutual-TLS client authentication (cert + key + optional CA bundle; #8). Both flow through the same memfd-backed curl -K config the existing alert-credential isolation already used, so the curl child's argv is byte-identical regardless of which auth modes are configured — /proc/*/cmdline reveals nothing about which channel or which credentials are in play. No breaking changes.
What's new
webhook_auth_header — single HTTP header for the custom webhook (#7)
webhook_url = https://siem.example.com/ingest/pamsignal
webhook_auth_header = Authorization: Bearer <token>Works with any single-header auth scheme: Bearer / OAuth, X-API-Key, Authorization: Splunk <token> (HEC), DD-API-KEY (Datadog), Wazuh API JWT, custom HMAC headers, etc. The header value is rendered into a memfd-backed curl -K config file — the secret never appears in argv, /proc/<pid>/cmdline, or any process listing. Validator at config load rejects CRLF / quote / backslash injection and refuses the key set without webhook_url.
Previously the only authenticated path was a reverse proxy in front of the receiver injecting the header — the bundled examples/nodejs-webhook/ reference receiver explicitly required this setup. The example README now documents direct configuration as the recommended path.
webhook_client_cert / webhook_client_key / webhook_ca_bundle — mTLS client auth (#8)
webhook_url = https://siem.internal.example.com/ingest
webhook_client_cert = /etc/pamsignal/webhook-client.crt
webhook_client_key = /etc/pamsignal/webhook-client.key
# Optional, only if the receiver's CA isn't in the system trust store
webhook_ca_bundle = /etc/pamsignal/internal-ca.pemTargets the operator profile that already runs PKI (internal CA + cert-manager / SPIFFE / service-mesh issuance). Stronger than Bearer alone — the private key never travels over the wire, replay windows don't exist, credential rotation is delegated to the cert-management pipeline. Combines additively with webhook_auth_header — the corporate-SIEM / Wazuh pattern (mTLS for transport-layer service identity, Bearer for per-request authorization).
Validator at config load: O_NOFOLLOW open (symlinks rejected), regular-file required, ownership outside {root, daemon-uid} rejected, webhook_client_key additionally rejected if group- or world-readable. Path strings constrained to printable ASCII minus " and \ for unambiguous handling in the curl -K config. Cross-field consistency: cert without key (or key without cert) is a config-load error; any TLS key without webhook_url is rejected.
Documentation refresh
docs/configuration.md— full "Custom webhook authentication" reference covering both modes, combined-pattern examples, and the validation rules.docs/architecture.md— new "Curl invocation (memfd-backed config)" subsection.docs/alerts.md— "Authentication (optional)" + "Mutual TLS (optional, advanced)" subsections under "Custom webhook (ECS JSON)".docs/deployment.md— cert/key file placement guidance.docs/threat-model.md— attack #3 extended (covers the new auth surface), NS8 rewritten (Bearer + mTLS now ship; HMAC-signed payloads remain out of scope).pamsignal.8.in— SECURITY + FILES sections updated.examples/nodejs-webhook/— adds an mTLS-enabledhttps.createServervariant.
Internal changes (no operator impact)
post_alert()andbuild_secrets_memfd()insrc/notify.cswap from positional arguments to acurl_config_tstruct that carries the optional auth header and TLS paths by name. The four legacy callers (Telegram, Slack/Teams/Discord, WhatsApp) pass struct-literal arguments and produce identical wire output to v0.3.x.build_secrets_memfdbuffer grows 2048 → 4096 to fit up to four config lines without truncation. Eachsnprintfappend is bounds-checked.
Tests
CMocka test_config suite grew 33 → 53 (20 new cases): 9 for webhook_auth_header parse + validation, 11 for mTLS parse + perm validation including symlink rejection, world/group-readable key rejection, missing-cert-or-key, no-webhook_url, path-with-quote, and combined Bearer+mTLS load. All 4 suites pass under ASAN + UBSAN. clang-tidy clean. OWASP ASVS L1 audit zero FAIL findings.
Upgrading
sudo apt upgrade pamsignal # Debian / Ubuntu
sudo dnf upgrade pamsignal # Fedora / EL9Existing v0.3.x configs continue to work unchanged. To enable the new auth options, see Configuration → Custom webhook authentication.
Closed issues
- #7 —
webhook_auth_headerfor the custom webhook channel - #8 — Optional mTLS client auth for the custom webhook channel
Full changelog: v0.3.4...v0.4.0