Security hardening and release readiness

ober

0234e609681d75d49d27d4032a4033b7cb1a38b2

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ab3cb5b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,45 @@
+name: CI
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+env:
+  JERBOA_VERSION: v0.2.3
+  JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild
+
+jobs:
+  verify:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install system tools
+        run: |
+          set -eu
+          sudo apt-get update
+          sudo apt-get install -y --no-install-recommends build-essential curl ca-certificates
+
+      - name: Install Rust components
+        run: |
+          set -eu
+          rustup target add wasm32-unknown-unknown
+
+      - name: Install cargo-audit
+        run: |
+          set -eu
+          cargo install cargo-audit --locked
+
+      - name: Bootstrap Jerboa
+        run: |
+          set -eu
+          sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin
+          "$JERBUILD" --jerboa-home
+
+      - name: Verify
+        run: make verify
diff --git a/.github/workflows/security-baseline.yml b/.github/workflows/security-baseline.yml
new file mode 100644
index 0000000..28a713e
--- /dev/null
+++ b/.github/workflows/security-baseline.yml
@@ -0,0 +1,35 @@
+name: Security Baseline
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  baseline:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Required release files
+        run: |
+          set -eu
+          test -f LICENSE
+          test -f SECURITY.md
+          test -f .gitignore
+          find . -maxdepth 1 -iname "README*" -type f | grep -q .
+
+      - name: High-confidence secret scan
+        run: |
+          set -eu
+          pattern="(BEGIN (RSA|OPENSSH|EC|DSA|PRIVATE) KEY|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-(ant-api03|proj|svcacct)-[A-Za-z0-9_-]{30,}|AKIA[0-9A-Z]{16})"
+          matches="$(git grep -n -I -E "$pattern" -- . ":!*.png" ":!*.jpg" ":!*.jpeg" ":!*.gif" ":!*.so" ":!*.dylib" ":!*.o" ":!*.a" ":!*.boot" ":!*.tar.gz" || true)"
+          if [ -n "$matches" ]; then
+            echo "$matches"
+            echo "High-confidence secret pattern found."
+            exit 1
+          fi
diff --git a/.gitignore b/.gitignore
index b0865f5..39c6104 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,6 @@ fuzz/artifacts/
 # jerbuild build artifacts
 /jdns
 support/ffi-symbols.gen
+
+# Release evidence generated by make release-evidence
+/dist/
diff --git a/.jerboa/security.json b/.jerboa/security.json
index 45af92b..2957aaa 100644
--- a/.jerboa/security.json
+++ b/.jerboa/security.json
@@ -16,7 +16,17 @@
     "process": { "shellInterpolation": "deny" },
     "network": { "requireTimeouts": true, "requireFailClosedSandbox": true, "maxReadBytes": 512 },
     "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false },
-    "daemon": { "requirePrivilegeDrop": true, "requireSetgroups": true }
+    "daemon": { "requirePrivilegeDrop": true, "requireSetgroups": true },
+    "supplyChain": { "sbom": "required", "reproducibilityReport": "required", "rustsec": "required" },
+    "releaseEvidence": {
+      "productionSoak": "current-soak-recorded-or-target-proof-required",
+      "edgeRateLimit": "target-evidence-recorded-required-before-public-release",
+      "proofFiles": ["JDNS_TARGET_SOAK_PROOF_FILE", "JDNS_TARGET_RATE_LIMIT_PROOF_FILE"],
+      "failClosedProofFlags": ["JDNS_REQUIRE_TARGET_SOAK_PROOF", "JDNS_REQUIRE_TARGET_RATE_LIMIT_PROOF"],
+      "evidenceSanitizer": "scripts/sanitize-evidence.sh",
+      "artifactArchivePolicy": "hashes-only",
+      "forbiddenEvidenceMaterial": ["private paths", "SSH clone URLs", "full uname output", "operational hostnames", "private zone data", "keys", "tokens"]
+    }
   },
   "suppressions": []
 }
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Makefile b/Makefile
index 3a2cb26..3e55143 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 # jerbuild bundles Chez Scheme + the jerboa stdlib + the jerboa-native Rust
 # crate, so building jdns needs only jerbuild + a C compiler + cargo (with the
 # wasm32-unknown-unknown target for the sandbox payloads). No jerboa checkout.
-JERBUILD ?= jerbuild
+JERBUILD ?= $(shell if [ -x "$(CURDIR)/../jerboa/dist/jerbuild" ]; then printf '%s\n' "$(CURDIR)/../jerboa/dist/jerbuild"; elif command -v jerbuild >/dev/null 2>&1; then command -v jerbuild; else printf '%s\n' jerbuild; fi)
 JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
 ifeq ($(JH),)
 $(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild)
@@ -13,11 +13,16 @@ LIBDIRS    := --libdirs lib:$(JH)/lib
 JEXEC      := $(JERBUILD) exec $(LIBDIRS)
 BIN        := jdns
 CARGO_AUDIT ?= $(shell command -v cargo-audit 2>/dev/null || printf '%s/.cargo/bin/cargo-audit' "$$HOME")
