Redact secrets from logs and downgrade PII log levels#468
Redact secrets from logs and downgrade PII log levels#468ChristianPavilonis wants to merge 3 commits intomainfrom
Conversation
aram356
left a comment
There was a problem hiding this comment.
Summary
This PR introduces a Redacted<T> wrapper type for secrets, downgrades PII/payload log levels from info!/debug! to debug!/trace!, and hard-caps the production log level to Info. Good foundational work — two issues need addressing before merge.
Blocking
1. PII/payload leak at WARN level in Prebid error path
crates/common/src/integrations/prebid.rs lines 879-884
log::warn! logs up to 1000 chars of raw response body on non-2xx status. This bypasses the debug/trace downgrades and will emit in production with the current Info max level. If Prebid returns echoed request/debug blobs, this can include user/device data.
Suggestion: only log the status code at warn! and move the body preview to trace!.
2. Insecure-secret sentinel checks are inconsistent
crates/common/src/settings.rs line 200 + crates/common/src/settings_data.rs line 37
See inline comments. validate_secret_key rejects "secret_key" (underscore) while settings_data.rs rejects "secret-key" (hyphen) — two different strings guarding against the same mistake with split behavior.
Non-blocking
See inline comments for details on each:
- ValidationError codes (
settings.rs:199):new()takes a code, not a human message - Missing regression test (cross-cutting): no test asserting
format!("{:?}", Settings)excludes secret literals across all secret-bearing fields — if a future field forgetsRedacted<T>, there is no safety net - build.rs #[path] coupling (
build.rs:9): fragile inclusion ofredacted.rs; needs a guard comment - Test code still logs synthetic ID (
synthetic.rs:266):log::info!("Generated synthetic ID: {}", synthetic_id)in test — minor inconsistency with PR goal - PII at debug level (
synthetic.rs:99): template input with raw IP/UA downgraded todebug!but nottrace!like other payload logs - Hard-capped log level (
main.rs:177): follow-up to make max level configurable for incident debugging - Non-constant-time auth comparison (
auth.rs:17): out of scope, follow-up forsubtle::ConstantTimeEq
CI Status
All 10 checks PASS: cargo fmt, cargo test, vitest, CodeQL, format-docs, format-typescript, Analyze (actions, javascript-typescript x2, rust).
- Split Prebid warn log so status stays at warn! and body moves to trace! - Consolidate secret-key placeholder checks into validate_secret_key - Use proper ValidationError codes (snake_case identifiers) - Remove redundant InsecureSecretKey error variant - Downgrade synthetic template input log from debug! to trace! - Add build.rs #[path] guard comment to redacted.rs - Downgrade test log from info! to debug!
Review feedback addressedAll blocking and non-blocking items from the review have been resolved in 544d386: Blocking fixes1. PII/payload leak at WARN level in Prebid error path ( 2. Inconsistent secret sentinel checks ( Non-blocking fixes3. ValidationError codes ( 4. PII at debug level ( 5. Guard comment on redacted.rs ( 6. Test log level ( Deferred to follow-up issues
All checks pass locally (fmt, clippy, 481 Rust tests, 239 JS tests, WASM build). |
aram356
left a comment
There was a problem hiding this comment.
Summary
The Redacted<T> wrapper and log-level downgrades achieve the stated goal of preventing secrets and PII from leaking in production logs. One validation gap needs addressing before merge.
Blocking
🔧 wrench
proxy_secretmissing validation: No#[validate]onPublisher.proxy_secret— an empty or placeholder value silently produces encryption with a known key (crates/common/src/settings.rs:27)
Non-blocking
🤔 thinking
- Synthetic IDs at
debug!: Template input moved totrace!but the IDs themselves remain atdebug!— inconsistent PII treatment (crates/common/src/synthetic.rs:112,135,143,176,crates/common/src/publisher.rs:241) secret_store_idasRedacted: This is a store identifier, not a secret. Adjacentconfig_store_idis plainString— inconsistency without clear benefit (crates/common/src/settings.rs:273)
🌱 seedling
RedactedmissingPartialEq: Forces*handler.username.expose() == usernamepattern — could be simplified (crates/common/src/redacted.rs:28)
🏕 camp site
println!in test code:endpoints.rstest module usesprintln!(~10 instances) adjacent to the.expose()changes. CLAUDE.md says to uselogmacros.
CI Status
- cargo fmt: PASS
- cargo clippy: PASS
- cargo test: PASS
- vitest: PASS
- format-docs: PASS
- format-typescript: PASS
- CodeQL: PASS
| /// Secret used to encrypt/decrypt proxied URLs in `/first-party/proxy`. | ||
| /// Keep this secret stable to allow existing links to decode. | ||
| pub proxy_secret: String, | ||
| pub proxy_secret: Redacted<String>, |
There was a problem hiding this comment.
🔧 wrench — proxy_secret has no empty-value or placeholder validation.
Publisher.proxy_secret is wrapped in Redacted<String> but has no #[validate] annotation. secret_key validates against empty and placeholder values. Handler::username/password use validate_redacted_not_empty. But proxy_secret has zero validation.
Since Redacted<String> implements Default (empty string), an unconfigured deployment silently derives an encryption key from SHA-256("") in encode_url/decode_url — encryption with a known key. The default config placeholder "change-me-proxy-secret" is also not caught.
Fix: Add validation, e.g.:
#[validate(custom(function = validate_redacted_not_empty))]
pub proxy_secret: Redacted<String>,Or a dedicated validator that also catches the placeholder value.
| let synthetic_id = format!("{}.{}", hmac_hash, random_suffix); | ||
|
|
||
| log::info!("Generated fresh ID: {}", synthetic_id); | ||
| log::debug!("Generated fresh ID: {}", synthetic_id); |
There was a problem hiding this comment.
🤔 thinking — Synthetic IDs still logged at debug! level.
The PR moved template input (containing client IP + user-agent) to trace! on line 99, but the generated/existing synthetic IDs remain at debug! here and on lines 135, 143, 176, and in publisher.rs:241.
These are user-identifying pseudonymous identifiers. Safe at the new Info default, but if an operator bumps to Debug for troubleshooting, they leak. Inconsistent with the PR's own treatment of the template input.
| pub enabled: bool, | ||
| pub config_store_id: String, | ||
| pub secret_store_id: String, | ||
| pub secret_store_id: Redacted<String>, |
There was a problem hiding this comment.
🤔 thinking — secret_store_id as Redacted is questionable.
This is a Fastly store identifier (like a table name), not a secret value. The adjacent config_store_id is left as a plain String, making the inconsistency visible. If this is intentional defense-in-depth, a doc comment explaining why would help. Otherwise it adds .expose() friction at both call sites in endpoints.rs (lines 152, 256) without security benefit.
| /// assert_eq!(format!("{:?}", secret), "[REDACTED]"); | ||
| /// assert_eq!(secret.expose(), "my-secret-key"); | ||
| /// ``` | ||
| #[derive(Clone, Serialize, Deserialize)] |
There was a problem hiding this comment.
🌱 seedling — Redacted missing PartialEq.
Auth comparison in auth.rs:17 requires *handler.username.expose() == username. Implementing PartialEq (either Redacted<T> == Redacted<T> or adding an eq(&self, other: &T) -> bool method) would avoid exposing the inner value just for equality checks.
Summary
Redacted<T>wrapper that prints[REDACTED]in Debug/Display while preserving transparent serde and.expose()access.debug!and payload logs totrace!so they no longer appear in production.DebugtoInfoto prevent verbose output in production by default.Changes
crates/common/src/redacted.rsRedacted<T>wrapper type with[REDACTED]Debug/Display, transparent serde,.expose(), and testscrates/common/src/lib.rsmod redactedcrates/common/src/settings.rsproxy_secret,secret_key,username,password,secret_store_idtoRedacted<String>; updated validatorscrates/common/build.rs#[path]for buildcrates/common/src/auth.rs.expose()for credential comparisoncrates/common/src/http_util.rs.expose()for proxy_secret accesscrates/common/src/settings_data.rs.expose()for secret_key comparisoncrates/common/src/request_signing/endpoints.rs.expose()for secret_store_idcrates/common/src/synthetic.rsinfo!todebug!crates/common/src/integrations/prebid.rsdebug!totrace!crates/common/src/integrations/aps.rsdebug!totrace!crates/common/src/integrations/adserver_mock.rsdebug!totrace!crates/fastly/src/main.rsDebug→Info; settings loginfo!→debug!AGENTS.mdtracingreference tologCloses
Closes #404
Test plan
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest runcd crates/js/lib && npm run formatcd docs && npm run formatcargo build --bin trusted-server-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)