Skip to content

Bind credentials to approved contexts and add secure provisioning - #1453

Open
ChristianPavilonis wants to merge 18 commits into
mainfrom
feature/1037-secret-bindings
Open

ChristianPavilonis wants to merge 18 commits into
mainfrom
feature/1037-secret-bindings

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #1037.

  • Bind all thirteen app credential roles to their fixed purpose, identity, and approved endpoint or route policy. Config contains no credential labels, physical key selectors, or inline-value fallback. Native credential values remain raw.
  • Resolve and validate one private credential/target snapshot before publishing runtime state. A config writer can recompute the envelope checksum, but changing a credential's purpose or destination selects an unprovisioned native identity rather than authorizing export.
  • Add reviewed ts secrets plan/check/set workflows, generated native declarations for all four adapters, secure direct Fastly writes, and operator/migration documentation.
  • Add targeted production-loader attacker regressions and real native HTTPS tests. Keep deployment evidence separate in Verify deployment readiness for context-derived secret bindings #1479. Related design draft: Specify context-derived secret bindings for #1037 #1407.

The threat model is a config-only writer, without native secret-writing, binding/deployment administration, or unreviewed execution with provisioning credentials. Complete already-provisioned contexts remain authorized. Retiring a binding blocks fresh loads, not credentials already captured by retained/in-flight snapshots.

Breaking change: reprovision before upgrading

Existing deployments must reprovision every required app credential under the generated bindings before deploying this version. Legacy secret names no longer work. Existing raw values can be reused; reprovisioning does not itself require rotation.

  1. Remove the thirteen legacy credential-reference fields. Set ec.partners[*].api_token_enabled and S3 auth.session_token_enabled where those optional credentials are needed; both default to false. Enabled missing credentials fail loading.
  2. Generate and independently review a binding plan, then provision every required binding and native declaration.
  3. Cut over matching binary/config versions in isolation, reconstruct runtime state, and verify traffic before retiring legacy bindings. Keep old/new config snapshots separate during rollout.

See the migration guide. Restoring the old binary restores the original attack.

Targeted security evidence

  • Production envelope reconstruction through pull dispatch has a working positive control. Valid-checksum destination/port/path mutations and publisher/inbound/handler key injection reject without reading the original pull credential, creating a backend, or sending HTTP.
  • Same-context replacement preserves old snapshot credentials until reconstruction; fresh-state replay fails after binding retirement.
  • Axum's real HTTP client exercises direct and pending sends over trusted HTTPS, untrusted-issuer and wrong-hostname rejection before HTTP, and all five redirect statuses to same-origin and other-origin sinks. Tests extend trust only with a test CA; production client policy is unchanged.
  • Native WASM checks validate exact S3 GET/HEAD signatures, signed Host/port, session credentials, trusted HTTPS, no-follow behavior, bad-certificate rejection despite proxy.certificate_check=false, and valid-checksum config attacks against unchanged native bindings. Redirect sinks have positive controls. Unsigned Viceroy startup probes are recorded separately from application sends.
  • Fastly runs its shipping S3 routes. Cloudflare and Spin currently do not dispatch S3 asset routes; their test-utils integrations invoke the same core exporter through real native stores/transports. This proves the native core/adapter contract, not shipping S3 routing on those adapters.
  • The Fastly native bad-certificate case proves Viceroy rejection, not independent enforcement of the app's forced-verification setting. An isolated forced-verification-off mutant still passes the native case; the independent core backend-spec assertion catches it. See TESTING.md for the evidence boundary.
  • A Node regression with a long-lived grandchild verifies SIGTERM during runtime startup reaps the owned process group. CI now runs the native checks with pinned runtime tools.

Validation

Local follow-up checks for c278e6a passed on the exact source contents committed here:

Check Result
Core host library 3,366 passed, 2 existing ignored
Fastly/Viceroy Adapter 247 passed; core WASI 3,360 passed, 6 ignored; additional suites passed
Fastly reusable sandbox 247 passed, 5 existing ignored
Axum / Cloudflare / Spin host suites Passed, existing doctest ignores retained
CLI 976 passed across unit/integration suites, 31 existing/manual/helper ignores
Cross-adapter parity / native fixture lib+bin 28 / 6 passed
Native security Axum HTTPS suite passed; Fastly, workerd, Spin 34 cases each passed
Clippy All eight repository aliases, native core, integration harness, and build-digest lint passed
JavaScript 1,838 tests across 75 files passed, 1 existing skipped; format and build passed
Docs / format / guards ESLint, Prettier, VitePress build, Rust format, build-digest tests, domain lint, WASM clock lint, and Node/script guards passed

Targeted mutants now fail the loaded-token, rate-limit, partner-pairing, pull-method, DataDome TLS, signing-read, route-isolation, stdin, environment-selector, and process-group regressions. The native Fastly TLS-policy mutant survives because Viceroy enforces certificate verification independently; its matching core backend assertion fails as expected. Native runs used Viceroy 0.21.0, Spin 4.1.0, and Wrangler 4.83.0/workerd with worker-build 0.8.5. Real stdin/hidden-input checks ran through Linux pipes and a PTY with fictional transport.

Existing ignored/manual/platform checks, macOS PTY behavior, and additional unrelated browser/deadline/EOF runtime harnesses were not rerun. Hosted CI will validate the pushed commit. Local checks do not certify deployment gates or every CI job.

Dependency and deployment readiness

EdgeZero is pinned to published commit c199355e5893fe58e050ed7bae8c5f8462e2f511, companion IABTechLab/edgezero#444. We are waiting for an accepted released version containing that prerequisite before final integration. No local path patch or released-prerequisite claim.

#1479 tracks the remaining deployment gates: dependency acceptance, actual config-publisher IAM separation, isolated provider create/replace/read-through and propagation/limits, protected provisioning CI, coordinated migration/rotation/retirement/downgrade rehearsals, and deployment-specific security evidence. Local tests do not clear those gates or authorize production rollout.

No real application credentials were read or provisioned. No production deployment, merge, or release tag was performed.

@ChristianPavilonis
ChristianPavilonis marked this pull request as ready for review October 27, 2026 19:46
Document the selected design for issue #1037 using raw native secret
values and purpose/identity/destination-derived lookup keys.

Include the independent comparison's required normalization, CLI/native
lookup, private-construction, retirement, and send-policy amendments.
This is a design-only change; implementation and runtime proof remain.
Require explicit Spin manifest paths before secret planning and checks so native declaration validation and plan fingerprints use the selected manifest.

Let Cargo check default fixture-generator freshness while preserving explicit executable overrides. Add CLI and shell regressions for both fixes.
@ChristianPavilonis
ChristianPavilonis force-pushed the feature/1037-secret-bindings branch from c131cc7 to 67c5bee Compare October 27, 2026 20:14
Merge base changes while preserving credential bindings, safe diagnostics and rewrite compatibility; provision generated secrets in EOF runtime fixtures.
…ve CodeQL false positives without changing runtime behavior.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This replaces operator-chosen secret references with context-derived bindings (#1037), adds ts secrets provisioning, and moves all four adapters onto EdgeZero's implicit-secret hooks. I found no path that exports a credential outside its loaded binding, and the derived keys match an independent implementation of the spec's golden vector. Changes are needed before merge: the EdgeZero pin rolls main's dependencies back, main's new forwarder credential has no binding role, two diagnostics were lost, pull tokens gained an undocumented 32-byte minimum, and the migration docs and --stdin path have operator hazards.

12 of the inline comments below carry a one-click GitHub suggestion; use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The other comments describe the fix in prose because it touches several files or lines outside the diff. Each suggestion was applied alone to 8bb27824e and checked with the gates named in its comment, then all twelve were applied together and the same gates rerun.

Blocking

🔧 wrench

  • EdgeZero pin rolls main's dependencies back: see inline at Cargo.toml:59
  • Main's trusted_forwarder.shared_secret needs a binding role: see cross-cutting below
  • Config errors collapsed into one generic message: see inline at crates/trusted-server-core/src/config.rs:133 (suggestion)
  • ts secrets plan hides the removed-reference path: see inline at crates/trusted-server-cli/src/commands/secrets/mod.rs:216 (suggestion)
  • Pull tokens newly need 32 bytes: see inline at crates/trusted-server-core/src/secret_bindings.rs:1170
  • Migration guide misses read-back and trimming hazards: see inline at docs/guide/secret-binding-migration.md:89 (suggestion)
  • --stdin keeps trailing newlines and echoes on a terminal: see inline at crates/trusted-server-cli/src/commands/secrets/mod.rs:163 (suggestion)

Non-blocking

♻️ refactor

  • Binding plan recompiled per request: see inline at crates/trusted-server-core/src/ec/registry.rs:148
  • Five security mutants survive the core suite: see inline at crates/trusted-server-core/src/ec/registry.rs:544
  • Pull request method unpinned: see inline at crates/trusted-server-core/src/ec/pull_sync.rs:898 (suggestion)
  • DataDome forced TLS and bound host unpinned: see inline at crates/trusted-server-core/src/integrations/datadome/protection.rs:961 (suggestion)
  • CLI tests depend on the ambient environment: see inline at crates/trusted-server-cli/src/commands/secrets/tests.rs:632
  • plan/check/set skip push-time validation: see inline at crates/trusted-server-cli/src/commands/secrets/mod.rs:212
  • check exit codes and per-entry store listing: see inline at crates/trusted-server-cli/src/commands/secrets/mod.rs:401
  • Fastly bad-certificate case may not detect an app regression: see inline at scripts/secret-bindings-runtime.mjs:320
  • S3 fixture values shared across routes: see inline at crates/trusted-server-integration-tests/src/lib.rs:63
  • SIGTERM test has no grandchild: see inline at scripts/secret-bindings-runtime.test.mjs:73
  • Spin encoder tests exercise a test-only copy: see inline at crates/trusted-server-adapter-spin/src/platform.rs:136
  • Newline guidance for non-bearer roles: see inline at docs/guide/secrets.md:244 (suggestion)
  • CI runner must control curl on PATH: see inline at docs/guide/secrets-ci.md:57 (suggestion)

🤔 thinking

  • LoadedConfig is discarded at load: see inline at crates/trusted-server-core/src/config_payload.rs:28

⛏ nitpick

  • CHANGELOG link and loose list: see inline at CHANGELOG.md:12 (suggestion)
  • Stale Axum release-gate sentence: see inline at docs/guide/axum-dev.md:41 (suggestion)
  • Broken Axum setup anchor: see inline at docs/guide/secrets.md:177 (suggestion)
  • ${name^^} fails in Bash 3.2: see inline at docs/guide/secrets.md:222 (suggestion)

Cross-cutting / body-level findings

  • 🔧 Merge main, and give trusted_forwarder.shared_secret a binding role. The branch conflicts with main in 13 files. Main's #1107 added [trusted_forwarder] shared_secret, which uses the operator-chosen key-name model this PR removes (main's trusted-server.example.toml: "shared_secret is a key in trusted_server_secrets, not the credential itself"). If the merge keeps that field as written, a config-only writer can point it at another purpose's key, which is the substitution #1037 closes. The merge needs a 13th implicit role: a descriptor and scope (for example forwarder.shared, subject App, export None) that is active when [trusted_forwarder] is present; the old field added to reject_removed_references; validation matching main's validate_trusted_forwarder_shared_secret; a ts secrets selector; docs and migration-table rows; and the "twelve" counts in tests, docs and the CHANGELOG updated.
  • 📌 Spec and plan still describe unfinished work. The design spec calls the schema "the proposed breaking schema, not configuration accepted by today's binary" (line 68) and the CLI "proposed, not available today" (line 263), and its status paragraph (line 7) still lists native HTTPS/no-follow behavior as an open release gate. The plan says the two highest-risk facts "remain unproven until implementation executes" (line 272), and its verification step runs cargo test -p trusted-server-core secret_resolution (line 214), which matches no tests now that secret_resolution.rs is gone.
  • 📌 Guide gaps. docs/guide/error-reference.md has no entries for the new load errors (removed credential reference, required resolved binding unavailable, resolved binding context mismatch). The docs/guide/cli.md command index has no ts secrets rows. docs/guide/proxy-signing.md:33 still says "Compute expected tstoken using proxy_secret".
  • 📌 Undocumented behavior changes. A DataDome test-bypass credential shorter than 32 bytes used to be ignored with a warning (base test short_test_bypass_credential_is_ignored_without_failing_startup); it now fails the whole config load, so it belongs under Breaking. Separately, --manifest now defaults to $EDGEZERO_MANIFEST before edgezero.toml (crates/trusted-server-cli/src/app_config.rs:31), which also changes which config ts audit and ts config ad-templates resolve; that needs a CHANGELOG line.
  • 📌 CI supply chain and diagnostics (.github/workflows/test.yml). The Spin tarball is downloaded without a checksum check (lines 302-308). wrangler is installed with npm install --no-save, outside any lockfile (line 242). cargo install worker-build rebuilds from source on every run with no cache (line 241). No step uploads the secret-bindings harness logs (runtime.log) on failure, on any adapter.
  • 📌 Harness hygiene. scripts/secret-bindings-runtime.sh:29 rebuilds trusted_server_adapter_spin.wasm with test-utils into the same wasm32-wasip1/release path where CI built the shipping artifact (test.yml:295); a separate target dir avoids that. scripts/test-fixture-generator.sh checks generator freshness, but no workflow runs it, so the generated ts1_ declarations in fastly.toml, crates/trusted-server-adapter-spin/spin.toml and crates/trusted-server-adapter-cloudflare/wrangler.ci.toml can drift from the compiler without CI noticing.
  • 📌 Dead legacy overlays. .github/actions/setup-integration-test-env/action.yml (lines 90-91, 102-103 and 133-134) and .github/workflows/test.yml:292-293 still export TRUSTED_SERVER__PUBLISHER__PROXY_SECRET and TRUSTED_SERVER__EC__PASSPHRASE. Both name properties this PR removes, so they have no effect on these builds and point readers at a configuration path that no longer exists.
  • 🌱 Reserve ts1_ on the signing read path too. request_signing/endpoints.rs:262-267 refuses ts1_ kids on create and rotate because Cloudflare's secret namespace is flat, but request_signing/signing.rs:146 loads whatever kid current-kid in jwks_store names. A writer who can change that config entry can point signing at an app credential's key. Applying the same check before get_bytes closes it.
  • ⛏ Small items. Pull-sync warnings no longer name the partner (for example "Pull sync: dispatch failed" in ec/pull_sync.rs), which makes one broken partner hard to find, and source_domain isn't secret. read_config_entry (settings_data.rs:94-97) drops the store and key from its error, though neither is secret. compile_bindings is restructured around a CodeQL 2.27.3 false positive (secret_bindings.rs:802-806, #[allow(clippy::question_mark)]); dismissing the alert would keep ?. The seven new expect messages in crates/trusted-server-adapter-axum/src/platform_https_tests.rs don't follow the "should ..." form, and the validate_credential asserts in secret_bindings_tests.rs carry no messages. In ec/pull_sync.rs, validated_pull_sync_url_rejects_http_scheme and validated_pull_sync_url_rejects_non_allowlisted_host (lines 667 and 679) now pass because the partner has no loaded binding, not because of the scheme or host; the checks they name live in normalize_pull_endpoint.