+DIST_DIR ?= dist/release-evidence
+SBOM_DIR ?= dist/sbom
+REPRO_DIR ?= dist/reproducibility
+SOAK_DIR ?= dist/soak
+FUZZ_EVIDENCE_DIR ?= dist/fuzz-evidence
 
 WASM_DIR     = wasm
 WASM_OUTPUTS = lib/jerboa-dns/sandbox/dns_parser.wasm lib/jerboa-dns/sandbox/cdb_parser.wasm
 
-.PHONY: all build binary wasm wasm-clean test security audit fuzz-check verify clean
+.PHONY: all build binary wasm wasm-clean test security audit fuzz-check fuzz-evidence sbom reproducibility-report soak-evidence verify release-evidence clean
 
 all: binary
 
@@ -63,6 +68,7 @@ audit:
 	(cd $(WASM_DIR) && "$(CARGO_AUDIT)" audit)
 
 fuzz-check:
+	cargo run --manifest-path fuzz/Cargo.toml --bin corpus_smoke
 	@if rustup toolchain list 2>/dev/null | grep -q '^nightly' && \
 	    { command -v cargo-fuzz >/dev/null 2>&1 || [ -x "$$HOME/.cargo/bin/cargo-fuzz" ]; }; then \
 		NIGHTLY_BIN=$$(dirname "$$(rustup which cargo --toolchain nightly)"); \
@@ -72,8 +78,83 @@ fuzz-check:
 		cargo check --manifest-path fuzz/Cargo.toml --bins; \
 	fi
 
-verify: security test fuzz-check audit
+fuzz-evidence:
+	FUZZ_EVIDENCE_DIR="$(FUZZ_EVIDENCE_DIR)" bash scripts/fuzz-evidence.sh
+
+sbom: binary
+	JERBUILD="$(JERBUILD)" SBOM_DIR="$(SBOM_DIR)" bash scripts/sbom.sh
+
+reproducibility-report:
+	JERBUILD="$(JERBUILD)" REPRO_DIR="$(REPRO_DIR)" bash scripts/reproducibility-report.sh
+
+soak-evidence:
+	SOAK_EVIDENCE_DIR="$(SOAK_DIR)" bash scripts/soak-evidence.sh
+
+verify: security test fuzz-check audit sbom reproducibility-report
+
+release-evidence: verify
+	rm -rf "$(DIST_DIR)"
+	mkdir -p "$(DIST_DIR)"
+	git rev-parse HEAD > "$(DIST_DIR)/git-commit.txt"
+	git status --short > "$(DIST_DIR)/git-status.txt"
+	{ \
+		echo "os=$$(uname -s 2>/dev/null || printf unknown)"; \
+		echo "kernel_release=$$(uname -r 2>/dev/null || printf unknown)"; \
+		echo "machine=$$(uname -m 2>/dev/null || printf unknown)"; \
+		$(JERBUILD) --version 2>/dev/null | sed 's/^/jerbuild_version=/'; \
+		if $(JERBUILD) --jerboa-home >/dev/null 2>&1; then echo "jerboa_home_status=present"; else echo "jerboa_home_status=missing"; fi; \
+	} > "$(DIST_DIR)/build-environment.txt"
+	cargo metadata --manifest-path wasm/Cargo.toml --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-wasm.json"
+	cargo metadata --manifest-path fuzz/Cargo.toml --locked --format-version 1 > "$(DIST_DIR)/cargo-metadata-fuzz.json"
+	(cd wasm && "$(CARGO_AUDIT)" audit > "../$(DIST_DIR)/rustsec-wasm.txt")
+	find lib/jerboa-dns/sandbox -type f -name '*.wasm' -exec shasum -a 256 {} \; > "$(DIST_DIR)/wasm-sha256.txt"
+	rm -rf "$(DIST_DIR)/sbom" "$(DIST_DIR)/reproducibility"
+	cp -R "$(SBOM_DIR)" "$(DIST_DIR)/sbom"
+	cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility"
+	cat "$(SBOM_DIR)/status.txt" > "$(DIST_DIR)/sbom.log"
+	cat "$(REPRO_DIR)/result.txt" > "$(DIST_DIR)/reproducibility.log"
+	grep -q '^artifact_archive_status=hashes-only$$' "$(REPRO_DIR)/result.txt"
+	grep -q '^scratch_build_logs_status=not-archived$$' "$(REPRO_DIR)/result.txt"
+	$(MAKE) --no-print-directory soak-evidence
+	rm -rf "$(DIST_DIR)/soak"
+	cp -R "$(SOAK_DIR)" "$(DIST_DIR)/soak"
+	cat "$(SOAK_DIR)/status.txt" > "$(DIST_DIR)/soak.log"
+	@if [ "$${JDNS_RUN_RELEASE_SOAK:-0}" = "1" ]; then \
+	grep -q '^tcp_smoke_status=pass$$' "$(SOAK_DIR)/status.txt"; \
+	fi
+	$(MAKE) --no-print-directory fuzz-evidence
+	rm -rf "$(DIST_DIR)/fuzz-evidence"
+	cp -R "$(FUZZ_EVIDENCE_DIR)" "$(DIST_DIR)/fuzz-evidence"
+	cat "$(FUZZ_EVIDENCE_DIR)/status.txt" > "$(DIST_DIR)/fuzz-evidence.log"
+	@if [ "$${JDNS_RUN_COVERAGE_FUZZ:-0}" = "1" ]; then \
+		grep -q '^coverage_fuzz_status=local-smoke-recorded$$' "$(FUZZ_EVIDENCE_DIR)/status.txt"; \
+	fi
+	{ \
+		printf 'abuse_policy_status=present\n'; \
+		printf 'authoritative_only_status=documented\n'; \
+		printf 'recursion_status=disabled\n'; \
+		printf 'any_query_policy_status=minimized-refused\n'; \
+		printf 'udp_payload_policy_status=capped-512-with-truncation\n'; \
+		printf 'tcp_resource_policy_status=documented\n'; \
+		printf 'edge_rate_limit_status=external-required-until-native-limit\n'; \
+		printf 'source=docs/rate-limit-amplification.md\n'; \
+	} > "$(DIST_DIR)/abuse-policy.txt"
+	grep -q '^abuse_policy_status=present$$' "$(DIST_DIR)/abuse-policy.txt"
+	grep -q '^any_query_policy_status=minimized-refused$$' "$(DIST_DIR)/abuse-policy.txt"
+	grep -q '^edge_rate_limit_status=external-required-until-native-limit$$' "$(DIST_DIR)/abuse-policy.txt"
+	sh scripts/sanitize-evidence.sh "$(DIST_DIR)"
+	sh scripts/sanitize-evidence.sh "$(SBOM_DIR)"
+	sh scripts/sanitize-evidence.sh "$(REPRO_DIR)"
+	sh scripts/sanitize-evidence.sh "$(SOAK_DIR)"
+	sh scripts/sanitize-evidence.sh "$(FUZZ_EVIDENCE_DIR)"
 
 clean:
-	rm -f $(BIN)
+	rm -f $(BIN) \
+		jdns-macos-arm64 jdns-data-macos-arm64 \
+		jdns-linux-amd64 jdns-data-linux-amd64 \
+		jdns-linux-arm64 jdns-data-linux-arm64 \
+		jdns-freebsd-amd64 jdns-data-freebsd-amd64 \
+		jdns*-main.c jdns*-libs.boot \
+		petite_boot.h scheme_boot.h app_boot.h program_boot.h \
+		support/ffi-symbols.gen
 	find lib \( -name '*.so' -o -name '*.wpo' \) -delete 2>/dev/null || true
diff --git a/README.md b/README.md
index 2635a99..1e68974 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,30 @@ make build
 make test
 ```
 
+Release evidence:
+
+```sh
+make release-evidence
+```
+
+The release bundle records DNS soak/load status under
+`dist/release-evidence/soak/`. By default this is a blocked, record-only status;
+set `JDNS_RUN_RELEASE_SOAK=1` with production-duration `JDNS_SOAK_SECONDS` to
+capture current UDP `dnsperf` evidence plus bounded DNS-over-TCP smoke.
+Bounded local coverage-guided fuzz evidence is recorded with:
+
+```sh
+JDNS_RUN_COVERAGE_FUZZ=1 JDNS_FUZZ_RUNS=2048 make fuzz-evidence
+```
+
+When `JDNS_RUN_COVERAGE_FUZZ=1` is set for `make release-evidence`, the release
+bundle also records `coverage_fuzz_status=local-smoke-recorded` under
+`dist/release-evidence/fuzz-evidence/` after `dns_query`, `cdb_record`, and
+`cdb_file` complete.
+Authoritative-only, amplification, and edge rate-limit policy is documented in
+`docs/rate-limit-amplification.md` and copied into
+`dist/release-evidence/abuse-policy.txt`.
+
 Static release targets live behind the `static-*` Make targets, for example:
 
 ```sh
@@ -27,6 +51,11 @@ is available before chroot. Embedded static builds can fall back to bounded
 nonblocking OS-thread tasks instead of failing at startup. TCP clients are capped
 at 128 concurrent sessions, and idle TCP reads/writes time out after 15 seconds.
 
+The daemon is authoritative-only and does not recurse or forward queries.
+Amplification-prone `ANY` queries are minimized with a small REFUSED response.
+Production deployments must still provide edge rate limits until native
+response-rate limiting exists.
+
 Filesystem sandbox setup fails closed by default. For local development only,
 you can allow a weaker fallback with either:
 
diff --git a/SECURITY.md b/SECURITY.md
index 9991390..5fd22c8 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -14,15 +14,40 @@ must be cut from a clean checkout after:
 - `make fuzz-check`
 - `make audit`
 - `make verify`
