v0.18.0 Slice 3: webhook tightening — mandatory secret + dev-bypass
`GITEA_WEBHOOK_SECRET` is now mandatory at startup. The framework
refuses to load_config() when the env var is empty unless the
operator opts into the dev-bypass with `RFC_APP_INSECURE_WEBHOOKS=1`.
This is the v0.18.0 startup-loud-failure shape — the pre-v0.18.0
"silently accept unsigned POSTs when the secret is empty" path is
the bug the proposal targets.
`webhooks.receive`:
* Defense in depth: refuses 500 if the secret is empty at request
time and the dev-bypass is not set (catches the case where
something mutates env after startup).
* Logs a loud warning every time a webhook lands under the
bypass — so a misconfigured production deployment shows up in
the logs even if the operator missed the warning at boot.
* Adds an INFO log at the unknown-repo branch (previously
silently 200-OK'd a hook on a fork or a stale Gitea binding).
`tmp_env` fixture binds a fake secret so the existing 277 tests
boot cleanly; the new test_webhooks_vertical.py exercises both
the production-secret path (valid signature, invalid signature,
missing signature) and the dev-bypass path (config loads with
empty secret when bypass set, refuses without it).
7 new tests; full suite: 284 passed.
This commit is contained in:
+15
-1
@@ -60,6 +60,20 @@ def load_config() -> Config:
|
||||
|
||||
enabled = [m.strip() for m in _optional("ENABLED_MODELS", "claude").split(",") if m.strip()]
|
||||
|
||||
# v0.18.0: `GITEA_WEBHOOK_SECRET` is now mandatory (per the
|
||||
# email + webhook hygiene proposal). An empty value used to
|
||||
# silently accept unsigned webhook POSTs — that was the
|
||||
# invisible-failure shape the proposal targets. Now the
|
||||
# framework refuses to start when the secret is empty unless
|
||||
# the operator opts into the dev-bypass with
|
||||
# `RFC_APP_INSECURE_WEBHOOKS=1`. Local-dev deployments without
|
||||
# a wired Gitea hook set the bypass; production MUST NOT.
|
||||
insecure_webhooks = os.environ.get("RFC_APP_INSECURE_WEBHOOKS", "").strip() == "1"
|
||||
if insecure_webhooks:
|
||||
webhook_secret = _optional("GITEA_WEBHOOK_SECRET")
|
||||
else:
|
||||
webhook_secret = _required("GITEA_WEBHOOK_SECRET")
|
||||
|
||||
return Config(
|
||||
gitea_url=_required("GITEA_URL").rstrip("/"),
|
||||
gitea_bot_user=_required("GITEA_BOT_USER"),
|
||||
@@ -72,7 +86,7 @@ def load_config() -> Config:
|
||||
secret_key=_required("SECRET_KEY"),
|
||||
database_path=database_path,
|
||||
owner_gitea_login=_optional("OWNER_GITEA_LOGIN"),
|
||||
webhook_secret=_optional("GITEA_WEBHOOK_SECRET"),
|
||||
webhook_secret=webhook_secret,
|
||||
enabled_models=enabled,
|
||||
anthropic_api_key=_optional("ANTHROPIC_API_KEY"),
|
||||
google_api_key=_optional("GOOGLE_API_KEY"),
|
||||
|
||||
Reference in New Issue
Block a user