Skip to content

fix(relay): parse env settings strictly and refuse invalid values - #9236

Draft
ThePumpingLemma wants to merge 1 commit into
mainfrom
dgrochowski/strict-env-parsing
Draft

ThePumpingLemma wants to merge 1 commit into
mainfrom
dgrochowski/strict-env-parsing

Conversation

@ThePumpingLemma

@ThePumpingLemma ThePumpingLemma commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The relay reads about 40 settings from the environment in crates/buzz-relay/src/config.rs. Strict helpers already existed (parse_bool, positive_u64_from_env), but most settings parsed by hand, each with its own rules. Bad values were misread or silently replaced by the default.

  • BUZZ_REQUIRE_AUTH_TOKEN, BUZZ_PUBKEY_ALLOWLIST, BUZZ_REQUIRE_RELAY_MEMBERSHIP, BUZZ_ALLOW_NIP_OA_AUTH, BUZZ_MEDIA_UPLOAD_RECORDS and BUZZ_SERVE_GIT_WEB_GUI took only exact true/1. TRUE, On or 1 meant false, so REST token auth or relay membership enforcement stayed off.
  • BUZZ_HUDDLE_AUDIO_AVAILABLE was inverted: anything but exact false/0 meant true, so FALSE left huddle audio on in a multi-pod deployment.
  • BUZZ_V1_ENABLED and the two BUZZ_MEMBERSHIP_*_ENABLED flags took only lowercase true. BUZZ_MESH took On but not TRUE.
  • A numeric typo (BUZZ_MAX_CONNECTIONS=10k) or overflow (BUZZ_SLOW_CLIENT_GRACE_LIMIT=300, a u8) quietly ran with the default. BUZZ_OPERATOR_LISTENER_TIMEOUT_MS logged and did the same.
  • BUZZ_SEND_BUFFER=0 was accepted. It reaches mpsc::channel(0) in connection.rs, which panics on every WebSocket connection. BUZZ_MAX_CONNECTIONS, BUZZ_MAX_CONCURRENT_HANDLERS and BUZZ_GIT_MAX_CONCURRENT_OPS took 0, which sizes a semaphore that admits nothing.

Change

Every on/off setting now goes through parse_bool. Its accepted set does not change: true/1/on or false/0/off, any case, whitespace trimmed, blank means off. yes is still not accepted; it now fails startup instead of meaning false.

One generic parser, parse_optional_number / parse_number, replaces every hand-written numeric parse. It trims the value. Unset or blank means the default. A parse or range failure is a ConfigError that names the variable. parse_positive (counts, sizes, rates) replaces positive_u64_from_env.

  • Settings that already filtered > 0 now reject 0 instead of falling back. BUZZ_SEND_BUFFER, BUZZ_MAX_CONNECTIONS, BUZZ_MAX_CONCURRENT_HANDLERS and BUZZ_GIT_MAX_CONCURRENT_OPS join them.
  • Settings where 0 means something keep accepting it: BUZZ_DRAIN_JITTER_MS, BUZZ_REPLICA_READ_MAX_AGE_MS, BUZZ_GIT_PACK_CACHE_MAX_BYTES, BUZZ_GIT_MAX_REPOS_PER_PUBKEY and the ports.
  • Every default and every existing clamp is unchanged.
  • parse_optional_bool duplicated parse_bool(name, false) and is gone.
  • Two stale comments (in config.rs and main.rs) said a malformed RELAY_OWNER_PUBKEY is warned and ignored. It fails startup, and the comments now say so.
  • .env.example states the accepted spellings. The changelog is generated from PR titles, so there is no hand-written entry.

Behaviour change for deployments with invalid values. A value the relay used to ignore or misread now stops startup with an error that names the variable. TRUE, On and 1 now turn on the flag they name, including the auth and membership flags. Blank numeric values still mean the default. That relaxes the rate limits, BUZZ_REPLICA_READ_MAX_AGE_MS, BUZZ_PUSH_GATEWAY_TIMEOUT_MS and BUZZ_V1_RETENTION_SECONDS, which used to reject blank. A blank BUZZ_HUDDLE_AUDIO_AVAILABLE now means off, like every other parse_bool setting. The chart, compose files, CI and scripts in this repo all use canonical values. The chart needed one fix: Helm rendered integers of a million or more as floats (BUZZ_GIT_MAX_PACK_BYTES="5.24288e+08"), which the relay used to ignore and would now refuse. deployment.yaml now casts every integer setting with int64 before quoting. Chart version 0.1.12.

Testing

New table test config::tests::env_settings_parse_strictly_or_refuse_startup runs each case through Config::from_env.

Before the fix, on origin/main plus the test:

$ cargo test -p buzz-relay --lib config::tests::env_settings_parse_strictly_or_refuse_startup
BUZZ_REQUIRE_AUTH_TOKEN="TRUE": resolved to the wrong value
BUZZ_REQUIRE_AUTH_TOKEN="On": resolved to the wrong value
BUZZ_REQUIRE_AUTH_TOKEN=" 1 ": resolved to the wrong value
BUZZ_REQUIRE_AUTH_TOKEN="yes": accepted
BUZZ_REQUIRE_AUTH_TOKEN="enabled": accepted
...
BUZZ_REQUIRE_RELAY_MEMBERSHIP="TRUE": resolved to the wrong value
BUZZ_HUDDLE_AUDIO_AVAILABLE="FALSE": resolved to the wrong value
BUZZ_MESH="TRUE": resolved to the wrong value
...
BUZZ_SEND_BUFFER="0": accepted
BUZZ_SEND_BUFFER=" 64 ": resolved to the wrong value
BUZZ_MAX_CONNECTIONS="0": accepted
BUZZ_MAX_CONNECTIONS="10k": accepted
BUZZ_MAX_CONCURRENT_HANDLERS="0": accepted
BUZZ_GIT_MAX_CONCURRENT_OPS="0": accepted
BUZZ_SLOW_CLIENT_GRACE_LIMIT="300": accepted
BUZZ_DB_POOL_SIZE="0": accepted
BUZZ_HEALTH_PORT="70000": accepted
BUZZ_GIT_MAX_PACK_BYTES="500MB": accepted
BUZZ_MEDIA_UPLOADS_PER_MINUTE="-1": accepted
BUZZ_OPERATOR_LISTENER_TIMEOUT_MS="50": accepted
test result: FAILED. 0 passed; 1 failed

That run had 69 failing rows (57 bool, 12 numeric). After the fix:

  • cargo test -p buzz-relay --lib config::: 96 passed, 0 failed.
  • cargo test -p buzz-relay --lib -- --skip audio::: 1619 passed, 0 failed, 597 ignored (they need Postgres).
  • cargo clippy -p buzz-relay --all-targets -- -D warnings: clean.
  • cargo fmt --all: no changes.

The DB pool size tests now expect 0 and junk to refuse startup instead of falling back.

Not covered

Review

Agent review found one blocker, now fixed in this commit:

  • The Helm chart rendered the git byte limits in scientific notation. With default values it produced BUZZ_GIT_MAX_PACK_BYTES="5.24288e+08" and BUZZ_GIT_PACK_CACHE_MAX_BYTES="5.36870912e+09". Strict parsing would have stopped every chart install at startup. deployment.yaml now casts every integer env value with | int64 | quote, and the chart is bumped to 0.1.12. The new helm-unittest case "renders large integer settings as plain integers" fails without the casts.

Also checked, no issues: blank huddle audio is off and the chart never renders it blank, blank numeric values fall back to the default, the zero rejection list is correct, every default and clamp is unchanged, every startup error names its variable, and tests that change env vars hold env_guards().

Open optional items:

  • parse_bool's error says "must be true or false" but also accepts 1/0/on/off. Listing the accepted spellings would make the new startup failures easier to fix.
  • BUZZ_MAX_COMMUNITIES_PER_OWNER (buzz-db) and nip_fi_config still parse on their own. Already listed under "Not covered".

Generated with Claude Code

Exercised on a local relay

Commit tested: 23afef733. Dev profile, isolated harness relay on :3030. Each setting was tested by restarting the relay with the variable changed.

Env Startup Observed
BUZZ_REQUIRE_AUTH_TOKEN=false (harness default) starts logs the "REST API requests bypass token auth" warning. POST /query with only the dev X-Pubkey header returns 200 with events
BUZZ_REQUIRE_AUTH_TOKEN=TRUE starts no bypass warning. The same X-Pubkey-only /query returns 401 {"error":"missing Nostr auth"}. NIP-98 requests and WS NIP-42 auth still work
BUZZ_SEND_BUFFER=0 refused, exit 1 Error: Configuration error: invalid config: BUZZ_SEND_BUFFER must be a positive integer
BUZZ_REQUIRE_AUTH_TOKEN=yes refused, exit 1 Error: Configuration error: invalid config: BUZZ_REQUIRE_AUTH_TOKEN must be true or false

Chart: helm template with the chart's ci/quickstart-values.yaml (rendered from a temp copy with helm dependency build). Every integer env renders as a plain quoted integer, including BUZZ_GIT_MAX_PACK_BYTES: "524288000" and BUZZ_GIT_PACK_CACHE_MAX_BYTES: "5368709120". No float or e+ value appears anywhere in the output. The bool envs render as "true"/"false".

Nit: the refusal says must be true or false, but 1/0/on/off are also accepted. Naming the accepted spellings would help operators.

Postgres-gated tests: the PR adds no ignored tests, and --ignored config:: matches 0. With BUZZ_TEST_DATABASE_URL set, the non-ignored cargo test -p buzz-relay --lib -- config:: gives 96 passed, 0 failed.

Not exercised:

  • The other on/off and numeric variables on a live relay; the table test covers them.
  • The membership/v1 flags.

Problem: config.rs already had strict helpers (parse_bool and
positive_u64_from_env), but most settings parsed by hand, each with its
own rules:

- BUZZ_REQUIRE_AUTH_TOKEN, BUZZ_PUBKEY_ALLOWLIST,
  BUZZ_REQUIRE_RELAY_MEMBERSHIP, BUZZ_ALLOW_NIP_OA_AUTH,
  BUZZ_MEDIA_UPLOAD_RECORDS and BUZZ_SERVE_GIT_WEB_GUI accepted only
  exact "true"/"1". TRUE, True, On or " 1 " silently meant false, which
  left REST token auth or relay membership enforcement off.
- BUZZ_HUDDLE_AUDIO_AVAILABLE was inverted: anything except exact
  "false"/"0" meant true, so FALSE or Off left huddle audio on.
- BUZZ_V1_ENABLED and the two BUZZ_MEMBERSHIP_*_ENABLED flags accepted
  only lowercase "true". BUZZ_MESH and BUZZ_MESH_DEMO_ECHO took any-case
  "on" but only lowercase "true".
- Numeric settings fell back to the default on any parse failure, so a
  typo like 10k or a u8 overflow (BUZZ_SLOW_CLIENT_GRACE_LIMIT=300)
  quietly ran with the default. BUZZ_OPERATOR_LISTENER_TIMEOUT_MS
  logged an error and did the same.
- BUZZ_SEND_BUFFER=0 was accepted and reaches mpsc::channel(0) for every
  WebSocket connection, which panics. BUZZ_MAX_CONNECTIONS,
  BUZZ_MAX_CONCURRENT_HANDLERS and BUZZ_GIT_MAX_CONCURRENT_OPS accepted
  0, which sizes a semaphore that admits nothing.

Change: every on/off setting now goes through parse_bool. Its accepted
set is unchanged: true/1/on or false/0/off in any case, surrounding
whitespace ignored, blank meaning off. "yes" stays unaccepted and now
fails startup instead of silently meaning false. One generic
parse_optional_number / parse_number pair replaces every hand-written
numeric parse. It trims the value, treats unset or blank as the default
and turns a parse or range failure into a ConfigError naming the
variable. parse_positive, the thin wrapper for counts, sizes and rates,
replaces positive_u64_from_env. Settings that already filtered `> 0` now
reject 0 instead of falling back, and the four settings above join them.
Settings where 0 has a meaning keep accepting it (BUZZ_DRAIN_JITTER_MS,
BUZZ_REPLICA_READ_MAX_AGE_MS, BUZZ_GIT_PACK_CACHE_MAX_BYTES,
BUZZ_GIT_MAX_REPOS_PER_PUBKEY, ports). Existing clamps
(BUZZ_PARTITION_AUDIT_INTERVAL_SECS, BUZZ_DRAIN_JITTER_MS,
BUZZ_MEDIA_MAX_CONCURRENT_UPLOADS_PER_PUBKEY) and every default are
unchanged. parse_optional_bool was a duplicate of parse_bool(name,
false) and is removed. Two stale comments now say that a malformed
RELAY_OWNER_PUBKEY fails startup (config.rs and main.rs).

Behaviour change for deployments: a value the relay used to ignore or
misread now stops startup with an error naming the variable. TRUE, On
and " 1 " now enable the security flags they were meant to enable.
Blank numeric values still mean the default; for the rate limits,
BUZZ_REPLICA_READ_MAX_AGE_MS, BUZZ_PUSH_GATEWAY_TIMEOUT_MS and
BUZZ_V1_RETENTION_SECONDS that is a relaxation (blank used to fail).
A blank BUZZ_HUDDLE_AUDIO_AVAILABLE now means off, as it does for every
other parse_bool setting.

Helm chart: Helm renders integers of a million or more as floats, so
the chart passed BUZZ_GIT_MAX_PACK_BYTES="5.24288e+08" and
BUZZ_GIT_PACK_CACHE_MAX_BYTES="5.36870912e+09". The relay used to
ignore both and run with its own defaults; strict parsing would refuse
to start every chart install. deployment.yaml now casts every integer
setting with int64 before quoting (as the storage-accounting CronJob
already does), and the chart version is bumped to 0.1.12.

Why safe: defaults are unchanged and every value in the chart, compose
files, CI and scripts now renders a canonical spelling. The new failures
only fire on values that were already being ignored or misread.

Tests: env_settings_parse_strictly_or_refuse_startup is a table test
through Config::from_env. It fails on main with 69 mismatches (e.g.
BUZZ_REQUIRE_AUTH_TOKEN="TRUE": resolved to the wrong value;
BUZZ_SEND_BUFFER="0": accepted) and passes with this change. The DB
pool size tests now assert refusal instead of fallback. The chart test
"renders large integer settings as plain integers" fails without the
int64 casts.

Signed-off-by: David Grochowski <dgrochowski@squareup.com>
@ThePumpingLemma
ThePumpingLemma force-pushed the dgrochowski/strict-env-parsing branch from 71db066 to 23afef7 Compare November 7, 2026 04:44

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant