From 16437172621a48efc1083bb4e92cd962e4ebc8b5 Mon Sep 17 00:00:00 2001 From: dhruv8sh Date: Fri, 18 Sep 2026 16:13:14 +0530 Subject: [PATCH 1/2] Add regression test asserting Settings debug output excludes secret values Signed-off-by: dhruv8sh --- crates/trusted-server-core/src/settings.rs | 132 +++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index f57714dcc..d6581f466 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -3806,6 +3806,138 @@ mod tests { ); } + // One distinctive canary per `Redacted` field reachable from + // `Settings`'s derived `Debug` impl. A new secret field added without the + // `Redacted` wrapper should be caught here by adding its own canary; + // re-run `rg 'Redacted<' crates/trusted-server-core/src` when touching + // this test to check the field list is still complete. + // + // Do not use `..Struct::default()` anywhere in this function. This test's + // entire purpose is exhaustive field coverage, and a default spread would + // silently swallow any field added to `Handler`, `TinybirdSettings`, or + // any other struct built here, defeating that coverage. List every field + // explicitly instead. + #[test] + fn settings_debug_output_redacts_every_secret_field() { + const CANARY_PROXY_SECRET: &str = "CANARY-PROXY-SECRET-0123456789"; + const CANARY_EC_PASSPHRASE: &str = "CANARY-EC-PASSPHRASE-0123456789"; + const CANARY_HANDLER_USERNAME: &str = "CANARY-HANDLER-USERNAME-0123456789"; + const CANARY_HANDLER_PASSWORD: &str = "CANARY-HANDLER-PASSWORD-0123456789"; + const CANARY_EC_PARTNER_API_TOKEN: &str = "CANARY-EC-PARTNER-API-TOKEN-0123456789"; + const CANARY_EC_PARTNER_TS_PULL_TOKEN: &str = "CANARY-EC-PARTNER-TS-PULL-TOKEN-0123456789"; + const CANARY_TRUSTED_CLIENT_IP_SHARED_SECRET: &str = + "CANARY-TRUSTED-CLIENT-IP-SHARED-SECRET-0123456789"; + const CANARY_S3_ACCESS_KEY_ID: &str = "CANARY-S3-ACCESS-KEY-ID-0123456789"; + const CANARY_S3_SECRET_ACCESS_KEY: &str = "CANARY-S3-SECRET-ACCESS-KEY-0123456789"; + const CANARY_S3_SESSION_TOKEN: &str = "CANARY-S3-SESSION-TOKEN-0123456789"; + const CANARY_TINYBIRD_AUCTION_TOKEN: &str = "CANARY-TINYBIRD-AUCTION-TOKEN-0123456789"; + const CANARY_TINYBIRD_ACCESS_TOKEN: &str = "CANARY-TINYBIRD-ACCESS-TOKEN-0123456789"; + + let mut settings = create_test_settings(); + + settings.publisher.proxy_secret = Redacted::new(CANARY_PROXY_SECRET.to_string()); + settings.ec.passphrase = Redacted::new(CANARY_EC_PASSPHRASE.to_string()); + + settings.handlers = vec![Handler { + path: "^/secure".to_string(), + username: Redacted::new(CANARY_HANDLER_USERNAME.to_string()), + password: Redacted::new(CANARY_HANDLER_PASSWORD.to_string()), + regex: OnceLock::new(), + }]; + + settings.ec.partners = vec![EcPartner { + name: "canary-partner".to_string(), + source_domain: "canary-partner.example".to_string(), + openrtb_atype: EcPartner::default_openrtb_atype(), + bidstream_enabled: false, + api_token: Some(Redacted::new(CANARY_EC_PARTNER_API_TOKEN.to_string())), + batch_rate_limit: EcPartner::default_batch_rate_limit(), + pull_sync_enabled: false, + pull_sync_url: None, + pull_sync_allowed_domains: Vec::new(), + pull_sync_ttl_sec: EcPartner::default_pull_sync_ttl_sec(), + pull_sync_rate_limit: EcPartner::default_pull_sync_rate_limit(), + ts_pull_token: Some(Redacted::new(CANARY_EC_PARTNER_TS_PULL_TOKEN.to_string())), + }]; + + settings.trusted_client_ip = Some(TrustedClientIpConfig { + ip_header: "fastly-client-ip".to_string(), + auth_header: "x-trusted-client-auth".to_string(), + shared_secret: Redacted::new(CANARY_TRUSTED_CLIENT_IP_SHARED_SECRET.to_string()), + }); + + let mut asset_route = ProxyAssetRoute::new("/s3-assets/", "https://s3.canary.example"); + asset_route.auth = Some(AssetOriginAuth::S3SigV4(S3SigV4AuthConfig { + region: "us-east-1".to_string(), + secret_store: None, + access_key_id: Redacted::new(CANARY_S3_ACCESS_KEY_ID.to_string()), + secret_access_key: Redacted::new(CANARY_S3_SECRET_ACCESS_KEY.to_string()), + session_token: Some(Redacted::new(CANARY_S3_SESSION_TOKEN.to_string())), + origin_query: None, + })); + settings.proxy.asset_routes = vec![asset_route]; + + settings.tinybird = TinybirdSettings { + auction_token_secret: Some(Redacted::new(CANARY_TINYBIRD_AUCTION_TOKEN.to_string())), + access_token_secret: Some(Redacted::new(CANARY_TINYBIRD_ACCESS_TOKEN.to_string())), + enabled: false, + api_host: String::new(), + secret_store: None, + auction_dataset: String::new(), + access_enabled: false, + access_dataset: String::new(), + access_sample_rate: 0.0f64, + max_body_bytes: 0, + }; + + let debug = format!("{settings:?}"); + + assert!( + debug.contains("[REDACTED]"), + "should redact secret fields in Settings debug output" + ); + + let canaries = [ + ("publisher.proxy_secret", CANARY_PROXY_SECRET), + ("ec.passphrase", CANARY_EC_PASSPHRASE), + ("handlers[].username", CANARY_HANDLER_USERNAME), + ("handlers[].password", CANARY_HANDLER_PASSWORD), + ("ec.partners[].api_token", CANARY_EC_PARTNER_API_TOKEN), + ( + "ec.partners[].ts_pull_token", + CANARY_EC_PARTNER_TS_PULL_TOKEN, + ), + ( + "trusted_client_ip.shared_secret", + CANARY_TRUSTED_CLIENT_IP_SHARED_SECRET, + ), + ( + "proxy.asset_routes[].auth.access_key_id", + CANARY_S3_ACCESS_KEY_ID, + ), + ( + "proxy.asset_routes[].auth.secret_access_key", + CANARY_S3_SECRET_ACCESS_KEY, + ), + ( + "proxy.asset_routes[].auth.session_token", + CANARY_S3_SESSION_TOKEN, + ), + ( + "tinybird.auction_token_secret", + CANARY_TINYBIRD_AUCTION_TOKEN, + ), + ("tinybird.access_token_secret", CANARY_TINYBIRD_ACCESS_TOKEN), + ]; + + for (field, canary) in canaries { + assert!( + !debug.contains(canary), + "should redact {field} in Settings debug output" + ); + } + } + #[test] fn trusted_client_ip_accepts_x_prefixed_ip_header() { let settings = Settings::from_toml(&trusted_client_ip_toml( From b70cb57a0af89cd9e6f8400a19a17e965bb3825c Mon Sep 17 00:00:00 2001 From: dhruv8sh Date: Tue, 22 Sep 2026 00:42:52 +0530 Subject: [PATCH 2/2] Guard the IntegrationSettings debug boundary and correct the test comment claims Adds a canary that pins IntegrationSettings hand-written Debug impl, which is the only thing keeping resolved DataDome credentials out of Settings debug output. Rewrites the leading comment to state what the canary list actually guarantees instead of an enforcement it cannot provide, and adds a positive assertion that a non-secret field stays visible so a blanket-redacting Debug impl would not pass unnoticed. Signed-off-by: dhruv8sh --- crates/trusted-server-core/src/settings.rs | 46 +++++++++++++++++----- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index d6581f466..dc0504398 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -3807,16 +3807,20 @@ mod tests { } // One distinctive canary per `Redacted` field reachable from - // `Settings`'s derived `Debug` impl. A new secret field added without the - // `Redacted` wrapper should be caught here by adding its own canary; - // re-run `rg 'Redacted<' crates/trusted-server-core/src` when touching - // this test to check the field list is still complete. + // `Settings`'s derived `Debug` impl. This is a regression guard over the + // field list below, not a completeness guarantee: a new secret field + // added without the `Redacted` wrapper has no canary here and will pass + // this test while leaking. Adding the canary is a manual step. // - // Do not use `..Struct::default()` anywhere in this function. This test's - // entire purpose is exhaustive field coverage, and a default spread would - // silently swallow any field added to `Handler`, `TinybirdSettings`, or - // any other struct built here, defeating that coverage. List every field - // explicitly instead. + // Integration configs are deliberately out of scope. They reach + // `Settings` as opaque JSON under `IntegrationSettings`, whose + // hand-written `Debug` impl prints only integration IDs, never values. + // + // Do not use `..Struct::default()` anywhere in this function. A default + // spread would let a new secret field be added to `Handler`, + // `TinybirdSettings`, or any other struct built here without forcing + // anyone to consider it. The compile break is the prompt; the canary + // list below is still maintained by hand. List every field explicitly. #[test] fn settings_debug_output_redacts_every_secret_field() { const CANARY_PROXY_SECRET: &str = "CANARY-PROXY-SECRET-0123456789"; @@ -3832,6 +3836,7 @@ mod tests { const CANARY_S3_SESSION_TOKEN: &str = "CANARY-S3-SESSION-TOKEN-0123456789"; const CANARY_TINYBIRD_AUCTION_TOKEN: &str = "CANARY-TINYBIRD-AUCTION-TOKEN-0123456789"; const CANARY_TINYBIRD_ACCESS_TOKEN: &str = "CANARY-TINYBIRD-ACCESS-TOKEN-0123456789"; + const CANARY_DATADOME_SERVER_SIDE_KEY: &str = "CANARY-DATADOME-SERVER-SIDE-KEY-0123456789"; let mut settings = create_test_settings(); @@ -3890,12 +3895,31 @@ mod tests { max_body_bytes: 0, }; + // `IntegrationSettings` stores integration configs as opaque JSON and + // relies on a hand-written `Debug` impl to suppress their values. That + // impl is the only thing keeping resolved DataDome credentials out of + // this output, so pin it here. + settings + .integrations + .insert_config( + "datadome", + &json!({ + "enabled": true, + "server_side_key_secret_name": CANARY_DATADOME_SERVER_SIDE_KEY, + }), + ) + .expect("should insert datadome integration config"); + let debug = format!("{settings:?}"); assert!( debug.contains("[REDACTED]"), "should redact secret fields in Settings debug output" ); + assert!( + debug.contains("^/secure"), + "should leave non-secret handler path visible in debug output" + ); let canaries = [ ("publisher.proxy_secret", CANARY_PROXY_SECRET), @@ -3928,6 +3952,10 @@ mod tests { CANARY_TINYBIRD_AUCTION_TOKEN, ), ("tinybird.access_token_secret", CANARY_TINYBIRD_ACCESS_TOKEN), + ( + "integrations.datadome.server_side_key_secret_name", + CANARY_DATADOME_SERVER_SIDE_KEY, + ), ]; for (field, canary) in canaries {