mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Remove public relay signing key fallback (#6729)
Require `BUZZ_RELAY_PRIVATE_KEY` on every relay startup and remove the shared public fallback key completely. For local development, `just bootstrap` now generates a random key once in the gitignored `.env` file. Re-running bootstrap preserves that key, so relay-authored events retain the same identity across restarts. The local relay recipes reload `.env`, while isolated CI and E2E launchers pass explicit per-run keys. Startup fails immediately when the key is missing or invalid, before connecting to Postgres or Redis. Deployed relays continue to receive their stable key from the existing chart-managed secret. Validated with: - `./scripts/test-ensure-local-relay-key.sh` - `cargo test -p buzz-relay --bin buzz-relay` - `cargo clippy -p buzz-relay --bin buzz-relay -- -D warnings` - `cargo fmt --all -- --check` - `shellcheck scripts/ensure-local-relay-key.sh scripts/test-ensure-local-relay-key.sh` Signed-off-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
@@ -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
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -529,6 +529,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 \
|
||||
@@ -714,6 +715,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 \
|
||||
|
||||
21
Justfile
21
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}"
|
||||
|
||||
11
TESTING.md
11
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
|
||||
|
||||
@@ -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<nostr::Keys> {
|
||||
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()));
|
||||
|
||||
@@ -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)
|
||||
|
||||
66
scripts/ensure-local-relay-key.sh
Executable file
66
scripts/ensure-local-relay-key.sh
Executable file
@@ -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}."
|
||||
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 \
|
||||
|
||||
33
scripts/test-ensure-local-relay-key.sh
Executable file
33
scripts/test-ensure-local-relay-key.sh
Executable file
@@ -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"
|
||||
Reference in New Issue
Block a user