+- `make release-evidence`
+
+Release evidence must include current SBOM/toolchain provenance, RustSec/Cargo
+metadata, and a repeated clean daemon build report covering the `jdns` binary,
+generated libraries, FFI symbols, and WASM payloads. A reproducibility
+`status=mismatch` report is a production blocker unless a release owner records
+an explicit exception. Reproducibility evidence must archive hashes/status only,
+not raw binaries, generated libraries, or scratch build logs.
 
 ## Hardening Expectations
 
-- DNS packet parsing and CDB record walking must stay covered by fuzz harnesses.
+- The daemon threat model is tracked in `docs/threat-model.md`.
+- Rate-limit and amplification policy is tracked in
+  `docs/rate-limit-amplification.md`.
+- DNS packet parsing and CDB record walking must stay covered by the fuzz
+  harnesses and corpus described in `docs/fuzzing.md`.
+- The daemon must remain authoritative-only; recursion and forwarding are out
+  of scope.
+- `ANY` queries must remain minimized instead of enumerating full zone records.
 - Filesystem sandbox fallback must fail closed unless explicitly enabled for
   local development.
 - Privilege drop, supplementary group clearing, timeouts, and packet-size caps
   are release gates.
 - Do not publish operational zone data, deployment paths, hostnames, or keys.
+- Public production deployment still requires UDP/TCP soak/load evidence,
+  coverage-guided fuzzing evidence, SBOM/reproducibility evidence, target edge
+  rate-limit proof, and external review.
+- Required target proof files must fail closed: `JDNS_TARGET_SOAK_PROOF_FILE`
+  must record `production_load_status=current-soak-recorded` and
+  `tcp_smoke_status=pass`, while `JDNS_TARGET_RATE_LIMIT_PROOF_FILE` must
+  record `edge_rate_limit_status=target-evidence-recorded`.
+- Target proof and release evidence must not include private paths, SSH clone
+  references, full host fingerprints, operational hostnames, private zone data,
+  keys, or tokens.
 
 ## Reporting
 
diff --git a/bench/run-server.sh b/bench/run-server.sh
index ea09236..fa76cda 100755
--- a/bench/run-server.sh
+++ b/bench/run-server.sh
@@ -56,7 +56,7 @@ echo "$PID" > "$LOG.pid"
 
 # Wait up to 10s for the listener to come up.
 for _ in $(seq 1 50); do
-  if grep -q "listening on" "$LOG" 2>/dev/null; then
+  if grep -Eq "listening on|evt=tcp_listening" "$LOG" 2>/dev/null; then
     echo "jdns up: pid=$PID port=$PORT mode=$MODE"
     exit 0
   fi
