gofundnode security

GoFundNode — Security Overview (GA readiness)

Last updated 2026-06-08

Purpose. The practical security/readiness reference a real customer (and our own on-call) needs before GA: which controls we actually have, where they live in code, what they guarantee, and — explicitly — which are gated / not yet implemented (so nobody assumes a control that isn't there). This is a documentation lane (L-SEC): it describes and, where cheap, points at the tests that pin the controls. It changes no server/** behavior.

Status: GA-readiness security baseline. Last updated: 2026-06-08. Controlling sources (read these, don't infer): CLAUDE.md (§2 laws, §8 settlement, §9 gates), docs/V1-API.md (§1 auth, §3 webhooks, §7 idempotency), docs/ADR/0046-fiat-resource-contribution-model.md (operator-payment sovereignty), docs/architecture/DECISIONS_LOCKED.md (D-LOCK-012/013), docs/BLOCKED_WORK_REGISTER.md (BW-23 / BW-24 and all gated items).

How to read the status tags: ✅ implemented + (where noted) test-pinned · ⚠️ implemented with a documented limitation · ⛔ NOT implemented / gated (cited to its blocking item). Do not treat a ⛔ as present.


0. TL;DR — what is and isn't protected at GA-candidate

AreaStatusOne-line
Inbound tenant request authn (HMAC)HMAC-SHA256 over ${ts}.${rawBody}, ≤300s replay window, constant-time compare, fail-closed 401.
Inbound replay / idempotencyIdempotency key mandatory + tenant-unique; replay returns 200, conflict 409; race-safe via unique constraint.
Outbound webhook signingSame HMAC scheme, separate per-tenant webhook_secret; raw-body signed; X-GFN-Delivery stable per-row dedupe key.
Inbound vs webhook secret separationTwo distinct DB columns (hmac_secretwebhook_secret); compromise of one does not forge the other.
Tenant isolation / opacity✅ (gates)Two AST arch-gates forbid any tenant route/webhook from reading operator-network/comp state.
Operator-leakage prevention✅ (gates)Operator identity, payout, binding, lease, node-topology never reach a tenant; only op-anon-* is permitted.
Secrets rotation📄 runbookSee SECRETS_ROTATION.md. 3 transcript-exposed creds must be rotated pre-GA (founder/ops action).
Applicant-PII envelope⛔ BW-24applicantProfile flows plaintext submission→operator. Three-layer envelope NOT implemented. Gates real PII.
Positive-evidence settlement⛔ BW-23/result trusts the operator-reported outcome; no independent adjudication. Gates production-grade success settlement.
Dependency hygieneNo exploitable request-path vuln in the GA-candidate config; safe in-major updates already absorbed; see §9 + the findings note.
Incident response📄 checklistSee INCIDENT_RESPONSE_CHECKLIST.md.

The two hard pre-GA gaps are BW-23 (positive-evidence) and BW-24 (PII envelope). Both are tracked, both are out of this lane's authority to build (verifier/scheduler substrate + KMS/schema), and both gate real external applicant exposure — not the private-beta canary, which uses only synthetic data and SMOKE_SKIP_SETTLEMENT.


1. HMAC signing + replay/idempotency stance (inbound)

Where: server/_core/hmac.ts (primitive), server/api/v1/middleware/hmac-verify.ts (inbound gate), docs/V1-API.md §1.

1.1 Signing scheme (✅)

1.2 Replay window (✅)

1.3 Idempotency (✅)

Replay vs idempotency — the stance. The HMAC ±300s window bounds signature replay (a captured signed request cannot be re-fired indefinitely). The idempotency key bounds application replay (even a perfectly-signed retry within the window does not double-submit). The two layers are independent and both required.

Tests that pin this: server/api/v1/__tests__/submissions-idempotency-race.test.ts (9), …/submissions-error-taxonomy.test.ts (6). Both run in pnpm test:beta.


2. Webhook verification + replay protection (outbound)

Where: server/api/v1/webhooks/delivery.ts (worker), …/webhooks/signer.ts (signer), docs/V1-API.md §3.

Test that pins this: server/api/v1/__tests__/webhook-delivery.test.ts (13; covers envelope shape, X-GFN-Delivery stability, retry classification, backoff). Runs in pnpm test:beta.


3. Inbound vs webhook secret separation (✅)

Where: server/billing/schema.ts (tenants table).


4. Tenant isolation + opacity guarantees (✅, gate-enforced)

Architecture law: CLAUDE.md §2 (Tenant-architectural baseline, D-LOCK-013) — a tenant is a customer, never part of the architecture; no shared mutable operator-state is tenant-readable. ADR-046 operator-payment sovereignty: "customers have no access to operator payout state."

Two complementary AST-level arch-gates (ts-morph, not text-grep, so prose/comments cannot trip or disable them) enforce the boundary in pnpm arch:gates:

4.1 no-operator-comp-tenant-read (read boundary)

scripts/architecture-gates/no-operator-comp-tenant-read.ts. Scans the v1 route layer. Fails if any route not on the explicit operator-authed allowlist either (a) imports an operator-network drizzle symbol (operatorRewards, gfnOperatorPayouts, operators, taskBundles, userNodeAffinity, …) or (b) names an operator-network table in a SQL literal. The operator-authed allowlist is explicit and reviewable — a new operator route is a deliberate edit there.

4.2 no-operator-comp-webhook-egress (egress boundary)

scripts/architecture-gates/no-operator-comp-webhook-egress.ts. Broader scope (server/**) — reaches every enqueueWebhook(...) call site, including the closer / billing workers the read-gate never sees. For each tenant-facing enqueueWebhook payload object-literal it fails if any forbidden field name appears at any nesting depth, as a property key or as a <obj>.<field> value read (catches the key-rename smuggle { x: settlement.operatorPayoutCredits } and the raw-value smuggle { ref: submission.assignedOperatorId }). Forbidden = operator-compensation (operatorPayoutCredits, platformShareCredits, rebateAccruedCredits, operatorRewardId) and operator-binding/routing/settlement identifiers (operatorId/assignedOperatorId/nodeId/leaseId/bindingEpoch/operatorIpHash/ payoutLegId/… in both camel and snake case).

Net opacity guarantee. A tenant sees only its own submissions, credits, and outcome. It never sees operator identity, operator payout, GoFundNode's platform margin, node→operator topology, lease/binding internals, or which operator was tried. The only permitted operator handle in a tenant surface is the anonymized op-anon-* token on GET /:id (never via a webhook, never the raw id).

Runtime regression also pins it: server/api/v1/__tests__/tenant-webhook-no-operator-comp.test.ts (the runtime contract mirroring the egress gate's forbidden-field set).


5. Operator-leakage prevention (✅)

Covered structurally by §4.2 (the egress gate) and operator-route ownership (§6 below). The complete documented tenant-facing submission.completed set is {submissionId, status, outcome, failureReason, ledger, screenshotUrl, actualCredits, refundedCredits} (docs/V1-API.md §3.2) — none of the operator/binding/payout identifiers legitimately appear in it, which is what makes forbidding their raw keys/values unambiguous. The presenter invariant (O5 §1.2): the only operator projection a tenant may receive is anonOperatorRef = op-anon-*, under a distinctly-named key, on GET /:id.


6. Operator-route auth + ownership (✅ auth; ⚠️ one ownership gap, gated)

Where: docs/security/W3-4-operator-route-auth-findings.md (full route sweep); server/api/v1/routes/operator-result.ts.


7. Applicant-PII envelope — ⛔ CURRENT LIMITATION (BW-24)

Status: NOT implemented. This is a hard pre-GA gap for real applicant PII.

Operational rule until BW-24 lands: do not route real applicant PII through POST /v1/submissions. The plaintext jsonb path is acceptable only for synthetic beta data.


8. Positive-evidence settlement adjudication — ⛔ CURRENT LIMITATION (BW-23)

Status: NOT implemented. The /result boundary trusts the operator.

Trust note for customers: until BW-23, a submission.completed{outcome:"success"} reflects the operator's report that the application was submitted, not an independently verified confirmation. Do not represent it as adjudicated success.


9. Dependency-hygiene posture (✅ — detail in the findings note)

Full audit: docs/findings/dependency-security-hygiene.md (W4-8) and the L-SEC update appended there. Summary for GA:


10. Residual GA security risks (the honest list)

#RiskStatus / blockerOwner
R1Applicant PII is plaintext end-to-end (no three-layer envelope).⛔ BW-24 — gates real PII.vault/crypto + schema
R2Success settlement trusts operator self-report (no positive-evidence adjudication).⛔ BW-23 — gates production-grade settlement.settlement/verifier
R3Unbound-submission ownership self-assert on the legacy /result path.⚠️ Closed by binding-at-issuance (D-BIND-1); fix is flag-gated + boot-gated (BW-11).scheduler-core
R43 transcript-exposed credentials (Neon pw, manus SSH key, GitHub token) need rotation.📄 SECRETS_ROTATION.md — founder/ops action, pre-GA.founder/ops
R5fastify v4 is EOL (2025-06-30); future advisories unpatchable on the v4 line.RECOMMENDATION — v4→v5 serialized migration, post-beta.deps owner / founder
R6drizzle-orm is 9 minor versions stale (0.36.4) and carries the (unreachable) SQLi advisory.RECOMMENDATION — 0.45.2 bump validated green in-lane but deferred (kit-pairing/migration path).deps owner / founder
R7@solana/* non-GA cluster sits in prod dependencies (drags the unpatchable bigint-buffer).RECOMMENDATION — gate/remove post-beta; ADR-046/047 founder call.founder/architecture
R8Arch-gates do not trace the call graph (route imports/SQL + inline webhook payloads only).⚠️ Documented gate limitation; durable fix = physical carve-out (issue #27 / BW-3).architecture
R9Counsel-gated GA legal substrate (sanctions live, TOS/DPA, 1099, treasury).⛔ BW-13…BW-19.founder/counsel

Bottom line. The request-path security controls (HMAC authn, replay/idempotency, webhook signing + dedupe, secret separation, tenant opacity, operator-route auth) are implemented and test-pinned. The two controls a security-conscious customer would most expect for real applicant traffic — PII-at-rest/in-transit encryption (R1/BW-24) and independently-verified success settlement (R2/BW-23) — are not yet built and are the headline GA gates. Everything else above is a recommendation or a counsel/founder decision, not an open hole in the beta configuration.