{"record":{"id":"6832586accab8954","repo":"zeroclaw-labs/zeroclaw","slug":"webhook-channel-requires-a-secret-configured-for","errorCode":null,"errorMessage":"webhook channel requires a `secret` configured for request authentication; set [channels.webhook.{}].secret in config or remove the channel to silence this error","messagePattern":"webhook channel requires a `secret` configured for request authentication; set \\[channels\\.webhook\\.(.+?)\\]\\.secret in config or remove the channel to silence this error","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/webhook.rs","lineNumber":349,"sourceCode":"                            total_attempts,\n                            err_msg,\n                            delay.as_millis()\n                        )\n                    );\n                    tokio::time::sleep(delay).await;\n                }\n            }\n        }\n\n        unreachable!(\"send loop exits via return or bail on the final attempt\")\n    }\n\n    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> Result<()> {\n        // Fail-fast: a webhook with no secret accepts *all* incoming requests,\n        // including unauthenticated ones.  Refuse to start so the operator is\n        // forced to configure a secret.\n        if self.secret.is_none() {\n            anyhow::bail!(\n                \"webhook channel requires a `secret` configured for request \\\n                 authentication; set [channels.webhook.{}].secret in config \\\n                 or remove the channel to silence this error\",\n                self.alias,\n            );\n        }\n\n        use axum::{\n            Router,\n            body::Bytes,\n            extract::State,\n            http::{HeaderMap, StatusCode},\n            routing::post,\n        };\n        use portable_atomic::{AtomicU64, Ordering};\n        use std::sync::Arc;\n\n        let counter = Arc::new(AtomicU64::new(0));","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/webhook.rs#L331-L367","documentation":"WebhookChannel::listen refuses to start when [channels.webhook.<alias>] has no secret configured. This is a deliberate fail-closed guard: the inbound HTTP server accepts arbitrary POSTs, and without a secret it would have to accept unauthenticated requests from anyone. The channel errors immediately at startup, naming the alias and the exact TOML key to set, instead of silently running an open endpoint.","triggerScenarios":"Config declares [channels.webhook.<alias>] with send_url/listen_path but omits secret; secret key set to an empty string; a config generated from an example template that predates the requirement. listen() (channel startup) fails before the axum router binds, so the whole daemon reports the channel as failed.","commonSituations":"First-time setup following an older example config; splitting config into a template and per-env overlay where the overlay held the secret but was not merged; migrating a dev setup (which used to work secretless) to a newer release that made the secret mandatory.","solutions":["Set [channels.webhook.<alias>].secret to a long random value: openssl rand -hex 32.","Configure senders to that webhook to sign payloads with the same secret (x-webhook-signature: sha256=<hex HMAC-SHA256 of the raw body>).","If the webhook channel is not actually needed, remove the whole [channels.webhook.<alias>] section so startup proceeds.","If the secret lives in an env-specific overlay, verify the merged config actually contains the key for that alias (zeroclaw config dump)."],"exampleFix":"# before\n[channels.webhook.main]\nsend_url = \"https://example.com/hook\"\nlisten_path = \"/hook\"\n# listen() -> \"webhook channel requires a `secret` configured ... set [channels.webhook.main].secret\"\n\n# after\n[channels.webhook.main]\nsend_url = \"https://example.com/hook\"\nlisten_path = \"/hook\"\nsecret = \"9f2c...\"   # openssl rand -hex 32; senders must HMAC-SHA256-sign bodies with it","handlingStrategy":"validation","validationCode":"// Before starting channels, assert the webhook alias has a non-empty secret.\nfn validate_webhook_config(alias: &str, cfg: &WebhookChannelConfig) -> anyhow::Result<()> {\n    anyhow::ensure!(\n        cfg.secret.as_deref().map(|s| !s.trim().is_empty()).unwrap_or(false),\n        \"[channels.webhook.{alias}] needs a non-empty `secret` (openssl rand -hex 32)\"\n    );\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"match webhook_channel.listen(tx).await {\n    Ok(()) => {}\n    Err(e) if e.to_string().contains(\"requires a `secret`\") => {\n        // Pure config defect: stop, generate a secret, update config, restart.\n        // Never catch-and-continue — that would run an unauthenticated endpoint.\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Generate secrets with openssl rand -hex 32 and store them via the secret store, not inline in committed configs.","Add a config-lint step in CI that fails when [channels.webhook.*] lacks secret.","Teach senders to produce x-webhook-signature: sha256=<hex HMAC-SHA256 of the raw body> from day one.","When a config template omits the key intentionally, comment it loudly so merges never drop the real secret."],"tags":["webhook","config","security","secret","fail-closed"],"backgroundTag":"missing-webhook-secret","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}