Repository navigation
Bind credentials to approved contexts and add secure provisioning - #1453
ChristianPavilonis wants to merge 18 commits into
Conversation
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.
c131cc7 to
67c5bee
Compare
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
left a comment
There was a problem hiding this comment.
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 to8bb27824eand 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_secretneeds 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 planhides the removed-reference path: see inline atcrates/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) --stdinkeeps trailing newlines and echoes on a terminal: see inline atcrates/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/setskip push-time validation: see inline atcrates/trusted-server-cli/src/commands/secrets/mod.rs:212checkexit codes and per-entry store listing: see inline atcrates/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
curlonPATH: see inline atdocs/guide/secrets-ci.md:57(suggestion)
🤔 thinking
LoadedConfigis discarded at load: see inline atcrates/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 atdocs/guide/secrets.md:222(suggestion)
Cross-cutting / body-level findings
- 🔧 Merge main, and give
trusted_forwarder.shared_secreta 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'strusted-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 exampleforwarder.shared, subject App, export None) that is active when[trusted_forwarder]is present; the old field added toreject_removed_references; validation matching main'svalidate_trusted_forwarder_shared_secret; ats secretsselector; 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 thatsecret_resolution.rsis gone. - 📌 Guide gaps.
docs/guide/error-reference.mdhas no entries for the new load errors (removed credential reference,required resolved binding unavailable,resolved binding context mismatch). Thedocs/guide/cli.mdcommand index has nots secretsrows.docs/guide/proxy-signing.md:33still 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,--manifestnow defaults to$EDGEZERO_MANIFESTbeforeedgezero.toml(crates/trusted-server-cli/src/app_config.rs:31), which also changes which configts auditandts config ad-templatesresolve; 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).wrangleris installed withnpm install --no-save, outside any lockfile (line 242).cargo install worker-buildrebuilds 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:29rebuildstrusted_server_adapter_spin.wasmwithtest-utilsinto the samewasm32-wasip1/releasepath where CI built the shipping artifact (test.yml:295); a separate target dir avoids that.scripts/test-fixture-generator.shchecks generator freshness, but no workflow runs it, so the generatedts1_declarations infastly.toml,crates/trusted-server-adapter-spin/spin.tomlandcrates/trusted-server-adapter-cloudflare/wrangler.ci.tomlcan 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-293still exportTRUSTED_SERVER__PUBLISHER__PROXY_SECRETandTRUSTED_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-267refusests1_kids on create and rotate because Cloudflare's secret namespace is flat, butrequest_signing/signing.rs:146loads whatever kidcurrent-kidinjwks_storenames. A writer who can change that config entry can point signing at an app credential's key. Applying the same check beforeget_bytescloses 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, andsource_domainisn't secret.read_config_entry(settings_data.rs:94-97) drops the store and key from its error, though neither is secret.compile_bindingsis 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 newexpectmessages incrates/trusted-server-adapter-axum/src/platform_https_tests.rsdon't follow the"should ..."form, and thevalidate_credentialasserts insecret_bindings_tests.rscarry no messages. Inec/pull_sync.rs,validated_pull_sync_url_rejects_http_schemeandvalidated_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 innormalize_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
prk-Jr
left a comment
There was a problem hiding this comment.
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
CompiledBindingDebug prints the handler username: see inline atcrates/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_headersnow receives signed headers: see inline atcrates/trusted-server-core/src/proxy.rs:1084
Cross-cutting / body-level findings
- 🔧 Branch conflicts with current
main.git merge-treeagainstmainatd1bb3fa19reports conflicts in 14 files, includingCargo.toml,Cargo.lock,config.rs,config_payload.rs,datadome/protection.rs,generate-viceroy-config.rsandtrusted-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.
dhruv8sh
left a comment
There was a problem hiding this comment.
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 asCONFLICTING.git merge-tree --write-tree origin/main 8bb27824e(main atd1bb3fa19) still conflicts, including ingenerate-viceroy-config.rs,tests/environments/axum.rs,tests/environments/cloudflare.rs,docs/guide/configuration.md,scripts/generate-integration-viceroy-configs.shandtrusted-server.example.toml. This is the same blocker both earlier reviews raised. It also has to settle[trusted_forwarder] shared_secretand 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
Resolve merge conflicts, preserve credential binding and base forwarding/deadline behavior, update the EdgeZero companion revision, and repair native fixtures.
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.
Review follow-up c278e6aPushed 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 summariesThis 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.
Implemented inline changes
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 limitsThe 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:
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 gatedWe 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. |
…t without changing auction behavior.
prk-Jr
left a comment
There was a problem hiding this comment.
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 secretsresource resolution on Fastly: see inline atCargo.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.
| 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 } |
There was a problem hiding this comment.
🔧 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. |
There was a problem hiding this comment.
⛏ 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.
| /// 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
left a comment
There was a problem hiding this comment.
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 atsecret_bindings.rs:755
Cross-cutting
- 📌 EdgeZero pinned to unreleased commit —
Cargo.toml:60-65pins edgezeroc199355e…(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 viavalidated_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(); |
There was a problem hiding this comment.
🤔 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"))?; |
There was a problem hiding this comment.
🤔 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(['-', '.', ' '], "_"); |
There was a problem hiding this comment.
⛏ 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 { |
There was a problem hiding this comment.
📝 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
left a comment
There was a problem hiding this comment.
Summary
Round 2 at 96fdb107d. The round-1 blockers are resolved:
- The EdgeZero pin now carries main's dependency stack.
trusted_forwarder.shared_secretis the thirteenth role, and its derived key matches an independent recompute.- The pull-token minimum is gone, and
--stdinrejects 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 to96fdb107dand 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 atdocs/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 secretsechoes TOML source lines: see inline atcrates/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.mdstill describesspin-sdk6: see inline atdocs/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_resourcesparses the Fastly config-store list as a paginated page, but Fastly returns a bare array. Every Fastlyplan,checkandsettherefore stops withunable to resolve complete Fastly Config Store inventory. - The fix is
c24cd9940on IABTechLab/edgezero#444, whose head is now217983c04, five commits past the pin. No release contains it yet; the latest is v0.0.11. - One detail for the repin: after
c24cd9940the config request has no query string (GET /resources/stores/config).FakeTransport'sstarts_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".
- At
- 📌 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_resolutiontest filter CompiledBindingDebug printing the handler username- the
LoadedConfigcapability boundary - per-request S3 recompilation and the extra settings clone (dhruv8sh's thread)
- CI supply chain: the Spin checksum, the wrangler lockfile, the
worker-buildcache, and harness log upload - the Spin
test-utilsbuild writing the shipping wasm path - fixture-generator freshness not checked in CI
- dead
PROXY_SECRET/EC__PASSPHRASEoverlays.scripts/integration-tests-browser.sh:36-38belongs on this list too: it also exports aTRUSTED_SERVER__EC__PARTNERSJSON with removed inlineapi_tokenvalues. - the four ready docs suggestions from round 1: the CHANGELOG link and list spacing, the stale
axum-dev.md:41sentence, thesecrets.md:178anchor, and thesecrets.md:223Bash 3.2 loop
- spec and plan staleness, including the "twelve" counts and the
- ♻️ The binding plan compiles at least four times per config load. That's
resolve_bindings,from_settingsinside runtime validation (built and discarded), theIntegrationRegistry::with_planloop, andfrom_settingsagain in each adapter'sbuild_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 stillpub, has no production callers, and builds inbound authentication from the mutableapi_tokenshadow without a binding check;#[cfg(test)] pub(crate)would close that off. - 📝 The
EDGEZERO_MANIFESTdefault isn't documented yet. The disposition says the default is "already described in CLI help/docs", butdocs/guide/cli.md:222still says ad-templates and audit--manifestdefault toedgezero.toml. EdgeZero'sconfig push/diff/validatekeep the literaledgezero.tomldefault. With the variable exported, as the deploy docs do,ts auditandts secretscan resolve<release-root>whilets config pushresolves./edgezero.toml. That needs thecli.mdfix and a CHANGELOG line, or anAppConfigArgsdefault that matches EdgeZero's. - ⛏ Small items:
- CHANGELOG and stale tests
CHANGELOG.md:72, merged from #1107, still says[trusted_forwarder]has a "secret-storeshared_secretreference" field, which contradicts the Breaking entry at line 12.settings.rs:4396: the{"auth_header": ...}case intrusted_forwarder_requires_explicit_fields_and_rejects_unknown_fieldsnow 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_urlreturnsNonefirst, so a missing binding is skipped silently.adapter-fastly/src/tinybird.rs:198-202dropped the dataset and host from the send log; neither is secret.96fdb107ddropped 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, whileaxum_secret_nameuses Unicodeto_uppercase. Acurrent-kidsuch astſ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.
- The
- Docs accuracy
secrets-ci.md:124says a changed manifest needs a new plan, but the plan has no manifest fingerprint, andservice_idis only syntax-checked.- The bad-certificate caveat in
TESTING.mdandscripts/README.md:47-49is 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.mddoesn't listjqforsmoke-fastly.shandsmoke-spin.sh, which now require it, and it has no row forscripts/test-fixture-generator.sh.- The hedges at
fastly.md:466-467andspin.md:61read as pending work, although the smokes already use generated bindings. fastly.md:471andaxum-dev.md:45describe diagnostics that now readtrusted_server: config entry unavailable.
- Examples and fixtures
- The publisher examples in
configuration.md(minimal, env-override, production),secret-binding-migration.md:51-53andfirst-party-proxy.md:445-447use theexample.complaceholders that validation rejects. Either restore base's sentence that the placeholders are rejected until replaced, or usepublisher.example.com. crates/trusted-server-integration-tests/src/lib.rs:59still givesec.pullandtinybird.auctionone shared literal across partners. Nothing asserts those exports natively yet, so this is latent.
- The publisher examples in
- Merge with main
git merge-treeagainst main4d55a3c6dreports a comment-only conflict incrates/trusted-server-cli/tests/proxy_trusted_server.rs, because main landed the samebrowser.ready()wait. Take main's file.
- CHANGELOG and stale tests
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
| 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 |
There was a problem hiding this comment.
🔧 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.
| 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 |
There was a problem hiding this comment.
🔧 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, ....
| 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"))?; |
There was a problem hiding this comment.
♻️ 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.
| crate::config::validate_settings_for_runtime(&settings) | ||
| .map_err(|_| invalid("trusted_server", "invalid resolved configuration"))?; |
There was a problem hiding this comment.
♻️ 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_secretsvalidate_admin_handler_passwordsPartnerRegistry::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")); | ||
| } |
There was a problem hiding this comment.
♻️ 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:
| } | |
| } | |
| #[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.
| 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: |
There was a problem hiding this comment.
⛏ 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:
| 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/.
| 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 |
There was a problem hiding this comment.
⛏ 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:
| 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() { |
There was a problem hiding this comment.
♻️ 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)) { |
There was a problem hiding this comment.
♻️ 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 { |
There was a problem hiding this comment.
♻️ 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.
Summary
Closes #1037.
ts secrets plan/check/setworkflows, generated native declarations for all four adapters, secure direct Fastly writes, and operator/migration documentation.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.
ec.partners[*].api_token_enabledand S3auth.session_token_enabledwhere those optional credentials are needed; both default tofalse. Enabled missing credentials fail loading.See the migration guide. Restoring the old binary restores the original attack.
Targeted security evidence
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.test-utilsintegrations invoke the same core exporter through real native stores/transports. This proves the native core/adapter contract, not shipping S3 routing on those adapters.Validation
Local follow-up checks for c278e6a passed on the exact source contents committed here:
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.