CI Status

  • browser integration tests: PASS
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • CodeQL: PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test: PASS (required)
  • CLAUDE.md symlink guard: PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (rust): PASS
  • cargo test (cross-adapter parity): PASS
  • ts dev lint domains (changed lines): PASS
  • HTML EOF runtime (Fastly): PASS
  • vitest: PASS
  • prepare integration artifacts: PASS
  • cargo test (axum native): PASS
  • Analyze (actions): PASS
  • format-typescript: PASS (required)
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • docs-proposal scripts: PASS
  • Analyze (python): PASS
  • format-docs: PASS (required)
  • cargo fmt: PASS (required)
  • CI Node pin guard: PASS
  • Analyze (javascript-typescript): PASS
  • PBS Terraform and Compose checks: PASS

Comment thread Cargo.toml Resolved
Comment thread docs/guide/secret-binding-migration.md Resolved
Comment thread docs/guide/secrets-ci.md Resolved
Comment thread CHANGELOG.md Resolved
Comment thread docs/guide/axum-dev.md Resolved
Comment thread docs/guide/secrets.md Resolved
Comment thread docs/guide/secrets.md Resolved

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Independent pass over the credential-binding engine, its runtime consumers (S3, pull sync, DataDome, Tinybird), the four adapters, and ts secrets. I found no path where a config-only change sends a credential to a destination or purpose other than the one its binding was derived for. Every field that affects sending is part of the derived key; the length-prefixed encoding has no collisions (the four golden vectors re-derive independently from the spec); and missing bindings fail closed on every adapter. The blocker below matches the existing review; the four inline findings are new and non-blocking.

Blocking

🔧 wrench

  • Branch conflicts with current main: see cross-cutting below

Non-blocking

🤔 thinking

  • CompiledBinding Debug prints the handler username: see inline at crates/trusted-server-core/src/secret_bindings.rs:201

📝 note

  • Pull allowlists with a trailing dot or several hosts no longer load: see inline at crates/trusted-server-core/src/secret_bindings.rs:747

⛏ nitpick

  • ABI domain separator duplicated as a literal: see inline at crates/trusted-server-core/src/secret_bindings.rs:619
  • unsigned_headers now receives signed headers: see inline at crates/trusted-server-core/src/proxy.rs:1084

Cross-cutting / body-level findings

  • 🔧 Branch conflicts with current main. git merge-tree against main at d1bb3fa19 reports conflicts in 14 files, including Cargo.toml, Cargo.lock, config.rs, config_payload.rs, datadome/protection.rs, generate-viceroy-config.rs and trusted-server.example.toml. The merge also needs to resolve main's [trusted_forwarder] shared_secret (#1107), which still uses the operator-chosen key-name model this PR removes. It needs a binding role and a removed-reference rejection, as the existing review describes. Hosted CI has not exercised the merged result.

CI Status

All 27 reported checks pass on 8bb27824e, including cargo test (required), cargo fmt (required), format-typescript (required), format-docs (required), the adapter suites, cross-adapter parity, integration and browser integration tests, and CodeQL.

Comment thread crates/trusted-server-core/src/proxy.rs Resolved