diff --git a/bench/run-tcp-smoke.sh b/bench/run-tcp-smoke.sh
new file mode 100755
index 0000000..d3d0f38
--- /dev/null
+++ b/bench/run-tcp-smoke.sh
@@ -0,0 +1,161 @@
+#!/usr/bin/env bash
+# Run bounded DNS-over-TCP query coverage against a local jdns instance.
+#
+# Usage:
+#   bench/run-tcp-smoke.sh <port> <queries-file> <case-count> <summary-out>
+
+set -euo pipefail
+
+PORT="${1:?port required}"
+QUERY_FILE="${2:?queries file required}"
+CASE_COUNT="${3:-128}"
+SUMMARY_OUT="${4:?summary output required}"
+
+mkdir -p "$(dirname "$SUMMARY_OUT")"
+
+if ! command -v python3 >/dev/null 2>&1; then
+  {
+    echo "status=blocked-python3-missing"
+    echo "tcp_smoke_cases=0"
+    echo "tcp_smoke_completed=0"
+    echo "tcp_smoke_failures=0"
+    echo "detail=python3 is required for DNS-over-TCP smoke evidence"
+  } > "$SUMMARY_OUT"
+  exit 0
+fi
+
+python3 - "$PORT" "$QUERY_FILE" "$CASE_COUNT" > "$SUMMARY_OUT" <<'PY'
+import math
+import socket
+import struct
+import sys
+
+port = int(sys.argv[1])
+query_file = sys.argv[2]
+case_count = int(sys.argv[3])
+
+qtypes = {
+    "A": 1,
+    "NS": 2,
+    "CNAME": 5,
+    "SOA": 6,
+    "MX": 15,
+    "TXT": 16,
+    "AAAA": 28,
+    "ANY": 255,
+}
+
+
+def load_queries(path, limit):
+    queries = []
+    with open(path, "r", encoding="utf-8") as handle:
+        for raw in handle:
+            line = raw.strip()
+            if not line or line.startswith("#"):
+                continue
+            parts = line.split()
+            if len(parts) < 2:
+                continue
+            queries.append((parts[0], parts[1].upper()))
+            if len(queries) >= limit:
+                break
+    return queries
+
+
+def encode_name(name):
+    labels = name.rstrip(".").split(".")
+    out = bytearray()
+    total = 1
+    for label in labels:
+        data = label.encode("ascii")
+        if len(data) == 0 or len(data) > 63:
+            raise ValueError("invalid label length for %r" % name)
+        total += 1 + len(data)
+        if total > 255:
+            raise ValueError("encoded name too long for %r" % name)
+        out.append(len(data))
+        out.extend(data)
+    out.append(0)
+    return bytes(out)
+
+
+def build_query(index, qname, qtype_name):
+    if qtype_name not in qtypes:
+        raise ValueError("unsupported qtype %r" % qtype_name)
+    qid = (0x6000 + index) & 0xFFFF
+    header = struct.pack("!HHHHHH", qid, 0x0100, 1, 0, 0, 0)
+    question = encode_name(qname) + struct.pack("!HH", qtypes[qtype_name], 1)
+    return qid, header + question
+
+
+def read_exact(sock, size):
+    data = bytearray()
+    while len(data) < size:
+        chunk = sock.recv(size - len(data))
+        if not chunk:
+            raise EOFError("short TCP DNS response")
+        data.extend(chunk)
+    return bytes(data)
+
+
+queries = load_queries(query_file, case_count)
+completed = 0
+failures = []
+rcode_counts = {}
+connections = 0
+max_per_connection = 0
+
+if not queries:
+    failures.append("no queries loaded from %s" % query_file)
+else:
+    per_connection = max(1, int(math.ceil(len(queries) / 16.0)))
+    max_per_connection = per_connection
+    for offset in range(0, len(queries), per_connection):
+        chunk = queries[offset:offset + per_connection]
+        try:
+            with socket.create_connection(("127.0.0.1", port), timeout=2.0) as sock:
+                sock.settimeout(2.0)
+                connections += 1
+                for local_index, (qname, qtype_name) in enumerate(chunk):
+                    index = offset + local_index
+                    qid, payload = build_query(index, qname, qtype_name)
+                    sock.sendall(struct.pack("!H", len(payload)) + payload)
+                    response_len = struct.unpack("!H", read_exact(sock, 2))[0]
+                    response = read_exact(sock, response_len)
+                    if response_len < 12:
+                        raise ValueError("response shorter than DNS header for %s %s" % (qname, qtype_name))
+                    rid, flags, qdcount, _ancount, _nscount, _arcount = struct.unpack("!HHHHHH", response[:12])
+                    if rid != qid:
+                        raise ValueError("response id mismatch for %s %s" % (qname, qtype_name))
+                    if (flags & 0x8000) == 0:
+                        raise ValueError("response QR bit missing for %s %s" % (qname, qtype_name))
+                    if qdcount != 1:
+                        raise ValueError("response qdcount=%d for %s %s" % (qdcount, qname, qtype_name))
+                    rcode = flags & 0x000F
+                    rcode_counts[rcode] = rcode_counts.get(rcode, 0) + 1
+                    if rcode not in (0, 3):
+                        raise ValueError("unexpected rcode=%d for %s %s" % (rcode, qname, qtype_name))
+                    completed += 1
+        except Exception as exc:
+            failures.append("case_%d=%s" % (offset, str(exc)))
+            if len(failures) >= 10:
+                break
+
+status = "pass" if queries and completed == len(queries) and not failures else "fail"
+
+print("repo=jerboa-dns")
+print("status=%s" % status)
+print("tcp_smoke_cases=%d" % len(queries))
+print("tcp_smoke_completed=%d" % completed)
+print("tcp_smoke_failures=%d" % (len(queries) - completed + len(failures)))
+print("tcp_smoke_connections=%d" % connections)
+print("tcp_max_queries_per_connection=%d" % max_per_connection)
+print("tcp_query_file=%s" % query_file)
+print("tcp_rcode_noerror=%d" % rcode_counts.get(0, 0))
+print("tcp_rcode_nxdomain=%d" % rcode_counts.get(3, 0))
+for rcode in sorted(rcode_counts):
+    if rcode not in (0, 3):
+        print("tcp_rcode_%d=%d" % (rcode, rcode_counts[rcode]))
+for failure in failures[:5]:
+    print("failure=%s" % failure)
+PY
diff --git a/bin/jdns-convert.ss b/bin/jdns-convert.ss
index 7dcebb4..f470308 100644
--- a/bin/jdns-convert.ss
+++ b/bin/jdns-convert.ss
@@ -8,7 +8,15 @@
 ;;;        If output is omitted, writes to stdout.
 ;;;        If input is omitted, reads from stdin.
 
-(import (chezscheme)) ; jerboa-security: suppress direct-chezscheme-import-user-code
+(import (scheme))
+
+(define (call-with-replacing-output-file path proc)
+  (call-with-port
+    (open-file-output-port path
+                           (file-options no-fail)
+                           (buffer-mode block)
+                           (native-transcoder))
+    proc))
 
 ;; ========== String Utilities ==========
 
