From ceb38fda9cf4ba05f81acbe7f914ae2cd080af4e Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Mon, 24 Aug 2026 14:41:33 -0700 Subject: [PATCH] Remove public relay key fallback Signed-off-by: Jordan Mecom --- .env.example | 4 +- .github/workflows/ci.yml | 2 + Justfile | 21 +++++++- TESTING.md | 11 +++-- crates/buzz-relay/src/main.rs | 55 +++++++++++---------- scripts/e2e-git-perms.sh | 1 + scripts/ensure-local-relay-key.sh | 66 ++++++++++++++++++++++++++ scripts/run-desktop-release-smoke.sh | 2 + scripts/start-isolated-test-relay.sh | 2 + scripts/start-relay-for-tests.sh | 5 +- scripts/test-ensure-local-relay-key.sh | 33 +++++++++++++ 11 files changed, 169 insertions(+), 33 deletions(-) create mode 100755 scripts/ensure-local-relay-key.sh create mode 100755 scripts/test-ensure-local-relay-key.sh diff --git a/.env.example b/.env.example index 0f7bbba6f13..cb503392b30 100644 --- a/.env.example +++ b/.env.example @@ -51,8 +51,8 @@ TYPESENSE_URL=http://localhost:8108 BUZZ_BIND_ADDR=0.0.0.0:3000 # Public WebSocket URL — used in NIP-42 auth challenges RELAY_URL=ws://localhost:3000 -# Stable relay signing key. Set this in dev if you want REST-created forum posts -# to keep resolving to the original author across relay restarts. +# Stable relay signing key (required). `just bootstrap` generates a random key in +# the gitignored .env file. Preserve that value across restarts and backups. # BUZZ_RELAY_PRIVATE_KEY=<32-byte hex private key> # Optional: path to the web UI dist directory. When set, the relay serves # the web frontend at / for browser requests. Leave unset for local dev diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59b9a73ec9b..f01a86959a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -522,6 +522,7 @@ jobs: REDIS_URL=redis://localhost:6379 \ RELAY_URL=ws://localhost:3000 \ BUZZ_BIND_ADDR=0.0.0.0:3000 \ + BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \ BUZZ_REQUIRE_AUTH_TOKEN=false \ BUZZ_RECONCILE_CHANNELS=true \ BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=100000 \ @@ -682,6 +683,7 @@ jobs: REDIS_URL=redis://localhost:6379 \ RELAY_URL=ws://localhost:3000 \ BUZZ_BIND_ADDR=0.0.0.0:3000 \ + BUZZ_RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" \ BUZZ_REQUIRE_AUTH_TOKEN=false \ BUZZ_RECONCILE_CHANNELS=true \ BUZZ_GIT_PROBE_WRITERS=8 \ diff --git a/Justfile b/Justfile index fe5d7bf2858..6c7740bc7ac 100644 --- a/Justfile +++ b/Justfile @@ -43,6 +43,7 @@ bootstrap: cp .env.example .env echo "Created .env from .env.example — review it before running just dev." fi + ./scripts/ensure-local-relay-key.sh .env # Start Docker services, run migrations, install desktop deps setup: bootstrap @@ -307,6 +308,7 @@ test: test-unit: #!/usr/bin/env bash set -euo pipefail + ./scripts/test-ensure-local-relay-key.sh if command -v cargo-nextest &>/dev/null; then cargo nextest run -p buzz-core -p buzz-auth --lib cargo nextest run -p buzz-voice --lib @@ -423,6 +425,9 @@ relay: bootstrap _ensure-migrations #!/usr/bin/env bash set -euo pipefail export PATH="{{justfile_directory()}}/bin:$PATH" + set -o allexport + source .env + set +o allexport cargo run -p buzz-relay # Start the relay with the built web UI served from it @@ -430,6 +435,9 @@ relay-web: bootstrap _ensure-migrations #!/usr/bin/env bash set -euo pipefail export PATH="{{justfile_directory()}}/bin:$PATH" + set -o allexport + source .env + set +o allexport [[ -d node_modules ]] || pnpm install pnpm -C web build BUZZ_WEB_DIR=./web/dist cargo run -p buzz-relay @@ -439,6 +447,9 @@ admin: bootstrap _ensure-migrations #!/usr/bin/env bash set -euo pipefail export PATH="{{justfile_directory()}}/bin:$PATH" + set -o allexport + source .env + set +o allexport [[ -d node_modules ]] || pnpm install pnpm -C admin-web build export BUZZ_ADMIN_HOST="${BUZZ_ADMIN_HOST:-admin.localhost:3000}" @@ -459,7 +470,12 @@ admin-check: fmt-check pnpm -C admin-web exec playwright test # Start the relay server in release mode -relay-release: _ensure-migrations +relay-release: bootstrap _ensure-migrations + #!/usr/bin/env bash + set -euo pipefail + set -o allexport + source .env + set +o allexport cargo run -p buzz-relay --release @@ -468,6 +484,9 @@ dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations #!/usr/bin/env bash set -euo pipefail export PATH="{{justfile_directory()}}/bin:$PATH" + set -o allexport + source .env + set +o allexport bind_addr="${BUZZ_BIND_ADDR:-0.0.0.0:3000}" relay_port="${bind_addr##*:}"; [[ -n "$relay_port" ]] || relay_port=3000 health_port="${BUZZ_HEALTH_PORT:-8080}" diff --git a/TESTING.md b/TESTING.md index 29d07a80de0..0e64b740665 100644 --- a/TESTING.md +++ b/TESTING.md @@ -29,7 +29,7 @@ CLI signs every request with NIP-98, so you don't need `nak` or hand-rolled ```bash . ./bin/activate-hermit # activate pinned toolchain -cp .env.example .env # one-time +just bootstrap # create .env and its stable relay key once just setup # start Docker services, run migrations ``` @@ -70,6 +70,9 @@ Rebuild after any code change — the steps below use the release binaries. In a separate terminal (it runs in the foreground): ```bash +set -o allexport +source .env # includes the key generated by just bootstrap +set +o allexport buzz-relay # release binary from step 2, serves ws://localhost:3000 # alternatives: # cargo run --release -p buzz-relay # rebuild + run in release @@ -89,9 +92,9 @@ curl -s http://localhost:8080/_readiness # → {"status":"ready"} > `BUZZ_HEALTH_PORT`) so K8s probes bypass auth middleware. The main app > port also exposes `/health` for convenience. -The relay starts in dev mode (`BUZZ_REQUIRE_AUTH_TOKEN=false`). The startup -log emits a WARN about this — that's expected for local testing. See the env -vars table at the bottom if you need to lock it down. +The relay starts in dev mode (`BUZZ_REQUIRE_AUTH_TOKEN=false`) with the stable +relay identity generated in `.env`. See the env vars table at the bottom if +you need to lock it down. > **Already running Buzz Desktop (or another relay) on `:3000` / `:8080` / > `:9102`?** Buzz binds three ports — main, health, metrics — and any of diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index 566b684f830..3e39b22f8b2 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -35,6 +35,16 @@ fn buzz_auto_migrate_enabled(value: Option<&str>) -> bool { }) } +fn relay_keypair_from_config(relay_private_key: Option<&str>) -> anyhow::Result { + let hex = relay_private_key.ok_or_else(|| { + anyhow::anyhow!( + "BUZZ_RELAY_PRIVATE_KEY must be set. Run `just bootstrap` for local \ + development or configure a stable 32-byte hex private key." + ) + })?; + nostr::Keys::parse(hex).map_err(|e| anyhow::anyhow!("invalid BUZZ_RELAY_PRIVATE_KEY: {e}")) +} + /// Controls how many per-community gauge series the usage poller emits. /// /// Datadog cost is proportional to the number of unique time-series. With ~25 @@ -143,6 +153,7 @@ async fn main() -> anyhow::Result<()> { error!("Invalid configuration: {e}"); anyhow::anyhow!("Configuration error: {e}") })?; + let relay_keypair = relay_keypair_from_config(config.relay_private_key.as_deref())?; info!( bind_addr = %config.bind_addr, relay_url = %config.relay_url, @@ -422,29 +433,6 @@ async fn main() -> anyhow::Result<()> { let workflow_config = buzz_workflow::WorkflowConfig::default(); let workflow_engine = Arc::new(WorkflowEngine::new(db.clone(), workflow_config)); - let relay_keypair = if let Some(hex) = &config.relay_private_key { - nostr::Keys::parse(hex) - .map_err(|e| anyhow::anyhow!("invalid BUZZ_RELAY_PRIVATE_KEY: {e}"))? - } else if !config.require_auth_token { - // Dev mode: use a deterministic keypair so addressable events (kind:39000/39001/39002) - // replace correctly across restarts. Without this, each restart generates a new pubkey - // and replace_addressable_event inserts duplicates instead of replacing. - const DEV_RELAY_PRIVKEY: &str = - "0000000000000000000000000000000000000000000000000000000000000001"; - let keys = nostr::Keys::parse(DEV_RELAY_PRIVKEY).expect("hardcoded dev key is valid"); - tracing::warn!( - pubkey = %keys.public_key().to_hex(), - "Using hardcoded dev relay keypair (BUZZ_REQUIRE_AUTH_TOKEN=false). \ - Set BUZZ_RELAY_PRIVATE_KEY for production." - ); - keys - } else { - panic!( - "BUZZ_RELAY_PRIVATE_KEY must be set when BUZZ_REQUIRE_AUTH_TOKEN=true. \ - A stable relay identity is required for production." - ); - }; - config .media .validate() @@ -2037,8 +2025,8 @@ mod tests { use super::{ buzz_auto_migrate_enabled, dropped_in_memory_keys, idle_timeout_secs, - refresh_legacy_active_gauge_recency, run_periodic_until_cancelled, EmissionScope, - InMemoryMetricKey, + refresh_legacy_active_gauge_recency, relay_keypair_from_config, + run_periodic_until_cancelled, EmissionScope, InMemoryMetricKey, }; use metrics::GaugeFn; use metrics_util::{ @@ -2086,6 +2074,23 @@ mod tests { assert!(buzz_auto_migrate_enabled(Some("on"))); } + #[test] + fn configured_relay_identity_is_preserved() { + let configured = nostr::Keys::generate(); + let secret = configured.secret_key().to_secret_hex(); + + let selected = relay_keypair_from_config(Some(&secret)).expect("configured key"); + + assert_eq!(selected.public_key(), configured.public_key()); + } + + #[test] + fn missing_relay_identity_is_rejected() { + let result = relay_keypair_from_config(None); + + assert!(result.is_err()); + } + #[test] fn test_emission_scope_off_disallows_every_community() { assert!(EmissionScope::All.allows(&Uuid::new_v4())); diff --git a/scripts/e2e-git-perms.sh b/scripts/e2e-git-perms.sh index f6a6a93063e..bc507a7ff31 100755 --- a/scripts/e2e-git-perms.sh +++ b/scripts/e2e-git-perms.sh @@ -336,6 +336,7 @@ export BUZZ_GIT_HOOK_HMAC_SECRET="${HMAC_SECRET}" export BUZZ_BIND_ADDR="${RELAY_HOST}:${RELAY_PORT}" export RELAY_URL="${RELAY_WS}" export RUST_LOG="buzz_relay=warn" +export BUZZ_RELAY_PRIVATE_KEY="${BUZZ_RELAY_PRIVATE_KEY:-$(openssl rand -hex 32)}" export BUZZ_REQUIRE_AUTH_TOKEN=false # Clean repos dir (isolated test state) diff --git a/scripts/ensure-local-relay-key.sh b/scripts/ensure-local-relay-key.sh new file mode 100755 index 00000000000..8cf926a3d74 --- /dev/null +++ b/scripts/ensure-local-relay-key.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +ENV_FILE="${1:-.env}" + +if [[ ! -f "${ENV_FILE}" ]]; then + echo "error: ${ENV_FILE} does not exist" >&2 + exit 1 +fi + +existing_key="$({ + unset BUZZ_RELAY_PRIVATE_KEY + set +u + # shellcheck disable=SC1090 + source "${ENV_FILE}" || exit 1 + printf '%s' "${BUZZ_RELAY_PRIVATE_KEY:-}" +})" + +if [[ -n "${existing_key}" ]]; then + chmod 600 "${ENV_FILE}" + exit 0 +fi + +relay_key="$(node <<'NODE' +const { randomBytes } = require("node:crypto"); +const curveOrder = BigInt( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", +); + +let bytes; +let scalar; +do { + bytes = randomBytes(32); + scalar = BigInt(`0x${bytes.toString("hex")}`); +} while (scalar === 0n || scalar >= curveOrder); + +process.stdout.write(bytes.toString("hex")); +NODE +)" + +temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" +trap 'rm -f "${temp_file}"' EXIT + +awk -v key="${relay_key}" ' + BEGIN { replaced = 0 } + /^[[:space:]]*(export[[:space:]]+)?BUZZ_RELAY_PRIVATE_KEY=/ { + if (!replaced) { + print "BUZZ_RELAY_PRIVATE_KEY=" key + replaced = 1 + } + next + } + { print } + END { + if (!replaced) { + if (NR > 0) print "" + print "BUZZ_RELAY_PRIVATE_KEY=" key + } + } +' "${ENV_FILE}" > "${temp_file}" + +chmod 600 "${temp_file}" +mv "${temp_file}" "${ENV_FILE}" +trap - EXIT + +echo "Generated BUZZ_RELAY_PRIVATE_KEY in ${ENV_FILE}." diff --git a/scripts/run-desktop-release-smoke.sh b/scripts/run-desktop-release-smoke.sh index 33f7013ab3e..85aee2e05cc 100755 --- a/scripts/run-desktop-release-smoke.sh +++ b/scripts/run-desktop-release-smoke.sh @@ -103,6 +103,7 @@ else RELAY_BIN="${ROOT}/target/ci/buzz-relay" fi log "starting relay at ${RELAY_HTTP_URL}" +RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" env \ DATABASE_URL="postgres://buzz:buzz_dev@localhost:5432/${DB_NAME}" \ REDIS_URL="redis://localhost:6379/${REDIS_DB}" \ @@ -110,6 +111,7 @@ env \ BUZZ_BIND_ADDR="127.0.0.1:${RELAY_PORT}" \ BUZZ_HEALTH_PORT="${HEALTH_PORT}" \ BUZZ_METRICS_PORT="${METRICS_PORT}" \ + BUZZ_RELAY_PRIVATE_KEY="${RELAY_PRIVATE_KEY}" \ BUZZ_REQUIRE_AUTH_TOKEN=false \ BUZZ_RECONCILE_CHANNELS=true \ BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=1000000 \ diff --git a/scripts/start-isolated-test-relay.sh b/scripts/start-isolated-test-relay.sh index 1e2047e99c9..1f6b6620c13 100755 --- a/scripts/start-isolated-test-relay.sh +++ b/scripts/start-isolated-test-relay.sh @@ -133,6 +133,7 @@ ok "Relay built" # survives (same pattern the perf stack uses). Logs to ${RELAY_LOG}. RELAY_LOG="${RELAY_LOG:-/tmp/dawn-relay-run.log}" TMUX_SESSION="${TMUX_SESSION:-dawn-relay}" +RELAY_PRIVATE_KEY="$(openssl rand -hex 32)" tmux kill-session -t "${TMUX_SESSION}" 2>/dev/null || true if command -v lsof >/dev/null 2>&1 && lsof -nP -iTCP:"${RELAY_MAIN}" -sTCP:LISTEN >/dev/null 2>&1; then err "Port ${RELAY_MAIN} is already in use; refusing to report a stale relay as this harness." @@ -151,6 +152,7 @@ tmux new-session -d -s "${TMUX_SESSION}" "cd '${REPO_ROOT}' && env \ BUZZ_S3_ACCESS_KEY=buzz_dev \ BUZZ_S3_SECRET_KEY=buzz_dev_secret \ BUZZ_S3_BUCKET=buzz-media \ + BUZZ_RELAY_PRIVATE_KEY=${RELAY_PRIVATE_KEY} \ BUZZ_REQUIRE_AUTH_TOKEN=false \ BUZZ_RECONCILE_CHANNELS=true \ './target/${CARGO_TARGET_PROFILE}/buzz-relay' > '${RELAY_LOG}' 2>&1" diff --git a/scripts/start-relay-for-tests.sh b/scripts/start-relay-for-tests.sh index b9d93935c08..4bd1a274084 100755 --- a/scripts/start-relay-for-tests.sh +++ b/scripts/start-relay-for-tests.sh @@ -152,6 +152,8 @@ fi log "Starting relay..." +TEST_RELAY_PRIVATE_KEY="${BUZZ_RELAY_PRIVATE_KEY:-$(openssl rand -hex 32)}" + # Optional NIP-43 membership gating: exported by callers that need a # membership-gated relay (e.g. the mesh lifecycle smoke). All three must be # set together — the relay fails fast otherwise. @@ -160,8 +162,8 @@ if [[ "${BUZZ_REQUIRE_RELAY_MEMBERSHIP:-}" == "true" ]]; then MEMBERSHIP_ENV+=( BUZZ_REQUIRE_RELAY_MEMBERSHIP=true RELAY_OWNER_PUBKEY="${RELAY_OWNER_PUBKEY:?RELAY_OWNER_PUBKEY required with BUZZ_REQUIRE_RELAY_MEMBERSHIP=true}" - BUZZ_RELAY_PRIVATE_KEY="${BUZZ_RELAY_PRIVATE_KEY:?BUZZ_RELAY_PRIVATE_KEY required with BUZZ_REQUIRE_RELAY_MEMBERSHIP=true}" ) + : "${BUZZ_RELAY_PRIVATE_KEY:?BUZZ_RELAY_PRIVATE_KEY required with BUZZ_REQUIRE_RELAY_MEMBERSHIP=true}" log "Membership gating enabled (NIP-43)" fi @@ -170,6 +172,7 @@ nohup env \ REDIS_URL=redis://localhost:6379 \ RELAY_URL=ws://localhost:3000 \ BUZZ_BIND_ADDR=0.0.0.0:3000 \ + BUZZ_RELAY_PRIVATE_KEY="${TEST_RELAY_PRIVATE_KEY}" \ BUZZ_REQUIRE_AUTH_TOKEN=false \ BUZZ_RECONCILE_CHANNELS=true \ BUZZ_GIT_PROBE_WRITERS=8 \ diff --git a/scripts/test-ensure-local-relay-key.sh b/scripts/test-ensure-local-relay-key.sh new file mode 100755 index 00000000000..be87100557e --- /dev/null +++ b/scripts/test-ensure-local-relay-key.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/buzz-relay-key-test.XXXXXX")" +trap 'rm -rf "${TEST_DIR}"' EXIT + +ENV_FILE="${TEST_DIR}/.env" +cp "${REPO_ROOT}/.env.example" "${ENV_FILE}" + +"${SCRIPT_DIR}/ensure-local-relay-key.sh" "${ENV_FILE}" >/dev/null +first_key="$(sed -n 's/^BUZZ_RELAY_PRIVATE_KEY=//p' "${ENV_FILE}")" + +if [[ ! "${first_key}" =~ ^[0-9a-f]{64}$ ]]; then + echo "FAIL: bootstrap did not generate a valid 32-byte hex relay key" >&2 + exit 1 +fi + +"${SCRIPT_DIR}/ensure-local-relay-key.sh" "${ENV_FILE}" >/dev/null +second_key="$(sed -n 's/^BUZZ_RELAY_PRIVATE_KEY=//p' "${ENV_FILE}")" + +if [[ "${first_key}" != "${second_key}" ]]; then + echo "FAIL: bootstrap replaced the existing relay key" >&2 + exit 1 +fi + +if [[ "$(grep -c '^BUZZ_RELAY_PRIVATE_KEY=' "${ENV_FILE}")" -ne 1 ]]; then + echo "FAIL: bootstrap wrote more than one relay key" >&2 + exit 1 +fi + +echo "PASS: bootstrap generates one relay key and reuses it"