Add secret-store references for app-config secrets#873
Conversation
Secret-bearing config fields (ec.passphrase, publisher.proxy_secret, ec.partners[].api_token, ec.partners[].ts_pull_token, handlers[].password) previously rode in the app-config blob as plaintext, so `ts config push` persisted them into the config store. Add an opt-in `[secrets]` mode where those fields instead hold secret-store key names, resolved from the platform secret store at settings load. Validation splits along the EdgeZero 3.3.8 boundary: push/deploy validates key-name shape and skips value-shape checks (a key name is not the secret); runtime resolves the references first, then runs full validation against the real values and fails closed if a referenced secret is missing or invalid. Fastly wires FastlyPlatformSecretStore into the config load path; the other adapters pass None and fail closed on store-mode blobs until wired in follow-ups. Local Viceroy secrets and a store-mode template are documented. Closes #846
Reject TRUSTED_SERVER__ secret env overrides at push/diff/validate time when store mode is enabled, so the push env overlay cannot overwrite a secret-ref key name with a plaintext value in the config blob (independent of --no-env). Omit the [secrets] section from the serialized blob whenever store mode is disabled, not only at the exact default, so inline-mode blobs stay compatible with binaries that predate the field under deny_unknown_fields. Fail closed in the resolver when a covered array section (ec.partners, handlers) is present in a non-array encoding instead of silently skipping it. Keep the partner API-token non-empty check in key-name mode and defer only the minimum-length rule. Correct the docs: the Fastly CLI reads secret values from stdin or a file (there is no --secret flag), and list every covered override including partner ts_pull_token.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #873 against main, focusing on the new [secrets] store-mode config path, secret reference resolution, deploy/runtime validation split, and Fastly adapter wiring. Overall the design is sound and CI is green, but I found one medium-risk validation gap where resolved EC partner secrets are not validated at settings-load/startup time.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
- Resolved EC partner secrets are not fully validated at settings load —
crates/trusted-server-core/src/config_payload.rs:80- Issue: Store-mode deploy validation correctly treats partner API tokens as key names and defers value-shape checks, but after runtime resolution this path only calls
Settings::from_json_valueandreject_placeholder_secrets(). The resolvedec.partners[].api_tokenlength/duplicate-hash checks and pull-sync consistency checks live inPartnerRegistry::from_config(... ResolvedValues), which Fastly currently constructs later per route rather than while loading settings. - Why it matters: A mis-seeded secret-store value can let the service start and serve unrelated routes even though the store-mode contract says invalid resolved secrets fail closed during settings load/startup. The affected partner routes will fail when they build the registry, but the bad config is detected later and less consistently than inline-mode deploy validation.
- Suggested fix: After resolving store-mode refs and deserializing settings, run
PartnerRegistry::from_config(&settings.ec.partners)?(or otherwise construct/cache the registry inAppState) before returning loaded settings, and add a regression test with a short resolved partnerapi_token.
- Issue: Store-mode deploy validation correctly treats partner API tokens as key names and defers value-shape checks, but after runtime resolution this path only calls
P3 / Low
None.
CI / Existing Reviews
gh pr checks reports all current checks passing, including Rust tests, clippy/check jobs, docs/TS formatting, vitest, and integration/parity tests. I found no existing reviews or inline comments to avoid duplicating.
| resolve_secret_refs(&mut data, secret_store)?; | ||
| } | ||
|
|
||
| let settings = Settings::from_json_value(data)?; |
There was a problem hiding this comment.
Automated review: Store-mode deploy validation now intentionally treats ec.partners[].api_token as a key name and skips the resolved token length/duplicate-hash checks, but after resolve_secret_refs this load path only runs Settings::from_json_value plus placeholder rejection. The checks that validate resolved partner tokens and pull-sync consistency live in PartnerRegistry::from_config(...), which Fastly currently builds later per request. That means a mis-seeded partner token can let startup succeed and only fail affected routes, rather than failing closed at settings load. Please validate the resolved partner registry here (or construct/cache it during AppState startup) and cover a short resolved partner token in a regression test.
Store mode defers ec.partners[].api_token value checks (length, token-hash uniqueness, pull-sync consistency) from deploy time, but the runtime load path only deserialized settings and rejected placeholders without building the partner registry. A mis-seeded partner secret could let the service start and serve unrelated routes, failing only on partner-specific routes instead of failing closed at startup. Build the partner registry in settings_from_config_blob_with_secrets so resolved-value validators run at load in both inline and store mode.
Summary
ts config pushfrom persisting secret values in the config store: an opt-in[secrets]mode makes secret-bearing config fields hold secret-store key names, resolved from the platform secret store at settings load.Changes
crates/trusted-server-core/src/secret_refs.rsPlatformSecretStore; mirrors EdgeZerosecret_walksemantics.crates/trusted-server-core/src/settings.rs[secrets]section (enabled,store);SecretFieldModesplit;validate_secret_key_names; gate admin-password check by mode.crates/trusted-server-core/src/config.rscrates/trusted-server-core/src/config_payload.rssettings_from_config_blob_with_secretsresolves refs between envelope verify and parse; fails closed when store-mode is enabled but no secret store is wired.crates/trusted-server-core/src/ec/registry.rsfrom_config_with_secret_modedefers the API-token length check to runtime in key-name mode.crates/trusted-server-core/src/settings_data.rsOption<&dyn PlatformSecretStore>.crates/trusted-server-adapter-fastly/src/app.rsFastlyPlatformSecretStoreinto the config load path.crates/trusted-server-adapter-axum/src/app.rsNone(fail closed on store-mode; wiring is a follow-up).crates/trusted-server-core/src/lib.rssecret_refsmodule.fastly.tomlts_secretsentries forec_passphrase/proxy_secret/admin_password.trusted-server.example.tomldocs/superpowers/plans/2026-07-09-secrets-to-secret-store.mdCloses
Closes #846
Test plan
cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spincargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare && cargo clippy-cloudflare-wasm && cargo clippy-spin-native && cargo clippy-spin-wasm(1.95.0)cargo fmt --all -- --checkcargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parityfastly compute serve: store-mode config booted with secrets resolved fromts_secrets(200 render); removing a secret entry fail-closed with a 500 naming the missing ref (ec.passphrase), no value leaked.Hardening note
Store-mode resolution runs before
Settingsparse; the runtime then runs full validation against the resolved values (min-length passphrase, placeholder rejection, admin-password check). A missing or invalid secret returns aConfigurationerror, which the Fastly entry point surfaces via the startup-error router (fail-closed) — nopanic!/unwrap!/expect!on the config-derived path. Push/deploy validation only checks key-name shape (non-empty, no whitespace/control chars).Checklist
unwrap()in production codelogmacros (notprintln!)ts_secretsvalues are obvious dev placeholders)