Skip to content

Add secret-store references for app-config secrets#873

Open
prk-Jr wants to merge 5 commits into
mainfrom
846-secrets-to-secret-store
Open

Add secret-store references for app-config secrets#873
prk-Jr wants to merge 5 commits into
mainfrom
846-secrets-to-secret-store

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Stops ts config push from 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.
  • Keeps the config blob free of plaintext secrets while preserving existing behavior — inline mode is unchanged and the default.
  • Fastly-first: the Fastly adapter resolves against its secret store; other adapters fail closed on store-mode blobs until wired in follow-ups.

Changes

File Change
crates/trusted-server-core/src/secret_refs.rs New. Resolver that walks the verified blob JSON and swaps secret-ref key names (nested + array paths) for values from PlatformSecretStore; mirrors EdgeZero secret_walk semantics.
crates/trusted-server-core/src/settings.rs Add [secrets] section (enabled, store); SecretFieldMode split; validate_secret_key_names; gate admin-password check by mode.
crates/trusted-server-core/src/config.rs Deploy validation branches on mode (key-name shape vs placeholder rejection); registry built with mode-aware token checks.
crates/trusted-server-core/src/config_payload.rs settings_from_config_blob_with_secrets resolves 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.rs from_config_with_secret_mode defers the API-token length check to runtime in key-name mode.
crates/trusted-server-core/src/settings_data.rs Loader takes Option<&dyn PlatformSecretStore>.
crates/trusted-server-adapter-fastly/src/app.rs Pass FastlyPlatformSecretStore into the config load path.
crates/trusted-server-adapter-axum/src/app.rs Pass None (fail closed on store-mode; wiring is a follow-up).
crates/trusted-server-core/src/lib.rs Register secret_refs module.
fastly.toml Local Viceroy ts_secrets entries for ec_passphrase / proxy_secret / admin_password.
trusted-server.example.toml Document store mode and the seed-values-first / rollout-order guidance.
docs/superpowers/plans/2026-07-09-secrets-to-secret-store.md Implementation plan.

Closes

Closes #846

Test plan

  • cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spin
  • cargo 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 -- --check
  • Parity: cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity
  • Manual testing via fastly compute serve: store-mode config booted with secrets resolved from ts_secrets (200 render); removing a secret entry fail-closed with a 500 naming the missing ref (ec.passphrase), no value leaked.
  • JS tests / JS format / docs format — no JS changes; docs formatting deferred to CI.

Hardening note

Store-mode resolution runs before Settings parse; the runtime then runs full validation against the resolved values (min-length passphrase, placeholder rejection, admin-password check). A missing or invalid secret returns a Configuration error, which the Fastly entry point surfaces via the startup-error router (fail-closed) — no panic!/unwrap!/expect! on the config-derived path. Push/deploy validation only checks key-name shape (non-empty, no whitespace/control chars).

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code
  • Uses log macros (not println!)
  • New code has tests
  • No secrets or credentials committed (local ts_secrets values are obvious dev placeholders)

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
@prk-Jr prk-Jr self-assigned this Jul 9, 2026
prk-Jr and others added 3 commits July 9, 2026 12:58
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.
@prk-Jr prk-Jr marked this pull request as ready for review July 9, 2026 08:48

@ChristianPavilonis ChristianPavilonis 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.

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

  1. 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_value and reject_placeholder_secrets(). The resolved ec.partners[].api_token length/duplicate-hash checks and pull-sync consistency checks live in PartnerRegistry::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 in AppState) before returning loaded settings, and add a regression test with a short resolved partner api_token.

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)?;

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.

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.
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.

Move secrets from trusted-server.toml to secret store

2 participants