@@ -466,7 +474,6 @@
   (let* ([entries (parse-tinydns-file input-path)]
          [groups (group-by-domain entries)])
     (if output-path
-      (call-with-output-file output-path ; jerboa-security: suppress call-with-output-file-overwrite-fail
-        (lambda (port) (emit-zone port groups))
-        'replace)
+      (call-with-replacing-output-file output-path
+        (lambda (port) (emit-zone port groups)))
       (emit-zone (current-output-port) groups))))
diff --git a/bin/jdns-data.ss b/bin/jdns-data.ss
index 73cbfea..e5d00dd 100644
--- a/bin/jdns-data.ss
+++ b/bin/jdns-data.ss
@@ -1,3 +1,3 @@
 #!chezscheme
-(import (chezscheme) (jerboa-dns main)) ; jerboa-security: suppress direct-chezscheme-import-user-code
+(import (scheme) (jerboa-dns main))
 (run-jdns-data! (cdr (command-line)))
diff --git a/bin/jdns.ss b/bin/jdns.ss
index b4edc5a..0f7e86f 100644
--- a/bin/jdns.ss
+++ b/bin/jdns.ss
@@ -1,3 +1,3 @@
 #!chezscheme
-(import (chezscheme) (jerboa-dns main)) ; jerboa-security: suppress direct-chezscheme-import-user-code
+(import (scheme) (jerboa-dns main))
 (run-jdns! (cdr (command-line)))
