mirror of
https://github.com/block/buzz.git
synced 2026-11-12 12:22:06 +00:00
Fixes all six HIGH findings from the buzz security report, one commit per finding. Independently reviewed to approval by Max at `0158ae542`, plus a deep isolated live pass (clean-room compose stack, weird ports, full product matrix) at the same head — see the buzz-security thread for evidence. `fe65c07c3` merges current `origin/main` on top (new commit, no rebase), inheriting the nostr 0.44.6 bump (#3135) and relay-admin ban gate (#3128). ## Findings and fixes | Finding | Commit | Fix | |---|---|---| | 003 — quinn-proto RUSTSEC-2026-0185 | `e5dcdec72` | Bump quinn-proto 0.11.14 → 0.11.16 (lockfile-only) | | 002/004 — linkify-it quadratic-parse DoS (GHSA-22p9-wv53-3rq4, GHSA-v245-v573-v5vm) | `923b3c20f` | pnpm override `linkify-it: ^5.0.2`; `pnpm why` confirms a single 5.0.2 copy | | 001 — media reads served unauthenticated by default | `0f277e3e2` | Helm `requireMediaGetAuth` defaults to `true` + rendered-chart test pinning the default | | 006 — removed workflow owners retain webhook-exfiltration authority | `4749bd56c` | Fail-closed per-fire authority gate (current owner/admin membership) on **all four** trigger doors (on_event, scheduler pre-claim, manual trigger, webhook — masked as generic 404), save-time gate for `call_webhook` defs, durable disable-on-removal wired to kinds 9001 + 9022 | | 005 — git Smart-HTTP reads ignore channel membership | `e648f2dba` + `0158ae542` | `authorize_git_read`: caller's **current active membership** in the repo's bound channel, checked before any hydration/subprocess on all three read doors (`info_refs` for both services + `upload_pack` POST). Uniform generic 404 denials (no membership probing), no repo-owner bypass, first-`buzz-channel`-tag binding semantics fail closed on ambiguous duplicates (mutation-verified test). Resolution follows the live kind:30617 announcement, so deleted/replaced announcements deny immediately. The committed `e2e-git-perms.sh` guest scenario previously asserted the vulnerability — now asserts denial. | ## Behavior changes to be aware of 1. **Unbound repos fail closed for git reads.** `buzz repos create` emits no `buzz-channel` tag, so CLI-created repos without a binding are unreadable via git HTTP. Correct per finding 005's fail-closed posture; a follow-up could bind CLI-created repos at creation time. 2. **006 is conservative:** a workflow disabled on owner removal does not auto-re-enable if the owner is re-added — explicit re-enable required. 3. Merge conflict resolution in `fe65c07c3`: kept main's `@radix-ui/react-dismissable-layer` 1.1.19 bump alongside the linkify-it security override (`pnpm-workspace.yaml` + lockfile). ## Verification at the merge head `fe65c07c3` (same shell) - buzz-relay `--lib`: 761 passed / 1 failed — the lone red is the known pre-existing `mesh_demo::demo_join_forwarded_arm_round_trips_echo` 504 flake, present on main - SEC-005 module incl. PG behavioral matrix: 8/8 (removed-member, never-member, owner-no-bypass, deleted-30617, malformed/ambiguous binding, owner-mismatch all denied) - buzz-workflow 153/0, buzz-db 84/0; `clippy --all-targets -D warnings` + `fmt --check` clean - Desktop JS 3637/3637, tsc clean, biome clean, file-size/px-text/pubkey-truncation gates clean - `helm lint` + `helm unittest` (40/40) on `deploy/charts/buzz` - All five pre-push hooks green (desktop-check, desktop-test, rust-tests, desktop-tauri-test, branch-skew) Prior review evidence at `0158ae542` (pre-merge): Max's independent exact-head approval + clean-room live regression pass (`WORK_LOGS/2026-07-27_SECURITY_HIGH_LIVE_TEST.md` in his workspace). Max will re-run the deep local pass at this post-merge head before merge. --------- Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
369 lines
16 KiB
YAML
369 lines
16 KiB
YAML
# Default values for buzz.
|
|
#
|
|
# Two supported tiers:
|
|
#
|
|
# PRODUCTION (default) — external Postgres/Redis/S3, existingSecret
|
|
# refs everywhere, no chart-side autogeneration, GitOps-safe (ArgoCD/Flux).
|
|
# HA-ready: replicaCount >= 2 (requires Redis; git state is object-store-
|
|
# backed, so no ReadWriteMany volume is needed — RWO per replica is fine).
|
|
#
|
|
# QUICKSTART — bundles in-cluster Postgres + Redis + MinIO and
|
|
# auto-generates relay secrets via the `lookup` pattern (NOT GitOps-safe —
|
|
# see README), single replica, evaluation only. Opt in by enabling each
|
|
# bundled service: postgresql.enabled, redis.enabled, minio.enabled.
|
|
# See ci/quickstart-values.yaml and the README.
|
|
#
|
|
# See examples/argocd-app.yaml and examples/flux-helmrelease.yaml for the
|
|
# canonical GitOps configurations.
|
|
|
|
# Intent marker for the evaluation profile, surfaced in NOTES.txt. It does NOT
|
|
# by itself enable any bundled service — set the per-service .enabled flags
|
|
# (postgresql / redis / minio) to bring them up in-cluster.
|
|
quickstart: false
|
|
|
|
# ── Image ────────────────────────────────────────────────────────────────────
|
|
image:
|
|
repository: ghcr.io/block/buzz
|
|
tag: "" # empty → .Chart.AppVersion
|
|
pullPolicy: IfNotPresent
|
|
pullSecrets: []
|
|
|
|
# ── Topology ────────────────────────────────────────────────────────────────
|
|
# replicaCount > 1 hard-requires Redis for buzz-pubsub (in-cluster or external).
|
|
# It does NOT require ReadWriteMany git storage: git ref/object state is
|
|
# object-store-backed (each request hydrates an ephemeral repo from S3; writer
|
|
# serialization is the object-store pointer CAS), and repo-name uniqueness lives
|
|
# in Postgres. Each replica can use its own ReadWriteOnce volume (or none).
|
|
replicaCount: 1
|
|
|
|
# ── Autoscaling ──────────────────────────────────────────────────────────────
|
|
# Requires Metrics Server for CPU. Optional WebSocket scaling additionally
|
|
# requires a custom-metrics adapter exposing its pod-level Prometheus gauge.
|
|
# Kubernetes HPA uses the larger replica recommendation from enabled metrics.
|
|
autoscaling:
|
|
enabled: false
|
|
minReplicas: 5
|
|
maxReplicas: 15
|
|
targetCPUUtilizationPercentage: 65
|
|
websocketMetricEnabled: true
|
|
websocketMetricName: buzz_ws_connections_active
|
|
targetWebsocketConnections: 5000
|
|
behavior:
|
|
scaleUp:
|
|
stabilizationWindowSeconds: 0
|
|
policies:
|
|
- type: Percent
|
|
value: 100
|
|
periodSeconds: 60
|
|
- type: Pods
|
|
value: 4
|
|
periodSeconds: 60
|
|
selectPolicy: Max
|
|
scaleDown:
|
|
stabilizationWindowSeconds: 600
|
|
policies:
|
|
- type: Pods
|
|
value: 1
|
|
periodSeconds: 120
|
|
selectPolicy: Min
|
|
|
|
# ── Public URL ───────────────────────────────────────────────────────────────
|
|
# Required. The wss:// URL clients use to connect. Drives:
|
|
# - RELAY_URL env (relay-side)
|
|
# - Default mediaBaseUrl (https://<host>/media)
|
|
# - Default ingress host
|
|
relayUrl: ""
|
|
mediaBaseUrl: ""
|
|
|
|
# ── Owner ────────────────────────────────────────────────────────────────────
|
|
# 64-char lowercase hex Nostr pubkey of the relay operator. Required when
|
|
# relay.requireRelayMembership=true (the production default).
|
|
ownerPubkey: ""
|
|
|
|
# ── Chart-managed secrets ────────────────────────────────────────────────────
|
|
# Production / GitOps path: create a Secret out-of-band with these keys and
|
|
# point `secrets.existingSecret` at it. Any key omitted from the existing
|
|
# Secret falls back to chart-side autogen (only effective at first install).
|
|
#
|
|
# Expected keys (all optional unless required by relay config):
|
|
# BUZZ_RELAY_PRIVATE_KEY — 64-char hex; relay identity (rotation = identity change)
|
|
# BUZZ_GIT_HOOK_HMAC_SECRET — 32+ chars; required when replicaCount > 1
|
|
# DATABASE_URL — full Postgres URL (preferred over externalPostgresql.url)
|
|
# READ_DATABASE_URL — optional Postgres read-replica URL; omit to keep all reads on the writer
|
|
# REDIS_URL — full Redis URL with auth
|
|
# BUZZ_S3_ACCESS_KEY — S3 access key
|
|
# BUZZ_S3_SECRET_KEY — S3 secret key
|
|
secrets:
|
|
existingSecret: ""
|
|
# Inline overrides (NOT recommended for production; they land in values).
|
|
relayPrivateKey: ""
|
|
gitHookHmacSecret: ""
|
|
|
|
# ── Relay behavior ───────────────────────────────────────────────────────────
|
|
relay:
|
|
bindAddr: "0.0.0.0:3000"
|
|
maxConnections: 10000
|
|
maxConcurrentHandlers: 1024
|
|
sendBuffer: 1000
|
|
requireAuthToken: true
|
|
requireRelayMembership: true
|
|
# Authenticated media reads: relay GET/HEAD /media/* requires Blossom
|
|
# kind 24242 t=get plus relay membership. Enabled by default so private
|
|
# attachments are never publicly readable by URL/hash. Only set false for
|
|
# local development or fully public communities — desktop, mobile, and CLI
|
|
# clients all attach read auth.
|
|
requireMediaGetAuth: true
|
|
allowNipOaAuth: true
|
|
pubkeyAllowlist: false
|
|
corsOrigins: []
|
|
# Huddle audio is safe only for single-pod relay deployments until an SFU
|
|
# exists. null lets the chart render false automatically when
|
|
# replicaCount > 1. Explicit true with replicaCount > 1 means the operator
|
|
# accepts/owns the external multi-pod audio/SFU behavior.
|
|
huddleAudioAvailable: null
|
|
ephemeralTtlOverride: 0
|
|
# Per-upload-event records (`_uploads/` moderation side channel). Off by
|
|
# default. Operators hosting communities for other people may have legal
|
|
# obligations (e.g. NCMEC reporting for US-serving providers) that require
|
|
# recording the network address of an upload; Buzz never collects IPs unless
|
|
# uploadIpHeader is set. When set (e.g. "cf-connecting-ip"), the connecting
|
|
# address reported by YOUR trusted edge is stored in the per-event record
|
|
# only — never served to clients, never in event data. uploadRecords must be
|
|
# true for uploadIpHeader to be valid (startup-checked).
|
|
uploadRecords: false
|
|
uploadIpHeader: ""
|
|
uploadPortHeader: ""
|
|
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /_liveness
|
|
port: health
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 10
|
|
timeoutSeconds: 3
|
|
failureThreshold: 3
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /_readiness
|
|
port: health
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
timeoutSeconds: 3
|
|
failureThreshold: 3
|
|
startupProbe:
|
|
httpGet:
|
|
path: /_liveness
|
|
port: health
|
|
failureThreshold: 60
|
|
periodSeconds: 2
|
|
|
|
resources:
|
|
requests:
|
|
cpu: "500m"
|
|
memory: "512Mi"
|
|
limits:
|
|
cpu: "2"
|
|
memory: "2Gi"
|
|
|
|
podAnnotations: {}
|
|
podLabels: {}
|
|
nodeSelector: {}
|
|
tolerations: []
|
|
affinity: {}
|
|
topologySpreadConstraints: []
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 65532
|
|
runAsGroup: 65532
|
|
fsGroup: 65532
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containerSecurityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: [ALL]
|
|
readOnlyRootFilesystem: false # git writes need a writable repo path
|
|
terminationGracePeriodSeconds: 60
|
|
|
|
extraEnv: []
|
|
extraEnvFrom: []
|
|
|
|
# ── Device pairing relay ─────────────────────────────────────────────────────
|
|
# Optional, stateless NIP-AB relay. When enabled, the main relay advertises
|
|
# pairingRelay.url in NIP-11 and Buzz clients use it instead of the legacy
|
|
# same-host /pair convention.
|
|
pairingRelay:
|
|
enabled: false
|
|
url: ""
|
|
replicaCount: 1
|
|
service:
|
|
type: ClusterIP
|
|
port: 5000
|
|
annotations: {}
|
|
podAnnotations: {}
|
|
podLabels: {}
|
|
resources:
|
|
requests:
|
|
cpu: "50m"
|
|
memory: "32Mi"
|
|
limits:
|
|
cpu: "250m"
|
|
memory: "128Mi"
|
|
|
|
# ── Service ──────────────────────────────────────────────────────────────────
|
|
service:
|
|
type: ClusterIP
|
|
port: 3000
|
|
healthPort: 8080
|
|
metricsPort: 9102
|
|
annotations: {}
|
|
|
|
serviceAccount:
|
|
create: true
|
|
name: ""
|
|
annotations: {}
|
|
|
|
podDisruptionBudget:
|
|
enabled: true
|
|
minAvailable: 1
|
|
maxUnavailable: ""
|
|
|
|
# ── Ingress (classic) ────────────────────────────────────────────────────────
|
|
# Mutually exclusive with httproute.enabled.
|
|
ingress:
|
|
enabled: false
|
|
className: ""
|
|
annotations: {}
|
|
hosts: [] # empty → derived from relayUrl
|
|
tls: [] # [{hosts: [...], secretName: "..."}]
|
|
|
|
# ── Gateway API (HTTPRoute) ──────────────────────────────────────────────────
|
|
httproute:
|
|
enabled: false
|
|
parentRefs: []
|
|
hostnames: []
|
|
rules: [] # empty → default match-all → service
|
|
|
|
# ── Git scratch volume ───────────────────────────────────────────────────────
|
|
# Ephemeral working space only. No persistent git state lives here — reads/writes
|
|
# hydrate ephemeral repos from object storage per request, and repo-name
|
|
# uniqueness lives in Postgres.
|
|
#
|
|
# enabled: true → mount a PVC at mountPath (durable across pod restarts, but a
|
|
# single ReadWriteOnce PVC binds to one node, so it does NOT support multi-pod
|
|
# scheduling across nodes on a Deployment).
|
|
# enabled: false → mount a per-pod emptyDir at mountPath (pure scratch), bounded
|
|
# by size. This is the correct choice for multi-replica HA: each pod gets its
|
|
# own local working space, nothing is shared, and there is no volume to
|
|
# multi-attach. Safe because the object store + Postgres are the sources of
|
|
# truth, not this disk.
|
|
persistence:
|
|
git:
|
|
enabled: true
|
|
mountPath: /var/lib/buzz/git
|
|
storageClass: ""
|
|
accessMode: ReadWriteOnce
|
|
size: 10Gi # PVC capacity or emptyDir sizeLimit
|
|
annotations: {}
|
|
existingClaim: ""
|
|
|
|
# ── Postgres ─────────────────────────────────────────────────────────────────
|
|
# Eval-only CloudPirates subchart. The relay's DATABASE_URL is composed in the
|
|
# chart-managed Secret with a chart-generated password; auth.existingSecret
|
|
# points this subchart at that same Secret/key so server and client agree.
|
|
postgresql:
|
|
enabled: false
|
|
auth:
|
|
database: buzz
|
|
username: buzz
|
|
existingSecret: '{{ if contains "buzz" .Release.Name }}{{ .Release.Name }}-relay{{ else }}{{ .Release.Name }}-buzz-relay{{ end }}'
|
|
secretKeys:
|
|
adminPasswordKey: postgres-password
|
|
persistence:
|
|
enabled: true
|
|
size: 10Gi
|
|
externalPostgresql:
|
|
url: "" # postgres://user:pass@host:5432/db — placeholder example, sadscan:disable np.postgres.1
|
|
|
|
# ── Redis ────────────────────────────────────────────────────────────────────
|
|
# Eval-only CloudPirates subchart (standalone). REDIS_URL is composed in the
|
|
# chart-managed Secret; auth.existingSecret points the subchart at that Secret
|
|
# so the server password matches the URL the relay dials.
|
|
redis:
|
|
enabled: false
|
|
auth:
|
|
existingSecret: '{{ if contains "buzz" .Release.Name }}{{ .Release.Name }}-relay{{ else }}{{ .Release.Name }}-buzz-relay{{ end }}'
|
|
existingSecretPasswordKey: redis-password
|
|
persistence:
|
|
enabled: true
|
|
size: 4Gi
|
|
externalRedis:
|
|
url: "" # redis://:pass@host:6379
|
|
|
|
# ── S3 / object storage (media) ──────────────────────────────────────────────
|
|
# Production: point endpoint/bucket at an external S3-compatible service and
|
|
# supply credentials (inline below or via secrets.existingSecret).
|
|
# Quickstart (`minio.enabled: true`): the chart runs an in-cluster, eval-only
|
|
# MinIO Deployment, creates the bucket via a post-install Job, and composes
|
|
# the endpoint + autogenerated credentials automatically.
|
|
#
|
|
# Storage metrics (hourly bucket sweep, BUZZ_STORAGE_METRICS — see env docs):
|
|
# the credentials above must additionally grant `s3:ListBucket` on the bucket
|
|
# ARN itself (bucket-level; distinct from the object-level GetObject/
|
|
# PutObject/DeleteObject perms already required for media). Without it the
|
|
# first sweep fails AccessDenied and buzz_storage_sweep_ok stays 0 — no other
|
|
# media functionality is affected. Set BUZZ_STORAGE_METRICS=off to disable
|
|
# the sweep entirely on a deployment that can't grant it.
|
|
# Note: buzz_storage_sweep_failures is a process-local gauge — on leader
|
|
# failover it resets to the new leader's local count, not a global total.
|
|
# Note: on a failed sweep attempt, the next retry fires on the next usage tick
|
|
# (default 300 s BUZZ_USAGE_METRICS_INTERVAL_SECS), not at sweep-interval
|
|
# cadence — so a permanently missing s3:ListBucket yields one cheap LIST call
|
|
# per tick until the permission is added.
|
|
s3:
|
|
endpoint: ""
|
|
bucket: "buzz-media"
|
|
accessKey: ""
|
|
secretKey: ""
|
|
|
|
# In-cluster MinIO for the quickstart profile only. Production deploys leave
|
|
# this disabled and use s3.* (or secrets.existingSecret) against managed S3.
|
|
minio:
|
|
enabled: false # quickstart: set true for bundled in-cluster MinIO
|
|
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
|
|
mcImage: minio/mc:RELEASE.2025-08-13T08-35-41Z
|
|
persistence:
|
|
enabled: true
|
|
size: 10Gi
|
|
|
|
# ── Git server config ────────────────────────────────────────────────────────
|
|
git:
|
|
maxPackBytes: 524288000 # 500 MiB
|
|
packCachePath: /var/cache/buzz/git-packs
|
|
packCacheMaxBytes: 5368709120 # 5 GiB
|
|
packCacheMaxConcurrentPopulations: 2
|
|
packCacheVolumeSize: 7Gi # per-pod emptyDir; includes cold-population staging
|
|
maxReposPerPubkey: 100
|
|
maxConcurrentOps: 20
|
|
|
|
# ── Migrations ───────────────────────────────────────────────────────────────
|
|
# Relay runs sqlx migrations at startup via BUZZ_AUTO_MIGRATE=true.
|
|
migrate:
|
|
autoMigrate: true
|
|
preUpgradeJob:
|
|
enabled: false
|
|
resources: {}
|
|
backoffLimit: 3
|
|
activeDeadlineSeconds: 600
|
|
|
|
# ── Monitoring ───────────────────────────────────────────────────────────────
|
|
serviceMonitor:
|
|
enabled: false
|
|
namespace: ""
|
|
interval: 30s
|
|
scrapeTimeout: 10s
|
|
labels: {}
|
|
|
|
# ── Free-form extra manifests ────────────────────────────────────────────────
|
|
extraManifests: []
|