@dhruv8sh dhruv8sh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This replaces operator-chosen secret references with context-derived native bindings (#1037), adds ts secrets plan/check/set, and moves all four adapters onto EdgeZero's implicit-secret hooks. I traced each exporter (S3 signing, pull sync, DataDome Protection, Tinybird) back to its loaded binding. Every outbound URL, host, region and bearer header comes from the compiled target, not from mutable settings. Forced TLS applies on every export path. Missing or changed bindings fail before a backend is created. I agree with the two existing reviews and won't repeat their findings. The one blocker still open is the merge with current main. The two new findings below are documentation gaps and don't block merge.

Both inline comments below carry a one-click GitHub suggestion. Use Commit suggestion, or Add suggestion to batch to apply both together.

Blocking

🔧 wrench

  • Branch still conflicts with current main: see cross-cutting below

Non-blocking

📝 note

  • Stricter endpoint grammar is missing from the migration guide: see inline at docs/guide/secret-binding-migration.md:32 (suggestion)

⛏ nitpick

  • "Reordering entries does not" overstates what a reorder skips: see inline at docs/guide/secret-binding-migration.md:106 (suggestion)

Cross-cutting / body-level findings

  • 🔧 Branch still conflicts with current main. GitHub reports the PR as CONFLICTING. git merge-tree --write-tree origin/main 8bb27824e (main at d1bb3fa19) still conflicts, including in generate-viceroy-config.rs, tests/environments/axum.rs, tests/environments/cloudflare.rs, docs/guide/configuration.md, scripts/generate-integration-viceroy-configs.sh and trusted-server.example.toml. This is the same blocker both earlier reviews raised. It also has to settle [trusted_forwarder] shared_secret and the EdgeZero pin rollback described there. All 27 green checks ran on the pre-merge tree.

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (python): PASS
  • Analyze (rust): PASS
  • browser integration tests: PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • CI Node pin guard: PASS
  • CLAUDE.md symlink guard: PASS
  • CodeQL: PASS
  • docs-proposal scripts: PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • HTML EOF runtime (Fastly): PASS
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • PBS Terraform and Compose checks: PASS
  • prepare integration artifacts: PASS
  • ts dev lint domains (changed lines): PASS
  • vitest: PASS

Comment thread docs/guide/secret-binding-migration.md Resolved
Comment thread docs/guide/secret-binding-migration.md Resolved
Resolve merge conflicts, preserve credential binding and base forwarding/deadline behavior, update the EdgeZero companion revision, and repair native fixtures.
ChristianPavilonis and others added 2 commits November 2, 2026 10:09
Resolve merge conflicts while preserving credential-binding changes, base-branch Lockr routing, and connection-readiness regression coverage.
Restore migration diagnostics and short partner pull-token support. Reject unsafe stdin, validate provisioning configs, and distinguish unsupported checks.

Retain partner registries with loaded adapter state, reserve app credential names on signing reads, and strengthen native and mutation regressions. Document migration hazards and the Viceroy TLS evidence boundary.
@ChristianPavilonis

Copy link
Copy Markdown
Collaborator Author

Review follow-up c278e6a

Pushed c278e6a18. This implements the agreed subset of the feedback. Thread resolutions below record fixes, prior fixes, and explicit deferrals; they do not mean every suggestion was implemented or that this PR is ready for release.

Responses to the review summaries

This accounts for the body-level points in aram356's review, prk-Jr's review, and dhruv8sh's review. Each inline point also receives a reply on its own thread.

Review point Disposition
Merge conflicts, dependency rollback, and forwarder scope, repeated in all three summaries Already addressed by the main merges in 742c5ba and 8ce273d. There are thirteen roles, including forwarder.shared; removed-reference rejection, validation, fixtures, and migration coverage are present. The v0.0.11-based EdgeZero c199355e pin retains the updated SDK stack and esi patch. A released prerequisite is still required before final integration.
Spec/plan still call implemented interfaces proposed, retain twelve-role counts, describe native proof as unfinished, and contain a secret_resolution test filter Deferred from the selected implementation. Those historical design documents still need reconciliation with current code. TESTING.md and the migration guide are the updated implementation/evidence references. No no-op test filter is cited as executed proof.
Error-reference entries, secrets rows in the command index, and proxy-signing's old proxy_secret wording Deferred with the unselected general documentation cleanup. The existing CLI credential section and newly updated migration/input sections do not claim these three gaps are fixed.
DataDome bypass now fails loading for short values; EDGEZERO_MANIFEST changes default selection The bypass compatibility change is now documented in CHANGELOG and migration guidance. The manifest default is already described in CLI help/docs; the separately requested CHANGELOG note and audit/ad-template impact wording were not added in this batch.
Spin download checksum, Wrangler lockfile, worker-build cache, and failure-log artifact upload All deferred from this PR at the maintainer's direction. No CI or toolchain files changed.
Spin test-utils overwriting the shipping output path; generated declaration freshness not wired into CI Both deferred with the CI/harness-hardening work. Runtime fixture checks were executed locally, but no artifact-directory or workflow change is claimed.
Dead legacy proxy/passphrase overlay exports Left unchanged with the unselected cleanup batch. They still need removal; the new compiler does not restore those removed public properties.
Reserve ts1_ names on the signing read path Fixed here. The existing normalized reservation predicate is shared by writes and RequestSigner::from_services, rejecting before any secret read. A no-read regression catches removal of the guard; legacy deletion/deactivation remains available. The migration guide documents affected legacy signing KIDs.
Pull warnings omit the partner; config lookup errors omit store/key Deferred. This pass applied the two specifically approved deserialization/provisioning diagnostic changes, not a wider diagnostics revision.
CodeQL workaround, existing expect wording, and missing assertion messages Existing compiler workaround retained so the scanner behavior does not regress; no alert was dismissed. The pre-existing wording/assertion cleanup was not selected. Newly added test imports were moved to module scope to follow repository style.
Pull HTTP/non-allowlisted tests passed for missing authority rather than their named condition Fixed here. They exercise normalize_pull_endpoint directly and assert the endpoint/assertion failure instead of accepting a missing-binding failure.

Implemented inline changes

  • Restore useful serde and secrets CLI diagnostics while preserving removed-value redaction.
  • Remove only the newly imposed pull-token minimum; keep nonempty, placeholder, and Bearer representation checks.
  • Reject terminal stdin and trailing CR/LF without trimming. Real Linux pipe and PTY checks cover rejection without writes, exact bytes, and hidden-input privacy.
  • Validate provisioning configs through the existing push validator, and distinguish unsupported presence checks with exit 3.
  • Retain each adapter's PartnerRegistry with its settings snapshot, including Fastly finalization and post-send pull sync. The broader LoadedConfig/type-visibility refactor, move accessor, and S3 preparation/caching remain deferred.
  • Add loaded-token collision, rate-limit, partner-pairing, pull-method, and DataDome backend assertions.
  • Use route-specific S3 fixture values and expectations, add a real grandchild to process-group cleanup tests, isolate CLI fixture selectors, and test the production Spin encoder instead of a copy.
  • Document write-only secret recovery/rotation, byte-preservation hazards, stricter endpoint policy on parked features, allowlists, TLS, and reordering's plan-regeneration requirement.

The remaining inline Debug-redaction, ABI-constant deduplication, unsigned_headers rename/redundant signing, CHANGELOG link/spacing, stale Axum sentence, broken Axum anchor, and Bash 3.2 example portability requests were not selected for implementation. They are explicitly deferred in their thread replies, not silently treated as fixes.

Security evidence and limits

The Fastly TLS concern was confirmed by execution. An isolated app-policy mutant still passed all 34 native checks under Viceroy 0.21.0 when both verified and nocert backends were declared and trusted-HTTPS positive controls were required. Viceroy rejected the bad certificate even with the app setting off. The independent core backend-spec test catches that same mutation. TESTING.md and scripts/README.md now distinguish these claims.

Checks against the pushed source contents passed:

  • Core host 3,366 tests; core WASI 3,360 tests, with their existing ignores retained.
  • CLI 976 tests across suites; real pipe/PTY tests and ambient-selector regressions passed. PTY checks were Linux-only.
  • Fastly default and reusable suites each 247 passed; Axum, Cloudflare, and Spin host suites passed.
  • Native Fastly, workerd, and Spin each passed 34 cases with distinct S3 route credentials. Cloudflare/Spin use their fixture integration for the native core/adapter contract, not shipping S3 dispatch.
  • Cross-adapter parity 28 passed; native fixture lib/bin 6 passed.
  • All eight clippy aliases, native core/integration/build-digest lint, format, domain, Node/script guards, docs lint/build, and JS test/format/build checks passed. JS had 1,838 passing tests and one existing skip.
  • The targeted core, route-isolation, input, selector, and process-group mutants fail their regressions. The simulator-masked native TLS mutant is the documented exception above.

Existing ignored/manual/platform checks, macOS PTY behavior, and additional unrelated browser/deadline/EOF runtime harnesses were not rerun. Hosted CI must validate this pushed commit; these results are not a claim that every CI job ran locally.

Still gated

We agreed to wait for a released EdgeZero version containing IABTechLab/edgezero#444. The current implementation pin remains c199355e; this is not a released-prerequisite claim. #1479 continues to gate dependency acceptance, IAM separation, protected provisioning, provider behavior, and migration/deployment rehearsals. Closing review discussions does not clear those gates. No production credentials, deployment, PR merge, or release tag were used.

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Follow-up pass over the changes since 8bb27824e: the two main merges, c278e6a18, and the two CodeQL log commits. The earlier blocker is resolved. The branch is current with main. trusted_forwarder.shared_secret is now the thirteenth binding role, with removed-reference rejection and the same length, character and placeholder checks. The signing read path refuses ts1_ kids before any secret read. --stdin rejects terminal input and trailing CR/LF. Load diagnostics are restored without echoing removed values. The pull-token minimum is gone. Each adapter state keeps its own partner registry. One new blocker: at the pinned EdgeZero revision, ts secrets cannot resolve Fastly resources against the real API.

Blocking

🔧 wrench

  • Pinned EdgeZero revision breaks ts secrets resource resolution on Fastly: see inline at Cargo.toml:60

Non-blocking

⛏ nitpick

  • Loader doc comment no longer matches its diagnostics: see inline at crates/trusted-server-core/src/config_payload.rs:60 (suggestion)

CI Status

All 30 check runs pass on 96fdb107d, including cargo test, cargo fmt, format-typescript and format-docs (required), the adapter suites, cross-adapter parity, integration, browser and trace tests, worker deadlines, the ts CLI suites on Linux and macOS, and CodeQL.

Comment thread Cargo.toml
edgezero-cli = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.11" }
edgezero-core = { git = "https://github.com/IABTechLab/edgezero", tag = "v0.0.11", default-features = false }
# The implicit-secret companion includes the ingress/pre-dispatch APIs from v0.0.11.
edgezero-adapter-axum = { git = "https://github.com/IABTechLab/edgezero", rev = "c199355e5893fe58e050ed7bae8c5f8462e2f511", default-features = false }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench: At c199355e, ts secrets plan, check and set --adapter fastly cannot resolve resources against the real Fastly API.

fastly_secrets::resolve_resources requests the inventory for every store in edgezero.toml. Our manifest declares [stores.config], so it always fetches GET /resources/stores/config?limit=100 and parses the reply as a paginated {"data":[…],"meta":{…}} page. Fastly returns the config-store list as a bare JSON array. EdgeZero's own deploy path at this same revision already parses it that way (edgezero-adapter-fastly/src/cli.rs:801, parse_store_inventory(ResourceKind::Config, …)). The companion PR fixed this after the pinned commit, in IABTechLab/edgezero#444 commit c24cd9940 ("Fix Fastly secret resource resolution and native name validation").

The migration guide documents Fastly as the only adapter with direct ts secrets set. At this head, every Fastly plan, check and set stops with unable to resolve complete Fastly Config Store inventory, so operators cannot provision before cutover. CI stays green because FakeTransport in crates/trusted-server-cli/src/commands/secrets/tests.rs (lines 881-882) replies to /resources/stores/config? with the same paginated shape the pinned parser expects.

Reproduction: in a scratch copy of 96fdb107d, I changed only that fake to reply with [{"id":"fictional-config-store","name":"trusted_server_config"}]. secrets_cli_plan_and_stdin_create_replace_use_actual_shared_transport then fails with "unable to resolve complete Fastly Config Store inventory". I did not call the real Fastly API.

Proposed fix (apply manually; it spans Cargo.toml, Cargo.lock and tests.rs): this folds into the release-pin gate you already have. The released EdgeZero version must contain c24cd9940. When you repin, change the fake to answer GET /resources/stores/config with a bare array, so the test matches Fastly's real shape. #1479 also still records the older pin 4f645b1e.

/// settings are invalid.
/// Rejects invalid envelopes, removed reference properties, malformed feature
/// configuration, missing/invalid native bindings, or invalid resolved settings.
/// Diagnostics never attach provider errors, values, or untrusted input snippets.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick: Since c278e6a18, line 72 passes serde's detailed error through. Some of those messages quote the offending input, for example invalid type: integer `123`, expected a string. Removed credential fields are rejected before parsing, so this can't leak credentials, but "never attach … untrusted input snippets" is no longer true.

Suggested change
/// Diagnostics never attach provider errors, values, or untrusted input snippets.
/// Diagnostics never attach provider errors or credential values. Structural
/// errors can quote non-credential config input, such as a misspelled field.

@dhruv8sh dhruv8sh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Introduces context-derived native secret bindings for all 13 credential roles, an atomic resolved-credential snapshot validated against freshly compiled context at each export site, and reviewed ts secrets plan/check/set provisioning. The security design is solid; remaining comments are non-blocking (hot-path cost, diagnostics, a naming edge case, and a stale spec).

Non-blocking

  • 🤔 Per-request full settings clone + plan recompile on authenticated S3 routes — inline at proxy.rs:944
  • 🤔 Native store errors are collapsed into one category — inline at secret_bindings.rs:1251
  • ⛏ Axum env-name normalization is not injective over store names — inline at secret_bindings.rs:1523
  • 📝 Stale SSC spec for pull_sync_allowed_domains — inline at secret_bindings.rs:755

Cross-cutting

  • 📌 EdgeZero pinned to unreleased commit — Cargo.toml:60-65 pins edgezero c199355e… (edgezero#444). Please keep a released-version bump as an explicit merge gate (tracked alongside #1479).
  • 👍 Capability design — non-deserializable ResolvedBindings, exact-match re-validation via validated_for_plan, strict endpoint grammar before URL parsing, and the reviewed-plan equality gate in the CLI are well thought out and well tested.

CI Status

All 30 reported checks: PASS.

.ok_or_else(fail)?;
// Check the actual route passed by the caller, not only the same-index route
// in settings. Reinterpreting an old capability for a new route is forbidden.
let mut effective = settings.clone();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking: apply_asset_origin_auth clones the full Settings (including every resolved credential string) and calls compile_bindings(&effective), which recompiles every handler/asset-route regex and deserializes DataDome config — on every authenticated S3 request, and twice when the image-optimizer preflight runs (HEAD then GET). Consider compiling only this route's S3 entries (e.g. a small compile_s3_route_bindings(route, index) helper building the subject the same way compile_bindings does), or compiling the route plan once in handle_asset_proxy_request and passing it into both auth calls.

} else {
let bytes = store
.get_bytes(store_name, &compiled.key)
.map_err(|_| invalid(&compiled.field_path, "native credential unavailable"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking: .map_err(|_| invalid(.., "native credential unavailable")) drops the store error, so a transient store outage is indistinguishable from an unprovisioned key — exactly what operators will need to tell apart during the reprovisioning cutover this PR requires. If PlatformSecretStore errors are known to be secret-free, consider keeping the sanitized message but change_context-ing and attaching the error kind/category.

/// ```
#[must_use]
pub fn axum_secret_name(store: &str, key: &str) -> String {
let normalize = |value: &str| value.to_uppercase().replace(['-', '.', ' '], "_");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick: axum_secret_name maps -, . and space to _ and uppercases, so stores a-b, a.b and a_b produce the same env var. Low risk since the store mapping comes from the trusted manifest, but the CLI only detects collisions between bindings, not store names. Consider rejecting a store name that changes under normalization, or reusing the injective encoding spin_component uses.

.ok()
.and_then(|url| url.host_str().map(str::to_owned))
.ok_or_else(|| invalid(field_path, "invalid credential endpoint"))?;
for allowed in &partner.pull_sync_allowed_domains {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note: every pull_sync_allowed_domains entry must now equal the endpoint host (backup hosts rejected). This is documented in docs/guide/secret-binding-migration.md:39, but docs/superpowers/specs/2026-03-24-ssc-technical-spec-design.md (≈ lines 1440, 1853) still describes the old membership check — worth updating or annotating.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round 2 at 96fdb107d. The round-1 blockers are resolved:

  • The EdgeZero pin now carries main's dependency stack.
  • trusted_forwarder.shared_secret is the thirteenth role, and its derived key matches an independent recompute.
  • The pull-token minimum is gone, and --stdin rejects a terminal and trailing CR/LF.

All six round-1 security mutants I re-ran now fail their tests. Two blockers remain:

  • The getting-started quickstart fails at ts secrets plan --no-env. This is a side effect of the push-validation step I asked for in round 1.
  • The pinned EdgeZero revision can't resolve Fastly resources, which agrees with prk-Jr's open thread.

The non-blocking items are mostly diagnostics that still disappear, and tests that can no longer fail.

8 of the inline comments below carry a one-click GitHub suggestion; use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The other comments describe the fix in prose, because it touches several hunks or files. Each suggestion was applied alone to 96fdb107d and checked with the gates named in its comment, then all of them were applied together and the same gates rerun.

Blocking

🔧 wrench

  • Quickstart fails at ts secrets plan --no-env: see inline at docs/guide/getting-started.md:110 (suggestion) and :76 (suggestion)
  • Pinned EdgeZero revision can't resolve Fastly resources: see cross-cutting below

Non-blocking

♻️ refactor

  • Normalization errors still collapsed; main's test inverted: see inline at crates/trusted-server-core/src/config.rs:137
  • Runtime load hides value-dependent failures: see inline at crates/trusted-server-core/src/config_payload.rs:84
  • Integrity check untested: see inline at crates/trusted-server-core/src/config_payload.rs:958 (suggestion)
  • Removed-reference check skips non-object owners: see inline at crates/trusted-server-core/src/secret_bindings.rs:1299
  • ts secrets echoes TOML source lines: see inline at crates/trusted-server-cli/src/commands/secrets/mod.rs:225 (suggestion)
  • Subprocess input tests can pass vacuously: see inline at crates/trusted-server-cli/src/commands/secrets/tests.rs:250
  • Harness exits 0 through a symlinked path: see inline at scripts/secret-bindings-runtime.mjs:654 (suggestion)
  • Spin manifest replacement test can't fail: see inline at crates/trusted-server-integration-tests/tests/html_eof_runtime/config.rs:337
  • Runner doesn't pin its case set or count: see inline at scripts/secret-bindings-runtime.mjs:459
  • Retention tests don't test retention: see inline at crates/trusted-server-adapter-fastly/src/app.rs:1391

🤔 thinking

  • Non-UTF-8 environment value panics and prints it: see inline at crates/trusted-server-cli/src/commands/secrets/fastly.rs:225

⛏ nitpick

  • Interactive check looks at stderr, prompt goes to stdout: see inline at crates/trusted-server-cli/src/commands/secrets/mod.rs:147 (suggestion)
  • spin.md still describes spin-sdk 6: see inline at docs/guide/spin.md:20 (suggestion)
  • Forwarder provisioning step has no command: see inline at docs/guide/ts-dev-proxy.md:382 (suggestion)

Cross-cutting / body-level findings

  • 🔧 Pinned EdgeZero revision can't resolve Fastly resources. I independently confirmed prk-Jr's open thread at Cargo.toml:60:
    • At c199355e, resolve_resources parses the Fastly config-store list as a paginated page, but Fastly returns a bare array. Every Fastly plan, check and set therefore stops with unable to resolve complete Fastly Config Store inventory.
    • The fix is c24cd9940 on IABTechLab/edgezero#444, whose head is now 217983c04, five commits past the pin. No release contains it yet; the latest is v0.0.11.
    • One detail for the repin: after c24cd9940 the config request has no query string (GET /resources/stores/config). FakeTransport's starts_with("/resources/stores/config?") (secrets/tests.rs:881) has to become an exact match on that path returning a bare array, or the repinned suite fails with "unexpected fictional provider operation".
  • 📌 Track the deferred items in an issue. The deferrals live in resolved threads, which are easy to lose once this merges. #1479 covers deployment readiness, not these:
    • spec and plan staleness, including the "twelve" counts and the secret_resolution test filter
    • CompiledBinding Debug printing the handler username
    • the LoadedConfig capability boundary
    • per-request S3 recompilation and the extra settings clone (dhruv8sh's thread)
    • CI supply chain: the Spin checksum, the wrangler lockfile, the worker-build cache, and harness log upload
    • the Spin test-utils build writing the shipping wasm path
    • fixture-generator freshness not checked in CI
    • dead PROXY_SECRET / EC__PASSPHRASE overlays. scripts/integration-tests-browser.sh:36-38 belongs on this list too: it also exports a TRUSTED_SERVER__EC__PARTNERS JSON with removed inline api_token values.
    • the four ready docs suggestions from round 1: the CHANGELOG link and list spacing, the stale axum-dev.md:41 sentence, the secrets.md:178 anchor, and the secrets.md:223 Bash 3.2 loop
  • ♻️ The binding plan compiles at least four times per config load. That's resolve_bindings, from_settings inside runtime validation (built and discarded), the IntegrationRegistry::with_plan loop, and from_settings again in each adapter's build_state_*, plus DataDome and Tinybird on Fastly. Production Fastly (reusable sandbox off by default) and Spin build state per request, so this runs on every request. Two fixes: let runtime validation hand back the registry it builds, or skip that build when the caller constructs one right after. Separately, PartnerRegistry::from_config (ec/registry.rs:126) is still pub, has no production callers, and builds inbound authentication from the mutable api_token shadow without a binding check; #[cfg(test)] pub(crate) would close that off.
  • 📝 The EDGEZERO_MANIFEST default isn't documented yet. The disposition says the default is "already described in CLI help/docs", but docs/guide/cli.md:222 still says ad-templates and audit --manifest default to edgezero.toml. EdgeZero's config push/diff/validate keep the literal edgezero.toml default. With the variable exported, as the deploy docs do, ts audit and ts secrets can resolve <release-root> while ts config push resolves ./edgezero.toml. That needs the cli.md fix and a CHANGELOG line, or an AppConfigArgs default that matches EdgeZero's.
  • ⛏ Small items:
    • CHANGELOG and stale tests
      • CHANGELOG.md:72, merged from #1107, still says [trusted_forwarder] has a "secret-store shared_secret reference" field, which contradicts the Breaking entry at line 12.
      • settings.rs:4396: the {"auth_header": ...} case in trusted_forwarder_requires_explicit_fields_and_rejects_unknown_fields now fails because the empty default shadow is too short, not because a field is missing.
    • Lost diagnostics
      • ec/pull_sync.rs:216: the "loaded credential binding unavailable" warning can't fire. validated_pull_sync_url returns None first, so a missing binding is skipped silently.
      • adapter-fastly/src/tinybird.rs:198-202 dropped the dataset and host from the send log; neither is secret.
      • 96fdb107d dropped the planned-provider count from a production warning to satisfy CodeQL. Dismissing that alert as a false positive would keep the count.
    • Signing guard
      • The ts1_ guard (request_signing/mod.rs:58) folds case with ASCII rules, while axum_secret_name uses Unicode to_uppercase. A current-kid such as tſ1_x (long s) escapes the guard and maps to the same Axum name. This only matters when the signing store and the app store share a name; !kid.is_ascii() || closes it.
    • Docs accuracy
      • secrets-ci.md:124 says a changed manifest needs a new plan, but the plan has no manifest fingerprint, and service_id is only syntax-checked.
      • The bad-certificate caveat in TESTING.md and scripts/README.md:47-49 is stated for Fastly only. workerd and Spin can't disable verification at all, so there too the case proves runtime rejection, not app-side forced verification.
      • scripts/README.md doesn't list jq for smoke-fastly.sh and smoke-spin.sh, which now require it, and it has no row for scripts/test-fixture-generator.sh.
      • The hedges at fastly.md:466-467 and spin.md:61 read as pending work, although the smokes already use generated bindings.
      • fastly.md:471 and axum-dev.md:45 describe diagnostics that now read trusted_server: config entry unavailable.
    • Examples and fixtures
      • The publisher examples in configuration.md (minimal, env-override, production), secret-binding-migration.md:51-53 and first-party-proxy.md:445-447 use the example.com placeholders that validation rejects. Either restore base's sentence that the placeholders are rejected until replaced, or use publisher.example.com.
      • crates/trusted-server-integration-tests/src/lib.rs:59 still gives ec.pull and tinybird.auction one shared literal across partners. Nothing asserts those exports natively yet, so this is latent.
    • Merge with main
      • git merge-tree against main 4d55a3c6d reports a comment-only conflict in crates/trusted-server-cli/tests/proxy_trusted_server.rs, because main landed the same browser.ready() wait. Take main's file.

CI Status

  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (python): PASS
  • Analyze (rust): PASS
  • CI Node pin guard: PASS
  • CLAUDE.md symlink guard: PASS
  • CodeQL: PASS
  • HTML EOF runtime (Fastly): PASS
  • PBS Terraform and Compose checks: PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • cargo test: PASS (required)
  • docs-proposal scripts: PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • integration tests (Fastly EC lifecycle): PASS
  • integration tests: PASS
  • prepare integration artifacts: PASS
  • trace runtime and browser acceptance: PASS
  • ts dev lint domains (changed lines): PASS
  • vitest: PASS
  • worker-deadlines (cloudflare): PASS
  • worker-deadlines (spin): PASS

Comment on lines +110 to +118
ts secrets plan --adapter axum --app-config trusted-server.toml --no-env --out bindings-axum.json
set +x
while IFS= read -r name; do
# Fictional local values only. Use protected input for existing credentials.
value="$(openssl rand -base64 32)"
export "$name=$value"
unset value
done < <(jq -r '.entries[].native.native_name' bindings-axum.json | sort -u)
ts secrets check --adapter axum --app-config trusted-server.toml --no-env --plan bindings-axum.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench: The quickstart now stops at this command.

c278e6a18 made prepare() run TrustedServerAppConfig::new before planning, which I asked for in round 1. The starter config passes that validation only with the .env.dev publisher overlays, and --no-env drops them. Following this section verbatim at 96fdb107d:

[ts] could not validate effective unresolved app configuration: Configuration error: Configuration validation failed: trusted_server: Insecure default value for: publisher.domain, publisher.cookie_domain, publisher.origin_url

The plan exits 2, the export loop reads a missing bindings-axum.json, check fails, and the server starts without credentials. Without --no-env the plan matches the overlay-applied config that ts config push published a few lines earlier. The production guides keep --no-env and still work, because a real config carries no placeholders. Line 76 has the same problem; there is a separate suggestion there.

Suggested change
ts secrets plan --adapter axum --app-config trusted-server.toml --no-env --out bindings-axum.json
set +x
while IFS= read -r name; do
# Fictional local values only. Use protected input for existing credentials.
value="$(openssl rand -base64 32)"
export "$name=$value"
unset value
done < <(jq -r '.entries[].native.native_name' bindings-axum.json | sort -u)
ts secrets check --adapter axum --app-config trusted-server.toml --no-env --plan bindings-axum.json
ts secrets plan --adapter axum --app-config trusted-server.toml --out bindings-axum.json
set +x
while IFS= read -r name; do
# Fictional local values only. Use protected input for existing credentials.
value="$(openssl rand -base64 32)"
export "$name=$value"
unset value
done < <(jq -r '.entries[].native.native_name' bindings-axum.json | sort -u)
ts secrets check --adapter axum --app-config trusted-server.toml --plan bindings-axum.json

Verified alone on 96fdb107d, and together with the other suggestions: prettier --check . in docs/. I also ran this exact sequence from a clean checkout of the head (starter config, .env.dev, the two unsets): plan exits 0 and check reports all three bindings present.

fixtures only, an offline Axum plan uses the same core keys:

```bash
ts secrets plan --adapter axum --app-config trusted-server.toml --no-env --out bindings-local.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench: Same failure as line 110. This plan also uses the starter config, so --no-env makes it fail push validation with Insecure default value for: publisher.domain, ....

Suggested change
ts secrets plan --adapter axum --app-config trusted-server.toml --no-env --out bindings-local.json
ts secrets plan --adapter axum --app-config trusted-server.toml --out bindings-local.json

Verified alone on 96fdb107d, and together with the other suggestions: prettier --check . in docs/.

settings
.normalize_deserialized()
.map_err(serde::de::Error::custom)?;
.map_err(|_| serde::de::Error::custom("invalid Trusted Server configuration"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor: The round-1 diagnostics fix stops one step short. c278e6a18 forwards serde's error on line 134, but this line still maps every normalization error to invalid Trusted Server configuration. Base used .map_err(serde::de::Error::custom).

normalize_deserialized can only fail on image-optimizer keys that collide after trimming (medium and " medium", or two profile sets). Those are profile names, not credentials, and they are exactly what the operator needs to find the collision. Main's app_config_image_optimizer_rejects_trimmed_key_collisions pinned that detail; this PR rewrote it at lines 796-800 to assert the detail is absent.

Proposed fix (apply manually; it spans this hunk and the test hunk, and either half alone fails the test):

        settings
            .normalize_deserialized()
            .map_err(serde::de::Error::custom)?;

and restore main's assertions in the test:

            for message in [toml_error.to_string(), json_error.to_string()] {
                for expected in [
                    "image_optimizer.profile_sets",
                    "default_images",
                    "collide after trimming",
                    &format!("{first:?}"),
                    &format!("{second:?}"),
                ] {
                    assert!(
                        message.contains(expected),
                        "should preserve collision context with {expected:?}: {message}"
                    );
                }
            }

With both changes the restored test and app_config_preserves_structural_error_details pass on the host core suite.

Comment on lines +84 to +85
crate::config::validate_settings_for_runtime(&settings)
.map_err(|_| invalid("trusted_server", "invalid resolved configuration"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor: Runtime load hides the failures operators can't catch any earlier.

Base propagated validate_settings_for_runtime(&settings)?. Now every error becomes trusted_server: invalid resolved configuration with no child frame. Collapsing the integration validators makes sense here, since validator errors can echo a field value and the shadows are installed by this point. But three of the checks depend on resolved values, so ts config validate can never run them, and their messages carry only field paths, handler paths and partner domains:

  • reject_placeholder_secrets
  • validate_admin_handler_passwords
  • PartnerRegistry::from_settings, which catches two partners whose inbound tokens collide

A probe that loaded two partners with the same inbound token through settings_from_config_blob printed only Configuration error: trusted_server: invalid resolved configuration. In production that request fails with nothing pointing at ec.partners, and ts secrets check can't help, because it never reads values.

Proposed fix (apply manually; it also moves the three calls out of validate_settings_for_runtime in config.rs):

    // These checks depend on resolved values, so app-config validation cannot
    // run them. Their messages name fields and partner domains, never values.
    crate::config::validate_resolved_values(&settings)?;
    crate::config::validate_settings_for_runtime(&settings)
        .map_err(|_| invalid("trusted_server", "invalid resolved configuration"))?;

The merge also dropped main's runtime_blob_accepts_disabled_browser_bidder_ownership_* and runtime_blob_rejects_enabled_browser_bidder_ownership_* tests, which pinned runtime diagnostics. If dropping them was deliberate, a note in the PR description would help the next reader.

let diagnostic = format!("{error:?}");
assert!(!diagnostic.contains("fictional-provider-diagnostic"));
assert!(!diagnostic.contains("unrelated-legacy-value"));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor: Nothing tests the envelope integrity check any more. The test rewrite dropped main's tampered_blob_hash_is_rejected (the merge also dropped didomi_geo_query_parameters_survive_blob_round_trip). With envelope.verify() deleted from line 69, every test in config_payload, settings_data and secret_bindings still passes. This restores the check, and also pins that it runs before any native read:

Suggested change
}
}
#[test]
fn tampered_blob_hash_is_rejected_before_any_native_read() {
let (data, store) = fixture();
let mut envelope = BlobEnvelope::new(data, "2026-10-26T00:00:00Z".to_owned());
envelope.sha256 = "ff".repeat(32);
let tampered = serde_json::to_string(&envelope).expect("should encode a tampered envelope");
let error = settings_from_config_blob(&tampered, &store, &DEFAULT_SECRET_STORE_ID.into())
.expect_err("should reject a checksum mismatch");
assert!(
error
.to_string()
.contains("config integrity verification failed"),
"should report the integrity failure: {error}"
);
assert!(
store.reads.lock().expect("should lock read spy").is_empty(),
"should reject before any native read"
);
}

Verified alone on 96fdb107d, and together with the other suggestions, under the full adapter gate: cargo fmt --check, all six adapter clippy aliases, the host core suite, test-fastly, test-fastly-reuse, test-axum, test-cloudflare, test-spin and the parity suite. With envelope.verify() removed, the new test fails.

Comment thread docs/guide/spin.md
Comment on lines +20 to +31
Spin 4.1.0 or newer, pinned as `spin` in `.tool-versions`. This branch temporarily
uses `spin-sdk` 6 to match the unreleased EdgeZero secret-provisioning APIs;
the runtime minimum remains unchanged. `spin-sdk` 7 imports
`wasi:http/types@0.3.0`, which Spin 4.0.x does not provide. Install or upgrade
from the [Spin releases](https://github.com/spinframework/spin/releases).

asdf has no first-party Spin plugin, so `asdf install` does not provide this
one; install it directly, or add a community plugin if you want asdf to manage
it. The pin records the required version and is what the smoke checks against.

Nothing in the build catches this. The component compiles, `cargo
test-spin` passes, and only `spin up` reports:
With `spin-sdk` 7, the build does not catch an older runtime. The component
compiles, `cargo test-spin` passes, and only `spin up` reports:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick: This still describes the round-1 rollback. Cargo.toml and Cargo.lock are back on spin-sdk 7 since the main merge, so "This branch temporarily uses spin-sdk 6" and "With spin-sdk 7, ..." are stale. This restores base's wording and keeps your releases link:

Suggested change
Spin 4.1.0 or newer, pinned as `spin` in `.tool-versions`. This branch temporarily
uses `spin-sdk` 6 to match the unreleased EdgeZero secret-provisioning APIs;
the runtime minimum remains unchanged. `spin-sdk` 7 imports
`wasi:http/types@0.3.0`, which Spin 4.0.x does not provide. Install or upgrade
from the [Spin releases](https://github.com/spinframework/spin/releases).
asdf has no first-party Spin plugin, so `asdf install` does not provide this
one; install it directly, or add a community plugin if you want asdf to manage
it. The pin records the required version and is what the smoke checks against.
Nothing in the build catches this. The component compiles, `cargo
test-spin` passes, and only `spin up` reports:
With `spin-sdk` 7, the build does not catch an older runtime. The component
compiles, `cargo test-spin` passes, and only `spin up` reports:
Spin 4.1.0 or newer, pinned as `spin` in `.tool-versions`. `spin-sdk` 7 imports
`wasi:http/types@0.3.0`, which Spin 4.0.x does not provide, so an older runtime
cannot start the component. Install or upgrade from the
[Spin releases](https://github.com/spinframework/spin/releases).
asdf has no first-party Spin plugin, so `asdf install` does not provide this
one; install it directly, or add a community plugin if you want asdf to manage
it. The pin records the required version and is what the smoke checks against.
Nothing in the build catches an older runtime. The component compiles, `cargo
test-spin` passes, and only `spin up` reports:

Verified alone on 96fdb107d, and together with the other suggestions: prettier --check . in docs/.

Comment on lines +382 to +383
Provision the generated `forwarder.shared` binding from the reviewed
[`ts secrets plan`](./secrets) with the actual token bytes from the local file, excluding its line terminator. No credential property belongs in app config. The publisher domain or its subdomains must

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick: "Excluding its line terminator" doesn't say how. The token file this page generates (openssl rand -hex 32 > ./forwarder-token.txt) ends in LF, which ts secrets set --stdin rejects. A concrete command saves a failed first attempt:

Suggested change
Provision the generated `forwarder.shared` binding from the reviewed
[`ts secrets plan`](./secrets) with the actual token bytes from the local file, excluding its line terminator. No credential property belongs in app config. The publisher domain or its subdomains must
Provision the generated `forwarder.shared` binding from the reviewed
[`ts secrets plan`](./secrets) with the actual token bytes from the local file, excluding its line terminator. The file generated below ends with a line feed, which `ts secrets set --stdin` rejects, so strip it when piping, for example `tr -d '\r\n' < ./forwarder-token.txt | ts secrets set forwarder.shared --adapter fastly --app-config trusted-server.toml --no-env --plan bindings.json --stdin --yes`. No credential property belongs in app config. The publisher domain or its subdomains must

Verified alone on 96fdb107d, and together with the other suggestions: prettier --check . in docs/.

}

#[test]
fn spin_manifest_replaces_previous_context_declarations_without_values() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor: This test can't fail. The loop varies only the publisher origin port, which feeds no binding context. The template is the checked-in spin.toml, generated from the same example config, so the template and both iterations carry identical declarations. With if !fixtures.is_empty() { return Ok(template.to_owned()); } at the top of spin_fixture_manifest, the test still passes.

Proposed fix (apply manually): change a real context between iterations, for example the handler username or an added S3 auth route. Then assert that the previous iteration's variable names are gone and the new ones are present. Also, the comment at line 10 says "publisher and route changes alter native identities", but the publisher subject is ["app"].

: 'trusted_server_adapter_spin.wasm'
))
)
for (const [name, envelope] of Object.entries(fixture.cases)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor: The runner iterates whatever cases the fixture emits and never checks how many assertions ran. Dropping a mutated case from secret-bindings-fixture.rs still passes both the runner and the fixture's own test.

Proposed fix (apply manually; two places):

// after parsing the fixture
assert.deepEqual(Object.keys(fixture.cases).sort(), [
  'cross-field',
  'ec-allowlist',
  'ec-url',
  'original',
  's3-destination',
])
// before the final summary log
assert.equal(results.length, 34)

}

#[cfg(test)]
mod partner_registry_tests {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor: reconstructed_state_uses_new_credentials_without_changing_retained_registry doesn't exercise retention. It builds two states and reads state.partner_registry, but never routes a request. If a handler went back to calling PartnerRegistry::from_settings(&state.settings) per request, the test would still pass, because Settings is immutable and the rebuilt registry is identical. The same ~115-line module is copied into the Axum (app.rs:752), Cloudflare (app.rs:831) and Spin (app.rs:1035) adapters.

The construction-time validation tests in this module are useful. Consider keeping one core copy of those, and either route a request through the retained state or rename the retention test to what it checks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scope secret references to prevent config-store writers exfiltrating unrelated secrets

5 participants