diff --git a/build.ss b/build.ss
index 2cd902d..8dcec47 100644
--- a/build.ss
+++ b/build.ss
@@ -1,4 +1,4 @@
-(import (chezscheme)) ; jerboa-security: suppress direct-chezscheme-import-user-code
+(import (scheme))
 (compile-imported-libraries #t)
 (generate-wpo-files #t)
 (import
diff --git a/docs/fuzzing.md b/docs/fuzzing.md
new file mode 100644
index 0000000..a0ea3cb
--- /dev/null
+++ b/docs/fuzzing.md
@@ -0,0 +1,45 @@
+# jerboa-dns Fuzzing And Corpus
+
+`make fuzz-check` is the required local and CI gate for parser hardening.
+
+It always runs `fuzz/src/bin/corpus_smoke.rs` under stable Rust. That smoke
+runner feeds every checked-in seed in:
+
+- `fuzz/corpus/dns_query`
+- `fuzz/corpus/cdb_record`
+- `fuzz/corpus/cdb_file`
+
+When nightly Rust and `cargo-fuzz` are available, `make fuzz-check` also builds
+the libFuzzer targets in `fuzz/fuzz_targets`.
+
+The checked-in corpus is safe for publication and intentionally contains no
+operational zone data. It covers normal A queries, DNS compression pointer
+loops, truncated headers, reserved labels, forward pointers, maximum labels,
+CDB record value validation, malformed CDB headers, and a minimal valid CDB
+record walk.
+
+Bounded local coverage-guided fuzz evidence is recorded with:
+
+```sh
+JDNS_RUN_COVERAGE_FUZZ=1 JDNS_FUZZ_RUNS=2048 make fuzz-evidence
+```
+
+The evidence runner copies checked-in seeds into an ignored work corpus under
+`dist/fuzz-evidence/work-corpus/` before invoking cargo-fuzz, so generated
+minimization inputs cannot mutate the safe-for-publication corpus under
+`fuzz/corpus/`.
+
+- coverage_fuzz_evidence_status: documented
+- coverage_fuzz_targets: dns_query-cdb_record-cdb_file
+- coverage_fuzz_runs_minimum: 2048
+
+Short local runs record `coverage_fuzz_status=local-smoke-recorded` and are
+useful for release packet completeness. Production claims still need sustained
+coverage-guided fuzzing evidence on a release host, plus UDP/TCP load/soak
+evidence recorded before any production daemon claim.
+`make release-evidence` now copies `make soak-evidence` output into
+`dist/release-evidence/soak/`; unless `JDNS_RUN_RELEASE_SOAK=1` is set for a
+current production-length run, that evidence intentionally records a blocked
+soak status. When the opt-in soak path is run, the evidence also includes a
+bounded DNS-over-TCP query/response smoke summary so TCP coverage is visible in
+the release packet instead of relying only on UDP `dnsperf` output.
diff --git a/docs/rate-limit-amplification.md b/docs/rate-limit-amplification.md
new file mode 100644
index 0000000..0a2ee24
--- /dev/null
+++ b/docs/rate-limit-amplification.md
@@ -0,0 +1,88 @@
+# Rate Limit And Amplification Policy
+
+`jerboa-dns` is authoritative-only. Its production safety depends on avoiding
+open-recursive behavior, bounding response size and TCP state, and deploying an
+edge rate-limit policy appropriate for the hosted zones.
+
+## Release Checklist Markers
+
+- authoritative_only_status: documented
+- recursion_status: disabled
+- any_query_policy_status: minimized-refused
+- udp_payload_policy_status: capped-512-with-truncation
+- tcp_resource_policy_status: documented
+- edge_rate_limit_status: external-required-until-native-limit
+- target_rate_limit_proof_status: required-before-public-release
+- amplification_review_status: required-before-public-release
+
+These markers are checked by `make security` and copied into release evidence.
+Changing one of them requires a matching implementation, tests, and refreshed
+load evidence.
+
+## Authoritative-Only Behavior
+
+- The daemon must not perform recursion, forwarding, cache lookup, or outbound
+  DNS resolution.
+- AXFR is disabled at the daemon boundary.
+- Non-IN classes are refused without a response body.
+- Unknown or malformed packets are dropped on the wire and logged.
+- `ANY` queries are amplification-prone and are minimized at the daemon
+  boundary with a small REFUSED response instead of enumerating zone records.
+
+## Response Size Policy
+
+- UDP receives at most 512-byte DNS packets and emits responses capped to the
+  512-byte DNS UDP limit.
+- When a UDP response would exceed the configured transport maximum, the daemon
+  sets TC and truncates to the question section.
+- TCP can carry larger protocol responses, but TCP sessions are bounded by
+  connection count, idle timeout, and read/write deadlines.
+- EDNS larger UDP payload negotiation is not a production feature in this
+  release gate. Do not claim EDNS-sized UDP behavior without a separate abuse
+  review and soak run.
+
+## Zone Authoring Guidance
+
+- Avoid unnecessary `ANY`-amplifying record sets, large TXT records, and broad
+  wildcard records in public zones.
+- Review SOA, NS, MX, SRV, TXT, and glue records for response size before
+  publication.
+- Keep wildcard coverage narrow and document why it is needed.
+- Treat private hostnames, internal addresses, and operational TXT records as
+  sensitive release-review items.
+
+## Edge Rate Limits
+
+The daemon does not yet include a native token-bucket or response-rate-limit
+implementation. Production deployments must provide equivalent rate limiting at
+the edge until that exists:
+
+- Per-source UDP query rate limits.
+- Separate limits for malformed traffic and repeated NXDOMAIN/REFUSED traffic.
+- TCP connection and connection-rate limits before the daemon.
+- Packet-filter or load-balancer rules that drop spoof-prone traffic where the
+  deployment environment can identify it.
+- Monitoring for response-size distribution, TC rate, refused `ANY` rate, and
+  dropped malformed query rate.
+
+Document the selected rate-limit layer, thresholds, and observed drops in the
+release packet. Target proof attached with `JDNS_TARGET_RATE_LIMIT_PROOF_FILE`
+must contain `edge_rate_limit_status=target-evidence-recorded` and is hashed
+into `dist/soak/`; required proof fails closed with
+`JDNS_REQUIRE_TARGET_RATE_LIMIT_PROOF=1`. Local short `dnsperf` smoke output is
+not production abuse evidence.
+
+## Production Evidence
+
+A production release needs:
+
+- `dist/release-evidence/abuse-policy.txt` with the markers above.
+- `dist/release-evidence/soak/status.txt` with
+  `production_load_status=current-soak-recorded` and `tcp_smoke_status=pass`.
+- DNS soak logs that include both UDP and TCP coverage for the selected zone
+  mix.
+- Edge rate-limit configuration or target-host firewall/load-balancer evidence.
+- `target_rate_limit_proof_status=present` in `dist/release-evidence/soak/status.txt`
+  when edge rate-limit proof is attached instead of generated by the local run.
+- External review of authoritative-only behavior, `ANY` minimization, response
+  sizing, and rate-limit thresholds.
diff --git a/docs/release-evidence.md b/docs/release-evidence.md
new file mode 100644
index 0000000..2b9fb44
--- /dev/null
+++ b/docs/release-evidence.md
@@ -0,0 +1,118 @@
+# Release Evidence
+
+`jerboa-dns` stays private until a release candidate has current evidence for
+the full daemon gate, dependency inventory, reproducibility, fuzzing, load
+testing, and external review.
+
+Run from a clean checkout:
+
+```sh
+make clean
+make release-evidence JERBUILD=/path/to/jerbuild
+```
+
+The `release-evidence` target runs `make verify` first, then writes untracked
+artifacts under `dist/release-evidence/`:
+
+- `git-commit.txt` and `git-status.txt`.
+- `build-environment.txt` with host-neutral OS and Jerboa toolchain status.
+- `sbom/` and `sbom.log` with toolchain identity, source/release-input hashes,
+  Cargo lockfile hashes, generated WASM hashes, native Rust input hashes, and
+  Git state for the local `jerboa` checkout when present.
+- `reproducibility/` and `reproducibility.log` with two clean daemon builds,
+  SHA-256 manifests for `jdns`, generated libraries/FFI symbols, WASM payloads,
+  and bounded hash diffs when the manifests do not match. Raw binaries,
+  generated libraries, and scratch build logs are not archived.
+- Cargo dependency metadata for the WASM parser workspace and fuzz workspace.
+- RustSec audit output for the release WASM parser workspace.
+- SHA-256 hashes for checked-in DNS/CDB parser WASM artifacts.
+- `fuzz-evidence/` and `fuzz-evidence.log` with bounded cargo-fuzz evidence
+  status. By default this records `coverage_fuzz_status=blocked-not-run`; run
+  with `JDNS_RUN_COVERAGE_FUZZ=1` to capture current local coverage-fuzz smoke.
+- tcp_soak_smoke_status: documented
+- tcp_soak_smoke_cases_minimum: 128
+- tcp_soak_smoke_scope_status: tcp-query-response
+- target_soak_proof_status: documented
+- target_rate_limit_proof_status: documented
+- `soak/` and `soak.log` with DNS load-test tool availability, historical
+  benchmark hashes, and current soak status. By default this records
+  `status=not-run` and `production_load_status=blocked-not-run`; run with
+  `JDNS_RUN_RELEASE_SOAK=1` to capture a current UDP `dnsperf` soak plus bounded
+  DNS-over-TCP query/response smoke.
+- `abuse-policy.txt`, generated from `docs/rate-limit-amplification.md`, with
+  authoritative-only, recursion-disabled, minimized `ANY`, UDP payload, TCP
+  resource, and external edge-rate-limit policy markers.
+
+To record a current production-length soak, run:
+
+```sh
+JDNS_RUN_RELEASE_SOAK=1 \
+JDNS_SOAK_MODE=sandbox \
+JDNS_SOAK_SECONDS=1800 \
+JDNS_SOAK_QPS=15000 \
+JDNS_TCP_SMOKE_CASES=128 \
+make release-evidence JERBUILD=/path/to/jerbuild
+```
+
+Shorter values are useful for harness smoke tests, but are recorded as
+`status=short-smoke-not-production`, `load_status=local-smoke-recorded`, and
+`production_load_status=blocked-not-run`; TCP smoke output is recorded as
+`tcp_smoke_status=pass` only after the local DNS-over-TCP query/response helper
+passes. Short runs do not satisfy the daemon release gate.
+On macOS/local runs where chroot is unavailable, `JDNS_SOAK_ALLOW_SANDBOX_FALLBACK=1`
+may be used only to verify the load-test harness; that evidence is not
+production sandbox evidence.
+
+If production-length soak or edge rate-limit evidence is captured by another
+release-host job, attach those proof files to the evidence bundle with:
+
+```sh
+JDNS_TARGET_SOAK_PROOF_FILE=/path/to/soak-status.txt \
+JDNS_TARGET_RATE_LIMIT_PROOF_FILE=/path/to/rate-limit-proof.txt \
+JDNS_REQUIRE_TARGET_SOAK_PROOF=1 \
+JDNS_REQUIRE_TARGET_RATE_LIMIT_PROOF=1 \
+make soak-evidence
+```
+
+The soak proof must contain both `production_load_status=current-soak-recorded`
+and `tcp_smoke_status=pass`. The rate-limit proof must contain
+`edge_rate_limit_status=target-evidence-recorded`. Supplied proof files are
+accepted only after marker, size, high-confidence secret, private-path, SSH
+clone reference, and full-host-fingerprint checks pass; accepted files are
+copied into `dist/soak/` and hashed. Missing, empty, marker-mismatched,
+private-material, or secret-bearing required proof files fail closed with
+`status=blocked-target-proof`.
+
+To also record bounded local cargo-fuzz evidence in the release packet, run:
+
+```sh
+JDNS_RUN_COVERAGE_FUZZ=1 \
+JDNS_FUZZ_RUNS=2048 \
+make release-evidence JERBUILD=/path/to/jerbuild
+```
+
+This records `coverage_fuzz_status=local-smoke-recorded` only if all three
+targets, `dns_query`, `cdb_record`, and `cdb_file`, complete. It is useful local
+coverage evidence but does not replace sustained release-host fuzzing.
+
+For a release candidate, archive those files with the candidate binaries and
+record the exact `JERBUILD` release used. The evidence sanitizer runs over
+release, SBOM, reproducibility, soak, and fuzz evidence before the target
+returns. Reproducibility is claimed only when
+`dist/release-evidence/reproducibility/result.txt` records `status=match`,
+`artifact_archive_status=hashes-only`, and
+`scratch_build_logs_status=not-archived`, or a release owner records an
+explicit exception. Soak/load evidence is claimed only
+when `dist/release-evidence/soak/status.txt` records `status=current-soak-recorded`
+for a production-length run, records `tcp_smoke_status=pass`, and the release
+owner reviews the copied dnsperf, watchdog, and TCP smoke logs, or when a
+required `target_soak_proof_status=present` proof file is reviewed and accepted.
+Edge rate-limit evidence is claimed only when the release packet contains
+reviewed target configuration/evidence and `target_rate_limit_proof_status=present`.
+
+The external review packet must include this file, `SECURITY.md`,
+`docs/threat-model.md`, `docs/rate-limit-amplification.md`, `docs/fuzzing.md`,
+the corpus under `fuzz/corpus/`, the latest UDP/TCP soak and load-test logs,
+`dist/release-evidence/soak/`, `dist/release-evidence/fuzz-evidence/`,