Fix weak and inconsistent secret default validation#467
Fix weak and inconsistent secret default validation#467ChristianPavilonis wants to merge 5 commits intomainfrom
Conversation
Reject all known placeholder values for synthetic.secret_key and publisher.proxy_secret at runtime startup so deployments using default secrets fail fast instead of running with predictable cryptographic keys.
aram356
left a comment
There was a problem hiding this comment.
Review Summary
| # | Severity | File | Finding |
|---|---|---|---|
| 1 | HIGH | settings.rs | proxy_secret has no validation — empty string passes and produces a deterministic crypto key |
| 2 | MEDIUM | settings_data.rs | Test is environment-dependent and can flap when CI overrides secrets |
| 3 | MEDIUM | settings.rs | Placeholder policy is scattered across two types and will drift |
| 4 | LOW | settings_data.rs | Only the first insecure field is reported; operator must fix-and-redeploy to find the second |
| 5 | NIT | settings.rs | Spurious #[allow(dead_code)] on public items that are actually used |
…r checks - Add #[validate(length(min = 1))] to proxy_secret to prevent empty strings from producing a deterministic crypto key - Centralize placeholder validation into Settings::reject_placeholder_secrets() which collects all violations into a single error - Rewrite tests to use explicit TOML input instead of build-time embedded config, making them deterministic across CI environments - Remove spurious #[allow(dead_code)] from public items that are used
Review Feedback AddressedAll 5 items from @aram356's review have been addressed in cbef697:
All 480 tests pass, |
aram356
left a comment
There was a problem hiding this comment.
Summary
Centralizes placeholder-secret rejection into Settings::reject_placeholder_secrets(), adds proxy_secret validation, and makes tests deterministic. Addresses all findings from the previous review round.
Blocking
🔧 wrench
- Removed
test_get_settingssmoke test not replaced: The originaltest_get_settingswas the only test exercising the fullget_settings()end-to-end path (embedded bytes → UTF-8 → parse → validate → placeholder check). The new tests only exerciseSettings::from_toml()+reject_placeholder_secrets()directly. A regression in the glue code would go undetected. (settings_data.rs)
❓ question
toml_with_secretsreplacement is fragile and can silently no-op:str::replace()returns the original string unchanged if the pattern doesn't match. Ifcrate_test_settings_str()changes quoting or default values, replacements silently do nothing. Theaccepts_non_placeholder_secretstest is especially vulnerable — both old and new values are non-placeholders, so it passes even if replacement fails. (settings_data.rs:56-66)
Non-blocking
🤔 thinking
- Case-sensitive placeholder matching:
"Secret-Key"or"SECRET-KEY"bypasses the check while still being predictable. Considereq_ignore_ascii_case. (settings.rs:205-207)
🌱 seedling
- Minimum length for crypto secrets:
min = 1prevents empty but"x"still passes validation then feeds intoSha256::digestfor XChaCha20-Poly1305 key derivation. Considermin = 16as a follow-up. (settings.rs:27, 194)
⛏ nitpick
- Double space in doc comment:
"...offending field. This centralises..."— rest of codebase uses single space. (settings.rs:388)
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- CodeQL: FAIL (appears infrastructure-related, not caused by this PR)
| ) | ||
| } | ||
|
|
||
| #[test] |
There was a problem hiding this comment.
🔧 wrench — The original test_get_settings was the only test exercising the full get_settings() end-to-end (embedded bytes → UTF-8 → parse → validate → placeholder check). The new tests only call Settings::from_toml() + reject_placeholder_secrets() directly.
Consider adding back a smoke test:
#[test]
fn get_settings_loads_embedded_config() {
let settings = super::get_settings();
assert!(settings.is_ok(), "should load settings from embedded TOML");
}| use crate::test_support::tests::crate_test_settings_str; | ||
|
|
||
| /// Builds a TOML string with the given secret values swapped in. | ||
| fn toml_with_secrets(secret_key: &str, proxy_secret: &str) -> String { |
There was a problem hiding this comment.
❓ question — str::replace() returns the original string unchanged if the pattern doesn't match. If crate_test_settings_str() ever changes quoting or default values, these replacements silently no-op.
The accepts_non_placeholder_secrets test is especially vulnerable: it replaces "test-secret-key" with "production-secret-key" — both non-placeholders — so even if replacement fails, the test passes.
Consider asserting the replacement actually took effect:
fn toml_with_secrets(secret_key: &str, proxy_secret: &str) -> String {
let original = crate_test_settings_str();
let result = original
.replace(/* ... */)
.replace(/* ... */);
assert_ne!(result, original, "should have replaced at least one secret value");
result
}| #[must_use] | ||
| pub fn is_placeholder_secret_key(secret_key: &str) -> bool { | ||
| Self::SECRET_KEY_PLACEHOLDERS.contains(&secret_key) | ||
| } |
There was a problem hiding this comment.
🤔 thinking — Placeholder matching is case-sensitive. Someone setting secret_key = "Secret-Key" bypasses the check while still using a predictable value. Consider case-insensitive matching:
pub fn is_placeholder_secret_key(secret_key: &str) -> bool {
Self::SECRET_KEY_PLACEHOLDERS
.iter()
.any(|p| p.eq_ignore_ascii_case(secret_key))
}| } | ||
|
|
||
| /// Checks all secret fields for known placeholder values and returns an | ||
| /// error listing every offending field. This centralises the placeholder |
There was a problem hiding this comment.
⛏ nitpick — Double space after period: "field. This" → "field. This"
Summary
synthetic.secret_key("secret-key","secret_key","trusted-server") andpublisher.proxy_secret("change-me-proxy-secret") at runtime startup so misconfigured deployments fail fast.is_placeholder_secret_key,is_placeholder_proxy_secret) with explicit placeholder lists, replacing the inconsistent checks that missed the actual TOML defaults.InsecureSecretKeytoInsecureDefault { field }so the error message identifies which secret triggered rejection.Changes
crates/common/src/error.rsInsecureSecretKey→InsecureDefault { field: String }with a descriptive display messagecrates/common/src/settings.rsPROXY_SECRET_PLACEHOLDERSconst +is_placeholder_proxy_secret()onPublisher; replacedvalidate_secret_key()withSECRET_KEY_PLACEHOLDERSconst +is_placeholder_secret_key()onSynthetic; added 4 unit testscrates/common/src/settings_data.rs== "secret-key"check with calls to both predicates; returns field-specificInsecureDefaulterrors; updated test to assert placeholder rejectioncrates/common/build.rsCloses
Closes #406
Test plan
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkChecklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)